diff --git a/CHANGELOG.md b/CHANGELOG.md index 95ec2b608..1656a2d2a 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). Tested on Artifactory 7.90.13 with Terraform 1.9.6 and OpenTofu 1.8.2 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 diff --git a/pkg/artifactory/provider/framework.go b/pkg/artifactory/provider/framework.go index 692d3303b..f0cb52a55 100644 --- a/pkg/artifactory/provider/framework.go +++ b/pkg/artifactory/provider/framework.go @@ -223,17 +223,29 @@ func (p *ArtifactoryProvider) Resources(ctx context.Context) []func() resource.R replication.NewLocalRepositoryMultiReplicationResource, replication.NewRemoteRepositoryReplicationResource, webhook.NewArtifactWebhookResource, + webhook.NewArtifactCustomWebhookResource, webhook.NewArtifactLifecycleWebhookResource, + webhook.NewArtifactLifecycleCustomWebhookResource, 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.NewReleaseBundleV2CustomWebhookResource, webhook.NewReleaseBundleV2PromotionWebhookResource, + webhook.NewReleaseBundleV2PromotionCustomWebhookResource, webhook.NewUserWebhookResource, + webhook.NewUserCustomWebhookResource, } } diff --git a/pkg/artifactory/provider/resources.go b/pkg/artifactory/provider/resources.go index 0ce00edcf..07ce411de 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/lifecycle/resource_artifactory_release_bundle_v2_promotion.go b/pkg/artifactory/resource/lifecycle/resource_artifactory_release_bundle_v2_promotion.go index f3ccf7abc..16affb630 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()) diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook.go b/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook.go index bd995e860..00733b9ce 100644 --- a/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook.go +++ b/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook.go @@ -2,879 +2,294 @@ package webhook import ( "context" - "fmt" - "net/http" "regexp" - "strings" - - "github.com/go-resty/resty/v2" - "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" - "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" - - "golang.org/x/exp/slices" + + "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-framework/types/basetypes" + validatorfw_string "github.com/jfrog/terraform-provider-shared/validator/fw/string" + "github.com/samber/lo" ) -var DomainSupported = []string{ - ArtifactLifecycleDomain, - ArtifactPropertyDomain, - ArtifactDomain, - ArtifactoryReleaseBundleDomain, - BuildDomain, - DestinationDomain, - DistributionDomain, - DockerDomain, - ReleaseBundleDomain, - ReleaseBundleV2Domain, - ReleaseBundleV2PromotionDomain, - UserDomain, +type CustomWebhookResource struct { + WebhookResource } -func baseCustomWebhookBaseSchema(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.", - }, - "secrets": { - Type: schema.TypeMap, - Optional: true, - Elem: &schema.Schema{ - Type: 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, - Optional: true, - ValidateDiagFunc: validator.All( - validator.StringIsNotEmpty, - validator.StringIsNotURL, - ), - Description: "Proxy key from Artifactory UI (Administration -> Proxies -> Configuration)", - }, - "http_headers": { - Type: schema.TypeMap, - Optional: true, - Elem: &schema.Schema{Type: 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, - Optional: true, - ValidateDiagFunc: validation.ToDiagFunc(validation.StringIsNotEmpty), - Description: "This attribute is used to build the request body. Used in custom webhooks", - }, +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.", }, - }, - } -} - -var repoWebhookSchema = func(webhookType string, version int, isCustom bool) map[string]*schema.Schema { - return utilsdk.MergeMaps(getBaseSchemaByVersion(webhookType, version, isCustom), map[string]*schema.Schema{ - "criteria": { - Type: schema.TypeSet, - Required: true, - MaxItems: 1, - Elem: &schema.Resource{ - Schema: utilsdk.MergeMaps(baseCriteriaSchema, map[string]*schema.Schema{ - "any_local": { - Type: schema.TypeBool, - Required: true, - Description: "Trigger on any local repositories", - }, - "any_remote": { - Type: schema.TypeBool, - Required: true, - Description: "Trigger on any remote repositories", - }, - "any_federated": { - Type: schema.TypeBool, - Required: true, - Description: "Trigger on any federated repositories", - }, - "repo_keys": { - Type: schema.TypeSet, - Required: true, - Elem: &schema.Schema{Type: 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]*schema.Schema { - return utilsdk.MergeMaps(getBaseSchemaByVersion(webhookType, version, isCustom), map[string]*schema.Schema{ - "criteria": { - Type: schema.TypeSet, - Required: true, - MaxItems: 1, - Elem: &schema.Resource{ - Schema: utilsdk.MergeMaps(baseCriteriaSchema, map[string]*schema.Schema{ - "any_build": { - Type: schema.TypeBool, - Required: true, - Description: "Trigger on any builds", - }, - "selected_builds": { - Type: schema.TypeSet, - Required: true, - Elem: &schema.Schema{Type: schema.TypeString}, - Description: "Trigger on this list of build IDs", - }, - }), + "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.", }, - Description: "Specifies where the webhook will be applied on which builds.", - }, - }) -} - -var releaseBundleWebhookSchema = func(webhookType string, version int, isCustom bool) map[string]*schema.Schema { - return utilsdk.MergeMaps(getBaseSchemaByVersion(webhookType, version, isCustom), map[string]*schema.Schema{ - "criteria": { - Type: schema.TypeSet, - Required: true, - MaxItems: 1, - Elem: &schema.Resource{ - Schema: utilsdk.MergeMaps(baseCriteriaSchema, map[string]*schema.Schema{ - "any_release_bundle": { - Type: schema.TypeBool, - Required: true, - Description: "Trigger on any release bundles or distributions", - }, - "registered_release_bundle_names": { - Type: schema.TypeSet, - Required: true, - Elem: &schema.Schema{Type: schema.TypeString}, - Description: "Trigger on this list of release bundle names", - }, - }), + "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", }, - 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]*schema.Schema { - return utilsdk.MergeMaps(getBaseSchemaByVersion(webhookType, version, isCustom), map[string]*schema.Schema{ - "criteria": { - Type: schema.TypeSet, - Required: true, - MaxItems: 1, - Elem: &schema.Resource{ - Schema: utilsdk.MergeMaps(baseCriteriaSchema, map[string]*schema.Schema{ - "any_release_bundle": { - Type: schema.TypeBool, - Required: true, - Description: "Trigger on any release bundles or distributions", - }, - "selected_release_bundles": { - Type: schema.TypeSet, - Required: true, - Elem: &schema.Schema{Type: schema.TypeString}, - Description: "Trigger on this list of release bundle names", - }, - }), + "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.", }, - 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]*schema.Schema { - return utilsdk.MergeMaps(getBaseSchemaByVersion(webhookType, version, isCustom), map[string]*schema.Schema{ - "criteria": { - Type: schema.TypeSet, - Required: true, - MaxItems: 1, - Elem: &schema.Resource{ - Schema: utilsdk.MergeMaps(baseCriteriaSchema, map[string]*schema.Schema{ - "selected_environments": { - Type: schema.TypeSet, - Required: true, - Elem: &schema.Schema{Type: schema.TypeString}, - Description: "Trigger on this list of environments", - }, - }), + "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", }, - Description: "Specifies where the webhook will be applied, on which release bundles promotion.", }, - }) + }, + Validators: []validator.Set{ + setvalidator.IsRequired(), + setvalidator.SizeAtLeast(1), + }, } -var userWebhookSchema = func(webhookType string, version int, isCustom bool) map[string]*schema.Schema { - return getBaseSchemaByVersion(webhookType, version, isCustom) +func (r *CustomWebhookResource) CreateSchema(domain string, criteriaBlock *schema.SetNestedBlock) schema.Schema { + return r.WebhookResource.CreateSchema(domain, criteriaBlock, customHandlerBlock) } -var artifactLifecycleWebhookSchema = func(webhookType string, version int, isCustom bool) map[string]*schema.Schema { - return getBaseSchemaByVersion(webhookType, version, isCustom) +func (r *CustomWebhookResource) Create(ctx context.Context, webhook CustomWebhookAPIModel, resp *resource.CreateResponse) { + createWebhook(r.ProviderData.Client, webhook, resp) } -type CustomBaseParams struct { - Key string `json:"key"` - Description string `json:"description"` - Enabled bool `json:"enabled"` - EventFilterAPIModel EventFilterAPIModel `json:"event_filter"` - Handlers []CustomHandler `json:"handlers"` +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 (w CustomBaseParams) Id() string { - return w.Key +func (r *CustomWebhookResource) Update(_ context.Context, key string, webhook CustomWebhookAPIModel, resp *resource.UpdateResponse) { + updateWebhook(r.ProviderData.Client, key, webhook, resp) } -type CustomHandler struct { - HandlerType string `json:"handler_type"` - Url string `json:"url"` - Secrets []KeyValuePairAPIModel `json:"secrets"` - Proxy string `json:"proxy"` - HttpHeaders []KeyValuePairAPIModel `json:"http_headers"` - Payload string `json:"payload,omitempty"` +type CustomWebhookBaseResourceModel struct { + WebhookBaseResourceModel } -type SecretName struct { - Name string `json:"name"` -} - -var packSecretsCustom = func(keyValuePairs []KeyValuePairAPIModel, d *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() - 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) - } +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() { + diags.Append(d...) } - return KVPairs -} - -func ResourceArtifactoryCustomWebhook(webhookType string) *schema.Resource { - - var unpackWebhook = func(data *schema.ResourceData) (CustomBaseParams, error) { - d := &utilsdk.ResourceData{ResourceData: data} - - var unpackHandlers = func(d *utilsdk.ResourceData) []CustomHandler { - var webhookHandlers []CustomHandler - - if v, ok := d.GetOk("handler"); ok { - handlers := v.(*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{ - 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 { - webhookHandler.Proxy = v.(string) - } - - if v, ok := h["http_headers"]; ok { - webhookHandler.HttpHeaders = unpackKeyValuePair(v.(map[string]interface{})) - } - - if v, ok := h["payload"]; ok { - webhookHandler.Payload = v.(string) - } + handlers := lo.Map( + m.Handlers.Elements(), + func(elem attr.Value, _ int) CustomHandlerAPIModel { + attrs := elem.(types.Object).Attributes() - webhookHandlers = append(webhookHandlers, webhookHandler) + 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(), } - - 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), + }, + ) + + *apiModel = CustomWebhookAPIModel{ + WebhookAPIModel: WebhookAPIModel{ + Key: m.Key.ValueString(), + Description: m.Description.ValueString(), + Enabled: m.Enabled.ValueBool(), + EventFilter: EventFilterAPIModel{ + Domain: domain, + EventTypes: eventTypes, }, - Handlers: unpackHandlers(d), - } - - return webhook, nil - } - - var packHandlers = func(d *schema.ResourceData, handlers []CustomHandler) []error { - setValue := utilsdk.MkLens(d) - resource := domainSchemaLookup(currentSchemaVersion, true, webhookType)[webhookType]["handler"].Elem.(*schema.Resource) - packedHandlers := make([]interface{}, len(handlers)) - for _, handler := range handlers { - packedHandler := map[string]interface{}{ - "url": handler.Url, - "proxy": handler.Proxy, - "payload": handler.Payload, - } - - 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", schema.NewSet(schema.HashResource(resource), packedHandlers)) - } - - var packWebhook = func(d *schema.ResourceData, webhook CustomBaseParams) 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 = append(errors, packHandlers(d, webhook.Handlers)...) - - if len(errors) > 0 { - return diag.Errorf("failed to pack webhook %q", errors) - } - - return nil - } - - var readWebhook = func(ctx context.Context, data *schema.ResourceData, m interface{}) diag.Diagnostics { - webhook := CustomBaseParams{} - - webhook.EventFilterAPIModel.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 diag.FromErr(err) - } - - if resp.StatusCode() == http.StatusNotFound { - data.SetId("") - return nil - } - - if resp.IsError() { - return 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 *schema.ResourceData, m interface{}) diag.Diagnostics { - webhook, err := unpackWebhook(data) - if err != nil { - return 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 diag.FromErr(err) - } - - if resp.IsError() { - return diag.Errorf("%s", artifactoryError.String()) - } - - data.SetId(webhook.Id()) - - return readWebhook(ctx, data, m) - } - - var updateWebhook = func(ctx context.Context, data *schema.ResourceData, m interface{}) diag.Diagnostics { - webhook, err := unpackWebhook(data) - if err != nil { - return 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 diag.FromErr(err) - } - - if resp.IsError() { - return diag.Errorf("%s", artifactoryError.String()) - } - - data.SetId(webhook.Id()) - - return readWebhook(ctx, data, m) - } - - var deleteWebhook = func(ctx context.Context, data *schema.ResourceData, m interface{}) 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 diag.FromErr(err) - } - - if resp.StatusCode() == http.StatusNotFound { - data.SetId("") - return nil - } - - if resp.IsError() { - return diag.Errorf("%s", artifactoryError.String()) - } - - return nil - } - - var eventTypesDiff = func(ctx context.Context, diff *schema.ResourceDiff, v interface{}) error { - eventTypes := diff.Get("event_types").(*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 *schema.ResourceDiff, v interface{}) error { - if resource, ok := diff.GetOk("criteria"); ok { - criteria := resource.(*schema.Set).List() - if len(criteria) == 0 { - return nil - } - return domainCriteriaValidationLookup[webhookType](ctx, criteria[0].(map[string]interface{})) - } - - return nil - } - - rs := schema.Resource{ - SchemaVersion: 2, - CreateContext: createWebhook, - ReadContext: readWebhook, - UpdateContext: updateWebhook, - DeleteContext: deleteWebhook, - - Importer: &schema.ResourceImporter{ - StateContext: schema.ImportStatePassthroughContext, }, - - Schema: domainSchemaLookup(currentSchemaVersion, true, webhookType)[webhookType], - - CustomizeDiff: customdiff.All( - eventTypesDiff, - criteriaDiff, - ), - Description: "Provides an Artifactory webhook resource", + Handlers: handlers, } - if webhookType == ReleaseBundleDomain { - rs.DeprecationMessage = "This resource is being deprecated and replaced by artifactory_destination_custom_webhook resource" - } - - return &rs + return } -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 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 packKeyValuePair = func(keyValuePairs []KeyValuePairAPIModel) map[string]interface{} { - kvPairs := make(map[string]interface{}) - for _, keyValuePair := range keyValuePairs { - kvPairs[keyValuePair.Name] = keyValuePair.Value - } - - return kvPairs +var customHandlerSetResourceModelElementTypes = types.ObjectType{ + AttrTypes: customHandlerSetResourceModelAttributeTypes, } -type EmptyWebhookCriteria struct{} - -var domainCriteriaLookup = map[string]interface{}{ - "artifact": RepoCriteriaAPIModel{}, - "artifact_property": RepoCriteriaAPIModel{}, - "docker": RepoCriteriaAPIModel{}, - "build": BuildCriteriaAPIModel{}, - "release_bundle": ReleaseBundleCriteriaAPIModel{}, - "distribution": ReleaseBundleCriteriaAPIModel{}, - "artifactory_release_bundle": ReleaseBundleCriteriaAPIModel{}, - "destination": ReleaseBundleCriteriaAPIModel{}, - "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{}{ - "artifact": packRepoCriteria, - "artifact_property": packRepoCriteria, - "docker": packRepoCriteria, - "build": packBuildCriteria, - "release_bundle": packReleaseBundleCriteria, - "distribution": packReleaseBundleCriteria, - "artifactory_release_bundle": packReleaseBundleCriteria, - "destination": packReleaseBundleCriteria, - "user": packEmptyCriteria, - "release_bundle_v2": packReleaseBundleV2Criteria, - "release_bundle_v2_promotion": packReleaseBundleV2PromotionCriteria, - "artifact_lifecycle": packEmptyCriteria, -} - -var domainUnpackLookup = map[string]func(map[string]interface{}, BaseCriteriaAPIModel) interface{}{ - "artifact": unpackRepoCriteria, - "artifact_property": unpackRepoCriteria, - "docker": unpackRepoCriteria, - "build": unpackBuildCriteria, - "release_bundle": unpackReleaseBundleCriteria, - "distribution": unpackReleaseBundleCriteria, - "artifactory_release_bundle": unpackReleaseBundleCriteria, - "destination": unpackReleaseBundleCriteria, - "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]*schema.Schema { - return map[string]map[string]*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), - "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), - "artifact_lifecycle": artifactLifecycleWebhookSchema(webhookType, version, isCustom), - } -} - -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": schema.NewSet(schema.HashString, artifactoryCriteria["repoKeys"].([]interface{})), - } - - if v, ok := artifactoryCriteria["anyFederated"]; ok { - criteria["any_federated"] = v.(bool) - } +func (m *CustomWebhookBaseResourceModel) fromAPIModel(ctx context.Context, apiModel CustomWebhookAPIModel, stateHandlers basetypes.SetValue) diag.Diagnostics { + diags := diag.Diagnostics{} - return criteria -} - -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{})), - } -} + m.Key = types.StringValue(apiModel.Key) -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()), - BaseCriteriaAPIModel: baseCriteria, + description := types.StringNull() + if apiModel.Description != "" { + description = types.StringValue(apiModel.Description) } -} + m.Description = description -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"].(*schema.Set).List()), - BaseCriteriaAPIModel: baseCriteria, - } -} + m.Enabled = types.BoolValue(apiModel.Enabled) -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{})), + eventTypes, d := types.SetValueFrom(ctx, types.StringType, apiModel.EventFilter.EventTypes) + if d.HasError() { + diags.Append(d...) } -} + m.EventTypes = eventTypes -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()), - BaseCriteriaAPIModel: baseCriteria, - } -} + handlers := lo.Map( + apiModel.Handlers, + func(handler CustomHandlerAPIModel, _ int) attr.Value { + secrets := types.MapNull(types.StringType) -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{})), - } -} - -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()), - 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{})), - } -} - -var unpackReleaseBundleV2PromotionCriteria = func(terraformCriteria map[string]interface{}, baseCriteria BaseCriteriaAPIModel) interface{} { - return ReleaseBundleV2PromotionCriteriaAPIModel{ - SelectedEnvironments: utilsdk.CastToStringArr(terraformCriteria["selected_environments"].(*schema.Set).List()), - } -} - -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.(*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()), + 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 + } } - webhookCriteria = domainUnpackLookup[webhookType](id, baseCriteria) - } - } - - return webhookCriteria -} + 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...) + } -var packCriteria = func(d *schema.ResourceData, webhookType string, criteria map[string]interface{}) []error { - setValue := utilsdk.MkLens(d) + httpHeaders = h + } - resource := domainSchemaLookup(currentSchemaVersion, false, webhookType)[webhookType]["criteria"].Elem.(*schema.Resource) - packedCriteria := domainPackLookup[webhookType](criteria) + 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...) + } - includePatterns := []interface{}{} - if v, ok := criteria["includePatterns"]; ok && v != nil { - includePatterns = v.([]interface{}) - } - packedCriteria["include_patterns"] = schema.NewSet(schema.HashString, includePatterns) + return h + }, + ) - excludePatterns := []interface{}{} - if v, ok := criteria["excludePatterns"]; ok && v != nil { - excludePatterns = v.([]interface{}) + handlersSet, d := types.SetValue( + customHandlerSetResourceModelElementTypes, + handlers, + ) + if d.HasError() { + diags.Append(d...) } - packedCriteria["exclude_patterns"] = schema.NewSet(schema.HashString, excludePatterns) - - return setValue("criteria", schema.NewSet(schema.HashResource(resource), []interface{}{packedCriteria})) -} + m.Handlers = handlersSet -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, - "artifactory_release_bundle": releaseBundleCriteriaValidation, - "destination": releaseBundleCriteriaValidation, - "user": emptyCriteriaValidation, - "release_bundle_v2": releaseBundleV2CriteriaValidation, - "release_bundle_v2_promotion": emptyCriteriaValidation, - "artifact_lifecycle": emptyCriteriaValidation, + return diags } -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"].(*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 +type CustomWebhookResourceModel struct { + CustomWebhookBaseResourceModel + WebhookCriteriaResourceModel } -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() +func (m CustomWebhookResourceModel) toAPIModel(ctx context.Context, domain string, criteriaAPIModel interface{}, apiModel *CustomWebhookAPIModel) (diags diag.Diagnostics) { + d := m.CustomWebhookBaseResourceModel.toAPIModel(ctx, domain, apiModel) - if !anyBuild && (len(selectedBuilds) == 0 && len(includePatterns) == 0) { - return fmt.Errorf("selected_builds or include_patterns cannot be empty when any_build is false") - } + apiModel.EventFilter.Criteria = criteriaAPIModel - return nil + return d } -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() - - if !anyReleaseBundle && len(registeredReleaseBundlesNames) == 0 { - return fmt.Errorf("registered_release_bundle_names cannot be empty when any_release_bundle is false") +func (m *CustomWebhookResourceModel) fromAPIModel(ctx context.Context, apiModel CustomWebhookAPIModel, stateHandlers basetypes.SetValue, criteriaSet *basetypes.SetValue) diag.Diagnostics { + if criteriaSet != nil { + m.Criteria = *criteriaSet } - return nil + return m.CustomWebhookBaseResourceModel.fromAPIModel(ctx, apiModel, stateHandlers) } -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() - - if !anyReleaseBundle && len(selectedReleaseBundles) == 0 { - return fmt.Errorf("selected_release_bundles cannot be empty when any_release_bundle is false") - } - - return nil +type CustomWebhookAPIModel struct { + WebhookAPIModel + Handlers []CustomHandlerAPIModel `json:"handlers"` } -var emptyCriteriaValidation = func(ctx context.Context, criteria map[string]interface{}) error { - return nil +func (w CustomWebhookAPIModel) Id() string { + return w.Key } -var packSecret = func(d *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() - 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 +type CustomHandlerAPIModel struct { + HandlerType string `json:"handler_type"` + Url string `json:"url"` + Secrets []KeyValuePairAPIModel `json:"secrets"` + Proxy *string `json:"proxy"` + HttpHeaders []KeyValuePairAPIModel `json:"http_headers"` + Payload string `json:"payload,omitempty"` } - 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 000000000..c789e83a2 --- /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 000000000..5c20ccd14 --- /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 new file mode 100644 index 000000000..64c4e719c --- /dev/null +++ b/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook_build.go @@ -0,0 +1,203 @@ +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 = &BuildCustomWebhookResource{} + +func NewBuildCustomWebhookResource() 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 + } + + buildValidateConfig(data.Criteria, resp) +} + +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) { + 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 := toBuildCriteriaAPIModel(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 *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_build_test.go b/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook_build_test.go new file mode 100644 index 000000000..f403f4553 --- /dev/null +++ b/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook_build_test.go @@ -0,0 +1,118 @@ +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-shared/testutil" + "github.com/jfrog/terraform-provider-shared/util" +) + +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) + + 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) }, + ProtoV6ProviderFactories: acctest.ProtoV6ProviderFactories, + 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"), + ), + }, + { + 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_release_bundle.go b/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook_release_bundle.go new file mode 100644 index 000000000..00c006dd4 --- /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_release_bundle_v2.go b/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook_release_bundle_v2.go new file mode 100644 index 000000000..992c8f6c4 --- /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_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 000000000..b48935569 --- /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 000000000..8d757f9ce --- /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_repo.go b/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook_repo.go new file mode 100644 index 000000000..b6720bf46 --- /dev/null +++ b/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook_repo.go @@ -0,0 +1,238 @@ +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 = &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) { + resp.Schema = r.CreateSchema(r.Domain, &repoCriteriaBlock) +} + +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, 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, 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, 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(), 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) { + 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 := toRepoCriteriaAPIModel(ctx, baseCriteria, criteriaAttrs) + + 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) + + criteriaSet, d := fromRepoCriteriaAPIMode(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_test.go b/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook_repo_test.go new file mode 100644 index 000000000..ef69384ad --- /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 83d3f4a0f..3f427710c 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" @@ -9,372 +10,49 @@ 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_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"} { +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(customWebhookTestCase(webhookType, t)) + resource.Test(customWebhookCriteriaValidationTestCase(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\" } }" - } - handler { - url = "https://google.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\" } }" - } - handler { - url = "https://google.com" - payload = "{ \"ref\": \"main\" , \"inputs\": { \"artifact_path\": \"test-repo/repo-path\" } }" - } - - 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\" } }"), - resource.TestCheckResourceAttr(fqrn, "handler.1.url", "https://google.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.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\" } }"), - } - - for _, eventType := range eventTypes { - eventTypeCheck := resource.TestCheckTypeSetElemAttr(fqrn, "event_types.*", eventType) - testChecks = append(testChecks, eventTypeCheck) - } - - return t, resource.TestCase{ - PreCheck: func() { acctest.PreCheck(t) }, - ProviderFactories: acctest.ProviderFactories, - CheckDestroy: acctest.VerifyDeleted(fqrn, "", testCheckWebhook), - - Steps: []resource.TestStep{ - { - Config: webhookConfig, - Check: resource.ComposeTestCheckFunc(testChecks...), - }, - { - ResourceName: fqrn, - ImportState: true, - ImportStateVerify: true, - ImportStateCheck: validator.CheckImportState(name, "key"), - ImportStateVerifyIgnore: []string{"handler.0.secrets", "handler.1.secrets"}, - }, - }, - } -} - -func TestAccCustomWebhook_BuildWithIncludePatterns(t *testing.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_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") + fqrn := fmt.Sprintf("artifactory_%s_custom_webhook.%s", webhookType, name) - params := map[string]interface{}{ - "webhookName": 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 } - 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"}, - }, - }, - }) -} - -func TestAccCustomWebhook_ArtifactLifecycle(t *testing.T) { - _, fqrn, name := testutil.MkNames("test-artifact-lifecycle-webhook", "artifactory_artifact_lifecycle_custom_webhook") params := map[string]interface{}{ + "webhookType": webhookType, "webhookName": name, + "eventTypes": webhook.DomainEventTypesSupported[webhookType], } - 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), + 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, - 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"}, + Config: webhookConfig, + ExpectError: regexp.MustCompile(domainValidationErrorMessageLookup[webhookType]), }, }, - }) -} - -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_custom_webhook_user.go b/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook_user.go new file mode 100644 index 000000000..453113cc2 --- /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 000000000..78982987e --- /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.go b/pkg/artifactory/resource/webhook/resource_artifactory_webhook.go index 36caea9a1..0670ae39d 100644 --- a/pkg/artifactory/resource/webhook/resource_artifactory_webhook.go +++ b/pkg/artifactory/resource/webhook/resource_artifactory_webhook.go @@ -86,66 +86,63 @@ 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 { - blocks = lo.Assign( - blocks, - map[string]schema.Block{ - "criteria": *criteriaBlock, - }, - ) + blocks["criteria"] = *criteriaBlock } return schema.Schema{ @@ -205,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, resp *resource.CreateResponse) { var artifactoryError artifactory.ArtifactoryErrorsResponse - response, err := r.ProviderData.Client.R(). + response, err := client.R(). SetBody(webhook). SetError(&artifactoryError). AddRetryCondition(retryOnProxyError). @@ -224,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, resp *resource.CreateResponse) { + createWebhook(r.ProviderData.Client, webhook, resp) +} + +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 := r.ProviderData.Client.R(). + response, err := client.R(). SetPathParam("webhookKey", key). SetResult(&webhook). SetError(&artifactoryError). @@ -249,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, 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, resp *resource.UpdateResponse) { var artifactoryError artifactory.ArtifactoryErrorsResponse - response, err := r.ProviderData.Client.R(). + response, err := client.R(). SetPathParam("webhookKey", key). SetBody(webhook). AddRetryCondition(retryOnProxyError). @@ -269,7 +274,11 @@ func (r *WebhookResource) Update(ctx context.Context, key string, webhook Webhoo } } -func (r *WebhookResource) Delete(ctx context.Context, key string, req resource.DeleteRequest, resp *resource.DeleteResponse) { +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, resp *resource.DeleteResponse) { var artifactoryError artifactory.ArtifactoryErrorsResponse response, err := r.ProviderData.Client.R(). SetPathParam("webhookKey", key). @@ -297,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"` @@ -305,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() { @@ -357,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, @@ -403,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) @@ -528,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 && len(v.([]interface{})) > 0 { + 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 && len(v.([]interface{})) > 0 { + 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 { @@ -544,10 +557,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"` 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 92% rename from pkg/artifactory/resource/webhook/resource_artifactory_artifact_lifecycle.go rename to pkg/artifactory/resource/webhook/resource_artifactory_webhook_artifact_lifecycle.go index 123400374..957c3be2f 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{ @@ -23,7 +23,7 @@ func NewArtifactLifecycleWebhookResource() resource.Resource { } type ArtifactLifecycleWebhookResourceModel struct { - WebhookNoCriteriaResourceModel + WebhookBaseResourceModel } type ArtifactLifecycleWebhookResource struct { @@ -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) { @@ -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_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_base.go b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_base.go deleted file mode 100644 index 678681aa4..000000000 --- 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_build.go b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_build.go index 322aff332..7cd196345 100644 --- a/pkg/artifactory/resource/webhook/resource_artifactory_webhook_build.go +++ b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_build.go @@ -41,47 +41,40 @@ 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.schema(r.Domain, &criteriaBlock) +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) { 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) @@ -112,7 +116,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 +137,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 +172,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 +189,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,25 +203,34 @@ func (r *BuildWebhookResource) ImportState(ctx context.Context, req resource.Imp r.WebhookResource.ImportState(ctx, req, resp) } -func (m BuildWebhookResourceModel) toAPIModel(ctx context.Context, domain string, apiModel *WebhookAPIModel) (diags diag.Diagnostics) { - critieriaObj := m.Criteria.Elements()[0].(types.Object) - critieriaAttrs := critieriaObj.Attributes() - - baseCriteria, d := m.WebhookResourceModel.toBaseCriteriaAPIModel(ctx, critieriaAttrs) +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() { diags.Append(d...) } - var selectedBuilds []string - d = critieriaAttrs["selected_builds"].(types.Set).ElementsAs(ctx, &selectedBuilds, false) + 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) { + criteriaObj := m.Criteria.Elements()[0].(types.Object) + criteriaAttrs := criteriaObj.Attributes() + + baseCriteria, d := m.WebhookResourceModel.toBaseCriteriaAPIModel(ctx, criteriaAttrs) if d.HasError() { diags.Append(d...) } - criteriaAPIModel := BuildCriteriaAPIModel{ - BaseCriteriaAPIModel: baseCriteria, - AnyBuild: critieriaAttrs["any_build"].(types.Bool).ValueBool(), - SelectedBuilds: selectedBuilds, + criteriaAPIModel, d := toBuildCriteriaAPIModel(ctx, baseCriteria, criteriaAttrs) + if d.HasError() { + diags.Append(d...) } d = m.WebhookResourceModel.toAPIModel(ctx, domain, criteriaAPIModel, apiModel) @@ -240,13 +253,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) - +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) @@ -270,10 +277,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_build_test.go b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_build_test.go new file mode 100644 index 000000000..3b1d0e505 --- /dev/null +++ b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_build_test.go @@ -0,0 +1,122 @@ +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-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_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) + + 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.ProtoV6ProviderFactories, + 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_release_bundle.go b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_release_bundle.go index df4d6c38d..989db7ac4 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.schema(r.Domain, &criteriaBlock) +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) @@ -146,7 +150,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 +171,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 +206,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 +223,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 } @@ -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 d848055f2..7859b6874 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.schema(r.Domain, &criteriaBlock) +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) @@ -112,7 +116,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 +137,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 +172,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 +189,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 } @@ -199,25 +203,32 @@ func (r *ReleaseBundleV2WebhookResource) ImportState(ctx context.Context, req re r.WebhookResource.ImportState(ctx, req, resp) } -func (m ReleaseBundleV2WebhookResourceModel) toAPIModel(ctx context.Context, domain string, apiModel *WebhookAPIModel) (diags diag.Diagnostics) { - critieriaObj := m.Criteria.Elements()[0].(types.Object) - critieriaAttrs := critieriaObj.Attributes() - - baseCriteria, d := m.WebhookResourceModel.toBaseCriteriaAPIModel(ctx, critieriaAttrs) +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...) } - var releaseBundleNames []string - d = critieriaAttrs["selected_release_bundles"].(types.Set).ElementsAs(ctx, &releaseBundleNames, false) + 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() + + baseCriteria, d := m.WebhookResourceModel.toBaseCriteriaAPIModel(ctx, criteriaAttrs) if d.HasError() { diags.Append(d...) } - criteriaAPIModel := ReleaseBundleV2CriteriaAPIModel{ - BaseCriteriaAPIModel: baseCriteria, - AnyReleaseBundle: critieriaAttrs["any_release_bundle"].(types.Bool).ValueBool(), - SelectedReleaseBundles: releaseBundleNames, + criteriaAPIModel, d := toReleaseBundleV2APIModel(ctx, baseCriteria, criteriaAttrs) + if d.HasError() { + diags.Append(d...) } d = m.WebhookResourceModel.toAPIModel(ctx, domain, criteriaAPIModel, apiModel) @@ -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...) } 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 25ea5dbf9..63ace990b 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.", +} + +func (r *ReleaseBundleV2PromotionWebhookResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { - resp.Schema = r.schema(r.Domain, &criteriaBlock) + resp.Schema = r.CreateSchema(r.Domain, &releaseBundleV2PromotionCriteriaBlock, handlerBlock) } func (r *ReleaseBundleV2PromotionWebhookResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { @@ -85,7 +86,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 +107,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 +142,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 +159,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 } @@ -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) { - critieriaObj := m.Criteria.Elements()[0].(types.Object) - critieriaAttrs := critieriaObj.Attributes() - +func toReleaseBundleV2PromotionAPIModel(ctx context.Context, criteriaAttrs map[string]attr.Value) (criteriaAPIModel ReleaseBundleV2PromotionCriteriaAPIModel, diags diag.Diagnostics) { 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...) } - 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...) diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_webhook_repo.go b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_repo.go index fc607bb47..4596e9a2c 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.schema(r.Domain, &criteriaBlock) +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,29 +229,35 @@ func (r *RepoWebhookResource) ImportState(ctx context.Context, req resource.Impo r.WebhookResource.ImportState(ctx, req, resp) } -func (m RepoWebhookResourceModel) toAPIModel(ctx context.Context, domain string, apiModel *WebhookAPIModel) (diags diag.Diagnostics) { - critieriaObj := m.Criteria.Elements()[0].(types.Object) - critieriaAttrs := critieriaObj.Attributes() - - baseCriteria, d := m.WebhookResourceModel.toBaseCriteriaAPIModel(ctx, critieriaAttrs) - if d.HasError() { - diags.Append(d...) - } - +func toRepoCriteriaAPIModel(ctx context.Context, baseCriteria BaseCriteriaAPIModel, criteriaAttrs map[string]attr.Value) (criteriaAPIModel RepoCriteriaAPIModel, diags diag.Diagnostics) { var repoKeys []string - d = critieriaAttrs["repo_keys"].(types.Set).ElementsAs(ctx, &repoKeys, false) + d := criteriaAttrs["repo_keys"].(types.Set).ElementsAs(ctx, &repoKeys, false) if d.HasError() { diags.Append(d...) } - criteriaAPIModel := RepoCriteriaAPIModel{ + 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(), + 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) { + criteriaObj := m.Criteria.Elements()[0].(types.Object) + criteriaAttrs := criteriaObj.Attributes() + + baseCriteria, d := m.WebhookResourceModel.toBaseCriteriaAPIModel(ctx, criteriaAttrs) + if d.HasError() { + diags.Append(d...) + } + + criteriaAPIModel, d := toRepoCriteriaAPIModel(ctx, baseCriteria, criteriaAttrs) + d = m.WebhookResourceModel.toAPIModel(ctx, domain, criteriaAPIModel, apiModel) if d.HasError() { diags.Append(d...) @@ -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) - +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) @@ -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_repo_test.go b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_repo_test.go new file mode 100644 index 000000000..3d4cbdf21 --- /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 9e006741f..20a15900f 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). diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_webhook_user.go b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_user.go index 88ba1aa08..f441d555c 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{ @@ -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...) } @@ -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) { @@ -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 }