From 6988079bb2e720b4c7d7e5661b05e02576f14289 Mon Sep 17 00:00:00 2001 From: Alex Hung Date: Mon, 30 Sep 2024 13:17:34 -0700 Subject: [PATCH 01/12] Refactor scheme func for custom webhook --- ...resource_artifactory_artifact_lifecycle.go | 2 +- .../resource_artifactory_custom_webhook.go | 352 +++++++++++------- .../webhook/resource_artifactory_webhook.go | 96 ++--- .../resource_artifactory_webhook_build.go | 2 +- ...urce_artifactory_webhook_release_bundle.go | 2 +- ...e_artifactory_webhook_release_bundle_v2.go | 2 +- ...ory_webhook_release_bundle_v2_promotion.go | 2 +- .../resource_artifactory_webhook_repo.go | 2 +- .../resource_artifactory_webhook_user.go | 2 +- 9 files changed, 270 insertions(+), 192 deletions(-) diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_artifact_lifecycle.go b/pkg/artifactory/resource/webhook/resource_artifactory_artifact_lifecycle.go index 12340037..ed9a0225 100644 --- a/pkg/artifactory/resource/webhook/resource_artifactory_artifact_lifecycle.go +++ b/pkg/artifactory/resource/webhook/resource_artifactory_artifact_lifecycle.go @@ -35,7 +35,7 @@ func (r *ArtifactLifecycleWebhookResource) Metadata(ctx context.Context, req res } func (r *ArtifactLifecycleWebhookResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { - resp.Schema = r.schema(r.Domain, nil) + resp.Schema = r.CreateSchema(r.Domain, nil, handlerBlock) } func (r *ArtifactLifecycleWebhookResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook.go b/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook.go index bd995e86..7f3116be 100644 --- a/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook.go +++ b/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook.go @@ -8,18 +8,96 @@ import ( "strings" "github.com/go-resty/resty/v2" + "github.com/hashicorp/terraform-plugin-framework-validators/mapvalidator" + "github.com/hashicorp/terraform-plugin-framework-validators/setvalidator" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + sdkv2_schema "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" "github.com/jfrog/terraform-provider-artifactory/v12/pkg/artifactory" "github.com/jfrog/terraform-provider-shared/util" utilsdk "github.com/jfrog/terraform-provider-shared/util/sdk" - "github.com/jfrog/terraform-provider-shared/validator" + util_validator "github.com/jfrog/terraform-provider-shared/validator" + validatorfw_string "github.com/jfrog/terraform-provider-shared/validator/fw/string" "golang.org/x/exp/slices" ) +type CustomWebhookResource struct { + WebhookResource +} + +var customHandlerBlock = schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "url": schema.StringAttribute{ + Required: true, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + validatorfw_string.IsURLHttpOrHttps(), + }, + Description: "Specifies the URL that the Webhook invokes. This will be the URL that Artifactory will send an HTTP POST request to.", + }, + "secrets": schema.MapAttribute{ + ElementType: types.StringType, + Optional: true, + Validators: []validator.Map{ + mapvalidator.ValueStringsAre( + stringvalidator.RegexMatches(regexp.MustCompile("^[a-zA-Z_][a-zA-Z0-9_]*$"), "Secret name must match '^[a-zA-Z_][a-zA-Z0-9_]*$'\""), + ), + }, + Description: "A set of sensitive values that will be injected in the request (headers and/or payload), comprise of key/value pair.", + }, + "proxy": schema.StringAttribute{ + Optional: true, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + validatorfw_string.RegexNotMatches(regexp.MustCompile(`^http.+`), "expected \"proxy\" not to be a valid url"), + }, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + Description: "Proxy key from Artifactory Proxies setting", + }, + "http_headers": schema.MapAttribute{ + ElementType: types.StringType, + Optional: true, + MarkdownDescription: "HTTP headers you wish to use to invoke the Webhook, comprise of key/value pair. Used in custom webhooks.", + }, + "payload": schema.StringAttribute{ + Optional: true, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + MarkdownDescription: "This attribute is used to build the request body. Used in custom webhooks", + }, + }, + }, + Validators: []validator.Set{ + setvalidator.IsRequired(), + setvalidator.SizeAtLeast(1), + }, +} + +func (r *CustomWebhookResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + r.WebhookResource.Metadata(ctx, req, resp) +} + +func (r *CustomWebhookResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + r.WebhookResource.Configure(ctx, req, resp) +} + +func (r *CustomWebhookResource) CreateSchema(domain string, criteriaBlock *schema.SetNestedBlock) schema.Schema { + return r.WebhookResource.CreateSchema(domain, criteriaBlock, customHandlerBlock) +} + var DomainSupported = []string{ ArtifactLifecycleDomain, ArtifactPropertyDomain, @@ -35,10 +113,10 @@ var DomainSupported = []string{ UserDomain, } -func baseCustomWebhookBaseSchema(webhookType string) map[string]*schema.Schema { - return map[string]*schema.Schema{ +func baseCustomWebhookBaseSchema(webhookType string) map[string]*sdkv2_schema.Schema { + return map[string]*sdkv2_schema.Schema{ "key": { - Type: schema.TypeString, + Type: sdkv2_schema.TypeString, Required: true, ValidateDiagFunc: validation.ToDiagFunc( validation.All( @@ -49,33 +127,33 @@ func baseCustomWebhookBaseSchema(webhookType string) map[string]*schema.Schema { Description: "Key of webhook. Must be between 2 and 200 characters. Cannot contain spaces.", }, "description": { - Type: schema.TypeString, + Type: sdkv2_schema.TypeString, Optional: true, ValidateDiagFunc: validation.ToDiagFunc(validation.StringLenBetween(0, 1000)), Description: "Description of webhook. Max length 1000 characters.", }, "enabled": { - Type: schema.TypeBool, + Type: sdkv2_schema.TypeBool, Optional: true, Default: true, Description: "Status of webhook. Default to 'true'", }, "event_types": { - Type: schema.TypeSet, + Type: sdkv2_schema.TypeSet, Required: true, MinItems: 1, - Elem: &schema.Schema{Type: schema.TypeString}, + Elem: &sdkv2_schema.Schema{Type: sdkv2_schema.TypeString}, Description: fmt.Sprintf("List of Events in Artifactory, Distribution, Release Bundle that function as the event trigger for the Webhook.\n"+ "Allow values: %v", strings.Trim(strings.Join(DomainEventTypesSupported[webhookType], ", "), "[]")), }, "handler": { - Type: schema.TypeSet, + Type: sdkv2_schema.TypeSet, Required: true, MinItems: 1, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ + Elem: &sdkv2_schema.Resource{ + Schema: map[string]*sdkv2_schema.Schema{ "url": { - Type: schema.TypeString, + Type: sdkv2_schema.TypeString, Required: true, ValidateDiagFunc: validation.ToDiagFunc( validation.All( @@ -86,31 +164,31 @@ func baseCustomWebhookBaseSchema(webhookType string) map[string]*schema.Schema { Description: "Specifies the URL that the Webhook invokes. This will be the URL that Artifactory will send an HTTP POST request to.", }, "secrets": { - Type: schema.TypeMap, + Type: sdkv2_schema.TypeMap, Optional: true, - Elem: &schema.Schema{ - Type: schema.TypeString, + Elem: &sdkv2_schema.Schema{ + Type: sdkv2_schema.TypeString, ValidateDiagFunc: validation.ToDiagFunc(validation.StringMatch(regexp.MustCompile("^[a-zA-Z_][a-zA-Z0-9_]*$"), "Secret name must match '^[a-zA-Z_][a-zA-Z0-9_]*$'\"")), }, Description: "A set of sensitive values that will be injected in the request (headers and/or payload), comprise of key/value pair.", }, "proxy": { - Type: schema.TypeString, + Type: sdkv2_schema.TypeString, Optional: true, - ValidateDiagFunc: validator.All( - validator.StringIsNotEmpty, - validator.StringIsNotURL, + ValidateDiagFunc: util_validator.All( + util_validator.StringIsNotEmpty, + util_validator.StringIsNotURL, ), Description: "Proxy key from Artifactory UI (Administration -> Proxies -> Configuration)", }, "http_headers": { - Type: schema.TypeMap, + Type: sdkv2_schema.TypeMap, Optional: true, - Elem: &schema.Schema{Type: schema.TypeString}, + Elem: &sdkv2_schema.Schema{Type: sdkv2_schema.TypeString}, Description: "HTTP headers you wish to use to invoke the Webhook, comprise of key/value pair. Used in custom webhooks.", }, "payload": { - Type: schema.TypeString, + Type: sdkv2_schema.TypeString, Optional: true, ValidateDiagFunc: validation.ToDiagFunc(validation.StringIsNotEmpty), Description: "This attribute is used to build the request body. Used in custom webhooks", @@ -121,33 +199,33 @@ func baseCustomWebhookBaseSchema(webhookType string) map[string]*schema.Schema { } } -var repoWebhookSchema = func(webhookType string, version int, isCustom bool) map[string]*schema.Schema { - return utilsdk.MergeMaps(getBaseSchemaByVersion(webhookType, version, isCustom), map[string]*schema.Schema{ +var repoWebhookSchema = func(webhookType string, version int, isCustom bool) map[string]*sdkv2_schema.Schema { + return utilsdk.MergeMaps(getBaseSchemaByVersion(webhookType, version, isCustom), map[string]*sdkv2_schema.Schema{ "criteria": { - Type: schema.TypeSet, + Type: sdkv2_schema.TypeSet, Required: true, MaxItems: 1, - Elem: &schema.Resource{ - Schema: utilsdk.MergeMaps(baseCriteriaSchema, map[string]*schema.Schema{ + Elem: &sdkv2_schema.Resource{ + Schema: utilsdk.MergeMaps(baseCriteriaSchema, map[string]*sdkv2_schema.Schema{ "any_local": { - Type: schema.TypeBool, + Type: sdkv2_schema.TypeBool, Required: true, Description: "Trigger on any local repositories", }, "any_remote": { - Type: schema.TypeBool, + Type: sdkv2_schema.TypeBool, Required: true, Description: "Trigger on any remote repositories", }, "any_federated": { - Type: schema.TypeBool, + Type: sdkv2_schema.TypeBool, Required: true, Description: "Trigger on any federated repositories", }, "repo_keys": { - Type: schema.TypeSet, + Type: sdkv2_schema.TypeSet, Required: true, - Elem: &schema.Schema{Type: schema.TypeString}, + Elem: &sdkv2_schema.Schema{Type: sdkv2_schema.TypeString}, Description: "Trigger on this list of repository keys", }, }), @@ -157,23 +235,23 @@ var repoWebhookSchema = func(webhookType string, version int, isCustom bool) map }) } -var buildWebhookSchema = func(webhookType string, version int, isCustom bool) map[string]*schema.Schema { - return utilsdk.MergeMaps(getBaseSchemaByVersion(webhookType, version, isCustom), map[string]*schema.Schema{ +var buildWebhookSchema = func(webhookType string, version int, isCustom bool) map[string]*sdkv2_schema.Schema { + return utilsdk.MergeMaps(getBaseSchemaByVersion(webhookType, version, isCustom), map[string]*sdkv2_schema.Schema{ "criteria": { - Type: schema.TypeSet, + Type: sdkv2_schema.TypeSet, Required: true, MaxItems: 1, - Elem: &schema.Resource{ - Schema: utilsdk.MergeMaps(baseCriteriaSchema, map[string]*schema.Schema{ + Elem: &sdkv2_schema.Resource{ + Schema: utilsdk.MergeMaps(baseCriteriaSchema, map[string]*sdkv2_schema.Schema{ "any_build": { - Type: schema.TypeBool, + Type: sdkv2_schema.TypeBool, Required: true, Description: "Trigger on any builds", }, "selected_builds": { - Type: schema.TypeSet, + Type: sdkv2_schema.TypeSet, Required: true, - Elem: &schema.Schema{Type: schema.TypeString}, + Elem: &sdkv2_schema.Schema{Type: sdkv2_schema.TypeString}, Description: "Trigger on this list of build IDs", }, }), @@ -183,23 +261,23 @@ var buildWebhookSchema = func(webhookType string, version int, isCustom bool) ma }) } -var releaseBundleWebhookSchema = func(webhookType string, version int, isCustom bool) map[string]*schema.Schema { - return utilsdk.MergeMaps(getBaseSchemaByVersion(webhookType, version, isCustom), map[string]*schema.Schema{ +var releaseBundleWebhookSchema = func(webhookType string, version int, isCustom bool) map[string]*sdkv2_schema.Schema { + return utilsdk.MergeMaps(getBaseSchemaByVersion(webhookType, version, isCustom), map[string]*sdkv2_schema.Schema{ "criteria": { - Type: schema.TypeSet, + Type: sdkv2_schema.TypeSet, Required: true, MaxItems: 1, - Elem: &schema.Resource{ - Schema: utilsdk.MergeMaps(baseCriteriaSchema, map[string]*schema.Schema{ + Elem: &sdkv2_schema.Resource{ + Schema: utilsdk.MergeMaps(baseCriteriaSchema, map[string]*sdkv2_schema.Schema{ "any_release_bundle": { - Type: schema.TypeBool, + Type: sdkv2_schema.TypeBool, Required: true, Description: "Trigger on any release bundles or distributions", }, "registered_release_bundle_names": { - Type: schema.TypeSet, + Type: sdkv2_schema.TypeSet, Required: true, - Elem: &schema.Schema{Type: schema.TypeString}, + Elem: &sdkv2_schema.Schema{Type: sdkv2_schema.TypeString}, Description: "Trigger on this list of release bundle names", }, }), @@ -209,23 +287,23 @@ var releaseBundleWebhookSchema = func(webhookType string, version int, isCustom }) } -var releaseBundleV2WebhookSchema = func(webhookType string, version int, isCustom bool) map[string]*schema.Schema { - return utilsdk.MergeMaps(getBaseSchemaByVersion(webhookType, version, isCustom), map[string]*schema.Schema{ +var releaseBundleV2WebhookSchema = func(webhookType string, version int, isCustom bool) map[string]*sdkv2_schema.Schema { + return utilsdk.MergeMaps(getBaseSchemaByVersion(webhookType, version, isCustom), map[string]*sdkv2_schema.Schema{ "criteria": { - Type: schema.TypeSet, + Type: sdkv2_schema.TypeSet, Required: true, MaxItems: 1, - Elem: &schema.Resource{ - Schema: utilsdk.MergeMaps(baseCriteriaSchema, map[string]*schema.Schema{ + Elem: &sdkv2_schema.Resource{ + Schema: utilsdk.MergeMaps(baseCriteriaSchema, map[string]*sdkv2_schema.Schema{ "any_release_bundle": { - Type: schema.TypeBool, + Type: sdkv2_schema.TypeBool, Required: true, Description: "Trigger on any release bundles or distributions", }, "selected_release_bundles": { - Type: schema.TypeSet, + Type: sdkv2_schema.TypeSet, Required: true, - Elem: &schema.Schema{Type: schema.TypeString}, + Elem: &sdkv2_schema.Schema{Type: sdkv2_schema.TypeString}, Description: "Trigger on this list of release bundle names", }, }), @@ -235,18 +313,18 @@ var releaseBundleV2WebhookSchema = func(webhookType string, version int, isCusto }) } -var releaseBundleV2PromotionWebhookSchema = func(webhookType string, version int, isCustom bool) map[string]*schema.Schema { - return utilsdk.MergeMaps(getBaseSchemaByVersion(webhookType, version, isCustom), map[string]*schema.Schema{ +var releaseBundleV2PromotionWebhookSchema = func(webhookType string, version int, isCustom bool) map[string]*sdkv2_schema.Schema { + return utilsdk.MergeMaps(getBaseSchemaByVersion(webhookType, version, isCustom), map[string]*sdkv2_schema.Schema{ "criteria": { - Type: schema.TypeSet, + Type: sdkv2_schema.TypeSet, Required: true, MaxItems: 1, - Elem: &schema.Resource{ - Schema: utilsdk.MergeMaps(baseCriteriaSchema, map[string]*schema.Schema{ + Elem: &sdkv2_schema.Resource{ + Schema: utilsdk.MergeMaps(baseCriteriaSchema, map[string]*sdkv2_schema.Schema{ "selected_environments": { - Type: schema.TypeSet, + Type: sdkv2_schema.TypeSet, Required: true, - Elem: &schema.Schema{Type: schema.TypeString}, + Elem: &sdkv2_schema.Schema{Type: sdkv2_schema.TypeString}, Description: "Trigger on this list of environments", }, }), @@ -256,27 +334,24 @@ var releaseBundleV2PromotionWebhookSchema = func(webhookType string, version int }) } -var userWebhookSchema = func(webhookType string, version int, isCustom bool) map[string]*schema.Schema { +var userWebhookSchema = func(webhookType string, version int, isCustom bool) map[string]*sdkv2_schema.Schema { return getBaseSchemaByVersion(webhookType, version, isCustom) } -var artifactLifecycleWebhookSchema = func(webhookType string, version int, isCustom bool) map[string]*schema.Schema { +var artifactLifecycleWebhookSchema = func(webhookType string, version int, isCustom bool) map[string]*sdkv2_schema.Schema { return getBaseSchemaByVersion(webhookType, version, isCustom) } -type CustomBaseParams struct { - Key string `json:"key"` - Description string `json:"description"` - Enabled bool `json:"enabled"` - EventFilterAPIModel EventFilterAPIModel `json:"event_filter"` - Handlers []CustomHandler `json:"handlers"` +type CustomWebookAPIModel struct { + WebhookAPIModel + Handlers []CustomHandlerAPIModel `json:"handlers"` } -func (w CustomBaseParams) Id() string { +func (w CustomWebookAPIModel) Id() string { return w.Key } -type CustomHandler struct { +type CustomHandlerAPIModel struct { HandlerType string `json:"handler_type"` Url string `json:"url"` Secrets []KeyValuePairAPIModel `json:"secrets"` @@ -289,12 +364,12 @@ type SecretName struct { Name string `json:"name"` } -var packSecretsCustom = func(keyValuePairs []KeyValuePairAPIModel, d *schema.ResourceData, url string) map[string]interface{} { +var packSecretsCustom = func(keyValuePairs []KeyValuePairAPIModel, d *sdkv2_schema.ResourceData, url string) map[string]interface{} { KVPairs := make(map[string]interface{}) // Get secrets from TF state var secrets map[string]interface{} if v, ok := d.GetOk("handler"); ok { - handlers := v.(*schema.Set).List() + handlers := v.(*sdkv2_schema.Set).List() for _, handler := range handlers { h := handler.(map[string]interface{}) // if url match, merge secret maps @@ -313,22 +388,22 @@ var packSecretsCustom = func(keyValuePairs []KeyValuePairAPIModel, d *schema.Res return KVPairs } -func ResourceArtifactoryCustomWebhook(webhookType string) *schema.Resource { +func ResourceArtifactoryCustomWebhook(webhookType string) *sdkv2_schema.Resource { - var unpackWebhook = func(data *schema.ResourceData) (CustomBaseParams, error) { + var unpackWebhook = func(data *sdkv2_schema.ResourceData) (CustomWebookAPIModel, error) { d := &utilsdk.ResourceData{ResourceData: data} - var unpackHandlers = func(d *utilsdk.ResourceData) []CustomHandler { - var webhookHandlers []CustomHandler + var unpackHandlers = func(d *utilsdk.ResourceData) []CustomHandlerAPIModel { + var webhookHandlers []CustomHandlerAPIModel if v, ok := d.GetOk("handler"); ok { - handlers := v.(*schema.Set).List() + handlers := v.(*sdkv2_schema.Set).List() for _, handler := range handlers { h := handler.(map[string]interface{}) // use this to filter out weirdness with terraform adding an extra blank webhook in a set // https://discuss.hashicorp.com/t/using-typeset-in-provider-always-adds-an-empty-element-on-update/18566/2 if h["url"].(string) != "" { - webhookHandler := CustomHandler{ + webhookHandler := CustomHandlerAPIModel{ HandlerType: "custom-webhook", Url: h["url"].(string), } @@ -357,14 +432,16 @@ func ResourceArtifactoryCustomWebhook(webhookType string) *schema.Resource { return webhookHandlers } - webhook := CustomBaseParams{ - Key: d.GetString("key", false), - Description: d.GetString("description", false), - Enabled: d.GetBool("enabled", false), - EventFilterAPIModel: EventFilterAPIModel{ - Domain: webhookType, - EventTypes: d.GetSet("event_types"), - Criteria: unpackCriteria(d, webhookType), + webhook := CustomWebookAPIModel{ + WebhookAPIModel: WebhookAPIModel{ + Key: d.GetString("key", false), + Description: d.GetString("description", false), + Enabled: d.GetBool("enabled", false), + EventFilter: EventFilterAPIModel{ + Domain: webhookType, + EventTypes: d.GetSet("event_types"), + Criteria: unpackCriteria(d, webhookType), + }, }, Handlers: unpackHandlers(d), } @@ -372,9 +449,9 @@ func ResourceArtifactoryCustomWebhook(webhookType string) *schema.Resource { return webhook, nil } - var packHandlers = func(d *schema.ResourceData, handlers []CustomHandler) []error { + var packHandlers = func(d *sdkv2_schema.ResourceData, handlers []CustomHandlerAPIModel) []error { setValue := utilsdk.MkLens(d) - resource := domainSchemaLookup(currentSchemaVersion, true, webhookType)[webhookType]["handler"].Elem.(*schema.Resource) + resource := domainSchemaLookup(currentSchemaVersion, true, webhookType)[webhookType]["handler"].Elem.(*sdkv2_schema.Resource) packedHandlers := make([]interface{}, len(handlers)) for _, handler := range handlers { packedHandler := map[string]interface{}{ @@ -394,18 +471,18 @@ func ResourceArtifactoryCustomWebhook(webhookType string) *schema.Resource { packedHandlers = append(packedHandlers, packedHandler) } - return setValue("handler", schema.NewSet(schema.HashResource(resource), packedHandlers)) + return setValue("handler", sdkv2_schema.NewSet(sdkv2_schema.HashResource(resource), packedHandlers)) } - var packWebhook = func(d *schema.ResourceData, webhook CustomBaseParams) diag.Diagnostics { + var packWebhook = func(d *sdkv2_schema.ResourceData, webhook CustomWebookAPIModel) diag.Diagnostics { setValue := utilsdk.MkLens(d) setValue("key", webhook.Key) setValue("description", webhook.Description) setValue("enabled", webhook.Enabled) - errors := setValue("event_types", webhook.EventFilterAPIModel.EventTypes) - if webhook.EventFilterAPIModel.Criteria != nil { - errors = append(errors, packCriteria(d, webhookType, webhook.EventFilterAPIModel.Criteria.(map[string]interface{}))...) + errors := setValue("event_types", webhook.EventFilter.EventTypes) + if webhook.EventFilter.Criteria != nil { + errors = append(errors, packCriteria(d, webhookType, webhook.EventFilter.Criteria.(map[string]interface{}))...) } errors = append(errors, packHandlers(d, webhook.Handlers)...) @@ -416,10 +493,10 @@ func ResourceArtifactoryCustomWebhook(webhookType string) *schema.Resource { return nil } - var readWebhook = func(ctx context.Context, data *schema.ResourceData, m interface{}) diag.Diagnostics { - webhook := CustomBaseParams{} + var readWebhook = func(ctx context.Context, data *sdkv2_schema.ResourceData, m interface{}) diag.Diagnostics { + webhook := CustomWebookAPIModel{} - webhook.EventFilterAPIModel.Criteria = domainCriteriaLookup[webhookType] + webhook.EventFilter.Criteria = domainCriteriaLookup[webhookType] var artifactoryError artifactory.ArtifactoryErrorsResponse resp, err := m.(util.ProviderMetadata).Client.R(). @@ -450,7 +527,7 @@ func ResourceArtifactoryCustomWebhook(webhookType string) *schema.Resource { return proxyNotFoundRegex.MatchString(string(response.Body()[:])) } - var createWebhook = func(ctx context.Context, data *schema.ResourceData, m interface{}) diag.Diagnostics { + var createWebhook = func(ctx context.Context, data *sdkv2_schema.ResourceData, m interface{}) diag.Diagnostics { webhook, err := unpackWebhook(data) if err != nil { return diag.FromErr(err) @@ -475,7 +552,7 @@ func ResourceArtifactoryCustomWebhook(webhookType string) *schema.Resource { return readWebhook(ctx, data, m) } - var updateWebhook = func(ctx context.Context, data *schema.ResourceData, m interface{}) diag.Diagnostics { + var updateWebhook = func(ctx context.Context, data *sdkv2_schema.ResourceData, m interface{}) diag.Diagnostics { webhook, err := unpackWebhook(data) if err != nil { return diag.FromErr(err) @@ -501,7 +578,7 @@ func ResourceArtifactoryCustomWebhook(webhookType string) *schema.Resource { return readWebhook(ctx, data, m) } - var deleteWebhook = func(ctx context.Context, data *schema.ResourceData, m interface{}) diag.Diagnostics { + var deleteWebhook = func(ctx context.Context, data *sdkv2_schema.ResourceData, m interface{}) diag.Diagnostics { var artifactoryError artifactory.ArtifactoryErrorsResponse resp, err := m.(util.ProviderMetadata).Client.R(). SetPathParam("webhookKey", data.Id()). @@ -524,8 +601,8 @@ func ResourceArtifactoryCustomWebhook(webhookType string) *schema.Resource { return nil } - var eventTypesDiff = func(ctx context.Context, diff *schema.ResourceDiff, v interface{}) error { - eventTypes := diff.Get("event_types").(*schema.Set).List() + var eventTypesDiff = func(ctx context.Context, diff *sdkv2_schema.ResourceDiff, v interface{}) error { + eventTypes := diff.Get("event_types").(*sdkv2_schema.Set).List() if len(eventTypes) == 0 { return nil } @@ -539,9 +616,9 @@ func ResourceArtifactoryCustomWebhook(webhookType string) *schema.Resource { return nil } - var criteriaDiff = func(ctx context.Context, diff *schema.ResourceDiff, v interface{}) error { + var criteriaDiff = func(ctx context.Context, diff *sdkv2_schema.ResourceDiff, v interface{}) error { if resource, ok := diff.GetOk("criteria"); ok { - criteria := resource.(*schema.Set).List() + criteria := resource.(*sdkv2_schema.Set).List() if len(criteria) == 0 { return nil } @@ -551,15 +628,15 @@ func ResourceArtifactoryCustomWebhook(webhookType string) *schema.Resource { return nil } - rs := schema.Resource{ + rs := sdkv2_schema.Resource{ SchemaVersion: 2, CreateContext: createWebhook, ReadContext: readWebhook, UpdateContext: updateWebhook, DeleteContext: deleteWebhook, - Importer: &schema.ResourceImporter{ - StateContext: schema.ImportStatePassthroughContext, + Importer: &sdkv2_schema.ResourceImporter{ + StateContext: sdkv2_schema.ImportStatePassthroughContext, }, Schema: domainSchemaLookup(currentSchemaVersion, true, webhookType)[webhookType], @@ -647,8 +724,8 @@ var domainUnpackLookup = map[string]func(map[string]interface{}, BaseCriteriaAPI "artifact_lifecycle": unpackEmptyCriteria, } -var domainSchemaLookup = func(version int, isCustom bool, webhookType string) map[string]map[string]*schema.Schema { - return map[string]map[string]*schema.Schema{ +var domainSchemaLookup = func(version int, isCustom bool, webhookType string) map[string]map[string]*sdkv2_schema.Schema { + return map[string]map[string]*sdkv2_schema.Schema{ "artifact": repoWebhookSchema(webhookType, version, isCustom), "artifact_property": repoWebhookSchema(webhookType, version, isCustom), "docker": repoWebhookSchema(webhookType, version, isCustom), @@ -669,7 +746,7 @@ var packRepoCriteria = func(artifactoryCriteria map[string]interface{}) map[stri "any_local": artifactoryCriteria["anyLocal"].(bool), "any_remote": artifactoryCriteria["anyRemote"].(bool), "any_federated": false, - "repo_keys": schema.NewSet(schema.HashString, artifactoryCriteria["repoKeys"].([]interface{})), + "repo_keys": sdkv2_schema.NewSet(sdkv2_schema.HashString, artifactoryCriteria["repoKeys"].([]interface{})), } if v, ok := artifactoryCriteria["anyFederated"]; ok { @@ -682,14 +759,14 @@ var packRepoCriteria = func(artifactoryCriteria map[string]interface{}) map[stri var packReleaseBundleCriteria = func(artifactoryCriteria map[string]interface{}) map[string]interface{} { return map[string]interface{}{ "any_release_bundle": artifactoryCriteria["anyReleaseBundle"].(bool), - "registered_release_bundle_names": schema.NewSet(schema.HashString, artifactoryCriteria["registeredReleaseBundlesNames"].([]interface{})), + "registered_release_bundle_names": sdkv2_schema.NewSet(sdkv2_schema.HashString, artifactoryCriteria["registeredReleaseBundlesNames"].([]interface{})), } } var unpackReleaseBundleCriteria = func(terraformCriteria map[string]interface{}, baseCriteria BaseCriteriaAPIModel) interface{} { return ReleaseBundleCriteriaAPIModel{ AnyReleaseBundle: terraformCriteria["any_release_bundle"].(bool), - RegisteredReleaseBundlesNames: utilsdk.CastToStringArr(terraformCriteria["registered_release_bundle_names"].(*schema.Set).List()), + RegisteredReleaseBundlesNames: utilsdk.CastToStringArr(terraformCriteria["registered_release_bundle_names"].(*sdkv2_schema.Set).List()), BaseCriteriaAPIModel: baseCriteria, } } @@ -699,7 +776,7 @@ var unpackRepoCriteria = func(terraformCriteria map[string]interface{}, baseCrit AnyLocal: terraformCriteria["any_local"].(bool), AnyRemote: terraformCriteria["any_remote"].(bool), AnyFederated: terraformCriteria["any_federated"].(bool), - RepoKeys: utilsdk.CastToStringArr(terraformCriteria["repo_keys"].(*schema.Set).List()), + RepoKeys: utilsdk.CastToStringArr(terraformCriteria["repo_keys"].(*sdkv2_schema.Set).List()), BaseCriteriaAPIModel: baseCriteria, } } @@ -707,14 +784,14 @@ var unpackRepoCriteria = func(terraformCriteria map[string]interface{}, baseCrit var packBuildCriteria = func(artifactoryCriteria map[string]interface{}) map[string]interface{} { return map[string]interface{}{ "any_build": artifactoryCriteria["anyBuild"].(bool), - "selected_builds": schema.NewSet(schema.HashString, artifactoryCriteria["selectedBuilds"].([]interface{})), + "selected_builds": sdkv2_schema.NewSet(sdkv2_schema.HashString, artifactoryCriteria["selectedBuilds"].([]interface{})), } } var unpackBuildCriteria = func(terraformCriteria map[string]interface{}, baseCriteria BaseCriteriaAPIModel) interface{} { return BuildCriteriaAPIModel{ AnyBuild: terraformCriteria["any_build"].(bool), - SelectedBuilds: utilsdk.CastToStringArr(terraformCriteria["selected_builds"].(*schema.Set).List()), + SelectedBuilds: utilsdk.CastToStringArr(terraformCriteria["selected_builds"].(*sdkv2_schema.Set).List()), BaseCriteriaAPIModel: baseCriteria, } } @@ -722,27 +799,27 @@ var unpackBuildCriteria = func(terraformCriteria map[string]interface{}, baseCri var packReleaseBundleV2Criteria = func(artifactoryCriteria map[string]interface{}) map[string]interface{} { return map[string]interface{}{ "any_release_bundle": artifactoryCriteria["anyReleaseBundle"].(bool), - "selected_release_bundles": schema.NewSet(schema.HashString, artifactoryCriteria["selectedReleaseBundles"].([]interface{})), + "selected_release_bundles": sdkv2_schema.NewSet(sdkv2_schema.HashString, artifactoryCriteria["selectedReleaseBundles"].([]interface{})), } } var unpackReleaseBundleV2Criteria = func(terraformCriteria map[string]interface{}, baseCriteria BaseCriteriaAPIModel) interface{} { return ReleaseBundleV2CriteriaAPIModel{ AnyReleaseBundle: terraformCriteria["any_release_bundle"].(bool), - SelectedReleaseBundles: utilsdk.CastToStringArr(terraformCriteria["selected_release_bundles"].(*schema.Set).List()), + SelectedReleaseBundles: utilsdk.CastToStringArr(terraformCriteria["selected_release_bundles"].(*sdkv2_schema.Set).List()), BaseCriteriaAPIModel: baseCriteria, } } var packReleaseBundleV2PromotionCriteria = func(artifactoryCriteria map[string]interface{}) map[string]interface{} { return map[string]interface{}{ - "selected_environments": schema.NewSet(schema.HashString, artifactoryCriteria["selectedEnvironments"].([]interface{})), + "selected_environments": sdkv2_schema.NewSet(sdkv2_schema.HashString, artifactoryCriteria["selectedEnvironments"].([]interface{})), } } var unpackReleaseBundleV2PromotionCriteria = func(terraformCriteria map[string]interface{}, baseCriteria BaseCriteriaAPIModel) interface{} { return ReleaseBundleV2PromotionCriteriaAPIModel{ - SelectedEnvironments: utilsdk.CastToStringArr(terraformCriteria["selected_environments"].(*schema.Set).List()), + SelectedEnvironments: utilsdk.CastToStringArr(terraformCriteria["selected_environments"].(*sdkv2_schema.Set).List()), } } @@ -758,13 +835,13 @@ var unpackCriteria = func(d *utilsdk.ResourceData, webhookType string) interface var webhookCriteria interface{} if v, ok := d.GetOk("criteria"); ok { - criteria := v.(*schema.Set).List() + criteria := v.(*sdkv2_schema.Set).List() if len(criteria) == 1 { id := criteria[0].(map[string]interface{}) baseCriteria := BaseCriteriaAPIModel{ - IncludePatterns: utilsdk.CastToStringArr(id["include_patterns"].(*schema.Set).List()), - ExcludePatterns: utilsdk.CastToStringArr(id["exclude_patterns"].(*schema.Set).List()), + IncludePatterns: utilsdk.CastToStringArr(id["include_patterns"].(*sdkv2_schema.Set).List()), + ExcludePatterns: utilsdk.CastToStringArr(id["exclude_patterns"].(*sdkv2_schema.Set).List()), } webhookCriteria = domainUnpackLookup[webhookType](id, baseCriteria) @@ -774,25 +851,25 @@ var unpackCriteria = func(d *utilsdk.ResourceData, webhookType string) interface return webhookCriteria } -var packCriteria = func(d *schema.ResourceData, webhookType string, criteria map[string]interface{}) []error { +var packCriteria = func(d *sdkv2_schema.ResourceData, webhookType string, criteria map[string]interface{}) []error { setValue := utilsdk.MkLens(d) - resource := domainSchemaLookup(currentSchemaVersion, false, webhookType)[webhookType]["criteria"].Elem.(*schema.Resource) + resource := domainSchemaLookup(currentSchemaVersion, false, webhookType)[webhookType]["criteria"].Elem.(*sdkv2_schema.Resource) packedCriteria := domainPackLookup[webhookType](criteria) includePatterns := []interface{}{} if v, ok := criteria["includePatterns"]; ok && v != nil { includePatterns = v.([]interface{}) } - packedCriteria["include_patterns"] = schema.NewSet(schema.HashString, includePatterns) + packedCriteria["include_patterns"] = sdkv2_schema.NewSet(sdkv2_schema.HashString, includePatterns) excludePatterns := []interface{}{} if v, ok := criteria["excludePatterns"]; ok && v != nil { excludePatterns = v.([]interface{}) } - packedCriteria["exclude_patterns"] = schema.NewSet(schema.HashString, excludePatterns) + packedCriteria["exclude_patterns"] = sdkv2_schema.NewSet(sdkv2_schema.HashString, excludePatterns) - return setValue("criteria", schema.NewSet(schema.HashResource(resource), []interface{}{packedCriteria})) + return setValue("criteria", sdkv2_schema.NewSet(sdkv2_schema.HashResource(resource), []interface{}{packedCriteria})) } var domainCriteriaValidationLookup = map[string]func(context.Context, map[string]interface{}) error{ @@ -814,7 +891,7 @@ var repoCriteriaValidation = func(ctx context.Context, criteria map[string]inter anyLocal := criteria["any_local"].(bool) anyRemote := criteria["any_remote"].(bool) anyFederated := criteria["any_federated"].(bool) - repoKeys := criteria["repo_keys"].(*schema.Set).List() + repoKeys := criteria["repo_keys"].(*sdkv2_schema.Set).List() if (!anyLocal && !anyRemote && !anyFederated) && len(repoKeys) == 0 { return fmt.Errorf("repo_keys cannot be empty when any_local, any_remote, and any_federated are false") @@ -825,8 +902,8 @@ var repoCriteriaValidation = func(ctx context.Context, criteria map[string]inter var buildCriteriaValidation = func(ctx context.Context, criteria map[string]interface{}) error { anyBuild := criteria["any_build"].(bool) - selectedBuilds := criteria["selected_builds"].(*schema.Set).List() - includePatterns := criteria["include_patterns"].(*schema.Set).List() + selectedBuilds := criteria["selected_builds"].(*sdkv2_schema.Set).List() + includePatterns := criteria["include_patterns"].(*sdkv2_schema.Set).List() if !anyBuild && (len(selectedBuilds) == 0 && len(includePatterns) == 0) { return fmt.Errorf("selected_builds or include_patterns cannot be empty when any_build is false") @@ -837,7 +914,7 @@ var buildCriteriaValidation = func(ctx context.Context, criteria map[string]inte var releaseBundleCriteriaValidation = func(ctx context.Context, criteria map[string]interface{}) error { anyReleaseBundle := criteria["any_release_bundle"].(bool) - registeredReleaseBundlesNames := criteria["registered_release_bundle_names"].(*schema.Set).List() + registeredReleaseBundlesNames := criteria["registered_release_bundle_names"].(*sdkv2_schema.Set).List() if !anyReleaseBundle && len(registeredReleaseBundlesNames) == 0 { return fmt.Errorf("registered_release_bundle_names cannot be empty when any_release_bundle is false") @@ -848,7 +925,7 @@ var releaseBundleCriteriaValidation = func(ctx context.Context, criteria map[str var releaseBundleV2CriteriaValidation = func(ctx context.Context, criteria map[string]interface{}) error { anyReleaseBundle := criteria["any_release_bundle"].(bool) - selectedReleaseBundles := criteria["selected_release_bundles"].(*schema.Set).List() + selectedReleaseBundles := criteria["selected_release_bundles"].(*sdkv2_schema.Set).List() if !anyReleaseBundle && len(selectedReleaseBundles) == 0 { return fmt.Errorf("selected_release_bundles cannot be empty when any_release_bundle is false") @@ -861,11 +938,11 @@ var emptyCriteriaValidation = func(ctx context.Context, criteria map[string]inte return nil } -var packSecret = func(d *schema.ResourceData, url string) string { +var packSecret = func(d *sdkv2_schema.ResourceData, url string) string { // Get secret from TF state var secret string if v, ok := d.GetOk("handler"); ok { - handlers := v.(*schema.Set).List() + handlers := v.(*sdkv2_schema.Set).List() for _, handler := range handlers { h := handler.(map[string]interface{}) // if urls match, assign the secret value from the state @@ -877,4 +954,3 @@ var packSecret = func(d *schema.ResourceData, url string) string { return secret } - diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_webhook.go b/pkg/artifactory/resource/webhook/resource_artifactory_webhook.go index 36caea9a..7c9dd46f 100644 --- a/pkg/artifactory/resource/webhook/resource_artifactory_webhook.go +++ b/pkg/artifactory/resource/webhook/resource_artifactory_webhook.go @@ -86,57 +86,59 @@ var patternsSchemaAttributes = func(description string) map[string]schema.Attrib } } -func (r *WebhookResource) schema(domain string, criteriaBlock *schema.SetNestedBlock) schema.Schema { - blocks := map[string]schema.Block{ - "handler": schema.SetNestedBlock{ - NestedObject: schema.NestedBlockObject{ - Attributes: map[string]schema.Attribute{ - "url": schema.StringAttribute{ - Required: true, - Validators: []validator.String{ - stringvalidator.LengthAtLeast(1), - validatorfw_string.IsURLHttpOrHttps(), - }, - Description: "Specifies the URL that the Webhook invokes. This will be the URL that Artifactory will send an HTTP POST request to.", - }, - "secret": schema.StringAttribute{ - Optional: true, - // Sensitive: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - Description: "Secret authentication token that will be sent to the configured URL.", - }, - "use_secret_for_signing": schema.BoolAttribute{ - Optional: true, - PlanModifiers: []planmodifier.Bool{ - boolplanmodifier.UseStateForUnknown(), - }, - MarkdownDescription: "When set to `true`, the secret will be used to sign the event payload, allowing the target to validate that the payload content has not been changed and will not be passed as part of the event. If left unset or set to `false`, the secret is passed through the `X-JFrog-Event-Auth` HTTP header.", - }, - "proxy": schema.StringAttribute{ - Optional: true, - Validators: []validator.String{ - stringvalidator.LengthAtLeast(1), - validatorfw_string.RegexNotMatches(regexp.MustCompile(`^http.+`), "expected \"proxy\" not to be a valid url"), - }, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - Description: "Proxy key from Artifactory Proxies setting", - }, - "custom_http_headers": schema.MapAttribute{ - ElementType: types.StringType, - Optional: true, - MarkdownDescription: "Custom HTTP headers you wish to use to invoke the Webhook, comprise of key/value pair.", - }, +var handlerBlock = schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "url": schema.StringAttribute{ + Required: true, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + validatorfw_string.IsURLHttpOrHttps(), }, + Description: "Specifies the URL that the Webhook invokes. This will be the URL that Artifactory will send an HTTP POST request to.", }, - Validators: []validator.Set{ - setvalidator.IsRequired(), - setvalidator.SizeAtLeast(1), + "secret": schema.StringAttribute{ + Optional: true, + Sensitive: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + Description: "Secret authentication token that will be sent to the configured URL.", + }, + "use_secret_for_signing": schema.BoolAttribute{ + Optional: true, + PlanModifiers: []planmodifier.Bool{ + boolplanmodifier.UseStateForUnknown(), + }, + MarkdownDescription: "When set to `true`, the secret will be used to sign the event payload, allowing the target to validate that the payload content has not been changed and will not be passed as part of the event. If left unset or set to `false`, the secret is passed through the `X-JFrog-Event-Auth` HTTP header.", + }, + "proxy": schema.StringAttribute{ + Optional: true, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + validatorfw_string.RegexNotMatches(regexp.MustCompile(`^http.+`), "expected \"proxy\" not to be a valid url"), + }, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + Description: "Proxy key from Artifactory Proxies setting", + }, + "custom_http_headers": schema.MapAttribute{ + ElementType: types.StringType, + Optional: true, + MarkdownDescription: "Custom HTTP headers you wish to use to invoke the Webhook, comprise of key/value pair.", }, }, + }, + Validators: []validator.Set{ + setvalidator.IsRequired(), + setvalidator.SizeAtLeast(1), + }, +} + +func (r *WebhookResource) CreateSchema(domain string, criteriaBlock *schema.SetNestedBlock, handlerBlock schema.SetNestedBlock) schema.Schema { + blocks := map[string]schema.Block{ + "handler": handlerBlock, } if criteriaBlock != nil { diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_webhook_build.go b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_build.go index 322aff33..11b5eb2b 100644 --- a/pkg/artifactory/resource/webhook/resource_artifactory_webhook_build.go +++ b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_build.go @@ -66,7 +66,7 @@ func (r *BuildWebhookResource) Schema(ctx context.Context, req resource.SchemaRe Description: "Specifies where the webhook will be applied on which builds.", } - resp.Schema = r.schema(r.Domain, &criteriaBlock) + resp.Schema = r.CreateSchema(r.Domain, &criteriaBlock, handlerBlock) } func (r *BuildWebhookResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_webhook_release_bundle.go b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_release_bundle.go index df4d6c38..86e6284b 100644 --- a/pkg/artifactory/resource/webhook/resource_artifactory_webhook_release_bundle.go +++ b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_release_bundle.go @@ -97,7 +97,7 @@ func (r *ReleaseBundleWebhookResource) Schema(ctx context.Context, req resource. Description: "Specifies where the webhook will be applied, on which release bundles or distributions.", } - resp.Schema = r.schema(r.Domain, &criteriaBlock) + resp.Schema = r.CreateSchema(r.Domain, &criteriaBlock, handlerBlock) if r.Domain == ReleaseBundleDomain { resp.Schema.DeprecationMessage = "This resource is being deprecated and replaced by artifactory_destination_webhook resource" } diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_webhook_release_bundle_v2.go b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_release_bundle_v2.go index d848055f..400d8cf7 100644 --- a/pkg/artifactory/resource/webhook/resource_artifactory_webhook_release_bundle_v2.go +++ b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_release_bundle_v2.go @@ -66,7 +66,7 @@ func (r *ReleaseBundleV2WebhookResource) Schema(ctx context.Context, req resourc Description: "Specifies where the webhook will be applied, on which release bundles or distributions.", } - resp.Schema = r.schema(r.Domain, &criteriaBlock) + resp.Schema = r.CreateSchema(r.Domain, &criteriaBlock, handlerBlock) } func (r *ReleaseBundleV2WebhookResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_webhook_release_bundle_v2_promotion.go b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_release_bundle_v2_promotion.go index 25ea5dbf..b12a98a1 100644 --- a/pkg/artifactory/resource/webhook/resource_artifactory_webhook_release_bundle_v2_promotion.go +++ b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_release_bundle_v2_promotion.go @@ -61,7 +61,7 @@ func (r *ReleaseBundleV2PromotionWebhookResource) Schema(ctx context.Context, re Description: "Specifies where the webhook will be applied, on which release bundles or distributions.", } - resp.Schema = r.schema(r.Domain, &criteriaBlock) + resp.Schema = r.CreateSchema(r.Domain, &criteriaBlock, handlerBlock) } func (r *ReleaseBundleV2PromotionWebhookResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_webhook_repo.go b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_repo.go index fc607bb4..a77a502f 100644 --- a/pkg/artifactory/resource/webhook/resource_artifactory_webhook_repo.go +++ b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_repo.go @@ -94,7 +94,7 @@ func (r *RepoWebhookResource) Schema(ctx context.Context, req resource.SchemaReq Description: "Specifies where the webhook will be applied on which repositories.", } - resp.Schema = r.schema(r.Domain, &criteriaBlock) + resp.Schema = r.CreateSchema(r.Domain, &criteriaBlock, handlerBlock) } func (r *RepoWebhookResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_webhook_user.go b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_user.go index 88ba1aa0..c6084e93 100644 --- a/pkg/artifactory/resource/webhook/resource_artifactory_webhook_user.go +++ b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_user.go @@ -55,7 +55,7 @@ func (r *UserWebhookResource) Metadata(ctx context.Context, req resource.Metadat } func (r *UserWebhookResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { - resp.Schema = r.schema(r.Domain, nil) + resp.Schema = r.CreateSchema(r.Domain, nil, handlerBlock) } func (r *UserWebhookResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { From f969441e499317638ffb7392c540af8293bae97a Mon Sep 17 00:00:00 2001 From: Alex Hung Date: Mon, 30 Sep 2024 15:19:16 -0700 Subject: [PATCH 02/12] Migrate repo type custom webhook resource to Plugin Framework --- pkg/artifactory/provider/framework.go | 3 + .../resource_artifactory_custom_webhook.go | 397 ++++++++++++------ ...esource_artifactory_custom_webhook_repo.go | 315 ++++++++++++++ ...esource_artifactory_custom_webhook_test.go | 185 +++++++- .../webhook/resource_artifactory_webhook.go | 35 +- 5 files changed, 784 insertions(+), 151 deletions(-) create mode 100644 pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook_repo.go diff --git a/pkg/artifactory/provider/framework.go b/pkg/artifactory/provider/framework.go index 692d3303..0a57c7ab 100644 --- a/pkg/artifactory/provider/framework.go +++ b/pkg/artifactory/provider/framework.go @@ -223,13 +223,16 @@ func (p *ArtifactoryProvider) Resources(ctx context.Context) []func() resource.R replication.NewLocalRepositoryMultiReplicationResource, replication.NewRemoteRepositoryReplicationResource, webhook.NewArtifactWebhookResource, + webhook.NewArtifactCustomWebhookResource, webhook.NewArtifactLifecycleWebhookResource, webhook.NewArtifactPropertyWebhookResource, + webhook.NewArtifactPropertyCustomWebhookResource, webhook.NewArtifactoryReleaseBundleWebhookResource, webhook.NewBuildWebhookResource, webhook.NewDestinationWebhookResource, webhook.NewDistributionWebhookResource, webhook.NewDockerWebhookResource, + webhook.NewDockerCustomWebhookResource, webhook.NewReleaseBundleWebhookResource, webhook.NewReleaseBundleV2WebhookResource, webhook.NewReleaseBundleV2PromotionWebhookResource, diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook.go b/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook.go index 7f3116be..fe12562f 100644 --- a/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook.go +++ b/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook.go @@ -11,13 +11,16 @@ import ( "github.com/hashicorp/terraform-plugin-framework-validators/mapvalidator" "github.com/hashicorp/terraform-plugin-framework-validators/setvalidator" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + sdkv2_diag "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff" sdkv2_schema "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" @@ -26,6 +29,7 @@ import ( utilsdk "github.com/jfrog/terraform-provider-shared/util/sdk" util_validator "github.com/jfrog/terraform-provider-shared/validator" validatorfw_string "github.com/jfrog/terraform-provider-shared/validator/fw/string" + "github.com/samber/lo" "golang.org/x/exp/slices" ) @@ -98,15 +102,258 @@ func (r *CustomWebhookResource) CreateSchema(domain string, criteriaBlock *schem return r.WebhookResource.CreateSchema(domain, criteriaBlock, customHandlerBlock) } +func (r *CustomWebhookResource) Create(ctx context.Context, webhook CustomWebhookAPIModel, req resource.CreateRequest, resp *resource.CreateResponse) { + createWebhook(r.ProviderData.Client, webhook, req, resp) +} + +func (r *CustomWebhookResource) Read(ctx context.Context, key string, webhook *CustomWebhookAPIModel, req resource.ReadRequest, resp *resource.ReadResponse) (found bool) { + return readWebhook(ctx, r.ProviderData.Client, key, webhook, req, resp) +} + +func (r *CustomWebhookResource) Update(_ context.Context, key string, webhook CustomWebhookAPIModel, req resource.UpdateRequest, resp *resource.UpdateResponse) { + updateWebhook(r.ProviderData.Client, key, webhook, req, resp) +} + +type CustomWebhookNoCriteriaResourceModel struct { + WebhookNoCriteriaResourceModel +} + +func (m CustomWebhookNoCriteriaResourceModel) toAPIModel(ctx context.Context, domain string, apiModel *CustomWebhookAPIModel) (diags diag.Diagnostics) { + var eventTypes []string + d := m.EventTypes.ElementsAs(ctx, &eventTypes, false) + if d.HasError() { + diags.Append(d...) + } + + handlers := lo.Map( + m.Handlers.Elements(), + func(elem attr.Value, _ int) CustomHandlerAPIModel { + attrs := elem.(types.Object).Attributes() + + secrets := lo.MapToSlice( + attrs["secrets"].(types.Map).Elements(), + func(k string, v attr.Value) KeyValuePairAPIModel { + return KeyValuePairAPIModel{ + Name: k, + Value: v.(types.String).ValueString(), + } + }, + ) + + httpHeaders := lo.MapToSlice( + attrs["http_headers"].(types.Map).Elements(), + func(k string, v attr.Value) KeyValuePairAPIModel { + return KeyValuePairAPIModel{ + Name: k, + Value: v.(types.String).ValueString(), + } + }, + ) + + return CustomHandlerAPIModel{ + HandlerType: "custom-webhook", + Url: attrs["url"].(types.String).ValueString(), + Secrets: secrets, + Proxy: attrs["proxy"].(types.String).ValueStringPointer(), + HttpHeaders: httpHeaders, + Payload: attrs["payload"].(types.String).ValueString(), + } + }, + ) + + *apiModel = CustomWebhookAPIModel{ + WebhookAPIModel: WebhookAPIModel{ + Key: m.Key.ValueString(), + Description: m.Description.ValueString(), + Enabled: m.Enabled.ValueBool(), + EventFilter: EventFilterAPIModel{ + Domain: domain, + EventTypes: eventTypes, + }, + }, + Handlers: handlers, + } + + return +} + +var customHandlerSetResourceModelAttributeTypes = map[string]attr.Type{ + "url": types.StringType, + "secrets": types.MapType{ElemType: types.StringType}, + "proxy": types.StringType, + "http_headers": types.MapType{ElemType: types.StringType}, + "payload": types.StringType, +} + +var customHandlerSetResourceModelElementTypes = types.ObjectType{ + AttrTypes: customHandlerSetResourceModelAttributeTypes, +} + +func (m *CustomWebhookNoCriteriaResourceModel) fromAPIModel(ctx context.Context, apiModel CustomWebhookAPIModel, stateHandlers basetypes.SetValue) diag.Diagnostics { + diags := diag.Diagnostics{} + + m.Key = types.StringValue(apiModel.Key) + + description := types.StringNull() + if apiModel.Description != "" { + description = types.StringValue(apiModel.Description) + } + m.Description = description + + m.Enabled = types.BoolValue(apiModel.Enabled) + + eventTypes, d := types.SetValueFrom(ctx, types.StringType, apiModel.EventFilter.EventTypes) + if d.HasError() { + diags.Append(d...) + } + m.EventTypes = eventTypes + + handlers := lo.Map( + apiModel.Handlers, + func(handler CustomHandlerAPIModel, _ int) attr.Value { + secrets := types.MapNull(types.StringType) + + matchedHandler, found := lo.Find( + stateHandlers.Elements(), + func(elem attr.Value) bool { + attrs := elem.(types.Object).Attributes() + return attrs["url"].(types.String).ValueString() == handler.Url + }, + ) + if found { + attrs := matchedHandler.(types.Object).Attributes() + s := attrs["secrets"].(types.Map) + if !s.IsNull() && len(s.Elements()) > 0 { + secrets = s + } + } + + httpHeaders := types.MapNull(types.StringType) + if len(handler.HttpHeaders) > 0 { + headerElems := lo.Associate( + handler.HttpHeaders, + func(kvPair KeyValuePairAPIModel) (string, attr.Value) { + return kvPair.Name, types.StringValue(kvPair.Value) + }, + ) + h, d := types.MapValue( + types.StringType, + headerElems, + ) + if d.HasError() { + diags.Append(d...) + } + + httpHeaders = h + } + + h, d := types.ObjectValue( + customHandlerSetResourceModelAttributeTypes, + map[string]attr.Value{ + "url": types.StringValue(handler.Url), + "secrets": secrets, + "proxy": types.StringPointerValue(handler.Proxy), + "http_headers": httpHeaders, + "payload": types.StringValue(handler.Payload), + }, + ) + if d.HasError() { + diags.Append(d...) + } + + return h + }, + ) + + handlersSet, d := types.SetValue( + customHandlerSetResourceModelElementTypes, + handlers, + ) + if d.HasError() { + diags.Append(d...) + } + m.Handlers = handlersSet + + return diags +} + +type CustomWebhookResourceModel struct { + CustomWebhookNoCriteriaResourceModel + Criteria types.Set `tfsdk:"criteria"` +} + +func (m *CustomWebhookResourceModel) toBaseCriteriaAPIModel(ctx context.Context, criteriaAttrs map[string]attr.Value) (BaseCriteriaAPIModel, diag.Diagnostics) { + diags := diag.Diagnostics{} + + var includePatterns []string + d := criteriaAttrs["include_patterns"].(types.Set).ElementsAs(ctx, &includePatterns, false) + if d.HasError() { + diags.Append(d...) + } + + var excludePatterns []string + d = criteriaAttrs["exclude_patterns"].(types.Set).ElementsAs(ctx, &excludePatterns, false) + if d.HasError() { + diags.Append(d...) + } + + return BaseCriteriaAPIModel{ + IncludePatterns: includePatterns, + ExcludePatterns: excludePatterns, + }, diags +} + +func (m CustomWebhookResourceModel) toAPIModel(ctx context.Context, domain string, criteriaAPIModel interface{}, apiModel *CustomWebhookAPIModel) (diags diag.Diagnostics) { + d := m.CustomWebhookNoCriteriaResourceModel.toAPIModel(ctx, domain, apiModel) + + apiModel.EventFilter.Criteria = criteriaAPIModel + + return d +} + +func (m *CustomWebhookResourceModel) fromBaseCriteriaAPIModel(ctx context.Context, criteriaAPIModel map[string]interface{}) (map[string]attr.Value, diag.Diagnostics) { + diags := diag.Diagnostics{} + + includePatterns := types.SetNull(types.StringType) + if v, ok := criteriaAPIModel["includePatterns"]; ok && v != nil { + ps, d := types.SetValueFrom(ctx, types.StringType, v) + if d.HasError() { + diags.Append(d...) + } + + includePatterns = ps + } + + excludePatterns := types.SetNull(types.StringType) + if v, ok := criteriaAPIModel["excludePatterns"]; ok && v != nil { + ps, d := types.SetValueFrom(ctx, types.StringType, v) + if d.HasError() { + diags.Append(d...) + } + + excludePatterns = ps + } + + return map[string]attr.Value{ + "include_patterns": includePatterns, + "exclude_patterns": excludePatterns, + }, diags +} + +func (m *CustomWebhookResourceModel) fromAPIModel(ctx context.Context, apiModel CustomWebhookAPIModel, stateHandlers basetypes.SetValue, criteriaSet *basetypes.SetValue) diag.Diagnostics { + if criteriaSet != nil { + m.Criteria = *criteriaSet + } + + return m.CustomWebhookNoCriteriaResourceModel.fromAPIModel(ctx, apiModel, stateHandlers) +} + var DomainSupported = []string{ ArtifactLifecycleDomain, - ArtifactPropertyDomain, - ArtifactDomain, ArtifactoryReleaseBundleDomain, BuildDomain, DestinationDomain, DistributionDomain, - DockerDomain, ReleaseBundleDomain, ReleaseBundleV2Domain, ReleaseBundleV2PromotionDomain, @@ -199,42 +446,6 @@ func baseCustomWebhookBaseSchema(webhookType string) map[string]*sdkv2_schema.Sc } } -var repoWebhookSchema = func(webhookType string, version int, isCustom bool) map[string]*sdkv2_schema.Schema { - return utilsdk.MergeMaps(getBaseSchemaByVersion(webhookType, version, isCustom), map[string]*sdkv2_schema.Schema{ - "criteria": { - Type: sdkv2_schema.TypeSet, - Required: true, - MaxItems: 1, - Elem: &sdkv2_schema.Resource{ - Schema: utilsdk.MergeMaps(baseCriteriaSchema, map[string]*sdkv2_schema.Schema{ - "any_local": { - Type: sdkv2_schema.TypeBool, - Required: true, - Description: "Trigger on any local repositories", - }, - "any_remote": { - Type: sdkv2_schema.TypeBool, - Required: true, - Description: "Trigger on any remote repositories", - }, - "any_federated": { - Type: sdkv2_schema.TypeBool, - Required: true, - Description: "Trigger on any federated repositories", - }, - "repo_keys": { - Type: sdkv2_schema.TypeSet, - Required: true, - Elem: &sdkv2_schema.Schema{Type: sdkv2_schema.TypeString}, - Description: "Trigger on this list of repository keys", - }, - }), - }, - Description: "Specifies where the webhook will be applied on which repositories.", - }, - }) -} - var buildWebhookSchema = func(webhookType string, version int, isCustom bool) map[string]*sdkv2_schema.Schema { return utilsdk.MergeMaps(getBaseSchemaByVersion(webhookType, version, isCustom), map[string]*sdkv2_schema.Schema{ "criteria": { @@ -342,12 +553,12 @@ var artifactLifecycleWebhookSchema = func(webhookType string, version int, isCus return getBaseSchemaByVersion(webhookType, version, isCustom) } -type CustomWebookAPIModel struct { +type CustomWebhookAPIModel struct { WebhookAPIModel Handlers []CustomHandlerAPIModel `json:"handlers"` } -func (w CustomWebookAPIModel) Id() string { +func (w CustomWebhookAPIModel) Id() string { return w.Key } @@ -355,7 +566,7 @@ type CustomHandlerAPIModel struct { HandlerType string `json:"handler_type"` Url string `json:"url"` Secrets []KeyValuePairAPIModel `json:"secrets"` - Proxy string `json:"proxy"` + Proxy *string `json:"proxy"` HttpHeaders []KeyValuePairAPIModel `json:"http_headers"` Payload string `json:"payload,omitempty"` } @@ -390,7 +601,7 @@ var packSecretsCustom = func(keyValuePairs []KeyValuePairAPIModel, d *sdkv2_sche func ResourceArtifactoryCustomWebhook(webhookType string) *sdkv2_schema.Resource { - var unpackWebhook = func(data *sdkv2_schema.ResourceData) (CustomWebookAPIModel, error) { + var unpackWebhook = func(data *sdkv2_schema.ResourceData) (CustomWebhookAPIModel, error) { d := &utilsdk.ResourceData{ResourceData: data} var unpackHandlers = func(d *utilsdk.ResourceData) []CustomHandlerAPIModel { @@ -413,7 +624,9 @@ func ResourceArtifactoryCustomWebhook(webhookType string) *sdkv2_schema.Resource } if v, ok := h["proxy"]; ok { - webhookHandler.Proxy = v.(string) + if s, ok := v.(string); ok { + webhookHandler.Proxy = &s + } } if v, ok := h["http_headers"]; ok { @@ -432,7 +645,7 @@ func ResourceArtifactoryCustomWebhook(webhookType string) *sdkv2_schema.Resource return webhookHandlers } - webhook := CustomWebookAPIModel{ + webhook := CustomWebhookAPIModel{ WebhookAPIModel: WebhookAPIModel{ Key: d.GetString("key", false), Description: d.GetString("description", false), @@ -456,10 +669,13 @@ func ResourceArtifactoryCustomWebhook(webhookType string) *sdkv2_schema.Resource for _, handler := range handlers { packedHandler := map[string]interface{}{ "url": handler.Url, - "proxy": handler.Proxy, "payload": handler.Payload, } + if handler.Proxy != nil { + packedHandler["proxy"] = *handler.Proxy + } + if handler.Secrets != nil { packedHandler["secrets"] = packSecretsCustom(handler.Secrets, d, handler.Url) } @@ -474,7 +690,7 @@ func ResourceArtifactoryCustomWebhook(webhookType string) *sdkv2_schema.Resource return setValue("handler", sdkv2_schema.NewSet(sdkv2_schema.HashResource(resource), packedHandlers)) } - var packWebhook = func(d *sdkv2_schema.ResourceData, webhook CustomWebookAPIModel) diag.Diagnostics { + var packWebhook = func(d *sdkv2_schema.ResourceData, webhook CustomWebhookAPIModel) sdkv2_diag.Diagnostics { setValue := utilsdk.MkLens(d) setValue("key", webhook.Key) @@ -487,14 +703,14 @@ func ResourceArtifactoryCustomWebhook(webhookType string) *sdkv2_schema.Resource errors = append(errors, packHandlers(d, webhook.Handlers)...) if len(errors) > 0 { - return diag.Errorf("failed to pack webhook %q", errors) + return sdkv2_diag.Errorf("failed to pack webhook %q", errors) } return nil } - var readWebhook = func(ctx context.Context, data *sdkv2_schema.ResourceData, m interface{}) diag.Diagnostics { - webhook := CustomWebookAPIModel{} + var readWebhook = func(ctx context.Context, data *sdkv2_schema.ResourceData, m interface{}) sdkv2_diag.Diagnostics { + webhook := CustomWebhookAPIModel{} webhook.EventFilter.Criteria = domainCriteriaLookup[webhookType] @@ -506,7 +722,7 @@ func ResourceArtifactoryCustomWebhook(webhookType string) *sdkv2_schema.Resource Get(WebhookURL) if err != nil { - return diag.FromErr(err) + return sdkv2_diag.FromErr(err) } if resp.StatusCode() == http.StatusNotFound { @@ -515,7 +731,7 @@ func ResourceArtifactoryCustomWebhook(webhookType string) *sdkv2_schema.Resource } if resp.IsError() { - return diag.Errorf("%s", artifactoryError.String()) + return sdkv2_diag.Errorf("%s", artifactoryError.String()) } return packWebhook(data, webhook) @@ -527,10 +743,10 @@ func ResourceArtifactoryCustomWebhook(webhookType string) *sdkv2_schema.Resource return proxyNotFoundRegex.MatchString(string(response.Body()[:])) } - var createWebhook = func(ctx context.Context, data *sdkv2_schema.ResourceData, m interface{}) diag.Diagnostics { + var createWebhook = func(ctx context.Context, data *sdkv2_schema.ResourceData, m interface{}) sdkv2_diag.Diagnostics { webhook, err := unpackWebhook(data) if err != nil { - return diag.FromErr(err) + return sdkv2_diag.FromErr(err) } var artifactoryError artifactory.ArtifactoryErrorsResponse @@ -540,11 +756,11 @@ func ResourceArtifactoryCustomWebhook(webhookType string) *sdkv2_schema.Resource AddRetryCondition(retryOnProxyError). Post(webhooksURL) if err != nil { - return diag.FromErr(err) + return sdkv2_diag.FromErr(err) } if resp.IsError() { - return diag.Errorf("%s", artifactoryError.String()) + return sdkv2_diag.Errorf("%s", artifactoryError.String()) } data.SetId(webhook.Id()) @@ -552,10 +768,10 @@ func ResourceArtifactoryCustomWebhook(webhookType string) *sdkv2_schema.Resource return readWebhook(ctx, data, m) } - var updateWebhook = func(ctx context.Context, data *sdkv2_schema.ResourceData, m interface{}) diag.Diagnostics { + var updateWebhook = func(ctx context.Context, data *sdkv2_schema.ResourceData, m interface{}) sdkv2_diag.Diagnostics { webhook, err := unpackWebhook(data) if err != nil { - return diag.FromErr(err) + return sdkv2_diag.FromErr(err) } var artifactoryError artifactory.ArtifactoryErrorsResponse @@ -566,11 +782,11 @@ func ResourceArtifactoryCustomWebhook(webhookType string) *sdkv2_schema.Resource AddRetryCondition(retryOnProxyError). Put(WebhookURL) if err != nil { - return diag.FromErr(err) + return sdkv2_diag.FromErr(err) } if resp.IsError() { - return diag.Errorf("%s", artifactoryError.String()) + return sdkv2_diag.Errorf("%s", artifactoryError.String()) } data.SetId(webhook.Id()) @@ -578,7 +794,7 @@ func ResourceArtifactoryCustomWebhook(webhookType string) *sdkv2_schema.Resource return readWebhook(ctx, data, m) } - var deleteWebhook = func(ctx context.Context, data *sdkv2_schema.ResourceData, m interface{}) diag.Diagnostics { + var deleteWebhook = func(ctx context.Context, data *sdkv2_schema.ResourceData, m interface{}) sdkv2_diag.Diagnostics { var artifactoryError artifactory.ArtifactoryErrorsResponse resp, err := m.(util.ProviderMetadata).Client.R(). SetPathParam("webhookKey", data.Id()). @@ -586,7 +802,7 @@ func ResourceArtifactoryCustomWebhook(webhookType string) *sdkv2_schema.Resource Delete(WebhookURL) if err != nil { - return diag.FromErr(err) + return sdkv2_diag.FromErr(err) } if resp.StatusCode() == http.StatusNotFound { @@ -595,7 +811,7 @@ func ResourceArtifactoryCustomWebhook(webhookType string) *sdkv2_schema.Resource } if resp.IsError() { - return diag.Errorf("%s", artifactoryError.String()) + return sdkv2_diag.Errorf("%s", artifactoryError.String()) } return nil @@ -680,9 +896,6 @@ var packKeyValuePair = func(keyValuePairs []KeyValuePairAPIModel) map[string]int type EmptyWebhookCriteria struct{} var domainCriteriaLookup = map[string]interface{}{ - "artifact": RepoCriteriaAPIModel{}, - "artifact_property": RepoCriteriaAPIModel{}, - "docker": RepoCriteriaAPIModel{}, "build": BuildCriteriaAPIModel{}, "release_bundle": ReleaseBundleCriteriaAPIModel{}, "distribution": ReleaseBundleCriteriaAPIModel{}, @@ -695,9 +908,6 @@ var domainCriteriaLookup = map[string]interface{}{ } var domainPackLookup = map[string]func(map[string]interface{}) map[string]interface{}{ - "artifact": packRepoCriteria, - "artifact_property": packRepoCriteria, - "docker": packRepoCriteria, "build": packBuildCriteria, "release_bundle": packReleaseBundleCriteria, "distribution": packReleaseBundleCriteria, @@ -710,9 +920,6 @@ var domainPackLookup = map[string]func(map[string]interface{}) map[string]interf } var domainUnpackLookup = map[string]func(map[string]interface{}, BaseCriteriaAPIModel) interface{}{ - "artifact": unpackRepoCriteria, - "artifact_property": unpackRepoCriteria, - "docker": unpackRepoCriteria, "build": unpackBuildCriteria, "release_bundle": unpackReleaseBundleCriteria, "distribution": unpackReleaseBundleCriteria, @@ -726,9 +933,6 @@ var domainUnpackLookup = map[string]func(map[string]interface{}, BaseCriteriaAPI var domainSchemaLookup = func(version int, isCustom bool, webhookType string) map[string]map[string]*sdkv2_schema.Schema { return map[string]map[string]*sdkv2_schema.Schema{ - "artifact": repoWebhookSchema(webhookType, version, isCustom), - "artifact_property": repoWebhookSchema(webhookType, version, isCustom), - "docker": repoWebhookSchema(webhookType, version, isCustom), "build": buildWebhookSchema(webhookType, version, isCustom), "release_bundle": releaseBundleWebhookSchema(webhookType, version, isCustom), "distribution": releaseBundleWebhookSchema(webhookType, version, isCustom), @@ -741,21 +945,6 @@ var domainSchemaLookup = func(version int, isCustom bool, webhookType string) ma } } -var packRepoCriteria = func(artifactoryCriteria map[string]interface{}) map[string]interface{} { - criteria := map[string]interface{}{ - "any_local": artifactoryCriteria["anyLocal"].(bool), - "any_remote": artifactoryCriteria["anyRemote"].(bool), - "any_federated": false, - "repo_keys": sdkv2_schema.NewSet(sdkv2_schema.HashString, artifactoryCriteria["repoKeys"].([]interface{})), - } - - if v, ok := artifactoryCriteria["anyFederated"]; ok { - criteria["any_federated"] = v.(bool) - } - - return criteria -} - var packReleaseBundleCriteria = func(artifactoryCriteria map[string]interface{}) map[string]interface{} { return map[string]interface{}{ "any_release_bundle": artifactoryCriteria["anyReleaseBundle"].(bool), @@ -771,16 +960,6 @@ var unpackReleaseBundleCriteria = func(terraformCriteria map[string]interface{}, } } -var unpackRepoCriteria = func(terraformCriteria map[string]interface{}, baseCriteria BaseCriteriaAPIModel) interface{} { - return RepoCriteriaAPIModel{ - AnyLocal: terraformCriteria["any_local"].(bool), - AnyRemote: terraformCriteria["any_remote"].(bool), - AnyFederated: terraformCriteria["any_federated"].(bool), - RepoKeys: utilsdk.CastToStringArr(terraformCriteria["repo_keys"].(*sdkv2_schema.Set).List()), - BaseCriteriaAPIModel: baseCriteria, - } -} - var packBuildCriteria = func(artifactoryCriteria map[string]interface{}) map[string]interface{} { return map[string]interface{}{ "any_build": artifactoryCriteria["anyBuild"].(bool), @@ -873,9 +1052,6 @@ var packCriteria = func(d *sdkv2_schema.ResourceData, webhookType string, criter } var domainCriteriaValidationLookup = map[string]func(context.Context, map[string]interface{}) error{ - "artifact": repoCriteriaValidation, - "artifact_property": repoCriteriaValidation, - "docker": repoCriteriaValidation, "build": buildCriteriaValidation, "release_bundle": releaseBundleCriteriaValidation, "distribution": releaseBundleCriteriaValidation, @@ -887,19 +1063,6 @@ var domainCriteriaValidationLookup = map[string]func(context.Context, map[string "artifact_lifecycle": emptyCriteriaValidation, } -var repoCriteriaValidation = func(ctx context.Context, criteria map[string]interface{}) error { - anyLocal := criteria["any_local"].(bool) - anyRemote := criteria["any_remote"].(bool) - anyFederated := criteria["any_federated"].(bool) - repoKeys := criteria["repo_keys"].(*sdkv2_schema.Set).List() - - if (!anyLocal && !anyRemote && !anyFederated) && len(repoKeys) == 0 { - return fmt.Errorf("repo_keys cannot be empty when any_local, any_remote, and any_federated are false") - } - - return nil -} - var buildCriteriaValidation = func(ctx context.Context, criteria map[string]interface{}) error { anyBuild := criteria["any_build"].(bool) selectedBuilds := criteria["selected_builds"].(*sdkv2_schema.Set).List() diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook_repo.go b/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook_repo.go new file mode 100644 index 00000000..47a26b06 --- /dev/null +++ b/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook_repo.go @@ -0,0 +1,315 @@ +package webhook + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework-validators/setvalidator" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + "github.com/jfrog/terraform-provider-shared/util" + "github.com/samber/lo" +) + +var _ resource.Resource = &RepoCustomWebhookResource{} + +func NewArtifactCustomWebhookResource() resource.Resource { + return &RepoCustomWebhookResource{ + CustomWebhookResource: CustomWebhookResource{ + WebhookResource: WebhookResource{ + TypeName: fmt.Sprintf("artifactory_%s_custom_webhook", ArtifactDomain), + Domain: ArtifactDomain, + Description: "Provides an artifact webhook resource. This can be used to register and manage Artifactory webhook subscription which enables you to be notified or notify other users when such events take place in Artifactory.", + }, + }, + } +} + +func NewArtifactPropertyCustomWebhookResource() resource.Resource { + return &RepoCustomWebhookResource{ + CustomWebhookResource: CustomWebhookResource{ + WebhookResource: WebhookResource{ + TypeName: fmt.Sprintf("artifactory_%s_custom_webhook", ArtifactPropertyDomain), + Domain: ArtifactPropertyDomain, + Description: "Provides an artifact property webhook resource. This can be used to register and manage Artifactory webhook subscription which enables you to be notified or notify other users when such events take place in Artifactory.", + }, + }, + } +} + +func NewDockerCustomWebhookResource() resource.Resource { + return &RepoCustomWebhookResource{ + CustomWebhookResource: CustomWebhookResource{ + WebhookResource: WebhookResource{ + TypeName: fmt.Sprintf("artifactory_%s_custom_webhook", DockerDomain), + Domain: DockerDomain, + Description: "Provides a Docker webhook resource. This can be used to register and manage Artifactory webhook subscription which enables you to be notified or notify other users when such events take place in Artifactory.", + }, + }, + } +} + +type RepoCustomWebhookResourceModel struct { + CustomWebhookResourceModel +} + +type RepoCustomWebhookResource struct { + CustomWebhookResource +} + +func (r *RepoCustomWebhookResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + r.WebhookResource.Metadata(ctx, req, resp) +} + +func (r *RepoCustomWebhookResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + criteriaBlock := schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: lo.Assign( + patternsSchemaAttributes("Simple comma separated wildcard patterns for repository artifact paths (with no leading slash).\nAnt-style path expressions are supported (*, **, ?).\nFor example: `org/apache/**`"), + map[string]schema.Attribute{ + "any_local": schema.BoolAttribute{ + Required: true, + Description: "Trigger on any local repositories", + }, + "any_remote": schema.BoolAttribute{ + Required: true, + Description: "Trigger on any remote repositories", + }, + "any_federated": schema.BoolAttribute{ + Required: true, + Description: "Trigger on any federated repositories", + }, + "repo_keys": schema.SetAttribute{ + ElementType: types.StringType, + Required: true, + Description: "Trigger on this list of repository keys", + }, + }, + ), + }, + Validators: []validator.Set{ + setvalidator.SizeBetween(1, 1), + setvalidator.IsRequired(), + }, + Description: "Specifies where the webhook will be applied on which repositories.", + } + + resp.Schema = r.CreateSchema(r.Domain, &criteriaBlock) +} + +func (r *RepoCustomWebhookResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + r.WebhookResource.Configure(ctx, req, resp) +} + +func (r RepoCustomWebhookResource) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) { + var data RepoWebhookResourceModel + + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + criteriaObj := data.Criteria.Elements()[0].(types.Object) + criteriaAttrs := criteriaObj.Attributes() + + anyLocal := criteriaAttrs["any_local"].(types.Bool).ValueBool() + anyRemote := criteriaAttrs["any_remote"].(types.Bool).ValueBool() + anyFederated := criteriaAttrs["any_federated"].(types.Bool).ValueBool() + + if (!anyLocal && !anyRemote && !anyFederated) && len(criteriaAttrs["repo_keys"].(types.Set).Elements()) == 0 { + resp.Diagnostics.AddAttributeError( + path.Root("criteria").AtSetValue(criteriaObj).AtName("repo_keys"), + "Invalid Attribute Configuration", + "repo_keys cannot be empty when any_local, any_remote, and any_federated are false", + ) + } +} + +func (r *RepoCustomWebhookResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + go util.SendUsageResourceCreate(ctx, r.ProviderData.Client.R(), r.ProviderData.ProductId, r.TypeName) + + var plan RepoCustomWebhookResourceModel + + // Read Terraform plan data into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + var webhook CustomWebhookAPIModel + resp.Diagnostics.Append(plan.toAPIModel(ctx, r.Domain, &webhook)...) + if resp.Diagnostics.HasError() { + return + } + + r.CustomWebhookResource.Create(ctx, webhook, req, resp) + if resp.Diagnostics.HasError() { + return + } + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *RepoCustomWebhookResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + go util.SendUsageResourceRead(ctx, r.ProviderData.Client.R(), r.ProviderData.ProductId, r.TypeName) + + var state RepoCustomWebhookResourceModel + + // Read Terraform state data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + var webhook CustomWebhookAPIModel + found := r.CustomWebhookResource.Read(ctx, state.Key.ValueString(), &webhook, req, resp) + if resp.Diagnostics.HasError() { + return + } + + if !found { + return + } + + resp.Diagnostics.Append(state.fromAPIModel(ctx, webhook, state.Handlers)...) + if resp.Diagnostics.HasError() { + return + } + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *RepoCustomWebhookResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + go util.SendUsageResourceUpdate(ctx, r.ProviderData.Client.R(), r.ProviderData.ProductId, r.TypeName) + + var plan RepoCustomWebhookResourceModel + + // Read Terraform plan data into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + var webhook CustomWebhookAPIModel + resp.Diagnostics.Append(plan.toAPIModel(ctx, r.Domain, &webhook)...) + if resp.Diagnostics.HasError() { + return + } + + r.CustomWebhookResource.Update(ctx, plan.Key.ValueString(), webhook, req, resp) + if resp.Diagnostics.HasError() { + return + } + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *RepoCustomWebhookResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + go util.SendUsageResourceDelete(ctx, r.ProviderData.Client.R(), r.ProviderData.ProductId, r.TypeName) + + var state RepoCustomWebhookResourceModel + + // Read Terraform prior state data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + + r.WebhookResource.Delete(ctx, state.Key.ValueString(), req, resp) + if resp.Diagnostics.HasError() { + return + } + + // If the logic reaches here, it implicitly succeeded and will remove + // the resource from state if there are no other errors. +} + +// ImportState imports the resource into the Terraform state. +func (r *RepoCustomWebhookResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + r.WebhookResource.ImportState(ctx, req, resp) +} + +func (m RepoCustomWebhookResourceModel) toAPIModel(ctx context.Context, domain string, apiModel *CustomWebhookAPIModel) (diags diag.Diagnostics) { + critieriaObj := m.Criteria.Elements()[0].(types.Object) + critieriaAttrs := critieriaObj.Attributes() + + baseCriteria, d := m.CustomWebhookResourceModel.toBaseCriteriaAPIModel(ctx, critieriaAttrs) + if d.HasError() { + diags.Append(d...) + } + + var repoKeys []string + d = critieriaAttrs["repo_keys"].(types.Set).ElementsAs(ctx, &repoKeys, false) + if d.HasError() { + diags.Append(d...) + } + + criteriaAPIModel := RepoCriteriaAPIModel{ + BaseCriteriaAPIModel: baseCriteria, + AnyLocal: critieriaAttrs["any_local"].(types.Bool).ValueBool(), + AnyRemote: critieriaAttrs["any_remote"].(types.Bool).ValueBool(), + AnyFederated: critieriaAttrs["any_federated"].(types.Bool).ValueBool(), + RepoKeys: repoKeys, + } + + d = m.CustomWebhookResourceModel.toAPIModel(ctx, domain, criteriaAPIModel, apiModel) + if d.HasError() { + diags.Append(d...) + } + + return +} + +func (m *RepoCustomWebhookResourceModel) fromAPIModel(ctx context.Context, apiModel CustomWebhookAPIModel, stateHandlers basetypes.SetValue) diag.Diagnostics { + diags := diag.Diagnostics{} + + criteriaAPIModel := apiModel.EventFilter.Criteria.(map[string]interface{}) + + baseCriteriaAttrs, d := m.CustomWebhookResourceModel.fromBaseCriteriaAPIModel(ctx, criteriaAPIModel) + + repoKeys := types.SetNull(types.StringType) + if v, ok := criteriaAPIModel["repoKeys"]; ok && v != nil { + ks, d := types.SetValueFrom(ctx, types.StringType, v) + if d.HasError() { + diags.Append(d...) + } + + repoKeys = ks + } + + criteria, d := types.ObjectValue( + repoCriteriaSetResourceModelAttributeTypes, + lo.Assign( + baseCriteriaAttrs, + map[string]attr.Value{ + "any_local": types.BoolValue(criteriaAPIModel["anyLocal"].(bool)), + "any_remote": types.BoolValue(criteriaAPIModel["anyRemote"].(bool)), + "any_federated": types.BoolValue(criteriaAPIModel["anyFederated"].(bool)), + "repo_keys": repoKeys, + }, + ), + ) + if d.HasError() { + diags.Append(d...) + } + criteriaSet, d := types.SetValue( + repoCriteriaSetResourceModelElementTypes, + []attr.Value{criteria}, + ) + if d.HasError() { + diags.Append(d...) + } + + d = m.CustomWebhookResourceModel.fromAPIModel(ctx, apiModel, stateHandlers, &criteriaSet) + if d.HasError() { + diags.Append(d...) + } + + return diags +} diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook_test.go b/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook_test.go index 83d3f4a0..6464588a 100644 --- a/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook_test.go +++ b/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/plancheck" "github.com/jfrog/terraform-provider-artifactory/v12/pkg/acctest" "github.com/jfrog/terraform-provider-artifactory/v12/pkg/artifactory/resource/webhook" "github.com/jfrog/terraform-provider-shared/testutil" @@ -68,10 +69,10 @@ func customWebhookTestCase(webhookType string, t *testing.T) (*testing.T, resour header-1 = "value-1" header-2 = "value-2" } - payload = "{ \"ref\": \"main\" , \"inputs\": { \"artifact_path\": \"test-repo/repo-path\" } }" + payload = "{ \"ref\": \"main\" , \"inputs\": { \"artifact_path\": \"test-repo/repo-path/1\" } }" } handler { - url = "https://google.com" + url = "https://yahoo.com" secrets = { secret3 = "value3" secret4 = "value4" @@ -80,11 +81,11 @@ func customWebhookTestCase(webhookType string, t *testing.T) (*testing.T, resour header-3 = "value-3" header-4 = "value-4" } - payload = "{ \"ref\": \"main\" , \"inputs\": { \"artifact_path\": \"test-repo/repo-path\" } }" + payload = "{ \"ref\": \"main\" , \"inputs\": { \"artifact_path\": \"test-repo/repo-path/2\" } }" } handler { - url = "https://google.com" - payload = "{ \"ref\": \"main\" , \"inputs\": { \"artifact_path\": \"test-repo/repo-path\" } }" + url = "https://msnbc.com" + payload = "{ \"ref\": \"main\" , \"inputs\": { \"artifact_path\": \"test-repo/repo-path/3\" } }" } depends_on = [artifactory_local_{{ .repoType }}_repository.{{ .repoName }}] @@ -111,19 +112,19 @@ func customWebhookTestCase(webhookType string, t *testing.T) (*testing.T, resour resource.TestCheckResourceAttr(fqrn, "handler.0.http_headers.%", "2"), resource.TestCheckResourceAttr(fqrn, "handler.0.http_headers.header-1", "value-1"), resource.TestCheckResourceAttr(fqrn, "handler.0.http_headers.header-2", "value-2"), - resource.TestCheckResourceAttr(fqrn, "handler.0.payload", "{ \"ref\": \"main\" , \"inputs\": { \"artifact_path\": \"test-repo/repo-path\" } }"), - resource.TestCheckResourceAttr(fqrn, "handler.1.url", "https://google.com"), + resource.TestCheckResourceAttr(fqrn, "handler.0.payload", "{ \"ref\": \"main\" , \"inputs\": { \"artifact_path\": \"test-repo/repo-path/1\" } }"), + resource.TestCheckResourceAttr(fqrn, "handler.1.url", "https://yahoo.com"), resource.TestCheckResourceAttr(fqrn, "handler.1.secrets.%", "2"), resource.TestCheckResourceAttr(fqrn, "handler.1.secrets.secret3", "value3"), resource.TestCheckResourceAttr(fqrn, "handler.1.secrets.secret4", "value4"), resource.TestCheckResourceAttr(fqrn, "handler.1.http_headers.%", "2"), resource.TestCheckResourceAttr(fqrn, "handler.1.http_headers.header-3", "value-3"), resource.TestCheckResourceAttr(fqrn, "handler.1.http_headers.header-4", "value-4"), - resource.TestCheckResourceAttr(fqrn, "handler.1.payload", "{ \"ref\": \"main\" , \"inputs\": { \"artifact_path\": \"test-repo/repo-path\" } }"), - resource.TestCheckResourceAttr(fqrn, "handler.2.url", "https://google.com"), + resource.TestCheckResourceAttr(fqrn, "handler.1.payload", "{ \"ref\": \"main\" , \"inputs\": { \"artifact_path\": \"test-repo/repo-path/2\" } }"), + resource.TestCheckResourceAttr(fqrn, "handler.2.url", "https://msnbc.com"), resource.TestCheckNoResourceAttr(fqrn, "handler.2.secrets"), resource.TestCheckNoResourceAttr(fqrn, "handler.2.http_headers"), - resource.TestCheckResourceAttr(fqrn, "handler.2.payload", "{ \"ref\": \"main\" , \"inputs\": { \"artifact_path\": \"test-repo/repo-path\" } }"), + resource.TestCheckResourceAttr(fqrn, "handler.2.payload", "{ \"ref\": \"main\" , \"inputs\": { \"artifact_path\": \"test-repo/repo-path/3\" } }"), } for _, eventType := range eventTypes { @@ -132,9 +133,9 @@ func customWebhookTestCase(webhookType string, t *testing.T) (*testing.T, resour } return t, resource.TestCase{ - PreCheck: func() { acctest.PreCheck(t) }, - ProviderFactories: acctest.ProviderFactories, - CheckDestroy: acctest.VerifyDeleted(fqrn, "", testCheckWebhook), + PreCheck: func() { acctest.PreCheck(t) }, + ProtoV6ProviderFactories: acctest.ProtoV6MuxProviderFactories, + CheckDestroy: acctest.VerifyDeleted(fqrn, "key", testCheckWebhook), Steps: []resource.TestStep{ { @@ -142,11 +143,159 @@ func customWebhookTestCase(webhookType string, t *testing.T) (*testing.T, resour Check: resource.ComposeTestCheckFunc(testChecks...), }, { - ResourceName: fqrn, - ImportState: true, - ImportStateVerify: true, - ImportStateCheck: validator.CheckImportState(name, "key"), - ImportStateVerifyIgnore: []string{"handler.0.secrets", "handler.1.secrets"}, + ResourceName: fqrn, + ImportState: true, + ImportStateId: name, + ImportStateVerify: true, + ImportStateVerifyIdentifierAttribute: "key", + ImportStateVerifyIgnore: []string{"handler.0.secrets", "handler.1.secrets"}, + }, + }, + } +} + +func TestAccCustomWebhook_UpgradeFromSDKv2(t *testing.T) { + // Can only realistically test these 3 types of webhook since creating + // build, release_bundle, or distribution in test environment is almost impossible + for _, webhookType := range []string{"artifact", "artifact_property", "docker"} { + t.Run(webhookType, func(t *testing.T) { + resource.Test(customWebhookMigrateFromSDKv2TestCase(webhookType, t)) + }) + } +} + +func customWebhookMigrateFromSDKv2TestCase(webhookType string, t *testing.T) (*testing.T, resource.TestCase) { + id := testutil.RandomInt() + name := fmt.Sprintf("custom-webhook-%d", id) + fqrn := fmt.Sprintf("artifactory_%s_custom_webhook.%s", webhookType, name) + + repoType := domainRepoTypeLookup[webhookType] + repoName := fmt.Sprintf("%s-local-%d", webhookType, id) + eventTypes := webhook.DomainEventTypesSupported[webhookType] + + params := map[string]interface{}{ + "repoName": repoName, + "repoType": repoType, + "webhookType": webhookType, + "webhookName": name, + "eventTypes": eventTypes, + "anyLocal": testutil.RandBool(), + "anyRemote": testutil.RandBool(), + "anyFederated": testutil.RandBool(), + } + config := util.ExecuteTemplate("TestAccWebhook{{ .webhookType }}Type", ` + resource "artifactory_local_{{ .repoType }}_repository" "{{ .repoName }}" { + key = "{{ .repoName }}" + } + + resource "artifactory_{{ .webhookType }}_custom_webhook" "{{ .webhookName }}" { + key = "{{ .webhookName }}" + description = "test description" + event_types = [{{ range $index, $eventType := .eventTypes}}{{if $index}},{{end}}"{{$eventType}}"{{end}}] + criteria { + any_local = {{ .anyLocal }} + any_remote = {{ .anyRemote }} + any_federated = {{ .anyFederated }} + repo_keys = ["{{ .repoName }}"] + include_patterns = ["foo/**"] + exclude_patterns = ["bar/**"] + } + handler { + url = "https://google.com" + secrets = { + secret1 = "value1" + secret2 = "value2" + } + http_headers = { + header-1 = "value-1" + header-2 = "value-2" + } + payload = "{ \"ref\": \"main\" , \"inputs\": { \"artifact_path\": \"test-repo/repo-path/1\" } }" + } + handler { + url = "https://yahoo.com" + secrets = { + secret3 = "value3" + secret4 = "value4" + } + http_headers = { + header-3 = "value-3" + header-4 = "value-4" + } + payload = "{ \"ref\": \"main\" , \"inputs\": { \"artifact_path\": \"test-repo/repo-path/2\" } }" + } + handler { + url = "https://msnbc.com" + payload = "{ \"ref\": \"main\" , \"inputs\": { \"artifact_path\": \"test-repo/repo-path/3\" } }" + } + + depends_on = [artifactory_local_{{ .repoType }}_repository.{{ .repoName }}] + } + `, params) + + testChecks := []resource.TestCheckFunc{ + resource.TestCheckResourceAttr(fqrn, "key", name), + resource.TestCheckResourceAttr(fqrn, "event_types.#", fmt.Sprintf("%d", len(eventTypes))), + resource.TestCheckResourceAttr(fqrn, "criteria.#", "1"), + resource.TestCheckResourceAttr(fqrn, "criteria.0.any_local", fmt.Sprintf("%t", params["anyLocal"])), + resource.TestCheckResourceAttr(fqrn, "criteria.0.any_remote", fmt.Sprintf("%t", params["anyRemote"])), + resource.TestCheckResourceAttr(fqrn, "criteria.0.any_federated", fmt.Sprintf("%t", params["anyFederated"])), + resource.TestCheckTypeSetElemAttr(fqrn, "criteria.0.repo_keys.*", repoName), + resource.TestCheckResourceAttr(fqrn, "criteria.0.include_patterns.#", "1"), + resource.TestCheckResourceAttr(fqrn, "criteria.0.include_patterns.0", "foo/**"), + resource.TestCheckResourceAttr(fqrn, "criteria.0.exclude_patterns.#", "1"), + resource.TestCheckResourceAttr(fqrn, "criteria.0.exclude_patterns.0", "bar/**"), + resource.TestCheckResourceAttr(fqrn, "handler.#", "3"), + resource.TestCheckResourceAttr(fqrn, "handler.0.url", "https://google.com"), + resource.TestCheckResourceAttr(fqrn, "handler.0.secrets.%", "2"), + resource.TestCheckResourceAttr(fqrn, "handler.0.secrets.secret1", "value1"), + resource.TestCheckResourceAttr(fqrn, "handler.0.secrets.secret2", "value2"), + resource.TestCheckResourceAttr(fqrn, "handler.0.http_headers.%", "2"), + resource.TestCheckResourceAttr(fqrn, "handler.0.http_headers.header-1", "value-1"), + resource.TestCheckResourceAttr(fqrn, "handler.0.http_headers.header-2", "value-2"), + resource.TestCheckResourceAttr(fqrn, "handler.0.payload", "{ \"ref\": \"main\" , \"inputs\": { \"artifact_path\": \"test-repo/repo-path/1\" } }"), + resource.TestCheckResourceAttr(fqrn, "handler.1.url", "https://yahoo.com"), + resource.TestCheckResourceAttr(fqrn, "handler.1.secrets.%", "2"), + resource.TestCheckResourceAttr(fqrn, "handler.1.secrets.secret3", "value3"), + resource.TestCheckResourceAttr(fqrn, "handler.1.secrets.secret4", "value4"), + resource.TestCheckResourceAttr(fqrn, "handler.1.http_headers.%", "2"), + resource.TestCheckResourceAttr(fqrn, "handler.1.http_headers.header-3", "value-3"), + resource.TestCheckResourceAttr(fqrn, "handler.1.http_headers.header-4", "value-4"), + resource.TestCheckResourceAttr(fqrn, "handler.1.payload", "{ \"ref\": \"main\" , \"inputs\": { \"artifact_path\": \"test-repo/repo-path/2\" } }"), + resource.TestCheckResourceAttr(fqrn, "handler.2.url", "https://msnbc.com"), + resource.TestCheckNoResourceAttr(fqrn, "handler.2.secrets"), + resource.TestCheckNoResourceAttr(fqrn, "handler.2.http_headers"), + resource.TestCheckResourceAttr(fqrn, "handler.2.payload", "{ \"ref\": \"main\" , \"inputs\": { \"artifact_path\": \"test-repo/repo-path/3\" } }"), + } + + for _, eventType := range eventTypes { + eventTypeCheck := resource.TestCheckTypeSetElemAttr(fqrn, "event_types.*", eventType) + testChecks = append(testChecks, eventTypeCheck) + } + + return t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + CheckDestroy: acctest.VerifyDeleted(fqrn, "key", testCheckWebhook), + + Steps: []resource.TestStep{ + { + Config: config, + ExternalProviders: map[string]resource.ExternalProvider{ + "artifactory": { + Source: "jfrog/artifactory", + VersionConstraint: "12.1.0", + }, + }, + Check: resource.ComposeTestCheckFunc(testChecks...), + }, + { + Config: config, + ProtoV6ProviderFactories: acctest.ProtoV6MuxProviderFactories, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectEmptyPlan(), + }, + }, }, }, } diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_webhook.go b/pkg/artifactory/resource/webhook/resource_artifactory_webhook.go index 7c9dd46f..a6abf9c5 100644 --- a/pkg/artifactory/resource/webhook/resource_artifactory_webhook.go +++ b/pkg/artifactory/resource/webhook/resource_artifactory_webhook.go @@ -142,12 +142,7 @@ func (r *WebhookResource) CreateSchema(domain string, criteriaBlock *schema.SetN } if criteriaBlock != nil { - blocks = lo.Assign( - blocks, - map[string]schema.Block{ - "criteria": *criteriaBlock, - }, - ) + blocks["criteria"] = *criteriaBlock } return schema.Schema{ @@ -207,9 +202,9 @@ func (r *WebhookResource) Configure(ctx context.Context, req resource.ConfigureR r.ProviderData = req.ProviderData.(util.ProviderMetadata) } -func (r *WebhookResource) Create(ctx context.Context, webhook WebhookAPIModel, req resource.CreateRequest, resp *resource.CreateResponse) { +func createWebhook[V WebhookAPIModel | CustomWebhookAPIModel](client *resty.Client, webhook V, req resource.CreateRequest, resp *resource.CreateResponse) { var artifactoryError artifactory.ArtifactoryErrorsResponse - response, err := r.ProviderData.Client.R(). + response, err := client.R(). SetBody(webhook). SetError(&artifactoryError). AddRetryCondition(retryOnProxyError). @@ -226,9 +221,13 @@ func (r *WebhookResource) Create(ctx context.Context, webhook WebhookAPIModel, r } } -func (r *WebhookResource) Read(ctx context.Context, key string, webhook *WebhookAPIModel, req resource.ReadRequest, resp *resource.ReadResponse) (found bool) { +func (r *WebhookResource) Create(_ context.Context, webhook WebhookAPIModel, req resource.CreateRequest, resp *resource.CreateResponse) { + createWebhook(r.ProviderData.Client, webhook, req, resp) +} + +func readWebhook[V WebhookAPIModel | CustomWebhookAPIModel](ctx context.Context, client *resty.Client, key string, webhook *V, req resource.ReadRequest, resp *resource.ReadResponse) (found bool) { var artifactoryError artifactory.ArtifactoryErrorsResponse - response, err := r.ProviderData.Client.R(). + response, err := client.R(). SetPathParam("webhookKey", key). SetResult(&webhook). SetError(&artifactoryError). @@ -251,9 +250,13 @@ func (r *WebhookResource) Read(ctx context.Context, key string, webhook *Webhook return true } -func (r *WebhookResource) Update(ctx context.Context, key string, webhook WebhookAPIModel, req resource.UpdateRequest, resp *resource.UpdateResponse) { +func (r *WebhookResource) Read(ctx context.Context, key string, webhook *WebhookAPIModel, req resource.ReadRequest, resp *resource.ReadResponse) (found bool) { + return readWebhook(ctx, r.ProviderData.Client, key, webhook, req, resp) +} + +func updateWebhook[V WebhookAPIModel | CustomWebhookAPIModel](client *resty.Client, key string, webhook V, req resource.UpdateRequest, resp *resource.UpdateResponse) { var artifactoryError artifactory.ArtifactoryErrorsResponse - response, err := r.ProviderData.Client.R(). + response, err := client.R(). SetPathParam("webhookKey", key). SetBody(webhook). AddRetryCondition(retryOnProxyError). @@ -271,6 +274,10 @@ func (r *WebhookResource) Update(ctx context.Context, key string, webhook Webhoo } } +func (r *WebhookResource) Update(_ context.Context, key string, webhook WebhookAPIModel, req resource.UpdateRequest, resp *resource.UpdateResponse) { + updateWebhook(r.ProviderData.Client, key, webhook, req, resp) +} + func (r *WebhookResource) Delete(ctx context.Context, key string, req resource.DeleteRequest, resp *resource.DeleteResponse) { var artifactoryError artifactory.ArtifactoryErrorsResponse response, err := r.ProviderData.Client.R(). @@ -546,10 +553,6 @@ type WebhookAPIModel struct { Handlers []HandlerAPIModel `json:"handlers"` } -func (w WebhookAPIModel) Id() string { - return w.Key -} - type EventFilterAPIModel struct { Domain string `json:"domain"` EventTypes []string `json:"event_types"` From 54e29a92bd5f2975038c18667576359236bf8b38 Mon Sep 17 00:00:00 2001 From: Alex Hung Date: Mon, 30 Sep 2024 16:25:47 -0700 Subject: [PATCH 03/12] Migrate build custom webhook resource to Plugin Framework Refactor common funcs --- ...resource_artifactory_artifact_lifecycle.go | 14 +- .../resource_artifactory_custom_webhook.go | 86 ++----- ...source_artifactory_custom_webhook_build.go | 215 ++++++++++++++++++ ...esource_artifactory_custom_webhook_repo.go | 91 +------- ...esource_artifactory_custom_webhook_test.go | 46 ++++ .../webhook/resource_artifactory_webhook.go | 168 +++++++------- .../resource_artifactory_webhook_build.go | 103 +++++---- ...urce_artifactory_webhook_release_bundle.go | 8 +- ...e_artifactory_webhook_release_bundle_v2.go | 8 +- ...ory_webhook_release_bundle_v2_promotion.go | 8 +- .../resource_artifactory_webhook_repo.go | 129 ++++++----- .../resource_artifactory_webhook_user.go | 14 +- 12 files changed, 525 insertions(+), 365 deletions(-) create mode 100644 pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook_build.go diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_artifact_lifecycle.go b/pkg/artifactory/resource/webhook/resource_artifactory_artifact_lifecycle.go index ed9a0225..7d3f2e82 100644 --- a/pkg/artifactory/resource/webhook/resource_artifactory_artifact_lifecycle.go +++ b/pkg/artifactory/resource/webhook/resource_artifactory_artifact_lifecycle.go @@ -23,7 +23,7 @@ func NewArtifactLifecycleWebhookResource() resource.Resource { } type ArtifactLifecycleWebhookResourceModel struct { - WebhookNoCriteriaResourceModel + WebhookBaseResourceModel } type ArtifactLifecycleWebhookResource struct { @@ -59,7 +59,7 @@ func (r *ArtifactLifecycleWebhookResource) Create(ctx context.Context, req resou return } - r.WebhookResource.Create(ctx, webhook, req, resp) + r.WebhookResource.Create(ctx, webhook, resp) if resp.Diagnostics.HasError() { return } @@ -80,7 +80,7 @@ func (r *ArtifactLifecycleWebhookResource) Read(ctx context.Context, req resourc } var webhook WebhookAPIModel - found := r.WebhookResource.Read(ctx, state.Key.ValueString(), &webhook, req, resp) + found := r.WebhookResource.Read(ctx, state.Key.ValueString(), &webhook, resp) if resp.Diagnostics.HasError() { return } @@ -115,7 +115,7 @@ func (r *ArtifactLifecycleWebhookResource) Update(ctx context.Context, req resou return } - r.WebhookResource.Update(ctx, plan.Key.ValueString(), webhook, req, resp) + r.WebhookResource.Update(ctx, plan.Key.ValueString(), webhook, resp) if resp.Diagnostics.HasError() { return } @@ -132,7 +132,7 @@ func (r *ArtifactLifecycleWebhookResource) Delete(ctx context.Context, req resou // Read Terraform prior state data into the model resp.Diagnostics.Append(req.State.Get(ctx, &state)...) - r.WebhookResource.Delete(ctx, state.Key.ValueString(), req, resp) + r.WebhookResource.Delete(ctx, state.Key.ValueString(), resp) if resp.Diagnostics.HasError() { return } @@ -147,7 +147,7 @@ func (r *ArtifactLifecycleWebhookResource) ImportState(ctx context.Context, req } func (m ArtifactLifecycleWebhookResourceModel) toAPIModel(ctx context.Context, domain string, apiModel *WebhookAPIModel) (diags diag.Diagnostics) { - d := m.WebhookNoCriteriaResourceModel.toAPIModel(ctx, domain, apiModel) + d := m.WebhookBaseResourceModel.toAPIModel(ctx, domain, apiModel) if d.HasError() { diags.Append(d...) } @@ -158,7 +158,7 @@ func (m ArtifactLifecycleWebhookResourceModel) toAPIModel(ctx context.Context, d func (m *ArtifactLifecycleWebhookResourceModel) fromAPIModel(ctx context.Context, apiModel WebhookAPIModel, stateHandlers basetypes.SetValue) diag.Diagnostics { diags := diag.Diagnostics{} - d := m.WebhookNoCriteriaResourceModel.fromAPIModel(ctx, apiModel, stateHandlers) + d := m.WebhookBaseResourceModel.fromAPIModel(ctx, apiModel, stateHandlers) if d.HasError() { diags.Append(d...) } diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook.go b/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook.go index fe12562f..603a14f9 100644 --- a/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook.go +++ b/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook.go @@ -90,35 +90,27 @@ var customHandlerBlock = schema.SetNestedBlock{ }, } -func (r *CustomWebhookResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { - r.WebhookResource.Metadata(ctx, req, resp) -} - -func (r *CustomWebhookResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { - r.WebhookResource.Configure(ctx, req, resp) -} - func (r *CustomWebhookResource) CreateSchema(domain string, criteriaBlock *schema.SetNestedBlock) schema.Schema { return r.WebhookResource.CreateSchema(domain, criteriaBlock, customHandlerBlock) } -func (r *CustomWebhookResource) Create(ctx context.Context, webhook CustomWebhookAPIModel, req resource.CreateRequest, resp *resource.CreateResponse) { - createWebhook(r.ProviderData.Client, webhook, req, resp) +func (r *CustomWebhookResource) Create(ctx context.Context, webhook CustomWebhookAPIModel, resp *resource.CreateResponse) { + createWebhook(r.ProviderData.Client, webhook, resp) } -func (r *CustomWebhookResource) Read(ctx context.Context, key string, webhook *CustomWebhookAPIModel, req resource.ReadRequest, resp *resource.ReadResponse) (found bool) { - return readWebhook(ctx, r.ProviderData.Client, key, webhook, req, resp) +func (r *CustomWebhookResource) Read(ctx context.Context, key string, webhook *CustomWebhookAPIModel, resp *resource.ReadResponse) (found bool) { + return readWebhook(ctx, r.ProviderData.Client, key, webhook, resp) } -func (r *CustomWebhookResource) Update(_ context.Context, key string, webhook CustomWebhookAPIModel, req resource.UpdateRequest, resp *resource.UpdateResponse) { - updateWebhook(r.ProviderData.Client, key, webhook, req, resp) +func (r *CustomWebhookResource) Update(_ context.Context, key string, webhook CustomWebhookAPIModel, resp *resource.UpdateResponse) { + updateWebhook(r.ProviderData.Client, key, webhook, resp) } -type CustomWebhookNoCriteriaResourceModel struct { - WebhookNoCriteriaResourceModel +type CustomWebhookBaseResourceModel struct { + WebhookBaseResourceModel } -func (m CustomWebhookNoCriteriaResourceModel) toAPIModel(ctx context.Context, domain string, apiModel *CustomWebhookAPIModel) (diags diag.Diagnostics) { +func (m CustomWebhookBaseResourceModel) toAPIModel(ctx context.Context, domain string, apiModel *CustomWebhookAPIModel) (diags diag.Diagnostics) { var eventTypes []string d := m.EventTypes.ElementsAs(ctx, &eventTypes, false) if d.HasError() { @@ -189,7 +181,7 @@ var customHandlerSetResourceModelElementTypes = types.ObjectType{ AttrTypes: customHandlerSetResourceModelAttributeTypes, } -func (m *CustomWebhookNoCriteriaResourceModel) fromAPIModel(ctx context.Context, apiModel CustomWebhookAPIModel, stateHandlers basetypes.SetValue) diag.Diagnostics { +func (m *CustomWebhookBaseResourceModel) fromAPIModel(ctx context.Context, apiModel CustomWebhookAPIModel, stateHandlers basetypes.SetValue) diag.Diagnostics { diags := diag.Diagnostics{} m.Key = types.StringValue(apiModel.Key) @@ -278,74 +270,24 @@ func (m *CustomWebhookNoCriteriaResourceModel) fromAPIModel(ctx context.Context, } type CustomWebhookResourceModel struct { - CustomWebhookNoCriteriaResourceModel - Criteria types.Set `tfsdk:"criteria"` -} - -func (m *CustomWebhookResourceModel) toBaseCriteriaAPIModel(ctx context.Context, criteriaAttrs map[string]attr.Value) (BaseCriteriaAPIModel, diag.Diagnostics) { - diags := diag.Diagnostics{} - - var includePatterns []string - d := criteriaAttrs["include_patterns"].(types.Set).ElementsAs(ctx, &includePatterns, false) - if d.HasError() { - diags.Append(d...) - } - - var excludePatterns []string - d = criteriaAttrs["exclude_patterns"].(types.Set).ElementsAs(ctx, &excludePatterns, false) - if d.HasError() { - diags.Append(d...) - } - - return BaseCriteriaAPIModel{ - IncludePatterns: includePatterns, - ExcludePatterns: excludePatterns, - }, diags + CustomWebhookBaseResourceModel + WebhookCriteriaResourceModel } func (m CustomWebhookResourceModel) toAPIModel(ctx context.Context, domain string, criteriaAPIModel interface{}, apiModel *CustomWebhookAPIModel) (diags diag.Diagnostics) { - d := m.CustomWebhookNoCriteriaResourceModel.toAPIModel(ctx, domain, apiModel) + d := m.CustomWebhookBaseResourceModel.toAPIModel(ctx, domain, apiModel) apiModel.EventFilter.Criteria = criteriaAPIModel return d } -func (m *CustomWebhookResourceModel) fromBaseCriteriaAPIModel(ctx context.Context, criteriaAPIModel map[string]interface{}) (map[string]attr.Value, diag.Diagnostics) { - diags := diag.Diagnostics{} - - includePatterns := types.SetNull(types.StringType) - if v, ok := criteriaAPIModel["includePatterns"]; ok && v != nil { - ps, d := types.SetValueFrom(ctx, types.StringType, v) - if d.HasError() { - diags.Append(d...) - } - - includePatterns = ps - } - - excludePatterns := types.SetNull(types.StringType) - if v, ok := criteriaAPIModel["excludePatterns"]; ok && v != nil { - ps, d := types.SetValueFrom(ctx, types.StringType, v) - if d.HasError() { - diags.Append(d...) - } - - excludePatterns = ps - } - - return map[string]attr.Value{ - "include_patterns": includePatterns, - "exclude_patterns": excludePatterns, - }, diags -} - func (m *CustomWebhookResourceModel) fromAPIModel(ctx context.Context, apiModel CustomWebhookAPIModel, stateHandlers basetypes.SetValue, criteriaSet *basetypes.SetValue) diag.Diagnostics { if criteriaSet != nil { m.Criteria = *criteriaSet } - return m.CustomWebhookNoCriteriaResourceModel.fromAPIModel(ctx, apiModel, stateHandlers) + return m.CustomWebhookBaseResourceModel.fromAPIModel(ctx, apiModel, stateHandlers) } var DomainSupported = []string{ diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook_build.go b/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook_build.go new file mode 100644 index 00000000..2aaa28ab --- /dev/null +++ b/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook_build.go @@ -0,0 +1,215 @@ +package webhook + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + "github.com/jfrog/terraform-provider-shared/util" +) + +var _ resource.Resource = &BuildCustomWebhookResource{} + +func NewCustomBuildWebhookResource() resource.Resource { + return &BuildCustomWebhookResource{ + CustomWebhookResource: CustomWebhookResource{ + WebhookResource: WebhookResource{ + TypeName: fmt.Sprintf("artifactory_%s_custom_webhook", BuildDomain), + Domain: BuildDomain, + Description: "Provides a build webhook resource. This can be used to register and manage Artifactory webhook subscription which enables you to be notified or notify other users when such events take place in Artifactory.", + }, + }, + } +} + +type BuildCustomWebhookResourceModel struct { + CustomWebhookResourceModel +} + +type BuildCustomWebhookResource struct { + CustomWebhookResource +} + +func (r *BuildCustomWebhookResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + r.WebhookResource.Metadata(ctx, req, resp) +} + +func (r *BuildCustomWebhookResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = r.CreateSchema(r.Domain, &buildCriteriaBlock) +} + +func (r *BuildCustomWebhookResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + r.WebhookResource.Configure(ctx, req, resp) +} + +func (r BuildCustomWebhookResource) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) { + var data BuildCustomWebhookResourceModel + + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + criteriaObj := data.Criteria.Elements()[0].(types.Object) + criteriaAttrs := criteriaObj.Attributes() + + anyBuild := criteriaAttrs["any_build"].(types.Bool).ValueBool() + + if !anyBuild && len(criteriaAttrs["selected_builds"].(types.Set).Elements()) == 0 && len(criteriaAttrs["include_patterns"].(types.Set).Elements()) == 0 { + resp.Diagnostics.AddAttributeError( + path.Root("criteria").AtSetValue(criteriaObj).AtName("any_build"), + "Invalid Attribute Configuration", + "selected_builds or include_patterns cannot be empty when any_build is false", + ) + } +} + +func (r *BuildCustomWebhookResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + go util.SendUsageResourceCreate(ctx, r.ProviderData.Client.R(), r.ProviderData.ProductId, r.TypeName) + + var plan BuildCustomWebhookResourceModel + + // Read Terraform plan data into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + var webhook CustomWebhookAPIModel + resp.Diagnostics.Append(plan.toAPIModel(ctx, r.Domain, &webhook)...) + if resp.Diagnostics.HasError() { + return + } + + r.CustomWebhookResource.Create(ctx, webhook, resp) + if resp.Diagnostics.HasError() { + return + } + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *BuildCustomWebhookResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + go util.SendUsageResourceRead(ctx, r.ProviderData.Client.R(), r.ProviderData.ProductId, r.TypeName) + + var state BuildCustomWebhookResourceModel + + // Read Terraform state data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + var webhook CustomWebhookAPIModel + found := r.CustomWebhookResource.Read(ctx, state.Key.ValueString(), &webhook, resp) + if resp.Diagnostics.HasError() { + return + } + + if !found { + return + } + + resp.Diagnostics.Append(state.fromAPIModel(ctx, webhook, state.Handlers)...) + if resp.Diagnostics.HasError() { + return + } + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *BuildCustomWebhookResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + go util.SendUsageResourceUpdate(ctx, r.ProviderData.Client.R(), r.ProviderData.ProductId, r.TypeName) + + var plan BuildCustomWebhookResourceModel + + // Read Terraform plan data into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + var webhook CustomWebhookAPIModel + resp.Diagnostics.Append(plan.toAPIModel(ctx, r.Domain, &webhook)...) + if resp.Diagnostics.HasError() { + return + } + + r.CustomWebhookResource.Update(ctx, plan.Key.ValueString(), webhook, resp) + if resp.Diagnostics.HasError() { + return + } + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *BuildCustomWebhookResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + go util.SendUsageResourceDelete(ctx, r.ProviderData.Client.R(), r.ProviderData.ProductId, r.TypeName) + + var state BuildWebhookResourceModel + + // Read Terraform prior state data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + + r.WebhookResource.Delete(ctx, state.Key.ValueString(), resp) + if resp.Diagnostics.HasError() { + return + } + + // If the logic reaches here, it implicitly succeeded and will remove + // the resource from state if there are no other errors. +} + +// ImportState imports the resource into the Terraform state. +func (r *BuildCustomWebhookResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + r.WebhookResource.ImportState(ctx, req, resp) +} + +func (m BuildCustomWebhookResourceModel) toAPIModel(ctx context.Context, domain string, apiModel *CustomWebhookAPIModel) (diags diag.Diagnostics) { + critieriaObj := m.Criteria.Elements()[0].(types.Object) + critieriaAttrs := critieriaObj.Attributes() + + baseCriteria, d := m.CustomWebhookResourceModel.toBaseCriteriaAPIModel(ctx, critieriaAttrs) + if d.HasError() { + diags.Append(d...) + } + + criteriaAPIModel, d := toBuildCriteriaAPIModel(ctx, baseCriteria, critieriaAttrs) + if d.HasError() { + diags.Append(d...) + } + + d = m.CustomWebhookResourceModel.toAPIModel(ctx, domain, criteriaAPIModel, apiModel) + if d.HasError() { + diags.Append(d...) + } + + return +} + +func (m *BuildCustomWebhookResourceModel) fromAPIModel(ctx context.Context, apiModel CustomWebhookAPIModel, stateHandlers basetypes.SetValue) diag.Diagnostics { + diags := diag.Diagnostics{} + + criteriaAPIModel := apiModel.EventFilter.Criteria.(map[string]interface{}) + + baseCriteriaAttrs, d := m.CustomWebhookResourceModel.fromBaseCriteriaAPIModel(ctx, criteriaAPIModel) + + criteriaSet, d := fromBuildAPIModel(ctx, criteriaAPIModel, baseCriteriaAttrs) + if d.HasError() { + diags.Append(d...) + } + + d = m.CustomWebhookResourceModel.fromAPIModel(ctx, apiModel, stateHandlers, &criteriaSet) + if d.HasError() { + diags.Append(d...) + } + + return diags +} diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook_repo.go b/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook_repo.go index 47a26b06..1f59d477 100644 --- a/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook_repo.go +++ b/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook_repo.go @@ -4,17 +4,12 @@ import ( "context" "fmt" - "github.com/hashicorp/terraform-plugin-framework-validators/setvalidator" - "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" - "github.com/hashicorp/terraform-plugin-framework/resource/schema" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" "github.com/jfrog/terraform-provider-shared/util" - "github.com/samber/lo" ) var _ resource.Resource = &RepoCustomWebhookResource{} @@ -68,39 +63,7 @@ func (r *RepoCustomWebhookResource) Metadata(ctx context.Context, req resource.M } func (r *RepoCustomWebhookResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { - criteriaBlock := schema.SetNestedBlock{ - NestedObject: schema.NestedBlockObject{ - Attributes: lo.Assign( - patternsSchemaAttributes("Simple comma separated wildcard patterns for repository artifact paths (with no leading slash).\nAnt-style path expressions are supported (*, **, ?).\nFor example: `org/apache/**`"), - map[string]schema.Attribute{ - "any_local": schema.BoolAttribute{ - Required: true, - Description: "Trigger on any local repositories", - }, - "any_remote": schema.BoolAttribute{ - Required: true, - Description: "Trigger on any remote repositories", - }, - "any_federated": schema.BoolAttribute{ - Required: true, - Description: "Trigger on any federated repositories", - }, - "repo_keys": schema.SetAttribute{ - ElementType: types.StringType, - Required: true, - Description: "Trigger on this list of repository keys", - }, - }, - ), - }, - Validators: []validator.Set{ - setvalidator.SizeBetween(1, 1), - setvalidator.IsRequired(), - }, - Description: "Specifies where the webhook will be applied on which repositories.", - } - - resp.Schema = r.CreateSchema(r.Domain, &criteriaBlock) + resp.Schema = r.CreateSchema(r.Domain, &repoCriteriaBlock) } func (r *RepoCustomWebhookResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { @@ -148,7 +111,7 @@ func (r *RepoCustomWebhookResource) Create(ctx context.Context, req resource.Cre return } - r.CustomWebhookResource.Create(ctx, webhook, req, resp) + r.CustomWebhookResource.Create(ctx, webhook, resp) if resp.Diagnostics.HasError() { return } @@ -169,7 +132,7 @@ func (r *RepoCustomWebhookResource) Read(ctx context.Context, req resource.ReadR } var webhook CustomWebhookAPIModel - found := r.CustomWebhookResource.Read(ctx, state.Key.ValueString(), &webhook, req, resp) + found := r.CustomWebhookResource.Read(ctx, state.Key.ValueString(), &webhook, resp) if resp.Diagnostics.HasError() { return } @@ -204,7 +167,7 @@ func (r *RepoCustomWebhookResource) Update(ctx context.Context, req resource.Upd return } - r.CustomWebhookResource.Update(ctx, plan.Key.ValueString(), webhook, req, resp) + r.CustomWebhookResource.Update(ctx, plan.Key.ValueString(), webhook, resp) if resp.Diagnostics.HasError() { return } @@ -221,7 +184,7 @@ func (r *RepoCustomWebhookResource) Delete(ctx context.Context, req resource.Del // Read Terraform prior state data into the model resp.Diagnostics.Append(req.State.Get(ctx, &state)...) - r.WebhookResource.Delete(ctx, state.Key.ValueString(), req, resp) + r.WebhookResource.Delete(ctx, state.Key.ValueString(), resp) if resp.Diagnostics.HasError() { return } @@ -244,19 +207,7 @@ func (m RepoCustomWebhookResourceModel) toAPIModel(ctx context.Context, domain s diags.Append(d...) } - var repoKeys []string - d = critieriaAttrs["repo_keys"].(types.Set).ElementsAs(ctx, &repoKeys, false) - if d.HasError() { - diags.Append(d...) - } - - criteriaAPIModel := RepoCriteriaAPIModel{ - BaseCriteriaAPIModel: baseCriteria, - AnyLocal: critieriaAttrs["any_local"].(types.Bool).ValueBool(), - AnyRemote: critieriaAttrs["any_remote"].(types.Bool).ValueBool(), - AnyFederated: critieriaAttrs["any_federated"].(types.Bool).ValueBool(), - RepoKeys: repoKeys, - } + criteriaAPIModel, d := toRepoCriteriaAPIModel(ctx, baseCriteria, critieriaAttrs) d = m.CustomWebhookResourceModel.toAPIModel(ctx, domain, criteriaAPIModel, apiModel) if d.HasError() { @@ -273,35 +224,7 @@ func (m *RepoCustomWebhookResourceModel) fromAPIModel(ctx context.Context, apiMo baseCriteriaAttrs, d := m.CustomWebhookResourceModel.fromBaseCriteriaAPIModel(ctx, criteriaAPIModel) - repoKeys := types.SetNull(types.StringType) - if v, ok := criteriaAPIModel["repoKeys"]; ok && v != nil { - ks, d := types.SetValueFrom(ctx, types.StringType, v) - if d.HasError() { - diags.Append(d...) - } - - repoKeys = ks - } - - criteria, d := types.ObjectValue( - repoCriteriaSetResourceModelAttributeTypes, - lo.Assign( - baseCriteriaAttrs, - map[string]attr.Value{ - "any_local": types.BoolValue(criteriaAPIModel["anyLocal"].(bool)), - "any_remote": types.BoolValue(criteriaAPIModel["anyRemote"].(bool)), - "any_federated": types.BoolValue(criteriaAPIModel["anyFederated"].(bool)), - "repo_keys": repoKeys, - }, - ), - ) - if d.HasError() { - diags.Append(d...) - } - criteriaSet, d := types.SetValue( - repoCriteriaSetResourceModelElementTypes, - []attr.Value{criteria}, - ) + criteriaSet, d := fromRepoCriteriaAPIMode(ctx, criteriaAPIModel, baseCriteriaAttrs) if d.HasError() { diags.Append(d...) } diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook_test.go b/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook_test.go index 6464588a..5a7fd0aa 100644 --- a/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook_test.go +++ b/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook_test.go @@ -2,6 +2,7 @@ package webhook_test import ( "fmt" + "regexp" "testing" "github.com/hashicorp/terraform-plugin-testing/helper/resource" @@ -13,6 +14,51 @@ import ( "github.com/jfrog/terraform-provider-shared/validator" ) +func TestAccCustomWebhook_CriteriaValidation(t *testing.T) { + for _, webhookType := range []string{webhook.ArtifactDomain, webhook.ArtifactPropertyDomain, webhook.ArtifactoryReleaseBundleDomain, webhook.BuildDomain, webhook.DestinationDomain, webhook.DistributionDomain, webhook.DockerDomain, webhook.ReleaseBundleDomain, webhook.ReleaseBundleV2Domain} { + t.Run(webhookType, func(t *testing.T) { + resource.Test(customWebhookCriteriaValidationTestCase(webhookType, t)) + }) + } +} + +func customWebhookCriteriaValidationTestCase(webhookType string, t *testing.T) (*testing.T, resource.TestCase) { + id := testutil.RandomInt() + name := fmt.Sprintf("webhook-%d", id) + fqrn := fmt.Sprintf("artifactory_%s_custom_webhook.%s", webhookType, name) + + var template string + switch webhookType { + case "artifact", "artifact_property", "docker": + template = repoTemplate + case "build": + template = buildTemplate + case "release_bundle", "distribution", "artifactory_release_bundle", "destination": + template = releaseBundleTemplate + case "release_bundle_v2": + template = releaseBundleV2Template + } + + params := map[string]interface{}{ + "webhookType": webhookType, + "webhookName": name, + "eventTypes": webhook.DomainEventTypesSupported[webhookType], + } + webhookConfig := util.ExecuteTemplate("TestAccCustomWebhookCriteriaValidation", template, params) + + return t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ProtoV6ProviderFactories: acctest.ProtoV6MuxProviderFactories, + CheckDestroy: acctest.VerifyDeleted(fqrn, "key", acctest.CheckRepo), + Steps: []resource.TestStep{ + { + Config: webhookConfig, + ExpectError: regexp.MustCompile(domainValidationErrorMessageLookup[webhookType]), + }, + }, + } +} + func TestAccCustomWebhook_AllTypes(t *testing.T) { // Can only realistically test these 3 types of webhook since creating // build, release_bundle, or distribution in test environment is almost impossible diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_webhook.go b/pkg/artifactory/resource/webhook/resource_artifactory_webhook.go index a6abf9c5..423b016e 100644 --- a/pkg/artifactory/resource/webhook/resource_artifactory_webhook.go +++ b/pkg/artifactory/resource/webhook/resource_artifactory_webhook.go @@ -202,7 +202,7 @@ func (r *WebhookResource) Configure(ctx context.Context, req resource.ConfigureR r.ProviderData = req.ProviderData.(util.ProviderMetadata) } -func createWebhook[V WebhookAPIModel | CustomWebhookAPIModel](client *resty.Client, webhook V, req resource.CreateRequest, resp *resource.CreateResponse) { +func createWebhook[V WebhookAPIModel | CustomWebhookAPIModel](client *resty.Client, webhook V, resp *resource.CreateResponse) { var artifactoryError artifactory.ArtifactoryErrorsResponse response, err := client.R(). SetBody(webhook). @@ -221,11 +221,11 @@ func createWebhook[V WebhookAPIModel | CustomWebhookAPIModel](client *resty.Clie } } -func (r *WebhookResource) Create(_ context.Context, webhook WebhookAPIModel, req resource.CreateRequest, resp *resource.CreateResponse) { - createWebhook(r.ProviderData.Client, webhook, req, resp) +func (r *WebhookResource) Create(_ context.Context, webhook WebhookAPIModel, resp *resource.CreateResponse) { + createWebhook(r.ProviderData.Client, webhook, resp) } -func readWebhook[V WebhookAPIModel | CustomWebhookAPIModel](ctx context.Context, client *resty.Client, key string, webhook *V, req resource.ReadRequest, resp *resource.ReadResponse) (found bool) { +func readWebhook[V WebhookAPIModel | CustomWebhookAPIModel](ctx context.Context, client *resty.Client, key string, webhook *V, resp *resource.ReadResponse) (found bool) { var artifactoryError artifactory.ArtifactoryErrorsResponse response, err := client.R(). SetPathParam("webhookKey", key). @@ -250,11 +250,11 @@ func readWebhook[V WebhookAPIModel | CustomWebhookAPIModel](ctx context.Context, return true } -func (r *WebhookResource) Read(ctx context.Context, key string, webhook *WebhookAPIModel, req resource.ReadRequest, resp *resource.ReadResponse) (found bool) { - return readWebhook(ctx, r.ProviderData.Client, key, webhook, req, resp) +func (r *WebhookResource) Read(ctx context.Context, key string, webhook *WebhookAPIModel, resp *resource.ReadResponse) (found bool) { + return readWebhook(ctx, r.ProviderData.Client, key, webhook, resp) } -func updateWebhook[V WebhookAPIModel | CustomWebhookAPIModel](client *resty.Client, key string, webhook V, req resource.UpdateRequest, resp *resource.UpdateResponse) { +func updateWebhook[V WebhookAPIModel | CustomWebhookAPIModel](client *resty.Client, key string, webhook V, resp *resource.UpdateResponse) { var artifactoryError artifactory.ArtifactoryErrorsResponse response, err := client.R(). SetPathParam("webhookKey", key). @@ -274,11 +274,11 @@ func updateWebhook[V WebhookAPIModel | CustomWebhookAPIModel](client *resty.Clie } } -func (r *WebhookResource) Update(_ context.Context, key string, webhook WebhookAPIModel, req resource.UpdateRequest, resp *resource.UpdateResponse) { - updateWebhook(r.ProviderData.Client, key, webhook, req, resp) +func (r *WebhookResource) Update(_ context.Context, key string, webhook WebhookAPIModel, resp *resource.UpdateResponse) { + updateWebhook(r.ProviderData.Client, key, webhook, resp) } -func (r *WebhookResource) Delete(ctx context.Context, key string, req resource.DeleteRequest, resp *resource.DeleteResponse) { +func (r *WebhookResource) Delete(ctx context.Context, key string, resp *resource.DeleteResponse) { var artifactoryError artifactory.ArtifactoryErrorsResponse response, err := r.ProviderData.Client.R(). SetPathParam("webhookKey", key). @@ -306,7 +306,7 @@ func (r *WebhookResource) ImportState(ctx context.Context, req resource.ImportSt resource.ImportStatePassthroughID(ctx, path.Root("key"), req, resp) } -type WebhookNoCriteriaResourceModel struct { +type WebhookBaseResourceModel struct { Key types.String `tfsdk:"key"` Description types.String `tfsdk:"description"` Enabled types.Bool `tfsdk:"enabled"` @@ -314,12 +314,7 @@ type WebhookNoCriteriaResourceModel struct { Handlers types.Set `tfsdk:"handler"` } -type WebhookResourceModel struct { - WebhookNoCriteriaResourceModel - Criteria types.Set `tfsdk:"criteria"` -} - -func (m WebhookNoCriteriaResourceModel) toAPIModel(ctx context.Context, domain string, apiModel *WebhookAPIModel) (diags diag.Diagnostics) { +func (m WebhookBaseResourceModel) toAPIModel(ctx context.Context, domain string, apiModel *WebhookAPIModel) (diags diag.Diagnostics) { var eventTypes []string d := m.EventTypes.ElementsAs(ctx, &eventTypes, false) if d.HasError() { @@ -366,40 +361,6 @@ func (m WebhookNoCriteriaResourceModel) toAPIModel(ctx context.Context, domain s return } -func (m WebhookResourceModel) toAPIModel(ctx context.Context, domain string, criteriaAPIModel interface{}, apiModel *WebhookAPIModel) (diags diag.Diagnostics) { - d := m.WebhookNoCriteriaResourceModel.toAPIModel(ctx, domain, apiModel) - - apiModel.EventFilter.Criteria = criteriaAPIModel - - return d -} - -func (m *WebhookResourceModel) toBaseCriteriaAPIModel(ctx context.Context, criteriaAttrs map[string]attr.Value) (BaseCriteriaAPIModel, diag.Diagnostics) { - diags := diag.Diagnostics{} - - var includePatterns []string - d := criteriaAttrs["include_patterns"].(types.Set).ElementsAs(ctx, &includePatterns, false) - if d.HasError() { - diags.Append(d...) - } - - var excludePatterns []string - d = criteriaAttrs["exclude_patterns"].(types.Set).ElementsAs(ctx, &excludePatterns, false) - if d.HasError() { - diags.Append(d...) - } - - return BaseCriteriaAPIModel{ - IncludePatterns: includePatterns, - ExcludePatterns: excludePatterns, - }, diags -} - -var patternsCriteriaSetResourceModelAttributeTypes = map[string]attr.Type{ - "include_patterns": types.SetType{ElemType: types.StringType}, - "exclude_patterns": types.SetType{ElemType: types.StringType}, -} - var handlerSetResourceModelAttributeTypes = map[string]attr.Type{ "url": types.StringType, "secret": types.StringType, @@ -412,36 +373,7 @@ var handlerSetResourceModelElementTypes = types.ObjectType{ AttrTypes: handlerSetResourceModelAttributeTypes, } -func (m *WebhookResourceModel) fromBaseCriteriaAPIModel(ctx context.Context, criteriaAPIModel map[string]interface{}) (map[string]attr.Value, diag.Diagnostics) { - diags := diag.Diagnostics{} - - includePatterns := types.SetNull(types.StringType) - if v, ok := criteriaAPIModel["includePatterns"]; ok && v != nil { - ps, d := types.SetValueFrom(ctx, types.StringType, v) - if d.HasError() { - diags.Append(d...) - } - - includePatterns = ps - } - - excludePatterns := types.SetNull(types.StringType) - if v, ok := criteriaAPIModel["excludePatterns"]; ok && v != nil { - ps, d := types.SetValueFrom(ctx, types.StringType, v) - if d.HasError() { - diags.Append(d...) - } - - excludePatterns = ps - } - - return map[string]attr.Value{ - "include_patterns": includePatterns, - "exclude_patterns": excludePatterns, - }, diags -} - -func (m *WebhookNoCriteriaResourceModel) fromAPIModel(ctx context.Context, apiModel WebhookAPIModel, stateHandlers basetypes.SetValue) diag.Diagnostics { +func (m *WebhookBaseResourceModel) fromAPIModel(ctx context.Context, apiModel WebhookAPIModel, stateHandlers basetypes.SetValue) diag.Diagnostics { diags := diag.Diagnostics{} m.Key = types.StringValue(apiModel.Key) @@ -537,12 +469,84 @@ func (m *WebhookNoCriteriaResourceModel) fromAPIModel(ctx context.Context, apiMo return diags } +type WebhookCriteriaResourceModel struct { + Criteria types.Set `tfsdk:"criteria"` +} + +func (m *WebhookCriteriaResourceModel) toBaseCriteriaAPIModel(ctx context.Context, criteriaAttrs map[string]attr.Value) (BaseCriteriaAPIModel, diag.Diagnostics) { + diags := diag.Diagnostics{} + + var includePatterns []string + d := criteriaAttrs["include_patterns"].(types.Set).ElementsAs(ctx, &includePatterns, false) + if d.HasError() { + diags.Append(d...) + } + + var excludePatterns []string + d = criteriaAttrs["exclude_patterns"].(types.Set).ElementsAs(ctx, &excludePatterns, false) + if d.HasError() { + diags.Append(d...) + } + + return BaseCriteriaAPIModel{ + IncludePatterns: includePatterns, + ExcludePatterns: excludePatterns, + }, diags +} + +var patternsCriteriaSetResourceModelAttributeTypes = map[string]attr.Type{ + "include_patterns": types.SetType{ElemType: types.StringType}, + "exclude_patterns": types.SetType{ElemType: types.StringType}, +} + +func (m *WebhookCriteriaResourceModel) fromBaseCriteriaAPIModel(ctx context.Context, criteriaAPIModel map[string]interface{}) (map[string]attr.Value, diag.Diagnostics) { + diags := diag.Diagnostics{} + + includePatterns := types.SetNull(types.StringType) + if v, ok := criteriaAPIModel["includePatterns"]; ok && v != nil { + ps, d := types.SetValueFrom(ctx, types.StringType, v) + if d.HasError() { + diags.Append(d...) + } + + includePatterns = ps + } + + excludePatterns := types.SetNull(types.StringType) + if v, ok := criteriaAPIModel["excludePatterns"]; ok && v != nil { + ps, d := types.SetValueFrom(ctx, types.StringType, v) + if d.HasError() { + diags.Append(d...) + } + + excludePatterns = ps + } + + return map[string]attr.Value{ + "include_patterns": includePatterns, + "exclude_patterns": excludePatterns, + }, diags +} + +type WebhookResourceModel struct { + WebhookBaseResourceModel + WebhookCriteriaResourceModel +} + +func (m WebhookResourceModel) toAPIModel(ctx context.Context, domain string, criteriaAPIModel interface{}, apiModel *WebhookAPIModel) (diags diag.Diagnostics) { + d := m.WebhookBaseResourceModel.toAPIModel(ctx, domain, apiModel) + + apiModel.EventFilter.Criteria = criteriaAPIModel + + return d +} + func (m *WebhookResourceModel) fromAPIModel(ctx context.Context, apiModel WebhookAPIModel, stateHandlers basetypes.SetValue, criteriaSet *basetypes.SetValue) diag.Diagnostics { if criteriaSet != nil { m.Criteria = *criteriaSet } - return m.WebhookNoCriteriaResourceModel.fromAPIModel(ctx, apiModel, stateHandlers) + return m.WebhookBaseResourceModel.fromAPIModel(ctx, apiModel, stateHandlers) } type WebhookAPIModel struct { diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_webhook_build.go b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_build.go index 11b5eb2b..21cb1b3a 100644 --- a/pkg/artifactory/resource/webhook/resource_artifactory_webhook_build.go +++ b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_build.go @@ -41,32 +41,32 @@ func (r *BuildWebhookResource) Metadata(ctx context.Context, req resource.Metada r.WebhookResource.Metadata(ctx, req, resp) } -func (r *BuildWebhookResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { - criteriaBlock := schema.SetNestedBlock{ - NestedObject: schema.NestedBlockObject{ - Attributes: lo.Assign( - patternsSchemaAttributes("Use Ant-style wildcard patterns to specify build names (i.e. artifact paths) in the build info repository (without a leading slash) that will be excluded from this permission target.\nAnt-style path expressions are supported (*, **, ?).\nFor example, an `apache/**` pattern will exclude the `apache` build info from the permission."), - map[string]schema.Attribute{ - "any_build": schema.BoolAttribute{ - Required: true, - Description: "Trigger on any builds", - }, - "selected_builds": schema.SetAttribute{ - ElementType: types.StringType, - Required: true, - Description: "Trigger on this list of build IDs", - }, +var buildCriteriaBlock = schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: lo.Assign( + patternsSchemaAttributes("Use Ant-style wildcard patterns to specify build names (i.e. artifact paths) in the build info repository (without a leading slash) that will be excluded from this permission target.\nAnt-style path expressions are supported (*, **, ?).\nFor example, an `apache/**` pattern will exclude the `apache` build info from the permission."), + map[string]schema.Attribute{ + "any_build": schema.BoolAttribute{ + Required: true, + Description: "Trigger on any builds", }, - ), - }, - Validators: []validator.Set{ - setvalidator.SizeBetween(1, 1), - setvalidator.IsRequired(), - }, - Description: "Specifies where the webhook will be applied on which builds.", - } + "selected_builds": schema.SetAttribute{ + ElementType: types.StringType, + Required: true, + Description: "Trigger on this list of build IDs", + }, + }, + ), + }, + Validators: []validator.Set{ + setvalidator.SizeBetween(1, 1), + setvalidator.IsRequired(), + }, + Description: "Specifies where the webhook will be applied on which builds.", +} - resp.Schema = r.CreateSchema(r.Domain, &criteriaBlock, handlerBlock) +func (r *BuildWebhookResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = r.CreateSchema(r.Domain, &buildCriteriaBlock, handlerBlock) } func (r *BuildWebhookResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { @@ -112,7 +112,7 @@ func (r *BuildWebhookResource) Create(ctx context.Context, req resource.CreateRe return } - r.WebhookResource.Create(ctx, webhook, req, resp) + r.WebhookResource.Create(ctx, webhook, resp) if resp.Diagnostics.HasError() { return } @@ -133,7 +133,7 @@ func (r *BuildWebhookResource) Read(ctx context.Context, req resource.ReadReques } var webhook WebhookAPIModel - found := r.WebhookResource.Read(ctx, state.Key.ValueString(), &webhook, req, resp) + found := r.WebhookResource.Read(ctx, state.Key.ValueString(), &webhook, resp) if resp.Diagnostics.HasError() { return } @@ -168,7 +168,7 @@ func (r *BuildWebhookResource) Update(ctx context.Context, req resource.UpdateRe return } - r.WebhookResource.Update(ctx, plan.Key.ValueString(), webhook, req, resp) + r.WebhookResource.Update(ctx, plan.Key.ValueString(), webhook, resp) if resp.Diagnostics.HasError() { return } @@ -185,7 +185,7 @@ func (r *BuildWebhookResource) Delete(ctx context.Context, req resource.DeleteRe // Read Terraform prior state data into the model resp.Diagnostics.Append(req.State.Get(ctx, &state)...) - r.WebhookResource.Delete(ctx, state.Key.ValueString(), req, resp) + r.WebhookResource.Delete(ctx, state.Key.ValueString(), resp) if resp.Diagnostics.HasError() { return } @@ -199,6 +199,22 @@ func (r *BuildWebhookResource) ImportState(ctx context.Context, req resource.Imp r.WebhookResource.ImportState(ctx, req, resp) } +var toBuildCriteriaAPIModel = func(ctx context.Context, baseCriteria BaseCriteriaAPIModel, criteriaAttrs map[string]attr.Value) (criteriaAPIModel BuildCriteriaAPIModel, diags diag.Diagnostics) { + var selectedBuilds []string + d := criteriaAttrs["selected_builds"].(types.Set).ElementsAs(ctx, &selectedBuilds, false) + if d.HasError() { + diags.Append(d...) + } + + criteriaAPIModel = BuildCriteriaAPIModel{ + BaseCriteriaAPIModel: baseCriteria, + AnyBuild: criteriaAttrs["any_build"].(types.Bool).ValueBool(), + SelectedBuilds: selectedBuilds, + } + + return +} + func (m BuildWebhookResourceModel) toAPIModel(ctx context.Context, domain string, apiModel *WebhookAPIModel) (diags diag.Diagnostics) { critieriaObj := m.Criteria.Elements()[0].(types.Object) critieriaAttrs := critieriaObj.Attributes() @@ -208,18 +224,11 @@ func (m BuildWebhookResourceModel) toAPIModel(ctx context.Context, domain string diags.Append(d...) } - var selectedBuilds []string - d = critieriaAttrs["selected_builds"].(types.Set).ElementsAs(ctx, &selectedBuilds, false) + criteriaAPIModel, d := toBuildCriteriaAPIModel(ctx, baseCriteria, critieriaAttrs) if d.HasError() { diags.Append(d...) } - criteriaAPIModel := BuildCriteriaAPIModel{ - BaseCriteriaAPIModel: baseCriteria, - AnyBuild: critieriaAttrs["any_build"].(types.Bool).ValueBool(), - SelectedBuilds: selectedBuilds, - } - d = m.WebhookResourceModel.toAPIModel(ctx, domain, criteriaAPIModel, apiModel) if d.HasError() { diags.Append(d...) @@ -240,13 +249,7 @@ var buildCriteriaSetResourceModelElementTypes = types.ObjectType{ AttrTypes: buildCriteriaSetResourceModelAttributeTypes, } -func (m *BuildWebhookResourceModel) fromAPIModel(ctx context.Context, apiModel WebhookAPIModel, stateHandlers basetypes.SetValue) diag.Diagnostics { - diags := diag.Diagnostics{} - - criteriaAPIModel := apiModel.EventFilter.Criteria.(map[string]interface{}) - - baseCriteriaAttrs, d := m.WebhookResourceModel.fromBaseCriteriaAPIModel(ctx, criteriaAPIModel) - +var fromBuildAPIModel = func(ctx context.Context, criteriaAPIModel map[string]interface{}, baseCriteriaAttrs map[string]attr.Value) (criteriaSet basetypes.SetValue, diags diag.Diagnostics) { selectedBuilds := types.SetNull(types.StringType) if v, ok := criteriaAPIModel["selectedBuilds"]; ok && v != nil { sb, d := types.SetValueFrom(ctx, types.StringType, v) @@ -270,10 +273,22 @@ func (m *BuildWebhookResourceModel) fromAPIModel(ctx context.Context, apiModel W if d.HasError() { diags.Append(d...) } - criteriaSet, d := types.SetValue( + criteriaSet, d = types.SetValue( buildCriteriaSetResourceModelElementTypes, []attr.Value{criteria}, ) + + return criteriaSet, diags +} + +func (m *BuildWebhookResourceModel) fromAPIModel(ctx context.Context, apiModel WebhookAPIModel, stateHandlers basetypes.SetValue) diag.Diagnostics { + diags := diag.Diagnostics{} + + criteriaAPIModel := apiModel.EventFilter.Criteria.(map[string]interface{}) + + baseCriteriaAttrs, d := m.WebhookResourceModel.fromBaseCriteriaAPIModel(ctx, criteriaAPIModel) + + criteriaSet, d := fromBuildAPIModel(ctx, criteriaAPIModel, baseCriteriaAttrs) if d.HasError() { diags.Append(d...) } diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_webhook_release_bundle.go b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_release_bundle.go index 86e6284b..bc376d8b 100644 --- a/pkg/artifactory/resource/webhook/resource_artifactory_webhook_release_bundle.go +++ b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_release_bundle.go @@ -146,7 +146,7 @@ func (r *ReleaseBundleWebhookResource) Create(ctx context.Context, req resource. return } - r.WebhookResource.Create(ctx, webhook, req, resp) + r.WebhookResource.Create(ctx, webhook, resp) if resp.Diagnostics.HasError() { return } @@ -167,7 +167,7 @@ func (r *ReleaseBundleWebhookResource) Read(ctx context.Context, req resource.Re } var webhook WebhookAPIModel - found := r.WebhookResource.Read(ctx, state.Key.ValueString(), &webhook, req, resp) + found := r.WebhookResource.Read(ctx, state.Key.ValueString(), &webhook, resp) if resp.Diagnostics.HasError() { return } @@ -202,7 +202,7 @@ func (r *ReleaseBundleWebhookResource) Update(ctx context.Context, req resource. return } - r.WebhookResource.Update(ctx, plan.Key.ValueString(), webhook, req, resp) + r.WebhookResource.Update(ctx, plan.Key.ValueString(), webhook, resp) if resp.Diagnostics.HasError() { return } @@ -219,7 +219,7 @@ func (r *ReleaseBundleWebhookResource) Delete(ctx context.Context, req resource. // Read Terraform prior state data into the model resp.Diagnostics.Append(req.State.Get(ctx, &state)...) - r.WebhookResource.Delete(ctx, state.Key.ValueString(), req, resp) + r.WebhookResource.Delete(ctx, state.Key.ValueString(), resp) if resp.Diagnostics.HasError() { return } diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_webhook_release_bundle_v2.go b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_release_bundle_v2.go index 400d8cf7..8e4f747a 100644 --- a/pkg/artifactory/resource/webhook/resource_artifactory_webhook_release_bundle_v2.go +++ b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_release_bundle_v2.go @@ -112,7 +112,7 @@ func (r *ReleaseBundleV2WebhookResource) Create(ctx context.Context, req resourc return } - r.WebhookResource.Create(ctx, webhook, req, resp) + r.WebhookResource.Create(ctx, webhook, resp) if resp.Diagnostics.HasError() { return } @@ -133,7 +133,7 @@ func (r *ReleaseBundleV2WebhookResource) Read(ctx context.Context, req resource. } var webhook WebhookAPIModel - found := r.WebhookResource.Read(ctx, state.Key.ValueString(), &webhook, req, resp) + found := r.WebhookResource.Read(ctx, state.Key.ValueString(), &webhook, resp) if resp.Diagnostics.HasError() { return } @@ -168,7 +168,7 @@ func (r *ReleaseBundleV2WebhookResource) Update(ctx context.Context, req resourc return } - r.WebhookResource.Update(ctx, plan.Key.ValueString(), webhook, req, resp) + r.WebhookResource.Update(ctx, plan.Key.ValueString(), webhook, resp) if resp.Diagnostics.HasError() { return } @@ -185,7 +185,7 @@ func (r *ReleaseBundleV2WebhookResource) Delete(ctx context.Context, req resourc // Read Terraform prior state data into the model resp.Diagnostics.Append(req.State.Get(ctx, &state)...) - r.WebhookResource.Delete(ctx, state.Key.ValueString(), req, resp) + r.WebhookResource.Delete(ctx, state.Key.ValueString(), resp) if resp.Diagnostics.HasError() { return } diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_webhook_release_bundle_v2_promotion.go b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_release_bundle_v2_promotion.go index b12a98a1..fe4e09cf 100644 --- a/pkg/artifactory/resource/webhook/resource_artifactory_webhook_release_bundle_v2_promotion.go +++ b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_release_bundle_v2_promotion.go @@ -85,7 +85,7 @@ func (r *ReleaseBundleV2PromotionWebhookResource) Create(ctx context.Context, re return } - r.WebhookResource.Create(ctx, webhook, req, resp) + r.WebhookResource.Create(ctx, webhook, resp) if resp.Diagnostics.HasError() { return } @@ -106,7 +106,7 @@ func (r *ReleaseBundleV2PromotionWebhookResource) Read(ctx context.Context, req } var webhook WebhookAPIModel - found := r.WebhookResource.Read(ctx, state.Key.ValueString(), &webhook, req, resp) + found := r.WebhookResource.Read(ctx, state.Key.ValueString(), &webhook, resp) if resp.Diagnostics.HasError() { return } @@ -141,7 +141,7 @@ func (r *ReleaseBundleV2PromotionWebhookResource) Update(ctx context.Context, re return } - r.WebhookResource.Update(ctx, plan.Key.ValueString(), webhook, req, resp) + r.WebhookResource.Update(ctx, plan.Key.ValueString(), webhook, resp) if resp.Diagnostics.HasError() { return } @@ -158,7 +158,7 @@ func (r *ReleaseBundleV2PromotionWebhookResource) Delete(ctx context.Context, re // Read Terraform prior state data into the model resp.Diagnostics.Append(req.State.Get(ctx, &state)...) - r.WebhookResource.Delete(ctx, state.Key.ValueString(), req, resp) + r.WebhookResource.Delete(ctx, state.Key.ValueString(), resp) if resp.Diagnostics.HasError() { return } diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_webhook_repo.go b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_repo.go index a77a502f..605308b8 100644 --- a/pkg/artifactory/resource/webhook/resource_artifactory_webhook_repo.go +++ b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_repo.go @@ -61,40 +61,40 @@ func (r *RepoWebhookResource) Metadata(ctx context.Context, req resource.Metadat r.WebhookResource.Metadata(ctx, req, resp) } -func (r *RepoWebhookResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { - criteriaBlock := schema.SetNestedBlock{ - NestedObject: schema.NestedBlockObject{ - Attributes: lo.Assign( - patternsSchemaAttributes("Simple comma separated wildcard patterns for repository artifact paths (with no leading slash).\nAnt-style path expressions are supported (*, **, ?).\nFor example: `org/apache/**`"), - map[string]schema.Attribute{ - "any_local": schema.BoolAttribute{ - Required: true, - Description: "Trigger on any local repositories", - }, - "any_remote": schema.BoolAttribute{ - Required: true, - Description: "Trigger on any remote repositories", - }, - "any_federated": schema.BoolAttribute{ - Required: true, - Description: "Trigger on any federated repositories", - }, - "repo_keys": schema.SetAttribute{ - ElementType: types.StringType, - Required: true, - Description: "Trigger on this list of repository keys", - }, +var repoCriteriaBlock = schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: lo.Assign( + patternsSchemaAttributes("Simple comma separated wildcard patterns for repository artifact paths (with no leading slash).\nAnt-style path expressions are supported (*, **, ?).\nFor example: `org/apache/**`"), + map[string]schema.Attribute{ + "any_local": schema.BoolAttribute{ + Required: true, + Description: "Trigger on any local repositories", }, - ), - }, - Validators: []validator.Set{ - setvalidator.SizeBetween(1, 1), - setvalidator.IsRequired(), - }, - Description: "Specifies where the webhook will be applied on which repositories.", - } + "any_remote": schema.BoolAttribute{ + Required: true, + Description: "Trigger on any remote repositories", + }, + "any_federated": schema.BoolAttribute{ + Required: true, + Description: "Trigger on any federated repositories", + }, + "repo_keys": schema.SetAttribute{ + ElementType: types.StringType, + Required: true, + Description: "Trigger on this list of repository keys", + }, + }, + ), + }, + Validators: []validator.Set{ + setvalidator.SizeBetween(1, 1), + setvalidator.IsRequired(), + }, + Description: "Specifies where the webhook will be applied on which repositories.", +} - resp.Schema = r.CreateSchema(r.Domain, &criteriaBlock, handlerBlock) +func (r *RepoWebhookResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = r.CreateSchema(r.Domain, &repoCriteriaBlock, handlerBlock) } func (r *RepoWebhookResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { @@ -142,7 +142,7 @@ func (r *RepoWebhookResource) Create(ctx context.Context, req resource.CreateReq return } - r.WebhookResource.Create(ctx, webhook, req, resp) + r.WebhookResource.Create(ctx, webhook, resp) if resp.Diagnostics.HasError() { return } @@ -163,7 +163,7 @@ func (r *RepoWebhookResource) Read(ctx context.Context, req resource.ReadRequest } var webhook WebhookAPIModel - found := r.WebhookResource.Read(ctx, state.Key.ValueString(), &webhook, req, resp) + found := r.WebhookResource.Read(ctx, state.Key.ValueString(), &webhook, resp) if resp.Diagnostics.HasError() { return } @@ -198,7 +198,7 @@ func (r *RepoWebhookResource) Update(ctx context.Context, req resource.UpdateReq return } - r.WebhookResource.Update(ctx, plan.Key.ValueString(), webhook, req, resp) + r.WebhookResource.Update(ctx, plan.Key.ValueString(), webhook, resp) if resp.Diagnostics.HasError() { return } @@ -215,7 +215,7 @@ func (r *RepoWebhookResource) Delete(ctx context.Context, req resource.DeleteReq // Read Terraform prior state data into the model resp.Diagnostics.Append(req.State.Get(ctx, &state)...) - r.WebhookResource.Delete(ctx, state.Key.ValueString(), req, resp) + r.WebhookResource.Delete(ctx, state.Key.ValueString(), resp) if resp.Diagnostics.HasError() { return } @@ -229,6 +229,24 @@ func (r *RepoWebhookResource) ImportState(ctx context.Context, req resource.Impo r.WebhookResource.ImportState(ctx, req, resp) } +var toRepoCriteriaAPIModel = func(ctx context.Context, baseCriteria BaseCriteriaAPIModel, criteriaAttrs map[string]attr.Value) (criteriaAPIModel RepoCriteriaAPIModel, diags diag.Diagnostics) { + var repoKeys []string + d := criteriaAttrs["repo_keys"].(types.Set).ElementsAs(ctx, &repoKeys, false) + if d.HasError() { + diags.Append(d...) + } + + criteriaAPIModel = RepoCriteriaAPIModel{ + BaseCriteriaAPIModel: baseCriteria, + AnyLocal: criteriaAttrs["any_local"].(types.Bool).ValueBool(), + AnyRemote: criteriaAttrs["any_remote"].(types.Bool).ValueBool(), + AnyFederated: criteriaAttrs["any_federated"].(types.Bool).ValueBool(), + RepoKeys: repoKeys, + } + + return +} + func (m RepoWebhookResourceModel) toAPIModel(ctx context.Context, domain string, apiModel *WebhookAPIModel) (diags diag.Diagnostics) { critieriaObj := m.Criteria.Elements()[0].(types.Object) critieriaAttrs := critieriaObj.Attributes() @@ -238,19 +256,7 @@ func (m RepoWebhookResourceModel) toAPIModel(ctx context.Context, domain string, diags.Append(d...) } - var repoKeys []string - d = critieriaAttrs["repo_keys"].(types.Set).ElementsAs(ctx, &repoKeys, false) - if d.HasError() { - diags.Append(d...) - } - - criteriaAPIModel := RepoCriteriaAPIModel{ - BaseCriteriaAPIModel: baseCriteria, - AnyLocal: critieriaAttrs["any_local"].(types.Bool).ValueBool(), - AnyRemote: critieriaAttrs["any_remote"].(types.Bool).ValueBool(), - AnyFederated: critieriaAttrs["any_federated"].(types.Bool).ValueBool(), - RepoKeys: repoKeys, - } + criteriaAPIModel, d := toRepoCriteriaAPIModel(ctx, baseCriteria, critieriaAttrs) d = m.WebhookResourceModel.toAPIModel(ctx, domain, criteriaAPIModel, apiModel) if d.HasError() { @@ -274,13 +280,7 @@ var repoCriteriaSetResourceModelElementTypes = types.ObjectType{ AttrTypes: repoCriteriaSetResourceModelAttributeTypes, } -func (m *RepoWebhookResourceModel) fromAPIModel(ctx context.Context, apiModel WebhookAPIModel, stateHandlers basetypes.SetValue) diag.Diagnostics { - diags := diag.Diagnostics{} - - criteriaAPIModel := apiModel.EventFilter.Criteria.(map[string]interface{}) - - baseCriteriaAttrs, d := m.WebhookResourceModel.fromBaseCriteriaAPIModel(ctx, criteriaAPIModel) - +var fromRepoCriteriaAPIMode = func(ctx context.Context, criteriaAPIModel map[string]interface{}, baseCriteriaAttrs map[string]attr.Value) (criteriaSet basetypes.SetValue, diags diag.Diagnostics) { repoKeys := types.SetNull(types.StringType) if v, ok := criteriaAPIModel["repoKeys"]; ok && v != nil { ks, d := types.SetValueFrom(ctx, types.StringType, v) @@ -306,7 +306,7 @@ func (m *RepoWebhookResourceModel) fromAPIModel(ctx context.Context, apiModel We if d.HasError() { diags.Append(d...) } - criteriaSet, d := types.SetValue( + criteriaSet, d = types.SetValue( repoCriteriaSetResourceModelElementTypes, []attr.Value{criteria}, ) @@ -314,6 +314,21 @@ func (m *RepoWebhookResourceModel) fromAPIModel(ctx context.Context, apiModel We diags.Append(d...) } + return +} + +func (m *RepoWebhookResourceModel) fromAPIModel(ctx context.Context, apiModel WebhookAPIModel, stateHandlers basetypes.SetValue) diag.Diagnostics { + diags := diag.Diagnostics{} + + criteriaAPIModel := apiModel.EventFilter.Criteria.(map[string]interface{}) + + baseCriteriaAttrs, d := m.WebhookResourceModel.fromBaseCriteriaAPIModel(ctx, criteriaAPIModel) + + criteriaSet, d := fromRepoCriteriaAPIMode(ctx, criteriaAPIModel, baseCriteriaAttrs) + if d.HasError() { + diags.Append(d...) + } + d = m.WebhookResourceModel.fromAPIModel(ctx, apiModel, stateHandlers, &criteriaSet) if d.HasError() { diags.Append(d...) diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_webhook_user.go b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_user.go index c6084e93..85a43492 100644 --- a/pkg/artifactory/resource/webhook/resource_artifactory_webhook_user.go +++ b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_user.go @@ -23,11 +23,11 @@ func NewUserWebhookResource() resource.Resource { } type UserWebhookResourceModel struct { - WebhookNoCriteriaResourceModel + WebhookBaseResourceModel } func (m UserWebhookResourceModel) toAPIModel(ctx context.Context, domain string, apiModel *WebhookAPIModel) (diags diag.Diagnostics) { - d := m.WebhookNoCriteriaResourceModel.toAPIModel(ctx, domain, apiModel) + d := m.WebhookBaseResourceModel.toAPIModel(ctx, domain, apiModel) if d.HasError() { diags.Append(d...) } @@ -38,7 +38,7 @@ func (m UserWebhookResourceModel) toAPIModel(ctx context.Context, domain string, func (m *UserWebhookResourceModel) fromAPIModel(ctx context.Context, apiModel WebhookAPIModel, stateHandlers basetypes.SetValue) diag.Diagnostics { diags := diag.Diagnostics{} - d := m.WebhookNoCriteriaResourceModel.fromAPIModel(ctx, apiModel, stateHandlers) + d := m.WebhookBaseResourceModel.fromAPIModel(ctx, apiModel, stateHandlers) if d.HasError() { diags.Append(d...) } @@ -79,7 +79,7 @@ func (r *UserWebhookResource) Create(ctx context.Context, req resource.CreateReq return } - r.WebhookResource.Create(ctx, webhook, req, resp) + r.WebhookResource.Create(ctx, webhook, resp) if resp.Diagnostics.HasError() { return } @@ -100,7 +100,7 @@ func (r *UserWebhookResource) Read(ctx context.Context, req resource.ReadRequest } var webhook WebhookAPIModel - found := r.WebhookResource.Read(ctx, state.Key.ValueString(), &webhook, req, resp) + found := r.WebhookResource.Read(ctx, state.Key.ValueString(), &webhook, resp) if resp.Diagnostics.HasError() { return } @@ -135,7 +135,7 @@ func (r *UserWebhookResource) Update(ctx context.Context, req resource.UpdateReq return } - r.WebhookResource.Update(ctx, plan.Key.ValueString(), webhook, req, resp) + r.WebhookResource.Update(ctx, plan.Key.ValueString(), webhook, resp) if resp.Diagnostics.HasError() { return } @@ -152,7 +152,7 @@ func (r *UserWebhookResource) Delete(ctx context.Context, req resource.DeleteReq // Read Terraform prior state data into the model resp.Diagnostics.Append(req.State.Get(ctx, &state)...) - r.WebhookResource.Delete(ctx, state.Key.ValueString(), req, resp) + r.WebhookResource.Delete(ctx, state.Key.ValueString(), resp) if resp.Diagnostics.HasError() { return } From b63b820cafeaa57016188225b7538030a636789e Mon Sep 17 00:00:00 2001 From: Alex Hung Date: Tue, 1 Oct 2024 09:00:19 -0700 Subject: [PATCH 04/12] Move webhook tests to own files --- ...e_artifactory_custom_webhook_build_test.go | 53 +++ ...ce_artifactory_custom_webhook_repo_test.go | 301 +++++++++++++++ ...esource_artifactory_custom_webhook_test.go | 331 ---------------- ...resource_artifactory_webhook_build_test.go | 66 ++++ .../resource_artifactory_webhook_repo_test.go | 317 +++++++++++++++ .../resource_artifactory_webhook_test.go | 360 ------------------ 6 files changed, 737 insertions(+), 691 deletions(-) create mode 100644 pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook_build_test.go create mode 100644 pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook_repo_test.go create mode 100644 pkg/artifactory/resource/webhook/resource_artifactory_webhook_build_test.go create mode 100644 pkg/artifactory/resource/webhook/resource_artifactory_webhook_repo_test.go diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook_build_test.go b/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook_build_test.go new file mode 100644 index 00000000..8a907f2e --- /dev/null +++ b/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook_build_test.go @@ -0,0 +1,53 @@ +package webhook_test + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/jfrog/terraform-provider-artifactory/v12/pkg/acctest" + "github.com/jfrog/terraform-provider-shared/testutil" + "github.com/jfrog/terraform-provider-shared/util" +) + +func TestAccCustomWebhook_BuildWithIncludePatterns(t *testing.T) { + id := testutil.RandomInt() + name := fmt.Sprintf("webhook-%d", id) + fqrn := fmt.Sprintf("artifactory_build_custom_webhook.%s", name) + + params := map[string]interface{}{ + "webhookName": name, + } + webhookConfig := util.ExecuteTemplate("TestAccCustomWebhookBuildPatterns", ` + resource "artifactory_build_custom_webhook" "{{ .webhookName }}" { + key = "{{ .webhookName }}" + description = "test description" + event_types = ["uploaded"] + criteria { + any_build = false + selected_builds = [] + include_patterns = ["foo"] + } + handler { + url = "https://google.com" + payload = "{ \"ref\": \"main\" , \"inputs\": { \"artifact_path\": \"test-repo/repo-path\" } }" + } + } + `, params) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ProviderFactories: acctest.ProviderFactories, + CheckDestroy: acctest.VerifyDeleted(fqrn, "", acctest.CheckRepo), + + Steps: []resource.TestStep{ + { + Config: webhookConfig, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(fqrn, "criteria.0.include_patterns.#", "1"), + resource.TestCheckResourceAttr(fqrn, "criteria.0.include_patterns.0", "foo"), + ), + }, + }, + }) +} diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook_repo_test.go b/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook_repo_test.go new file mode 100644 index 00000000..ef69384a --- /dev/null +++ b/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook_repo_test.go @@ -0,0 +1,301 @@ +package webhook_test + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/plancheck" + "github.com/jfrog/terraform-provider-artifactory/v12/pkg/acctest" + "github.com/jfrog/terraform-provider-artifactory/v12/pkg/artifactory/resource/webhook" + "github.com/jfrog/terraform-provider-shared/testutil" + "github.com/jfrog/terraform-provider-shared/util" +) + +func TestAccCustomWebhook_AllRepoTypes(t *testing.T) { + // Can only realistically test these 3 types of webhook since creating + // build, release_bundle, or distribution in test environment is almost impossible + for _, webhookType := range []string{"artifact", "artifact_property", "docker"} { + t.Run(webhookType, func(t *testing.T) { + resource.Test(customWebhookTestCase(webhookType, t)) + }) + } +} + +func customWebhookTestCase(webhookType string, t *testing.T) (*testing.T, resource.TestCase) { + id := testutil.RandomInt() + name := fmt.Sprintf("custom-webhook-%d", id) + fqrn := fmt.Sprintf("artifactory_%s_custom_webhook.%s", webhookType, name) + + repoType := domainRepoTypeLookup[webhookType] + repoName := fmt.Sprintf("%s-local-%d", webhookType, id) + eventTypes := webhook.DomainEventTypesSupported[webhookType] + + params := map[string]interface{}{ + "repoName": repoName, + "repoType": repoType, + "webhookType": webhookType, + "webhookName": name, + "eventTypes": eventTypes, + "anyLocal": testutil.RandBool(), + "anyRemote": testutil.RandBool(), + "anyFederated": testutil.RandBool(), + } + webhookConfig := util.ExecuteTemplate("TestAccWebhook{{ .webhookType }}Type", ` + resource "artifactory_local_{{ .repoType }}_repository" "{{ .repoName }}" { + key = "{{ .repoName }}" + } + + resource "artifactory_{{ .webhookType }}_custom_webhook" "{{ .webhookName }}" { + key = "{{ .webhookName }}" + description = "test description" + event_types = [{{ range $index, $eventType := .eventTypes}}{{if $index}},{{end}}"{{$eventType}}"{{end}}] + criteria { + any_local = {{ .anyLocal }} + any_remote = {{ .anyRemote }} + any_federated = {{ .anyFederated }} + repo_keys = ["{{ .repoName }}"] + include_patterns = ["foo/**"] + exclude_patterns = ["bar/**"] + } + handler { + url = "https://google.com" + secrets = { + secret1 = "value1" + secret2 = "value2" + } + http_headers = { + header-1 = "value-1" + header-2 = "value-2" + } + payload = "{ \"ref\": \"main\" , \"inputs\": { \"artifact_path\": \"test-repo/repo-path/1\" } }" + } + handler { + url = "https://yahoo.com" + secrets = { + secret3 = "value3" + secret4 = "value4" + } + http_headers = { + header-3 = "value-3" + header-4 = "value-4" + } + payload = "{ \"ref\": \"main\" , \"inputs\": { \"artifact_path\": \"test-repo/repo-path/2\" } }" + } + handler { + url = "https://msnbc.com" + payload = "{ \"ref\": \"main\" , \"inputs\": { \"artifact_path\": \"test-repo/repo-path/3\" } }" + } + + depends_on = [artifactory_local_{{ .repoType }}_repository.{{ .repoName }}] + } + `, params) + + testChecks := []resource.TestCheckFunc{ + resource.TestCheckResourceAttr(fqrn, "key", name), + resource.TestCheckResourceAttr(fqrn, "event_types.#", fmt.Sprintf("%d", len(eventTypes))), + resource.TestCheckResourceAttr(fqrn, "criteria.#", "1"), + resource.TestCheckResourceAttr(fqrn, "criteria.0.any_local", fmt.Sprintf("%t", params["anyLocal"])), + resource.TestCheckResourceAttr(fqrn, "criteria.0.any_remote", fmt.Sprintf("%t", params["anyRemote"])), + resource.TestCheckResourceAttr(fqrn, "criteria.0.any_federated", fmt.Sprintf("%t", params["anyFederated"])), + resource.TestCheckTypeSetElemAttr(fqrn, "criteria.0.repo_keys.*", repoName), + resource.TestCheckResourceAttr(fqrn, "criteria.0.include_patterns.#", "1"), + resource.TestCheckResourceAttr(fqrn, "criteria.0.include_patterns.0", "foo/**"), + resource.TestCheckResourceAttr(fqrn, "criteria.0.exclude_patterns.#", "1"), + resource.TestCheckResourceAttr(fqrn, "criteria.0.exclude_patterns.0", "bar/**"), + resource.TestCheckResourceAttr(fqrn, "handler.#", "3"), + resource.TestCheckResourceAttr(fqrn, "handler.0.url", "https://google.com"), + resource.TestCheckResourceAttr(fqrn, "handler.0.secrets.%", "2"), + resource.TestCheckResourceAttr(fqrn, "handler.0.secrets.secret1", "value1"), + resource.TestCheckResourceAttr(fqrn, "handler.0.secrets.secret2", "value2"), + resource.TestCheckResourceAttr(fqrn, "handler.0.http_headers.%", "2"), + resource.TestCheckResourceAttr(fqrn, "handler.0.http_headers.header-1", "value-1"), + resource.TestCheckResourceAttr(fqrn, "handler.0.http_headers.header-2", "value-2"), + resource.TestCheckResourceAttr(fqrn, "handler.0.payload", "{ \"ref\": \"main\" , \"inputs\": { \"artifact_path\": \"test-repo/repo-path/1\" } }"), + resource.TestCheckResourceAttr(fqrn, "handler.1.url", "https://yahoo.com"), + resource.TestCheckResourceAttr(fqrn, "handler.1.secrets.%", "2"), + resource.TestCheckResourceAttr(fqrn, "handler.1.secrets.secret3", "value3"), + resource.TestCheckResourceAttr(fqrn, "handler.1.secrets.secret4", "value4"), + resource.TestCheckResourceAttr(fqrn, "handler.1.http_headers.%", "2"), + resource.TestCheckResourceAttr(fqrn, "handler.1.http_headers.header-3", "value-3"), + resource.TestCheckResourceAttr(fqrn, "handler.1.http_headers.header-4", "value-4"), + resource.TestCheckResourceAttr(fqrn, "handler.1.payload", "{ \"ref\": \"main\" , \"inputs\": { \"artifact_path\": \"test-repo/repo-path/2\" } }"), + resource.TestCheckResourceAttr(fqrn, "handler.2.url", "https://msnbc.com"), + resource.TestCheckNoResourceAttr(fqrn, "handler.2.secrets"), + resource.TestCheckNoResourceAttr(fqrn, "handler.2.http_headers"), + resource.TestCheckResourceAttr(fqrn, "handler.2.payload", "{ \"ref\": \"main\" , \"inputs\": { \"artifact_path\": \"test-repo/repo-path/3\" } }"), + } + + for _, eventType := range eventTypes { + eventTypeCheck := resource.TestCheckTypeSetElemAttr(fqrn, "event_types.*", eventType) + testChecks = append(testChecks, eventTypeCheck) + } + + return t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ProtoV6ProviderFactories: acctest.ProtoV6MuxProviderFactories, + CheckDestroy: acctest.VerifyDeleted(fqrn, "key", testCheckWebhook), + + Steps: []resource.TestStep{ + { + Config: webhookConfig, + Check: resource.ComposeTestCheckFunc(testChecks...), + }, + { + ResourceName: fqrn, + ImportState: true, + ImportStateId: name, + ImportStateVerify: true, + ImportStateVerifyIdentifierAttribute: "key", + ImportStateVerifyIgnore: []string{"handler.0.secrets", "handler.1.secrets"}, + }, + }, + } +} + +func TestAccCustomWebhook_AllRepoTypes_UpgradeFromSDKv2(t *testing.T) { + // Can only realistically test these 3 types of webhook since creating + // build, release_bundle, or distribution in test environment is almost impossible + for _, webhookType := range []string{"artifact", "artifact_property", "docker"} { + t.Run(webhookType, func(t *testing.T) { + resource.Test(customWebhookMigrateFromSDKv2TestCase(webhookType, t)) + }) + } +} + +func customWebhookMigrateFromSDKv2TestCase(webhookType string, t *testing.T) (*testing.T, resource.TestCase) { + id := testutil.RandomInt() + name := fmt.Sprintf("custom-webhook-%d", id) + fqrn := fmt.Sprintf("artifactory_%s_custom_webhook.%s", webhookType, name) + + repoType := domainRepoTypeLookup[webhookType] + repoName := fmt.Sprintf("%s-local-%d", webhookType, id) + eventTypes := webhook.DomainEventTypesSupported[webhookType] + + params := map[string]interface{}{ + "repoName": repoName, + "repoType": repoType, + "webhookType": webhookType, + "webhookName": name, + "eventTypes": eventTypes, + "anyLocal": testutil.RandBool(), + "anyRemote": testutil.RandBool(), + "anyFederated": testutil.RandBool(), + } + config := util.ExecuteTemplate("TestAccWebhook{{ .webhookType }}Type", ` + resource "artifactory_local_{{ .repoType }}_repository" "{{ .repoName }}" { + key = "{{ .repoName }}" + } + + resource "artifactory_{{ .webhookType }}_custom_webhook" "{{ .webhookName }}" { + key = "{{ .webhookName }}" + description = "test description" + event_types = [{{ range $index, $eventType := .eventTypes}}{{if $index}},{{end}}"{{$eventType}}"{{end}}] + criteria { + any_local = {{ .anyLocal }} + any_remote = {{ .anyRemote }} + any_federated = {{ .anyFederated }} + repo_keys = ["{{ .repoName }}"] + include_patterns = ["foo/**"] + exclude_patterns = ["bar/**"] + } + handler { + url = "https://google.com" + secrets = { + secret1 = "value1" + secret2 = "value2" + } + http_headers = { + header-1 = "value-1" + header-2 = "value-2" + } + payload = "{ \"ref\": \"main\" , \"inputs\": { \"artifact_path\": \"test-repo/repo-path/1\" } }" + } + handler { + url = "https://yahoo.com" + secrets = { + secret3 = "value3" + secret4 = "value4" + } + http_headers = { + header-3 = "value-3" + header-4 = "value-4" + } + payload = "{ \"ref\": \"main\" , \"inputs\": { \"artifact_path\": \"test-repo/repo-path/2\" } }" + } + handler { + url = "https://msnbc.com" + payload = "{ \"ref\": \"main\" , \"inputs\": { \"artifact_path\": \"test-repo/repo-path/3\" } }" + } + + depends_on = [artifactory_local_{{ .repoType }}_repository.{{ .repoName }}] + } + `, params) + + testChecks := []resource.TestCheckFunc{ + resource.TestCheckResourceAttr(fqrn, "key", name), + resource.TestCheckResourceAttr(fqrn, "event_types.#", fmt.Sprintf("%d", len(eventTypes))), + resource.TestCheckResourceAttr(fqrn, "criteria.#", "1"), + resource.TestCheckResourceAttr(fqrn, "criteria.0.any_local", fmt.Sprintf("%t", params["anyLocal"])), + resource.TestCheckResourceAttr(fqrn, "criteria.0.any_remote", fmt.Sprintf("%t", params["anyRemote"])), + resource.TestCheckResourceAttr(fqrn, "criteria.0.any_federated", fmt.Sprintf("%t", params["anyFederated"])), + resource.TestCheckTypeSetElemAttr(fqrn, "criteria.0.repo_keys.*", repoName), + resource.TestCheckResourceAttr(fqrn, "criteria.0.include_patterns.#", "1"), + resource.TestCheckResourceAttr(fqrn, "criteria.0.include_patterns.0", "foo/**"), + resource.TestCheckResourceAttr(fqrn, "criteria.0.exclude_patterns.#", "1"), + resource.TestCheckResourceAttr(fqrn, "criteria.0.exclude_patterns.0", "bar/**"), + resource.TestCheckResourceAttr(fqrn, "handler.#", "3"), + resource.TestCheckResourceAttr(fqrn, "handler.0.url", "https://google.com"), + resource.TestCheckResourceAttr(fqrn, "handler.0.secrets.%", "2"), + resource.TestCheckResourceAttr(fqrn, "handler.0.secrets.secret1", "value1"), + resource.TestCheckResourceAttr(fqrn, "handler.0.secrets.secret2", "value2"), + resource.TestCheckResourceAttr(fqrn, "handler.0.http_headers.%", "2"), + resource.TestCheckResourceAttr(fqrn, "handler.0.http_headers.header-1", "value-1"), + resource.TestCheckResourceAttr(fqrn, "handler.0.http_headers.header-2", "value-2"), + resource.TestCheckResourceAttr(fqrn, "handler.0.payload", "{ \"ref\": \"main\" , \"inputs\": { \"artifact_path\": \"test-repo/repo-path/1\" } }"), + resource.TestCheckResourceAttr(fqrn, "handler.1.url", "https://yahoo.com"), + resource.TestCheckResourceAttr(fqrn, "handler.1.secrets.%", "2"), + resource.TestCheckResourceAttr(fqrn, "handler.1.secrets.secret3", "value3"), + resource.TestCheckResourceAttr(fqrn, "handler.1.secrets.secret4", "value4"), + resource.TestCheckResourceAttr(fqrn, "handler.1.http_headers.%", "2"), + resource.TestCheckResourceAttr(fqrn, "handler.1.http_headers.header-3", "value-3"), + resource.TestCheckResourceAttr(fqrn, "handler.1.http_headers.header-4", "value-4"), + resource.TestCheckResourceAttr(fqrn, "handler.1.payload", "{ \"ref\": \"main\" , \"inputs\": { \"artifact_path\": \"test-repo/repo-path/2\" } }"), + resource.TestCheckResourceAttr(fqrn, "handler.2.url", "https://msnbc.com"), + resource.TestCheckNoResourceAttr(fqrn, "handler.2.secrets"), + resource.TestCheckNoResourceAttr(fqrn, "handler.2.http_headers"), + resource.TestCheckResourceAttr(fqrn, "handler.2.payload", "{ \"ref\": \"main\" , \"inputs\": { \"artifact_path\": \"test-repo/repo-path/3\" } }"), + } + + for _, eventType := range eventTypes { + eventTypeCheck := resource.TestCheckTypeSetElemAttr(fqrn, "event_types.*", eventType) + testChecks = append(testChecks, eventTypeCheck) + } + + return t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + CheckDestroy: acctest.VerifyDeleted(fqrn, "key", testCheckWebhook), + + Steps: []resource.TestStep{ + { + Config: config, + ExternalProviders: map[string]resource.ExternalProvider{ + "artifactory": { + Source: "jfrog/artifactory", + VersionConstraint: "12.1.0", + }, + }, + Check: resource.ComposeTestCheckFunc(testChecks...), + }, + { + Config: config, + ProtoV6ProviderFactories: acctest.ProtoV6MuxProviderFactories, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectEmptyPlan(), + }, + }, + }, + }, + } +} diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook_test.go b/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook_test.go index 5a7fd0aa..0bf6dc65 100644 --- a/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook_test.go +++ b/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook_test.go @@ -6,7 +6,6 @@ import ( "testing" "github.com/hashicorp/terraform-plugin-testing/helper/resource" - "github.com/hashicorp/terraform-plugin-testing/plancheck" "github.com/jfrog/terraform-provider-artifactory/v12/pkg/acctest" "github.com/jfrog/terraform-provider-artifactory/v12/pkg/artifactory/resource/webhook" "github.com/jfrog/terraform-provider-shared/testutil" @@ -59,336 +58,6 @@ func customWebhookCriteriaValidationTestCase(webhookType string, t *testing.T) ( } } -func TestAccCustomWebhook_AllTypes(t *testing.T) { - // Can only realistically test these 3 types of webhook since creating - // build, release_bundle, or distribution in test environment is almost impossible - for _, webhookType := range []string{"artifact", "artifact_property", "docker"} { - t.Run(webhookType, func(t *testing.T) { - resource.Test(customWebhookTestCase(webhookType, t)) - }) - } -} - -func customWebhookTestCase(webhookType string, t *testing.T) (*testing.T, resource.TestCase) { - id := testutil.RandomInt() - name := fmt.Sprintf("custom-webhook-%d", id) - fqrn := fmt.Sprintf("artifactory_%s_custom_webhook.%s", webhookType, name) - - repoType := domainRepoTypeLookup[webhookType] - repoName := fmt.Sprintf("%s-local-%d", webhookType, id) - eventTypes := webhook.DomainEventTypesSupported[webhookType] - - params := map[string]interface{}{ - "repoName": repoName, - "repoType": repoType, - "webhookType": webhookType, - "webhookName": name, - "eventTypes": eventTypes, - "anyLocal": testutil.RandBool(), - "anyRemote": testutil.RandBool(), - "anyFederated": testutil.RandBool(), - } - webhookConfig := util.ExecuteTemplate("TestAccWebhook{{ .webhookType }}Type", ` - resource "artifactory_local_{{ .repoType }}_repository" "{{ .repoName }}" { - key = "{{ .repoName }}" - } - - resource "artifactory_{{ .webhookType }}_custom_webhook" "{{ .webhookName }}" { - key = "{{ .webhookName }}" - description = "test description" - event_types = [{{ range $index, $eventType := .eventTypes}}{{if $index}},{{end}}"{{$eventType}}"{{end}}] - criteria { - any_local = {{ .anyLocal }} - any_remote = {{ .anyRemote }} - any_federated = {{ .anyFederated }} - repo_keys = ["{{ .repoName }}"] - include_patterns = ["foo/**"] - exclude_patterns = ["bar/**"] - } - handler { - url = "https://google.com" - secrets = { - secret1 = "value1" - secret2 = "value2" - } - http_headers = { - header-1 = "value-1" - header-2 = "value-2" - } - payload = "{ \"ref\": \"main\" , \"inputs\": { \"artifact_path\": \"test-repo/repo-path/1\" } }" - } - handler { - url = "https://yahoo.com" - secrets = { - secret3 = "value3" - secret4 = "value4" - } - http_headers = { - header-3 = "value-3" - header-4 = "value-4" - } - payload = "{ \"ref\": \"main\" , \"inputs\": { \"artifact_path\": \"test-repo/repo-path/2\" } }" - } - handler { - url = "https://msnbc.com" - payload = "{ \"ref\": \"main\" , \"inputs\": { \"artifact_path\": \"test-repo/repo-path/3\" } }" - } - - depends_on = [artifactory_local_{{ .repoType }}_repository.{{ .repoName }}] - } - `, params) - - testChecks := []resource.TestCheckFunc{ - resource.TestCheckResourceAttr(fqrn, "key", name), - resource.TestCheckResourceAttr(fqrn, "event_types.#", fmt.Sprintf("%d", len(eventTypes))), - resource.TestCheckResourceAttr(fqrn, "criteria.#", "1"), - resource.TestCheckResourceAttr(fqrn, "criteria.0.any_local", fmt.Sprintf("%t", params["anyLocal"])), - resource.TestCheckResourceAttr(fqrn, "criteria.0.any_remote", fmt.Sprintf("%t", params["anyRemote"])), - resource.TestCheckResourceAttr(fqrn, "criteria.0.any_federated", fmt.Sprintf("%t", params["anyFederated"])), - resource.TestCheckTypeSetElemAttr(fqrn, "criteria.0.repo_keys.*", repoName), - resource.TestCheckResourceAttr(fqrn, "criteria.0.include_patterns.#", "1"), - resource.TestCheckResourceAttr(fqrn, "criteria.0.include_patterns.0", "foo/**"), - resource.TestCheckResourceAttr(fqrn, "criteria.0.exclude_patterns.#", "1"), - resource.TestCheckResourceAttr(fqrn, "criteria.0.exclude_patterns.0", "bar/**"), - resource.TestCheckResourceAttr(fqrn, "handler.#", "3"), - resource.TestCheckResourceAttr(fqrn, "handler.0.url", "https://google.com"), - resource.TestCheckResourceAttr(fqrn, "handler.0.secrets.%", "2"), - resource.TestCheckResourceAttr(fqrn, "handler.0.secrets.secret1", "value1"), - resource.TestCheckResourceAttr(fqrn, "handler.0.secrets.secret2", "value2"), - resource.TestCheckResourceAttr(fqrn, "handler.0.http_headers.%", "2"), - resource.TestCheckResourceAttr(fqrn, "handler.0.http_headers.header-1", "value-1"), - resource.TestCheckResourceAttr(fqrn, "handler.0.http_headers.header-2", "value-2"), - resource.TestCheckResourceAttr(fqrn, "handler.0.payload", "{ \"ref\": \"main\" , \"inputs\": { \"artifact_path\": \"test-repo/repo-path/1\" } }"), - resource.TestCheckResourceAttr(fqrn, "handler.1.url", "https://yahoo.com"), - resource.TestCheckResourceAttr(fqrn, "handler.1.secrets.%", "2"), - resource.TestCheckResourceAttr(fqrn, "handler.1.secrets.secret3", "value3"), - resource.TestCheckResourceAttr(fqrn, "handler.1.secrets.secret4", "value4"), - resource.TestCheckResourceAttr(fqrn, "handler.1.http_headers.%", "2"), - resource.TestCheckResourceAttr(fqrn, "handler.1.http_headers.header-3", "value-3"), - resource.TestCheckResourceAttr(fqrn, "handler.1.http_headers.header-4", "value-4"), - resource.TestCheckResourceAttr(fqrn, "handler.1.payload", "{ \"ref\": \"main\" , \"inputs\": { \"artifact_path\": \"test-repo/repo-path/2\" } }"), - resource.TestCheckResourceAttr(fqrn, "handler.2.url", "https://msnbc.com"), - resource.TestCheckNoResourceAttr(fqrn, "handler.2.secrets"), - resource.TestCheckNoResourceAttr(fqrn, "handler.2.http_headers"), - resource.TestCheckResourceAttr(fqrn, "handler.2.payload", "{ \"ref\": \"main\" , \"inputs\": { \"artifact_path\": \"test-repo/repo-path/3\" } }"), - } - - for _, eventType := range eventTypes { - eventTypeCheck := resource.TestCheckTypeSetElemAttr(fqrn, "event_types.*", eventType) - testChecks = append(testChecks, eventTypeCheck) - } - - return t, resource.TestCase{ - PreCheck: func() { acctest.PreCheck(t) }, - ProtoV6ProviderFactories: acctest.ProtoV6MuxProviderFactories, - CheckDestroy: acctest.VerifyDeleted(fqrn, "key", testCheckWebhook), - - Steps: []resource.TestStep{ - { - Config: webhookConfig, - Check: resource.ComposeTestCheckFunc(testChecks...), - }, - { - ResourceName: fqrn, - ImportState: true, - ImportStateId: name, - ImportStateVerify: true, - ImportStateVerifyIdentifierAttribute: "key", - ImportStateVerifyIgnore: []string{"handler.0.secrets", "handler.1.secrets"}, - }, - }, - } -} - -func TestAccCustomWebhook_UpgradeFromSDKv2(t *testing.T) { - // Can only realistically test these 3 types of webhook since creating - // build, release_bundle, or distribution in test environment is almost impossible - for _, webhookType := range []string{"artifact", "artifact_property", "docker"} { - t.Run(webhookType, func(t *testing.T) { - resource.Test(customWebhookMigrateFromSDKv2TestCase(webhookType, t)) - }) - } -} - -func customWebhookMigrateFromSDKv2TestCase(webhookType string, t *testing.T) (*testing.T, resource.TestCase) { - id := testutil.RandomInt() - name := fmt.Sprintf("custom-webhook-%d", id) - fqrn := fmt.Sprintf("artifactory_%s_custom_webhook.%s", webhookType, name) - - repoType := domainRepoTypeLookup[webhookType] - repoName := fmt.Sprintf("%s-local-%d", webhookType, id) - eventTypes := webhook.DomainEventTypesSupported[webhookType] - - params := map[string]interface{}{ - "repoName": repoName, - "repoType": repoType, - "webhookType": webhookType, - "webhookName": name, - "eventTypes": eventTypes, - "anyLocal": testutil.RandBool(), - "anyRemote": testutil.RandBool(), - "anyFederated": testutil.RandBool(), - } - config := util.ExecuteTemplate("TestAccWebhook{{ .webhookType }}Type", ` - resource "artifactory_local_{{ .repoType }}_repository" "{{ .repoName }}" { - key = "{{ .repoName }}" - } - - resource "artifactory_{{ .webhookType }}_custom_webhook" "{{ .webhookName }}" { - key = "{{ .webhookName }}" - description = "test description" - event_types = [{{ range $index, $eventType := .eventTypes}}{{if $index}},{{end}}"{{$eventType}}"{{end}}] - criteria { - any_local = {{ .anyLocal }} - any_remote = {{ .anyRemote }} - any_federated = {{ .anyFederated }} - repo_keys = ["{{ .repoName }}"] - include_patterns = ["foo/**"] - exclude_patterns = ["bar/**"] - } - handler { - url = "https://google.com" - secrets = { - secret1 = "value1" - secret2 = "value2" - } - http_headers = { - header-1 = "value-1" - header-2 = "value-2" - } - payload = "{ \"ref\": \"main\" , \"inputs\": { \"artifact_path\": \"test-repo/repo-path/1\" } }" - } - handler { - url = "https://yahoo.com" - secrets = { - secret3 = "value3" - secret4 = "value4" - } - http_headers = { - header-3 = "value-3" - header-4 = "value-4" - } - payload = "{ \"ref\": \"main\" , \"inputs\": { \"artifact_path\": \"test-repo/repo-path/2\" } }" - } - handler { - url = "https://msnbc.com" - payload = "{ \"ref\": \"main\" , \"inputs\": { \"artifact_path\": \"test-repo/repo-path/3\" } }" - } - - depends_on = [artifactory_local_{{ .repoType }}_repository.{{ .repoName }}] - } - `, params) - - testChecks := []resource.TestCheckFunc{ - resource.TestCheckResourceAttr(fqrn, "key", name), - resource.TestCheckResourceAttr(fqrn, "event_types.#", fmt.Sprintf("%d", len(eventTypes))), - resource.TestCheckResourceAttr(fqrn, "criteria.#", "1"), - resource.TestCheckResourceAttr(fqrn, "criteria.0.any_local", fmt.Sprintf("%t", params["anyLocal"])), - resource.TestCheckResourceAttr(fqrn, "criteria.0.any_remote", fmt.Sprintf("%t", params["anyRemote"])), - resource.TestCheckResourceAttr(fqrn, "criteria.0.any_federated", fmt.Sprintf("%t", params["anyFederated"])), - resource.TestCheckTypeSetElemAttr(fqrn, "criteria.0.repo_keys.*", repoName), - resource.TestCheckResourceAttr(fqrn, "criteria.0.include_patterns.#", "1"), - resource.TestCheckResourceAttr(fqrn, "criteria.0.include_patterns.0", "foo/**"), - resource.TestCheckResourceAttr(fqrn, "criteria.0.exclude_patterns.#", "1"), - resource.TestCheckResourceAttr(fqrn, "criteria.0.exclude_patterns.0", "bar/**"), - resource.TestCheckResourceAttr(fqrn, "handler.#", "3"), - resource.TestCheckResourceAttr(fqrn, "handler.0.url", "https://google.com"), - resource.TestCheckResourceAttr(fqrn, "handler.0.secrets.%", "2"), - resource.TestCheckResourceAttr(fqrn, "handler.0.secrets.secret1", "value1"), - resource.TestCheckResourceAttr(fqrn, "handler.0.secrets.secret2", "value2"), - resource.TestCheckResourceAttr(fqrn, "handler.0.http_headers.%", "2"), - resource.TestCheckResourceAttr(fqrn, "handler.0.http_headers.header-1", "value-1"), - resource.TestCheckResourceAttr(fqrn, "handler.0.http_headers.header-2", "value-2"), - resource.TestCheckResourceAttr(fqrn, "handler.0.payload", "{ \"ref\": \"main\" , \"inputs\": { \"artifact_path\": \"test-repo/repo-path/1\" } }"), - resource.TestCheckResourceAttr(fqrn, "handler.1.url", "https://yahoo.com"), - resource.TestCheckResourceAttr(fqrn, "handler.1.secrets.%", "2"), - resource.TestCheckResourceAttr(fqrn, "handler.1.secrets.secret3", "value3"), - resource.TestCheckResourceAttr(fqrn, "handler.1.secrets.secret4", "value4"), - resource.TestCheckResourceAttr(fqrn, "handler.1.http_headers.%", "2"), - resource.TestCheckResourceAttr(fqrn, "handler.1.http_headers.header-3", "value-3"), - resource.TestCheckResourceAttr(fqrn, "handler.1.http_headers.header-4", "value-4"), - resource.TestCheckResourceAttr(fqrn, "handler.1.payload", "{ \"ref\": \"main\" , \"inputs\": { \"artifact_path\": \"test-repo/repo-path/2\" } }"), - resource.TestCheckResourceAttr(fqrn, "handler.2.url", "https://msnbc.com"), - resource.TestCheckNoResourceAttr(fqrn, "handler.2.secrets"), - resource.TestCheckNoResourceAttr(fqrn, "handler.2.http_headers"), - resource.TestCheckResourceAttr(fqrn, "handler.2.payload", "{ \"ref\": \"main\" , \"inputs\": { \"artifact_path\": \"test-repo/repo-path/3\" } }"), - } - - for _, eventType := range eventTypes { - eventTypeCheck := resource.TestCheckTypeSetElemAttr(fqrn, "event_types.*", eventType) - testChecks = append(testChecks, eventTypeCheck) - } - - return t, resource.TestCase{ - PreCheck: func() { acctest.PreCheck(t) }, - CheckDestroy: acctest.VerifyDeleted(fqrn, "key", testCheckWebhook), - - Steps: []resource.TestStep{ - { - Config: config, - ExternalProviders: map[string]resource.ExternalProvider{ - "artifactory": { - Source: "jfrog/artifactory", - VersionConstraint: "12.1.0", - }, - }, - Check: resource.ComposeTestCheckFunc(testChecks...), - }, - { - Config: config, - ProtoV6ProviderFactories: acctest.ProtoV6MuxProviderFactories, - ConfigPlanChecks: resource.ConfigPlanChecks{ - PreApply: []plancheck.PlanCheck{ - plancheck.ExpectEmptyPlan(), - }, - }, - }, - }, - } -} - -func TestAccCustomWebhook_BuildWithIncludePatterns(t *testing.T) { - id := testutil.RandomInt() - name := fmt.Sprintf("webhook-%d", id) - fqrn := fmt.Sprintf("artifactory_build_custom_webhook.%s", name) - - params := map[string]interface{}{ - "webhookName": name, - } - webhookConfig := util.ExecuteTemplate("TestAccCustomWebhookBuildPatterns", ` - resource "artifactory_build_custom_webhook" "{{ .webhookName }}" { - key = "{{ .webhookName }}" - description = "test description" - event_types = ["uploaded"] - criteria { - any_build = false - selected_builds = [] - include_patterns = ["foo"] - } - handler { - url = "https://google.com" - payload = "{ \"ref\": \"main\" , \"inputs\": { \"artifact_path\": \"test-repo/repo-path\" } }" - } - } - `, params) - - resource.Test(t, resource.TestCase{ - PreCheck: func() { acctest.PreCheck(t) }, - ProviderFactories: acctest.ProviderFactories, - CheckDestroy: acctest.VerifyDeleted(fqrn, "", acctest.CheckRepo), - - Steps: []resource.TestStep{ - { - Config: webhookConfig, - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr(fqrn, "criteria.0.include_patterns.#", "1"), - resource.TestCheckResourceAttr(fqrn, "criteria.0.include_patterns.0", "foo"), - ), - }, - }, - }) -} - func TestAccCustomWebhook_User(t *testing.T) { _, fqrn, name := testutil.MkNames("test-user-webhook", "artifactory_user_custom_webhook") diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_webhook_build_test.go b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_build_test.go new file mode 100644 index 00000000..6180c05a --- /dev/null +++ b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_build_test.go @@ -0,0 +1,66 @@ +package webhook_test + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/jfrog/terraform-provider-artifactory/v12/pkg/acctest" + "github.com/jfrog/terraform-provider-shared/testutil" + "github.com/jfrog/terraform-provider-shared/util" +) + +var buildTemplate = ` + resource "artifactory_{{ .webhookType }}_webhook" "{{ .webhookName }}" { + key = "{{ .webhookName }}" + description = "test description" + event_types = [{{ range $index, $eventType := .eventTypes}}{{if $index}},{{end}}"{{$eventType}}"{{end}}] + criteria { + any_build = false + selected_builds = [] + } + handler { + url = "https://google.com" + } + } +` + +func TestAccWebhook_BuildWithIncludePatterns(t *testing.T) { + id := testutil.RandomInt() + name := fmt.Sprintf("webhook-%d", id) + fqrn := fmt.Sprintf("artifactory_build_webhook.%s", name) + + params := map[string]interface{}{ + "webhookName": name, + } + webhookConfig := util.ExecuteTemplate("TestAccWebhookBuildPatterns", ` + resource "artifactory_build_webhook" "{{ .webhookName }}" { + key = "{{ .webhookName }}" + description = "test description" + event_types = ["uploaded"] + criteria { + any_build = false + selected_builds = [] + include_patterns = ["foo"] + } + handler { + url = "https://google.com" + } + } + `, params) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ProtoV6ProviderFactories: acctest.ProtoV6MuxProviderFactories, + CheckDestroy: acctest.VerifyDeleted(fqrn, "key", acctest.CheckRepo), + Steps: []resource.TestStep{ + { + Config: webhookConfig, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(fqrn, "criteria.0.include_patterns.#", "1"), + resource.TestCheckResourceAttr(fqrn, "criteria.0.include_patterns.0", "foo"), + ), + }, + }, + }) +} diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_webhook_repo_test.go b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_repo_test.go new file mode 100644 index 00000000..3d4cbdf2 --- /dev/null +++ b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_repo_test.go @@ -0,0 +1,317 @@ +package webhook_test + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/plancheck" + "github.com/jfrog/terraform-provider-artifactory/v12/pkg/acctest" + "github.com/jfrog/terraform-provider-artifactory/v12/pkg/artifactory/resource/webhook" + "github.com/jfrog/terraform-provider-shared/testutil" + "github.com/jfrog/terraform-provider-shared/util" +) + +var domainRepoTypeLookup = map[string]string{ + "artifact": "generic", + "artifact_property": "generic", + "docker": "docker_v2", +} + +var repoTemplate = ` + resource "artifactory_{{ .webhookType }}_webhook" "{{ .webhookName }}" { + key = "{{ .webhookName }}" + description = "test description" + event_types = [{{ range $index, $eventType := .eventTypes}}{{if $index}},{{end}}"{{$eventType}}"{{end}}] + criteria { + any_local = false + any_remote = false + any_federated = false + repo_keys = [] + } + handler { + url = "https://google.com" + } + } +` + +func TestAccWebhook_AllRepoTypes(t *testing.T) { + // Can only realistically test these 3 types of webhook since creating + // build, release_bundle, or distribution in test environment is almost impossible + for _, webhookType := range []string{"artifact", "artifact_property", "docker"} { + t.Run(webhookType, func(t *testing.T) { + resource.Test(webhookTestCase(webhookType, t)) + }) + } +} + +func webhookTestCase(webhookType string, t *testing.T) (*testing.T, resource.TestCase) { + id := testutil.RandomInt() + name := fmt.Sprintf("webhook-%d", id) + fqrn := fmt.Sprintf("artifactory_%s_webhook.%s", webhookType, name) + + repoType := domainRepoTypeLookup[webhookType] + repoName := fmt.Sprintf("%s-local-%d", webhookType, id) + eventTypes := webhook.DomainEventTypesSupported[webhookType] + + params := map[string]interface{}{ + "repoName": repoName, + "repoType": repoType, + "webhookType": webhookType, + "webhookName": name, + "eventTypes": eventTypes, + "anyLocal": testutil.RandBool(), + "anyRemote": testutil.RandBool(), + "anyFederated": testutil.RandBool(), + } + webhookConfig := util.ExecuteTemplate("TestAccWebhook{{ .webhookType }}Type", ` + resource "artifactory_local_{{ .repoType }}_repository" "{{ .repoName }}" { + key = "{{ .repoName }}" + } + + resource "artifactory_{{ .webhookType }}_webhook" "{{ .webhookName }}" { + key = "{{ .webhookName }}" + description = "test description" + event_types = [{{ range $index, $eventType := .eventTypes}}{{if $index}},{{end}}"{{$eventType}}"{{end}}] + criteria { + any_local = {{ .anyLocal }} + any_remote = {{ .anyRemote }} + any_federated = {{ .anyFederated }} + repo_keys = [artifactory_local_{{ .repoType }}_repository.{{ .repoName }}.key] + include_patterns = ["foo/**"] + exclude_patterns = ["bar/**"] + } + handler { + url = "https://google.com" + secret = "fake-secret" + use_secret_for_signing = true + custom_http_headers = { + header-1 = "value-1" + header-2 = "value-2" + } + } + handler { + url = "https://tempurl.com" + } + } + `, params) + + updatedConfig := util.ExecuteTemplate("TestAccWebhook{{ .webhookType }}Type", ` + resource "artifactory_local_{{ .repoType }}_repository" "{{ .repoName }}" { + key = "{{ .repoName }}" + } + + resource "artifactory_{{ .webhookType }}_webhook" "{{ .webhookName }}" { + key = "{{ .webhookName }}" + description = "test description" + event_types = [{{ range $index, $eventType := .eventTypes}}{{if $index}},{{end}}"{{$eventType}}"{{end}}] + criteria { + any_local = {{ .anyLocal }} + any_remote = {{ .anyRemote }} + any_federated = {{ .anyFederated }} + repo_keys = [artifactory_local_{{ .repoType }}_repository.{{ .repoName }}.key] + } + handler { + url = "https://google.com" + secret = "fake-secret" + custom_http_headers = { + header-1 = "value-1" + header-2 = "value-2" + } + } + handler { + url = "https://tempurl.com" + } + } + `, params) + + testChecks := []resource.TestCheckFunc{ + resource.TestCheckResourceAttr(fqrn, "key", name), + resource.TestCheckResourceAttr(fqrn, "event_types.#", fmt.Sprintf("%d", len(eventTypes))), + resource.TestCheckResourceAttr(fqrn, "criteria.#", "1"), + resource.TestCheckResourceAttr(fqrn, "criteria.0.any_local", fmt.Sprintf("%t", params["anyLocal"])), + resource.TestCheckResourceAttr(fqrn, "criteria.0.any_remote", fmt.Sprintf("%t", params["anyRemote"])), + resource.TestCheckResourceAttr(fqrn, "criteria.0.any_federated", fmt.Sprintf("%t", params["anyFederated"])), + resource.TestCheckTypeSetElemAttr(fqrn, "criteria.0.repo_keys.*", repoName), + resource.TestCheckResourceAttr(fqrn, "criteria.0.include_patterns.#", "1"), + resource.TestCheckResourceAttr(fqrn, "criteria.0.include_patterns.0", "foo/**"), + resource.TestCheckResourceAttr(fqrn, "criteria.0.exclude_patterns.#", "1"), + resource.TestCheckResourceAttr(fqrn, "criteria.0.exclude_patterns.0", "bar/**"), + resource.TestCheckResourceAttr(fqrn, "handler.#", "2"), + resource.TestCheckResourceAttr(fqrn, "handler.0.url", "https://google.com"), + resource.TestCheckResourceAttr(fqrn, "handler.0.secret", "fake-secret"), + resource.TestCheckResourceAttr(fqrn, "handler.0.use_secret_for_signing", "true"), + resource.TestCheckResourceAttr(fqrn, "handler.0.custom_http_headers.%", "2"), + resource.TestCheckResourceAttr(fqrn, "handler.0.custom_http_headers.header-1", "value-1"), + resource.TestCheckResourceAttr(fqrn, "handler.0.custom_http_headers.header-2", "value-2"), + resource.TestCheckResourceAttr(fqrn, "handler.1.url", "https://tempurl.com"), + resource.TestCheckNoResourceAttr(fqrn, "handler.1.custom_http_headers"), + } + + updatedTestChecks := []resource.TestCheckFunc{ + resource.TestCheckResourceAttr(fqrn, "key", name), + resource.TestCheckResourceAttr(fqrn, "event_types.#", fmt.Sprintf("%d", len(eventTypes))), + resource.TestCheckResourceAttr(fqrn, "criteria.#", "1"), + resource.TestCheckResourceAttr(fqrn, "criteria.0.any_local", fmt.Sprintf("%t", params["anyLocal"])), + resource.TestCheckResourceAttr(fqrn, "criteria.0.any_remote", fmt.Sprintf("%t", params["anyRemote"])), + resource.TestCheckResourceAttr(fqrn, "criteria.0.any_federated", fmt.Sprintf("%t", params["anyFederated"])), + resource.TestCheckTypeSetElemAttr(fqrn, "criteria.0.repo_keys.*", repoName), + resource.TestCheckResourceAttr(fqrn, "criteria.0.include_patterns.#", "0"), + resource.TestCheckResourceAttr(fqrn, "criteria.0.exclude_patterns.#", "0"), + resource.TestCheckResourceAttr(fqrn, "handler.#", "2"), + resource.TestCheckResourceAttr(fqrn, "handler.0.url", "https://google.com"), + resource.TestCheckResourceAttr(fqrn, "handler.0.secret", "fake-secret"), + resource.TestCheckNoResourceAttr(fqrn, "handler.0.use_secret_for_signing"), + resource.TestCheckResourceAttr(fqrn, "handler.0.custom_http_headers.%", "2"), + resource.TestCheckResourceAttr(fqrn, "handler.0.custom_http_headers.header-1", "value-1"), + resource.TestCheckResourceAttr(fqrn, "handler.0.custom_http_headers.header-2", "value-2"), + resource.TestCheckResourceAttr(fqrn, "handler.1.url", "https://tempurl.com"), + resource.TestCheckResourceAttr(fqrn, "handler.1.custom_http_headers.#", "0"), + } + + for _, eventType := range eventTypes { + eventTypeCheck := resource.TestCheckTypeSetElemAttr(fqrn, "event_types.*", eventType) + testChecks = append(testChecks, eventTypeCheck) + } + + return t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ProtoV6ProviderFactories: acctest.ProtoV6MuxProviderFactories, + CheckDestroy: acctest.VerifyDeleted(fqrn, "key", testCheckWebhook), + Steps: []resource.TestStep{ + { + Config: webhookConfig, + Check: resource.ComposeTestCheckFunc(testChecks...), + }, + { + Config: updatedConfig, + Check: resource.ComposeTestCheckFunc(updatedTestChecks...), + }, + { + ResourceName: fqrn, + ImportState: true, + ImportStateId: name, + ImportStateVerify: true, + ImportStateVerifyIdentifierAttribute: "key", + ImportStateVerifyIgnore: []string{"handler.0.secret", "handler.1.secret"}, + }, + }, + } +} + +func TestAccWebhook_AllRepoTypes_UpgradeFromSDKv2(t *testing.T) { + // Can only realistically test these 3 types of webhook since creating + // build, release_bundle, or distribution in test environment is almost impossible + for _, webhookType := range []string{"artifact", "artifact_property", "docker"} { + t.Run(webhookType, func(t *testing.T) { + resource.Test(webhookMigrateFromSDKv2TestCase(webhookType, t)) + }) + } +} + +func webhookMigrateFromSDKv2TestCase(webhookType string, t *testing.T) (*testing.T, resource.TestCase) { + id := testutil.RandomInt() + name := fmt.Sprintf("webhook-%d", id) + fqrn := fmt.Sprintf("artifactory_%s_webhook.%s", webhookType, name) + + repoType := domainRepoTypeLookup[webhookType] + repoName := fmt.Sprintf("%s-local-%d", webhookType, id) + eventTypes := webhook.DomainEventTypesSupported[webhookType] + + params := map[string]interface{}{ + "repoName": repoName, + "repoType": repoType, + "webhookType": webhookType, + "webhookName": name, + "eventTypes": eventTypes, + "anyLocal": testutil.RandBool(), + "anyRemote": testutil.RandBool(), + "anyFederated": testutil.RandBool(), + } + config := util.ExecuteTemplate("TestAccWebhook{{ .webhookType }}Type", ` + resource "artifactory_local_{{ .repoType }}_repository" "{{ .repoName }}" { + key = "{{ .repoName }}" + } + + resource "artifactory_{{ .webhookType }}_webhook" "{{ .webhookName }}" { + key = "{{ .webhookName }}" + description = "test description" + event_types = [{{ range $index, $eventType := .eventTypes}}{{if $index}},{{end}}"{{$eventType}}"{{end}}] + criteria { + any_local = {{ .anyLocal }} + any_remote = {{ .anyRemote }} + any_federated = {{ .anyFederated }} + repo_keys = [artifactory_local_{{ .repoType }}_repository.{{ .repoName }}.key] + include_patterns = ["foo/**"] + exclude_patterns = ["bar/**"] + } + handler { + url = "https://google.com" + secret = "fake-secret" + use_secret_for_signing = true + custom_http_headers = { + header-1 = "value-1" + header-2 = "value-2" + } + } + handler { + url = "https://tempurl.com" + } + } + `, params) + + testChecks := []resource.TestCheckFunc{ + resource.TestCheckResourceAttr(fqrn, "key", name), + resource.TestCheckResourceAttr(fqrn, "event_types.#", fmt.Sprintf("%d", len(eventTypes))), + resource.TestCheckResourceAttr(fqrn, "criteria.#", "1"), + resource.TestCheckResourceAttr(fqrn, "criteria.0.any_local", fmt.Sprintf("%t", params["anyLocal"])), + resource.TestCheckResourceAttr(fqrn, "criteria.0.any_remote", fmt.Sprintf("%t", params["anyRemote"])), + resource.TestCheckResourceAttr(fqrn, "criteria.0.any_federated", fmt.Sprintf("%t", params["anyFederated"])), + resource.TestCheckTypeSetElemAttr(fqrn, "criteria.0.repo_keys.*", repoName), + resource.TestCheckResourceAttr(fqrn, "criteria.0.include_patterns.#", "1"), + resource.TestCheckResourceAttr(fqrn, "criteria.0.include_patterns.0", "foo/**"), + resource.TestCheckResourceAttr(fqrn, "criteria.0.exclude_patterns.#", "1"), + resource.TestCheckResourceAttr(fqrn, "criteria.0.exclude_patterns.0", "bar/**"), + resource.TestCheckResourceAttr(fqrn, "handler.#", "2"), + resource.TestCheckResourceAttr(fqrn, "handler.0.url", "https://google.com"), + resource.TestCheckResourceAttr(fqrn, "handler.0.secret", "fake-secret"), + resource.TestCheckResourceAttr(fqrn, "handler.0.use_secret_for_signing", "true"), + resource.TestCheckResourceAttr(fqrn, "handler.0.custom_http_headers.%", "2"), + resource.TestCheckResourceAttr(fqrn, "handler.0.custom_http_headers.header-1", "value-1"), + resource.TestCheckResourceAttr(fqrn, "handler.0.custom_http_headers.header-2", "value-2"), + resource.TestCheckResourceAttr(fqrn, "handler.1.url", "https://tempurl.com"), + resource.TestCheckResourceAttr(fqrn, "handler.1.secret", ""), + resource.TestCheckNoResourceAttr(fqrn, "handler.1.custom_http_headers"), + } + + for _, eventType := range eventTypes { + eventTypeCheck := resource.TestCheckTypeSetElemAttr(fqrn, "event_types.*", eventType) + testChecks = append(testChecks, eventTypeCheck) + } + + return t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + CheckDestroy: acctest.VerifyDeleted(fqrn, "key", testCheckWebhook), + Steps: []resource.TestStep{ + { + Config: config, + ExternalProviders: map[string]resource.ExternalProvider{ + "artifactory": { + Source: "jfrog/artifactory", + VersionConstraint: "12.1.0", + }, + }, + Check: resource.ComposeTestCheckFunc(testChecks...), + }, + { + Config: config, + ProtoV6ProviderFactories: acctest.ProtoV6MuxProviderFactories, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectEmptyPlan(), + }, + }, + }, + }, + } +} diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_webhook_test.go b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_test.go index 9e006741..20a15900 100644 --- a/pkg/artifactory/resource/webhook/resource_artifactory_webhook_test.go +++ b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_test.go @@ -7,7 +7,6 @@ import ( "github.com/go-resty/resty/v2" "github.com/hashicorp/terraform-plugin-testing/helper/resource" - "github.com/hashicorp/terraform-plugin-testing/plancheck" "github.com/jfrog/terraform-provider-artifactory/v12/pkg/acctest" "github.com/jfrog/terraform-provider-artifactory/v12/pkg/artifactory/resource/webhook" "github.com/jfrog/terraform-provider-shared/client" @@ -15,12 +14,6 @@ import ( "github.com/jfrog/terraform-provider-shared/util" ) -var domainRepoTypeLookup = map[string]string{ - "artifact": "generic", - "artifact_property": "generic", - "docker": "docker_v2", -} - var domainValidationErrorMessageLookup = map[string]string{ "artifact": `repo_keys cannot be empty when any_local, any_remote, and any_federated are\s*false`, "artifact_property": `repo_keys cannot be empty when any_local, any_remote, and any_federated are\s*false`, @@ -31,38 +24,6 @@ var domainValidationErrorMessageLookup = map[string]string{ "artifactory_release_bundle": `registered_release_bundle_names cannot be empty when any_release_bundle is\s*false`, } -var repoTemplate = ` - resource "artifactory_{{ .webhookType }}_webhook" "{{ .webhookName }}" { - key = "{{ .webhookName }}" - description = "test description" - event_types = [{{ range $index, $eventType := .eventTypes}}{{if $index}},{{end}}"{{$eventType}}"{{end}}] - criteria { - any_local = false - any_remote = false - any_federated = false - repo_keys = [] - } - handler { - url = "https://google.com" - } - } -` - -var buildTemplate = ` - resource "artifactory_{{ .webhookType }}_webhook" "{{ .webhookName }}" { - key = "{{ .webhookName }}" - description = "test description" - event_types = [{{ range $index, $eventType := .eventTypes}}{{if $index}},{{end}}"{{$eventType}}"{{end}}] - criteria { - any_build = false - selected_builds = [] - } - handler { - url = "https://google.com" - } - } -` - var releaseBundleTemplate = ` resource "artifactory_{{ .webhookType }}_webhook" "{{ .webhookName }}" { key = "{{ .webhookName }}" @@ -258,327 +219,6 @@ func TestAccWebhook_HandlerValidation_ProxyWithURL(t *testing.T) { }) } -func TestAccWebhook_BuildWithIncludePatterns(t *testing.T) { - id := testutil.RandomInt() - name := fmt.Sprintf("webhook-%d", id) - fqrn := fmt.Sprintf("artifactory_build_webhook.%s", name) - - params := map[string]interface{}{ - "webhookName": name, - } - webhookConfig := util.ExecuteTemplate("TestAccWebhookBuildPatterns", ` - resource "artifactory_build_webhook" "{{ .webhookName }}" { - key = "{{ .webhookName }}" - description = "test description" - event_types = ["uploaded"] - criteria { - any_build = false - selected_builds = [] - include_patterns = ["foo"] - } - handler { - url = "https://google.com" - } - } - `, params) - - resource.Test(t, resource.TestCase{ - PreCheck: func() { acctest.PreCheck(t) }, - ProtoV6ProviderFactories: acctest.ProtoV6MuxProviderFactories, - CheckDestroy: acctest.VerifyDeleted(fqrn, "key", acctest.CheckRepo), - Steps: []resource.TestStep{ - { - Config: webhookConfig, - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr(fqrn, "criteria.0.include_patterns.#", "1"), - resource.TestCheckResourceAttr(fqrn, "criteria.0.include_patterns.0", "foo"), - ), - }, - }, - }) -} - -func TestAccWebhook_AllTypes(t *testing.T) { - // Can only realistically test these 3 types of webhook since creating - // build, release_bundle, or distribution in test environment is almost impossible - for _, webhookType := range []string{"artifact", "artifact_property", "docker"} { - t.Run(webhookType, func(t *testing.T) { - resource.Test(webhookTestCase(webhookType, t)) - }) - } -} - -func webhookTestCase(webhookType string, t *testing.T) (*testing.T, resource.TestCase) { - id := testutil.RandomInt() - name := fmt.Sprintf("webhook-%d", id) - fqrn := fmt.Sprintf("artifactory_%s_webhook.%s", webhookType, name) - - repoType := domainRepoTypeLookup[webhookType] - repoName := fmt.Sprintf("%s-local-%d", webhookType, id) - eventTypes := webhook.DomainEventTypesSupported[webhookType] - - params := map[string]interface{}{ - "repoName": repoName, - "repoType": repoType, - "webhookType": webhookType, - "webhookName": name, - "eventTypes": eventTypes, - "anyLocal": testutil.RandBool(), - "anyRemote": testutil.RandBool(), - "anyFederated": testutil.RandBool(), - } - webhookConfig := util.ExecuteTemplate("TestAccWebhook{{ .webhookType }}Type", ` - resource "artifactory_local_{{ .repoType }}_repository" "{{ .repoName }}" { - key = "{{ .repoName }}" - } - - resource "artifactory_{{ .webhookType }}_webhook" "{{ .webhookName }}" { - key = "{{ .webhookName }}" - description = "test description" - event_types = [{{ range $index, $eventType := .eventTypes}}{{if $index}},{{end}}"{{$eventType}}"{{end}}] - criteria { - any_local = {{ .anyLocal }} - any_remote = {{ .anyRemote }} - any_federated = {{ .anyFederated }} - repo_keys = [artifactory_local_{{ .repoType }}_repository.{{ .repoName }}.key] - include_patterns = ["foo/**"] - exclude_patterns = ["bar/**"] - } - handler { - url = "https://google.com" - secret = "fake-secret" - use_secret_for_signing = true - custom_http_headers = { - header-1 = "value-1" - header-2 = "value-2" - } - } - handler { - url = "https://tempurl.com" - } - } - `, params) - - updatedConfig := util.ExecuteTemplate("TestAccWebhook{{ .webhookType }}Type", ` - resource "artifactory_local_{{ .repoType }}_repository" "{{ .repoName }}" { - key = "{{ .repoName }}" - } - - resource "artifactory_{{ .webhookType }}_webhook" "{{ .webhookName }}" { - key = "{{ .webhookName }}" - description = "test description" - event_types = [{{ range $index, $eventType := .eventTypes}}{{if $index}},{{end}}"{{$eventType}}"{{end}}] - criteria { - any_local = {{ .anyLocal }} - any_remote = {{ .anyRemote }} - any_federated = {{ .anyFederated }} - repo_keys = [artifactory_local_{{ .repoType }}_repository.{{ .repoName }}.key] - } - handler { - url = "https://google.com" - secret = "fake-secret" - custom_http_headers = { - header-1 = "value-1" - header-2 = "value-2" - } - } - handler { - url = "https://tempurl.com" - } - } - `, params) - - testChecks := []resource.TestCheckFunc{ - resource.TestCheckResourceAttr(fqrn, "key", name), - resource.TestCheckResourceAttr(fqrn, "event_types.#", fmt.Sprintf("%d", len(eventTypes))), - resource.TestCheckResourceAttr(fqrn, "criteria.#", "1"), - resource.TestCheckResourceAttr(fqrn, "criteria.0.any_local", fmt.Sprintf("%t", params["anyLocal"])), - resource.TestCheckResourceAttr(fqrn, "criteria.0.any_remote", fmt.Sprintf("%t", params["anyRemote"])), - resource.TestCheckResourceAttr(fqrn, "criteria.0.any_federated", fmt.Sprintf("%t", params["anyFederated"])), - resource.TestCheckTypeSetElemAttr(fqrn, "criteria.0.repo_keys.*", repoName), - resource.TestCheckResourceAttr(fqrn, "criteria.0.include_patterns.#", "1"), - resource.TestCheckResourceAttr(fqrn, "criteria.0.include_patterns.0", "foo/**"), - resource.TestCheckResourceAttr(fqrn, "criteria.0.exclude_patterns.#", "1"), - resource.TestCheckResourceAttr(fqrn, "criteria.0.exclude_patterns.0", "bar/**"), - resource.TestCheckResourceAttr(fqrn, "handler.#", "2"), - resource.TestCheckResourceAttr(fqrn, "handler.0.url", "https://google.com"), - resource.TestCheckResourceAttr(fqrn, "handler.0.secret", "fake-secret"), - resource.TestCheckResourceAttr(fqrn, "handler.0.use_secret_for_signing", "true"), - resource.TestCheckResourceAttr(fqrn, "handler.0.custom_http_headers.%", "2"), - resource.TestCheckResourceAttr(fqrn, "handler.0.custom_http_headers.header-1", "value-1"), - resource.TestCheckResourceAttr(fqrn, "handler.0.custom_http_headers.header-2", "value-2"), - resource.TestCheckResourceAttr(fqrn, "handler.1.url", "https://tempurl.com"), - resource.TestCheckNoResourceAttr(fqrn, "handler.1.custom_http_headers"), - } - - updatedTestChecks := []resource.TestCheckFunc{ - resource.TestCheckResourceAttr(fqrn, "key", name), - resource.TestCheckResourceAttr(fqrn, "event_types.#", fmt.Sprintf("%d", len(eventTypes))), - resource.TestCheckResourceAttr(fqrn, "criteria.#", "1"), - resource.TestCheckResourceAttr(fqrn, "criteria.0.any_local", fmt.Sprintf("%t", params["anyLocal"])), - resource.TestCheckResourceAttr(fqrn, "criteria.0.any_remote", fmt.Sprintf("%t", params["anyRemote"])), - resource.TestCheckResourceAttr(fqrn, "criteria.0.any_federated", fmt.Sprintf("%t", params["anyFederated"])), - resource.TestCheckTypeSetElemAttr(fqrn, "criteria.0.repo_keys.*", repoName), - resource.TestCheckResourceAttr(fqrn, "criteria.0.include_patterns.#", "0"), - resource.TestCheckResourceAttr(fqrn, "criteria.0.exclude_patterns.#", "0"), - resource.TestCheckResourceAttr(fqrn, "handler.#", "2"), - resource.TestCheckResourceAttr(fqrn, "handler.0.url", "https://google.com"), - resource.TestCheckResourceAttr(fqrn, "handler.0.secret", "fake-secret"), - resource.TestCheckNoResourceAttr(fqrn, "handler.0.use_secret_for_signing"), - resource.TestCheckResourceAttr(fqrn, "handler.0.custom_http_headers.%", "2"), - resource.TestCheckResourceAttr(fqrn, "handler.0.custom_http_headers.header-1", "value-1"), - resource.TestCheckResourceAttr(fqrn, "handler.0.custom_http_headers.header-2", "value-2"), - resource.TestCheckResourceAttr(fqrn, "handler.1.url", "https://tempurl.com"), - resource.TestCheckResourceAttr(fqrn, "handler.1.custom_http_headers.#", "0"), - } - - for _, eventType := range eventTypes { - eventTypeCheck := resource.TestCheckTypeSetElemAttr(fqrn, "event_types.*", eventType) - testChecks = append(testChecks, eventTypeCheck) - } - - return t, resource.TestCase{ - PreCheck: func() { acctest.PreCheck(t) }, - ProtoV6ProviderFactories: acctest.ProtoV6MuxProviderFactories, - CheckDestroy: acctest.VerifyDeleted(fqrn, "key", testCheckWebhook), - Steps: []resource.TestStep{ - { - Config: webhookConfig, - Check: resource.ComposeTestCheckFunc(testChecks...), - }, - { - Config: updatedConfig, - Check: resource.ComposeTestCheckFunc(updatedTestChecks...), - }, - { - ResourceName: fqrn, - ImportState: true, - ImportStateId: name, - ImportStateVerify: true, - ImportStateVerifyIdentifierAttribute: "key", - ImportStateVerifyIgnore: []string{"handler.0.secret", "handler.1.secret"}, - }, - }, - } -} - -func TestAccWebhook_UpgradeFromSDKv2(t *testing.T) { - // Can only realistically test these 3 types of webhook since creating - // build, release_bundle, or distribution in test environment is almost impossible - for _, webhookType := range []string{"artifact", "artifact_property", "docker"} { - t.Run(webhookType, func(t *testing.T) { - resource.Test(webhookMigrateFromSDKv2TestCase(webhookType, t)) - }) - } -} - -func webhookMigrateFromSDKv2TestCase(webhookType string, t *testing.T) (*testing.T, resource.TestCase) { - id := testutil.RandomInt() - name := fmt.Sprintf("webhook-%d", id) - fqrn := fmt.Sprintf("artifactory_%s_webhook.%s", webhookType, name) - - repoType := domainRepoTypeLookup[webhookType] - repoName := fmt.Sprintf("%s-local-%d", webhookType, id) - eventTypes := webhook.DomainEventTypesSupported[webhookType] - - params := map[string]interface{}{ - "repoName": repoName, - "repoType": repoType, - "webhookType": webhookType, - "webhookName": name, - "eventTypes": eventTypes, - "anyLocal": testutil.RandBool(), - "anyRemote": testutil.RandBool(), - "anyFederated": testutil.RandBool(), - } - config := util.ExecuteTemplate("TestAccWebhook{{ .webhookType }}Type", ` - resource "artifactory_local_{{ .repoType }}_repository" "{{ .repoName }}" { - key = "{{ .repoName }}" - } - - resource "artifactory_{{ .webhookType }}_webhook" "{{ .webhookName }}" { - key = "{{ .webhookName }}" - description = "test description" - event_types = [{{ range $index, $eventType := .eventTypes}}{{if $index}},{{end}}"{{$eventType}}"{{end}}] - criteria { - any_local = {{ .anyLocal }} - any_remote = {{ .anyRemote }} - any_federated = {{ .anyFederated }} - repo_keys = [artifactory_local_{{ .repoType }}_repository.{{ .repoName }}.key] - include_patterns = ["foo/**"] - exclude_patterns = ["bar/**"] - } - handler { - url = "https://google.com" - secret = "fake-secret" - use_secret_for_signing = true - custom_http_headers = { - header-1 = "value-1" - header-2 = "value-2" - } - } - handler { - url = "https://tempurl.com" - } - } - `, params) - - testChecks := []resource.TestCheckFunc{ - resource.TestCheckResourceAttr(fqrn, "key", name), - resource.TestCheckResourceAttr(fqrn, "event_types.#", fmt.Sprintf("%d", len(eventTypes))), - resource.TestCheckResourceAttr(fqrn, "criteria.#", "1"), - resource.TestCheckResourceAttr(fqrn, "criteria.0.any_local", fmt.Sprintf("%t", params["anyLocal"])), - resource.TestCheckResourceAttr(fqrn, "criteria.0.any_remote", fmt.Sprintf("%t", params["anyRemote"])), - resource.TestCheckResourceAttr(fqrn, "criteria.0.any_federated", fmt.Sprintf("%t", params["anyFederated"])), - resource.TestCheckTypeSetElemAttr(fqrn, "criteria.0.repo_keys.*", repoName), - resource.TestCheckResourceAttr(fqrn, "criteria.0.include_patterns.#", "1"), - resource.TestCheckResourceAttr(fqrn, "criteria.0.include_patterns.0", "foo/**"), - resource.TestCheckResourceAttr(fqrn, "criteria.0.exclude_patterns.#", "1"), - resource.TestCheckResourceAttr(fqrn, "criteria.0.exclude_patterns.0", "bar/**"), - resource.TestCheckResourceAttr(fqrn, "handler.#", "2"), - resource.TestCheckResourceAttr(fqrn, "handler.0.url", "https://google.com"), - resource.TestCheckResourceAttr(fqrn, "handler.0.secret", "fake-secret"), - resource.TestCheckResourceAttr(fqrn, "handler.0.use_secret_for_signing", "true"), - resource.TestCheckResourceAttr(fqrn, "handler.0.custom_http_headers.%", "2"), - resource.TestCheckResourceAttr(fqrn, "handler.0.custom_http_headers.header-1", "value-1"), - resource.TestCheckResourceAttr(fqrn, "handler.0.custom_http_headers.header-2", "value-2"), - resource.TestCheckResourceAttr(fqrn, "handler.1.url", "https://tempurl.com"), - resource.TestCheckResourceAttr(fqrn, "handler.1.secret", ""), - resource.TestCheckNoResourceAttr(fqrn, "handler.1.custom_http_headers"), - } - - for _, eventType := range eventTypes { - eventTypeCheck := resource.TestCheckTypeSetElemAttr(fqrn, "event_types.*", eventType) - testChecks = append(testChecks, eventTypeCheck) - } - - return t, resource.TestCase{ - PreCheck: func() { acctest.PreCheck(t) }, - CheckDestroy: acctest.VerifyDeleted(fqrn, "key", testCheckWebhook), - Steps: []resource.TestStep{ - { - Config: config, - ExternalProviders: map[string]resource.ExternalProvider{ - "artifactory": { - Source: "jfrog/artifactory", - VersionConstraint: "12.1.0", - }, - }, - Check: resource.ComposeTestCheckFunc(testChecks...), - }, - { - Config: config, - ProtoV6ProviderFactories: acctest.ProtoV6MuxProviderFactories, - ConfigPlanChecks: resource.ConfigPlanChecks{ - PreApply: []plancheck.PlanCheck{ - plancheck.ExpectEmptyPlan(), - }, - }, - }, - }, - } -} - func testCheckWebhook(id string, request *resty.Request) (*resty.Response, error) { return request. SetPathParam("webhookKey", id). From bc63c34938334674b69c7c41bdb4533b88e7ddd4 Mon Sep 17 00:00:00 2001 From: Alex Hung Date: Tue, 1 Oct 2024 09:42:54 -0700 Subject: [PATCH 05/12] Migrate release bundle type webhook resources to Plugin Framework --- pkg/artifactory/provider/framework.go | 5 + .../resource_artifactory_custom_webhook.go | 107 -------- ...source_artifactory_custom_webhook_build.go | 10 +- ...e_artifactory_custom_webhook_build_test.go | 6 +- ...tifactory_custom_webhook_release_bundle.go | 243 ++++++++++++++++++ ...esource_artifactory_custom_webhook_repo.go | 8 +- .../resource_artifactory_webhook_build.go | 12 +- ...resource_artifactory_webhook_build_test.go | 2 +- ...urce_artifactory_webhook_release_bundle.go | 125 +++++---- ...e_artifactory_webhook_release_bundle_v2.go | 10 +- ...ory_webhook_release_bundle_v2_promotion.go | 6 +- .../resource_artifactory_webhook_repo.go | 12 +- 12 files changed, 355 insertions(+), 191 deletions(-) create mode 100644 pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook_release_bundle.go diff --git a/pkg/artifactory/provider/framework.go b/pkg/artifactory/provider/framework.go index 0a57c7ab..bc581edf 100644 --- a/pkg/artifactory/provider/framework.go +++ b/pkg/artifactory/provider/framework.go @@ -228,12 +228,17 @@ func (p *ArtifactoryProvider) Resources(ctx context.Context) []func() resource.R webhook.NewArtifactPropertyWebhookResource, webhook.NewArtifactPropertyCustomWebhookResource, webhook.NewArtifactoryReleaseBundleWebhookResource, + webhook.NewArtifactoryReleaseBundleCustomWebhookResource, webhook.NewBuildWebhookResource, + webhook.NewBuildCustomWebhookResource, webhook.NewDestinationWebhookResource, + webhook.NewDestinationCustomWebhookResource, webhook.NewDistributionWebhookResource, + webhook.NewDistributionCustomWebhookResource, webhook.NewDockerWebhookResource, webhook.NewDockerCustomWebhookResource, webhook.NewReleaseBundleWebhookResource, + webhook.NewReleaseBundleCustomWebhookResource, webhook.NewReleaseBundleV2WebhookResource, webhook.NewReleaseBundleV2PromotionWebhookResource, webhook.NewUserWebhookResource, diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook.go b/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook.go index 603a14f9..6a18db7a 100644 --- a/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook.go +++ b/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook.go @@ -292,11 +292,6 @@ func (m *CustomWebhookResourceModel) fromAPIModel(ctx context.Context, apiModel var DomainSupported = []string{ ArtifactLifecycleDomain, - ArtifactoryReleaseBundleDomain, - BuildDomain, - DestinationDomain, - DistributionDomain, - ReleaseBundleDomain, ReleaseBundleV2Domain, ReleaseBundleV2PromotionDomain, UserDomain, @@ -388,58 +383,6 @@ func baseCustomWebhookBaseSchema(webhookType string) map[string]*sdkv2_schema.Sc } } -var buildWebhookSchema = func(webhookType string, version int, isCustom bool) map[string]*sdkv2_schema.Schema { - return utilsdk.MergeMaps(getBaseSchemaByVersion(webhookType, version, isCustom), map[string]*sdkv2_schema.Schema{ - "criteria": { - Type: sdkv2_schema.TypeSet, - Required: true, - MaxItems: 1, - Elem: &sdkv2_schema.Resource{ - Schema: utilsdk.MergeMaps(baseCriteriaSchema, map[string]*sdkv2_schema.Schema{ - "any_build": { - Type: sdkv2_schema.TypeBool, - Required: true, - Description: "Trigger on any builds", - }, - "selected_builds": { - Type: sdkv2_schema.TypeSet, - Required: true, - Elem: &sdkv2_schema.Schema{Type: sdkv2_schema.TypeString}, - Description: "Trigger on this list of build IDs", - }, - }), - }, - Description: "Specifies where the webhook will be applied on which builds.", - }, - }) -} - -var releaseBundleWebhookSchema = func(webhookType string, version int, isCustom bool) map[string]*sdkv2_schema.Schema { - return utilsdk.MergeMaps(getBaseSchemaByVersion(webhookType, version, isCustom), map[string]*sdkv2_schema.Schema{ - "criteria": { - Type: sdkv2_schema.TypeSet, - Required: true, - MaxItems: 1, - Elem: &sdkv2_schema.Resource{ - Schema: utilsdk.MergeMaps(baseCriteriaSchema, map[string]*sdkv2_schema.Schema{ - "any_release_bundle": { - Type: sdkv2_schema.TypeBool, - Required: true, - Description: "Trigger on any release bundles or distributions", - }, - "registered_release_bundle_names": { - Type: sdkv2_schema.TypeSet, - Required: true, - Elem: &sdkv2_schema.Schema{Type: sdkv2_schema.TypeString}, - Description: "Trigger on this list of release bundle names", - }, - }), - }, - Description: "Specifies where the webhook will be applied, on which release bundles or distributions.", - }, - }) -} - var releaseBundleV2WebhookSchema = func(webhookType string, version int, isCustom bool) map[string]*sdkv2_schema.Schema { return utilsdk.MergeMaps(getBaseSchemaByVersion(webhookType, version, isCustom), map[string]*sdkv2_schema.Schema{ "criteria": { @@ -838,11 +781,6 @@ var packKeyValuePair = func(keyValuePairs []KeyValuePairAPIModel) map[string]int type EmptyWebhookCriteria struct{} var domainCriteriaLookup = map[string]interface{}{ - "build": BuildCriteriaAPIModel{}, - "release_bundle": ReleaseBundleCriteriaAPIModel{}, - "distribution": ReleaseBundleCriteriaAPIModel{}, - "artifactory_release_bundle": ReleaseBundleCriteriaAPIModel{}, - "destination": ReleaseBundleCriteriaAPIModel{}, "user": EmptyWebhookCriteria{}, "release_bundle_v2": ReleaseBundleV2CriteriaAPIModel{}, "release_bundle_v2_promotion": ReleaseBundleV2PromotionCriteriaAPIModel{}, @@ -850,11 +788,6 @@ var domainCriteriaLookup = map[string]interface{}{ } var domainPackLookup = map[string]func(map[string]interface{}) map[string]interface{}{ - "build": packBuildCriteria, - "release_bundle": packReleaseBundleCriteria, - "distribution": packReleaseBundleCriteria, - "artifactory_release_bundle": packReleaseBundleCriteria, - "destination": packReleaseBundleCriteria, "user": packEmptyCriteria, "release_bundle_v2": packReleaseBundleV2Criteria, "release_bundle_v2_promotion": packReleaseBundleV2PromotionCriteria, @@ -862,11 +795,6 @@ var domainPackLookup = map[string]func(map[string]interface{}) map[string]interf } var domainUnpackLookup = map[string]func(map[string]interface{}, BaseCriteriaAPIModel) interface{}{ - "build": unpackBuildCriteria, - "release_bundle": unpackReleaseBundleCriteria, - "distribution": unpackReleaseBundleCriteria, - "artifactory_release_bundle": unpackReleaseBundleCriteria, - "destination": unpackReleaseBundleCriteria, "user": unpackEmptyCriteria, "release_bundle_v2": unpackReleaseBundleV2Criteria, "release_bundle_v2_promotion": unpackReleaseBundleV2PromotionCriteria, @@ -875,11 +803,6 @@ var domainUnpackLookup = map[string]func(map[string]interface{}, BaseCriteriaAPI var domainSchemaLookup = func(version int, isCustom bool, webhookType string) map[string]map[string]*sdkv2_schema.Schema { return map[string]map[string]*sdkv2_schema.Schema{ - "build": buildWebhookSchema(webhookType, version, isCustom), - "release_bundle": releaseBundleWebhookSchema(webhookType, version, isCustom), - "distribution": releaseBundleWebhookSchema(webhookType, version, isCustom), - "artifactory_release_bundle": releaseBundleWebhookSchema(webhookType, version, isCustom), - "destination": releaseBundleWebhookSchema(webhookType, version, isCustom), "user": userWebhookSchema(webhookType, version, isCustom), "release_bundle_v2": releaseBundleV2WebhookSchema(webhookType, version, isCustom), "release_bundle_v2_promotion": releaseBundleV2PromotionWebhookSchema(webhookType, version, isCustom), @@ -887,36 +810,6 @@ var domainSchemaLookup = func(version int, isCustom bool, webhookType string) ma } } -var packReleaseBundleCriteria = func(artifactoryCriteria map[string]interface{}) map[string]interface{} { - return map[string]interface{}{ - "any_release_bundle": artifactoryCriteria["anyReleaseBundle"].(bool), - "registered_release_bundle_names": sdkv2_schema.NewSet(sdkv2_schema.HashString, artifactoryCriteria["registeredReleaseBundlesNames"].([]interface{})), - } -} - -var unpackReleaseBundleCriteria = func(terraformCriteria map[string]interface{}, baseCriteria BaseCriteriaAPIModel) interface{} { - return ReleaseBundleCriteriaAPIModel{ - AnyReleaseBundle: terraformCriteria["any_release_bundle"].(bool), - RegisteredReleaseBundlesNames: utilsdk.CastToStringArr(terraformCriteria["registered_release_bundle_names"].(*sdkv2_schema.Set).List()), - BaseCriteriaAPIModel: baseCriteria, - } -} - -var packBuildCriteria = func(artifactoryCriteria map[string]interface{}) map[string]interface{} { - return map[string]interface{}{ - "any_build": artifactoryCriteria["anyBuild"].(bool), - "selected_builds": sdkv2_schema.NewSet(sdkv2_schema.HashString, artifactoryCriteria["selectedBuilds"].([]interface{})), - } -} - -var unpackBuildCriteria = func(terraformCriteria map[string]interface{}, baseCriteria BaseCriteriaAPIModel) interface{} { - return BuildCriteriaAPIModel{ - AnyBuild: terraformCriteria["any_build"].(bool), - SelectedBuilds: utilsdk.CastToStringArr(terraformCriteria["selected_builds"].(*sdkv2_schema.Set).List()), - BaseCriteriaAPIModel: baseCriteria, - } -} - var packReleaseBundleV2Criteria = func(artifactoryCriteria map[string]interface{}) map[string]interface{} { return map[string]interface{}{ "any_release_bundle": artifactoryCriteria["anyReleaseBundle"].(bool), diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook_build.go b/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook_build.go index 2aaa28ab..a50468f0 100644 --- a/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook_build.go +++ b/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook_build.go @@ -14,7 +14,7 @@ import ( var _ resource.Resource = &BuildCustomWebhookResource{} -func NewCustomBuildWebhookResource() resource.Resource { +func NewBuildCustomWebhookResource() resource.Resource { return &BuildCustomWebhookResource{ CustomWebhookResource: CustomWebhookResource{ WebhookResource: WebhookResource{ @@ -173,15 +173,15 @@ func (r *BuildCustomWebhookResource) ImportState(ctx context.Context, req resour } func (m BuildCustomWebhookResourceModel) toAPIModel(ctx context.Context, domain string, apiModel *CustomWebhookAPIModel) (diags diag.Diagnostics) { - critieriaObj := m.Criteria.Elements()[0].(types.Object) - critieriaAttrs := critieriaObj.Attributes() + criteriaObj := m.Criteria.Elements()[0].(types.Object) + criteriaAttrs := criteriaObj.Attributes() - baseCriteria, d := m.CustomWebhookResourceModel.toBaseCriteriaAPIModel(ctx, critieriaAttrs) + baseCriteria, d := m.CustomWebhookResourceModel.toBaseCriteriaAPIModel(ctx, criteriaAttrs) if d.HasError() { diags.Append(d...) } - criteriaAPIModel, d := toBuildCriteriaAPIModel(ctx, baseCriteria, critieriaAttrs) + criteriaAPIModel, d := toBuildCriteriaAPIModel(ctx, baseCriteria, criteriaAttrs) if d.HasError() { diags.Append(d...) } diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook_build_test.go b/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook_build_test.go index 8a907f2e..627a887b 100644 --- a/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook_build_test.go +++ b/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook_build_test.go @@ -36,9 +36,9 @@ func TestAccCustomWebhook_BuildWithIncludePatterns(t *testing.T) { `, params) resource.Test(t, resource.TestCase{ - PreCheck: func() { acctest.PreCheck(t) }, - ProviderFactories: acctest.ProviderFactories, - CheckDestroy: acctest.VerifyDeleted(fqrn, "", acctest.CheckRepo), + PreCheck: func() { acctest.PreCheck(t) }, + ProtoV6ProviderFactories: acctest.ProtoV6ProviderFactories, + CheckDestroy: acctest.VerifyDeleted(fqrn, "", acctest.CheckRepo), Steps: []resource.TestStep{ { diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook_release_bundle.go b/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook_release_bundle.go new file mode 100644 index 00000000..00c006dd --- /dev/null +++ b/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook_release_bundle.go @@ -0,0 +1,243 @@ +package webhook + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + "github.com/jfrog/terraform-provider-shared/util" +) + +var _ resource.Resource = &ReleaseBundleCustomWebhookResource{} + +func NewArtifactoryReleaseBundleCustomWebhookResource() resource.Resource { + return &ReleaseBundleCustomWebhookResource{ + CustomWebhookResource: CustomWebhookResource{ + WebhookResource: WebhookResource{ + TypeName: fmt.Sprintf("artifactory_%s_custom_webhook", ArtifactoryReleaseBundleDomain), + Domain: ArtifactoryReleaseBundleDomain, + Description: "Provides an Artifactory webhook resource. This can be used to register and manage Artifactory webhook subscription which enables you to be notified or notify other users when such events take place in Artifactory.:", + }, + }, + } +} + +func NewDestinationCustomWebhookResource() resource.Resource { + return &ReleaseBundleCustomWebhookResource{ + CustomWebhookResource: CustomWebhookResource{ + WebhookResource: WebhookResource{ + TypeName: fmt.Sprintf("artifactory_%s_custom_webhook", DestinationDomain), + Domain: DestinationDomain, + Description: "Provides an Artifactory webhook resource. This can be used to register and manage Artifactory webhook subscription which enables you to be notified or notify other users when such events take place in Artifactory.:", + }, + }, + } +} + +func NewDistributionCustomWebhookResource() resource.Resource { + return &ReleaseBundleCustomWebhookResource{ + CustomWebhookResource: CustomWebhookResource{ + WebhookResource: WebhookResource{ + TypeName: fmt.Sprintf("artifactory_%s_custom_webhook", DistributionDomain), + Domain: DistributionDomain, + Description: "Provides an Artifactory webhook resource. This can be used to register and manage Artifactory webhook subscription which enables you to be notified or notify other users when such events take place in Artifactory.:", + }, + }, + } +} + +func NewReleaseBundleCustomWebhookResource() resource.Resource { + return &ReleaseBundleCustomWebhookResource{ + CustomWebhookResource: CustomWebhookResource{ + WebhookResource: WebhookResource{ + TypeName: fmt.Sprintf("artifactory_%s_custom_webhook", ReleaseBundleDomain), + Domain: ReleaseBundleDomain, + Description: "Provides an Artifactory webhook resource. This can be used to register and manage Artifactory webhook subscription which enables you to be notified or notify other users when such events take place in Artifactory.\n\n" + + "!>This resource is being deprecated and replaced by `artifactory_destination_webhook` resource.", + }, + }, + } +} + +type ReleaseBundleCustomWebhookResourceModel struct { + CustomWebhookResourceModel +} + +type ReleaseBundleCustomWebhookResource struct { + CustomWebhookResource +} + +func (r *ReleaseBundleCustomWebhookResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + r.WebhookResource.Metadata(ctx, req, resp) +} + +func (r *ReleaseBundleCustomWebhookResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = r.CreateSchema(r.Domain, &releaseBundleCriteriaBlock) + if r.Domain == ReleaseBundleDomain { + resp.Schema.DeprecationMessage = "This resource is being deprecated and replaced by artifactory_destination_webhook resource" + } +} + +func (r *ReleaseBundleCustomWebhookResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + r.WebhookResource.Configure(ctx, req, resp) +} + +func (r ReleaseBundleCustomWebhookResource) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) { + var data ReleaseBundleCustomWebhookResourceModel + + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + releaseBundleValidateConfig(data.Criteria, resp) +} + +func (r *ReleaseBundleCustomWebhookResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + go util.SendUsageResourceCreate(ctx, r.ProviderData.Client.R(), r.ProviderData.ProductId, r.TypeName) + + var plan ReleaseBundleCustomWebhookResourceModel + + // Read Terraform plan data into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + var webhook CustomWebhookAPIModel + resp.Diagnostics.Append(plan.toAPIModel(ctx, r.Domain, &webhook)...) + if resp.Diagnostics.HasError() { + return + } + + r.CustomWebhookResource.Create(ctx, webhook, resp) + if resp.Diagnostics.HasError() { + return + } + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *ReleaseBundleCustomWebhookResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + go util.SendUsageResourceRead(ctx, r.ProviderData.Client.R(), r.ProviderData.ProductId, r.TypeName) + + var state ReleaseBundleCustomWebhookResourceModel + + // Read Terraform state data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + var webhook CustomWebhookAPIModel + found := r.CustomWebhookResource.Read(ctx, state.Key.ValueString(), &webhook, resp) + if resp.Diagnostics.HasError() { + return + } + + if !found { + return + } + + resp.Diagnostics.Append(state.fromAPIModel(ctx, webhook, state.Handlers)...) + if resp.Diagnostics.HasError() { + return + } + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *ReleaseBundleCustomWebhookResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + go util.SendUsageResourceUpdate(ctx, r.ProviderData.Client.R(), r.ProviderData.ProductId, r.TypeName) + + var plan ReleaseBundleCustomWebhookResourceModel + + // Read Terraform plan data into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + var webhook CustomWebhookAPIModel + resp.Diagnostics.Append(plan.toAPIModel(ctx, r.Domain, &webhook)...) + if resp.Diagnostics.HasError() { + return + } + + r.CustomWebhookResource.Update(ctx, plan.Key.ValueString(), webhook, resp) + if resp.Diagnostics.HasError() { + return + } + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *ReleaseBundleCustomWebhookResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + go util.SendUsageResourceDelete(ctx, r.ProviderData.Client.R(), r.ProviderData.ProductId, r.TypeName) + + var state ReleaseBundleCustomWebhookResourceModel + + // Read Terraform prior state data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + + r.WebhookResource.Delete(ctx, state.Key.ValueString(), resp) + if resp.Diagnostics.HasError() { + return + } + + // If the logic reaches here, it implicitly succeeded and will remove + // the resource from state if there are no other errors. +} + +// ImportState imports the resource into the Terraform state. +func (r *ReleaseBundleCustomWebhookResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + r.WebhookResource.ImportState(ctx, req, resp) +} + +func (m ReleaseBundleCustomWebhookResourceModel) toAPIModel(ctx context.Context, domain string, apiModel *CustomWebhookAPIModel) (diags diag.Diagnostics) { + criteriaObj := m.Criteria.Elements()[0].(types.Object) + criteriaAttrs := criteriaObj.Attributes() + + baseCriteria, d := m.CustomWebhookResourceModel.toBaseCriteriaAPIModel(ctx, criteriaAttrs) + if d.HasError() { + diags.Append(d...) + } + + criteriaAPIModel, d := toReleaseBundleCriteriaAPIModel(ctx, baseCriteria, criteriaAttrs) + if d.HasError() { + diags.Append(d...) + } + + d = m.CustomWebhookResourceModel.toAPIModel(ctx, domain, criteriaAPIModel, apiModel) + if d.HasError() { + diags.Append(d...) + } + + return +} + +func (m *ReleaseBundleCustomWebhookResourceModel) fromAPIModel(ctx context.Context, apiModel CustomWebhookAPIModel, stateHandlers basetypes.SetValue) diag.Diagnostics { + diags := diag.Diagnostics{} + + criteriaAPIModel := apiModel.EventFilter.Criteria.(map[string]interface{}) + + baseCriteriaAttrs, d := m.CustomWebhookResourceModel.fromBaseCriteriaAPIModel(ctx, criteriaAPIModel) + + criteriaSet, d := fromReleaseBundleCriteriaAPIModel(ctx, criteriaAPIModel, baseCriteriaAttrs) + if d.HasError() { + diags.Append(d...) + } + + d = m.CustomWebhookResourceModel.fromAPIModel(ctx, apiModel, stateHandlers, &criteriaSet) + if d.HasError() { + diags.Append(d...) + } + + return diags +} diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook_repo.go b/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook_repo.go index 1f59d477..b6720bf4 100644 --- a/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook_repo.go +++ b/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook_repo.go @@ -199,15 +199,15 @@ func (r *RepoCustomWebhookResource) ImportState(ctx context.Context, req resourc } func (m RepoCustomWebhookResourceModel) toAPIModel(ctx context.Context, domain string, apiModel *CustomWebhookAPIModel) (diags diag.Diagnostics) { - critieriaObj := m.Criteria.Elements()[0].(types.Object) - critieriaAttrs := critieriaObj.Attributes() + criteriaObj := m.Criteria.Elements()[0].(types.Object) + criteriaAttrs := criteriaObj.Attributes() - baseCriteria, d := m.CustomWebhookResourceModel.toBaseCriteriaAPIModel(ctx, critieriaAttrs) + baseCriteria, d := m.CustomWebhookResourceModel.toBaseCriteriaAPIModel(ctx, criteriaAttrs) if d.HasError() { diags.Append(d...) } - criteriaAPIModel, d := toRepoCriteriaAPIModel(ctx, baseCriteria, critieriaAttrs) + criteriaAPIModel, d := toRepoCriteriaAPIModel(ctx, baseCriteria, criteriaAttrs) d = m.CustomWebhookResourceModel.toAPIModel(ctx, domain, criteriaAPIModel, apiModel) if d.HasError() { diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_webhook_build.go b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_build.go index 21cb1b3a..254adac5 100644 --- a/pkg/artifactory/resource/webhook/resource_artifactory_webhook_build.go +++ b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_build.go @@ -199,7 +199,7 @@ func (r *BuildWebhookResource) ImportState(ctx context.Context, req resource.Imp r.WebhookResource.ImportState(ctx, req, resp) } -var toBuildCriteriaAPIModel = func(ctx context.Context, baseCriteria BaseCriteriaAPIModel, criteriaAttrs map[string]attr.Value) (criteriaAPIModel BuildCriteriaAPIModel, diags diag.Diagnostics) { +func toBuildCriteriaAPIModel(ctx context.Context, baseCriteria BaseCriteriaAPIModel, criteriaAttrs map[string]attr.Value) (criteriaAPIModel BuildCriteriaAPIModel, diags diag.Diagnostics) { var selectedBuilds []string d := criteriaAttrs["selected_builds"].(types.Set).ElementsAs(ctx, &selectedBuilds, false) if d.HasError() { @@ -216,15 +216,15 @@ var toBuildCriteriaAPIModel = func(ctx context.Context, baseCriteria BaseCriteri } func (m BuildWebhookResourceModel) toAPIModel(ctx context.Context, domain string, apiModel *WebhookAPIModel) (diags diag.Diagnostics) { - critieriaObj := m.Criteria.Elements()[0].(types.Object) - critieriaAttrs := critieriaObj.Attributes() + criteriaObj := m.Criteria.Elements()[0].(types.Object) + criteriaAttrs := criteriaObj.Attributes() - baseCriteria, d := m.WebhookResourceModel.toBaseCriteriaAPIModel(ctx, critieriaAttrs) + baseCriteria, d := m.WebhookResourceModel.toBaseCriteriaAPIModel(ctx, criteriaAttrs) if d.HasError() { diags.Append(d...) } - criteriaAPIModel, d := toBuildCriteriaAPIModel(ctx, baseCriteria, critieriaAttrs) + criteriaAPIModel, d := toBuildCriteriaAPIModel(ctx, baseCriteria, criteriaAttrs) if d.HasError() { diags.Append(d...) } @@ -249,7 +249,7 @@ var buildCriteriaSetResourceModelElementTypes = types.ObjectType{ AttrTypes: buildCriteriaSetResourceModelAttributeTypes, } -var fromBuildAPIModel = func(ctx context.Context, criteriaAPIModel map[string]interface{}, baseCriteriaAttrs map[string]attr.Value) (criteriaSet basetypes.SetValue, diags diag.Diagnostics) { +func fromBuildAPIModel(ctx context.Context, criteriaAPIModel map[string]interface{}, baseCriteriaAttrs map[string]attr.Value) (criteriaSet basetypes.SetValue, diags diag.Diagnostics) { selectedBuilds := types.SetNull(types.StringType) if v, ok := criteriaAPIModel["selectedBuilds"]; ok && v != nil { sb, d := types.SetValueFrom(ctx, types.StringType, v) diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_webhook_build_test.go b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_build_test.go index 6180c05a..b23e58e9 100644 --- a/pkg/artifactory/resource/webhook/resource_artifactory_webhook_build_test.go +++ b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_build_test.go @@ -51,7 +51,7 @@ func TestAccWebhook_BuildWithIncludePatterns(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(t) }, - ProtoV6ProviderFactories: acctest.ProtoV6MuxProviderFactories, + ProtoV6ProviderFactories: acctest.ProtoV6ProviderFactories, CheckDestroy: acctest.VerifyDeleted(fqrn, "key", acctest.CheckRepo), Steps: []resource.TestStep{ { diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_webhook_release_bundle.go b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_release_bundle.go index bc376d8b..989db7ac 100644 --- a/pkg/artifactory/resource/webhook/resource_artifactory_webhook_release_bundle.go +++ b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_release_bundle.go @@ -72,32 +72,32 @@ func (r *ReleaseBundleWebhookResource) Metadata(ctx context.Context, req resourc r.WebhookResource.Metadata(ctx, req, resp) } -func (r *ReleaseBundleWebhookResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { - criteriaBlock := schema.SetNestedBlock{ - NestedObject: schema.NestedBlockObject{ - Attributes: lo.Assign( - patternsSchemaAttributes("Simple wildcard patterns for Release Bundle names.\nAnt-style path expressions are supported (*, **, ?).\nFor example: `product_*`"), - map[string]schema.Attribute{ - "any_release_bundle": schema.BoolAttribute{ - Required: true, - Description: "Trigger on any release bundles or distributions", - }, - "registered_release_bundle_names": schema.SetAttribute{ - ElementType: types.StringType, - Required: true, - Description: "Trigger on this list of release bundle names", - }, +var releaseBundleCriteriaBlock = schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: lo.Assign( + patternsSchemaAttributes("Simple wildcard patterns for Release Bundle names.\nAnt-style path expressions are supported (*, **, ?).\nFor example: `product_*`"), + map[string]schema.Attribute{ + "any_release_bundle": schema.BoolAttribute{ + Required: true, + Description: "Trigger on any release bundles or distributions", }, - ), - }, - Validators: []validator.Set{ - setvalidator.SizeBetween(1, 1), - setvalidator.IsRequired(), - }, - Description: "Specifies where the webhook will be applied, on which release bundles or distributions.", - } + "registered_release_bundle_names": schema.SetAttribute{ + ElementType: types.StringType, + Required: true, + Description: "Trigger on this list of release bundle names", + }, + }, + ), + }, + Validators: []validator.Set{ + setvalidator.SizeBetween(1, 1), + setvalidator.IsRequired(), + }, + Description: "Specifies where the webhook will be applied, on which release bundles or distributions.", +} - resp.Schema = r.CreateSchema(r.Domain, &criteriaBlock, handlerBlock) +func (r *ReleaseBundleWebhookResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = r.CreateSchema(r.Domain, &releaseBundleCriteriaBlock, handlerBlock) if r.Domain == ReleaseBundleDomain { resp.Schema.DeprecationMessage = "This resource is being deprecated and replaced by artifactory_destination_webhook resource" } @@ -107,15 +107,8 @@ func (r *ReleaseBundleWebhookResource) Configure(ctx context.Context, req resour r.WebhookResource.Configure(ctx, req, resp) } -func (r ReleaseBundleWebhookResource) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) { - var data ReleaseBundleWebhookResourceModel - - resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) - if resp.Diagnostics.HasError() { - return - } - - criteriaObj := data.Criteria.Elements()[0].(types.Object) +func releaseBundleValidateConfig(criteria basetypes.SetValue, resp *resource.ValidateConfigResponse) { + criteriaObj := criteria.Elements()[0].(types.Object) criteriaAttrs := criteriaObj.Attributes() anyReleaseBundle := criteriaAttrs["any_release_bundle"].(types.Bool).ValueBool() @@ -129,6 +122,17 @@ func (r ReleaseBundleWebhookResource) ValidateConfig(ctx context.Context, req re } } +func (r ReleaseBundleWebhookResource) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) { + var data ReleaseBundleWebhookResourceModel + + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + releaseBundleValidateConfig(data.Criteria, resp) +} + func (r *ReleaseBundleWebhookResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { go util.SendUsageResourceCreate(ctx, r.ProviderData.Client.R(), r.ProviderData.ProductId, r.TypeName) @@ -233,25 +237,35 @@ func (r *ReleaseBundleWebhookResource) ImportState(ctx context.Context, req reso r.WebhookResource.ImportState(ctx, req, resp) } -func (m ReleaseBundleWebhookResourceModel) toAPIModel(ctx context.Context, domain string, apiModel *WebhookAPIModel) (diags diag.Diagnostics) { - critieriaObj := m.Criteria.Elements()[0].(types.Object) - critieriaAttrs := critieriaObj.Attributes() +func toReleaseBundleCriteriaAPIModel(ctx context.Context, baseCriteria BaseCriteriaAPIModel, criteriaAttrs map[string]attr.Value) (criteriaAPIModel ReleaseBundleCriteriaAPIModel, diags diag.Diagnostics) { - baseCriteria, d := m.WebhookResourceModel.toBaseCriteriaAPIModel(ctx, critieriaAttrs) + var releaseBundleNames []string + d := criteriaAttrs["registered_release_bundle_names"].(types.Set).ElementsAs(ctx, &releaseBundleNames, false) if d.HasError() { diags.Append(d...) } - var releaseBundleNames []string - d = critieriaAttrs["registered_release_bundle_names"].(types.Set).ElementsAs(ctx, &releaseBundleNames, false) + criteriaAPIModel = ReleaseBundleCriteriaAPIModel{ + BaseCriteriaAPIModel: baseCriteria, + AnyReleaseBundle: criteriaAttrs["any_release_bundle"].(types.Bool).ValueBool(), + RegisteredReleaseBundlesNames: releaseBundleNames, + } + + return +} + +func (m ReleaseBundleWebhookResourceModel) toAPIModel(ctx context.Context, domain string, apiModel *WebhookAPIModel) (diags diag.Diagnostics) { + criteriaObj := m.Criteria.Elements()[0].(types.Object) + criteriaAttrs := criteriaObj.Attributes() + + baseCriteria, d := m.WebhookResourceModel.toBaseCriteriaAPIModel(ctx, criteriaAttrs) if d.HasError() { diags.Append(d...) } - criteriaAPIModel := ReleaseBundleCriteriaAPIModel{ - BaseCriteriaAPIModel: baseCriteria, - AnyReleaseBundle: critieriaAttrs["any_release_bundle"].(types.Bool).ValueBool(), - RegisteredReleaseBundlesNames: releaseBundleNames, + criteriaAPIModel, d := toReleaseBundleCriteriaAPIModel(ctx, baseCriteria, criteriaAttrs) + if d.HasError() { + diags.Append(d...) } d = m.WebhookResourceModel.toAPIModel(ctx, domain, criteriaAPIModel, apiModel) @@ -274,13 +288,7 @@ var releaseBundleCriteriaSetResourceModelElementTypes = types.ObjectType{ AttrTypes: releaseBundleCriteriaSetResourceModelAttributeTypes, } -func (m *ReleaseBundleWebhookResourceModel) fromAPIModel(ctx context.Context, apiModel WebhookAPIModel, stateHandlers basetypes.SetValue) diag.Diagnostics { - diags := diag.Diagnostics{} - - criteriaAPIModel := apiModel.EventFilter.Criteria.(map[string]interface{}) - - baseCriteriaAttrs, d := m.WebhookResourceModel.fromBaseCriteriaAPIModel(ctx, criteriaAPIModel) - +func fromReleaseBundleCriteriaAPIModel(ctx context.Context, criteriaAPIModel map[string]interface{}, baseCriteriaAttrs map[string]attr.Value) (criteriaSet basetypes.SetValue, diags diag.Diagnostics) { releaseBundleNames := types.SetNull(types.StringType) if v, ok := criteriaAPIModel["registeredReleaseBundlesNames"]; ok && v != nil { rb, d := types.SetValueFrom(ctx, types.StringType, v) @@ -304,7 +312,7 @@ func (m *ReleaseBundleWebhookResourceModel) fromAPIModel(ctx context.Context, ap if d.HasError() { diags.Append(d...) } - criteriaSet, d := types.SetValue( + criteriaSet, d = types.SetValue( releaseBundleCriteriaSetResourceModelElementTypes, []attr.Value{criteria}, ) @@ -312,6 +320,21 @@ func (m *ReleaseBundleWebhookResourceModel) fromAPIModel(ctx context.Context, ap diags.Append(d...) } + return +} + +func (m *ReleaseBundleWebhookResourceModel) fromAPIModel(ctx context.Context, apiModel WebhookAPIModel, stateHandlers basetypes.SetValue) diag.Diagnostics { + diags := diag.Diagnostics{} + + criteriaAPIModel := apiModel.EventFilter.Criteria.(map[string]interface{}) + + baseCriteriaAttrs, d := m.WebhookResourceModel.fromBaseCriteriaAPIModel(ctx, criteriaAPIModel) + + criteriaSet, d := fromReleaseBundleCriteriaAPIModel(ctx, criteriaAPIModel, baseCriteriaAttrs) + if d.HasError() { + diags.Append(d...) + } + d = m.WebhookResourceModel.fromAPIModel(ctx, apiModel, stateHandlers, &criteriaSet) if d.HasError() { diags.Append(d...) diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_webhook_release_bundle_v2.go b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_release_bundle_v2.go index 8e4f747a..a021004b 100644 --- a/pkg/artifactory/resource/webhook/resource_artifactory_webhook_release_bundle_v2.go +++ b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_release_bundle_v2.go @@ -200,23 +200,23 @@ func (r *ReleaseBundleV2WebhookResource) ImportState(ctx context.Context, req re } func (m ReleaseBundleV2WebhookResourceModel) toAPIModel(ctx context.Context, domain string, apiModel *WebhookAPIModel) (diags diag.Diagnostics) { - critieriaObj := m.Criteria.Elements()[0].(types.Object) - critieriaAttrs := critieriaObj.Attributes() + criteriaObj := m.Criteria.Elements()[0].(types.Object) + criteriaAttrs := criteriaObj.Attributes() - baseCriteria, d := m.WebhookResourceModel.toBaseCriteriaAPIModel(ctx, critieriaAttrs) + baseCriteria, d := m.WebhookResourceModel.toBaseCriteriaAPIModel(ctx, criteriaAttrs) if d.HasError() { diags.Append(d...) } var releaseBundleNames []string - d = critieriaAttrs["selected_release_bundles"].(types.Set).ElementsAs(ctx, &releaseBundleNames, false) + d = criteriaAttrs["selected_release_bundles"].(types.Set).ElementsAs(ctx, &releaseBundleNames, false) if d.HasError() { diags.Append(d...) } criteriaAPIModel := ReleaseBundleV2CriteriaAPIModel{ BaseCriteriaAPIModel: baseCriteria, - AnyReleaseBundle: critieriaAttrs["any_release_bundle"].(types.Bool).ValueBool(), + AnyReleaseBundle: criteriaAttrs["any_release_bundle"].(types.Bool).ValueBool(), SelectedReleaseBundles: releaseBundleNames, } diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_webhook_release_bundle_v2_promotion.go b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_release_bundle_v2_promotion.go index fe4e09cf..eb10b9d4 100644 --- a/pkg/artifactory/resource/webhook/resource_artifactory_webhook_release_bundle_v2_promotion.go +++ b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_release_bundle_v2_promotion.go @@ -173,11 +173,11 @@ func (r *ReleaseBundleV2PromotionWebhookResource) ImportState(ctx context.Contex } func (m ReleaseBundleV2PromotionWebhookResourceModel) toAPIModel(ctx context.Context, domain string, apiModel *WebhookAPIModel) (diags diag.Diagnostics) { - critieriaObj := m.Criteria.Elements()[0].(types.Object) - critieriaAttrs := critieriaObj.Attributes() + criteriaObj := m.Criteria.Elements()[0].(types.Object) + criteriaAttrs := criteriaObj.Attributes() var environments []string - d := critieriaAttrs["selected_environments"].(types.Set).ElementsAs(ctx, &environments, false) + d := criteriaAttrs["selected_environments"].(types.Set).ElementsAs(ctx, &environments, false) if d.HasError() { diags.Append(d...) } diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_webhook_repo.go b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_repo.go index 605308b8..4596e9a2 100644 --- a/pkg/artifactory/resource/webhook/resource_artifactory_webhook_repo.go +++ b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_repo.go @@ -229,7 +229,7 @@ func (r *RepoWebhookResource) ImportState(ctx context.Context, req resource.Impo r.WebhookResource.ImportState(ctx, req, resp) } -var toRepoCriteriaAPIModel = func(ctx context.Context, baseCriteria BaseCriteriaAPIModel, criteriaAttrs map[string]attr.Value) (criteriaAPIModel RepoCriteriaAPIModel, diags diag.Diagnostics) { +func toRepoCriteriaAPIModel(ctx context.Context, baseCriteria BaseCriteriaAPIModel, criteriaAttrs map[string]attr.Value) (criteriaAPIModel RepoCriteriaAPIModel, diags diag.Diagnostics) { var repoKeys []string d := criteriaAttrs["repo_keys"].(types.Set).ElementsAs(ctx, &repoKeys, false) if d.HasError() { @@ -248,15 +248,15 @@ var toRepoCriteriaAPIModel = func(ctx context.Context, baseCriteria BaseCriteria } func (m RepoWebhookResourceModel) toAPIModel(ctx context.Context, domain string, apiModel *WebhookAPIModel) (diags diag.Diagnostics) { - critieriaObj := m.Criteria.Elements()[0].(types.Object) - critieriaAttrs := critieriaObj.Attributes() + criteriaObj := m.Criteria.Elements()[0].(types.Object) + criteriaAttrs := criteriaObj.Attributes() - baseCriteria, d := m.WebhookResourceModel.toBaseCriteriaAPIModel(ctx, critieriaAttrs) + baseCriteria, d := m.WebhookResourceModel.toBaseCriteriaAPIModel(ctx, criteriaAttrs) if d.HasError() { diags.Append(d...) } - criteriaAPIModel, d := toRepoCriteriaAPIModel(ctx, baseCriteria, critieriaAttrs) + criteriaAPIModel, d := toRepoCriteriaAPIModel(ctx, baseCriteria, criteriaAttrs) d = m.WebhookResourceModel.toAPIModel(ctx, domain, criteriaAPIModel, apiModel) if d.HasError() { @@ -280,7 +280,7 @@ var repoCriteriaSetResourceModelElementTypes = types.ObjectType{ AttrTypes: repoCriteriaSetResourceModelAttributeTypes, } -var fromRepoCriteriaAPIMode = func(ctx context.Context, criteriaAPIModel map[string]interface{}, baseCriteriaAttrs map[string]attr.Value) (criteriaSet basetypes.SetValue, diags diag.Diagnostics) { +func fromRepoCriteriaAPIMode(ctx context.Context, criteriaAPIModel map[string]interface{}, baseCriteriaAttrs map[string]attr.Value) (criteriaSet basetypes.SetValue, diags diag.Diagnostics) { repoKeys := types.SetNull(types.StringType) if v, ok := criteriaAPIModel["repoKeys"]; ok && v != nil { ks, d := types.SetValueFrom(ctx, types.StringType, v) From 5c1c5877e7ad374916baf901fd15b5dd3c5444ad Mon Sep 17 00:00:00 2001 From: Alex Hung Date: Tue, 1 Oct 2024 11:11:21 -0700 Subject: [PATCH 06/12] Migrate artifact lifecycle webhook resource to Plugin Framework Fix state drift with patterns --- pkg/artifactory/provider/framework.go | 1 + .../resource_artifactory_custom_webhook.go | 34 ---- ...ctory_custom_webhook_artifact_lifecycle.go | 169 ++++++++++++++++++ ..._custom_webhook_artifact_lifecycle_test.go | 131 ++++++++++++++ ...source_artifactory_custom_webhook_build.go | 14 +- ...e_artifactory_custom_webhook_build_test.go | 67 ++++++- ...esource_artifactory_custom_webhook_test.go | 57 ------ .../webhook/resource_artifactory_webhook.go | 4 +- ...artifactory_webhook_artifact_lifecycle.go} | 2 +- ...actory_webhook_artifact_lifecycle_test.go} | 0 .../resource_artifactory_webhook_build.go | 22 ++- ...resource_artifactory_webhook_build_test.go | 58 +++++- 12 files changed, 441 insertions(+), 118 deletions(-) create mode 100644 pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook_artifact_lifecycle.go create mode 100644 pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook_artifact_lifecycle_test.go rename pkg/artifactory/resource/webhook/{resource_artifactory_artifact_lifecycle.go => resource_artifactory_webhook_artifact_lifecycle.go} (98%) rename pkg/artifactory/resource/webhook/{resource_artifactory_artifact_lifecycle_test.go => resource_artifactory_webhook_artifact_lifecycle_test.go} (100%) diff --git a/pkg/artifactory/provider/framework.go b/pkg/artifactory/provider/framework.go index bc581edf..6eb0fbd4 100644 --- a/pkg/artifactory/provider/framework.go +++ b/pkg/artifactory/provider/framework.go @@ -225,6 +225,7 @@ func (p *ArtifactoryProvider) Resources(ctx context.Context) []func() resource.R webhook.NewArtifactWebhookResource, webhook.NewArtifactCustomWebhookResource, webhook.NewArtifactLifecycleWebhookResource, + webhook.NewArtifactLifecycleCustomWebhookResource, webhook.NewArtifactPropertyWebhookResource, webhook.NewArtifactPropertyCustomWebhookResource, webhook.NewArtifactoryReleaseBundleWebhookResource, diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook.go b/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook.go index 6a18db7a..62516c51 100644 --- a/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook.go +++ b/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook.go @@ -291,7 +291,6 @@ func (m *CustomWebhookResourceModel) fromAPIModel(ctx context.Context, apiModel } var DomainSupported = []string{ - ArtifactLifecycleDomain, ReleaseBundleV2Domain, ReleaseBundleV2PromotionDomain, UserDomain, @@ -784,21 +783,18 @@ var domainCriteriaLookup = map[string]interface{}{ "user": EmptyWebhookCriteria{}, "release_bundle_v2": ReleaseBundleV2CriteriaAPIModel{}, "release_bundle_v2_promotion": ReleaseBundleV2PromotionCriteriaAPIModel{}, - "artifact_lifecycle": EmptyWebhookCriteria{}, } var domainPackLookup = map[string]func(map[string]interface{}) map[string]interface{}{ "user": packEmptyCriteria, "release_bundle_v2": packReleaseBundleV2Criteria, "release_bundle_v2_promotion": packReleaseBundleV2PromotionCriteria, - "artifact_lifecycle": packEmptyCriteria, } var domainUnpackLookup = map[string]func(map[string]interface{}, BaseCriteriaAPIModel) interface{}{ "user": unpackEmptyCriteria, "release_bundle_v2": unpackReleaseBundleV2Criteria, "release_bundle_v2_promotion": unpackReleaseBundleV2PromotionCriteria, - "artifact_lifecycle": unpackEmptyCriteria, } var domainSchemaLookup = func(version int, isCustom bool, webhookType string) map[string]map[string]*sdkv2_schema.Schema { @@ -806,7 +802,6 @@ var domainSchemaLookup = func(version int, isCustom bool, webhookType string) ma "user": userWebhookSchema(webhookType, version, isCustom), "release_bundle_v2": releaseBundleV2WebhookSchema(webhookType, version, isCustom), "release_bundle_v2_promotion": releaseBundleV2PromotionWebhookSchema(webhookType, version, isCustom), - "artifact_lifecycle": artifactLifecycleWebhookSchema(webhookType, version, isCustom), } } @@ -887,38 +882,9 @@ var packCriteria = func(d *sdkv2_schema.ResourceData, webhookType string, criter } var domainCriteriaValidationLookup = map[string]func(context.Context, map[string]interface{}) error{ - "build": buildCriteriaValidation, - "release_bundle": releaseBundleCriteriaValidation, - "distribution": releaseBundleCriteriaValidation, - "artifactory_release_bundle": releaseBundleCriteriaValidation, - "destination": releaseBundleCriteriaValidation, "user": emptyCriteriaValidation, "release_bundle_v2": releaseBundleV2CriteriaValidation, "release_bundle_v2_promotion": emptyCriteriaValidation, - "artifact_lifecycle": emptyCriteriaValidation, -} - -var buildCriteriaValidation = func(ctx context.Context, criteria map[string]interface{}) error { - anyBuild := criteria["any_build"].(bool) - selectedBuilds := criteria["selected_builds"].(*sdkv2_schema.Set).List() - includePatterns := criteria["include_patterns"].(*sdkv2_schema.Set).List() - - if !anyBuild && (len(selectedBuilds) == 0 && len(includePatterns) == 0) { - return fmt.Errorf("selected_builds or include_patterns cannot be empty when any_build is false") - } - - return nil -} - -var releaseBundleCriteriaValidation = func(ctx context.Context, criteria map[string]interface{}) error { - anyReleaseBundle := criteria["any_release_bundle"].(bool) - registeredReleaseBundlesNames := criteria["registered_release_bundle_names"].(*sdkv2_schema.Set).List() - - if !anyReleaseBundle && len(registeredReleaseBundlesNames) == 0 { - return fmt.Errorf("registered_release_bundle_names cannot be empty when any_release_bundle is false") - } - - return nil } var releaseBundleV2CriteriaValidation = func(ctx context.Context, criteria map[string]interface{}) error { diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook_artifact_lifecycle.go b/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook_artifact_lifecycle.go new file mode 100644 index 00000000..c789e83a --- /dev/null +++ b/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook_artifact_lifecycle.go @@ -0,0 +1,169 @@ +package webhook + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + "github.com/jfrog/terraform-provider-shared/util" +) + +var _ resource.Resource = &ArtifactLifecycleCustomWebhookResource{} + +func NewArtifactLifecycleCustomWebhookResource() resource.Resource { + return &ArtifactLifecycleCustomWebhookResource{ + CustomWebhookResource: CustomWebhookResource{ + WebhookResource: WebhookResource{ + TypeName: fmt.Sprintf("artifactory_%s_custom_webhook", ArtifactLifecycleDomain), + Domain: ArtifactLifecycleDomain, + Description: "Provides an Artifactory webhook resource. This can be used to register and manage Artifactory webhook subscription which enables you to be notified or notify other users when such events take place in Artifactory.:", + }, + }, + } +} + +type ArtifactLifecycleCustomWebhookResourceModel struct { + CustomWebhookBaseResourceModel +} + +type ArtifactLifecycleCustomWebhookResource struct { + CustomWebhookResource +} + +func (r *ArtifactLifecycleCustomWebhookResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + r.WebhookResource.Metadata(ctx, req, resp) +} + +func (r *ArtifactLifecycleCustomWebhookResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = r.CreateSchema(r.Domain, nil) +} + +func (r *ArtifactLifecycleCustomWebhookResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + r.WebhookResource.Configure(ctx, req, resp) +} + +func (r *ArtifactLifecycleCustomWebhookResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + go util.SendUsageResourceCreate(ctx, r.ProviderData.Client.R(), r.ProviderData.ProductId, r.TypeName) + + var plan ArtifactLifecycleCustomWebhookResourceModel + + // Read Terraform plan data into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + var webhook CustomWebhookAPIModel + resp.Diagnostics.Append(plan.toAPIModel(ctx, r.Domain, &webhook)...) + if resp.Diagnostics.HasError() { + return + } + + r.CustomWebhookResource.Create(ctx, webhook, resp) + if resp.Diagnostics.HasError() { + return + } + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *ArtifactLifecycleCustomWebhookResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + go util.SendUsageResourceRead(ctx, r.ProviderData.Client.R(), r.ProviderData.ProductId, r.TypeName) + + var state ArtifactLifecycleCustomWebhookResourceModel + + // Read Terraform state data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + var webhook CustomWebhookAPIModel + found := r.CustomWebhookResource.Read(ctx, state.Key.ValueString(), &webhook, resp) + if resp.Diagnostics.HasError() { + return + } + + if !found { + return + } + + resp.Diagnostics.Append(state.fromAPIModel(ctx, webhook, state.Handlers)...) + if resp.Diagnostics.HasError() { + return + } + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *ArtifactLifecycleCustomWebhookResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + go util.SendUsageResourceUpdate(ctx, r.ProviderData.Client.R(), r.ProviderData.ProductId, r.TypeName) + + var plan ArtifactLifecycleCustomWebhookResourceModel + + // Read Terraform plan data into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + var webhook CustomWebhookAPIModel + resp.Diagnostics.Append(plan.toAPIModel(ctx, r.Domain, &webhook)...) + if resp.Diagnostics.HasError() { + return + } + + r.CustomWebhookResource.Update(ctx, plan.Key.ValueString(), webhook, resp) + if resp.Diagnostics.HasError() { + return + } + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *ArtifactLifecycleCustomWebhookResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + go util.SendUsageResourceDelete(ctx, r.ProviderData.Client.R(), r.ProviderData.ProductId, r.TypeName) + + var state ArtifactLifecycleCustomWebhookResourceModel + + // Read Terraform prior state data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + + r.WebhookResource.Delete(ctx, state.Key.ValueString(), resp) + if resp.Diagnostics.HasError() { + return + } + + // If the logic reaches here, it implicitly succeeded and will remove + // the resource from state if there are no other errors. +} + +// ImportState imports the resource into the Terraform state. +func (r *ArtifactLifecycleCustomWebhookResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + r.WebhookResource.ImportState(ctx, req, resp) +} + +func (m ArtifactLifecycleCustomWebhookResourceModel) toAPIModel(ctx context.Context, domain string, apiModel *CustomWebhookAPIModel) (diags diag.Diagnostics) { + d := m.CustomWebhookBaseResourceModel.toAPIModel(ctx, domain, apiModel) + if d.HasError() { + diags.Append(d...) + } + + return +} + +func (m *ArtifactLifecycleCustomWebhookResourceModel) fromAPIModel(ctx context.Context, apiModel CustomWebhookAPIModel, stateHandlers basetypes.SetValue) diag.Diagnostics { + diags := diag.Diagnostics{} + + d := m.CustomWebhookBaseResourceModel.fromAPIModel(ctx, apiModel, stateHandlers) + if d.HasError() { + diags.Append(d...) + } + + return diags +} diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook_artifact_lifecycle_test.go b/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook_artifact_lifecycle_test.go new file mode 100644 index 00000000..5c20ccd1 --- /dev/null +++ b/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook_artifact_lifecycle_test.go @@ -0,0 +1,131 @@ +package webhook_test + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/plancheck" + "github.com/jfrog/terraform-provider-artifactory/v12/pkg/acctest" + "github.com/jfrog/terraform-provider-shared/testutil" + "github.com/jfrog/terraform-provider-shared/util" +) + +func TestAccCustomWebhook_ArtifactLifecycle_UpgradeFromSDKv2(t *testing.T) { + _, fqrn, name := testutil.MkNames("test-artifact-lifecycle-webhook", "artifactory_artifact_lifecycle_custom_webhook") + + params := map[string]interface{}{ + "webhookName": name, + } + config := util.ExecuteTemplate("TestAccCustomWebhookArtifactLifecycle", ` + resource "artifactory_artifact_lifecycle_custom_webhook" "{{ .webhookName }}" { + key = "{{ .webhookName }}" + description = "test description" + event_types = ["archive", "restore"] + handler { + url = "https://google.com" + secrets = { + secret1 = "value1" + secret2 = "value2" + } + http_headers = { + header-1 = "value-1" + header-2 = "value-2" + } + payload = "{ \"ref\": \"main\" , \"inputs\": { \"artifact_path\": \"test-repo/repo-path\" } }" + } + } + `, params) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + CheckDestroy: acctest.VerifyDeleted(fqrn, "key", acctest.CheckRepo), + Steps: []resource.TestStep{ + { + Config: config, + ExternalProviders: map[string]resource.ExternalProvider{ + "artifactory": { + Source: "jfrog/artifactory", + VersionConstraint: "12.1.0", + }, + }, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(fqrn, "event_types.#", "2"), + resource.TestCheckResourceAttr(fqrn, "handler.#", "1"), + resource.TestCheckResourceAttr(fqrn, "handler.0.url", "https://google.com"), + resource.TestCheckResourceAttr(fqrn, "handler.0.secrets.secret1", "value1"), + resource.TestCheckResourceAttr(fqrn, "handler.0.secrets.secret2", "value2"), + resource.TestCheckResourceAttr(fqrn, "handler.0.http_headers.%", "2"), + resource.TestCheckResourceAttr(fqrn, "handler.0.http_headers.header-1", "value-1"), + resource.TestCheckResourceAttr(fqrn, "handler.0.http_headers.header-2", "value-2"), + resource.TestCheckResourceAttr(fqrn, "handler.0.payload", "{ \"ref\": \"main\" , \"inputs\": { \"artifact_path\": \"test-repo/repo-path\" } }"), + ), + }, + { + Config: config, + ProtoV6ProviderFactories: acctest.ProtoV6MuxProviderFactories, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectEmptyPlan(), + }, + }, + }, + }, + }) +} + +func TestAccCustomWebhook_ArtifactLifecycle(t *testing.T) { + _, fqrn, name := testutil.MkNames("test-artifact-lifecycle-webhook", "artifactory_artifact_lifecycle_custom_webhook") + + params := map[string]interface{}{ + "webhookName": name, + } + webhookConfig := util.ExecuteTemplate("TestAccCustomWebhookArtifactLifecycle", ` + resource "artifactory_artifact_lifecycle_custom_webhook" "{{ .webhookName }}" { + key = "{{ .webhookName }}" + description = "test description" + event_types = ["archive", "restore"] + handler { + url = "https://google.com" + secrets = { + secret1 = "value1" + secret2 = "value2" + } + http_headers = { + header-1 = "value-1" + header-2 = "value-2" + } + payload = "{ \"ref\": \"main\" , \"inputs\": { \"artifact_path\": \"test-repo/repo-path\" } }" + } + } + `, params) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ProtoV6ProviderFactories: acctest.ProtoV6ProviderFactories, + CheckDestroy: acctest.VerifyDeleted(fqrn, "key", acctest.CheckRepo), + Steps: []resource.TestStep{ + { + Config: webhookConfig, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(fqrn, "event_types.#", "2"), + resource.TestCheckResourceAttr(fqrn, "handler.#", "1"), + resource.TestCheckResourceAttr(fqrn, "handler.0.url", "https://google.com"), + resource.TestCheckResourceAttr(fqrn, "handler.0.secrets.secret1", "value1"), + resource.TestCheckResourceAttr(fqrn, "handler.0.secrets.secret2", "value2"), + resource.TestCheckResourceAttr(fqrn, "handler.0.http_headers.%", "2"), + resource.TestCheckResourceAttr(fqrn, "handler.0.http_headers.header-1", "value-1"), + resource.TestCheckResourceAttr(fqrn, "handler.0.http_headers.header-2", "value-2"), + resource.TestCheckResourceAttr(fqrn, "handler.0.payload", "{ \"ref\": \"main\" , \"inputs\": { \"artifact_path\": \"test-repo/repo-path\" } }"), + ), + }, + { + ResourceName: fqrn, + ImportState: true, + ImportStateId: name, + ImportStateVerify: true, + ImportStateVerifyIdentifierAttribute: "key", + ImportStateVerifyIgnore: []string{"handler.0.secrets"}, + }, + }, + }) +} diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook_build.go b/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook_build.go index a50468f0..64c4e719 100644 --- a/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook_build.go +++ b/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook_build.go @@ -5,7 +5,6 @@ import ( "fmt" "github.com/hashicorp/terraform-plugin-framework/diag" - "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" @@ -54,18 +53,7 @@ func (r BuildCustomWebhookResource) ValidateConfig(ctx context.Context, req reso return } - criteriaObj := data.Criteria.Elements()[0].(types.Object) - criteriaAttrs := criteriaObj.Attributes() - - anyBuild := criteriaAttrs["any_build"].(types.Bool).ValueBool() - - if !anyBuild && len(criteriaAttrs["selected_builds"].(types.Set).Elements()) == 0 && len(criteriaAttrs["include_patterns"].(types.Set).Elements()) == 0 { - resp.Diagnostics.AddAttributeError( - path.Root("criteria").AtSetValue(criteriaObj).AtName("any_build"), - "Invalid Attribute Configuration", - "selected_builds or include_patterns cannot be empty when any_build is false", - ) - } + buildValidateConfig(data.Criteria, resp) } func (r *BuildCustomWebhookResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook_build_test.go b/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook_build_test.go index 627a887b..f403f455 100644 --- a/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook_build_test.go +++ b/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook_build_test.go @@ -5,12 +5,69 @@ import ( "testing" "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/plancheck" "github.com/jfrog/terraform-provider-artifactory/v12/pkg/acctest" "github.com/jfrog/terraform-provider-shared/testutil" "github.com/jfrog/terraform-provider-shared/util" ) -func TestAccCustomWebhook_BuildWithIncludePatterns(t *testing.T) { +func TestAccCustomWebhook_Build_UpgradeFromSDKv2(t *testing.T) { + id := testutil.RandomInt() + name := fmt.Sprintf("webhook-%d", id) + fqrn := fmt.Sprintf("artifactory_build_custom_webhook.%s", name) + + params := map[string]interface{}{ + "webhookName": name, + } + config := util.ExecuteTemplate("TestAccCustomWebhook_Build_UpgradeFromSDKv2", ` + resource "artifactory_build_custom_webhook" "{{ .webhookName }}" { + key = "{{ .webhookName }}" + description = "test description" + event_types = ["uploaded"] + criteria { + any_build = false + selected_builds = [] + include_patterns = ["foo"] + } + handler { + url = "https://google.com" + payload = "{ \"ref\": \"main\" , \"inputs\": { \"artifact_path\": \"test-repo/repo-path\" } }" + } + } + `, params) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + CheckDestroy: acctest.VerifyDeleted(fqrn, "key", acctest.CheckRepo), + + Steps: []resource.TestStep{ + { + Config: config, + ExternalProviders: map[string]resource.ExternalProvider{ + "artifactory": { + Source: "jfrog/artifactory", + VersionConstraint: "12.1.0", + }, + }, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(fqrn, "criteria.0.include_patterns.#", "1"), + resource.TestCheckResourceAttr(fqrn, "criteria.0.include_patterns.0", "foo"), + ), + }, + { + Config: config, + ProtoV6ProviderFactories: acctest.ProtoV6ProviderFactories, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectEmptyPlan(), + }, + }, + }, + }, + }) +} + +func TestAccCustomWebhook_Build(t *testing.T) { id := testutil.RandomInt() name := fmt.Sprintf("webhook-%d", id) fqrn := fmt.Sprintf("artifactory_build_custom_webhook.%s", name) @@ -48,6 +105,14 @@ func TestAccCustomWebhook_BuildWithIncludePatterns(t *testing.T) { resource.TestCheckResourceAttr(fqrn, "criteria.0.include_patterns.0", "foo"), ), }, + { + ResourceName: fqrn, + ImportState: true, + ImportStateId: name, + ImportStateVerify: true, + ImportStateVerifyIdentifierAttribute: "key", + ImportStateVerifyIgnore: []string{"handler.0.secrets"}, + }, }, }) } diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook_test.go b/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook_test.go index 0bf6dc65..74e77e3e 100644 --- a/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook_test.go +++ b/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook_test.go @@ -115,63 +115,6 @@ func TestAccCustomWebhook_User(t *testing.T) { }) } -func TestAccCustomWebhook_ArtifactLifecycle(t *testing.T) { - _, fqrn, name := testutil.MkNames("test-artifact-lifecycle-webhook", "artifactory_artifact_lifecycle_custom_webhook") - - params := map[string]interface{}{ - "webhookName": name, - } - webhookConfig := util.ExecuteTemplate("TestAccCustomWebhookArtifactLifecycle", ` - resource "artifactory_artifact_lifecycle_custom_webhook" "{{ .webhookName }}" { - key = "{{ .webhookName }}" - description = "test description" - event_types = ["archive", "restore"] - handler { - url = "https://google.com" - secrets = { - secret1 = "value1" - secret2 = "value2" - } - http_headers = { - header-1 = "value-1" - header-2 = "value-2" - } - payload = "{ \"ref\": \"main\" , \"inputs\": { \"artifact_path\": \"test-repo/repo-path\" } }" - } - } - `, params) - - resource.Test(t, resource.TestCase{ - PreCheck: func() { acctest.PreCheck(t) }, - ProviderFactories: acctest.ProviderFactories, - CheckDestroy: acctest.VerifyDeleted(fqrn, "", acctest.CheckRepo), - - Steps: []resource.TestStep{ - { - Config: webhookConfig, - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr(fqrn, "event_types.#", "2"), - resource.TestCheckResourceAttr(fqrn, "handler.#", "1"), - resource.TestCheckResourceAttr(fqrn, "handler.0.url", "https://google.com"), - resource.TestCheckResourceAttr(fqrn, "handler.0.secrets.secret1", "value1"), - resource.TestCheckResourceAttr(fqrn, "handler.0.secrets.secret2", "value2"), - resource.TestCheckResourceAttr(fqrn, "handler.0.http_headers.%", "2"), - resource.TestCheckResourceAttr(fqrn, "handler.0.http_headers.header-1", "value-1"), - resource.TestCheckResourceAttr(fqrn, "handler.0.http_headers.header-2", "value-2"), - resource.TestCheckResourceAttr(fqrn, "handler.0.payload", "{ \"ref\": \"main\" , \"inputs\": { \"artifact_path\": \"test-repo/repo-path\" } }"), - ), - }, - { - ResourceName: fqrn, - ImportState: true, - ImportStateVerify: true, - ImportStateCheck: validator.CheckImportState(name, "key"), - ImportStateVerifyIgnore: []string{"handler.0.secrets"}, - }, - }, - }) -} - func TestAccCustomWebhook_ReleaseBundleV2Promotion(t *testing.T) { _, fqrn, name := testutil.MkNames("test-release-bundle-v2-promotion-webhook", "artifactory_release_bundle_v2_promotion_custom_webhook") diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_webhook.go b/pkg/artifactory/resource/webhook/resource_artifactory_webhook.go index 423b016e..0670ae39 100644 --- a/pkg/artifactory/resource/webhook/resource_artifactory_webhook.go +++ b/pkg/artifactory/resource/webhook/resource_artifactory_webhook.go @@ -503,7 +503,7 @@ func (m *WebhookCriteriaResourceModel) fromBaseCriteriaAPIModel(ctx context.Cont diags := diag.Diagnostics{} includePatterns := types.SetNull(types.StringType) - if v, ok := criteriaAPIModel["includePatterns"]; ok && v != nil { + if v, ok := criteriaAPIModel["includePatterns"]; ok && v != nil && len(v.([]interface{})) > 0 { ps, d := types.SetValueFrom(ctx, types.StringType, v) if d.HasError() { diags.Append(d...) @@ -513,7 +513,7 @@ func (m *WebhookCriteriaResourceModel) fromBaseCriteriaAPIModel(ctx context.Cont } excludePatterns := types.SetNull(types.StringType) - if v, ok := criteriaAPIModel["excludePatterns"]; ok && v != nil { + if v, ok := criteriaAPIModel["excludePatterns"]; ok && v != nil && len(v.([]interface{})) > 0 { ps, d := types.SetValueFrom(ctx, types.StringType, v) if d.HasError() { diags.Append(d...) diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_artifact_lifecycle.go b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_artifact_lifecycle.go similarity index 98% rename from pkg/artifactory/resource/webhook/resource_artifactory_artifact_lifecycle.go rename to pkg/artifactory/resource/webhook/resource_artifactory_webhook_artifact_lifecycle.go index 7d3f2e82..957c3be2 100644 --- a/pkg/artifactory/resource/webhook/resource_artifactory_artifact_lifecycle.go +++ b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_artifact_lifecycle.go @@ -10,7 +10,7 @@ import ( "github.com/jfrog/terraform-provider-shared/util" ) -var _ resource.Resource = &ReleaseBundleWebhookResource{} +var _ resource.Resource = &ArtifactLifecycleWebhookResource{} func NewArtifactLifecycleWebhookResource() resource.Resource { return &ArtifactLifecycleWebhookResource{ diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_artifact_lifecycle_test.go b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_artifact_lifecycle_test.go similarity index 100% rename from pkg/artifactory/resource/webhook/resource_artifactory_artifact_lifecycle_test.go rename to pkg/artifactory/resource/webhook/resource_artifactory_webhook_artifact_lifecycle_test.go diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_webhook_build.go b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_build.go index 254adac5..7cd19634 100644 --- a/pkg/artifactory/resource/webhook/resource_artifactory_webhook_build.go +++ b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_build.go @@ -73,15 +73,8 @@ func (r *BuildWebhookResource) Configure(ctx context.Context, req resource.Confi r.WebhookResource.Configure(ctx, req, resp) } -func (r BuildWebhookResource) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) { - var data BuildWebhookResourceModel - - resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) - if resp.Diagnostics.HasError() { - return - } - - criteriaObj := data.Criteria.Elements()[0].(types.Object) +func buildValidateConfig(criteria basetypes.SetValue, resp *resource.ValidateConfigResponse) { + criteriaObj := criteria.Elements()[0].(types.Object) criteriaAttrs := criteriaObj.Attributes() anyBuild := criteriaAttrs["any_build"].(types.Bool).ValueBool() @@ -95,6 +88,17 @@ func (r BuildWebhookResource) ValidateConfig(ctx context.Context, req resource.V } } +func (r BuildWebhookResource) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) { + var data BuildWebhookResourceModel + + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + buildValidateConfig(data.Criteria, resp) +} + func (r *BuildWebhookResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { go util.SendUsageResourceCreate(ctx, r.ProviderData.Client.R(), r.ProviderData.ProductId, r.TypeName) diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_webhook_build_test.go b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_build_test.go index b23e58e9..3b1d0e50 100644 --- a/pkg/artifactory/resource/webhook/resource_artifactory_webhook_build_test.go +++ b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_build_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/plancheck" "github.com/jfrog/terraform-provider-artifactory/v12/pkg/acctest" "github.com/jfrog/terraform-provider-shared/testutil" "github.com/jfrog/terraform-provider-shared/util" @@ -25,7 +26,62 @@ var buildTemplate = ` } ` -func TestAccWebhook_BuildWithIncludePatterns(t *testing.T) { +func TestAccWebhook_Build_UpgradeFromSDKv2(t *testing.T) { + id := testutil.RandomInt() + name := fmt.Sprintf("webhook-%d", id) + fqrn := fmt.Sprintf("artifactory_build_webhook.%s", name) + + params := map[string]interface{}{ + "webhookName": name, + } + config := util.ExecuteTemplate("TestAccWebhook_Build_UpgradeFromSDKv2", ` + resource "artifactory_build_webhook" "{{ .webhookName }}" { + key = "{{ .webhookName }}" + description = "test description" + event_types = ["uploaded"] + criteria { + any_build = false + selected_builds = [] + include_patterns = ["foo"] + } + handler { + url = "https://google.com" + } + } + `, params) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + CheckDestroy: acctest.VerifyDeleted(fqrn, "key", acctest.CheckRepo), + + Steps: []resource.TestStep{ + { + Config: config, + ExternalProviders: map[string]resource.ExternalProvider{ + "artifactory": { + Source: "jfrog/artifactory", + VersionConstraint: "12.1.0", + }, + }, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(fqrn, "criteria.0.include_patterns.#", "1"), + resource.TestCheckResourceAttr(fqrn, "criteria.0.include_patterns.0", "foo"), + ), + }, + { + Config: config, + ProtoV6ProviderFactories: acctest.ProtoV6ProviderFactories, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectEmptyPlan(), + }, + }, + }, + }, + }) +} + +func TestAccWebhook_Build(t *testing.T) { id := testutil.RandomInt() name := fmt.Sprintf("webhook-%d", id) fqrn := fmt.Sprintf("artifactory_build_webhook.%s", name) From 7bde00ffe1f2790d18bf29778a8612a1396bba1d Mon Sep 17 00:00:00 2001 From: Alex Hung Date: Tue, 1 Oct 2024 11:30:56 -0700 Subject: [PATCH 07/12] Migrate release bundle v2 webhook resource to Plugin Framework --- .../resource_artifactory_custom_webhook.go | 58 ----- ...actory_custom_webhook_release_bundle_v2.go | 204 ++++++++++++++++++ ...e_artifactory_webhook_release_bundle_v2.go | 115 +++++----- 3 files changed, 270 insertions(+), 107 deletions(-) create mode 100644 pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook_release_bundle_v2.go diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook.go b/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook.go index 62516c51..f5273f6a 100644 --- a/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook.go +++ b/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook.go @@ -291,7 +291,6 @@ func (m *CustomWebhookResourceModel) fromAPIModel(ctx context.Context, apiModel } var DomainSupported = []string{ - ReleaseBundleV2Domain, ReleaseBundleV2PromotionDomain, UserDomain, } @@ -382,32 +381,6 @@ func baseCustomWebhookBaseSchema(webhookType string) map[string]*sdkv2_schema.Sc } } -var releaseBundleV2WebhookSchema = func(webhookType string, version int, isCustom bool) map[string]*sdkv2_schema.Schema { - return utilsdk.MergeMaps(getBaseSchemaByVersion(webhookType, version, isCustom), map[string]*sdkv2_schema.Schema{ - "criteria": { - Type: sdkv2_schema.TypeSet, - Required: true, - MaxItems: 1, - Elem: &sdkv2_schema.Resource{ - Schema: utilsdk.MergeMaps(baseCriteriaSchema, map[string]*sdkv2_schema.Schema{ - "any_release_bundle": { - Type: sdkv2_schema.TypeBool, - Required: true, - Description: "Trigger on any release bundles or distributions", - }, - "selected_release_bundles": { - Type: sdkv2_schema.TypeSet, - Required: true, - Elem: &sdkv2_schema.Schema{Type: sdkv2_schema.TypeString}, - Description: "Trigger on this list of release bundle names", - }, - }), - }, - Description: "Specifies where the webhook will be applied, on which release bundles or distributions.", - }, - }) -} - var releaseBundleV2PromotionWebhookSchema = func(webhookType string, version int, isCustom bool) map[string]*sdkv2_schema.Schema { return utilsdk.MergeMaps(getBaseSchemaByVersion(webhookType, version, isCustom), map[string]*sdkv2_schema.Schema{ "criteria": { @@ -781,45 +754,26 @@ type EmptyWebhookCriteria struct{} var domainCriteriaLookup = map[string]interface{}{ "user": EmptyWebhookCriteria{}, - "release_bundle_v2": ReleaseBundleV2CriteriaAPIModel{}, "release_bundle_v2_promotion": ReleaseBundleV2PromotionCriteriaAPIModel{}, } var domainPackLookup = map[string]func(map[string]interface{}) map[string]interface{}{ "user": packEmptyCriteria, - "release_bundle_v2": packReleaseBundleV2Criteria, "release_bundle_v2_promotion": packReleaseBundleV2PromotionCriteria, } var domainUnpackLookup = map[string]func(map[string]interface{}, BaseCriteriaAPIModel) interface{}{ "user": unpackEmptyCriteria, - "release_bundle_v2": unpackReleaseBundleV2Criteria, "release_bundle_v2_promotion": unpackReleaseBundleV2PromotionCriteria, } var domainSchemaLookup = func(version int, isCustom bool, webhookType string) map[string]map[string]*sdkv2_schema.Schema { return map[string]map[string]*sdkv2_schema.Schema{ "user": userWebhookSchema(webhookType, version, isCustom), - "release_bundle_v2": releaseBundleV2WebhookSchema(webhookType, version, isCustom), "release_bundle_v2_promotion": releaseBundleV2PromotionWebhookSchema(webhookType, version, isCustom), } } -var packReleaseBundleV2Criteria = func(artifactoryCriteria map[string]interface{}) map[string]interface{} { - return map[string]interface{}{ - "any_release_bundle": artifactoryCriteria["anyReleaseBundle"].(bool), - "selected_release_bundles": sdkv2_schema.NewSet(sdkv2_schema.HashString, artifactoryCriteria["selectedReleaseBundles"].([]interface{})), - } -} - -var unpackReleaseBundleV2Criteria = func(terraformCriteria map[string]interface{}, baseCriteria BaseCriteriaAPIModel) interface{} { - return ReleaseBundleV2CriteriaAPIModel{ - AnyReleaseBundle: terraformCriteria["any_release_bundle"].(bool), - SelectedReleaseBundles: utilsdk.CastToStringArr(terraformCriteria["selected_release_bundles"].(*sdkv2_schema.Set).List()), - BaseCriteriaAPIModel: baseCriteria, - } -} - var packReleaseBundleV2PromotionCriteria = func(artifactoryCriteria map[string]interface{}) map[string]interface{} { return map[string]interface{}{ "selected_environments": sdkv2_schema.NewSet(sdkv2_schema.HashString, artifactoryCriteria["selectedEnvironments"].([]interface{})), @@ -883,21 +837,9 @@ var packCriteria = func(d *sdkv2_schema.ResourceData, webhookType string, criter var domainCriteriaValidationLookup = map[string]func(context.Context, map[string]interface{}) error{ "user": emptyCriteriaValidation, - "release_bundle_v2": releaseBundleV2CriteriaValidation, "release_bundle_v2_promotion": emptyCriteriaValidation, } -var releaseBundleV2CriteriaValidation = func(ctx context.Context, criteria map[string]interface{}) error { - anyReleaseBundle := criteria["any_release_bundle"].(bool) - selectedReleaseBundles := criteria["selected_release_bundles"].(*sdkv2_schema.Set).List() - - if !anyReleaseBundle && len(selectedReleaseBundles) == 0 { - return fmt.Errorf("selected_release_bundles cannot be empty when any_release_bundle is false") - } - - return nil -} - var emptyCriteriaValidation = func(ctx context.Context, criteria map[string]interface{}) error { return nil } diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook_release_bundle_v2.go b/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook_release_bundle_v2.go new file mode 100644 index 00000000..992c8f6c --- /dev/null +++ b/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook_release_bundle_v2.go @@ -0,0 +1,204 @@ +package webhook + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + "github.com/jfrog/terraform-provider-shared/util" +) + +var _ resource.Resource = &ReleaseBundleV2CustomWebhookResource{} + +func NewReleaseBundleV2CustomWebhookResource() resource.Resource { + return &ReleaseBundleV2CustomWebhookResource{ + CustomWebhookResource: CustomWebhookResource{ + WebhookResource: WebhookResource{ + TypeName: fmt.Sprintf("artifactory_%s_custom_webhook", ReleaseBundleV2Domain), + Domain: ReleaseBundleV2Domain, + Description: "Provides an Artifactory webhook resource. This can be used to register and manage Artifactory webhook subscription which enables you to be notified or notify other users when such events take place in Artifactory.:", + }, + }, + } +} + +type ReleaseBundleV2CustomWebhookResource struct { + CustomWebhookResource +} + +type ReleaseBundleV2CustomWebhookResourceModel struct { + CustomWebhookResourceModel +} + +func (r *ReleaseBundleV2CustomWebhookResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + r.WebhookResource.Metadata(ctx, req, resp) +} + +func (r *ReleaseBundleV2CustomWebhookResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = r.CreateSchema(r.Domain, &releaseBundleV2CriteriaBlock) +} + +func (r *ReleaseBundleV2CustomWebhookResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + r.WebhookResource.Configure(ctx, req, resp) +} + +func (r ReleaseBundleV2CustomWebhookResource) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) { + var data ReleaseBundleV2CustomWebhookResourceModel + + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + releaseBundleV2ValidatConfig(data.Criteria, resp) +} + +func (r *ReleaseBundleV2CustomWebhookResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + go util.SendUsageResourceCreate(ctx, r.ProviderData.Client.R(), r.ProviderData.ProductId, r.TypeName) + + var plan ReleaseBundleV2CustomWebhookResourceModel + + // Read Terraform plan data into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + var webhook CustomWebhookAPIModel + resp.Diagnostics.Append(plan.toAPIModel(ctx, r.Domain, &webhook)...) + if resp.Diagnostics.HasError() { + return + } + + r.CustomWebhookResource.Create(ctx, webhook, resp) + if resp.Diagnostics.HasError() { + return + } + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *ReleaseBundleV2CustomWebhookResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + go util.SendUsageResourceRead(ctx, r.ProviderData.Client.R(), r.ProviderData.ProductId, r.TypeName) + + var state ReleaseBundleV2CustomWebhookResourceModel + + // Read Terraform state data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + var webhook CustomWebhookAPIModel + found := r.CustomWebhookResource.Read(ctx, state.Key.ValueString(), &webhook, resp) + if resp.Diagnostics.HasError() { + return + } + + if !found { + return + } + + resp.Diagnostics.Append(state.fromAPIModel(ctx, webhook, state.Handlers)...) + if resp.Diagnostics.HasError() { + return + } + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *ReleaseBundleV2CustomWebhookResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + go util.SendUsageResourceUpdate(ctx, r.ProviderData.Client.R(), r.ProviderData.ProductId, r.TypeName) + + var plan ReleaseBundleV2CustomWebhookResourceModel + + // Read Terraform plan data into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + var webhook CustomWebhookAPIModel + resp.Diagnostics.Append(plan.toAPIModel(ctx, r.Domain, &webhook)...) + if resp.Diagnostics.HasError() { + return + } + + r.CustomWebhookResource.Update(ctx, plan.Key.ValueString(), webhook, resp) + if resp.Diagnostics.HasError() { + return + } + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *ReleaseBundleV2CustomWebhookResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + go util.SendUsageResourceDelete(ctx, r.ProviderData.Client.R(), r.ProviderData.ProductId, r.TypeName) + + var state ReleaseBundleV2CustomWebhookResourceModel + + // Read Terraform prior state data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + + r.WebhookResource.Delete(ctx, state.Key.ValueString(), resp) + if resp.Diagnostics.HasError() { + return + } + + // If the logic reaches here, it implicitly succeeded and will remove + // the resource from state if there are no other errors. +} + +// ImportState imports the resource into the Terraform state. +func (r *ReleaseBundleV2CustomWebhookResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + r.WebhookResource.ImportState(ctx, req, resp) +} + +func (m ReleaseBundleV2CustomWebhookResourceModel) toAPIModel(ctx context.Context, domain string, apiModel *CustomWebhookAPIModel) (diags diag.Diagnostics) { + criteriaObj := m.Criteria.Elements()[0].(types.Object) + criteriaAttrs := criteriaObj.Attributes() + + baseCriteria, d := m.CustomWebhookResourceModel.toBaseCriteriaAPIModel(ctx, criteriaAttrs) + if d.HasError() { + diags.Append(d...) + } + + criteriaAPIModel, d := toReleaseBundleV2APIModel(ctx, baseCriteria, criteriaAttrs) + if d.HasError() { + diags.Append(d...) + } + + d = m.CustomWebhookResourceModel.toAPIModel(ctx, domain, criteriaAPIModel, apiModel) + if d.HasError() { + diags.Append(d...) + } + + return +} + +func (m *ReleaseBundleV2CustomWebhookResourceModel) fromAPIModel(ctx context.Context, apiModel CustomWebhookAPIModel, stateHandlers basetypes.SetValue) diag.Diagnostics { + diags := diag.Diagnostics{} + + criteriaAPIModel := apiModel.EventFilter.Criteria.(map[string]interface{}) + + baseCriteriaAttrs, d := m.CustomWebhookResourceModel.fromBaseCriteriaAPIModel(ctx, criteriaAPIModel) + + criteriaSet, d := fromReleaseBundleV2APIModel(ctx, criteriaAPIModel, baseCriteriaAttrs) + + if d.HasError() { + diags.Append(d...) + } + + d = m.CustomWebhookResourceModel.fromAPIModel(ctx, apiModel, stateHandlers, &criteriaSet) + if d.HasError() { + diags.Append(d...) + } + + return diags +} diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_webhook_release_bundle_v2.go b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_release_bundle_v2.go index a021004b..7859b687 100644 --- a/pkg/artifactory/resource/webhook/resource_artifactory_webhook_release_bundle_v2.go +++ b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_release_bundle_v2.go @@ -41,47 +41,40 @@ func (r *ReleaseBundleV2WebhookResource) Metadata(ctx context.Context, req resou r.WebhookResource.Metadata(ctx, req, resp) } -func (r *ReleaseBundleV2WebhookResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { - criteriaBlock := schema.SetNestedBlock{ - NestedObject: schema.NestedBlockObject{ - Attributes: lo.Assign( - patternsSchemaAttributes("Simple wildcard patterns for Release Bundle names.\nAnt-style path expressions are supported (*, **, ?).\nFor example: `product_*`"), - map[string]schema.Attribute{ - "any_release_bundle": schema.BoolAttribute{ - Required: true, - Description: "Includes all existing release bundles and any future release bundles.", - }, - "selected_release_bundles": schema.SetAttribute{ - ElementType: types.StringType, - Required: true, - Description: "Trigger on this list of release bundle names", - }, +var releaseBundleV2CriteriaBlock = schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: lo.Assign( + patternsSchemaAttributes("Simple wildcard patterns for Release Bundle names.\nAnt-style path expressions are supported (*, **, ?).\nFor example: `product_*`"), + map[string]schema.Attribute{ + "any_release_bundle": schema.BoolAttribute{ + Required: true, + Description: "Includes all existing release bundles and any future release bundles.", }, - ), - }, - Validators: []validator.Set{ - setvalidator.SizeBetween(1, 1), - setvalidator.IsRequired(), - }, - Description: "Specifies where the webhook will be applied, on which release bundles or distributions.", - } + "selected_release_bundles": schema.SetAttribute{ + ElementType: types.StringType, + Required: true, + Description: "Trigger on this list of release bundle names", + }, + }, + ), + }, + Validators: []validator.Set{ + setvalidator.SizeBetween(1, 1), + setvalidator.IsRequired(), + }, + Description: "Specifies where the webhook will be applied, on which release bundles or distributions.", +} - resp.Schema = r.CreateSchema(r.Domain, &criteriaBlock, handlerBlock) +func (r *ReleaseBundleV2WebhookResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = r.CreateSchema(r.Domain, &releaseBundleV2CriteriaBlock, handlerBlock) } func (r *ReleaseBundleV2WebhookResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { r.WebhookResource.Configure(ctx, req, resp) } -func (r ReleaseBundleV2WebhookResource) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) { - var data ReleaseBundleV2WebhookResourceModel - - resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) - if resp.Diagnostics.HasError() { - return - } - - criteriaObj := data.Criteria.Elements()[0].(types.Object) +func releaseBundleV2ValidatConfig(criteria basetypes.SetValue, resp *resource.ValidateConfigResponse) { + criteriaObj := criteria.Elements()[0].(types.Object) criteriaAttrs := criteriaObj.Attributes() anyReleaseBundle := criteriaAttrs["any_release_bundle"].(types.Bool).ValueBool() @@ -95,6 +88,17 @@ func (r ReleaseBundleV2WebhookResource) ValidateConfig(ctx context.Context, req } } +func (r ReleaseBundleV2WebhookResource) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) { + var data ReleaseBundleV2WebhookResourceModel + + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + releaseBundleV2ValidatConfig(data.Criteria, resp) +} + func (r *ReleaseBundleV2WebhookResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { go util.SendUsageResourceCreate(ctx, r.ProviderData.Client.R(), r.ProviderData.ProductId, r.TypeName) @@ -199,6 +203,20 @@ func (r *ReleaseBundleV2WebhookResource) ImportState(ctx context.Context, req re r.WebhookResource.ImportState(ctx, req, resp) } +func toReleaseBundleV2APIModel(ctx context.Context, baseCriteria BaseCriteriaAPIModel, criteriaAttrs map[string]attr.Value) (criteriaAPIModel ReleaseBundleV2CriteriaAPIModel, diags diag.Diagnostics) { + var releaseBundleNames []string + d := criteriaAttrs["selected_release_bundles"].(types.Set).ElementsAs(ctx, &releaseBundleNames, false) + if d.HasError() { + diags.Append(d...) + } + + return ReleaseBundleV2CriteriaAPIModel{ + BaseCriteriaAPIModel: baseCriteria, + AnyReleaseBundle: criteriaAttrs["any_release_bundle"].(types.Bool).ValueBool(), + SelectedReleaseBundles: releaseBundleNames, + }, diags +} + func (m ReleaseBundleV2WebhookResourceModel) toAPIModel(ctx context.Context, domain string, apiModel *WebhookAPIModel) (diags diag.Diagnostics) { criteriaObj := m.Criteria.Elements()[0].(types.Object) criteriaAttrs := criteriaObj.Attributes() @@ -208,18 +226,11 @@ func (m ReleaseBundleV2WebhookResourceModel) toAPIModel(ctx context.Context, dom diags.Append(d...) } - var releaseBundleNames []string - d = criteriaAttrs["selected_release_bundles"].(types.Set).ElementsAs(ctx, &releaseBundleNames, false) + criteriaAPIModel, d := toReleaseBundleV2APIModel(ctx, baseCriteria, criteriaAttrs) if d.HasError() { diags.Append(d...) } - criteriaAPIModel := ReleaseBundleV2CriteriaAPIModel{ - BaseCriteriaAPIModel: baseCriteria, - AnyReleaseBundle: criteriaAttrs["any_release_bundle"].(types.Bool).ValueBool(), - SelectedReleaseBundles: releaseBundleNames, - } - d = m.WebhookResourceModel.toAPIModel(ctx, domain, criteriaAPIModel, apiModel) if d.HasError() { diags.Append(d...) @@ -240,13 +251,7 @@ var releaseBundleV2CriteriaSetResourceModelElementTypes = types.ObjectType{ AttrTypes: releaseBundleV2CriteriaSetResourceModelAttributeTypes, } -func (m *ReleaseBundleV2WebhookResourceModel) fromAPIModel(ctx context.Context, apiModel WebhookAPIModel, stateHandlers basetypes.SetValue) diag.Diagnostics { - diags := diag.Diagnostics{} - - criteriaAPIModel := apiModel.EventFilter.Criteria.(map[string]interface{}) - - baseCriteriaAttrs, d := m.WebhookResourceModel.fromBaseCriteriaAPIModel(ctx, criteriaAPIModel) - +func fromReleaseBundleV2APIModel(ctx context.Context, criteriaAPIModel map[string]interface{}, baseCriteriaAttrs map[string]attr.Value) (criteriaSet basetypes.SetValue, diags diag.Diagnostics) { releaseBundleNames := types.SetNull(types.StringType) if v, ok := criteriaAPIModel["selectedReleaseBundles"]; ok && v != nil { rb, d := types.SetValueFrom(ctx, types.StringType, v) @@ -270,10 +275,22 @@ func (m *ReleaseBundleV2WebhookResourceModel) fromAPIModel(ctx context.Context, if d.HasError() { diags.Append(d...) } - criteriaSet, d := types.SetValue( + criteriaSet, d = types.SetValue( releaseBundleV2CriteriaSetResourceModelElementTypes, []attr.Value{criteria}, ) + + return +} + +func (m *ReleaseBundleV2WebhookResourceModel) fromAPIModel(ctx context.Context, apiModel WebhookAPIModel, stateHandlers basetypes.SetValue) diag.Diagnostics { + diags := diag.Diagnostics{} + + criteriaAPIModel := apiModel.EventFilter.Criteria.(map[string]interface{}) + + baseCriteriaAttrs, d := m.WebhookResourceModel.fromBaseCriteriaAPIModel(ctx, criteriaAPIModel) + + criteriaSet, d := fromReleaseBundleV2APIModel(ctx, criteriaAPIModel, baseCriteriaAttrs) if d.HasError() { diags.Append(d...) } From c4ed3b4fce4210ee4472af3702042b2794aca2e3 Mon Sep 17 00:00:00 2001 From: Alex Hung Date: Tue, 1 Oct 2024 12:10:03 -0700 Subject: [PATCH 08/12] Migrate release bundle v2 promotion webhook resource to Plugin Framework --- pkg/artifactory/provider/framework.go | 2 + .../resource_artifactory_custom_webhook.go | 49 +---- ...tom_webhook_release_bundle_v2_promotion.go | 187 ++++++++++++++++++ ...ebhook_release_bundle_v2_promotion_test.go | 161 +++++++++++++++ ...esource_artifactory_custom_webhook_test.go | 71 ------- ...ory_webhook_release_bundle_v2_promotion.go | 88 +++++---- 6 files changed, 409 insertions(+), 149 deletions(-) create mode 100644 pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook_release_bundle_v2_promotion.go create mode 100644 pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook_release_bundle_v2_promotion_test.go diff --git a/pkg/artifactory/provider/framework.go b/pkg/artifactory/provider/framework.go index 6eb0fbd4..f03e2929 100644 --- a/pkg/artifactory/provider/framework.go +++ b/pkg/artifactory/provider/framework.go @@ -241,7 +241,9 @@ func (p *ArtifactoryProvider) Resources(ctx context.Context) []func() resource.R webhook.NewReleaseBundleWebhookResource, webhook.NewReleaseBundleCustomWebhookResource, webhook.NewReleaseBundleV2WebhookResource, + webhook.NewReleaseBundleV2CustomWebhookResource, webhook.NewReleaseBundleV2PromotionWebhookResource, + webhook.NewReleaseBundleV2PromotionCustomWebhookResource, webhook.NewUserWebhookResource, } } diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook.go b/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook.go index f5273f6a..af7bbda0 100644 --- a/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook.go +++ b/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook.go @@ -291,7 +291,6 @@ func (m *CustomWebhookResourceModel) fromAPIModel(ctx context.Context, apiModel } var DomainSupported = []string{ - ReleaseBundleV2PromotionDomain, UserDomain, } @@ -381,27 +380,6 @@ func baseCustomWebhookBaseSchema(webhookType string) map[string]*sdkv2_schema.Sc } } -var releaseBundleV2PromotionWebhookSchema = func(webhookType string, version int, isCustom bool) map[string]*sdkv2_schema.Schema { - return utilsdk.MergeMaps(getBaseSchemaByVersion(webhookType, version, isCustom), map[string]*sdkv2_schema.Schema{ - "criteria": { - Type: sdkv2_schema.TypeSet, - Required: true, - MaxItems: 1, - Elem: &sdkv2_schema.Resource{ - Schema: utilsdk.MergeMaps(baseCriteriaSchema, map[string]*sdkv2_schema.Schema{ - "selected_environments": { - Type: sdkv2_schema.TypeSet, - Required: true, - Elem: &sdkv2_schema.Schema{Type: sdkv2_schema.TypeString}, - Description: "Trigger on this list of environments", - }, - }), - }, - Description: "Specifies where the webhook will be applied, on which release bundles promotion.", - }, - }) -} - var userWebhookSchema = func(webhookType string, version int, isCustom bool) map[string]*sdkv2_schema.Schema { return getBaseSchemaByVersion(webhookType, version, isCustom) } @@ -753,36 +731,20 @@ var packKeyValuePair = func(keyValuePairs []KeyValuePairAPIModel) map[string]int type EmptyWebhookCriteria struct{} var domainCriteriaLookup = map[string]interface{}{ - "user": EmptyWebhookCriteria{}, - "release_bundle_v2_promotion": ReleaseBundleV2PromotionCriteriaAPIModel{}, + "user": EmptyWebhookCriteria{}, } var domainPackLookup = map[string]func(map[string]interface{}) map[string]interface{}{ - "user": packEmptyCriteria, - "release_bundle_v2_promotion": packReleaseBundleV2PromotionCriteria, + "user": packEmptyCriteria, } var domainUnpackLookup = map[string]func(map[string]interface{}, BaseCriteriaAPIModel) interface{}{ - "user": unpackEmptyCriteria, - "release_bundle_v2_promotion": unpackReleaseBundleV2PromotionCriteria, + "user": unpackEmptyCriteria, } var domainSchemaLookup = func(version int, isCustom bool, webhookType string) map[string]map[string]*sdkv2_schema.Schema { return map[string]map[string]*sdkv2_schema.Schema{ - "user": userWebhookSchema(webhookType, version, isCustom), - "release_bundle_v2_promotion": releaseBundleV2PromotionWebhookSchema(webhookType, version, isCustom), - } -} - -var packReleaseBundleV2PromotionCriteria = func(artifactoryCriteria map[string]interface{}) map[string]interface{} { - return map[string]interface{}{ - "selected_environments": sdkv2_schema.NewSet(sdkv2_schema.HashString, artifactoryCriteria["selectedEnvironments"].([]interface{})), - } -} - -var unpackReleaseBundleV2PromotionCriteria = func(terraformCriteria map[string]interface{}, baseCriteria BaseCriteriaAPIModel) interface{} { - return ReleaseBundleV2PromotionCriteriaAPIModel{ - SelectedEnvironments: utilsdk.CastToStringArr(terraformCriteria["selected_environments"].(*sdkv2_schema.Set).List()), + "user": userWebhookSchema(webhookType, version, isCustom), } } @@ -836,8 +798,7 @@ var packCriteria = func(d *sdkv2_schema.ResourceData, webhookType string, criter } var domainCriteriaValidationLookup = map[string]func(context.Context, map[string]interface{}) error{ - "user": emptyCriteriaValidation, - "release_bundle_v2_promotion": emptyCriteriaValidation, + "user": emptyCriteriaValidation, } var emptyCriteriaValidation = func(ctx context.Context, criteria map[string]interface{}) error { diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook_release_bundle_v2_promotion.go b/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook_release_bundle_v2_promotion.go new file mode 100644 index 00000000..b4893556 --- /dev/null +++ b/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook_release_bundle_v2_promotion.go @@ -0,0 +1,187 @@ +package webhook + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + "github.com/jfrog/terraform-provider-shared/util" +) + +var _ resource.Resource = &ReleaseBundleV2PromotionCustomWebhookResource{} + +func NewReleaseBundleV2PromotionCustomWebhookResource() resource.Resource { + return &ReleaseBundleV2PromotionCustomWebhookResource{ + CustomWebhookResource: CustomWebhookResource{ + WebhookResource: WebhookResource{ + TypeName: fmt.Sprintf("artifactory_%s_custom_webhook", ReleaseBundleV2PromotionDomain), + Domain: ReleaseBundleV2PromotionDomain, + Description: "Provides an Artifactory webhook resource. This can be used to register and manage Artifactory webhook subscription which enables you to be notified or notify other users when such events take place in Artifactory.:", + }, + }, + } +} + +type ReleaseBundleV2PromotionCustomWebhookResourceModel struct { + CustomWebhookResourceModel +} + +type ReleaseBundleV2PromotionCustomWebhookResource struct { + CustomWebhookResource +} + +func (r *ReleaseBundleV2PromotionCustomWebhookResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + r.WebhookResource.Metadata(ctx, req, resp) +} + +func (r *ReleaseBundleV2PromotionCustomWebhookResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = r.CreateSchema(r.Domain, &releaseBundleV2PromotionCriteriaBlock) +} + +func (r *ReleaseBundleV2PromotionCustomWebhookResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + r.WebhookResource.Configure(ctx, req, resp) +} + +func (r *ReleaseBundleV2PromotionCustomWebhookResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + go util.SendUsageResourceCreate(ctx, r.ProviderData.Client.R(), r.ProviderData.ProductId, r.TypeName) + + var plan ReleaseBundleV2PromotionCustomWebhookResourceModel + + // Read Terraform plan data into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + var webhook CustomWebhookAPIModel + resp.Diagnostics.Append(plan.toAPIModel(ctx, r.Domain, &webhook)...) + if resp.Diagnostics.HasError() { + return + } + + r.CustomWebhookResource.Create(ctx, webhook, resp) + if resp.Diagnostics.HasError() { + return + } + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *ReleaseBundleV2PromotionCustomWebhookResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + go util.SendUsageResourceRead(ctx, r.ProviderData.Client.R(), r.ProviderData.ProductId, r.TypeName) + + var state ReleaseBundleV2PromotionCustomWebhookResourceModel + + // Read Terraform state data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + var webhook CustomWebhookAPIModel + found := r.CustomWebhookResource.Read(ctx, state.Key.ValueString(), &webhook, resp) + if resp.Diagnostics.HasError() { + return + } + + if !found { + return + } + + resp.Diagnostics.Append(state.fromAPIModel(ctx, webhook, state.Handlers)...) + if resp.Diagnostics.HasError() { + return + } + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *ReleaseBundleV2PromotionCustomWebhookResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + go util.SendUsageResourceUpdate(ctx, r.ProviderData.Client.R(), r.ProviderData.ProductId, r.TypeName) + + var plan ReleaseBundleV2PromotionCustomWebhookResourceModel + + // Read Terraform plan data into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + var webhook CustomWebhookAPIModel + resp.Diagnostics.Append(plan.toAPIModel(ctx, r.Domain, &webhook)...) + if resp.Diagnostics.HasError() { + return + } + + r.CustomWebhookResource.Update(ctx, plan.Key.ValueString(), webhook, resp) + if resp.Diagnostics.HasError() { + return + } + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *ReleaseBundleV2PromotionCustomWebhookResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + go util.SendUsageResourceDelete(ctx, r.ProviderData.Client.R(), r.ProviderData.ProductId, r.TypeName) + + var state ReleaseBundleV2PromotionCustomWebhookResourceModel + + // Read Terraform prior state data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + + r.WebhookResource.Delete(ctx, state.Key.ValueString(), resp) + if resp.Diagnostics.HasError() { + return + } + + // If the logic reaches here, it implicitly succeeded and will remove + // the resource from state if there are no other errors. +} + +// ImportState imports the resource into the Terraform state. +func (r *ReleaseBundleV2PromotionCustomWebhookResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + r.WebhookResource.ImportState(ctx, req, resp) +} + +func (m ReleaseBundleV2PromotionCustomWebhookResourceModel) toAPIModel(ctx context.Context, domain string, apiModel *CustomWebhookAPIModel) (diags diag.Diagnostics) { + criteriaObj := m.Criteria.Elements()[0].(types.Object) + criteriaAttrs := criteriaObj.Attributes() + + criteriaAPIModel, d := toReleaseBundleV2PromotionAPIModel(ctx, criteriaAttrs) + if d.HasError() { + diags.Append(d...) + } + + d = m.CustomWebhookResourceModel.toAPIModel(ctx, domain, criteriaAPIModel, apiModel) + if d.HasError() { + diags.Append(d...) + } + + return +} + +func (m *ReleaseBundleV2PromotionCustomWebhookResourceModel) fromAPIModel(ctx context.Context, apiModel CustomWebhookAPIModel, stateHandlers basetypes.SetValue) diag.Diagnostics { + diags := diag.Diagnostics{} + + criteriaAPIModel := apiModel.EventFilter.Criteria.(map[string]interface{}) + + baseCriteriaAttrs, d := m.CustomWebhookResourceModel.fromBaseCriteriaAPIModel(ctx, criteriaAPIModel) + + criteriaSet, d := fromReleaseBundleV2PromotionAPIModel(ctx, criteriaAPIModel, baseCriteriaAttrs) + if d.HasError() { + diags.Append(d...) + } + + d = m.CustomWebhookResourceModel.fromAPIModel(ctx, apiModel, stateHandlers, &criteriaSet) + if d.HasError() { + diags.Append(d...) + } + + return diags +} diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook_release_bundle_v2_promotion_test.go b/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook_release_bundle_v2_promotion_test.go new file mode 100644 index 00000000..8d757f9c --- /dev/null +++ b/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook_release_bundle_v2_promotion_test.go @@ -0,0 +1,161 @@ +package webhook_test + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/plancheck" + "github.com/jfrog/terraform-provider-artifactory/v12/pkg/acctest" + "github.com/jfrog/terraform-provider-shared/testutil" + "github.com/jfrog/terraform-provider-shared/util" +) + +func TestAccCustomWebhook_ReleaseBundleV2Promotion_UpgradeFromSDKv2(t *testing.T) { + _, fqrn, name := testutil.MkNames("test-release-bundle-v2-promotion-webhook", "artifactory_release_bundle_v2_promotion_custom_webhook") + + params := map[string]interface{}{ + "webhookName": name, + } + config := util.ExecuteTemplate("TestAccCustomWebhookReleaseBundleV2Promotion", ` + resource "artifactory_release_bundle_v2_promotion_custom_webhook" "{{ .webhookName }}" { + key = "{{ .webhookName }}" + description = "test description" + event_types = [ + "release_bundle_v2_promotion_completed", + "release_bundle_v2_promotion_failed", + "release_bundle_v2_promotion_started", + ] + criteria { + selected_environments = [ + "PROD", + "DEV", + ] + } + handler { + url = "https://google.com" + secrets = { + secret1 = "value1" + secret2 = "value2" + } + http_headers = { + header-1 = "value-1" + header-2 = "value-2" + } + payload = "{ \"ref\": \"main\" , \"inputs\": { \"artifact_path\": \"test-repo/repo-path\" } }" + } + } + `, params) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + CheckDestroy: acctest.VerifyDeleted(fqrn, "key", acctest.CheckRepo), + + Steps: []resource.TestStep{ + { + Config: config, + ExternalProviders: map[string]resource.ExternalProvider{ + "artifactory": { + Source: "jfrog/artifactory", + VersionConstraint: "12.1.0", + }, + }, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(fqrn, "event_types.#", "3"), + resource.TestCheckResourceAttr(fqrn, "criteria.#", "1"), + resource.TestCheckResourceAttr(fqrn, "criteria.0.selected_environments.#", "2"), + resource.TestCheckTypeSetElemAttr(fqrn, "criteria.0.selected_environments.*", "PROD"), + resource.TestCheckTypeSetElemAttr(fqrn, "criteria.0.selected_environments.*", "DEV"), + resource.TestCheckResourceAttr(fqrn, "handler.#", "1"), + resource.TestCheckResourceAttr(fqrn, "handler.0.url", "https://google.com"), + resource.TestCheckResourceAttr(fqrn, "handler.0.secrets.secret1", "value1"), + resource.TestCheckResourceAttr(fqrn, "handler.0.secrets.secret2", "value2"), + resource.TestCheckResourceAttr(fqrn, "handler.0.http_headers.%", "2"), + resource.TestCheckResourceAttr(fqrn, "handler.0.http_headers.header-1", "value-1"), + resource.TestCheckResourceAttr(fqrn, "handler.0.http_headers.header-2", "value-2"), + resource.TestCheckResourceAttr(fqrn, "handler.0.payload", "{ \"ref\": \"main\" , \"inputs\": { \"artifact_path\": \"test-repo/repo-path\" } }"), + ), + }, + { + Config: config, + ProtoV6ProviderFactories: acctest.ProtoV6ProviderFactories, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectEmptyPlan(), + }, + }, + }, + }, + }) +} + +func TestAccCustomWebhook_ReleaseBundleV2Promotion(t *testing.T) { + _, fqrn, name := testutil.MkNames("test-release-bundle-v2-promotion-webhook", "artifactory_release_bundle_v2_promotion_custom_webhook") + + params := map[string]interface{}{ + "webhookName": name, + } + webhookConfig := util.ExecuteTemplate("TestAccCustomWebhookReleaseBundleV2Promotion", ` + resource "artifactory_release_bundle_v2_promotion_custom_webhook" "{{ .webhookName }}" { + key = "{{ .webhookName }}" + description = "test description" + event_types = [ + "release_bundle_v2_promotion_completed", + "release_bundle_v2_promotion_failed", + "release_bundle_v2_promotion_started", + ] + criteria { + selected_environments = [ + "PROD", + "DEV", + ] + } + handler { + url = "https://google.com" + secrets = { + secret1 = "value1" + secret2 = "value2" + } + http_headers = { + header-1 = "value-1" + header-2 = "value-2" + } + payload = "{ \"ref\": \"main\" , \"inputs\": { \"artifact_path\": \"test-repo/repo-path\" } }" + } + } + `, params) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ProtoV6ProviderFactories: acctest.ProtoV6ProviderFactories, + CheckDestroy: acctest.VerifyDeleted(fqrn, "key", acctest.CheckRepo), + + Steps: []resource.TestStep{ + { + Config: webhookConfig, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(fqrn, "event_types.#", "3"), + resource.TestCheckResourceAttr(fqrn, "criteria.#", "1"), + resource.TestCheckResourceAttr(fqrn, "criteria.0.selected_environments.#", "2"), + resource.TestCheckTypeSetElemAttr(fqrn, "criteria.0.selected_environments.*", "PROD"), + resource.TestCheckTypeSetElemAttr(fqrn, "criteria.0.selected_environments.*", "DEV"), + resource.TestCheckResourceAttr(fqrn, "handler.#", "1"), + resource.TestCheckResourceAttr(fqrn, "handler.0.url", "https://google.com"), + resource.TestCheckResourceAttr(fqrn, "handler.0.secrets.secret1", "value1"), + resource.TestCheckResourceAttr(fqrn, "handler.0.secrets.secret2", "value2"), + resource.TestCheckResourceAttr(fqrn, "handler.0.http_headers.%", "2"), + resource.TestCheckResourceAttr(fqrn, "handler.0.http_headers.header-1", "value-1"), + resource.TestCheckResourceAttr(fqrn, "handler.0.http_headers.header-2", "value-2"), + resource.TestCheckResourceAttr(fqrn, "handler.0.payload", "{ \"ref\": \"main\" , \"inputs\": { \"artifact_path\": \"test-repo/repo-path\" } }"), + ), + }, + { + ResourceName: fqrn, + ImportState: true, + ImportStateId: name, + ImportStateVerify: true, + ImportStateVerifyIdentifierAttribute: "key", + ImportStateVerifyIgnore: []string{"handler.0.secrets"}, + }, + }, + }) +} diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook_test.go b/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook_test.go index 74e77e3e..1faf47db 100644 --- a/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook_test.go +++ b/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook_test.go @@ -114,74 +114,3 @@ func TestAccCustomWebhook_User(t *testing.T) { }, }) } - -func TestAccCustomWebhook_ReleaseBundleV2Promotion(t *testing.T) { - _, fqrn, name := testutil.MkNames("test-release-bundle-v2-promotion-webhook", "artifactory_release_bundle_v2_promotion_custom_webhook") - - params := map[string]interface{}{ - "webhookName": name, - } - webhookConfig := util.ExecuteTemplate("TestAccCustomWebhookReleaseBundleV2Promotion", ` - resource "artifactory_release_bundle_v2_promotion_custom_webhook" "{{ .webhookName }}" { - key = "{{ .webhookName }}" - description = "test description" - event_types = [ - "release_bundle_v2_promotion_completed", - "release_bundle_v2_promotion_failed", - "release_bundle_v2_promotion_started", - ] - criteria { - selected_environments = [ - "PROD", - "DEV", - ] - } - handler { - url = "https://google.com" - secrets = { - secret1 = "value1" - secret2 = "value2" - } - http_headers = { - header-1 = "value-1" - header-2 = "value-2" - } - payload = "{ \"ref\": \"main\" , \"inputs\": { \"artifact_path\": \"test-repo/repo-path\" } }" - } - } - `, params) - - resource.Test(t, resource.TestCase{ - PreCheck: func() { acctest.PreCheck(t) }, - ProviderFactories: acctest.ProviderFactories, - CheckDestroy: acctest.VerifyDeleted(fqrn, "", acctest.CheckRepo), - - Steps: []resource.TestStep{ - { - Config: webhookConfig, - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr(fqrn, "event_types.#", "3"), - resource.TestCheckResourceAttr(fqrn, "criteria.#", "1"), - resource.TestCheckResourceAttr(fqrn, "criteria.0.selected_environments.#", "2"), - resource.TestCheckTypeSetElemAttr(fqrn, "criteria.0.selected_environments.*", "PROD"), - resource.TestCheckTypeSetElemAttr(fqrn, "criteria.0.selected_environments.*", "DEV"), - resource.TestCheckResourceAttr(fqrn, "handler.#", "1"), - resource.TestCheckResourceAttr(fqrn, "handler.0.url", "https://google.com"), - resource.TestCheckResourceAttr(fqrn, "handler.0.secrets.secret1", "value1"), - resource.TestCheckResourceAttr(fqrn, "handler.0.secrets.secret2", "value2"), - resource.TestCheckResourceAttr(fqrn, "handler.0.http_headers.%", "2"), - resource.TestCheckResourceAttr(fqrn, "handler.0.http_headers.header-1", "value-1"), - resource.TestCheckResourceAttr(fqrn, "handler.0.http_headers.header-2", "value-2"), - resource.TestCheckResourceAttr(fqrn, "handler.0.payload", "{ \"ref\": \"main\" , \"inputs\": { \"artifact_path\": \"test-repo/repo-path\" } }"), - ), - }, - { - ResourceName: fqrn, - ImportState: true, - ImportStateVerify: true, - ImportStateCheck: validator.CheckImportState(name, "key"), - ImportStateVerifyIgnore: []string{"handler.0.secrets"}, - }, - }, - }) -} diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_webhook_release_bundle_v2_promotion.go b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_release_bundle_v2_promotion.go index eb10b9d4..63ace990 100644 --- a/pkg/artifactory/resource/webhook/resource_artifactory_webhook_release_bundle_v2_promotion.go +++ b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_release_bundle_v2_promotion.go @@ -16,7 +16,7 @@ import ( "github.com/samber/lo" ) -var _ resource.Resource = &ReleaseBundleV2WebhookResource{} +var _ resource.Resource = &ReleaseBundleV2PromotionWebhookResource{} func NewReleaseBundleV2PromotionWebhookResource() resource.Resource { return &ReleaseBundleV2PromotionWebhookResource{ @@ -40,28 +40,29 @@ func (r *ReleaseBundleV2PromotionWebhookResource) Metadata(ctx context.Context, r.WebhookResource.Metadata(ctx, req, resp) } -func (r *ReleaseBundleV2PromotionWebhookResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { - criteriaBlock := schema.SetNestedBlock{ - NestedObject: schema.NestedBlockObject{ - Attributes: lo.Assign( - patternsSchemaAttributes(""), - map[string]schema.Attribute{ - "selected_environments": schema.SetAttribute{ - ElementType: types.StringType, - Required: true, - Description: "Trigger on this list of environments", - }, +var releaseBundleV2PromotionCriteriaBlock = schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: lo.Assign( + patternsSchemaAttributes(""), + map[string]schema.Attribute{ + "selected_environments": schema.SetAttribute{ + ElementType: types.StringType, + Required: true, + Description: "Trigger on this list of environments", }, - ), - }, - Validators: []validator.Set{ - setvalidator.SizeBetween(1, 1), - setvalidator.IsRequired(), - }, - Description: "Specifies where the webhook will be applied, on which release bundles or distributions.", - } + }, + ), + }, + Validators: []validator.Set{ + setvalidator.SizeBetween(1, 1), + setvalidator.IsRequired(), + }, + Description: "Specifies where the webhook will be applied, on which release bundles or distributions.", +} - resp.Schema = r.CreateSchema(r.Domain, &criteriaBlock, handlerBlock) +func (r *ReleaseBundleV2PromotionWebhookResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + + resp.Schema = r.CreateSchema(r.Domain, &releaseBundleV2PromotionCriteriaBlock, handlerBlock) } func (r *ReleaseBundleV2PromotionWebhookResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { @@ -172,20 +173,29 @@ func (r *ReleaseBundleV2PromotionWebhookResource) ImportState(ctx context.Contex r.WebhookResource.ImportState(ctx, req, resp) } -func (m ReleaseBundleV2PromotionWebhookResourceModel) toAPIModel(ctx context.Context, domain string, apiModel *WebhookAPIModel) (diags diag.Diagnostics) { - criteriaObj := m.Criteria.Elements()[0].(types.Object) - criteriaAttrs := criteriaObj.Attributes() - +func toReleaseBundleV2PromotionAPIModel(ctx context.Context, criteriaAttrs map[string]attr.Value) (criteriaAPIModel ReleaseBundleV2PromotionCriteriaAPIModel, diags diag.Diagnostics) { var environments []string d := criteriaAttrs["selected_environments"].(types.Set).ElementsAs(ctx, &environments, false) if d.HasError() { diags.Append(d...) } - criteriaAPIModel := ReleaseBundleV2PromotionCriteriaAPIModel{ + criteriaAPIModel = ReleaseBundleV2PromotionCriteriaAPIModel{ SelectedEnvironments: environments, } + return +} + +func (m ReleaseBundleV2PromotionWebhookResourceModel) toAPIModel(ctx context.Context, domain string, apiModel *WebhookAPIModel) (diags diag.Diagnostics) { + criteriaObj := m.Criteria.Elements()[0].(types.Object) + criteriaAttrs := criteriaObj.Attributes() + + criteriaAPIModel, d := toReleaseBundleV2PromotionAPIModel(ctx, criteriaAttrs) + if d.HasError() { + diags.Append(d...) + } + d = m.WebhookResourceModel.toAPIModel(ctx, domain, criteriaAPIModel, apiModel) if d.HasError() { diags.Append(d...) @@ -205,13 +215,7 @@ var releaseBundleV2PromotionCriteriaSetResourceModelElementTypes = types.ObjectT AttrTypes: releaseBundleV2PromotionCriteriaSetResourceModelAttributeTypes, } -func (m *ReleaseBundleV2PromotionWebhookResourceModel) fromAPIModel(ctx context.Context, apiModel WebhookAPIModel, stateHandlers basetypes.SetValue) diag.Diagnostics { - diags := diag.Diagnostics{} - - criteriaAPIModel := apiModel.EventFilter.Criteria.(map[string]interface{}) - - baseCriteriaAttrs, d := m.WebhookResourceModel.fromBaseCriteriaAPIModel(ctx, criteriaAPIModel) - +func fromReleaseBundleV2PromotionAPIModel(ctx context.Context, criteriaAPIModel map[string]interface{}, baseCriteriaAttrs map[string]attr.Value) (criteriaSet basetypes.SetValue, diags diag.Diagnostics) { releaseBundleNames := types.SetNull(types.StringType) if v, ok := criteriaAPIModel["selectedEnvironments"]; ok && v != nil { rb, d := types.SetValueFrom(ctx, types.StringType, v) @@ -234,7 +238,8 @@ func (m *ReleaseBundleV2PromotionWebhookResourceModel) fromAPIModel(ctx context. if d.HasError() { diags.Append(d...) } - criteriaSet, d := types.SetValue( + + criteriaSet, d = types.SetValue( releaseBundleV2PromotionCriteriaSetResourceModelElementTypes, []attr.Value{criteria}, ) @@ -242,6 +247,21 @@ func (m *ReleaseBundleV2PromotionWebhookResourceModel) fromAPIModel(ctx context. diags.Append(d...) } + return +} + +func (m *ReleaseBundleV2PromotionWebhookResourceModel) fromAPIModel(ctx context.Context, apiModel WebhookAPIModel, stateHandlers basetypes.SetValue) diag.Diagnostics { + diags := diag.Diagnostics{} + + criteriaAPIModel := apiModel.EventFilter.Criteria.(map[string]interface{}) + + baseCriteriaAttrs, d := m.WebhookResourceModel.fromBaseCriteriaAPIModel(ctx, criteriaAPIModel) + + criteriaSet, d := fromReleaseBundleV2PromotionAPIModel(ctx, criteriaAPIModel, baseCriteriaAttrs) + if d.HasError() { + diags.Append(d...) + } + d = m.WebhookResourceModel.fromAPIModel(ctx, apiModel, stateHandlers, &criteriaSet) if d.HasError() { diags.Append(d...) From 08f5600f6653e1446eceef1fb3e29b1d4e42a7a0 Mon Sep 17 00:00:00 2001 From: Alex Hung Date: Tue, 1 Oct 2024 14:40:09 -0700 Subject: [PATCH 09/12] Migrate user custom webhook resource to Plugin Framework --- pkg/artifactory/provider/framework.go | 1 + pkg/artifactory/provider/resources.go | 6 - .../resource_artifactory_custom_webhook.go | 528 ------------------ ...esource_artifactory_custom_webhook_test.go | 58 -- ...esource_artifactory_custom_webhook_user.go | 169 ++++++ ...ce_artifactory_custom_webhook_user_test.go | 133 +++++ .../resource_artifactory_webhook_base.go | 183 ------ .../resource_artifactory_webhook_user.go | 2 +- 8 files changed, 304 insertions(+), 776 deletions(-) create mode 100644 pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook_user.go create mode 100644 pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook_user_test.go delete mode 100644 pkg/artifactory/resource/webhook/resource_artifactory_webhook_base.go diff --git a/pkg/artifactory/provider/framework.go b/pkg/artifactory/provider/framework.go index f03e2929..f0cb52a5 100644 --- a/pkg/artifactory/provider/framework.go +++ b/pkg/artifactory/provider/framework.go @@ -245,6 +245,7 @@ func (p *ArtifactoryProvider) Resources(ctx context.Context) []func() resource.R webhook.NewReleaseBundleV2PromotionWebhookResource, webhook.NewReleaseBundleV2PromotionCustomWebhookResource, webhook.NewUserWebhookResource, + webhook.NewUserCustomWebhookResource, } } diff --git a/pkg/artifactory/provider/resources.go b/pkg/artifactory/provider/resources.go index 0ce00edc..07ce411d 100644 --- a/pkg/artifactory/provider/resources.go +++ b/pkg/artifactory/provider/resources.go @@ -12,7 +12,6 @@ import ( "github.com/jfrog/terraform-provider-artifactory/v12/pkg/artifactory/resource/repository/remote" "github.com/jfrog/terraform-provider-artifactory/v12/pkg/artifactory/resource/repository/virtual" "github.com/jfrog/terraform-provider-artifactory/v12/pkg/artifactory/resource/security" - "github.com/jfrog/terraform-provider-artifactory/v12/pkg/artifactory/resource/webhook" utilsdk "github.com/jfrog/terraform-provider-shared/util/sdk" ) @@ -124,10 +123,5 @@ func resourcesMap() map[string]*schema.Resource { resourcesMap[federatedResourceName] = federated.ResourceArtifactoryFederatedGenericRepository(repoType) } - for _, webhookType := range webhook.DomainSupported { - webhookCustomResourceName := fmt.Sprintf("artifactory_%s_custom_webhook", webhookType) - resourcesMap[webhookCustomResourceName] = webhook.ResourceArtifactoryCustomWebhook(webhookType) - } - return utilsdk.AddTelemetry(productId, resourcesMap) } diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook.go b/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook.go index af7bbda0..00733b9c 100644 --- a/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook.go +++ b/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook.go @@ -2,12 +2,8 @@ package webhook import ( "context" - "fmt" - "net/http" "regexp" - "strings" - "github.com/go-resty/resty/v2" "github.com/hashicorp/terraform-plugin-framework-validators/mapvalidator" "github.com/hashicorp/terraform-plugin-framework-validators/setvalidator" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" @@ -20,18 +16,8 @@ import ( "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - sdkv2_diag "github.com/hashicorp/terraform-plugin-sdk/v2/diag" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff" - sdkv2_schema "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" - "github.com/jfrog/terraform-provider-artifactory/v12/pkg/artifactory" - "github.com/jfrog/terraform-provider-shared/util" - utilsdk "github.com/jfrog/terraform-provider-shared/util/sdk" - util_validator "github.com/jfrog/terraform-provider-shared/validator" validatorfw_string "github.com/jfrog/terraform-provider-shared/validator/fw/string" "github.com/samber/lo" - - "golang.org/x/exp/slices" ) type CustomWebhookResource struct { @@ -290,104 +276,6 @@ func (m *CustomWebhookResourceModel) fromAPIModel(ctx context.Context, apiModel return m.CustomWebhookBaseResourceModel.fromAPIModel(ctx, apiModel, stateHandlers) } -var DomainSupported = []string{ - UserDomain, -} - -func baseCustomWebhookBaseSchema(webhookType string) map[string]*sdkv2_schema.Schema { - return map[string]*sdkv2_schema.Schema{ - "key": { - Type: sdkv2_schema.TypeString, - Required: true, - ValidateDiagFunc: validation.ToDiagFunc( - validation.All( - validation.StringLenBetween(2, 200), - validation.StringDoesNotContainAny(" "), - ), - ), - Description: "Key of webhook. Must be between 2 and 200 characters. Cannot contain spaces.", - }, - "description": { - Type: sdkv2_schema.TypeString, - Optional: true, - ValidateDiagFunc: validation.ToDiagFunc(validation.StringLenBetween(0, 1000)), - Description: "Description of webhook. Max length 1000 characters.", - }, - "enabled": { - Type: sdkv2_schema.TypeBool, - Optional: true, - Default: true, - Description: "Status of webhook. Default to 'true'", - }, - "event_types": { - Type: sdkv2_schema.TypeSet, - Required: true, - MinItems: 1, - Elem: &sdkv2_schema.Schema{Type: sdkv2_schema.TypeString}, - Description: fmt.Sprintf("List of Events in Artifactory, Distribution, Release Bundle that function as the event trigger for the Webhook.\n"+ - "Allow values: %v", strings.Trim(strings.Join(DomainEventTypesSupported[webhookType], ", "), "[]")), - }, - "handler": { - Type: sdkv2_schema.TypeSet, - Required: true, - MinItems: 1, - Elem: &sdkv2_schema.Resource{ - Schema: map[string]*sdkv2_schema.Schema{ - "url": { - Type: sdkv2_schema.TypeString, - Required: true, - ValidateDiagFunc: validation.ToDiagFunc( - validation.All( - validation.IsURLWithHTTPorHTTPS, - validation.StringIsNotEmpty, - ), - ), - Description: "Specifies the URL that the Webhook invokes. This will be the URL that Artifactory will send an HTTP POST request to.", - }, - "secrets": { - Type: sdkv2_schema.TypeMap, - Optional: true, - Elem: &sdkv2_schema.Schema{ - Type: sdkv2_schema.TypeString, - ValidateDiagFunc: validation.ToDiagFunc(validation.StringMatch(regexp.MustCompile("^[a-zA-Z_][a-zA-Z0-9_]*$"), "Secret name must match '^[a-zA-Z_][a-zA-Z0-9_]*$'\"")), - }, - Description: "A set of sensitive values that will be injected in the request (headers and/or payload), comprise of key/value pair.", - }, - "proxy": { - Type: sdkv2_schema.TypeString, - Optional: true, - ValidateDiagFunc: util_validator.All( - util_validator.StringIsNotEmpty, - util_validator.StringIsNotURL, - ), - Description: "Proxy key from Artifactory UI (Administration -> Proxies -> Configuration)", - }, - "http_headers": { - Type: sdkv2_schema.TypeMap, - Optional: true, - Elem: &sdkv2_schema.Schema{Type: sdkv2_schema.TypeString}, - Description: "HTTP headers you wish to use to invoke the Webhook, comprise of key/value pair. Used in custom webhooks.", - }, - "payload": { - Type: sdkv2_schema.TypeString, - Optional: true, - ValidateDiagFunc: validation.ToDiagFunc(validation.StringIsNotEmpty), - Description: "This attribute is used to build the request body. Used in custom webhooks", - }, - }, - }, - }, - } -} - -var userWebhookSchema = func(webhookType string, version int, isCustom bool) map[string]*sdkv2_schema.Schema { - return getBaseSchemaByVersion(webhookType, version, isCustom) -} - -var artifactLifecycleWebhookSchema = func(webhookType string, version int, isCustom bool) map[string]*sdkv2_schema.Schema { - return getBaseSchemaByVersion(webhookType, version, isCustom) -} - type CustomWebhookAPIModel struct { WebhookAPIModel Handlers []CustomHandlerAPIModel `json:"handlers"` @@ -405,419 +293,3 @@ type CustomHandlerAPIModel struct { HttpHeaders []KeyValuePairAPIModel `json:"http_headers"` Payload string `json:"payload,omitempty"` } - -type SecretName struct { - Name string `json:"name"` -} - -var packSecretsCustom = func(keyValuePairs []KeyValuePairAPIModel, d *sdkv2_schema.ResourceData, url string) map[string]interface{} { - KVPairs := make(map[string]interface{}) - // Get secrets from TF state - var secrets map[string]interface{} - if v, ok := d.GetOk("handler"); ok { - handlers := v.(*sdkv2_schema.Set).List() - for _, handler := range handlers { - h := handler.(map[string]interface{}) - // if url match, merge secret maps - if h["url"] == url { - secrets = utilsdk.MergeMaps(secrets, h["secrets"].(map[string]interface{})) - } - } - } - // We assign secret the value from the state, because it's not returned in the API body response - for _, keyValuePair := range keyValuePairs { - if v, ok := secrets[keyValuePair.Name]; ok { - KVPairs[keyValuePair.Name] = v.(string) - } - } - - return KVPairs -} - -func ResourceArtifactoryCustomWebhook(webhookType string) *sdkv2_schema.Resource { - - var unpackWebhook = func(data *sdkv2_schema.ResourceData) (CustomWebhookAPIModel, error) { - d := &utilsdk.ResourceData{ResourceData: data} - - var unpackHandlers = func(d *utilsdk.ResourceData) []CustomHandlerAPIModel { - var webhookHandlers []CustomHandlerAPIModel - - if v, ok := d.GetOk("handler"); ok { - handlers := v.(*sdkv2_schema.Set).List() - for _, handler := range handlers { - h := handler.(map[string]interface{}) - // use this to filter out weirdness with terraform adding an extra blank webhook in a set - // https://discuss.hashicorp.com/t/using-typeset-in-provider-always-adds-an-empty-element-on-update/18566/2 - if h["url"].(string) != "" { - webhookHandler := CustomHandlerAPIModel{ - HandlerType: "custom-webhook", - Url: h["url"].(string), - } - - if v, ok := h["secrets"]; ok { - webhookHandler.Secrets = unpackKeyValuePair(v.(map[string]interface{})) - } - - if v, ok := h["proxy"]; ok { - if s, ok := v.(string); ok { - webhookHandler.Proxy = &s - } - } - - if v, ok := h["http_headers"]; ok { - webhookHandler.HttpHeaders = unpackKeyValuePair(v.(map[string]interface{})) - } - - if v, ok := h["payload"]; ok { - webhookHandler.Payload = v.(string) - } - - webhookHandlers = append(webhookHandlers, webhookHandler) - } - } - } - - return webhookHandlers - } - - webhook := CustomWebhookAPIModel{ - WebhookAPIModel: WebhookAPIModel{ - Key: d.GetString("key", false), - Description: d.GetString("description", false), - Enabled: d.GetBool("enabled", false), - EventFilter: EventFilterAPIModel{ - Domain: webhookType, - EventTypes: d.GetSet("event_types"), - Criteria: unpackCriteria(d, webhookType), - }, - }, - Handlers: unpackHandlers(d), - } - - return webhook, nil - } - - var packHandlers = func(d *sdkv2_schema.ResourceData, handlers []CustomHandlerAPIModel) []error { - setValue := utilsdk.MkLens(d) - resource := domainSchemaLookup(currentSchemaVersion, true, webhookType)[webhookType]["handler"].Elem.(*sdkv2_schema.Resource) - packedHandlers := make([]interface{}, len(handlers)) - for _, handler := range handlers { - packedHandler := map[string]interface{}{ - "url": handler.Url, - "payload": handler.Payload, - } - - if handler.Proxy != nil { - packedHandler["proxy"] = *handler.Proxy - } - - if handler.Secrets != nil { - packedHandler["secrets"] = packSecretsCustom(handler.Secrets, d, handler.Url) - } - - if handler.HttpHeaders != nil { - packedHandler["http_headers"] = packKeyValuePair(handler.HttpHeaders) - } - - packedHandlers = append(packedHandlers, packedHandler) - } - - return setValue("handler", sdkv2_schema.NewSet(sdkv2_schema.HashResource(resource), packedHandlers)) - } - - var packWebhook = func(d *sdkv2_schema.ResourceData, webhook CustomWebhookAPIModel) sdkv2_diag.Diagnostics { - setValue := utilsdk.MkLens(d) - - setValue("key", webhook.Key) - setValue("description", webhook.Description) - setValue("enabled", webhook.Enabled) - errors := setValue("event_types", webhook.EventFilter.EventTypes) - if webhook.EventFilter.Criteria != nil { - errors = append(errors, packCriteria(d, webhookType, webhook.EventFilter.Criteria.(map[string]interface{}))...) - } - errors = append(errors, packHandlers(d, webhook.Handlers)...) - - if len(errors) > 0 { - return sdkv2_diag.Errorf("failed to pack webhook %q", errors) - } - - return nil - } - - var readWebhook = func(ctx context.Context, data *sdkv2_schema.ResourceData, m interface{}) sdkv2_diag.Diagnostics { - webhook := CustomWebhookAPIModel{} - - webhook.EventFilter.Criteria = domainCriteriaLookup[webhookType] - - var artifactoryError artifactory.ArtifactoryErrorsResponse - resp, err := m.(util.ProviderMetadata).Client.R(). - SetPathParam("webhookKey", data.Id()). - SetResult(&webhook). - SetError(&artifactoryError). - Get(WebhookURL) - - if err != nil { - return sdkv2_diag.FromErr(err) - } - - if resp.StatusCode() == http.StatusNotFound { - data.SetId("") - return nil - } - - if resp.IsError() { - return sdkv2_diag.Errorf("%s", artifactoryError.String()) - } - - return packWebhook(data, webhook) - } - - var retryOnProxyError = func(response *resty.Response, _r error) bool { - var proxyNotFoundRegex = regexp.MustCompile("proxy with key '.*' not found") - - return proxyNotFoundRegex.MatchString(string(response.Body()[:])) - } - - var createWebhook = func(ctx context.Context, data *sdkv2_schema.ResourceData, m interface{}) sdkv2_diag.Diagnostics { - webhook, err := unpackWebhook(data) - if err != nil { - return sdkv2_diag.FromErr(err) - } - - var artifactoryError artifactory.ArtifactoryErrorsResponse - resp, err := m.(util.ProviderMetadata).Client.R(). - SetBody(webhook). - SetError(&artifactoryError). - AddRetryCondition(retryOnProxyError). - Post(webhooksURL) - if err != nil { - return sdkv2_diag.FromErr(err) - } - - if resp.IsError() { - return sdkv2_diag.Errorf("%s", artifactoryError.String()) - } - - data.SetId(webhook.Id()) - - return readWebhook(ctx, data, m) - } - - var updateWebhook = func(ctx context.Context, data *sdkv2_schema.ResourceData, m interface{}) sdkv2_diag.Diagnostics { - webhook, err := unpackWebhook(data) - if err != nil { - return sdkv2_diag.FromErr(err) - } - - var artifactoryError artifactory.ArtifactoryErrorsResponse - resp, err := m.(util.ProviderMetadata).Client.R(). - SetPathParam("webhookKey", data.Id()). - SetBody(webhook). - SetError(&artifactoryError). - AddRetryCondition(retryOnProxyError). - Put(WebhookURL) - if err != nil { - return sdkv2_diag.FromErr(err) - } - - if resp.IsError() { - return sdkv2_diag.Errorf("%s", artifactoryError.String()) - } - - data.SetId(webhook.Id()) - - return readWebhook(ctx, data, m) - } - - var deleteWebhook = func(ctx context.Context, data *sdkv2_schema.ResourceData, m interface{}) sdkv2_diag.Diagnostics { - var artifactoryError artifactory.ArtifactoryErrorsResponse - resp, err := m.(util.ProviderMetadata).Client.R(). - SetPathParam("webhookKey", data.Id()). - SetError(&artifactoryError). - Delete(WebhookURL) - - if err != nil { - return sdkv2_diag.FromErr(err) - } - - if resp.StatusCode() == http.StatusNotFound { - data.SetId("") - return nil - } - - if resp.IsError() { - return sdkv2_diag.Errorf("%s", artifactoryError.String()) - } - - return nil - } - - var eventTypesDiff = func(ctx context.Context, diff *sdkv2_schema.ResourceDiff, v interface{}) error { - eventTypes := diff.Get("event_types").(*sdkv2_schema.Set).List() - if len(eventTypes) == 0 { - return nil - } - - eventTypesSupported := DomainEventTypesSupported[webhookType] - for _, eventType := range eventTypes { - if !slices.Contains(eventTypesSupported, eventType.(string)) { - return fmt.Errorf("event_type %s not supported for domain %s", eventType, webhookType) - } - } - return nil - } - - var criteriaDiff = func(ctx context.Context, diff *sdkv2_schema.ResourceDiff, v interface{}) error { - if resource, ok := diff.GetOk("criteria"); ok { - criteria := resource.(*sdkv2_schema.Set).List() - if len(criteria) == 0 { - return nil - } - return domainCriteriaValidationLookup[webhookType](ctx, criteria[0].(map[string]interface{})) - } - - return nil - } - - rs := sdkv2_schema.Resource{ - SchemaVersion: 2, - CreateContext: createWebhook, - ReadContext: readWebhook, - UpdateContext: updateWebhook, - DeleteContext: deleteWebhook, - - Importer: &sdkv2_schema.ResourceImporter{ - StateContext: sdkv2_schema.ImportStatePassthroughContext, - }, - - Schema: domainSchemaLookup(currentSchemaVersion, true, webhookType)[webhookType], - - CustomizeDiff: customdiff.All( - eventTypesDiff, - criteriaDiff, - ), - Description: "Provides an Artifactory webhook resource", - } - - if webhookType == ReleaseBundleDomain { - rs.DeprecationMessage = "This resource is being deprecated and replaced by artifactory_destination_custom_webhook resource" - } - - return &rs -} - -var unpackKeyValuePair = func(keyValuePairs map[string]interface{}) []KeyValuePairAPIModel { - var kvPairs []KeyValuePairAPIModel - for key, value := range keyValuePairs { - keyValuePair := KeyValuePairAPIModel{ - Name: key, - Value: value.(string), - } - kvPairs = append(kvPairs, keyValuePair) - } - - return kvPairs -} - -var packKeyValuePair = func(keyValuePairs []KeyValuePairAPIModel) map[string]interface{} { - kvPairs := make(map[string]interface{}) - for _, keyValuePair := range keyValuePairs { - kvPairs[keyValuePair.Name] = keyValuePair.Value - } - - return kvPairs -} - -type EmptyWebhookCriteria struct{} - -var domainCriteriaLookup = map[string]interface{}{ - "user": EmptyWebhookCriteria{}, -} - -var domainPackLookup = map[string]func(map[string]interface{}) map[string]interface{}{ - "user": packEmptyCriteria, -} - -var domainUnpackLookup = map[string]func(map[string]interface{}, BaseCriteriaAPIModel) interface{}{ - "user": unpackEmptyCriteria, -} - -var domainSchemaLookup = func(version int, isCustom bool, webhookType string) map[string]map[string]*sdkv2_schema.Schema { - return map[string]map[string]*sdkv2_schema.Schema{ - "user": userWebhookSchema(webhookType, version, isCustom), - } -} - -var packEmptyCriteria = func(artifactoryCriteria map[string]interface{}) map[string]interface{} { - return map[string]interface{}{} -} - -var unpackEmptyCriteria = func(terraformCriteria map[string]interface{}, baseCriteria BaseCriteriaAPIModel) interface{} { - return EmptyWebhookCriteria{} -} - -var unpackCriteria = func(d *utilsdk.ResourceData, webhookType string) interface{} { - var webhookCriteria interface{} - - if v, ok := d.GetOk("criteria"); ok { - criteria := v.(*sdkv2_schema.Set).List() - if len(criteria) == 1 { - id := criteria[0].(map[string]interface{}) - - baseCriteria := BaseCriteriaAPIModel{ - IncludePatterns: utilsdk.CastToStringArr(id["include_patterns"].(*sdkv2_schema.Set).List()), - ExcludePatterns: utilsdk.CastToStringArr(id["exclude_patterns"].(*sdkv2_schema.Set).List()), - } - - webhookCriteria = domainUnpackLookup[webhookType](id, baseCriteria) - } - } - - return webhookCriteria -} - -var packCriteria = func(d *sdkv2_schema.ResourceData, webhookType string, criteria map[string]interface{}) []error { - setValue := utilsdk.MkLens(d) - - resource := domainSchemaLookup(currentSchemaVersion, false, webhookType)[webhookType]["criteria"].Elem.(*sdkv2_schema.Resource) - packedCriteria := domainPackLookup[webhookType](criteria) - - includePatterns := []interface{}{} - if v, ok := criteria["includePatterns"]; ok && v != nil { - includePatterns = v.([]interface{}) - } - packedCriteria["include_patterns"] = sdkv2_schema.NewSet(sdkv2_schema.HashString, includePatterns) - - excludePatterns := []interface{}{} - if v, ok := criteria["excludePatterns"]; ok && v != nil { - excludePatterns = v.([]interface{}) - } - packedCriteria["exclude_patterns"] = sdkv2_schema.NewSet(sdkv2_schema.HashString, excludePatterns) - - return setValue("criteria", sdkv2_schema.NewSet(sdkv2_schema.HashResource(resource), []interface{}{packedCriteria})) -} - -var domainCriteriaValidationLookup = map[string]func(context.Context, map[string]interface{}) error{ - "user": emptyCriteriaValidation, -} - -var emptyCriteriaValidation = func(ctx context.Context, criteria map[string]interface{}) error { - return nil -} - -var packSecret = func(d *sdkv2_schema.ResourceData, url string) string { - // Get secret from TF state - var secret string - if v, ok := d.GetOk("handler"); ok { - handlers := v.(*sdkv2_schema.Set).List() - for _, handler := range handlers { - h := handler.(map[string]interface{}) - // if urls match, assign the secret value from the state - if h["url"].(string) == url { - secret = h["secret"].(string) - } - } - } - - return secret -} diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook_test.go b/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook_test.go index 1faf47db..3f427710 100644 --- a/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook_test.go +++ b/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook_test.go @@ -10,7 +10,6 @@ import ( "github.com/jfrog/terraform-provider-artifactory/v12/pkg/artifactory/resource/webhook" "github.com/jfrog/terraform-provider-shared/testutil" "github.com/jfrog/terraform-provider-shared/util" - "github.com/jfrog/terraform-provider-shared/validator" ) func TestAccCustomWebhook_CriteriaValidation(t *testing.T) { @@ -57,60 +56,3 @@ func customWebhookCriteriaValidationTestCase(webhookType string, t *testing.T) ( }, } } - -func TestAccCustomWebhook_User(t *testing.T) { - _, fqrn, name := testutil.MkNames("test-user-webhook", "artifactory_user_custom_webhook") - - params := map[string]interface{}{ - "webhookName": name, - } - webhookConfig := util.ExecuteTemplate("TestAccCustomWebhookUser", ` - resource "artifactory_user_custom_webhook" "{{ .webhookName }}" { - key = "{{ .webhookName }}" - description = "test description" - event_types = ["locked"] - handler { - url = "https://google.com" - secrets = { - secret1 = "value1" - secret2 = "value2" - } - http_headers = { - header-1 = "value-1" - header-2 = "value-2" - } - payload = "{ \"ref\": \"main\" , \"inputs\": { \"artifact_path\": \"test-repo/repo-path\" } }" - } - } - `, params) - - resource.Test(t, resource.TestCase{ - PreCheck: func() { acctest.PreCheck(t) }, - ProviderFactories: acctest.ProviderFactories, - CheckDestroy: acctest.VerifyDeleted(fqrn, "", acctest.CheckRepo), - - Steps: []resource.TestStep{ - { - Config: webhookConfig, - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr(fqrn, "event_types.#", "1"), - resource.TestCheckResourceAttr(fqrn, "handler.#", "1"), - resource.TestCheckResourceAttr(fqrn, "handler.0.url", "https://google.com"), - resource.TestCheckResourceAttr(fqrn, "handler.0.secrets.secret1", "value1"), - resource.TestCheckResourceAttr(fqrn, "handler.0.secrets.secret2", "value2"), - resource.TestCheckResourceAttr(fqrn, "handler.0.http_headers.%", "2"), - resource.TestCheckResourceAttr(fqrn, "handler.0.http_headers.header-1", "value-1"), - resource.TestCheckResourceAttr(fqrn, "handler.0.http_headers.header-2", "value-2"), - resource.TestCheckResourceAttr(fqrn, "handler.0.payload", "{ \"ref\": \"main\" , \"inputs\": { \"artifact_path\": \"test-repo/repo-path\" } }"), - ), - }, - { - ResourceName: fqrn, - ImportState: true, - ImportStateVerify: true, - ImportStateCheck: validator.CheckImportState(name, "key"), - ImportStateVerifyIgnore: []string{"handler.0.secrets"}, - }, - }, - }) -} diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook_user.go b/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook_user.go new file mode 100644 index 00000000..453113cc --- /dev/null +++ b/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook_user.go @@ -0,0 +1,169 @@ +package webhook + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + "github.com/jfrog/terraform-provider-shared/util" +) + +var _ resource.Resource = &UserCustomWebhookResource{} + +func NewUserCustomWebhookResource() resource.Resource { + return &UserCustomWebhookResource{ + CustomWebhookResource: CustomWebhookResource{ + WebhookResource: WebhookResource{ + TypeName: fmt.Sprintf("artifactory_%s_custom_webhook", UserDomain), + Domain: UserDomain, + Description: "Provides an Artifactory webhook resource. This can be used to register and manage Artifactory webhook subscription which enables you to be notified or notify other users when such events take place in Artifactory.:", + }, + }, + } +} + +type UserCustomWebhookResourceModel struct { + CustomWebhookBaseResourceModel +} + +func (m UserCustomWebhookResourceModel) toAPIModel(ctx context.Context, domain string, apiModel *CustomWebhookAPIModel) (diags diag.Diagnostics) { + d := m.CustomWebhookBaseResourceModel.toAPIModel(ctx, domain, apiModel) + if d.HasError() { + diags.Append(d...) + } + + return +} + +func (m *UserCustomWebhookResourceModel) fromAPIModel(ctx context.Context, apiModel CustomWebhookAPIModel, stateHandlers basetypes.SetValue) diag.Diagnostics { + diags := diag.Diagnostics{} + + d := m.CustomWebhookBaseResourceModel.fromAPIModel(ctx, apiModel, stateHandlers) + if d.HasError() { + diags.Append(d...) + } + + return diags +} + +type UserCustomWebhookResource struct { + CustomWebhookResource +} + +func (r *UserCustomWebhookResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + r.WebhookResource.Metadata(ctx, req, resp) +} + +func (r *UserCustomWebhookResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = r.CreateSchema(r.Domain, nil) +} + +func (r *UserCustomWebhookResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + r.WebhookResource.Configure(ctx, req, resp) +} + +func (r *UserCustomWebhookResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + go util.SendUsageResourceCreate(ctx, r.ProviderData.Client.R(), r.ProviderData.ProductId, r.TypeName) + + var plan UserCustomWebhookResourceModel + + // Read Terraform plan data into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + var webhook CustomWebhookAPIModel + resp.Diagnostics.Append(plan.toAPIModel(ctx, r.Domain, &webhook)...) + if resp.Diagnostics.HasError() { + return + } + + r.CustomWebhookResource.Create(ctx, webhook, resp) + if resp.Diagnostics.HasError() { + return + } + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *UserCustomWebhookResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + go util.SendUsageResourceRead(ctx, r.ProviderData.Client.R(), r.ProviderData.ProductId, r.TypeName) + + var state UserCustomWebhookResourceModel + + // Read Terraform state data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + var webhook CustomWebhookAPIModel + found := r.CustomWebhookResource.Read(ctx, state.Key.ValueString(), &webhook, resp) + if resp.Diagnostics.HasError() { + return + } + + if !found { + return + } + + resp.Diagnostics.Append(state.fromAPIModel(ctx, webhook, state.Handlers)...) + if resp.Diagnostics.HasError() { + return + } + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *UserCustomWebhookResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + go util.SendUsageResourceUpdate(ctx, r.ProviderData.Client.R(), r.ProviderData.ProductId, r.TypeName) + + var plan UserCustomWebhookResourceModel + + // Read Terraform plan data into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + var webhook CustomWebhookAPIModel + resp.Diagnostics.Append(plan.toAPIModel(ctx, r.Domain, &webhook)...) + if resp.Diagnostics.HasError() { + return + } + + r.CustomWebhookResource.Update(ctx, plan.Key.ValueString(), webhook, resp) + if resp.Diagnostics.HasError() { + return + } + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *UserCustomWebhookResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + go util.SendUsageResourceDelete(ctx, r.ProviderData.Client.R(), r.ProviderData.ProductId, r.TypeName) + + var state UserCustomWebhookResourceModel + + // Read Terraform prior state data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + + r.WebhookResource.Delete(ctx, state.Key.ValueString(), resp) + if resp.Diagnostics.HasError() { + return + } + + // If the logic reaches here, it implicitly succeeded and will remove + // the resource from state if there are no other errors. +} + +// ImportState imports the resource into the Terraform state. +func (r *UserCustomWebhookResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + r.WebhookResource.ImportState(ctx, req, resp) +} diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook_user_test.go b/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook_user_test.go new file mode 100644 index 00000000..78982987 --- /dev/null +++ b/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook_user_test.go @@ -0,0 +1,133 @@ +package webhook_test + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/plancheck" + "github.com/jfrog/terraform-provider-artifactory/v12/pkg/acctest" + "github.com/jfrog/terraform-provider-shared/testutil" + "github.com/jfrog/terraform-provider-shared/util" +) + +func TestAccCustomWebhook_User_UpgradeFromSDKv2(t *testing.T) { + _, fqrn, name := testutil.MkNames("test-user-webhook", "artifactory_user_custom_webhook") + + params := map[string]interface{}{ + "webhookName": name, + } + config := util.ExecuteTemplate("TestAccCustomWebhookUser", ` + resource "artifactory_user_custom_webhook" "{{ .webhookName }}" { + key = "{{ .webhookName }}" + description = "test description" + event_types = ["locked"] + handler { + url = "https://google.com" + secrets = { + secret1 = "value1" + secret2 = "value2" + } + http_headers = { + header-1 = "value-1" + header-2 = "value-2" + } + payload = "{ \"ref\": \"main\" , \"inputs\": { \"artifact_path\": \"test-repo/repo-path\" } }" + } + } + `, params) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + CheckDestroy: acctest.VerifyDeleted(fqrn, "key", acctest.CheckRepo), + + Steps: []resource.TestStep{ + { + Config: config, + ExternalProviders: map[string]resource.ExternalProvider{ + "artifactory": { + Source: "jfrog/artifactory", + VersionConstraint: "12.1.0", + }, + }, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(fqrn, "event_types.#", "1"), + resource.TestCheckResourceAttr(fqrn, "handler.#", "1"), + resource.TestCheckResourceAttr(fqrn, "handler.0.url", "https://google.com"), + resource.TestCheckResourceAttr(fqrn, "handler.0.secrets.secret1", "value1"), + resource.TestCheckResourceAttr(fqrn, "handler.0.secrets.secret2", "value2"), + resource.TestCheckResourceAttr(fqrn, "handler.0.http_headers.%", "2"), + resource.TestCheckResourceAttr(fqrn, "handler.0.http_headers.header-1", "value-1"), + resource.TestCheckResourceAttr(fqrn, "handler.0.http_headers.header-2", "value-2"), + resource.TestCheckResourceAttr(fqrn, "handler.0.payload", "{ \"ref\": \"main\" , \"inputs\": { \"artifact_path\": \"test-repo/repo-path\" } }"), + ), + }, + { + Config: config, + ProtoV6ProviderFactories: acctest.ProtoV6ProviderFactories, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectEmptyPlan(), + }, + }, + }, + }, + }) +} + +func TestAccCustomWebhook_User(t *testing.T) { + _, fqrn, name := testutil.MkNames("test-user-webhook", "artifactory_user_custom_webhook") + + params := map[string]interface{}{ + "webhookName": name, + } + webhookConfig := util.ExecuteTemplate("TestAccCustomWebhookUser", ` + resource "artifactory_user_custom_webhook" "{{ .webhookName }}" { + key = "{{ .webhookName }}" + description = "test description" + event_types = ["locked"] + handler { + url = "https://google.com" + secrets = { + secret1 = "value1" + secret2 = "value2" + } + http_headers = { + header-1 = "value-1" + header-2 = "value-2" + } + payload = "{ \"ref\": \"main\" , \"inputs\": { \"artifact_path\": \"test-repo/repo-path\" } }" + } + } + `, params) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ProtoV6ProviderFactories: acctest.ProtoV6ProviderFactories, + CheckDestroy: acctest.VerifyDeleted(fqrn, "key", acctest.CheckRepo), + + Steps: []resource.TestStep{ + { + Config: webhookConfig, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(fqrn, "event_types.#", "1"), + resource.TestCheckResourceAttr(fqrn, "handler.#", "1"), + resource.TestCheckResourceAttr(fqrn, "handler.0.url", "https://google.com"), + resource.TestCheckResourceAttr(fqrn, "handler.0.secrets.secret1", "value1"), + resource.TestCheckResourceAttr(fqrn, "handler.0.secrets.secret2", "value2"), + resource.TestCheckResourceAttr(fqrn, "handler.0.http_headers.%", "2"), + resource.TestCheckResourceAttr(fqrn, "handler.0.http_headers.header-1", "value-1"), + resource.TestCheckResourceAttr(fqrn, "handler.0.http_headers.header-2", "value-2"), + resource.TestCheckResourceAttr(fqrn, "handler.0.payload", "{ \"ref\": \"main\" , \"inputs\": { \"artifact_path\": \"test-repo/repo-path\" } }"), + ), + }, + { + ResourceName: fqrn, + ImportState: true, + ImportStateId: name, + ImportStateVerify: true, + ImportStateVerifyIdentifierAttribute: "key", + ImportStateVerifyIgnore: []string{"handler.0.secrets"}, + }, + }, + }) +} diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_webhook_base.go b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_base.go deleted file mode 100644 index 678681aa..00000000 --- a/pkg/artifactory/resource/webhook/resource_artifactory_webhook_base.go +++ /dev/null @@ -1,183 +0,0 @@ -package webhook - -import ( - "fmt" - "strings" - - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" - "github.com/jfrog/terraform-provider-shared/validator" -) - -var baseCriteriaSchema = map[string]*schema.Schema{ - "include_patterns": { - Type: schema.TypeSet, - Optional: true, - Elem: &schema.Schema{Type: schema.TypeString}, - Description: `Simple comma separated wildcard patterns for repository artifact paths (with no leading slash).\nAnt-style path expressions are supported (*, **, ?).\nFor example: "org/apache/**"`, - }, - "exclude_patterns": { - Type: schema.TypeSet, - Optional: true, - Elem: &schema.Schema{Type: schema.TypeString}, - Description: `Simple comma separated wildcard patterns for repository artifact paths (with no leading slash).\nAnt-style path expressions are supported (*, **, ?).\nFor example: "org/apache/**"`, - }, -} - -func getBaseSchemaByVersion(webhookType string, version int, isCustom bool) map[string]*schema.Schema { - if isCustom { - return baseCustomWebhookBaseSchema(webhookType) - } - if version == 1 && !isCustom { - return baseWebhookBaseSchemaV1(webhookType) - } - return baseWebhookBaseSchemaV2(webhookType) -} - -func baseWebhookBaseSchemaV1(webhookType string) map[string]*schema.Schema { - return map[string]*schema.Schema{ - "key": { - Type: schema.TypeString, - Required: true, - ValidateDiagFunc: validation.ToDiagFunc( - validation.All( - validation.StringLenBetween(2, 200), - validation.StringDoesNotContainAny(" "), - ), - ), - Description: "Key of webhook. Must be between 2 and 200 characters. Cannot contain spaces.", - }, - "description": { - Type: schema.TypeString, - Optional: true, - ValidateDiagFunc: validation.ToDiagFunc(validation.StringLenBetween(0, 1000)), - Description: "Description of webhook. Max length 1000 characters.", - }, - "enabled": { - Type: schema.TypeBool, - Optional: true, - Default: true, - Description: "Status of webhook. Default to 'true'", - }, - "event_types": { - Type: schema.TypeSet, - Required: true, - MinItems: 1, - Elem: &schema.Schema{Type: schema.TypeString}, - Description: fmt.Sprintf("List of Events in Artifactory, Distribution, Release Bundle that function as the event trigger for the Webhook.\n"+ - "Allow values: %v", strings.Trim(strings.Join(DomainEventTypesSupported[webhookType], ", "), "[]")), - }, - "url": { - Type: schema.TypeString, - Required: true, - ValidateDiagFunc: validation.ToDiagFunc( - validation.All( - validation.IsURLWithHTTPorHTTPS, - validation.StringIsNotEmpty, - ), - ), - Description: "Specifies the URL that the Webhook invokes. This will be the URL that Artifactory will send an HTTP POST request to.", - }, - "secret": { - Type: schema.TypeString, - Optional: true, - ValidateDiagFunc: validation.ToDiagFunc(validation.StringIsNotEmpty), - Description: "Secret authentication token that will be sent to the configured URL.", - }, - "proxy": { - Type: schema.TypeString, - Optional: true, - ValidateDiagFunc: validation.ToDiagFunc(validation.StringIsNotEmpty), - Description: "Proxy key from Artifactory Proxies setting", - }, - "custom_http_headers": { - Type: schema.TypeMap, - Optional: true, - Elem: &schema.Schema{Type: schema.TypeString}, - Description: "Custom HTTP headers you wish to use to invoke the Webhook, comprise of key/value pair.", - }, - } -} - -func baseWebhookBaseSchemaV2(webhookType string) map[string]*schema.Schema { - return map[string]*schema.Schema{ - "key": { - Type: schema.TypeString, - Required: true, - ValidateDiagFunc: validation.ToDiagFunc( - validation.All( - validation.StringLenBetween(2, 200), - validation.StringDoesNotContainAny(" "), - ), - ), - Description: "Key of webhook. Must be between 2 and 200 characters. Cannot contain spaces.", - }, - "description": { - Type: schema.TypeString, - Optional: true, - ValidateDiagFunc: validation.ToDiagFunc(validation.StringLenBetween(0, 1000)), - Description: "Description of webhook. Max length 1000 characters.", - }, - "enabled": { - Type: schema.TypeBool, - Optional: true, - Default: true, - Description: "Status of webhook. Default to 'true'", - }, - "event_types": { - Type: schema.TypeSet, - Required: true, - MinItems: 1, - Elem: &schema.Schema{Type: schema.TypeString}, - Description: fmt.Sprintf("List of Events in Artifactory, Distribution, Release Bundle that function as the event trigger for the Webhook.\n"+ - "Allow values: %v", strings.Trim(strings.Join(DomainEventTypesSupported[webhookType], ", "), "[]")), - }, - "handler": { - Type: schema.TypeSet, - Required: true, - MinItems: 1, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "url": { - Type: schema.TypeString, - Required: true, - ValidateDiagFunc: validation.ToDiagFunc( - validation.All( - validation.IsURLWithHTTPorHTTPS, - validation.StringIsNotEmpty, - ), - ), - Description: "Specifies the URL that the Webhook invokes. This will be the URL that Artifactory will send an HTTP POST request to.", - }, - "secret": { - Type: schema.TypeString, - Optional: true, - ValidateDiagFunc: validation.ToDiagFunc(validation.StringIsNotEmpty), - Description: "Secret authentication token that will be sent to the configured URL. The value will be sent as `x-jfrog-event-auth` header.", - }, - "use_secret_for_signing": { - Type: schema.TypeBool, - Optional: true, - Default: false, - Description: "When set to `true`, the secret will be used to sign the event payload, allowing the target to validate that the payload content has not been changed and will not be passed as part of the event. If left unset or set to `false`, the secret is passed through the `X-JFrog-Event-Auth` HTTP header.", - }, - "proxy": { - Type: schema.TypeString, - Optional: true, - ValidateDiagFunc: validator.All( - validator.StringIsNotEmpty, - validator.StringIsNotURL, - ), - Description: "Proxy key from Artifactory UI (Administration -> Proxies -> Configuration)", - }, - "custom_http_headers": { - Type: schema.TypeMap, - Optional: true, - Elem: &schema.Schema{Type: schema.TypeString}, - Description: "Custom HTTP headers you wish to use to invoke the Webhook, comprise of key/value pair.", - }, - }, - }, - }, - } -} diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_webhook_user.go b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_user.go index 85a43492..f441d555 100644 --- a/pkg/artifactory/resource/webhook/resource_artifactory_webhook_user.go +++ b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_user.go @@ -10,7 +10,7 @@ import ( "github.com/jfrog/terraform-provider-shared/util" ) -var _ resource.Resource = &ReleaseBundleWebhookResource{} +var _ resource.Resource = &UserWebhookResource{} func NewUserWebhookResource() resource.Resource { return &UserWebhookResource{ From 325b26673953d9b9be0b16e09358697d652e0bf0 Mon Sep 17 00:00:00 2001 From: Alex Hung Date: Tue, 1 Oct 2024 15:40:20 -0700 Subject: [PATCH 10/12] Update CHANGELOG --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 95ec2b60..afae342b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,9 @@ -## 12.1.1 (October 1, 2024). Tested on Artifactory 7.90.13 with Terraform 1.9.6 and OpenTofu 1.8.2 +## 12.1.1 (October 2, 2024) IMPROVEMENTS: * resource/artifactory_\*\_webhook is migrated to Plugin Framework. PR: [#1087](https://github.com/jfrog/terraform-provider-artifactory/pull/1087) +* resource/artifactory_\*\_custom_webhook is migrated to Plugin Framework. PR: [#1089](https://github.com/jfrog/terraform-provider-artifactory/pull/1089) ## 12.1.0 (September 26, 2024). Tested on Artifactory 7.90.10 with Terraform 1.9.6 and OpenTofu 1.8.2 From 9bfb09ae793626cd2186bc73d5e04394a9aa766d Mon Sep 17 00:00:00 2001 From: Alex Hung Date: Tue, 1 Oct 2024 16:23:23 -0700 Subject: [PATCH 11/12] Add retry to release bundle v2 promotion --- .../resource_artifactory_release_bundle_v2_promotion.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/pkg/artifactory/resource/lifecycle/resource_artifactory_release_bundle_v2_promotion.go b/pkg/artifactory/resource/lifecycle/resource_artifactory_release_bundle_v2_promotion.go index f3ccf7ab..16affb63 100644 --- a/pkg/artifactory/resource/lifecycle/resource_artifactory_release_bundle_v2_promotion.go +++ b/pkg/artifactory/resource/lifecycle/resource_artifactory_release_bundle_v2_promotion.go @@ -4,7 +4,9 @@ import ( "context" "fmt" "net/http" + "regexp" + "github.com/go-resty/resty/v2" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/resource" @@ -161,6 +163,12 @@ func (r *ReleaseBundleV2PromotionResource) Configure(ctx context.Context, req re r.ProviderData = req.ProviderData.(util.ProviderMetadata) } +var retryOnNotAssignedToEnvironmentError = func(response *resty.Response, _r error) bool { + var notAssignedToEnvironmentRegex = regexp.MustCompile(".*not assigned to environment.*") + + return notAssignedToEnvironmentRegex.MatchString(string(response.Body()[:])) +} + func (r *ReleaseBundleV2PromotionResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { go util.SendUsageResourceCreate(ctx, r.ProviderData.Client.R(), r.ProviderData.ProductId, r.TypeName) @@ -195,6 +203,7 @@ func (r *ReleaseBundleV2PromotionResource) Create(ctx context.Context, req resou }). SetBody(promotion). SetResult(&result). + AddRetryCondition(retryOnNotAssignedToEnvironmentError). Post(ReleaseBundleV2PromotionEndpoint) if err != nil { utilfw.UnableToCreateResourceError(resp, err.Error()) From 4ae9965b4087fc34b8b0fa247fb53f5cecdfc58b Mon Sep 17 00:00:00 2001 From: JFrog CI Date: Wed, 2 Oct 2024 00:01:10 +0000 Subject: [PATCH 12/12] JFrog Pipelines - Add Artifactory version to CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index afae342b..1656a2d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -## 12.1.1 (October 2, 2024) +## 12.1.1 (October 2, 2024). Tested on Artifactory 7.90.13 with Terraform 1.9.6 and OpenTofu 1.8.2 IMPROVEMENTS: