diff --git a/CHANGELOG.md b/CHANGELOG.md index 0933a49f5..95ec2b608 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## 12.1.1 (October 1, 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) + ## 12.1.0 (September 26, 2024). Tested on Artifactory 7.90.10 with Terraform 1.9.6 and OpenTofu 1.8.2 IMPROVEMENTS: diff --git a/go.mod b/go.mod index 404eeb106..dc0e346dc 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ module github.com/jfrog/terraform-provider-artifactory/v12 go 1.22.7 require ( - github.com/go-resty/resty/v2 v2.15.2 + github.com/go-resty/resty/v2 v2.15.3 github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 github.com/hashicorp/go-version v1.7.0 github.com/hashicorp/terraform-plugin-docs v0.19.4 diff --git a/go.sum b/go.sum index 51e2ff55a..509bdd4d3 100644 --- a/go.sum +++ b/go.sum @@ -49,8 +49,8 @@ github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+ github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow= github.com/go-git/go-git/v5 v5.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZtys= github.com/go-git/go-git/v5 v5.12.0/go.mod h1:FTM9VKtnI2m65hNI/TenDDDnUf2Q9FHnXYjuz9i5OEY= -github.com/go-resty/resty/v2 v2.15.2 h1:wLGqKU9l9tOIa2RyePoyu4ZUnDkUWfp2LZ0u6fMXExc= -github.com/go-resty/resty/v2 v2.15.2/go.mod h1:0fHAoK7JoBy/Ch36N8VFeMsK7xQOHhvWaC3iOktwmIU= +github.com/go-resty/resty/v2 v2.15.3 h1:bqff+hcqAflpiF591hhJzNdkRsFhlB96CYfBwSFvql8= +github.com/go-resty/resty/v2 v2.15.3/go.mod h1:0fHAoK7JoBy/Ch36N8VFeMsK7xQOHhvWaC3iOktwmIU= github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68= github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= diff --git a/pkg/artifactory/provider/framework.go b/pkg/artifactory/provider/framework.go index b8dff0f75..692d3303b 100644 --- a/pkg/artifactory/provider/framework.go +++ b/pkg/artifactory/provider/framework.go @@ -22,6 +22,7 @@ import ( "github.com/jfrog/terraform-provider-artifactory/v12/pkg/artifactory/resource/replication" "github.com/jfrog/terraform-provider-artifactory/v12/pkg/artifactory/resource/security" "github.com/jfrog/terraform-provider-artifactory/v12/pkg/artifactory/resource/user" + "github.com/jfrog/terraform-provider-artifactory/v12/pkg/artifactory/resource/webhook" "github.com/jfrog/terraform-provider-shared/client" "github.com/jfrog/terraform-provider-shared/util" validatorfw_string "github.com/jfrog/terraform-provider-shared/validator/fw/string" @@ -221,6 +222,18 @@ func (p *ArtifactoryProvider) Resources(ctx context.Context) []func() resource.R replication.NewLocalRepositorySingleReplicationResource, replication.NewLocalRepositoryMultiReplicationResource, replication.NewRemoteRepositoryReplicationResource, + webhook.NewArtifactWebhookResource, + webhook.NewArtifactLifecycleWebhookResource, + webhook.NewArtifactPropertyWebhookResource, + webhook.NewArtifactoryReleaseBundleWebhookResource, + webhook.NewBuildWebhookResource, + webhook.NewDestinationWebhookResource, + webhook.NewDistributionWebhookResource, + webhook.NewDockerWebhookResource, + webhook.NewReleaseBundleWebhookResource, + webhook.NewReleaseBundleV2WebhookResource, + webhook.NewReleaseBundleV2PromotionWebhookResource, + webhook.NewUserWebhookResource, } } diff --git a/pkg/artifactory/provider/resources.go b/pkg/artifactory/provider/resources.go index 0051461be..0ce00edcf 100644 --- a/pkg/artifactory/provider/resources.go +++ b/pkg/artifactory/provider/resources.go @@ -124,9 +124,7 @@ func resourcesMap() map[string]*schema.Resource { resourcesMap[federatedResourceName] = federated.ResourceArtifactoryFederatedGenericRepository(repoType) } - for _, webhookType := range webhook.TypesSupported { - webhookResourceName := fmt.Sprintf("artifactory_%s_webhook", webhookType) - resourcesMap[webhookResourceName] = webhook.ResourceArtifactoryWebhook(webhookType) + for _, webhookType := range webhook.DomainSupported { webhookCustomResourceName := fmt.Sprintf("artifactory_%s_custom_webhook", webhookType) resourcesMap[webhookCustomResourceName] = webhook.ResourceArtifactoryCustomWebhook(webhookType) } diff --git a/pkg/artifactory/resource/artifact/resource_artifactory_item_properties.go b/pkg/artifactory/resource/artifact/resource_artifactory_item_properties.go index adf5c9464..a58c772f1 100644 --- a/pkg/artifactory/resource/artifact/resource_artifactory_item_properties.go +++ b/pkg/artifactory/resource/artifact/resource_artifactory_item_properties.go @@ -21,7 +21,6 @@ import ( "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-log/tflog" "github.com/jfrog/terraform-provider-shared/util" utilfw "github.com/jfrog/terraform-provider-shared/util/fw" validatorfw_string "github.com/jfrog/terraform-provider-shared/validator/fw/string" @@ -303,11 +302,6 @@ func (r *ItemPropertiesResource) Update(ctx context.Context, req resource.Update return } - tflog.Debug(ctx, "Update", map[string]interface{}{ - "planProperties": planProperties, - "stateProperties": stateProperties, - }) - _, propKeysToRemove := lo.Difference( lo.Keys(planProperties), lo.Keys(stateProperties), diff --git a/pkg/artifactory/resource/repository/remote/resource_artifactory_remote_generic_repository.go b/pkg/artifactory/resource/repository/remote/resource_artifactory_remote_generic_repository.go index eeaa4e1ec..bc53fa512 100644 --- a/pkg/artifactory/resource/repository/remote/resource_artifactory_remote_generic_repository.go +++ b/pkg/artifactory/resource/repository/remote/resource_artifactory_remote_generic_repository.go @@ -17,7 +17,6 @@ type GenericRemoteRepo struct { } var genericSchemaV3 = lo.Assign( - baseSchema, map[string]*schema.Schema{ "propagate_query_params": { Type: schema.TypeBool, @@ -47,23 +46,23 @@ var getSchemas = func(s map[string]*schema.Schema) map[int16]map[string]*schema. return map[int16]map[string]*schema.Schema{ 0: lo.Assign( baseSchemaV1, - s, + genericSchemaV3, ), 1: lo.Assign( baseSchemaV1, - s, + genericSchemaV3, ), 2: lo.Assign( baseSchemaV2, - s, + genericSchemaV3, ), 3: lo.Assign( baseSchemaV3, - s, + genericSchemaV3, ), 4: lo.Assign( baseSchemaV3, - genericSchemaV4, + s, ), } } diff --git a/pkg/artifactory/resource/repository/remote/resource_artifactory_remote_repository_test.go b/pkg/artifactory/resource/repository/remote/resource_artifactory_remote_repository_test.go index e5f426ab2..0668d3208 100644 --- a/pkg/artifactory/resource/repository/remote/resource_artifactory_remote_repository_test.go +++ b/pkg/artifactory/resource/repository/remote/resource_artifactory_remote_repository_test.go @@ -1204,7 +1204,7 @@ func TestAccRemoteRepository_generic_migrate_to_schema_v4(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(t) }, - CheckDestroy: acctest.VerifyDeleted(fqrn, "", acctest.CheckRepo), + CheckDestroy: acctest.VerifyDeleted(fqrn, "key", acctest.CheckRepo), Steps: []resource.TestStep{ { Config: config, @@ -1223,8 +1223,11 @@ func TestAccRemoteRepository_generic_migrate_to_schema_v4(t *testing.T) { { ProviderFactories: acctest.ProviderFactories, Config: config, - PlanOnly: true, - ConfigPlanChecks: testutil.ConfigPlanChecks(""), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectEmptyPlan(), + }, + }, }, }, }) diff --git a/pkg/artifactory/resource/security/resource_artifactory_distribution_public_key.go b/pkg/artifactory/resource/security/resource_artifactory_distribution_public_key.go index 5dd0b3d32..650e0ac60 100644 --- a/pkg/artifactory/resource/security/resource_artifactory_distribution_public_key.go +++ b/pkg/artifactory/resource/security/resource_artifactory_distribution_public_key.go @@ -14,7 +14,6 @@ import ( "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-log/tflog" "github.com/jfrog/terraform-provider-shared/util" utilfw "github.com/jfrog/terraform-provider-shared/util/fw" @@ -218,7 +217,6 @@ func (r *DistributionPublicKeyResource) Read(ctx context.Context, req resource.R for _, key := range publicKeys.Keys { if key.Alias == state.Alias.ValueString() { resp.Diagnostics.Append(state.FromAPIModel(ctx, &key)...) - tflog.Debug(ctx, fmt.Sprintf("state after: %v", state)) } } if resp.Diagnostics.HasError() { diff --git a/pkg/artifactory/resource/security/resource_artifactory_scoped_token.go b/pkg/artifactory/resource/security/resource_artifactory_scoped_token.go index 3c13641aa..3c3d07f9e 100644 --- a/pkg/artifactory/resource/security/resource_artifactory_scoped_token.go +++ b/pkg/artifactory/resource/security/resource_artifactory_scoped_token.go @@ -666,9 +666,6 @@ func (r *ScopedTokenResourceModel) splitScopes(ctx context.Context, scopes strin }) } } - tflog.Debug(ctx, "ScopedTokenResourceModel.splitScopes", map[string]any{ - "separatorIndices": separatorIndices, - }) // insert a zero to the begining of the slice to represent the first index separatorIndices = append([]int{0}, separatorIndices...) @@ -687,9 +684,7 @@ func (r *ScopedTokenResourceModel) splitScopes(ctx context.Context, scopes strin // trim the end of string off for next iteration scopesCopy = scopesCopy[:idx] } - tflog.Debug(ctx, "ScopedTokenResourceModel.splitScopes", map[string]any{ - "scopesList": scopesList, - }) + return scopesList } diff --git a/pkg/artifactory/resource/security/resource_artifactory_vault_configuration.go b/pkg/artifactory/resource/security/resource_artifactory_vault_configuration.go index 262cd0716..2e53321b8 100644 --- a/pkg/artifactory/resource/security/resource_artifactory_vault_configuration.go +++ b/pkg/artifactory/resource/security/resource_artifactory_vault_configuration.go @@ -15,7 +15,6 @@ import ( "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-log/tflog" "github.com/jfrog/terraform-provider-shared/util" utilfw "github.com/jfrog/terraform-provider-shared/util/fw" validatorfw_string "github.com/jfrog/terraform-provider-shared/validator/fw/string" @@ -323,10 +322,6 @@ func (r VaultConfigurationResource) ValidateConfig(ctx context.Context, req reso configAttrs := data.Config.Attributes() authAttrs := configAttrs["auth"].(types.Object).Attributes() authType := authAttrs["type"].(types.String) - tflog.Debug(ctx, "ValidateConfig", map[string]interface{}{ - "configAttrs": configAttrs, - "authAttrs": authAttrs, - }) switch authType.ValueString() { case "Certificate": diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_artifact_lifecycle.go b/pkg/artifactory/resource/webhook/resource_artifactory_artifact_lifecycle.go index e3ace9afd..123400374 100644 --- a/pkg/artifactory/resource/webhook/resource_artifactory_artifact_lifecycle.go +++ b/pkg/artifactory/resource/webhook/resource_artifactory_artifact_lifecycle.go @@ -1,7 +1,167 @@ package webhook -import "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +import ( + "context" + "fmt" -var artifactLifecycleWebhookSchema = func(webhookType string, version int, isCustom bool) map[string]*schema.Schema { - return getBaseSchemaByVersion(webhookType, version, isCustom) + "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 = &ReleaseBundleWebhookResource{} + +func NewArtifactLifecycleWebhookResource() resource.Resource { + return &ArtifactLifecycleWebhookResource{ + WebhookResource: WebhookResource{ + TypeName: fmt.Sprintf("artifactory_%s_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 ArtifactLifecycleWebhookResourceModel struct { + WebhookNoCriteriaResourceModel +} + +type ArtifactLifecycleWebhookResource struct { + WebhookResource +} + +func (r *ArtifactLifecycleWebhookResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + r.WebhookResource.Metadata(ctx, req, resp) +} + +func (r *ArtifactLifecycleWebhookResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = r.schema(r.Domain, nil) +} + +func (r *ArtifactLifecycleWebhookResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + r.WebhookResource.Configure(ctx, req, resp) +} + +func (r *ArtifactLifecycleWebhookResource) 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 ArtifactLifecycleWebhookResourceModel + + // Read Terraform plan data into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + var webhook WebhookAPIModel + resp.Diagnostics.Append(plan.toAPIModel(ctx, r.Domain, &webhook)...) + if resp.Diagnostics.HasError() { + return + } + + r.WebhookResource.Create(ctx, webhook, req, resp) + if resp.Diagnostics.HasError() { + return + } + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *ArtifactLifecycleWebhookResource) 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 ArtifactLifecycleWebhookResourceModel + + // Read Terraform state data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + var webhook WebhookAPIModel + found := r.WebhookResource.Read(ctx, state.Key.ValueString(), &webhook, req, resp) + if resp.Diagnostics.HasError() { + return + } + + if !found { + return + } + + resp.Diagnostics.Append(state.fromAPIModel(ctx, webhook, state.Handlers)...) + if resp.Diagnostics.HasError() { + return + } + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *ArtifactLifecycleWebhookResource) 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 ArtifactLifecycleWebhookResourceModel + + // Read Terraform plan data into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + var webhook WebhookAPIModel + resp.Diagnostics.Append(plan.toAPIModel(ctx, r.Domain, &webhook)...) + if resp.Diagnostics.HasError() { + return + } + + r.WebhookResource.Update(ctx, plan.Key.ValueString(), webhook, req, resp) + if resp.Diagnostics.HasError() { + return + } + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *ArtifactLifecycleWebhookResource) 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 ArtifactLifecycleWebhookResourceModel + + // Read Terraform prior state data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + + r.WebhookResource.Delete(ctx, state.Key.ValueString(), req, resp) + if resp.Diagnostics.HasError() { + return + } + + // If the logic reaches here, it implicitly succeeded and will remove + // the resource from state if there are no other errors. +} + +// ImportState imports the resource into the Terraform state. +func (r *ArtifactLifecycleWebhookResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + r.WebhookResource.ImportState(ctx, req, resp) +} + +func (m ArtifactLifecycleWebhookResourceModel) toAPIModel(ctx context.Context, domain string, apiModel *WebhookAPIModel) (diags diag.Diagnostics) { + d := m.WebhookNoCriteriaResourceModel.toAPIModel(ctx, domain, apiModel) + if d.HasError() { + diags.Append(d...) + } + + return +} + +func (m *ArtifactLifecycleWebhookResourceModel) fromAPIModel(ctx context.Context, apiModel WebhookAPIModel, stateHandlers basetypes.SetValue) diag.Diagnostics { + diags := diag.Diagnostics{} + + d := m.WebhookNoCriteriaResourceModel.fromAPIModel(ctx, apiModel, stateHandlers) + if d.HasError() { + diags.Append(d...) + } + + return diags } diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_artifact_lifecycle_test.go b/pkg/artifactory/resource/webhook/resource_artifactory_artifact_lifecycle_test.go index 64c56fe3a..31c19aad9 100644 --- a/pkg/artifactory/resource/webhook/resource_artifactory_artifact_lifecycle_test.go +++ b/pkg/artifactory/resource/webhook/resource_artifactory_artifact_lifecycle_test.go @@ -1,22 +1,84 @@ 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" - "github.com/jfrog/terraform-provider-shared/validator" ) +func TestAccWebhook_ArtifactLifecycle_UpgradeFromSDKv2(t *testing.T) { + _, fqrn, name := testutil.MkNames("test-artifact-lifecycle", "artifactory_artifact_lifecycle_webhook") + + params := map[string]interface{}{ + "webhookName": name, + } + webhookConfig := util.ExecuteTemplate("TestAccWebhook_User", ` + resource "artifactory_artifact_lifecycle_webhook" "{{ .webhookName }}" { + key = "{{ .webhookName }}" + description = "test description" + event_types = [ + "archive", + "restore", + ] + handler { + url = "https://google.com" + secret = "fake-secret" + use_secret_for_signing = true + custom_http_headers = { + header-1 = "value-1" + header-2 = "value-2" + } + } + } + `, params) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + CheckDestroy: acctest.VerifyDeleted(fqrn, "key", acctest.CheckRepo), + + Steps: []resource.TestStep{ + { + Config: webhookConfig, + ExternalProviders: map[string]resource.ExternalProvider{ + "artifactory": { + Source: "jfrog/artifactory", + VersionConstraint: "12.1.0", + }, + }, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(fqrn, "key", name), + 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.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"), + ), + }, + { + Config: webhookConfig, + ProtoV6ProviderFactories: acctest.ProtoV6ProviderFactories, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectEmptyPlan(), + }, + }, + }, + }, + }) +} + func TestAccWebhook_ArtifactLifecycle(t *testing.T) { _, fqrn, name := testutil.MkNames("test-artifact-lifecycle", "artifactory_artifact_lifecycle_webhook") params := map[string]interface{}{ - "webhookName": name, - "useSecretForSigning": testutil.RandBool(), + "webhookName": name, } webhookConfig := util.ExecuteTemplate("TestAccWebhook_User", ` resource "artifactory_artifact_lifecycle_webhook" "{{ .webhookName }}" { @@ -29,7 +91,7 @@ func TestAccWebhook_ArtifactLifecycle(t *testing.T) { handler { url = "https://google.com" secret = "fake-secret" - use_secret_for_signing = {{ .useSecretForSigning }} + use_secret_for_signing = true custom_http_headers = { header-1 = "value-1" header-2 = "value-2" @@ -39,9 +101,9 @@ func TestAccWebhook_ArtifactLifecycle(t *testing.T) { `, params) resource.Test(t, resource.TestCase{ - PreCheck: func() { acctest.PreCheck(t) }, - ProviderFactories: acctest.ProviderFactories, - CheckDestroy: acctest.VerifyDeleted(fqrn, "", acctest.CheckRepo), + PreCheck: func() { acctest.PreCheck(t) }, + ProtoV6ProviderFactories: acctest.ProtoV6MuxProviderFactories, + CheckDestroy: acctest.VerifyDeleted(fqrn, "key", acctest.CheckRepo), Steps: []resource.TestStep{ { @@ -52,18 +114,19 @@ func TestAccWebhook_ArtifactLifecycle(t *testing.T) { resource.TestCheckResourceAttr(fqrn, "handler.#", "1"), 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", fmt.Sprintf("%t", params["useSecretForSigning"])), + 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"), ), }, { - ResourceName: fqrn, - ImportState: true, - ImportStateVerify: true, - ImportStateCheck: validator.CheckImportState(name, "key"), - ImportStateVerifyIgnore: []string{"handler.0.secret"}, + ResourceName: fqrn, + ImportState: true, + ImportStateId: name, + ImportStateVerify: true, + ImportStateVerifyIdentifierAttribute: "key", + ImportStateVerifyIgnore: []string{"handler.0.secret"}, }}, }) } diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook.go b/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook.go index e23a0e4a7..bd995e860 100644 --- a/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook.go +++ b/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook.go @@ -8,7 +8,6 @@ import ( "strings" "github.com/go-resty/resty/v2" - "github.com/hashicorp/terraform-plugin-log/tflog" "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" @@ -21,6 +20,21 @@ import ( "golang.org/x/exp/slices" ) +var DomainSupported = []string{ + ArtifactLifecycleDomain, + ArtifactPropertyDomain, + ArtifactDomain, + ArtifactoryReleaseBundleDomain, + BuildDomain, + DestinationDomain, + DistributionDomain, + DockerDomain, + ReleaseBundleDomain, + ReleaseBundleV2Domain, + ReleaseBundleV2PromotionDomain, + UserDomain, +} + func baseCustomWebhookBaseSchema(webhookType string) map[string]*schema.Schema { return map[string]*schema.Schema{ "key": { @@ -107,12 +121,155 @@ func baseCustomWebhookBaseSchema(webhookType string) map[string]*schema.Schema { } } +var repoWebhookSchema = func(webhookType string, version int, isCustom bool) map[string]*schema.Schema { + return utilsdk.MergeMaps(getBaseSchemaByVersion(webhookType, version, isCustom), map[string]*schema.Schema{ + "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", + }, + }), + }, + 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", + }, + }), + }, + 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", + }, + }), + }, + 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", + }, + }), + }, + Description: "Specifies where the webhook will be applied, on which release bundles promotion.", + }, + }) +} + +var userWebhookSchema = func(webhookType string, version int, isCustom bool) map[string]*schema.Schema { + return getBaseSchemaByVersion(webhookType, version, isCustom) +} + +var artifactLifecycleWebhookSchema = func(webhookType string, version int, isCustom bool) map[string]*schema.Schema { + return getBaseSchemaByVersion(webhookType, version, isCustom) +} + type CustomBaseParams struct { - Key string `json:"key"` - Description string `json:"description"` - Enabled bool `json:"enabled"` - EventFilter EventFilter `json:"event_filter"` - Handlers []CustomHandler `json:"handlers"` + Key string `json:"key"` + Description string `json:"description"` + Enabled bool `json:"enabled"` + EventFilterAPIModel EventFilterAPIModel `json:"event_filter"` + Handlers []CustomHandler `json:"handlers"` } func (w CustomBaseParams) Id() string { @@ -120,19 +277,19 @@ func (w CustomBaseParams) Id() string { } type CustomHandler struct { - HandlerType string `json:"handler_type"` - Url string `json:"url"` - Secrets []KeyValuePair `json:"secrets"` - Proxy string `json:"proxy"` - HttpHeaders []KeyValuePair `json:"http_headers"` - Payload string `json:"payload,omitempty"` + 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 SecretName struct { Name string `json:"name"` } -var packSecretsCustom = func(keyValuePairs []KeyValuePair, d *schema.ResourceData, url string) map[string]interface{} { +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{} @@ -204,7 +361,7 @@ func ResourceArtifactoryCustomWebhook(webhookType string) *schema.Resource { Key: d.GetString("key", false), Description: d.GetString("description", false), Enabled: d.GetBool("enabled", false), - EventFilter: EventFilter{ + EventFilterAPIModel: EventFilterAPIModel{ Domain: webhookType, EventTypes: d.GetSet("event_types"), Criteria: unpackCriteria(d, webhookType), @@ -246,9 +403,9 @@ func ResourceArtifactoryCustomWebhook(webhookType string) *schema.Resource { setValue("key", webhook.Key) setValue("description", webhook.Description) setValue("enabled", webhook.Enabled) - errors := setValue("event_types", webhook.EventFilter.EventTypes) - if webhook.EventFilter.Criteria != nil { - errors = append(errors, packCriteria(d, webhookType, webhook.EventFilter.Criteria.(map[string]interface{}))...) + errors := 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)...) @@ -260,18 +417,16 @@ func ResourceArtifactoryCustomWebhook(webhookType string) *schema.Resource { } var readWebhook = func(ctx context.Context, data *schema.ResourceData, m interface{}) diag.Diagnostics { - tflog.Debug(ctx, "tflog.Debug(ctx, \"readWebhook\")") - webhook := CustomBaseParams{} - webhook.EventFilter.Criteria = domainCriteriaLookup[webhookType] + 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(WhUrl) + Get(WebhookURL) if err != nil { return diag.FromErr(err) @@ -296,8 +451,6 @@ func ResourceArtifactoryCustomWebhook(webhookType string) *schema.Resource { } var createWebhook = func(ctx context.Context, data *schema.ResourceData, m interface{}) diag.Diagnostics { - tflog.Debug(ctx, "createWebhook") - webhook, err := unpackWebhook(data) if err != nil { return diag.FromErr(err) @@ -308,7 +461,7 @@ func ResourceArtifactoryCustomWebhook(webhookType string) *schema.Resource { SetBody(webhook). SetError(&artifactoryError). AddRetryCondition(retryOnProxyError). - Post(webhooksUrl) + Post(webhooksURL) if err != nil { return diag.FromErr(err) } @@ -323,8 +476,6 @@ func ResourceArtifactoryCustomWebhook(webhookType string) *schema.Resource { } var updateWebhook = func(ctx context.Context, data *schema.ResourceData, m interface{}) diag.Diagnostics { - tflog.Debug(ctx, "updateWebhook") - webhook, err := unpackWebhook(data) if err != nil { return diag.FromErr(err) @@ -336,7 +487,7 @@ func ResourceArtifactoryCustomWebhook(webhookType string) *schema.Resource { SetBody(webhook). SetError(&artifactoryError). AddRetryCondition(retryOnProxyError). - Put(WhUrl) + Put(WebhookURL) if err != nil { return diag.FromErr(err) } @@ -351,13 +502,11 @@ func ResourceArtifactoryCustomWebhook(webhookType string) *schema.Resource { } var deleteWebhook = func(ctx context.Context, data *schema.ResourceData, m interface{}) diag.Diagnostics { - tflog.Debug(ctx, "deleteWebhook") - var artifactoryError artifactory.ArtifactoryErrorsResponse resp, err := m.(util.ProviderMetadata).Client.R(). SetPathParam("webhookKey", data.Id()). SetError(&artifactoryError). - Delete(WhUrl) + Delete(WebhookURL) if err != nil { return diag.FromErr(err) @@ -376,8 +525,6 @@ func ResourceArtifactoryCustomWebhook(webhookType string) *schema.Resource { } var eventTypesDiff = func(ctx context.Context, diff *schema.ResourceDiff, v interface{}) error { - tflog.Debug(ctx, "eventTypesDiff") - eventTypes := diff.Get("event_types").(*schema.Set).List() if len(eventTypes) == 0 { return nil @@ -393,8 +540,6 @@ func ResourceArtifactoryCustomWebhook(webhookType string) *schema.Resource { } var criteriaDiff = func(ctx context.Context, diff *schema.ResourceDiff, v interface{}) error { - tflog.Debug(ctx, "criteriaDiff") - if resource, ok := diff.GetOk("criteria"); ok { criteria := resource.(*schema.Set).List() if len(criteria) == 0 { @@ -426,9 +571,310 @@ func ResourceArtifactoryCustomWebhook(webhookType string) *schema.Resource { Description: "Provides an Artifactory webhook resource", } - if webhookType == "artifactory_release_bundle" { + if webhookType == ReleaseBundleDomain { rs.DeprecationMessage = "This resource is being deprecated and replaced by artifactory_destination_custom_webhook resource" } return &rs } + +var unpackKeyValuePair = func(keyValuePairs map[string]interface{}) []KeyValuePairAPIModel { + var kvPairs []KeyValuePairAPIModel + for key, value := range keyValuePairs { + keyValuePair := KeyValuePairAPIModel{ + Name: key, + Value: value.(string), + } + kvPairs = append(kvPairs, keyValuePair) + } + + return kvPairs +} + +var packKeyValuePair = func(keyValuePairs []KeyValuePairAPIModel) map[string]interface{} { + kvPairs := make(map[string]interface{}) + for _, keyValuePair := range keyValuePairs { + kvPairs[keyValuePair.Name] = keyValuePair.Value + } + + return kvPairs +} + +type EmptyWebhookCriteria struct{} + +var domainCriteriaLookup = map[string]interface{}{ + "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) + } + + 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{})), + } +} + +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, + } +} + +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, + } +} + +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{})), + } +} + +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, + } +} + +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()), + } + + webhookCriteria = domainUnpackLookup[webhookType](id, baseCriteria) + } + } + + return webhookCriteria +} + +var packCriteria = func(d *schema.ResourceData, webhookType string, criteria map[string]interface{}) []error { + setValue := utilsdk.MkLens(d) + + resource := domainSchemaLookup(currentSchemaVersion, false, webhookType)[webhookType]["criteria"].Elem.(*schema.Resource) + packedCriteria := domainPackLookup[webhookType](criteria) + + includePatterns := []interface{}{} + if v, ok := criteria["includePatterns"]; ok && v != nil { + includePatterns = v.([]interface{}) + } + packedCriteria["include_patterns"] = schema.NewSet(schema.HashString, includePatterns) + + excludePatterns := []interface{}{} + if v, ok := criteria["excludePatterns"]; ok && v != nil { + excludePatterns = v.([]interface{}) + } + packedCriteria["exclude_patterns"] = schema.NewSet(schema.HashString, excludePatterns) + + return setValue("criteria", schema.NewSet(schema.HashResource(resource), []interface{}{packedCriteria})) +} + +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, +} + +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 +} + +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() + + if !anyBuild && (len(selectedBuilds) == 0 && len(includePatterns) == 0) { + return fmt.Errorf("selected_builds or include_patterns cannot be empty when any_build is false") + } + + return nil +} + +var releaseBundleCriteriaValidation = func(ctx context.Context, criteria map[string]interface{}) error { + anyReleaseBundle := criteria["any_release_bundle"].(bool) + registeredReleaseBundlesNames := criteria["registered_release_bundle_names"].(*schema.Set).List() + + if !anyReleaseBundle && len(registeredReleaseBundlesNames) == 0 { + return fmt.Errorf("registered_release_bundle_names cannot be empty when any_release_bundle is false") + } + + return nil +} + +var releaseBundleV2CriteriaValidation = func(ctx context.Context, criteria map[string]interface{}) error { + 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 +} + +var emptyCriteriaValidation = func(ctx context.Context, criteria map[string]interface{}) error { + return nil +} + +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 +} + diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_webhook.go b/pkg/artifactory/resource/webhook/resource_artifactory_webhook.go index eb39a81cb..36caea9a1 100644 --- a/pkg/artifactory/resource/webhook/resource_artifactory_webhook.go +++ b/pkg/artifactory/resource/webhook/resource_artifactory_webhook.go @@ -5,551 +5,576 @@ import ( "fmt" "net/http" "regexp" + "strings" "github.com/go-resty/resty/v2" - "github.com/hashicorp/terraform-plugin-log/tflog" - "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-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/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier" + "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" "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" + utilfw "github.com/jfrog/terraform-provider-shared/util/fw" + validatorfw_string "github.com/jfrog/terraform-provider-shared/validator/fw/string" + "github.com/samber/lo" +) - "golang.org/x/exp/slices" +const ( + webhooksURL = "/event/api/v1/subscriptions" + WebhookURL = "/event/api/v1/subscriptions/{webhookKey}" + + ArtifactLifecycleDomain = "artifact_lifecycle" + ArtifactPropertyDomain = "artifact_property" + ArtifactDomain = "artifact" + ArtifactoryReleaseBundleDomain = "artifactory_release_bundle" + BuildDomain = "build" + DestinationDomain = "destination" + DistributionDomain = "distribution" + DockerDomain = "docker" + ReleaseBundleDomain = "release_bundle" + ReleaseBundleV2Domain = "release_bundle_v2" + ReleaseBundleV2PromotionDomain = "release_bundle_v2_promotion" + UserDomain = "user" ) -var TypesSupported = []string{ - "artifact", - "artifact_property", - "docker", - "build", - "release_bundle", - "distribution", - "artifactory_release_bundle", - "destination", - "user", - "release_bundle_v2", - "release_bundle_v2_promotion", - "artifact_lifecycle", -} +const currentSchemaVersion = 2 var DomainEventTypesSupported = map[string][]string{ - "artifact": {"deployed", "deleted", "moved", "copied", "cached"}, - "artifact_property": {"added", "deleted"}, - "docker": {"pushed", "deleted", "promoted"}, - "build": {"uploaded", "deleted", "promoted"}, - "release_bundle": {"created", "signed", "deleted"}, - "distribution": {"distribute_started", "distribute_completed", "distribute_aborted", "distribute_failed", "delete_started", "delete_completed", "delete_failed"}, - "artifactory_release_bundle": {"received", "delete_started", "delete_completed", "delete_failed"}, - "destination": {"received", "delete_started", "delete_completed", "delete_failed"}, - "user": {"locked"}, - "release_bundle_v2": {"release_bundle_v2_started", "release_bundle_v2_failed", "release_bundle_v2_completed"}, - "release_bundle_v2_promotion": {"release_bundle_v2_promotion_completed", "release_bundle_v2_promotion_failed", "release_bundle_v2_promotion_started"}, - "artifact_lifecycle": {"archive", "restore"}, -} - -type BaseParams struct { - Key string `json:"key"` - Description string `json:"description"` - Enabled bool `json:"enabled"` - EventFilter EventFilter `json:"event_filter"` - Handlers []Handler `json:"handlers"` -} - -func (w BaseParams) Id() string { - return w.Key -} - -type EventFilter struct { - Domain string `json:"domain"` - EventTypes []string `json:"event_types"` - Criteria interface{} `json:"criteria"` + ArtifactDomain: {"deployed", "deleted", "moved", "copied", "cached"}, + ArtifactPropertyDomain: {"added", "deleted"}, + DockerDomain: {"pushed", "deleted", "promoted"}, + BuildDomain: {"uploaded", "deleted", "promoted"}, + ReleaseBundleDomain: {"created", "signed", "deleted"}, + DistributionDomain: {"distribute_started", "distribute_completed", "distribute_aborted", "distribute_failed", "delete_started", "delete_completed", "delete_failed"}, + ArtifactoryReleaseBundleDomain: {"received", "delete_started", "delete_completed", "delete_failed"}, + DestinationDomain: {"received", "delete_started", "delete_completed", "delete_failed"}, + UserDomain: {"locked"}, + ReleaseBundleV2Domain: {"release_bundle_v2_started", "release_bundle_v2_failed", "release_bundle_v2_completed"}, + ReleaseBundleV2PromotionDomain: {"release_bundle_v2_promotion_completed", "release_bundle_v2_promotion_failed", "release_bundle_v2_promotion_started"}, + ArtifactLifecycleDomain: {"archive", "restore"}, } -type Handler struct { - HandlerType string `json:"handler_type"` - Url string `json:"url"` - Secret string `json:"secret"` - UseSecretForSigning bool `json:"use_secret_for_signing"` - Proxy string `json:"proxy"` - CustomHttpHeaders []KeyValuePair `json:"custom_http_headers"` +type WebhookResource struct { + ProviderData util.ProviderMetadata + TypeName string + Domain string + Description string } -type KeyValuePair struct { - Name string `json:"name"` - Value string `json:"value"` +var patternsSchemaAttributes = func(description string) map[string]schema.Attribute { + return map[string]schema.Attribute{ + "include_patterns": schema.SetAttribute{ + ElementType: types.StringType, + Optional: true, + MarkdownDescription: description, + }, + "exclude_patterns": schema.SetAttribute{ + ElementType: types.StringType, + Optional: true, + MarkdownDescription: description, + }, + } } -const webhooksUrl = "/event/api/v1/subscriptions" - -const WhUrl = webhooksUrl + "/{webhookKey}" - -const currentSchemaVersion = 2 - -var unpackKeyValuePair = func(keyValuePairs map[string]interface{}) []KeyValuePair { - var kvPairs []KeyValuePair - for key, value := range keyValuePairs { - keyValuePair := KeyValuePair{ - Name: key, - Value: value.(string), - } - kvPairs = append(kvPairs, keyValuePair) +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.", + }, + }, + }, + Validators: []validator.Set{ + setvalidator.IsRequired(), + setvalidator.SizeAtLeast(1), + }, + }, } - return kvPairs -} - -var packKeyValuePair = func(keyValuePairs []KeyValuePair) map[string]interface{} { - kvPairs := make(map[string]interface{}) - for _, keyValuePair := range keyValuePairs { - kvPairs[keyValuePair.Name] = keyValuePair.Value + if criteriaBlock != nil { + blocks = lo.Assign( + blocks, + map[string]schema.Block{ + "criteria": *criteriaBlock, + }, + ) } - return kvPairs + return schema.Schema{ + Version: currentSchemaVersion, + Attributes: map[string]schema.Attribute{ + "key": schema.StringAttribute{ + Required: true, + Validators: []validator.String{ + stringvalidator.LengthBetween(2, 200), + stringvalidator.NoneOf(" "), + }, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Description: "Key of webhook. Must be between 2 and 200 characters. Cannot contain spaces.", + }, + "description": schema.StringAttribute{ + Optional: true, + Validators: []validator.String{ + stringvalidator.LengthBetween(0, 1000), + }, + Description: "Description of webhook. Max length 1000 characters.", + }, + "enabled": schema.BoolAttribute{ + Optional: true, + Computed: true, + Default: booldefault.StaticBool(true), + MarkdownDescription: "Status of webhook. Default to `true`", + }, + "event_types": schema.SetAttribute{ + ElementType: types.StringType, + Required: true, + Validators: []validator.Set{ + setvalidator.SizeAtLeast(1), + setvalidator.ValueStringsAre( + stringvalidator.OneOf(DomainEventTypesSupported[domain]...), + ), + }, + 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[ArtifactDomain], ", "), "[]")), + }, + }, + Blocks: blocks, + MarkdownDescription: r.Description, + } } -var domainCriteriaLookup = map[string]interface{}{ - "artifact": RepoWebhookCriteria{}, - "artifact_property": RepoWebhookCriteria{}, - "docker": RepoWebhookCriteria{}, - "build": BuildWebhookCriteria{}, - "release_bundle": ReleaseBundleWebhookCriteria{}, - "distribution": ReleaseBundleWebhookCriteria{}, - "artifactory_release_bundle": ReleaseBundleWebhookCriteria{}, - "destination": ReleaseBundleWebhookCriteria{}, - "user": EmptyWebhookCriteria{}, - "release_bundle_v2": ReleaseBundleV2WebhookCriteria{}, - "release_bundle_v2_promotion": ReleaseBundleV2PromotionWebhookCriteria{}, - "artifact_lifecycle": EmptyWebhookCriteria{}, +func (r *WebhookResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = r.TypeName } -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, +func (r *WebhookResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + // Prevent panic if the provider has not been configured. + if req.ProviderData == nil { + return + } + r.ProviderData = req.ProviderData.(util.ProviderMetadata) } -var domainUnpackLookup = map[string]func(map[string]interface{}, BaseWebhookCriteria) 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, -} +func (r *WebhookResource) Create(ctx context.Context, webhook WebhookAPIModel, req resource.CreateRequest, resp *resource.CreateResponse) { + var artifactoryError artifactory.ArtifactoryErrorsResponse + response, err := r.ProviderData.Client.R(). + SetBody(webhook). + SetError(&artifactoryError). + AddRetryCondition(retryOnProxyError). + Post(webhooksURL) + + if err != nil { + utilfw.UnableToCreateResourceError(resp, err.Error()) + return + } -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), + if response.IsError() { + utilfw.UnableToCreateResourceError(resp, artifactoryError.String()) + return } } -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{}) +func (r *WebhookResource) Read(ctx context.Context, key string, webhook *WebhookAPIModel, req resource.ReadRequest, resp *resource.ReadResponse) (found bool) { + var artifactoryError artifactory.ArtifactoryErrorsResponse + response, err := r.ProviderData.Client.R(). + SetPathParam("webhookKey", key). + SetResult(&webhook). + SetError(&artifactoryError). + Get(WebhookURL) + if err != nil { + utilfw.UnableToRefreshResourceError(resp, err.Error()) + return false + } - baseCriteria := BaseWebhookCriteria{ - IncludePatterns: utilsdk.CastToStringArr(id["include_patterns"].(*schema.Set).List()), - ExcludePatterns: utilsdk.CastToStringArr(id["exclude_patterns"].(*schema.Set).List()), - } + if response.StatusCode() == http.StatusNotFound { + resp.State.RemoveResource(ctx) + return false + } - webhookCriteria = domainUnpackLookup[webhookType](id, baseCriteria) - } + if response.IsError() { + utilfw.UnableToRefreshResourceError(resp, artifactoryError.String()) + return false } - return webhookCriteria + return true } -var packCriteria = func(d *schema.ResourceData, webhookType string, criteria map[string]interface{}) []error { - setValue := utilsdk.MkLens(d) - - resource := domainSchemaLookup(currentSchemaVersion, false, webhookType)[webhookType]["criteria"].Elem.(*schema.Resource) - packedCriteria := domainPackLookup[webhookType](criteria) - - includePatterns := []interface{}{} - if v, ok := criteria["includePatterns"]; ok && v != nil { - includePatterns = v.([]interface{}) +func (r *WebhookResource) Update(ctx context.Context, key string, webhook WebhookAPIModel, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var artifactoryError artifactory.ArtifactoryErrorsResponse + response, err := r.ProviderData.Client.R(). + SetPathParam("webhookKey", key). + SetBody(webhook). + AddRetryCondition(retryOnProxyError). + SetError(&artifactoryError). + Put(WebhookURL) + + if err != nil { + utilfw.UnableToUpdateResourceError(resp, err.Error()) + return } - packedCriteria["include_patterns"] = schema.NewSet(schema.HashString, includePatterns) - excludePatterns := []interface{}{} - if v, ok := criteria["excludePatterns"]; ok && v != nil { - excludePatterns = v.([]interface{}) + if response.IsError() { + utilfw.UnableToUpdateResourceError(resp, artifactoryError.String()) + return } - packedCriteria["exclude_patterns"] = schema.NewSet(schema.HashString, excludePatterns) - - return setValue("criteria", schema.NewSet(schema.HashResource(resource), []interface{}{packedCriteria})) } -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, -} +func (r *WebhookResource) Delete(ctx context.Context, key string, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var artifactoryError artifactory.ArtifactoryErrorsResponse + response, err := r.ProviderData.Client.R(). + SetPathParam("webhookKey", key). + SetError(&artifactoryError). + Delete(WebhookURL) -var emptyCriteriaValidation = func(ctx context.Context, criteria map[string]interface{}) error { - return nil -} + if err != nil { + utilfw.UnableToDeleteResourceError(resp, err.Error()) + return + } -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) - } - } + if response.StatusCode() == http.StatusNotFound { + resp.State.RemoveResource(ctx) + return } - return secret + if response.IsError() { + utilfw.UnableToDeleteResourceError(resp, artifactoryError.String()) + return + } } -func ResourceArtifactoryWebhook(webhookType string) *schema.Resource { - - var unpackWebhook = func(data *schema.ResourceData) (BaseParams, error) { - d := &utilsdk.ResourceData{ResourceData: data} - - var unpackHandlers = func(d *utilsdk.ResourceData) []Handler { - var webhookHandlers []Handler - - 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 := Handler{ - HandlerType: "webhook", - Url: h["url"].(string), - } - - if v, ok := h["secret"]; ok { - webhookHandler.Secret = v.(string) - } +// ImportState imports the resource into the Terraform state. +func (r *WebhookResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("key"), req, resp) +} - if v, ok := h["use_secret_for_signing"]; ok { - webhookHandler.UseSecretForSigning = v.(bool) - } +type WebhookNoCriteriaResourceModel struct { + Key types.String `tfsdk:"key"` + Description types.String `tfsdk:"description"` + Enabled types.Bool `tfsdk:"enabled"` + EventTypes types.Set `tfsdk:"event_types"` + Handlers types.Set `tfsdk:"handler"` +} - if v, ok := h["proxy"]; ok { - webhookHandler.Proxy = v.(string) - } +type WebhookResourceModel struct { + WebhookNoCriteriaResourceModel + Criteria types.Set `tfsdk:"criteria"` +} - if v, ok := h["custom_http_headers"]; ok { - webhookHandler.CustomHttpHeaders = unpackKeyValuePair(v.(map[string]interface{})) - } +func (m WebhookNoCriteriaResourceModel) toAPIModel(ctx context.Context, domain string, apiModel *WebhookAPIModel) (diags diag.Diagnostics) { + var eventTypes []string + d := m.EventTypes.ElementsAs(ctx, &eventTypes, false) + if d.HasError() { + diags.Append(d...) + } - webhookHandlers = append(webhookHandlers, webhookHandler) + handlers := lo.Map( + m.Handlers.Elements(), + func(elem attr.Value, _ int) HandlerAPIModel { + attrs := elem.(types.Object).Attributes() + + customHttpHeaders := lo.MapToSlice( + attrs["custom_http_headers"].(types.Map).Elements(), + func(k string, v attr.Value) KeyValuePairAPIModel { + return KeyValuePairAPIModel{ + Name: k, + Value: v.(types.String).ValueString(), } - } + }, + ) + + return HandlerAPIModel{ + HandlerType: "webhook", + Url: attrs["url"].(types.String).ValueString(), + Secret: attrs["secret"].(types.String).ValueStringPointer(), + UseSecretForSigning: attrs["use_secret_for_signing"].(types.Bool).ValueBoolPointer(), + Proxy: attrs["proxy"].(types.String).ValueStringPointer(), + CustomHttpHeaders: customHttpHeaders, } - - return webhookHandlers - } - - webhook := BaseParams{ - Key: d.GetString("key", false), - Description: d.GetString("description", false), - Enabled: d.GetBool("enabled", false), - EventFilter: EventFilter{ - Domain: webhookType, - EventTypes: d.GetSet("event_types"), - Criteria: unpackCriteria(d, webhookType), - }, - Handlers: unpackHandlers(d), - } - - return webhook, nil + }, + ) + + *apiModel = WebhookAPIModel{ + Key: m.Key.ValueString(), + Description: m.Description.ValueString(), + Enabled: m.Enabled.ValueBool(), + EventFilter: EventFilterAPIModel{ + Domain: domain, + EventTypes: eventTypes, + }, + Handlers: handlers, } - var packHandlers = func(d *schema.ResourceData, handlers []Handler) []error { - setValue := utilsdk.MkLens(d) - resource := domainSchemaLookup(currentSchemaVersion, false, webhookType)[webhookType]["handler"].Elem.(*schema.Resource) - var packedHandlers []interface{} - for _, handler := range handlers { - packedHandler := map[string]interface{}{ - "url": handler.Url, - "secret": packSecret(d, handler.Url), - "use_secret_for_signing": handler.UseSecretForSigning, - "proxy": handler.Proxy, - } + return +} - if handler.CustomHttpHeaders != nil { - packedHandler["custom_http_headers"] = packKeyValuePair(handler.CustomHttpHeaders) - } +func (m WebhookResourceModel) toAPIModel(ctx context.Context, domain string, criteriaAPIModel interface{}, apiModel *WebhookAPIModel) (diags diag.Diagnostics) { + d := m.WebhookNoCriteriaResourceModel.toAPIModel(ctx, domain, apiModel) - packedHandlers = append(packedHandlers, packedHandler) - } - - return setValue("handler", schema.NewSet(schema.HashResource(resource), packedHandlers)) - } + apiModel.EventFilter.Criteria = criteriaAPIModel - var packWebhook = func(d *schema.ResourceData, webhook BaseParams) diag.Diagnostics { - setValue := utilsdk.MkLens(d) - - setValue("key", webhook.Key) - setValue("description", webhook.Description) - setValue("enabled", webhook.Enabled) - errors := setValue("event_types", webhook.EventFilter.EventTypes) - if webhook.EventFilter.Criteria != nil { - errors = append(errors, packCriteria(d, webhookType, webhook.EventFilter.Criteria.(map[string]interface{}))...) - } - errors = append(errors, packHandlers(d, webhook.Handlers)...) + return d +} - if len(errors) > 0 { - return diag.Errorf("failed to pack webhook %q", errors) - } +func (m *WebhookResourceModel) toBaseCriteriaAPIModel(ctx context.Context, criteriaAttrs map[string]attr.Value) (BaseCriteriaAPIModel, diag.Diagnostics) { + diags := diag.Diagnostics{} - return nil + var includePatterns []string + d := criteriaAttrs["include_patterns"].(types.Set).ElementsAs(ctx, &includePatterns, false) + if d.HasError() { + diags.Append(d...) } - var readWebhook = func(ctx context.Context, data *schema.ResourceData, m interface{}) diag.Diagnostics { - tflog.Debug(ctx, "tflog.Debug(ctx, \"readWebhook\")") - - webhook := BaseParams{} - - webhook.EventFilter.Criteria = domainCriteriaLookup[webhookType] - - var artifactoryError artifactory.ArtifactoryErrorsResponse - resp, err := m.(util.ProviderMetadata).Client.R(). - SetPathParam("webhookKey", data.Id()). - SetResult(&webhook). - SetError(&artifactoryError). - Get(WhUrl) - - 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 excludePatterns []string + d = criteriaAttrs["exclude_patterns"].(types.Set).ElementsAs(ctx, &excludePatterns, false) + if d.HasError() { + diags.Append(d...) } - var retryOnProxyError = func(response *resty.Response, _r error) bool { - var proxyNotFoundRegex = regexp.MustCompile("proxy with key '.*' not found") + return BaseCriteriaAPIModel{ + IncludePatterns: includePatterns, + ExcludePatterns: excludePatterns, + }, diags +} - return proxyNotFoundRegex.MatchString(string(response.Body()[:])) - } +var patternsCriteriaSetResourceModelAttributeTypes = map[string]attr.Type{ + "include_patterns": types.SetType{ElemType: types.StringType}, + "exclude_patterns": types.SetType{ElemType: types.StringType}, +} - var createWebhook = func(ctx context.Context, data *schema.ResourceData, m interface{}) diag.Diagnostics { - tflog.Debug(ctx, "createWebhook") +var handlerSetResourceModelAttributeTypes = map[string]attr.Type{ + "url": types.StringType, + "secret": types.StringType, + "use_secret_for_signing": types.BoolType, + "proxy": types.StringType, + "custom_http_headers": types.MapType{ElemType: types.StringType}, +} - webhook, err := unpackWebhook(data) - if err != nil { - return diag.FromErr(err) - } +var handlerSetResourceModelElementTypes = types.ObjectType{ + AttrTypes: handlerSetResourceModelAttributeTypes, +} - var artifactoryError artifactory.ArtifactoryErrorsResponse - resp, err := m.(util.ProviderMetadata).Client.R(). - SetBody(webhook). - AddRetryCondition(retryOnProxyError). - SetError(&artifactoryError). - Post(webhooksUrl) - if err != nil { - return diag.FromErr(err) - } +func (m *WebhookResourceModel) fromBaseCriteriaAPIModel(ctx context.Context, criteriaAPIModel map[string]interface{}) (map[string]attr.Value, diag.Diagnostics) { + diags := diag.Diagnostics{} - if resp.IsError() { - return diag.Errorf("%s", artifactoryError.String()) + 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...) } - data.SetId(webhook.Id()) - - return readWebhook(ctx, data, m) + includePatterns = ps } - var updateWebhook = func(ctx context.Context, data *schema.ResourceData, m interface{}) diag.Diagnostics { - tflog.Debug(ctx, "updateWebhook") - - 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). - AddRetryCondition(retryOnProxyError). - SetError(&artifactoryError). - Put(WhUrl) - if err != nil { - return diag.FromErr(err) + 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...) } - if resp.IsError() { - return diag.Errorf("%s", artifactoryError.String()) - } - - data.SetId(webhook.Id()) - - return readWebhook(ctx, data, m) + excludePatterns = ps } - var deleteWebhook = func(ctx context.Context, data *schema.ResourceData, m interface{}) diag.Diagnostics { - tflog.Debug(ctx, "deleteWebhook") + return map[string]attr.Value{ + "include_patterns": includePatterns, + "exclude_patterns": excludePatterns, + }, diags +} - var artifactoryError artifactory.ArtifactoryErrorsResponse - resp, err := m.(util.ProviderMetadata).Client.R(). - SetPathParam("webhookKey", data.Id()). - SetError(&artifactoryError). - Delete(WhUrl) +func (m *WebhookNoCriteriaResourceModel) fromAPIModel(ctx context.Context, apiModel WebhookAPIModel, stateHandlers basetypes.SetValue) diag.Diagnostics { + diags := diag.Diagnostics{} - if err != nil { - return diag.FromErr(err) - } + m.Key = types.StringValue(apiModel.Key) - if resp.StatusCode() == http.StatusNotFound { - data.SetId("") - return nil - } + description := types.StringNull() + if apiModel.Description != "" { + description = types.StringValue(apiModel.Description) + } + m.Description = description - if resp.IsError() { - return diag.Errorf("%s", artifactoryError.String()) - } + m.Enabled = types.BoolValue(apiModel.Enabled) - return nil + eventTypes, d := types.SetValueFrom(ctx, types.StringType, apiModel.EventFilter.EventTypes) + if d.HasError() { + diags.Append(d...) } + m.EventTypes = eventTypes + + handlers := lo.Map( + apiModel.Handlers, + func(handler HandlerAPIModel, _ int) attr.Value { + customHttpHeaders := types.MapNull(types.StringType) + if len(handler.CustomHttpHeaders) > 0 { + headerElems := lo.Associate( + handler.CustomHttpHeaders, + 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 eventTypesDiff = func(ctx context.Context, diff *schema.ResourceDiff, v interface{}) error { - tflog.Debug(ctx, "eventTypesDiff") + customHttpHeaders = h + } - eventTypes := diff.Get("event_types").(*schema.Set).List() - if len(eventTypes) == 0 { - return nil - } + secret := types.StringNull() + useSecretForSigning := types.BoolPointerValue(handler.UseSecretForSigning) + + 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["secret"].(types.String) + if !s.IsNull() && s.ValueString() != "" { + secret = s + } - 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) + // API doesn't include 'use_secret_for_signing' if set to 'false' + // so need set state to null if attribute is defined in config and set to 'false' + u := attrs["use_secret_for_signing"].(types.Bool) + if handler.UseSecretForSigning == nil && !u.IsNull() && !u.ValueBool() { + useSecretForSigning = types.BoolNull() + } } - } - return nil - } - var criteriaDiff = func(ctx context.Context, diff *schema.ResourceDiff, v interface{}) error { - tflog.Debug(ctx, "criteriaDiff") - - if resource, ok := diff.GetOk("criteria"); ok { - criteria := resource.(*schema.Set).List() - if len(criteria) == 0 { - return nil + h, d := types.ObjectValue( + handlerSetResourceModelAttributeTypes, + map[string]attr.Value{ + "url": types.StringValue(handler.Url), + "secret": secret, + "use_secret_for_signing": useSecretForSigning, + "proxy": types.StringPointerValue(handler.Proxy), + "custom_http_headers": customHttpHeaders, + }, + ) + if d.HasError() { + diags.Append(d...) } - return domainCriteriaValidationLookup[webhookType](ctx, criteria[0].(map[string]interface{})) - } - return nil + return h + }, + ) + + handlersSet, d := types.SetValue( + handlerSetResourceModelElementTypes, + handlers, + ) + if d.HasError() { + diags.Append(d...) } + m.Handlers = handlersSet + + return diags +} - // Previous version of the schema - // see example in https://www.terraform.io/plugin/sdkv2/resources/state-migration#terraform-v0-12-sdk-state-migrations - resourceSchemaV1 := &schema.Resource{ - Schema: domainSchemaLookup(1, false, webhookType)[webhookType], +func (m *WebhookResourceModel) fromAPIModel(ctx context.Context, apiModel WebhookAPIModel, stateHandlers basetypes.SetValue, criteriaSet *basetypes.SetValue) diag.Diagnostics { + if criteriaSet != nil { + m.Criteria = *criteriaSet } - rs := schema.Resource{ - SchemaVersion: 2, - CreateContext: createWebhook, - ReadContext: readWebhook, - UpdateContext: updateWebhook, - DeleteContext: deleteWebhook, + return m.WebhookNoCriteriaResourceModel.fromAPIModel(ctx, apiModel, stateHandlers) +} - Importer: &schema.ResourceImporter{ - StateContext: schema.ImportStatePassthroughContext, - }, +type WebhookAPIModel struct { + Key string `json:"key"` + Description string `json:"description"` + Enabled bool `json:"enabled"` + EventFilter EventFilterAPIModel `json:"event_filter"` + Handlers []HandlerAPIModel `json:"handlers"` +} - Schema: domainSchemaLookup(currentSchemaVersion, false, webhookType)[webhookType], - StateUpgraders: []schema.StateUpgrader{ - { - Type: resourceSchemaV1.CoreConfigSchema().ImpliedType(), - Upgrade: ResourceStateUpgradeV1, - Version: 1, - }, - }, +func (w WebhookAPIModel) Id() string { + return w.Key +} - CustomizeDiff: customdiff.All( - eventTypesDiff, - criteriaDiff, - ), - Description: "Provides an Artifactory webhook resource", - } +type EventFilterAPIModel struct { + Domain string `json:"domain"` + EventTypes []string `json:"event_types"` + Criteria interface{} `json:"criteria,omitempty"` +} - if webhookType == "artifactory_release_bundle" { - rs.DeprecationMessage = "This resource is being deprecated and replaced by artifactory_destination_webhook resource" - } +type BaseCriteriaAPIModel struct { + IncludePatterns []string `json:"includePatterns"` + ExcludePatterns []string `json:"excludePatterns"` +} - return &rs +type HandlerAPIModel struct { + HandlerType string `json:"handler_type"` + Url string `json:"url"` + Secret *string `json:"secret"` + UseSecretForSigning *bool `json:"use_secret_for_signing,omitempty"` + Proxy *string `json:"proxy"` + CustomHttpHeaders []KeyValuePairAPIModel `json:"custom_http_headers"` } -// ResourceStateUpgradeV1 see the corresponding unit test TestWebhookResourceStateUpgradeV1 -// for more details on the schema transformation -func ResourceStateUpgradeV1(_ context.Context, rawState map[string]interface{}, _ interface{}) (map[string]interface{}, error) { - rawState["handler"] = []map[string]interface{}{ - { - "url": rawState["url"], - "secret": rawState["secret"], - "proxy": rawState["proxy"], - "custom_http_headers": rawState["custom_http_headers"], - }, - } +type KeyValuePairAPIModel struct { + Name string `json:"name"` + Value string `json:"value"` +} - delete(rawState, "url") - delete(rawState, "secret") - delete(rawState, "proxy") - delete(rawState, "custom_http_headers") +var retryOnProxyError = func(response *resty.Response, _r error) bool { + var proxyNotFoundRegex = regexp.MustCompile("proxy with key '.*' not found") - return rawState, nil + return proxyNotFoundRegex.MatchString(string(response.Body()[:])) } diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_webhook_base.go b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_base.go index f75669ef7..678681aa4 100644 --- a/pkg/artifactory/resource/webhook/resource_artifactory_webhook_base.go +++ b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_base.go @@ -9,11 +9,6 @@ import ( "github.com/jfrog/terraform-provider-shared/validator" ) -type BaseWebhookCriteria struct { - IncludePatterns []string `json:"includePatterns"` - ExcludePatterns []string `json:"excludePatterns"` -} - var baseCriteriaSchema = map[string]*schema.Schema{ "include_patterns": { Type: schema.TypeSet, diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_webhook_build.go b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_build.go index 542235d89..322aff332 100644 --- a/pkg/artifactory/resource/webhook/resource_artifactory_webhook_build.go +++ b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_build.go @@ -4,65 +4,290 @@ import ( "context" "fmt" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - utilsdk "github.com/jfrog/terraform-provider-shared/util/sdk" + "github.com/hashicorp/terraform-plugin-framework-validators/setvalidator" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + "github.com/jfrog/terraform-provider-shared/util" + "github.com/samber/lo" ) -type BuildWebhookCriteria struct { - BaseWebhookCriteria - AnyBuild bool `json:"anyBuild"` - SelectedBuilds []string `json:"selectedBuilds"` +var _ resource.Resource = &BuildWebhookResource{} + +func NewBuildWebhookResource() resource.Resource { + return &BuildWebhookResource{ + WebhookResource: WebhookResource{ + TypeName: fmt.Sprintf("artifactory_%s_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 BuildWebhookResourceModel struct { + WebhookResourceModel +} + +type BuildWebhookResource struct { + WebhookResource +} + +func (r *BuildWebhookResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + r.WebhookResource.Metadata(ctx, req, resp) } -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, +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": { - Type: schema.TypeSet, + "selected_builds": schema.SetAttribute{ + ElementType: types.StringType, Required: true, - Elem: &schema.Schema{Type: schema.TypeString}, Description: "Trigger on this list of build IDs", }, - }), - }, - Description: "Specifies where the webhook will be applied on which builds.", + }, + ), + }, + 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) 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) + criteriaAttrs := criteriaObj.Attributes() + + anyBuild := criteriaAttrs["any_build"].(types.Bool).ValueBool() + + if !anyBuild && len(criteriaAttrs["selected_builds"].(types.Set).Elements()) == 0 && len(criteriaAttrs["include_patterns"].(types.Set).Elements()) == 0 { + resp.Diagnostics.AddAttributeError( + path.Root("criteria").AtSetValue(criteriaObj).AtName("any_build"), + "Invalid Attribute Configuration", + "selected_builds or include_patterns cannot be empty when any_build is false", + ) + } +} + +func (r *BuildWebhookResource) 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 BuildWebhookResourceModel + + // Read Terraform plan data into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + var webhook WebhookAPIModel + resp.Diagnostics.Append(plan.toAPIModel(ctx, r.Domain, &webhook)...) + if resp.Diagnostics.HasError() { + return + } + + r.WebhookResource.Create(ctx, webhook, req, resp) + if resp.Diagnostics.HasError() { + return + } + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *BuildWebhookResource) 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 BuildWebhookResourceModel + + // Read Terraform state data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + var webhook WebhookAPIModel + found := r.WebhookResource.Read(ctx, state.Key.ValueString(), &webhook, req, resp) + if resp.Diagnostics.HasError() { + return + } + + if !found { + return + } + + resp.Diagnostics.Append(state.fromAPIModel(ctx, webhook, state.Handlers)...) + if resp.Diagnostics.HasError() { + return + } + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *BuildWebhookResource) 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 BuildWebhookResourceModel + + // Read Terraform plan data into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + var webhook WebhookAPIModel + resp.Diagnostics.Append(plan.toAPIModel(ctx, r.Domain, &webhook)...) + if resp.Diagnostics.HasError() { + return + } + + r.WebhookResource.Update(ctx, plan.Key.ValueString(), webhook, req, resp) + if resp.Diagnostics.HasError() { + return + } + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) } -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{})), +func (r *BuildWebhookResource) 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(), req, resp) + if resp.Diagnostics.HasError() { + return } + + // If the logic reaches here, it implicitly succeeded and will remove + // the resource from state if there are no other errors. } -var unpackBuildCriteria = func(terraformCriteria map[string]interface{}, baseCriteria BaseWebhookCriteria) interface{} { - return BuildWebhookCriteria{ - AnyBuild: terraformCriteria["any_build"].(bool), - SelectedBuilds: utilsdk.CastToStringArr(terraformCriteria["selected_builds"].(*schema.Set).List()), - BaseWebhookCriteria: baseCriteria, +// ImportState imports the resource into the Terraform state. +func (r *BuildWebhookResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + 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) + if d.HasError() { + diags.Append(d...) + } + + var selectedBuilds []string + d = critieriaAttrs["selected_builds"].(types.Set).ElementsAs(ctx, &selectedBuilds, false) + if d.HasError() { + diags.Append(d...) } + + criteriaAPIModel := BuildCriteriaAPIModel{ + BaseCriteriaAPIModel: baseCriteria, + AnyBuild: critieriaAttrs["any_build"].(types.Bool).ValueBool(), + SelectedBuilds: selectedBuilds, + } + + d = m.WebhookResourceModel.toAPIModel(ctx, domain, criteriaAPIModel, apiModel) + if d.HasError() { + diags.Append(d...) + } + + return } -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() +var buildCriteriaSetResourceModelAttributeTypes = lo.Assign( + patternsCriteriaSetResourceModelAttributeTypes, + map[string]attr.Type{ + "any_build": types.BoolType, + "selected_builds": types.SetType{ElemType: types.StringType}, + }, +) + +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) - if !anyBuild && (len(selectedBuilds) == 0 && len(includePatterns) == 0) { - return fmt.Errorf("selected_builds or include_patterns cannot be empty when any_build is false") + selectedBuilds := types.SetNull(types.StringType) + if v, ok := criteriaAPIModel["selectedBuilds"]; ok && v != nil { + sb, d := types.SetValueFrom(ctx, types.StringType, v) + if d.HasError() { + diags.Append(d...) + } + + selectedBuilds = sb + } + + criteria, d := types.ObjectValue( + buildCriteriaSetResourceModelAttributeTypes, + lo.Assign( + baseCriteriaAttrs, + map[string]attr.Value{ + "any_build": types.BoolValue(criteriaAPIModel["anyBuild"].(bool)), + "selected_builds": selectedBuilds, + }, + ), + ) + if d.HasError() { + diags.Append(d...) + } + criteriaSet, d := types.SetValue( + buildCriteriaSetResourceModelElementTypes, + []attr.Value{criteria}, + ) + if d.HasError() { + diags.Append(d...) } - return nil + d = m.WebhookResourceModel.fromAPIModel(ctx, apiModel, stateHandlers, &criteriaSet) + if d.HasError() { + diags.Append(d...) + } + + return diags +} + +type BuildCriteriaAPIModel struct { + BaseCriteriaAPIModel + AnyBuild bool `json:"anyBuild"` + SelectedBuilds []string `json:"selectedBuilds"` } 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 809b19764..df4d6c38d 100644 --- a/pkg/artifactory/resource/webhook/resource_artifactory_webhook_release_bundle.go +++ b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_release_bundle.go @@ -4,67 +4,324 @@ import ( "context" "fmt" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - utilsdk "github.com/jfrog/terraform-provider-shared/util/sdk" + "github.com/hashicorp/terraform-plugin-framework-validators/setvalidator" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + "github.com/jfrog/terraform-provider-shared/util" + "github.com/samber/lo" ) -type ReleaseBundleWebhookCriteria struct { - BaseWebhookCriteria - AnyReleaseBundle bool `json:"anyReleaseBundle"` - RegisteredReleaseBundlesNames []string `json:"registeredReleaseBundlesNames"` +var _ resource.Resource = &ReleaseBundleWebhookResource{} + +func NewArtifactoryReleaseBundleWebhookResource() resource.Resource { + return &ReleaseBundleWebhookResource{ + WebhookResource: WebhookResource{ + TypeName: fmt.Sprintf("artifactory_%s_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 NewDestinationWebhookResource() resource.Resource { + return &ReleaseBundleWebhookResource{ + WebhookResource: WebhookResource{ + TypeName: fmt.Sprintf("artifactory_%s_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 NewDistributionWebhookResource() resource.Resource { + return &ReleaseBundleWebhookResource{ + WebhookResource: WebhookResource{ + TypeName: fmt.Sprintf("artifactory_%s_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.:", + }, + } } -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, +func NewReleaseBundleWebhookResource() resource.Resource { + return &ReleaseBundleWebhookResource{ + WebhookResource: WebhookResource{ + TypeName: fmt.Sprintf("artifactory_%s_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 ReleaseBundleWebhookResourceModel struct { + WebhookResourceModel +} + +type ReleaseBundleWebhookResource struct { + WebhookResource +} + +func (r *ReleaseBundleWebhookResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + 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": { - Type: schema.TypeSet, + "registered_release_bundle_names": schema.SetAttribute{ + ElementType: types.StringType, Required: true, - Elem: &schema.Schema{Type: schema.TypeString}, Description: "Trigger on this list of release bundle names", }, - }), - }, - Description: "Specifies where the webhook will be applied, on which release bundles or distributions.", + }, + ), + }, + 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) + if r.Domain == ReleaseBundleDomain { + resp.Schema.DeprecationMessage = "This resource is being deprecated and replaced by artifactory_destination_webhook resource" + } +} + +func (r *ReleaseBundleWebhookResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + 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) + criteriaAttrs := criteriaObj.Attributes() + + anyReleaseBundle := criteriaAttrs["any_release_bundle"].(types.Bool).ValueBool() + + if !anyReleaseBundle && len(criteriaAttrs["registered_release_bundle_names"].(types.Set).Elements()) == 0 { + resp.Diagnostics.AddAttributeError( + path.Root("criteria").AtSetValue(criteriaObj).AtName("any_release_bundle"), + "Invalid Attribute Configuration", + "registered_release_bundle_names cannot be empty when any_release_bundle is false", + ) + } +} + +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) + + var plan ReleaseBundleWebhookResourceModel + + // Read Terraform plan data into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + var webhook WebhookAPIModel + resp.Diagnostics.Append(plan.toAPIModel(ctx, r.Domain, &webhook)...) + if resp.Diagnostics.HasError() { + return + } + + r.WebhookResource.Create(ctx, webhook, req, resp) + if resp.Diagnostics.HasError() { + return + } + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *ReleaseBundleWebhookResource) 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 ReleaseBundleWebhookResourceModel + + // Read Terraform state data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + var webhook WebhookAPIModel + found := r.WebhookResource.Read(ctx, state.Key.ValueString(), &webhook, req, resp) + if resp.Diagnostics.HasError() { + return + } + + if !found { + return + } + + resp.Diagnostics.Append(state.fromAPIModel(ctx, webhook, state.Handlers)...) + if resp.Diagnostics.HasError() { + return + } + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) } -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{})), +func (r *ReleaseBundleWebhookResource) 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 ReleaseBundleWebhookResourceModel + + // Read Terraform plan data into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return } + + var webhook WebhookAPIModel + resp.Diagnostics.Append(plan.toAPIModel(ctx, r.Domain, &webhook)...) + if resp.Diagnostics.HasError() { + return + } + + r.WebhookResource.Update(ctx, plan.Key.ValueString(), webhook, req, resp) + if resp.Diagnostics.HasError() { + return + } + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *ReleaseBundleWebhookResource) 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 ReleaseBundleWebhookResourceModel + + // Read Terraform prior state data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + + r.WebhookResource.Delete(ctx, state.Key.ValueString(), req, resp) + if resp.Diagnostics.HasError() { + return + } + + // If the logic reaches here, it implicitly succeeded and will remove + // the resource from state if there are no other errors. +} + +// ImportState imports the resource into the Terraform state. +func (r *ReleaseBundleWebhookResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + r.WebhookResource.ImportState(ctx, req, resp) } -var unpackReleaseBundleCriteria = func(terraformCriteria map[string]interface{}, baseCriteria BaseWebhookCriteria) interface{} { - return ReleaseBundleWebhookCriteria{ - AnyReleaseBundle: terraformCriteria["any_release_bundle"].(bool), - RegisteredReleaseBundlesNames: utilsdk.CastToStringArr(terraformCriteria["registered_release_bundle_names"].(*schema.Set).List()), - BaseWebhookCriteria: baseCriteria, +func (m ReleaseBundleWebhookResourceModel) 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...) + } + + var releaseBundleNames []string + d = critieriaAttrs["registered_release_bundle_names"].(types.Set).ElementsAs(ctx, &releaseBundleNames, false) + if d.HasError() { + diags.Append(d...) } + + criteriaAPIModel := ReleaseBundleCriteriaAPIModel{ + BaseCriteriaAPIModel: baseCriteria, + AnyReleaseBundle: critieriaAttrs["any_release_bundle"].(types.Bool).ValueBool(), + RegisteredReleaseBundlesNames: releaseBundleNames, + } + + d = m.WebhookResourceModel.toAPIModel(ctx, domain, criteriaAPIModel, apiModel) + if d.HasError() { + diags.Append(d...) + } + + return +} + +var releaseBundleCriteriaSetResourceModelAttributeTypes = lo.Assign( + patternsCriteriaSetResourceModelAttributeTypes, + map[string]attr.Type{ + "any_release_bundle": types.BoolType, + "registered_release_bundle_names": types.SetType{ElemType: types.StringType}, + }, +) + +var releaseBundleCriteriaSetResourceModelElementTypes = types.ObjectType{ + AttrTypes: releaseBundleCriteriaSetResourceModelAttributeTypes, } -var releaseBundleCriteriaValidation = func(ctx context.Context, criteria map[string]interface{}) error { - tflog.Debug(ctx, "releaseBundleCriteriaValidation") +func (m *ReleaseBundleWebhookResourceModel) fromAPIModel(ctx context.Context, apiModel WebhookAPIModel, stateHandlers basetypes.SetValue) diag.Diagnostics { + diags := diag.Diagnostics{} - anyReleaseBundle := criteria["any_release_bundle"].(bool) - registeredReleaseBundlesNames := criteria["registered_release_bundle_names"].(*schema.Set).List() + criteriaAPIModel := apiModel.EventFilter.Criteria.(map[string]interface{}) - if !anyReleaseBundle && len(registeredReleaseBundlesNames) == 0 { - return fmt.Errorf("registered_release_bundle_names cannot be empty when any_release_bundle is false") + baseCriteriaAttrs, d := m.WebhookResourceModel.fromBaseCriteriaAPIModel(ctx, criteriaAPIModel) + + releaseBundleNames := types.SetNull(types.StringType) + if v, ok := criteriaAPIModel["registeredReleaseBundlesNames"]; ok && v != nil { + rb, d := types.SetValueFrom(ctx, types.StringType, v) + if d.HasError() { + diags.Append(d...) + } + + releaseBundleNames = rb + } + + criteria, d := types.ObjectValue( + releaseBundleCriteriaSetResourceModelAttributeTypes, + lo.Assign( + baseCriteriaAttrs, + map[string]attr.Value{ + "any_release_bundle": types.BoolValue(criteriaAPIModel["anyReleaseBundles"].(bool)), + "registered_release_bundle_names": releaseBundleNames, + }, + ), + ) + if d.HasError() { + diags.Append(d...) + } + criteriaSet, d := types.SetValue( + releaseBundleCriteriaSetResourceModelElementTypes, + []attr.Value{criteria}, + ) + if d.HasError() { + diags.Append(d...) } - return nil + d = m.WebhookResourceModel.fromAPIModel(ctx, apiModel, stateHandlers, &criteriaSet) + if d.HasError() { + diags.Append(d...) + } + + return diags +} + +type ReleaseBundleCriteriaAPIModel struct { + BaseCriteriaAPIModel + AnyReleaseBundle bool `json:"anyReleaseBundle"` + RegisteredReleaseBundlesNames []string `json:"registeredReleaseBundlesNames"` } 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 25cd9ff4b..d848055f2 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 @@ -4,67 +4,290 @@ import ( "context" "fmt" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - utilsdk "github.com/jfrog/terraform-provider-shared/util/sdk" + "github.com/hashicorp/terraform-plugin-framework-validators/setvalidator" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + "github.com/jfrog/terraform-provider-shared/util" + "github.com/samber/lo" ) -type ReleaseBundleV2WebhookCriteria struct { - BaseWebhookCriteria - AnyReleaseBundle bool `json:"anyReleaseBundle"` - SelectedReleaseBundles []string `json:"selectedReleaseBundles"` +var _ resource.Resource = &ReleaseBundleV2WebhookResource{} + +func NewReleaseBundleV2WebhookResource() resource.Resource { + return &ReleaseBundleV2WebhookResource{ + WebhookResource: WebhookResource{ + TypeName: fmt.Sprintf("artifactory_%s_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 ReleaseBundleV2WebhookResourceModel struct { + WebhookResourceModel +} + +type ReleaseBundleV2WebhookResource struct { + WebhookResource +} + +func (r *ReleaseBundleV2WebhookResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + r.WebhookResource.Metadata(ctx, req, resp) } -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, +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: "Trigger on any release bundles or distributions", + Description: "Includes all existing release bundles and any future release bundles.", }, - "selected_release_bundles": { - Type: schema.TypeSet, + "selected_release_bundles": schema.SetAttribute{ + ElementType: types.StringType, Required: true, - Elem: &schema.Schema{Type: schema.TypeString}, Description: "Trigger on this list of release bundle names", }, - }), - }, - Description: "Specifies where the webhook will be applied, on which release bundles or distributions.", + }, + ), + }, + 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) 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) + criteriaAttrs := criteriaObj.Attributes() + + anyReleaseBundle := criteriaAttrs["any_release_bundle"].(types.Bool).ValueBool() + + if !anyReleaseBundle && len(criteriaAttrs["selected_release_bundles"].(types.Set).Elements()) == 0 { + resp.Diagnostics.AddAttributeError( + path.Root("criteria").AtSetValue(criteriaObj).AtName("any_release_bundle"), + "Invalid Attribute Configuration", + "selected_release_bundles cannot be empty when any_release_bundle is false", + ) + } } -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{})), +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) + + var plan ReleaseBundleV2WebhookResourceModel + + // Read Terraform plan data into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return } + + var webhook WebhookAPIModel + resp.Diagnostics.Append(plan.toAPIModel(ctx, r.Domain, &webhook)...) + if resp.Diagnostics.HasError() { + return + } + + r.WebhookResource.Create(ctx, webhook, req, resp) + if resp.Diagnostics.HasError() { + return + } + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) } -var unpackReleaseBundleV2Criteria = func(terraformCriteria map[string]interface{}, baseCriteria BaseWebhookCriteria) interface{} { - return ReleaseBundleV2WebhookCriteria{ - AnyReleaseBundle: terraformCriteria["any_release_bundle"].(bool), - SelectedReleaseBundles: utilsdk.CastToStringArr(terraformCriteria["selected_release_bundles"].(*schema.Set).List()), - BaseWebhookCriteria: baseCriteria, +func (r *ReleaseBundleV2WebhookResource) 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 ReleaseBundleV2WebhookResourceModel + + // Read Terraform state data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + var webhook WebhookAPIModel + found := r.WebhookResource.Read(ctx, state.Key.ValueString(), &webhook, req, resp) + if resp.Diagnostics.HasError() { + return } + + if !found { + return + } + + resp.Diagnostics.Append(state.fromAPIModel(ctx, webhook, state.Handlers)...) + if resp.Diagnostics.HasError() { + return + } + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) } -var releaseBundleV2CriteriaValidation = func(ctx context.Context, criteria map[string]interface{}) error { - tflog.Debug(ctx, "releaseBundleV2CriteriaValidation") +func (r *ReleaseBundleV2WebhookResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + go util.SendUsageResourceUpdate(ctx, r.ProviderData.Client.R(), r.ProviderData.ProductId, r.TypeName) - anyReleaseBundle := criteria["any_release_bundle"].(bool) - selectedReleaseBundles := criteria["selected_release_bundles"].(*schema.Set).List() + var plan ReleaseBundleV2WebhookResourceModel - if !anyReleaseBundle && len(selectedReleaseBundles) == 0 { - return fmt.Errorf("selected_release_bundles cannot be empty when any_release_bundle is false") + // Read Terraform plan data into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return } - return nil + var webhook WebhookAPIModel + resp.Diagnostics.Append(plan.toAPIModel(ctx, r.Domain, &webhook)...) + if resp.Diagnostics.HasError() { + return + } + + r.WebhookResource.Update(ctx, plan.Key.ValueString(), webhook, req, resp) + if resp.Diagnostics.HasError() { + return + } + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *ReleaseBundleV2WebhookResource) 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 ReleaseBundleV2WebhookResourceModel + + // Read Terraform prior state data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + + r.WebhookResource.Delete(ctx, state.Key.ValueString(), req, resp) + if resp.Diagnostics.HasError() { + return + } + + // If the logic reaches here, it implicitly succeeded and will remove + // the resource from state if there are no other errors. +} + +// ImportState imports the resource into the Terraform state. +func (r *ReleaseBundleV2WebhookResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + 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) + if d.HasError() { + diags.Append(d...) + } + + var releaseBundleNames []string + d = critieriaAttrs["selected_release_bundles"].(types.Set).ElementsAs(ctx, &releaseBundleNames, false) + if d.HasError() { + diags.Append(d...) + } + + criteriaAPIModel := ReleaseBundleV2CriteriaAPIModel{ + BaseCriteriaAPIModel: baseCriteria, + AnyReleaseBundle: critieriaAttrs["any_release_bundle"].(types.Bool).ValueBool(), + SelectedReleaseBundles: releaseBundleNames, + } + + d = m.WebhookResourceModel.toAPIModel(ctx, domain, criteriaAPIModel, apiModel) + if d.HasError() { + diags.Append(d...) + } + + return +} + +var releaseBundleV2CriteriaSetResourceModelAttributeTypes = lo.Assign( + patternsCriteriaSetResourceModelAttributeTypes, + map[string]attr.Type{ + "any_release_bundle": types.BoolType, + "selected_release_bundles": types.SetType{ElemType: types.StringType}, + }, +) + +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) + + releaseBundleNames := types.SetNull(types.StringType) + if v, ok := criteriaAPIModel["selectedReleaseBundles"]; ok && v != nil { + rb, d := types.SetValueFrom(ctx, types.StringType, v) + if d.HasError() { + diags.Append(d...) + } + + releaseBundleNames = rb + } + + criteria, d := types.ObjectValue( + releaseBundleV2CriteriaSetResourceModelAttributeTypes, + lo.Assign( + baseCriteriaAttrs, + map[string]attr.Value{ + "any_release_bundle": types.BoolValue(criteriaAPIModel["anyReleaseBundles"].(bool)), + "selected_release_bundles": releaseBundleNames, + }, + ), + ) + if d.HasError() { + diags.Append(d...) + } + criteriaSet, d := types.SetValue( + releaseBundleV2CriteriaSetResourceModelElementTypes, + []attr.Value{criteria}, + ) + if d.HasError() { + diags.Append(d...) + } + + d = m.WebhookResourceModel.fromAPIModel(ctx, apiModel, stateHandlers, &criteriaSet) + if d.HasError() { + diags.Append(d...) + } + + return diags +} + +type ReleaseBundleV2CriteriaAPIModel struct { + BaseCriteriaAPIModel + AnyReleaseBundle bool `json:"anyReleaseBundle"` + SelectedReleaseBundles []string `json:"selectedReleaseBundles"` } 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 e55c37559..25ea5dbf9 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 @@ -1,43 +1,255 @@ package webhook import ( - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - utilsdk "github.com/jfrog/terraform-provider-shared/util/sdk" + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework-validators/setvalidator" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + "github.com/jfrog/terraform-provider-shared/util" + "github.com/samber/lo" ) -type ReleaseBundleV2PromotionWebhookCriteria struct { - SelectedEnvironments []string `json:"selectedEnvironments"` +var _ resource.Resource = &ReleaseBundleV2WebhookResource{} + +func NewReleaseBundleV2PromotionWebhookResource() resource.Resource { + return &ReleaseBundleV2PromotionWebhookResource{ + WebhookResource: WebhookResource{ + TypeName: fmt.Sprintf("artifactory_%s_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 ReleaseBundleV2PromotionWebhookResourceModel struct { + WebhookResourceModel +} + +type ReleaseBundleV2PromotionWebhookResource struct { + WebhookResource +} + +func (r *ReleaseBundleV2PromotionWebhookResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + r.WebhookResource.Metadata(ctx, req, resp) } -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, +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, - Elem: &schema.Schema{Type: schema.TypeString}, Description: "Trigger on this list of environments", }, - }), - }, - Description: "Specifies where the webhook will be applied, on which release bundles promotion.", + }, + ), + }, + 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 *ReleaseBundleV2PromotionWebhookResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + r.WebhookResource.Configure(ctx, req, resp) +} + +func (r *ReleaseBundleV2PromotionWebhookResource) 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 ReleaseBundleV2PromotionWebhookResourceModel + + // Read Terraform plan data into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + var webhook WebhookAPIModel + resp.Diagnostics.Append(plan.toAPIModel(ctx, r.Domain, &webhook)...) + if resp.Diagnostics.HasError() { + return + } + + r.WebhookResource.Create(ctx, webhook, req, resp) + if resp.Diagnostics.HasError() { + return + } + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *ReleaseBundleV2PromotionWebhookResource) 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 ReleaseBundleV2PromotionWebhookResourceModel + + // Read Terraform state data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + var webhook WebhookAPIModel + found := r.WebhookResource.Read(ctx, state.Key.ValueString(), &webhook, req, resp) + if resp.Diagnostics.HasError() { + return + } + + if !found { + return + } + + resp.Diagnostics.Append(state.fromAPIModel(ctx, webhook, state.Handlers)...) + if resp.Diagnostics.HasError() { + return + } + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *ReleaseBundleV2PromotionWebhookResource) 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 ReleaseBundleV2PromotionWebhookResourceModel + + // Read Terraform plan data into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + var webhook WebhookAPIModel + resp.Diagnostics.Append(plan.toAPIModel(ctx, r.Domain, &webhook)...) + if resp.Diagnostics.HasError() { + return + } + + r.WebhookResource.Update(ctx, plan.Key.ValueString(), webhook, req, resp) + if resp.Diagnostics.HasError() { + return + } + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) } -var packReleaseBundleV2PromotionCriteria = func(artifactoryCriteria map[string]interface{}) map[string]interface{} { - return map[string]interface{}{ - "selected_environments": schema.NewSet(schema.HashString, artifactoryCriteria["selectedEnvironments"].([]interface{})), +func (r *ReleaseBundleV2PromotionWebhookResource) 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 ReleaseBundleV2PromotionWebhookResourceModel + + // Read Terraform prior state data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + + r.WebhookResource.Delete(ctx, state.Key.ValueString(), req, resp) + if resp.Diagnostics.HasError() { + return } + + // If the logic reaches here, it implicitly succeeded and will remove + // the resource from state if there are no other errors. +} + +// ImportState imports the resource into the Terraform state. +func (r *ReleaseBundleV2PromotionWebhookResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + r.WebhookResource.ImportState(ctx, req, resp) } -var unpackReleaseBundleV2PromotionCriteria = func(terraformCriteria map[string]interface{}, baseCriteria BaseWebhookCriteria) interface{} { - return ReleaseBundleV2PromotionWebhookCriteria{ - SelectedEnvironments: utilsdk.CastToStringArr(terraformCriteria["selected_environments"].(*schema.Set).List()), +func (m ReleaseBundleV2PromotionWebhookResourceModel) toAPIModel(ctx context.Context, domain string, apiModel *WebhookAPIModel) (diags diag.Diagnostics) { + critieriaObj := m.Criteria.Elements()[0].(types.Object) + critieriaAttrs := critieriaObj.Attributes() + + var environments []string + d := critieriaAttrs["selected_environments"].(types.Set).ElementsAs(ctx, &environments, false) + if d.HasError() { + diags.Append(d...) + } + + criteriaAPIModel := ReleaseBundleV2PromotionCriteriaAPIModel{ + SelectedEnvironments: environments, + } + + d = m.WebhookResourceModel.toAPIModel(ctx, domain, criteriaAPIModel, apiModel) + if d.HasError() { + diags.Append(d...) } + + return +} + +var releaseBundleV2PromotionCriteriaSetResourceModelAttributeTypes = lo.Assign( + patternsCriteriaSetResourceModelAttributeTypes, + map[string]attr.Type{ + "selected_environments": types.SetType{ElemType: types.StringType}, + }, +) + +var releaseBundleV2PromotionCriteriaSetResourceModelElementTypes = types.ObjectType{ + 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) + + releaseBundleNames := types.SetNull(types.StringType) + if v, ok := criteriaAPIModel["selectedEnvironments"]; ok && v != nil { + rb, d := types.SetValueFrom(ctx, types.StringType, v) + if d.HasError() { + diags.Append(d...) + } + + releaseBundleNames = rb + } + + criteria, d := types.ObjectValue( + releaseBundleV2PromotionCriteriaSetResourceModelAttributeTypes, + lo.Assign( + baseCriteriaAttrs, + map[string]attr.Value{ + "selected_environments": releaseBundleNames, + }, + ), + ) + if d.HasError() { + diags.Append(d...) + } + criteriaSet, d := types.SetValue( + releaseBundleV2PromotionCriteriaSetResourceModelElementTypes, + []attr.Value{criteria}, + ) + if d.HasError() { + diags.Append(d...) + } + + d = m.WebhookResourceModel.fromAPIModel(ctx, apiModel, stateHandlers, &criteriaSet) + if d.HasError() { + diags.Append(d...) + } + + return diags +} + +type ReleaseBundleV2PromotionCriteriaAPIModel struct { + SelectedEnvironments []string `json:"selectedEnvironments"` } diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_webhook_release_bundle_v2_promotion_test.go b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_release_bundle_v2_promotion_test.go index 455bff89b..aa213e7a4 100644 --- a/pkg/artifactory/resource/webhook/resource_artifactory_webhook_release_bundle_v2_promotion_test.go +++ b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_release_bundle_v2_promotion_test.go @@ -1,22 +1,95 @@ 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" - "github.com/jfrog/terraform-provider-shared/validator" ) +func TestAccWebhook_ReleaseBundleV2Promotion_UpgradeFromSDKv2(t *testing.T) { + _, fqrn, name := testutil.MkNames("test-release-bundle-v2-promotion", "artifactory_release_bundle_v2_promotion_webhook") + + params := map[string]interface{}{ + "webhookName": name, + } + config := util.ExecuteTemplate("TestAccWebhook_User", ` + resource "artifactory_release_bundle_v2_promotion_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" + secret = "fake-secret" + use_secret_for_signing = true + custom_http_headers = { + header-1 = "value-1" + header-2 = "value-2" + } + } + } + `, 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, "key", name), + 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.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"), + ), + }, + { + Config: config, + ProtoV6ProviderFactories: acctest.ProtoV6ProviderFactories, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectEmptyPlan(), + }, + }, + }, + }, + }) +} + func TestAccWebhook_ReleaseBundleV2Promotion(t *testing.T) { _, fqrn, name := testutil.MkNames("test-release-bundle-v2-promotion", "artifactory_release_bundle_v2_promotion_webhook") params := map[string]interface{}{ - "webhookName": name, - "useSecretForSigning": testutil.RandBool(), + "webhookName": name, } webhookConfig := util.ExecuteTemplate("TestAccWebhook_User", ` resource "artifactory_release_bundle_v2_promotion_webhook" "{{ .webhookName }}" { @@ -36,7 +109,7 @@ func TestAccWebhook_ReleaseBundleV2Promotion(t *testing.T) { handler { url = "https://google.com" secret = "fake-secret" - use_secret_for_signing = {{ .useSecretForSigning }} + use_secret_for_signing = true custom_http_headers = { header-1 = "value-1" header-2 = "value-2" @@ -46,9 +119,9 @@ func TestAccWebhook_ReleaseBundleV2Promotion(t *testing.T) { `, params) resource.Test(t, resource.TestCase{ - PreCheck: func() { acctest.PreCheck(t) }, - ProviderFactories: acctest.ProviderFactories, - CheckDestroy: acctest.VerifyDeleted(fqrn, "", acctest.CheckRepo), + PreCheck: func() { acctest.PreCheck(t) }, + ProtoV6ProviderFactories: acctest.ProtoV6ProviderFactories, + CheckDestroy: acctest.VerifyDeleted(fqrn, "key", acctest.CheckRepo), Steps: []resource.TestStep{ { @@ -63,18 +136,19 @@ func TestAccWebhook_ReleaseBundleV2Promotion(t *testing.T) { resource.TestCheckResourceAttr(fqrn, "handler.#", "1"), 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", fmt.Sprintf("%t", params["useSecretForSigning"])), + 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"), ), }, { - ResourceName: fqrn, - ImportState: true, - ImportStateVerify: true, - ImportStateCheck: validator.CheckImportState(name, "key"), - ImportStateVerifyIgnore: []string{"handler.0.secret"}, + ResourceName: fqrn, + ImportState: true, + ImportStateId: name, + ImportStateVerify: true, + ImportStateVerifyIdentifierAttribute: "key", + ImportStateVerifyIgnore: []string{"handler.0.secret"}, }}, }) } diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_webhook_repo.go b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_repo.go index 93e9e3547..fc607bb47 100644 --- a/pkg/artifactory/resource/webhook/resource_artifactory_webhook_repo.go +++ b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_repo.go @@ -4,91 +4,328 @@ import ( "context" "fmt" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - utilsdk "github.com/jfrog/terraform-provider-shared/util/sdk" + "github.com/hashicorp/terraform-plugin-framework-validators/setvalidator" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + "github.com/jfrog/terraform-provider-shared/util" + "github.com/samber/lo" ) -type RepoWebhookCriteria struct { - BaseWebhookCriteria - AnyLocal bool `json:"anyLocal"` - AnyRemote bool `json:"anyRemote"` - AnyFederated bool `json:"anyFederated"` - RepoKeys []string `json:"repoKeys"` +var _ resource.Resource = &RepoWebhookResource{} + +func NewArtifactWebhookResource() resource.Resource { + return &RepoWebhookResource{ + WebhookResource: WebhookResource{ + TypeName: fmt.Sprintf("artifactory_%s_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 NewArtifactPropertyWebhookResource() resource.Resource { + return &RepoWebhookResource{ + WebhookResource: WebhookResource{ + TypeName: fmt.Sprintf("artifactory_%s_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 NewDockerWebhookResource() resource.Resource { + return &RepoWebhookResource{ + WebhookResource: WebhookResource{ + TypeName: fmt.Sprintf("artifactory_%s_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 RepoWebhookResourceModel struct { + WebhookResourceModel +} + +type RepoWebhookResource struct { + WebhookResource +} + +func (r *RepoWebhookResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + r.WebhookResource.Metadata(ctx, req, resp) } -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, +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": { - Type: schema.TypeBool, + "any_remote": schema.BoolAttribute{ Required: true, Description: "Trigger on any remote repositories", }, - "any_federated": { - Type: schema.TypeBool, + "any_federated": schema.BoolAttribute{ Required: true, Description: "Trigger on any federated repositories", }, - "repo_keys": { - Type: schema.TypeSet, + "repo_keys": schema.SetAttribute{ + ElementType: types.StringType, 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.", + }, + ), + }, + 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) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + r.WebhookResource.Configure(ctx, req, resp) +} + +func (r RepoWebhookResource) 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 *RepoWebhookResource) 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 RepoWebhookResourceModel + + // Read Terraform plan data into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + var webhook WebhookAPIModel + resp.Diagnostics.Append(plan.toAPIModel(ctx, r.Domain, &webhook)...) + if resp.Diagnostics.HasError() { + return + } + + r.WebhookResource.Create(ctx, webhook, req, resp) + if resp.Diagnostics.HasError() { + return + } + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *RepoWebhookResource) 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 RepoWebhookResourceModel + + // Read Terraform state data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + var webhook WebhookAPIModel + found := r.WebhookResource.Read(ctx, state.Key.ValueString(), &webhook, req, resp) + if resp.Diagnostics.HasError() { + return + } + + if !found { + return + } + + resp.Diagnostics.Append(state.fromAPIModel(ctx, webhook, state.Handlers)...) + if resp.Diagnostics.HasError() { + return + } + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) } -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{})), +func (r *RepoWebhookResource) 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 RepoWebhookResourceModel + + // Read Terraform plan data into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + var webhook WebhookAPIModel + resp.Diagnostics.Append(plan.toAPIModel(ctx, r.Domain, &webhook)...) + if resp.Diagnostics.HasError() { + return + } + + r.WebhookResource.Update(ctx, plan.Key.ValueString(), webhook, req, resp) + if resp.Diagnostics.HasError() { + return } - if v, ok := artifactoryCriteria["anyFederated"]; ok { - criteria["any_federated"] = v.(bool) + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *RepoWebhookResource) 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 RepoWebhookResourceModel + + // Read Terraform prior state data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + + r.WebhookResource.Delete(ctx, state.Key.ValueString(), req, resp) + if resp.Diagnostics.HasError() { + return } - return criteria + // 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 *RepoWebhookResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + r.WebhookResource.ImportState(ctx, req, resp) } -var unpackRepoCriteria = func(terraformCriteria map[string]interface{}, baseCriteria BaseWebhookCriteria) interface{} { - return RepoWebhookCriteria{ - AnyLocal: terraformCriteria["any_local"].(bool), - AnyRemote: terraformCriteria["any_remote"].(bool), - AnyFederated: terraformCriteria["any_federated"].(bool), - RepoKeys: utilsdk.CastToStringArr(terraformCriteria["repo_keys"].(*schema.Set).List()), - BaseWebhookCriteria: baseCriteria, +func (m RepoWebhookResourceModel) toAPIModel(ctx context.Context, domain string, apiModel *WebhookAPIModel) (diags diag.Diagnostics) { + critieriaObj := m.Criteria.Elements()[0].(types.Object) + critieriaAttrs := critieriaObj.Attributes() + + baseCriteria, d := m.WebhookResourceModel.toBaseCriteriaAPIModel(ctx, critieriaAttrs) + if d.HasError() { + diags.Append(d...) + } + + var repoKeys []string + d = critieriaAttrs["repo_keys"].(types.Set).ElementsAs(ctx, &repoKeys, false) + if d.HasError() { + diags.Append(d...) + } + + criteriaAPIModel := RepoCriteriaAPIModel{ + BaseCriteriaAPIModel: baseCriteria, + AnyLocal: critieriaAttrs["any_local"].(types.Bool).ValueBool(), + AnyRemote: critieriaAttrs["any_remote"].(types.Bool).ValueBool(), + AnyFederated: critieriaAttrs["any_federated"].(types.Bool).ValueBool(), + RepoKeys: repoKeys, } + + d = m.WebhookResourceModel.toAPIModel(ctx, domain, criteriaAPIModel, apiModel) + if d.HasError() { + diags.Append(d...) + } + + return } -var repoCriteriaValidation = func(ctx context.Context, criteria map[string]interface{}) error { - tflog.Debug(ctx, "repoCriteriaValidation") +var repoCriteriaSetResourceModelAttributeTypes = lo.Assign( + patternsCriteriaSetResourceModelAttributeTypes, + map[string]attr.Type{ + "any_local": types.BoolType, + "any_remote": types.BoolType, + "any_federated": types.BoolType, + "repo_keys": types.SetType{ElemType: types.StringType}, + }, +) + +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) - anyLocal := criteria["any_local"].(bool) - anyRemote := criteria["any_remote"].(bool) - anyFederated := criteria["any_federated"].(bool) - repoKeys := criteria["repo_keys"].(*schema.Set).List() + repoKeys := types.SetNull(types.StringType) + if v, ok := criteriaAPIModel["repoKeys"]; ok && v != nil { + ks, d := types.SetValueFrom(ctx, types.StringType, v) + if d.HasError() { + diags.Append(d...) + } - 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") + repoKeys = ks } - return nil + criteria, d := types.ObjectValue( + repoCriteriaSetResourceModelAttributeTypes, + lo.Assign( + baseCriteriaAttrs, + map[string]attr.Value{ + "any_local": types.BoolValue(criteriaAPIModel["anyLocal"].(bool)), + "any_remote": types.BoolValue(criteriaAPIModel["anyRemote"].(bool)), + "any_federated": types.BoolValue(criteriaAPIModel["anyFederated"].(bool)), + "repo_keys": repoKeys, + }, + ), + ) + if d.HasError() { + diags.Append(d...) + } + criteriaSet, d := types.SetValue( + repoCriteriaSetResourceModelElementTypes, + []attr.Value{criteria}, + ) + if d.HasError() { + diags.Append(d...) + } + + d = m.WebhookResourceModel.fromAPIModel(ctx, apiModel, stateHandlers, &criteriaSet) + if d.HasError() { + diags.Append(d...) + } + + return diags +} + +type RepoCriteriaAPIModel struct { + BaseCriteriaAPIModel + AnyLocal bool `json:"anyLocal"` + AnyRemote bool `json:"anyRemote"` + AnyFederated bool `json:"anyFederated"` + RepoKeys []string `json:"repoKeys"` } diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_webhook_test.go b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_test.go index 7a7bbfb93..9e006741f 100644 --- a/pkg/artifactory/resource/webhook/resource_artifactory_webhook_test.go +++ b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_test.go @@ -1,21 +1,18 @@ package webhook_test import ( - "context" "fmt" - "reflect" "regexp" - "slices" "testing" "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" "github.com/jfrog/terraform-provider-shared/testutil" "github.com/jfrog/terraform-provider-shared/util" - "github.com/jfrog/terraform-provider-shared/validator" ) var domainRepoTypeLookup = map[string]string{ @@ -25,13 +22,13 @@ var domainRepoTypeLookup = map[string]string{ } var domainValidationErrorMessageLookup = map[string]string{ - "artifact": "repo_keys cannot be empty when any_local, any_remote, and any_federated are false", - "artifact_property": "repo_keys cannot be empty when any_local, any_remote, and any_federated are false", - "docker": "repo_keys cannot be empty when any_local, any_remote, and any_federated are false", - "build": "selected_builds or include_patterns cannot be empty when any_build is false", - "release_bundle": "registered_release_bundle_names cannot be empty when any_release_bundle is false", - "distribution": "registered_release_bundle_names cannot be empty when any_release_bundle is false", - "artifactory_release_bundle": "registered_release_bundle_names cannot be empty when any_release_bundle is false", + "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`, + "docker": `repo_keys cannot be empty when any_local, any_remote, and any_federated are\s*false`, + "build": `selected_builds or include_patterns cannot be empty when any_build is false`, + "release_bundle": `registered_release_bundle_names cannot be empty when any_release_bundle is\s*false`, + "distribution": `registered_release_bundle_names cannot be empty when any_release_bundle is\s*false`, + "artifactory_release_bundle": `registered_release_bundle_names cannot be empty when any_release_bundle is\s*false`, } var repoTemplate = ` @@ -97,12 +94,10 @@ var releaseBundleV2Template = ` ` func TestAccWebhook_CriteriaValidation(t *testing.T) { - for _, webhookType := range webhook.TypesSupported { - if !slices.Contains([]string{"user", "release_bundle_v2_promotion", "artifact_lifecycle"}, webhookType) { - t.Run(webhookType, func(t *testing.T) { - resource.Test(webhookCriteriaValidationTestCase(webhookType, 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(webhookCriteriaValidationTestCase(webhookType, t)) + }) } } @@ -131,10 +126,9 @@ func webhookCriteriaValidationTestCase(webhookType string, t *testing.T) (*testi webhookConfig := util.ExecuteTemplate("TestAccWebhookCriteriaValidation", template, params) return t, resource.TestCase{ - PreCheck: func() { acctest.PreCheck(t) }, - ProviderFactories: acctest.ProviderFactories, - CheckDestroy: acctest.VerifyDeleted(fqrn, "", acctest.CheckRepo), - + PreCheck: func() { acctest.PreCheck(t) }, + ProtoV6ProviderFactories: acctest.ProtoV6MuxProviderFactories, + CheckDestroy: acctest.VerifyDeleted(fqrn, "key", acctest.CheckRepo), Steps: []resource.TestStep{ { Config: webhookConfig, @@ -173,14 +167,13 @@ func TestAccWebhook_EventTypesValidation(t *testing.T) { `, params) resource.Test(t, resource.TestCase{ - PreCheck: func() { acctest.PreCheck(t) }, - ProviderFactories: acctest.ProviderFactories, - CheckDestroy: acctest.VerifyDeleted(fqrn, "", acctest.CheckRepo), - + PreCheck: func() { acctest.PreCheck(t) }, + ProtoV6ProviderFactories: acctest.ProtoV6MuxProviderFactories, + CheckDestroy: acctest.VerifyDeleted(fqrn, "key", acctest.CheckRepo), Steps: []resource.TestStep{ { Config: webhookConfig, - ExpectError: regexp.MustCompile(fmt.Sprintf("event_type %s not supported for domain artifact", wrongEventType)), + ExpectError: regexp.MustCompile(fmt.Sprintf(`value must be one of:\s*\["deployed" "deleted" "moved" "copied" "cached"\], got: "%s"`, wrongEventType)), }, }, }) @@ -213,14 +206,13 @@ func TestAccWebhook_HandlerValidation_EmptyProxy(t *testing.T) { `, params) resource.Test(t, resource.TestCase{ - PreCheck: func() { acctest.PreCheck(t) }, - ProviderFactories: acctest.ProviderFactories, - CheckDestroy: acctest.VerifyDeleted(fqrn, "", acctest.CheckRepo), - + PreCheck: func() { acctest.PreCheck(t) }, + ProtoV6ProviderFactories: acctest.ProtoV6MuxProviderFactories, + CheckDestroy: acctest.VerifyDeleted(fqrn, "key", acctest.CheckRepo), Steps: []resource.TestStep{ { Config: webhookConfig, - ExpectError: regexp.MustCompile(`expected "proxy" to not be an empty string`), + ExpectError: regexp.MustCompile(`proxy\s*string length must be at least 1, got: 0`), }, }, }) @@ -234,6 +226,7 @@ func TestAccWebhook_HandlerValidation_ProxyWithURL(t *testing.T) { params := map[string]interface{}{ "webhookName": name, } + webhookConfig := util.ExecuteTemplate("TestAccWebhookEventTypesValidation", ` resource "artifactory_artifact_webhook" "{{ .webhookName }}" { key = "{{ .webhookName }}" @@ -253,14 +246,13 @@ func TestAccWebhook_HandlerValidation_ProxyWithURL(t *testing.T) { `, params) resource.Test(t, resource.TestCase{ - PreCheck: func() { acctest.PreCheck(t) }, - ProviderFactories: acctest.ProviderFactories, - CheckDestroy: acctest.VerifyDeleted(fqrn, "", acctest.CheckRepo), - + PreCheck: func() { acctest.PreCheck(t) }, + ProtoV6ProviderFactories: acctest.ProtoV6MuxProviderFactories, + CheckDestroy: acctest.VerifyDeleted(fqrn, "key", acctest.CheckRepo), Steps: []resource.TestStep{ { Config: webhookConfig, - ExpectError: regexp.MustCompile(`expected "proxy" not to be a valid url, got https://google.com`), + ExpectError: regexp.MustCompile(`.*expected "proxy" not to be a valid url.*`), }, }, }) @@ -291,10 +283,9 @@ func TestAccWebhook_BuildWithIncludePatterns(t *testing.T) { `, params) resource.Test(t, resource.TestCase{ - PreCheck: func() { acctest.PreCheck(t) }, - ProviderFactories: acctest.ProviderFactories, - CheckDestroy: acctest.VerifyDeleted(fqrn, "", acctest.CheckRepo), - + PreCheck: func() { acctest.PreCheck(t) }, + ProtoV6ProviderFactories: acctest.ProtoV6MuxProviderFactories, + CheckDestroy: acctest.VerifyDeleted(fqrn, "key", acctest.CheckRepo), Steps: []resource.TestStep{ { Config: webhookConfig, @@ -327,15 +318,14 @@ func webhookTestCase(webhookType string, t *testing.T) (*testing.T, resource.Tes 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(), - "useSecretForSigning": testutil.RandBool(), + "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 }}" { @@ -357,7 +347,7 @@ func webhookTestCase(webhookType string, t *testing.T) (*testing.T, resource.Tes handler { url = "https://google.com" secret = "fake-secret" - use_secret_for_signing = {{ .useSecretForSigning }} + use_secret_for_signing = true custom_http_headers = { header-1 = "value-1" header-2 = "value-2" @@ -387,7 +377,6 @@ func webhookTestCase(webhookType string, t *testing.T) (*testing.T, resource.Tes handler { url = "https://google.com" secret = "fake-secret" - use_secret_for_signing = {{ .useSecretForSigning }} custom_http_headers = { header-1 = "value-1" header-2 = "value-2" @@ -414,12 +403,11 @@ func webhookTestCase(webhookType string, t *testing.T) (*testing.T, resource.Tes 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", fmt.Sprintf("%t", params["useSecretForSigning"])), + 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"), } @@ -436,12 +424,11 @@ func webhookTestCase(webhookType string, t *testing.T) (*testing.T, resource.Tes 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", fmt.Sprintf("%t", params["useSecretForSigning"])), + 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.secret", ""), resource.TestCheckResourceAttr(fqrn, "handler.1.custom_http_headers.#", "0"), } @@ -451,10 +438,9 @@ func webhookTestCase(webhookType string, t *testing.T) (*testing.T, resource.Tes } return t, resource.TestCase{ - PreCheck: func() { acctest.PreCheck(t) }, - ProviderFactories: acctest.ProviderFactories, - CheckDestroy: acctest.VerifyDeleted(fqrn, "", testCheckWebhook), - + PreCheck: func() { acctest.PreCheck(t) }, + ProtoV6ProviderFactories: acctest.ProtoV6MuxProviderFactories, + CheckDestroy: acctest.VerifyDeleted(fqrn, "key", testCheckWebhook), Steps: []resource.TestStep{ { Config: webhookConfig, @@ -465,11 +451,129 @@ func webhookTestCase(webhookType string, t *testing.T) (*testing.T, resource.Tes Check: resource.ComposeTestCheckFunc(updatedTestChecks...), }, { - ResourceName: fqrn, - ImportState: true, - ImportStateVerify: true, - ImportStateCheck: validator.CheckImportState(name, "key"), - ImportStateVerifyIgnore: []string{"handler.0.secret"}, + 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(), + }, + }, }, }, } @@ -479,13 +583,13 @@ func testCheckWebhook(id string, request *resty.Request) (*resty.Response, error return request. SetPathParam("webhookKey", id). AddRetryCondition(client.NeverRetry). - Get(webhook.WhUrl) + Get(webhook.WebhookURL) } func TestAccWebhook_GH476WebHookChangeBearerSet0(t *testing.T) { - _, fqrn, name := testutil.MkNames("foo", "artifactory_artifact_webhook") + _, fqrn, name := testutil.MkNames("test-webhook", "artifactory_artifact_webhook") - format := ` + temp := ` resource "artifactory_artifact_webhook" "{{ .webhookName }}" { key = "{{ .webhookName }}" @@ -511,7 +615,7 @@ func TestAccWebhook_GH476WebHookChangeBearerSet0(t *testing.T) { firstToken := testutil.RandomInt() config1 := util.ExecuteTemplate( "TestAccWebhook{{ .webhookName }}", - format, + temp, map[string]interface{}{ "webhookName": name, "token": firstToken, @@ -520,81 +624,34 @@ func TestAccWebhook_GH476WebHookChangeBearerSet0(t *testing.T) { secondToken := testutil.RandomInt() config2 := util.ExecuteTemplate( "TestAccWebhook{{ .webhookName }}", - format, + temp, map[string]interface{}{ "webhookName": name, "token": secondToken, }, ) - thirdToken := testutil.RandomInt() - config3 := util.ExecuteTemplate( - "TestAccWebhook{{ .webhookName }}", - format, - map[string]interface{}{ - "webhookName": name, - "token": thirdToken, - }, - ) - resource.Test(t, resource.TestCase{ - PreCheck: func() { acctest.PreCheck(t) }, - ProviderFactories: acctest.ProviderFactories, - CheckDestroy: acctest.VerifyDeleted(fqrn, "", testCheckWebhook), + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ProtoV6ProviderFactories: acctest.ProtoV6ProviderFactories, + CheckDestroy: acctest.VerifyDeleted(fqrn, "key", testCheckWebhook), Steps: []resource.TestStep{ { - Config: config1, - Check: resource.TestCheckResourceAttr(fqrn, "handler.0.custom_http_headers.Authorization", fmt.Sprintf("Bearer %d", firstToken)), + Config: config1, + Check: resource.TestCheckResourceAttr(fqrn, "handler.0.custom_http_headers.Authorization", fmt.Sprintf("Bearer %d", firstToken)), + ConfigPlanChecks: testutil.ConfigPlanChecks(fqrn), }, { Config: config2, Check: resource.TestCheckResourceAttr(fqrn, "handler.0.custom_http_headers.Authorization", fmt.Sprintf("Bearer %d", secondToken)), }, { - Config: config3, - Check: resource.TestCheckResourceAttr(fqrn, "handler.0.custom_http_headers.Authorization", fmt.Sprintf("Bearer %d", thirdToken)), - }, - { - ResourceName: fqrn, - ImportState: true, - ImportStateVerify: true, - ImportStateCheck: validator.CheckImportState(name, "key"), + ResourceName: fqrn, + ImportState: true, + ImportStateId: name, + ImportStateVerify: true, + ImportStateVerifyIdentifierAttribute: "key", }, }, }) } - -// Unit tests for state migration func -func TestWebhook_ResourceStateUpgradeV1(t *testing.T) { - v1Data := map[string]interface{}{ - "url": "https://google.com", - "secret": "fake-secret", - "proxy": "fake-proxy-key", - "custom_http_headers": map[string]interface{}{ - "header-1": "fake-value-1", - "header-2": "fake-value-2", - }, - } - v2Data := map[string]interface{}{ - "handler": []map[string]interface{}{ - { - "url": "https://google.com", - "secret": "fake-secret", - "proxy": "fake-proxy-key", - "custom_http_headers": map[string]interface{}{ - "header-1": "fake-value-1", - "header-2": "fake-value-2", - }, - }, - }, - } - - actual, err := webhook.ResourceStateUpgradeV1(context.Background(), v1Data, nil) - - if err != nil { - t.Fatalf("error migrating state: %s", err) - } - - if !reflect.DeepEqual(v2Data, actual) { - t.Fatalf("expected: %v\n\ngot: %v", v2Data, actual) - } -} diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_webhook_user.go b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_user.go index 5695d8ff9..88ba1aa08 100644 --- a/pkg/artifactory/resource/webhook/resource_artifactory_webhook_user.go +++ b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_user.go @@ -1,19 +1,167 @@ package webhook import ( - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "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" ) -type EmptyWebhookCriteria struct{} +var _ resource.Resource = &ReleaseBundleWebhookResource{} + +func NewUserWebhookResource() resource.Resource { + return &UserWebhookResource{ + WebhookResource: WebhookResource{ + TypeName: fmt.Sprintf("artifactory_%s_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 UserWebhookResourceModel struct { + WebhookNoCriteriaResourceModel +} + +func (m UserWebhookResourceModel) toAPIModel(ctx context.Context, domain string, apiModel *WebhookAPIModel) (diags diag.Diagnostics) { + d := m.WebhookNoCriteriaResourceModel.toAPIModel(ctx, domain, apiModel) + if d.HasError() { + diags.Append(d...) + } + + return +} + +func (m *UserWebhookResourceModel) fromAPIModel(ctx context.Context, apiModel WebhookAPIModel, stateHandlers basetypes.SetValue) diag.Diagnostics { + diags := diag.Diagnostics{} + + d := m.WebhookNoCriteriaResourceModel.fromAPIModel(ctx, apiModel, stateHandlers) + if d.HasError() { + diags.Append(d...) + } + + return diags +} + +type UserWebhookResource struct { + WebhookResource +} + +func (r *UserWebhookResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + r.WebhookResource.Metadata(ctx, req, resp) +} + +func (r *UserWebhookResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = r.schema(r.Domain, nil) +} + +func (r *UserWebhookResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + r.WebhookResource.Configure(ctx, req, resp) +} + +func (r *UserWebhookResource) 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 UserWebhookResourceModel -var userWebhookSchema = func(webhookType string, version int, isCustom bool) map[string]*schema.Schema { - return getBaseSchemaByVersion(webhookType, version, isCustom) + // Read Terraform plan data into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + var webhook WebhookAPIModel + resp.Diagnostics.Append(plan.toAPIModel(ctx, r.Domain, &webhook)...) + if resp.Diagnostics.HasError() { + return + } + + r.WebhookResource.Create(ctx, webhook, req, resp) + if resp.Diagnostics.HasError() { + return + } + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *UserWebhookResource) 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 UserWebhookResourceModel + + // Read Terraform state data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + var webhook WebhookAPIModel + found := r.WebhookResource.Read(ctx, state.Key.ValueString(), &webhook, req, resp) + if resp.Diagnostics.HasError() { + return + } + + if !found { + return + } + + resp.Diagnostics.Append(state.fromAPIModel(ctx, webhook, state.Handlers)...) + if resp.Diagnostics.HasError() { + return + } + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) } -var packEmptyCriteria = func(artifactoryCriteria map[string]interface{}) map[string]interface{} { - return map[string]interface{}{} +func (r *UserWebhookResource) 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 UserWebhookResourceModel + + // Read Terraform plan data into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + var webhook WebhookAPIModel + resp.Diagnostics.Append(plan.toAPIModel(ctx, r.Domain, &webhook)...) + if resp.Diagnostics.HasError() { + return + } + + r.WebhookResource.Update(ctx, plan.Key.ValueString(), webhook, req, resp) + if resp.Diagnostics.HasError() { + return + } + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *UserWebhookResource) 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 UserWebhookResourceModel + + // Read Terraform prior state data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + + r.WebhookResource.Delete(ctx, state.Key.ValueString(), req, resp) + if resp.Diagnostics.HasError() { + return + } + + // If the logic reaches here, it implicitly succeeded and will remove + // the resource from state if there are no other errors. } -var unpackEmptyCriteria = func(terraformCriteria map[string]interface{}, baseCriteria BaseWebhookCriteria) interface{} { - return EmptyWebhookCriteria{} +// ImportState imports the resource into the Terraform state. +func (r *UserWebhookResource) 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_webhook_user_test.go b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_user_test.go index f0f63746e..9d0d1bcf5 100644 --- a/pkg/artifactory/resource/webhook/resource_artifactory_webhook_user_test.go +++ b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_user_test.go @@ -1,22 +1,79 @@ 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" - "github.com/jfrog/terraform-provider-shared/validator" ) +func TestAccWebhook_User_UpgradeFromSDKv2(t *testing.T) { + _, fqrn, name := testutil.MkNames("test-user-webhook", "artifactory_user_webhook") + + params := map[string]interface{}{ + "webhookName": name, + } + webhookConfig := util.ExecuteTemplate("TestAccWebhook_User", ` + resource "artifactory_user_webhook" "{{ .webhookName }}" { + key = "{{ .webhookName }}" + description = "test description" + event_types = ["locked"] + handler { + url = "https://google.com" + secret = "fake-secret" + use_secret_for_signing = true + custom_http_headers = { + header-1 = "value-1" + header-2 = "value-2" + } + } + } + `, params) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + CheckDestroy: acctest.VerifyDeleted(fqrn, "key", acctest.CheckRepo), + + Steps: []resource.TestStep{ + { + Config: webhookConfig, + ExternalProviders: map[string]resource.ExternalProvider{ + "artifactory": { + Source: "jfrog/artifactory", + VersionConstraint: "12.1.0", + }, + }, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(fqrn, "handler.#", "1"), + 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"), + ), + }, + { + Config: webhookConfig, + ProtoV6ProviderFactories: acctest.ProtoV6ProviderFactories, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectEmptyPlan(), + }, + }, + }, + }, + }) +} + func TestAccWebhook_User(t *testing.T) { _, fqrn, name := testutil.MkNames("test-user-webhook", "artifactory_user_webhook") params := map[string]interface{}{ - "webhookName": name, - "useSecretForSigning": testutil.RandBool(), + "webhookName": name, } webhookConfig := util.ExecuteTemplate("TestAccWebhook_User", ` resource "artifactory_user_webhook" "{{ .webhookName }}" { @@ -26,7 +83,7 @@ func TestAccWebhook_User(t *testing.T) { handler { url = "https://google.com" secret = "fake-secret" - use_secret_for_signing = {{ .useSecretForSigning }} + use_secret_for_signing = true custom_http_headers = { header-1 = "value-1" header-2 = "value-2" @@ -36,9 +93,9 @@ func TestAccWebhook_User(t *testing.T) { `, params) resource.Test(t, resource.TestCase{ - PreCheck: func() { acctest.PreCheck(t) }, - ProviderFactories: acctest.ProviderFactories, - CheckDestroy: acctest.VerifyDeleted(fqrn, "", acctest.CheckRepo), + PreCheck: func() { acctest.PreCheck(t) }, + ProtoV6ProviderFactories: acctest.ProtoV6ProviderFactories, + CheckDestroy: acctest.VerifyDeleted(fqrn, "key", acctest.CheckRepo), Steps: []resource.TestStep{ { @@ -47,18 +104,19 @@ func TestAccWebhook_User(t *testing.T) { resource.TestCheckResourceAttr(fqrn, "handler.#", "1"), 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", fmt.Sprintf("%t", params["useSecretForSigning"])), + 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"), ), }, { - ResourceName: fqrn, - ImportState: true, - ImportStateVerify: true, - ImportStateCheck: validator.CheckImportState(name, "key"), - ImportStateVerifyIgnore: []string{"handler.0.secret"}, + ResourceName: fqrn, + ImportState: true, + ImportStateId: name, + ImportStateVerify: true, + ImportStateVerifyIdentifierAttribute: "key", + ImportStateVerifyIgnore: []string{"handler.0.secret"}, }}, }) }