From 116e4a2135b1c6f5d1360f0a0b00411d1c10b9bc Mon Sep 17 00:00:00 2001 From: Alex Hung Date: Wed, 25 Sep 2024 12:33:08 -0700 Subject: [PATCH 01/17] Refactoring cleanup And start of migrating artifact webhook --- pkg/artifactory/provider/framework.go | 2 + .../resource_artifactory_artifact_webhook.go | 11 + .../resource_artifactory_custom_webhook.go | 8 +- .../webhook/resource_artifactory_webhook.go | 202 ++++++++++-------- .../resource_artifactory_webhook_test.go | 2 +- 5 files changed, 126 insertions(+), 99 deletions(-) create mode 100644 pkg/artifactory/resource/webhook/resource_artifactory_artifact_webhook.go diff --git a/pkg/artifactory/provider/framework.go b/pkg/artifactory/provider/framework.go index b8dff0f75..c6a65d285 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,7 @@ func (p *ArtifactoryProvider) Resources(ctx context.Context) []func() resource.R replication.NewLocalRepositorySingleReplicationResource, replication.NewLocalRepositoryMultiReplicationResource, replication.NewRemoteRepositoryReplicationResource, + webhook.NewArtifactWebhookResource, } } diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_artifact_webhook.go b/pkg/artifactory/resource/webhook/resource_artifactory_artifact_webhook.go new file mode 100644 index 000000000..03f0894f5 --- /dev/null +++ b/pkg/artifactory/resource/webhook/resource_artifactory_artifact_webhook.go @@ -0,0 +1,11 @@ +package webhook + +import "github.com/hashicorp/terraform-plugin-framework/resource" + +var _ resource.Resource = &ArtifactWebhookResource{} + +func NewArtifactWebhookResource() resource.Resource { + return &ArtifactWebhookResource{ + TypeName: "artifactory_artifact_webhook", + } +} diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook.go b/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook.go index e23a0e4a7..48fc98dfb 100644 --- a/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook.go +++ b/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook.go @@ -271,7 +271,7 @@ func ResourceArtifactoryCustomWebhook(webhookType string) *schema.Resource { SetPathParam("webhookKey", data.Id()). SetResult(&webhook). SetError(&artifactoryError). - Get(WhUrl) + Get(WebhookURL) if err != nil { return diag.FromErr(err) @@ -308,7 +308,7 @@ func ResourceArtifactoryCustomWebhook(webhookType string) *schema.Resource { SetBody(webhook). SetError(&artifactoryError). AddRetryCondition(retryOnProxyError). - Post(webhooksUrl) + Post(webhooksURL) if err != nil { return diag.FromErr(err) } @@ -336,7 +336,7 @@ func ResourceArtifactoryCustomWebhook(webhookType string) *schema.Resource { SetBody(webhook). SetError(&artifactoryError). AddRetryCondition(retryOnProxyError). - Put(WhUrl) + Put(WebhookURL) if err != nil { return diag.FromErr(err) } @@ -357,7 +357,7 @@ func ResourceArtifactoryCustomWebhook(webhookType string) *schema.Resource { resp, err := m.(util.ProviderMetadata).Client.R(). SetPathParam("webhookKey", data.Id()). SetError(&artifactoryError). - Delete(WhUrl) + Delete(WebhookURL) if err != nil { return diag.FromErr(err) diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_webhook.go b/pkg/artifactory/resource/webhook/resource_artifactory_webhook.go index eb39a81cb..c5d414ad7 100644 --- a/pkg/artifactory/resource/webhook/resource_artifactory_webhook.go +++ b/pkg/artifactory/resource/webhook/resource_artifactory_webhook.go @@ -18,34 +18,54 @@ import ( "golang.org/x/exp/slices" ) +const ( + webhooksURL = "/event/api/v1/subscriptions" + WebhookURL = "/event/api/v1/subscriptions/{webhookKey}" + + ArtifactLifecycleType = "artifact_lifecycle" + ArtifactPropertyType = "artifact_property" + ArtifactType = "artifact" + ArtifactoryReleaseBundleType = "artifactory_release_bundle" + BuildType = "build" + DestinationType = "destination" + DistributionType = "distribution" + DockerType = "docker" + ReleaseBundleType = "release_bundle" + ReleaseBundleV2Type = "release_bundle_v2" + ReleaseBundleV2PromotionType = "release_bundle_v2_promotion" + UserType = "user" +) + +const currentSchemaVersion = 2 + 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", + ArtifactLifecycleType, + ArtifactPropertyType, + // ArtifactType, + ArtifactoryReleaseBundleType, + BuildType, + DestinationType, + DistributionType, + DockerType, + ReleaseBundleType, + ReleaseBundleV2Type, + ReleaseBundleV2PromotionType, + UserType, } 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"}, + ArtifactType: {"deployed", "deleted", "moved", "copied", "cached"}, + ArtifactPropertyType: {"added", "deleted"}, + DockerType: {"pushed", "deleted", "promoted"}, + BuildType: {"uploaded", "deleted", "promoted"}, + ReleaseBundleType: {"created", "signed", "deleted"}, + DistributionType: {"distribute_started", "distribute_completed", "distribute_aborted", "distribute_failed", "delete_started", "delete_completed", "delete_failed"}, + ArtifactoryReleaseBundleType: {"received", "delete_started", "delete_completed", "delete_failed"}, + DestinationType: {"received", "delete_started", "delete_completed", "delete_failed"}, + UserType: {"locked"}, + ReleaseBundleV2Type: {"release_bundle_v2_started", "release_bundle_v2_failed", "release_bundle_v2_completed"}, + ReleaseBundleV2PromotionType: {"release_bundle_v2_promotion_completed", "release_bundle_v2_promotion_failed", "release_bundle_v2_promotion_started"}, + ArtifactLifecycleType: {"archive", "restore"}, } type BaseParams struct { @@ -80,12 +100,6 @@ type KeyValuePair struct { Value string `json:"value"` } -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 { @@ -109,64 +123,64 @@ var packKeyValuePair = func(keyValuePairs []KeyValuePair) map[string]interface{} } 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{}, + ArtifactType: RepoWebhookCriteria{}, + ArtifactPropertyType: RepoWebhookCriteria{}, + DockerType: RepoWebhookCriteria{}, + BuildType: BuildWebhookCriteria{}, + ReleaseBundleType: ReleaseBundleWebhookCriteria{}, + DistributionType: ReleaseBundleWebhookCriteria{}, + ArtifactoryReleaseBundleType: ReleaseBundleWebhookCriteria{}, + DestinationType: ReleaseBundleWebhookCriteria{}, + UserType: EmptyWebhookCriteria{}, + ReleaseBundleV2Type: ReleaseBundleV2WebhookCriteria{}, + ReleaseBundleV2PromotionType: ReleaseBundleV2PromotionWebhookCriteria{}, + ArtifactLifecycleType: 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, + ArtifactType: packRepoCriteria, + ArtifactPropertyType: packRepoCriteria, + DockerType: packRepoCriteria, + BuildType: packBuildCriteria, + ReleaseBundleType: packReleaseBundleCriteria, + DistributionType: packReleaseBundleCriteria, + ArtifactoryReleaseBundleType: packReleaseBundleCriteria, + DestinationType: packReleaseBundleCriteria, + UserType: packEmptyCriteria, + ReleaseBundleV2Type: packReleaseBundleV2Criteria, + ReleaseBundleV2PromotionType: packReleaseBundleV2PromotionCriteria, + ArtifactLifecycleType: packEmptyCriteria, } 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, + ArtifactType: unpackRepoCriteria, + ArtifactPropertyType: unpackRepoCriteria, + DockerType: unpackRepoCriteria, + BuildType: unpackBuildCriteria, + ReleaseBundleType: unpackReleaseBundleCriteria, + DistributionType: unpackReleaseBundleCriteria, + ArtifactoryReleaseBundleType: unpackReleaseBundleCriteria, + DestinationType: unpackReleaseBundleCriteria, + UserType: unpackEmptyCriteria, + ReleaseBundleV2Type: unpackReleaseBundleV2Criteria, + ReleaseBundleV2PromotionType: unpackReleaseBundleV2PromotionCriteria, + ArtifactLifecycleType: 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), + ArtifactType: repoWebhookSchema(webhookType, version, isCustom), + ArtifactPropertyType: repoWebhookSchema(webhookType, version, isCustom), + DockerType: repoWebhookSchema(webhookType, version, isCustom), + BuildType: buildWebhookSchema(webhookType, version, isCustom), + ReleaseBundleType: releaseBundleWebhookSchema(webhookType, version, isCustom), + DistributionType: releaseBundleWebhookSchema(webhookType, version, isCustom), + ArtifactoryReleaseBundleType: releaseBundleWebhookSchema(webhookType, version, isCustom), + DestinationType: releaseBundleWebhookSchema(webhookType, version, isCustom), + UserType: userWebhookSchema(webhookType, version, isCustom), + ReleaseBundleV2Type: releaseBundleV2WebhookSchema(webhookType, version, isCustom), + ReleaseBundleV2PromotionType: releaseBundleV2PromotionWebhookSchema(webhookType, version, isCustom), + ArtifactLifecycleType: artifactLifecycleWebhookSchema(webhookType, version, isCustom), } } @@ -212,18 +226,18 @@ var packCriteria = func(d *schema.ResourceData, webhookType string, criteria map } 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, + ArtifactType: repoCriteriaValidation, + ArtifactPropertyType: repoCriteriaValidation, + DockerType: repoCriteriaValidation, + BuildType: buildCriteriaValidation, + ReleaseBundleType: releaseBundleCriteriaValidation, + DistributionType: releaseBundleCriteriaValidation, + ArtifactoryReleaseBundleType: releaseBundleCriteriaValidation, + DestinationType: releaseBundleCriteriaValidation, + UserType: emptyCriteriaValidation, + ReleaseBundleV2Type: releaseBundleV2CriteriaValidation, + ReleaseBundleV2PromotionType: emptyCriteriaValidation, + ArtifactLifecycleType: emptyCriteriaValidation, } var emptyCriteriaValidation = func(ctx context.Context, criteria map[string]interface{}) error { @@ -359,7 +373,7 @@ func ResourceArtifactoryWebhook(webhookType string) *schema.Resource { SetPathParam("webhookKey", data.Id()). SetResult(&webhook). SetError(&artifactoryError). - Get(WhUrl) + Get(WebhookURL) if err != nil { return diag.FromErr(err) @@ -396,7 +410,7 @@ func ResourceArtifactoryWebhook(webhookType string) *schema.Resource { SetBody(webhook). AddRetryCondition(retryOnProxyError). SetError(&artifactoryError). - Post(webhooksUrl) + Post(webhooksURL) if err != nil { return diag.FromErr(err) } @@ -424,7 +438,7 @@ func ResourceArtifactoryWebhook(webhookType string) *schema.Resource { SetBody(webhook). AddRetryCondition(retryOnProxyError). SetError(&artifactoryError). - Put(WhUrl) + Put(WebhookURL) if err != nil { return diag.FromErr(err) } @@ -445,7 +459,7 @@ func ResourceArtifactoryWebhook(webhookType string) *schema.Resource { resp, err := m.(util.ProviderMetadata).Client.R(). SetPathParam("webhookKey", data.Id()). SetError(&artifactoryError). - Delete(WhUrl) + Delete(WebhookURL) if err != nil { return diag.FromErr(err) diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_webhook_test.go b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_test.go index 7a7bbfb93..75a79bb61 100644 --- a/pkg/artifactory/resource/webhook/resource_artifactory_webhook_test.go +++ b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_test.go @@ -479,7 +479,7 @@ 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) { From 65b0b9e552c65a9bc0067f1b4745247ad99d0bd9 Mon Sep 17 00:00:00 2001 From: Alex Hung Date: Thu, 26 Sep 2024 08:57:23 -0700 Subject: [PATCH 02/17] Migrate artifact webhook to Plugin Framework --- .../resource_artifactory_artifact_webhook.go | 627 +++++++++++++++++- .../resource_artifactory_custom_webhook.go | 34 +- .../webhook/resource_artifactory_webhook.go | 80 +-- .../resource_artifactory_webhook_base.go | 2 +- .../resource_artifactory_webhook_build.go | 10 +- ...urce_artifactory_webhook_release_bundle.go | 6 +- ...e_artifactory_webhook_release_bundle_v2.go | 6 +- ...ory_webhook_release_bundle_v2_promotion.go | 2 +- .../resource_artifactory_webhook_repo.go | 18 +- .../resource_artifactory_webhook_test.go | 75 +-- .../resource_artifactory_webhook_user.go | 2 +- 11 files changed, 742 insertions(+), 120 deletions(-) diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_artifact_webhook.go b/pkg/artifactory/resource/webhook/resource_artifactory_artifact_webhook.go index 03f0894f5..197d14646 100644 --- a/pkg/artifactory/resource/webhook/resource_artifactory_artifact_webhook.go +++ b/pkg/artifactory/resource/webhook/resource_artifactory_artifact_webhook.go @@ -1,6 +1,32 @@ package webhook -import "github.com/hashicorp/terraform-plugin-framework/resource" +import ( + "context" + "fmt" + "net/http" + "regexp" + "strings" + + "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/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "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" + utilfw "github.com/jfrog/terraform-provider-shared/util/fw" + validatorfw_string "github.com/jfrog/terraform-provider-shared/validator/fw/string" + "github.com/samber/lo" +) var _ resource.Resource = &ArtifactWebhookResource{} @@ -9,3 +35,602 @@ func NewArtifactWebhookResource() resource.Resource { TypeName: "artifactory_artifact_webhook", } } + +type ArtifactWebhookResource struct { + ProviderData util.ProviderMetadata + TypeName string +} + +type ArtifactWebhookResourceModel struct { + ID types.String `tfsdk:"id"` + Key types.String `tfsdk:"key"` + Description types.String `tfsdk:"description"` + Enabled types.Bool `tfsdk:"enabled"` + EventTypes types.Set `tfsdk:"event_types"` + Criteria types.Set `tfsdk:"criteria"` + Handlers types.Set `tfsdk:"handler"` +} + +func (m ArtifactWebhookResourceModel) toAPIModel(ctx context.Context, apiModel *ArtifactWebhookAPIModel) (diags diag.Diagnostics) { + critieriaObj := m.Criteria.Elements()[0].(types.Object) + critieriaAttrs := critieriaObj.Attributes() + + var includePatterns []string + d := critieriaAttrs["include_patterns"].(types.Set).ElementsAs(ctx, &includePatterns, false) + if d.HasError() { + diags.Append(d...) + } + + var excludePatterns []string + d = critieriaAttrs["exclude_patterns"].(types.Set).ElementsAs(ctx, &excludePatterns, false) + 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...) + } + + var eventTypes []string + d = m.EventTypes.ElementsAs(ctx, &eventTypes, false) + if d.HasError() { + diags.Append(d...) + } + + handlers := lo.Map( + m.Handlers.Elements(), + func(elem attr.Value, _ int) 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).ValueString(), + UseSecretForSigning: attrs["use_secret_for_signing"].(types.Bool).ValueBool(), + Proxy: attrs["proxy"].(types.String).ValueString(), + CustomHttpHeaders: customHttpHeaders, + } + }, + ) + + *apiModel = ArtifactWebhookAPIModel{ + BaseAPIModel: BaseAPIModel{ + Key: m.Key.ValueString(), + Description: m.Description.ValueString(), + Enabled: m.Enabled.ValueBool(), + EventFilter: EventFilterAPIModel{ + Domain: ArtifactType, + EventTypes: eventTypes, + Criteria: RepoCriteriaAPIModel{ + BaseCriteriaAPIModel: BaseCriteriaAPIModel{ + IncludePatterns: includePatterns, + ExcludePatterns: excludePatterns, + }, + AnyLocal: critieriaAttrs["any_local"].(types.Bool).ValueBool(), + AnyRemote: critieriaAttrs["any_remote"].(types.Bool).ValueBool(), + AnyFederated: critieriaAttrs["any_federated"].(types.Bool).ValueBool(), + RepoKeys: repoKeys, + }, + }, + Handlers: handlers, + }, + } + + return +} + +var criteriaSetResourceModelAttributeTypes = map[string]attr.Type{ + "include_patterns": types.SetType{ElemType: types.StringType}, + "exclude_patterns": types.SetType{ElemType: types.StringType}, + "any_local": types.BoolType, + "any_remote": types.BoolType, + "any_federated": types.BoolType, + "repo_keys": types.SetType{ElemType: types.StringType}, +} + +var criteriaSetResourceModelElementTypes = types.ObjectType{ + AttrTypes: criteriaSetResourceModelAttributeTypes, +} + +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}, +} + +var handlerSetResourceModelElementTypes = types.ObjectType{ + AttrTypes: handlerSetResourceModelAttributeTypes, +} + +func (m *ArtifactWebhookResourceModel) fromAPIModel(ctx context.Context, apiModel ArtifactWebhookAPIModel, stateHandlers basetypes.SetValue) diag.Diagnostics { + diags := diag.Diagnostics{} + + m.ID = types.StringValue(apiModel.Key) + m.Key = types.StringValue(apiModel.Key) + + description := types.StringNull() + if apiModel.Description != "" { + description = types.StringValue(apiModel.Description) + } + m.Description = description + + m.Enabled = types.BoolValue(apiModel.Enabled) + + eventTypes, d := types.SetValueFrom(ctx, types.StringType, apiModel.EventFilter.EventTypes) + if d.HasError() { + diags.Append(d...) + } + m.EventTypes = eventTypes + + criteriaAPIModel := apiModel.EventFilter.Criteria.(map[string]interface{}) + + includePatterns := types.SetNull(types.StringType) + if v, ok := criteriaAPIModel["includePatterns"]; ok && v != nil { + ps, d := types.SetValueFrom(ctx, types.StringType, v) + if d.HasError() { + diags.Append(d...) + } + + includePatterns = ps + } + + excludePatterns := types.SetNull(types.StringType) + if v, ok := criteriaAPIModel["excludePatterns"]; ok && v != nil { + ps, d := types.SetValueFrom(ctx, types.StringType, v) + if d.HasError() { + diags.Append(d...) + } + + excludePatterns = ps + } + + repoKeys := types.SetNull(types.StringType) + if v, ok := criteriaAPIModel["repoKeys"]; ok && v != nil { + ks, d := types.SetValueFrom(ctx, types.StringType, v) + if d.HasError() { + diags.Append(d...) + } + + repoKeys = ks + } + + criteria, d := types.ObjectValue( + criteriaSetResourceModelAttributeTypes, + map[string]attr.Value{ + "include_patterns": includePatterns, + "exclude_patterns": excludePatterns, + "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( + criteriaSetResourceModelElementTypes, + []attr.Value{criteria}, + ) + if d.HasError() { + diags.Append(d...) + } + m.Criteria = criteriaSet + + 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...) + } + + customHttpHeaders = h + } + + secret := types.StringValue("") + 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() + if !attrs["secret"].(types.String).IsNull() { + secret = attrs["secret"].(types.String) + } + } + + proxy := types.StringNull() + if handler.Proxy != "" { + proxy = types.StringValue(handler.Proxy) + } + + h, d := types.ObjectValue( + handlerSetResourceModelAttributeTypes, + map[string]attr.Value{ + "url": types.StringValue(handler.Url), + "secret": secret, + "use_secret_for_signing": types.BoolValue(handler.UseSecretForSigning), + "proxy": proxy, + "custom_http_headers": customHttpHeaders, + }, + ) + if d.HasError() { + diags.Append(d...) + } + + return h + }, + ) + + handlersSet, d := types.SetValue( + handlerSetResourceModelElementTypes, + handlers, + ) + if d.HasError() { + diags.Append(d...) + } + m.Handlers = handlersSet + + return diags +} + +type ArtifactWebhookAPIModel struct { + BaseAPIModel +} + +func (r *ArtifactWebhookResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = r.TypeName +} + +func (r *ArtifactWebhookResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Version: 2, + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ // for backward compatability + Computed: true, + }, + "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[ArtifactType]...), + ), + }, + 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[ArtifactType], ", "), "[]")), + }, + }, + Blocks: map[string]schema.Block{ + "criteria": schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "include_patterns": schema.SetAttribute{ + ElementType: types.StringType, + Optional: true, + MarkdownDescription: "Simple comma separated wildcard patterns for repository artifact paths (with no leading slash).\nAnt-style path expressions are supported (*, **, ?).\nFor example: `org/apache/**`", + }, + "exclude_patterns": schema.SetAttribute{ + ElementType: types.StringType, + Optional: true, + MarkdownDescription: "Simple comma separated wildcard patterns for repository artifact paths (with no leading slash).\nAnt-style path expressions are supported (*, **, ?).\nFor example: `org/apache/**`", + }, + "any_local": schema.BoolAttribute{ + Required: true, + Description: "Trigger on any local repositories", + }, + "any_remote": schema.BoolAttribute{ + Required: true, + Description: "Trigger on any remote repositories", + }, + "any_federated": schema.BoolAttribute{ + Required: true, + Description: "Trigger on any federated repositories", + }, + "repo_keys": schema.SetAttribute{ + ElementType: types.StringType, + Required: true, + Description: "Trigger on this list of repository keys", + }, + }, + }, + Validators: []validator.Set{ + setvalidator.SizeBetween(1, 1), + setvalidator.IsRequired(), + }, + Description: "Specifies where the webhook will be applied on which repositories.", + }, + "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, + Computed: true, + Default: stringdefault.StaticString(""), // for backward compatability + // Sensitive: true, + Description: "Secret authentication token that will be sent to the configured URL.", + }, + "use_secret_for_signing": schema.BoolAttribute{ + Optional: true, + Computed: true, + Default: booldefault.StaticBool(false), + 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"), + }, + 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), + }, + }, + }, + MarkdownDescription: "This resource enables you to creates a new Release Bundle v2, uniquely identified by a combination of repository key, name, and version. For more information, see [Understanding Release Bundles v2](https://jfrog.com/help/r/jfrog-artifactory-documentation/understanding-release-bundles-v2) and [REST API](https://jfrog.com/help/r/jfrog-rest-apis/create-release-bundle-v2-version).", + } +} + +func (r *ArtifactWebhookResource) 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) +} + +func (r ArtifactWebhookResource) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) { + var data ArtifactWebhookResourceModel + + 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 *ArtifactWebhookResource) 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 ArtifactWebhookResourceModel + + // Read Terraform plan data into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + var webhook ArtifactWebhookAPIModel + resp.Diagnostics.Append(plan.toAPIModel(ctx, &webhook)...) + if resp.Diagnostics.HasError() { + return + } + + 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 + } + + if response.IsError() { + utilfw.UnableToCreateResourceError(resp, artifactoryError.String()) + return + } + + plan.ID = plan.Key + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *ArtifactWebhookResource) 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 ArtifactWebhookResourceModel + + // Read Terraform state data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + var webhook ArtifactWebhookAPIModel + var artifactoryError artifactory.ArtifactoryErrorsResponse + + response, err := r.ProviderData.Client.R(). + SetPathParam("webhookKey", state.Key.ValueString()). + SetResult(&webhook). + SetError(&artifactoryError). + Get(WebhookURL) + if err != nil { + utilfw.UnableToRefreshResourceError(resp, err.Error()) + return + } + + if response.StatusCode() == http.StatusNotFound { + resp.State.RemoveResource(ctx) + return + } + + if response.IsError() { + utilfw.UnableToRefreshResourceError(resp, artifactoryError.String()) + 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 *ArtifactWebhookResource) 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 ArtifactWebhookResourceModel + + // Read Terraform plan data into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + var webhook ArtifactWebhookAPIModel + resp.Diagnostics.Append(plan.toAPIModel(ctx, &webhook)...) + if resp.Diagnostics.HasError() { + return + } + + var artifactoryError artifactory.ArtifactoryErrorsResponse + response, err := r.ProviderData.Client.R(). + SetPathParam("webhookKey", plan.Key.ValueString()). + SetBody(webhook). + AddRetryCondition(retryOnProxyError). + SetError(&artifactoryError). + Put(WebhookURL) + if err != nil { + utilfw.UnableToUpdateResourceError(resp, err.Error()) + return + } + if response.IsError() { + utilfw.UnableToUpdateResourceError(resp, artifactoryError.String()) + return + } + + plan.ID = plan.Key + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *ArtifactWebhookResource) 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 ArtifactWebhookResourceModel + + // Read Terraform prior state data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + + var artifactoryError artifactory.ArtifactoryErrorsResponse + response, err := r.ProviderData.Client.R(). + SetPathParam("webhookKey", state.Key.ValueString()). + SetError(&artifactoryError). + Delete(WebhookURL) + + if err != nil { + utilfw.UnableToDeleteResourceError(resp, err.Error()) + return + } + + if response.StatusCode() == http.StatusNotFound { + resp.State.RemoveResource(ctx) + return + } + + if response.IsError() { + utilfw.UnableToDeleteResourceError(resp, artifactoryError.String()) + 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 *ArtifactWebhookResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("key"), req, resp) +} diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook.go b/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook.go index 48fc98dfb..ef9b89384 100644 --- a/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook.go +++ b/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook.go @@ -108,11 +108,11 @@ func baseCustomWebhookBaseSchema(webhookType string) map[string]*schema.Schema { } 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 +120,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 +204,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 +246,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)...) @@ -264,7 +264,7 @@ func ResourceArtifactoryCustomWebhook(webhookType string) *schema.Resource { webhook := CustomBaseParams{} - webhook.EventFilter.Criteria = domainCriteriaLookup[webhookType] + webhook.EventFilterAPIModel.Criteria = domainCriteriaLookup[webhookType] var artifactoryError artifactory.ArtifactoryErrorsResponse resp, err := m.(util.ProviderMetadata).Client.R(). diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_webhook.go b/pkg/artifactory/resource/webhook/resource_artifactory_webhook.go index c5d414ad7..8c78f8275 100644 --- a/pkg/artifactory/resource/webhook/resource_artifactory_webhook.go +++ b/pkg/artifactory/resource/webhook/resource_artifactory_webhook.go @@ -68,42 +68,42 @@ var DomainEventTypesSupported = map[string][]string{ ArtifactLifecycleType: {"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"` +type BaseAPIModel struct { + Key string `json:"key"` + Description string `json:"description"` + Enabled bool `json:"enabled"` + EventFilter EventFilterAPIModel `json:"event_filter"` + Handlers []HandlerAPIModel `json:"handlers"` } -func (w BaseParams) Id() string { +func (w BaseAPIModel) Id() string { return w.Key } -type EventFilter struct { +type EventFilterAPIModel struct { Domain string `json:"domain"` EventTypes []string `json:"event_types"` Criteria interface{} `json:"criteria"` } -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 HandlerAPIModel 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 []KeyValuePairAPIModel `json:"custom_http_headers"` } -type KeyValuePair struct { +type KeyValuePairAPIModel struct { Name string `json:"name"` Value string `json:"value"` } -var unpackKeyValuePair = func(keyValuePairs map[string]interface{}) []KeyValuePair { - var kvPairs []KeyValuePair +var unpackKeyValuePair = func(keyValuePairs map[string]interface{}) []KeyValuePairAPIModel { + var kvPairs []KeyValuePairAPIModel for key, value := range keyValuePairs { - keyValuePair := KeyValuePair{ + keyValuePair := KeyValuePairAPIModel{ Name: key, Value: value.(string), } @@ -113,7 +113,7 @@ var unpackKeyValuePair = func(keyValuePairs map[string]interface{}) []KeyValuePa return kvPairs } -var packKeyValuePair = func(keyValuePairs []KeyValuePair) map[string]interface{} { +var packKeyValuePair = func(keyValuePairs []KeyValuePairAPIModel) map[string]interface{} { kvPairs := make(map[string]interface{}) for _, keyValuePair := range keyValuePairs { kvPairs[keyValuePair.Name] = keyValuePair.Value @@ -123,9 +123,9 @@ var packKeyValuePair = func(keyValuePairs []KeyValuePair) map[string]interface{} } var domainCriteriaLookup = map[string]interface{}{ - ArtifactType: RepoWebhookCriteria{}, - ArtifactPropertyType: RepoWebhookCriteria{}, - DockerType: RepoWebhookCriteria{}, + ArtifactType: RepoCriteriaAPIModel{}, + ArtifactPropertyType: RepoCriteriaAPIModel{}, + DockerType: RepoCriteriaAPIModel{}, BuildType: BuildWebhookCriteria{}, ReleaseBundleType: ReleaseBundleWebhookCriteria{}, DistributionType: ReleaseBundleWebhookCriteria{}, @@ -152,7 +152,7 @@ var domainPackLookup = map[string]func(map[string]interface{}) map[string]interf ArtifactLifecycleType: packEmptyCriteria, } -var domainUnpackLookup = map[string]func(map[string]interface{}, BaseWebhookCriteria) interface{}{ +var domainUnpackLookup = map[string]func(map[string]interface{}, BaseCriteriaAPIModel) interface{}{ ArtifactType: unpackRepoCriteria, ArtifactPropertyType: unpackRepoCriteria, DockerType: unpackRepoCriteria, @@ -192,7 +192,7 @@ var unpackCriteria = func(d *utilsdk.ResourceData, webhookType string) interface if len(criteria) == 1 { id := criteria[0].(map[string]interface{}) - baseCriteria := BaseWebhookCriteria{ + baseCriteria := BaseCriteriaAPIModel{ IncludePatterns: utilsdk.CastToStringArr(id["include_patterns"].(*schema.Set).List()), ExcludePatterns: utilsdk.CastToStringArr(id["exclude_patterns"].(*schema.Set).List()), } @@ -261,13 +261,19 @@ var packSecret = func(d *schema.ResourceData, url string) string { return secret } +var retryOnProxyError = func(response *resty.Response, _r error) bool { + var proxyNotFoundRegex = regexp.MustCompile("proxy with key '.*' not found") + + return proxyNotFoundRegex.MatchString(string(response.Body()[:])) +} + func ResourceArtifactoryWebhook(webhookType string) *schema.Resource { - var unpackWebhook = func(data *schema.ResourceData) (BaseParams, error) { + var unpackWebhook = func(data *schema.ResourceData) (BaseAPIModel, error) { d := &utilsdk.ResourceData{ResourceData: data} - var unpackHandlers = func(d *utilsdk.ResourceData) []Handler { - var webhookHandlers []Handler + var unpackHandlers = func(d *utilsdk.ResourceData) []HandlerAPIModel { + var webhookHandlers []HandlerAPIModel if v, ok := d.GetOk("handler"); ok { handlers := v.(*schema.Set).List() @@ -276,7 +282,7 @@ func ResourceArtifactoryWebhook(webhookType string) *schema.Resource { // 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{ + webhookHandler := HandlerAPIModel{ HandlerType: "webhook", Url: h["url"].(string), } @@ -305,11 +311,11 @@ func ResourceArtifactoryWebhook(webhookType string) *schema.Resource { return webhookHandlers } - webhook := BaseParams{ + webhook := BaseAPIModel{ Key: d.GetString("key", false), Description: d.GetString("description", false), Enabled: d.GetBool("enabled", false), - EventFilter: EventFilter{ + EventFilter: EventFilterAPIModel{ Domain: webhookType, EventTypes: d.GetSet("event_types"), Criteria: unpackCriteria(d, webhookType), @@ -320,7 +326,7 @@ func ResourceArtifactoryWebhook(webhookType string) *schema.Resource { return webhook, nil } - var packHandlers = func(d *schema.ResourceData, handlers []Handler) []error { + var packHandlers = func(d *schema.ResourceData, handlers []HandlerAPIModel) []error { setValue := utilsdk.MkLens(d) resource := domainSchemaLookup(currentSchemaVersion, false, webhookType)[webhookType]["handler"].Elem.(*schema.Resource) var packedHandlers []interface{} @@ -342,7 +348,7 @@ func ResourceArtifactoryWebhook(webhookType string) *schema.Resource { return setValue("handler", schema.NewSet(schema.HashResource(resource), packedHandlers)) } - var packWebhook = func(d *schema.ResourceData, webhook BaseParams) diag.Diagnostics { + var packWebhook = func(d *schema.ResourceData, webhook BaseAPIModel) diag.Diagnostics { setValue := utilsdk.MkLens(d) setValue("key", webhook.Key) @@ -364,7 +370,7 @@ func ResourceArtifactoryWebhook(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 := BaseParams{} + webhook := BaseAPIModel{} webhook.EventFilter.Criteria = domainCriteriaLookup[webhookType] @@ -391,12 +397,6 @@ func ResourceArtifactoryWebhook(webhookType string) *schema.Resource { return packWebhook(data, webhook) } - var retryOnProxyError = func(response *resty.Response, _r error) bool { - var proxyNotFoundRegex = regexp.MustCompile("proxy with key '.*' not found") - - return proxyNotFoundRegex.MatchString(string(response.Body()[:])) - } - var createWebhook = func(ctx context.Context, data *schema.ResourceData, m interface{}) diag.Diagnostics { tflog.Debug(ctx, "createWebhook") diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_webhook_base.go b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_base.go index f75669ef7..a40080205 100644 --- a/pkg/artifactory/resource/webhook/resource_artifactory_webhook_base.go +++ b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_base.go @@ -9,7 +9,7 @@ import ( "github.com/jfrog/terraform-provider-shared/validator" ) -type BaseWebhookCriteria struct { +type BaseCriteriaAPIModel struct { IncludePatterns []string `json:"includePatterns"` ExcludePatterns []string `json:"excludePatterns"` } diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_webhook_build.go b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_build.go index 542235d89..e55cc5f04 100644 --- a/pkg/artifactory/resource/webhook/resource_artifactory_webhook_build.go +++ b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_build.go @@ -9,7 +9,7 @@ import ( ) type BuildWebhookCriteria struct { - BaseWebhookCriteria + BaseCriteriaAPIModel AnyBuild bool `json:"anyBuild"` SelectedBuilds []string `json:"selectedBuilds"` } @@ -47,11 +47,11 @@ var packBuildCriteria = func(artifactoryCriteria map[string]interface{}) map[str } } -var unpackBuildCriteria = func(terraformCriteria map[string]interface{}, baseCriteria BaseWebhookCriteria) interface{} { +var unpackBuildCriteria = func(terraformCriteria map[string]interface{}, baseCriteria BaseCriteriaAPIModel) interface{} { return BuildWebhookCriteria{ - AnyBuild: terraformCriteria["any_build"].(bool), - SelectedBuilds: utilsdk.CastToStringArr(terraformCriteria["selected_builds"].(*schema.Set).List()), - BaseWebhookCriteria: baseCriteria, + AnyBuild: terraformCriteria["any_build"].(bool), + SelectedBuilds: utilsdk.CastToStringArr(terraformCriteria["selected_builds"].(*schema.Set).List()), + BaseCriteriaAPIModel: baseCriteria, } } 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..e3d5e31b9 100644 --- a/pkg/artifactory/resource/webhook/resource_artifactory_webhook_release_bundle.go +++ b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_release_bundle.go @@ -10,7 +10,7 @@ import ( ) type ReleaseBundleWebhookCriteria struct { - BaseWebhookCriteria + BaseCriteriaAPIModel AnyReleaseBundle bool `json:"anyReleaseBundle"` RegisteredReleaseBundlesNames []string `json:"registeredReleaseBundlesNames"` } @@ -48,11 +48,11 @@ var packReleaseBundleCriteria = func(artifactoryCriteria map[string]interface{}) } } -var unpackReleaseBundleCriteria = func(terraformCriteria map[string]interface{}, baseCriteria BaseWebhookCriteria) interface{} { +var unpackReleaseBundleCriteria = func(terraformCriteria map[string]interface{}, baseCriteria BaseCriteriaAPIModel) interface{} { return ReleaseBundleWebhookCriteria{ AnyReleaseBundle: terraformCriteria["any_release_bundle"].(bool), RegisteredReleaseBundlesNames: utilsdk.CastToStringArr(terraformCriteria["registered_release_bundle_names"].(*schema.Set).List()), - BaseWebhookCriteria: baseCriteria, + BaseCriteriaAPIModel: baseCriteria, } } 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..a349dc3e0 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 @@ -10,7 +10,7 @@ import ( ) type ReleaseBundleV2WebhookCriteria struct { - BaseWebhookCriteria + BaseCriteriaAPIModel AnyReleaseBundle bool `json:"anyReleaseBundle"` SelectedReleaseBundles []string `json:"selectedReleaseBundles"` } @@ -48,11 +48,11 @@ var packReleaseBundleV2Criteria = func(artifactoryCriteria map[string]interface{ } } -var unpackReleaseBundleV2Criteria = func(terraformCriteria map[string]interface{}, baseCriteria BaseWebhookCriteria) interface{} { +var unpackReleaseBundleV2Criteria = func(terraformCriteria map[string]interface{}, baseCriteria BaseCriteriaAPIModel) interface{} { return ReleaseBundleV2WebhookCriteria{ AnyReleaseBundle: terraformCriteria["any_release_bundle"].(bool), SelectedReleaseBundles: utilsdk.CastToStringArr(terraformCriteria["selected_release_bundles"].(*schema.Set).List()), - BaseWebhookCriteria: baseCriteria, + BaseCriteriaAPIModel: baseCriteria, } } 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..ec5fa8d7f 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 @@ -36,7 +36,7 @@ var packReleaseBundleV2PromotionCriteria = func(artifactoryCriteria map[string]i } } -var unpackReleaseBundleV2PromotionCriteria = func(terraformCriteria map[string]interface{}, baseCriteria BaseWebhookCriteria) interface{} { +var unpackReleaseBundleV2PromotionCriteria = func(terraformCriteria map[string]interface{}, baseCriteria BaseCriteriaAPIModel) interface{} { return ReleaseBundleV2PromotionWebhookCriteria{ SelectedEnvironments: utilsdk.CastToStringArr(terraformCriteria["selected_environments"].(*schema.Set).List()), } diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_webhook_repo.go b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_repo.go index 93e9e3547..0945ecf28 100644 --- a/pkg/artifactory/resource/webhook/resource_artifactory_webhook_repo.go +++ b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_repo.go @@ -9,8 +9,8 @@ import ( utilsdk "github.com/jfrog/terraform-provider-shared/util/sdk" ) -type RepoWebhookCriteria struct { - BaseWebhookCriteria +type RepoCriteriaAPIModel struct { + BaseCriteriaAPIModel AnyLocal bool `json:"anyLocal"` AnyRemote bool `json:"anyRemote"` AnyFederated bool `json:"anyFederated"` @@ -68,13 +68,13 @@ var packRepoCriteria = func(artifactoryCriteria map[string]interface{}) map[stri return criteria } -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, +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, } } diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_webhook_test.go b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_test.go index 75a79bb61..0a7005b4f 100644 --- a/pkg/artifactory/resource/webhook/resource_artifactory_webhook_test.go +++ b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_test.go @@ -131,10 +131,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 +172,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 +211,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`), }, }, }) @@ -233,7 +230,9 @@ func TestAccWebhook_HandlerValidation_ProxyWithURL(t *testing.T) { params := map[string]interface{}{ "webhookName": name, + "proxy": fmt.Sprintf("test-proxy-%d", id), } + webhookConfig := util.ExecuteTemplate("TestAccWebhookEventTypesValidation", ` resource "artifactory_artifact_webhook" "{{ .webhookName }}" { key = "{{ .webhookName }}" @@ -253,14 +252,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 +289,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, @@ -451,10 +448,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 +461,12 @@ 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, + ImportStateVerify: true, + ImportStateCheck: validator.CheckImportState(name, "key"), + ImportStateVerifyIdentifierAttribute: "key", + ImportStateVerifyIgnore: []string{"handler.0.secret", "handler.1.secret"}, }, }, } @@ -483,7 +480,7 @@ func testCheckWebhook(id string, request *resty.Request) (*resty.Response, error } func TestAccWebhook_GH476WebHookChangeBearerSet0(t *testing.T) { - _, fqrn, name := testutil.MkNames("foo", "artifactory_artifact_webhook") + _, fqrn, name := testutil.MkNames("test-webhook", "artifactory_artifact_webhook") format := ` resource "artifactory_artifact_webhook" "{{ .webhookName }}" { @@ -536,14 +533,14 @@ func TestAccWebhook_GH476WebHookChangeBearerSet0(t *testing.T) { }, ) resource.Test(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: 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, diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_webhook_user.go b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_user.go index 5695d8ff9..9f02176b0 100644 --- a/pkg/artifactory/resource/webhook/resource_artifactory_webhook_user.go +++ b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_user.go @@ -14,6 +14,6 @@ var packEmptyCriteria = func(artifactoryCriteria map[string]interface{}) map[str return map[string]interface{}{} } -var unpackEmptyCriteria = func(terraformCriteria map[string]interface{}, baseCriteria BaseWebhookCriteria) interface{} { +var unpackEmptyCriteria = func(terraformCriteria map[string]interface{}, baseCriteria BaseCriteriaAPIModel) interface{} { return EmptyWebhookCriteria{} } From f30d3ff2869fdccc8737da72fb4efb239ac5eb3e Mon Sep 17 00:00:00 2001 From: Alex Hung Date: Thu, 26 Sep 2024 14:25:15 -0700 Subject: [PATCH 03/17] Migrate artifact property and docker webhooks Refactor out the common repository based resource code --- pkg/artifactory/provider/framework.go | 2 + pkg/artifactory/provider/resources.go | 2 +- .../resource_artifactory_artifact_webhook.go | 636 ------------------ .../webhook/resource_artifactory_webhook.go | 595 ++++++++++++---- .../resource_artifactory_webhook_base.go | 5 - .../resource_artifactory_webhook_repo.go | 538 +++++++++++++-- .../resource_artifactory_webhook_test.go | 2 +- 7 files changed, 924 insertions(+), 856 deletions(-) delete mode 100644 pkg/artifactory/resource/webhook/resource_artifactory_artifact_webhook.go diff --git a/pkg/artifactory/provider/framework.go b/pkg/artifactory/provider/framework.go index c6a65d285..bf6db17f3 100644 --- a/pkg/artifactory/provider/framework.go +++ b/pkg/artifactory/provider/framework.go @@ -223,6 +223,8 @@ func (p *ArtifactoryProvider) Resources(ctx context.Context) []func() resource.R replication.NewLocalRepositoryMultiReplicationResource, replication.NewRemoteRepositoryReplicationResource, webhook.NewArtifactWebhookResource, + webhook.NewArtifactPropertyWebhookResource, + webhook.NewDockerWebhookResource, } } diff --git a/pkg/artifactory/provider/resources.go b/pkg/artifactory/provider/resources.go index 0051461be..ee2e6fd28 100644 --- a/pkg/artifactory/provider/resources.go +++ b/pkg/artifactory/provider/resources.go @@ -124,7 +124,7 @@ func resourcesMap() map[string]*schema.Resource { resourcesMap[federatedResourceName] = federated.ResourceArtifactoryFederatedGenericRepository(repoType) } - for _, webhookType := range webhook.TypesSupported { + for _, webhookType := range webhook.DomainSupported { webhookResourceName := fmt.Sprintf("artifactory_%s_webhook", webhookType) resourcesMap[webhookResourceName] = webhook.ResourceArtifactoryWebhook(webhookType) webhookCustomResourceName := fmt.Sprintf("artifactory_%s_custom_webhook", webhookType) diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_artifact_webhook.go b/pkg/artifactory/resource/webhook/resource_artifactory_artifact_webhook.go deleted file mode 100644 index 197d14646..000000000 --- a/pkg/artifactory/resource/webhook/resource_artifactory_artifact_webhook.go +++ /dev/null @@ -1,636 +0,0 @@ -package webhook - -import ( - "context" - "fmt" - "net/http" - "regexp" - "strings" - - "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/planmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" - "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" - utilfw "github.com/jfrog/terraform-provider-shared/util/fw" - validatorfw_string "github.com/jfrog/terraform-provider-shared/validator/fw/string" - "github.com/samber/lo" -) - -var _ resource.Resource = &ArtifactWebhookResource{} - -func NewArtifactWebhookResource() resource.Resource { - return &ArtifactWebhookResource{ - TypeName: "artifactory_artifact_webhook", - } -} - -type ArtifactWebhookResource struct { - ProviderData util.ProviderMetadata - TypeName string -} - -type ArtifactWebhookResourceModel struct { - ID types.String `tfsdk:"id"` - Key types.String `tfsdk:"key"` - Description types.String `tfsdk:"description"` - Enabled types.Bool `tfsdk:"enabled"` - EventTypes types.Set `tfsdk:"event_types"` - Criteria types.Set `tfsdk:"criteria"` - Handlers types.Set `tfsdk:"handler"` -} - -func (m ArtifactWebhookResourceModel) toAPIModel(ctx context.Context, apiModel *ArtifactWebhookAPIModel) (diags diag.Diagnostics) { - critieriaObj := m.Criteria.Elements()[0].(types.Object) - critieriaAttrs := critieriaObj.Attributes() - - var includePatterns []string - d := critieriaAttrs["include_patterns"].(types.Set).ElementsAs(ctx, &includePatterns, false) - if d.HasError() { - diags.Append(d...) - } - - var excludePatterns []string - d = critieriaAttrs["exclude_patterns"].(types.Set).ElementsAs(ctx, &excludePatterns, false) - 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...) - } - - var eventTypes []string - d = m.EventTypes.ElementsAs(ctx, &eventTypes, false) - if d.HasError() { - diags.Append(d...) - } - - handlers := lo.Map( - m.Handlers.Elements(), - func(elem attr.Value, _ int) 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).ValueString(), - UseSecretForSigning: attrs["use_secret_for_signing"].(types.Bool).ValueBool(), - Proxy: attrs["proxy"].(types.String).ValueString(), - CustomHttpHeaders: customHttpHeaders, - } - }, - ) - - *apiModel = ArtifactWebhookAPIModel{ - BaseAPIModel: BaseAPIModel{ - Key: m.Key.ValueString(), - Description: m.Description.ValueString(), - Enabled: m.Enabled.ValueBool(), - EventFilter: EventFilterAPIModel{ - Domain: ArtifactType, - EventTypes: eventTypes, - Criteria: RepoCriteriaAPIModel{ - BaseCriteriaAPIModel: BaseCriteriaAPIModel{ - IncludePatterns: includePatterns, - ExcludePatterns: excludePatterns, - }, - AnyLocal: critieriaAttrs["any_local"].(types.Bool).ValueBool(), - AnyRemote: critieriaAttrs["any_remote"].(types.Bool).ValueBool(), - AnyFederated: critieriaAttrs["any_federated"].(types.Bool).ValueBool(), - RepoKeys: repoKeys, - }, - }, - Handlers: handlers, - }, - } - - return -} - -var criteriaSetResourceModelAttributeTypes = map[string]attr.Type{ - "include_patterns": types.SetType{ElemType: types.StringType}, - "exclude_patterns": types.SetType{ElemType: types.StringType}, - "any_local": types.BoolType, - "any_remote": types.BoolType, - "any_federated": types.BoolType, - "repo_keys": types.SetType{ElemType: types.StringType}, -} - -var criteriaSetResourceModelElementTypes = types.ObjectType{ - AttrTypes: criteriaSetResourceModelAttributeTypes, -} - -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}, -} - -var handlerSetResourceModelElementTypes = types.ObjectType{ - AttrTypes: handlerSetResourceModelAttributeTypes, -} - -func (m *ArtifactWebhookResourceModel) fromAPIModel(ctx context.Context, apiModel ArtifactWebhookAPIModel, stateHandlers basetypes.SetValue) diag.Diagnostics { - diags := diag.Diagnostics{} - - m.ID = types.StringValue(apiModel.Key) - m.Key = types.StringValue(apiModel.Key) - - description := types.StringNull() - if apiModel.Description != "" { - description = types.StringValue(apiModel.Description) - } - m.Description = description - - m.Enabled = types.BoolValue(apiModel.Enabled) - - eventTypes, d := types.SetValueFrom(ctx, types.StringType, apiModel.EventFilter.EventTypes) - if d.HasError() { - diags.Append(d...) - } - m.EventTypes = eventTypes - - criteriaAPIModel := apiModel.EventFilter.Criteria.(map[string]interface{}) - - includePatterns := types.SetNull(types.StringType) - if v, ok := criteriaAPIModel["includePatterns"]; ok && v != nil { - ps, d := types.SetValueFrom(ctx, types.StringType, v) - if d.HasError() { - diags.Append(d...) - } - - includePatterns = ps - } - - excludePatterns := types.SetNull(types.StringType) - if v, ok := criteriaAPIModel["excludePatterns"]; ok && v != nil { - ps, d := types.SetValueFrom(ctx, types.StringType, v) - if d.HasError() { - diags.Append(d...) - } - - excludePatterns = ps - } - - repoKeys := types.SetNull(types.StringType) - if v, ok := criteriaAPIModel["repoKeys"]; ok && v != nil { - ks, d := types.SetValueFrom(ctx, types.StringType, v) - if d.HasError() { - diags.Append(d...) - } - - repoKeys = ks - } - - criteria, d := types.ObjectValue( - criteriaSetResourceModelAttributeTypes, - map[string]attr.Value{ - "include_patterns": includePatterns, - "exclude_patterns": excludePatterns, - "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( - criteriaSetResourceModelElementTypes, - []attr.Value{criteria}, - ) - if d.HasError() { - diags.Append(d...) - } - m.Criteria = criteriaSet - - 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...) - } - - customHttpHeaders = h - } - - secret := types.StringValue("") - 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() - if !attrs["secret"].(types.String).IsNull() { - secret = attrs["secret"].(types.String) - } - } - - proxy := types.StringNull() - if handler.Proxy != "" { - proxy = types.StringValue(handler.Proxy) - } - - h, d := types.ObjectValue( - handlerSetResourceModelAttributeTypes, - map[string]attr.Value{ - "url": types.StringValue(handler.Url), - "secret": secret, - "use_secret_for_signing": types.BoolValue(handler.UseSecretForSigning), - "proxy": proxy, - "custom_http_headers": customHttpHeaders, - }, - ) - if d.HasError() { - diags.Append(d...) - } - - return h - }, - ) - - handlersSet, d := types.SetValue( - handlerSetResourceModelElementTypes, - handlers, - ) - if d.HasError() { - diags.Append(d...) - } - m.Handlers = handlersSet - - return diags -} - -type ArtifactWebhookAPIModel struct { - BaseAPIModel -} - -func (r *ArtifactWebhookResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = r.TypeName -} - -func (r *ArtifactWebhookResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { - resp.Schema = schema.Schema{ - Version: 2, - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ // for backward compatability - Computed: true, - }, - "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[ArtifactType]...), - ), - }, - 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[ArtifactType], ", "), "[]")), - }, - }, - Blocks: map[string]schema.Block{ - "criteria": schema.SetNestedBlock{ - NestedObject: schema.NestedBlockObject{ - Attributes: map[string]schema.Attribute{ - "include_patterns": schema.SetAttribute{ - ElementType: types.StringType, - Optional: true, - MarkdownDescription: "Simple comma separated wildcard patterns for repository artifact paths (with no leading slash).\nAnt-style path expressions are supported (*, **, ?).\nFor example: `org/apache/**`", - }, - "exclude_patterns": schema.SetAttribute{ - ElementType: types.StringType, - Optional: true, - MarkdownDescription: "Simple comma separated wildcard patterns for repository artifact paths (with no leading slash).\nAnt-style path expressions are supported (*, **, ?).\nFor example: `org/apache/**`", - }, - "any_local": schema.BoolAttribute{ - Required: true, - Description: "Trigger on any local repositories", - }, - "any_remote": schema.BoolAttribute{ - Required: true, - Description: "Trigger on any remote repositories", - }, - "any_federated": schema.BoolAttribute{ - Required: true, - Description: "Trigger on any federated repositories", - }, - "repo_keys": schema.SetAttribute{ - ElementType: types.StringType, - Required: true, - Description: "Trigger on this list of repository keys", - }, - }, - }, - Validators: []validator.Set{ - setvalidator.SizeBetween(1, 1), - setvalidator.IsRequired(), - }, - Description: "Specifies where the webhook will be applied on which repositories.", - }, - "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, - Computed: true, - Default: stringdefault.StaticString(""), // for backward compatability - // Sensitive: true, - Description: "Secret authentication token that will be sent to the configured URL.", - }, - "use_secret_for_signing": schema.BoolAttribute{ - Optional: true, - Computed: true, - Default: booldefault.StaticBool(false), - 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"), - }, - 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), - }, - }, - }, - MarkdownDescription: "This resource enables you to creates a new Release Bundle v2, uniquely identified by a combination of repository key, name, and version. For more information, see [Understanding Release Bundles v2](https://jfrog.com/help/r/jfrog-artifactory-documentation/understanding-release-bundles-v2) and [REST API](https://jfrog.com/help/r/jfrog-rest-apis/create-release-bundle-v2-version).", - } -} - -func (r *ArtifactWebhookResource) 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) -} - -func (r ArtifactWebhookResource) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) { - var data ArtifactWebhookResourceModel - - 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 *ArtifactWebhookResource) 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 ArtifactWebhookResourceModel - - // Read Terraform plan data into the model - resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) - if resp.Diagnostics.HasError() { - return - } - - var webhook ArtifactWebhookAPIModel - resp.Diagnostics.Append(plan.toAPIModel(ctx, &webhook)...) - if resp.Diagnostics.HasError() { - return - } - - 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 - } - - if response.IsError() { - utilfw.UnableToCreateResourceError(resp, artifactoryError.String()) - return - } - - plan.ID = plan.Key - - // Save data into Terraform state - resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) -} - -func (r *ArtifactWebhookResource) 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 ArtifactWebhookResourceModel - - // Read Terraform state data into the model - resp.Diagnostics.Append(req.State.Get(ctx, &state)...) - if resp.Diagnostics.HasError() { - return - } - - var webhook ArtifactWebhookAPIModel - var artifactoryError artifactory.ArtifactoryErrorsResponse - - response, err := r.ProviderData.Client.R(). - SetPathParam("webhookKey", state.Key.ValueString()). - SetResult(&webhook). - SetError(&artifactoryError). - Get(WebhookURL) - if err != nil { - utilfw.UnableToRefreshResourceError(resp, err.Error()) - return - } - - if response.StatusCode() == http.StatusNotFound { - resp.State.RemoveResource(ctx) - return - } - - if response.IsError() { - utilfw.UnableToRefreshResourceError(resp, artifactoryError.String()) - 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 *ArtifactWebhookResource) 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 ArtifactWebhookResourceModel - - // Read Terraform plan data into the model - resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) - if resp.Diagnostics.HasError() { - return - } - - var webhook ArtifactWebhookAPIModel - resp.Diagnostics.Append(plan.toAPIModel(ctx, &webhook)...) - if resp.Diagnostics.HasError() { - return - } - - var artifactoryError artifactory.ArtifactoryErrorsResponse - response, err := r.ProviderData.Client.R(). - SetPathParam("webhookKey", plan.Key.ValueString()). - SetBody(webhook). - AddRetryCondition(retryOnProxyError). - SetError(&artifactoryError). - Put(WebhookURL) - if err != nil { - utilfw.UnableToUpdateResourceError(resp, err.Error()) - return - } - if response.IsError() { - utilfw.UnableToUpdateResourceError(resp, artifactoryError.String()) - return - } - - plan.ID = plan.Key - - // Save data into Terraform state - resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) -} - -func (r *ArtifactWebhookResource) 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 ArtifactWebhookResourceModel - - // Read Terraform prior state data into the model - resp.Diagnostics.Append(req.State.Get(ctx, &state)...) - - var artifactoryError artifactory.ArtifactoryErrorsResponse - response, err := r.ProviderData.Client.R(). - SetPathParam("webhookKey", state.Key.ValueString()). - SetError(&artifactoryError). - Delete(WebhookURL) - - if err != nil { - utilfw.UnableToDeleteResourceError(resp, err.Error()) - return - } - - if response.StatusCode() == http.StatusNotFound { - resp.State.RemoveResource(ctx) - return - } - - if response.IsError() { - utilfw.UnableToDeleteResourceError(resp, artifactoryError.String()) - 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 *ArtifactWebhookResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { - resource.ImportStatePassthroughID(ctx, path.Root("key"), req, resp) -} diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_webhook.go b/pkg/artifactory/resource/webhook/resource_artifactory_webhook.go index 8c78f8275..fa2bfc01e 100644 --- a/pkg/artifactory/resource/webhook/resource_artifactory_webhook.go +++ b/pkg/artifactory/resource/webhook/resource_artifactory_webhook.go @@ -5,15 +5,32 @@ import ( "fmt" "net/http" "regexp" + "strings" "github.com/go-resty/resty/v2" + "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/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "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/hashicorp/terraform-plugin-log/tflog" - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + sdkv2_diag "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + sdkv2_schema "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/jfrog/terraform-provider-artifactory/v12/pkg/artifactory" "github.com/jfrog/terraform-provider-shared/util" utilsdk "github.com/jfrog/terraform-provider-shared/util/sdk" + validatorfw_string "github.com/jfrog/terraform-provider-shared/validator/fw/string" + "github.com/samber/lo" "golang.org/x/exp/slices" ) @@ -22,53 +39,349 @@ const ( webhooksURL = "/event/api/v1/subscriptions" WebhookURL = "/event/api/v1/subscriptions/{webhookKey}" - ArtifactLifecycleType = "artifact_lifecycle" - ArtifactPropertyType = "artifact_property" - ArtifactType = "artifact" - ArtifactoryReleaseBundleType = "artifactory_release_bundle" - BuildType = "build" - DestinationType = "destination" - DistributionType = "distribution" - DockerType = "docker" - ReleaseBundleType = "release_bundle" - ReleaseBundleV2Type = "release_bundle_v2" - ReleaseBundleV2PromotionType = "release_bundle_v2_promotion" - UserType = "user" + 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" ) const currentSchemaVersion = 2 -var TypesSupported = []string{ - ArtifactLifecycleType, - ArtifactPropertyType, - // ArtifactType, - ArtifactoryReleaseBundleType, - BuildType, - DestinationType, - DistributionType, - DockerType, - ReleaseBundleType, - ReleaseBundleV2Type, - ReleaseBundleV2PromotionType, - UserType, +var DomainSupported = []string{ + ArtifactLifecycleDomain, + ArtifactoryReleaseBundleDomain, + BuildDomain, + DestinationDomain, + DistributionDomain, + ReleaseBundleDomain, + ReleaseBundleV2Domain, + ReleaseBundleV2PromotionDomain, + UserDomain, } var DomainEventTypesSupported = map[string][]string{ - ArtifactType: {"deployed", "deleted", "moved", "copied", "cached"}, - ArtifactPropertyType: {"added", "deleted"}, - DockerType: {"pushed", "deleted", "promoted"}, - BuildType: {"uploaded", "deleted", "promoted"}, - ReleaseBundleType: {"created", "signed", "deleted"}, - DistributionType: {"distribute_started", "distribute_completed", "distribute_aborted", "distribute_failed", "delete_started", "delete_completed", "delete_failed"}, - ArtifactoryReleaseBundleType: {"received", "delete_started", "delete_completed", "delete_failed"}, - DestinationType: {"received", "delete_started", "delete_completed", "delete_failed"}, - UserType: {"locked"}, - ReleaseBundleV2Type: {"release_bundle_v2_started", "release_bundle_v2_failed", "release_bundle_v2_completed"}, - ReleaseBundleV2PromotionType: {"release_bundle_v2_promotion_completed", "release_bundle_v2_promotion_failed", "release_bundle_v2_promotion_started"}, - ArtifactLifecycleType: {"archive", "restore"}, + 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 BaseAPIModel struct { +type WebhookResource struct { + ProviderData util.ProviderMetadata + TypeName string + Domain string + Description string +} + +func (r *WebhookResource) schema(domain string, criteriaBlock schema.SetNestedBlock) schema.Schema { + return schema.Schema{ + Version: 2, + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ // for backward compatability + Computed: true, + }, + "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: map[string]schema.Block{ + "criteria": criteriaBlock, + "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, + Computed: true, + Default: stringdefault.StaticString(""), // for backward compatability + // Sensitive: true, + Description: "Secret authentication token that will be sent to the configured URL.", + }, + "use_secret_for_signing": schema.BoolAttribute{ + Optional: true, + Computed: true, + Default: booldefault.StaticBool(false), + 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"), + }, + 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), + }, + }, + }, + MarkdownDescription: r.Description, + } +} + +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) +} + +// 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) +} + +type WebhookResourceModel struct { + ID types.String `tfsdk:"id"` + Key types.String `tfsdk:"key"` + Description types.String `tfsdk:"description"` + Enabled types.Bool `tfsdk:"enabled"` + EventTypes types.Set `tfsdk:"event_types"` + Criteria types.Set `tfsdk:"criteria"` + Handlers types.Set `tfsdk:"handler"` +} + +func (m WebhookResourceModel) toAPIModel(ctx context.Context, domain string, toCriteriaAPIModel interface{}, apiModel *WebhookAPIModel) (diags diag.Diagnostics) { + critieriaObj := m.Criteria.Elements()[0].(types.Object) + critieriaAttrs := critieriaObj.Attributes() + + var includePatterns []string + d := critieriaAttrs["include_patterns"].(types.Set).ElementsAs(ctx, &includePatterns, false) + if d.HasError() { + diags.Append(d...) + } + + var excludePatterns []string + d = critieriaAttrs["exclude_patterns"].(types.Set).ElementsAs(ctx, &excludePatterns, false) + 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...) + } + + var eventTypes []string + d = m.EventTypes.ElementsAs(ctx, &eventTypes, false) + if d.HasError() { + diags.Append(d...) + } + + handlers := lo.Map( + m.Handlers.Elements(), + func(elem attr.Value, _ int) 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).ValueString(), + UseSecretForSigning: attrs["use_secret_for_signing"].(types.Bool).ValueBool(), + Proxy: attrs["proxy"].(types.String).ValueString(), + CustomHttpHeaders: customHttpHeaders, + } + }, + ) + + *apiModel = WebhookAPIModel{ + Key: m.Key.ValueString(), + Description: m.Description.ValueString(), + Enabled: m.Enabled.ValueBool(), + EventFilter: EventFilterAPIModel{ + Domain: domain, + EventTypes: eventTypes, + Criteria: toCriteriaAPIModel, + }, + Handlers: handlers, + } + + return +} + +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}, +} + +var handlerSetResourceModelElementTypes = types.ObjectType{ + AttrTypes: handlerSetResourceModelAttributeTypes, +} + +func (m *WebhookResourceModel) fromAPIModel(ctx context.Context, apiModel WebhookAPIModel, stateHandlers basetypes.SetValue, criteriaSet basetypes.SetValue) diag.Diagnostics { + diags := diag.Diagnostics{} + + m.ID = types.StringValue(apiModel.Key) + m.Key = types.StringValue(apiModel.Key) + + description := types.StringNull() + if apiModel.Description != "" { + description = types.StringValue(apiModel.Description) + } + m.Description = description + + m.Enabled = types.BoolValue(apiModel.Enabled) + + eventTypes, d := types.SetValueFrom(ctx, types.StringType, apiModel.EventFilter.EventTypes) + if d.HasError() { + diags.Append(d...) + } + m.EventTypes = eventTypes + m.Criteria = criteriaSet + + 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...) + } + + customHttpHeaders = h + } + + secret := types.StringValue("") + 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() + if !attrs["secret"].(types.String).IsNull() { + secret = attrs["secret"].(types.String) + } + } + + proxy := types.StringNull() + if handler.Proxy != "" { + proxy = types.StringValue(handler.Proxy) + } + + h, d := types.ObjectValue( + handlerSetResourceModelAttributeTypes, + map[string]attr.Value{ + "url": types.StringValue(handler.Url), + "secret": secret, + "use_secret_for_signing": types.BoolValue(handler.UseSecretForSigning), + "proxy": proxy, + "custom_http_headers": customHttpHeaders, + }, + ) + if d.HasError() { + diags.Append(d...) + } + + return h + }, + ) + + handlersSet, d := types.SetValue( + handlerSetResourceModelElementTypes, + handlers, + ) + if d.HasError() { + diags.Append(d...) + } + m.Handlers = handlersSet + + return diags +} + +type WebhookAPIModel struct { Key string `json:"key"` Description string `json:"description"` Enabled bool `json:"enabled"` @@ -76,7 +389,7 @@ type BaseAPIModel struct { Handlers []HandlerAPIModel `json:"handlers"` } -func (w BaseAPIModel) Id() string { +func (w WebhookAPIModel) Id() string { return w.Key } @@ -86,6 +399,11 @@ type EventFilterAPIModel struct { Criteria interface{} `json:"criteria"` } +type BaseCriteriaAPIModel struct { + IncludePatterns []string `json:"includePatterns"` + ExcludePatterns []string `json:"excludePatterns"` +} + type HandlerAPIModel struct { HandlerType string `json:"handler_type"` Url string `json:"url"` @@ -123,64 +441,52 @@ var packKeyValuePair = func(keyValuePairs []KeyValuePairAPIModel) map[string]int } var domainCriteriaLookup = map[string]interface{}{ - ArtifactType: RepoCriteriaAPIModel{}, - ArtifactPropertyType: RepoCriteriaAPIModel{}, - DockerType: RepoCriteriaAPIModel{}, - BuildType: BuildWebhookCriteria{}, - ReleaseBundleType: ReleaseBundleWebhookCriteria{}, - DistributionType: ReleaseBundleWebhookCriteria{}, - ArtifactoryReleaseBundleType: ReleaseBundleWebhookCriteria{}, - DestinationType: ReleaseBundleWebhookCriteria{}, - UserType: EmptyWebhookCriteria{}, - ReleaseBundleV2Type: ReleaseBundleV2WebhookCriteria{}, - ReleaseBundleV2PromotionType: ReleaseBundleV2PromotionWebhookCriteria{}, - ArtifactLifecycleType: EmptyWebhookCriteria{}, + BuildDomain: BuildWebhookCriteria{}, + ReleaseBundleDomain: ReleaseBundleWebhookCriteria{}, + DistributionDomain: ReleaseBundleWebhookCriteria{}, + ArtifactoryReleaseBundleDomain: ReleaseBundleWebhookCriteria{}, + DestinationDomain: ReleaseBundleWebhookCriteria{}, + UserDomain: EmptyWebhookCriteria{}, + ReleaseBundleV2Domain: ReleaseBundleV2WebhookCriteria{}, + ReleaseBundleV2PromotionDomain: ReleaseBundleV2PromotionWebhookCriteria{}, + ArtifactLifecycleDomain: EmptyWebhookCriteria{}, } var domainPackLookup = map[string]func(map[string]interface{}) map[string]interface{}{ - ArtifactType: packRepoCriteria, - ArtifactPropertyType: packRepoCriteria, - DockerType: packRepoCriteria, - BuildType: packBuildCriteria, - ReleaseBundleType: packReleaseBundleCriteria, - DistributionType: packReleaseBundleCriteria, - ArtifactoryReleaseBundleType: packReleaseBundleCriteria, - DestinationType: packReleaseBundleCriteria, - UserType: packEmptyCriteria, - ReleaseBundleV2Type: packReleaseBundleV2Criteria, - ReleaseBundleV2PromotionType: packReleaseBundleV2PromotionCriteria, - ArtifactLifecycleType: packEmptyCriteria, + BuildDomain: packBuildCriteria, + ReleaseBundleDomain: packReleaseBundleCriteria, + DistributionDomain: packReleaseBundleCriteria, + ArtifactoryReleaseBundleDomain: packReleaseBundleCriteria, + DestinationDomain: packReleaseBundleCriteria, + UserDomain: packEmptyCriteria, + ReleaseBundleV2Domain: packReleaseBundleV2Criteria, + ReleaseBundleV2PromotionDomain: packReleaseBundleV2PromotionCriteria, + ArtifactLifecycleDomain: packEmptyCriteria, } var domainUnpackLookup = map[string]func(map[string]interface{}, BaseCriteriaAPIModel) interface{}{ - ArtifactType: unpackRepoCriteria, - ArtifactPropertyType: unpackRepoCriteria, - DockerType: unpackRepoCriteria, - BuildType: unpackBuildCriteria, - ReleaseBundleType: unpackReleaseBundleCriteria, - DistributionType: unpackReleaseBundleCriteria, - ArtifactoryReleaseBundleType: unpackReleaseBundleCriteria, - DestinationType: unpackReleaseBundleCriteria, - UserType: unpackEmptyCriteria, - ReleaseBundleV2Type: unpackReleaseBundleV2Criteria, - ReleaseBundleV2PromotionType: unpackReleaseBundleV2PromotionCriteria, - ArtifactLifecycleType: unpackEmptyCriteria, + BuildDomain: unpackBuildCriteria, + ReleaseBundleDomain: unpackReleaseBundleCriteria, + DistributionDomain: unpackReleaseBundleCriteria, + ArtifactoryReleaseBundleDomain: unpackReleaseBundleCriteria, + DestinationDomain: unpackReleaseBundleCriteria, + UserDomain: unpackEmptyCriteria, + ReleaseBundleV2Domain: unpackReleaseBundleV2Criteria, + ReleaseBundleV2PromotionDomain: unpackReleaseBundleV2PromotionCriteria, + ArtifactLifecycleDomain: unpackEmptyCriteria, } -var domainSchemaLookup = func(version int, isCustom bool, webhookType string) map[string]map[string]*schema.Schema { - return map[string]map[string]*schema.Schema{ - ArtifactType: repoWebhookSchema(webhookType, version, isCustom), - ArtifactPropertyType: repoWebhookSchema(webhookType, version, isCustom), - DockerType: repoWebhookSchema(webhookType, version, isCustom), - BuildType: buildWebhookSchema(webhookType, version, isCustom), - ReleaseBundleType: releaseBundleWebhookSchema(webhookType, version, isCustom), - DistributionType: releaseBundleWebhookSchema(webhookType, version, isCustom), - ArtifactoryReleaseBundleType: releaseBundleWebhookSchema(webhookType, version, isCustom), - DestinationType: releaseBundleWebhookSchema(webhookType, version, isCustom), - UserType: userWebhookSchema(webhookType, version, isCustom), - ReleaseBundleV2Type: releaseBundleV2WebhookSchema(webhookType, version, isCustom), - ReleaseBundleV2PromotionType: releaseBundleV2PromotionWebhookSchema(webhookType, version, isCustom), - ArtifactLifecycleType: artifactLifecycleWebhookSchema(webhookType, version, isCustom), +var domainSchemaLookup = func(version int, isCustom bool, webhookType string) map[string]map[string]*sdkv2_schema.Schema { + return map[string]map[string]*sdkv2_schema.Schema{ + BuildDomain: buildWebhookSchema(webhookType, version, isCustom), + ReleaseBundleDomain: releaseBundleWebhookSchema(webhookType, version, isCustom), + DistributionDomain: releaseBundleWebhookSchema(webhookType, version, isCustom), + ArtifactoryReleaseBundleDomain: releaseBundleWebhookSchema(webhookType, version, isCustom), + DestinationDomain: releaseBundleWebhookSchema(webhookType, version, isCustom), + UserDomain: userWebhookSchema(webhookType, version, isCustom), + ReleaseBundleV2Domain: releaseBundleV2WebhookSchema(webhookType, version, isCustom), + ReleaseBundleV2PromotionDomain: releaseBundleV2PromotionWebhookSchema(webhookType, version, isCustom), + ArtifactLifecycleDomain: artifactLifecycleWebhookSchema(webhookType, version, isCustom), } } @@ -188,13 +494,13 @@ var unpackCriteria = func(d *utilsdk.ResourceData, webhookType string) interface var webhookCriteria interface{} if v, ok := d.GetOk("criteria"); ok { - criteria := v.(*schema.Set).List() + criteria := v.(*sdkv2_schema.Set).List() if len(criteria) == 1 { id := criteria[0].(map[string]interface{}) baseCriteria := BaseCriteriaAPIModel{ - IncludePatterns: utilsdk.CastToStringArr(id["include_patterns"].(*schema.Set).List()), - ExcludePatterns: utilsdk.CastToStringArr(id["exclude_patterns"].(*schema.Set).List()), + IncludePatterns: utilsdk.CastToStringArr(id["include_patterns"].(*sdkv2_schema.Set).List()), + ExcludePatterns: utilsdk.CastToStringArr(id["exclude_patterns"].(*sdkv2_schema.Set).List()), } webhookCriteria = domainUnpackLookup[webhookType](id, baseCriteria) @@ -204,51 +510,48 @@ var unpackCriteria = func(d *utilsdk.ResourceData, webhookType string) interface return webhookCriteria } -var packCriteria = func(d *schema.ResourceData, webhookType string, criteria map[string]interface{}) []error { +var packCriteria = func(d *sdkv2_schema.ResourceData, webhookType string, criteria map[string]interface{}) []error { setValue := utilsdk.MkLens(d) - resource := domainSchemaLookup(currentSchemaVersion, false, webhookType)[webhookType]["criteria"].Elem.(*schema.Resource) + resource := domainSchemaLookup(currentSchemaVersion, false, webhookType)[webhookType]["criteria"].Elem.(*sdkv2_schema.Resource) packedCriteria := domainPackLookup[webhookType](criteria) includePatterns := []interface{}{} if v, ok := criteria["includePatterns"]; ok && v != nil { includePatterns = v.([]interface{}) } - packedCriteria["include_patterns"] = schema.NewSet(schema.HashString, includePatterns) + packedCriteria["include_patterns"] = sdkv2_schema.NewSet(sdkv2_schema.HashString, includePatterns) excludePatterns := []interface{}{} if v, ok := criteria["excludePatterns"]; ok && v != nil { excludePatterns = v.([]interface{}) } - packedCriteria["exclude_patterns"] = schema.NewSet(schema.HashString, excludePatterns) + packedCriteria["exclude_patterns"] = sdkv2_schema.NewSet(sdkv2_schema.HashString, excludePatterns) - return setValue("criteria", schema.NewSet(schema.HashResource(resource), []interface{}{packedCriteria})) + return setValue("criteria", sdkv2_schema.NewSet(sdkv2_schema.HashResource(resource), []interface{}{packedCriteria})) } var domainCriteriaValidationLookup = map[string]func(context.Context, map[string]interface{}) error{ - ArtifactType: repoCriteriaValidation, - ArtifactPropertyType: repoCriteriaValidation, - DockerType: repoCriteriaValidation, - BuildType: buildCriteriaValidation, - ReleaseBundleType: releaseBundleCriteriaValidation, - DistributionType: releaseBundleCriteriaValidation, - ArtifactoryReleaseBundleType: releaseBundleCriteriaValidation, - DestinationType: releaseBundleCriteriaValidation, - UserType: emptyCriteriaValidation, - ReleaseBundleV2Type: releaseBundleV2CriteriaValidation, - ReleaseBundleV2PromotionType: emptyCriteriaValidation, - ArtifactLifecycleType: emptyCriteriaValidation, + BuildDomain: buildCriteriaValidation, + ReleaseBundleDomain: releaseBundleCriteriaValidation, + DistributionDomain: releaseBundleCriteriaValidation, + ArtifactoryReleaseBundleDomain: releaseBundleCriteriaValidation, + DestinationDomain: releaseBundleCriteriaValidation, + UserDomain: emptyCriteriaValidation, + ReleaseBundleV2Domain: releaseBundleV2CriteriaValidation, + ReleaseBundleV2PromotionDomain: emptyCriteriaValidation, + ArtifactLifecycleDomain: emptyCriteriaValidation, } var emptyCriteriaValidation = func(ctx context.Context, criteria map[string]interface{}) error { return nil } -var packSecret = func(d *schema.ResourceData, url string) string { +var packSecret = func(d *sdkv2_schema.ResourceData, url string) string { // Get secret from TF state var secret string if v, ok := d.GetOk("handler"); ok { - handlers := v.(*schema.Set).List() + handlers := v.(*sdkv2_schema.Set).List() for _, handler := range handlers { h := handler.(map[string]interface{}) // if urls match, assign the secret value from the state @@ -267,16 +570,16 @@ var retryOnProxyError = func(response *resty.Response, _r error) bool { return proxyNotFoundRegex.MatchString(string(response.Body()[:])) } -func ResourceArtifactoryWebhook(webhookType string) *schema.Resource { +func ResourceArtifactoryWebhook(webhookType string) *sdkv2_schema.Resource { - var unpackWebhook = func(data *schema.ResourceData) (BaseAPIModel, error) { + var unpackWebhook = func(data *sdkv2_schema.ResourceData) (WebhookAPIModel, error) { d := &utilsdk.ResourceData{ResourceData: data} var unpackHandlers = func(d *utilsdk.ResourceData) []HandlerAPIModel { var webhookHandlers []HandlerAPIModel if v, ok := d.GetOk("handler"); ok { - handlers := v.(*schema.Set).List() + handlers := v.(*sdkv2_schema.Set).List() for _, handler := range handlers { h := handler.(map[string]interface{}) // use this to filter out weirdness with terraform adding an extra blank webhook in a set @@ -311,7 +614,7 @@ func ResourceArtifactoryWebhook(webhookType string) *schema.Resource { return webhookHandlers } - webhook := BaseAPIModel{ + webhook := WebhookAPIModel{ Key: d.GetString("key", false), Description: d.GetString("description", false), Enabled: d.GetBool("enabled", false), @@ -326,9 +629,9 @@ func ResourceArtifactoryWebhook(webhookType string) *schema.Resource { return webhook, nil } - var packHandlers = func(d *schema.ResourceData, handlers []HandlerAPIModel) []error { + var packHandlers = func(d *sdkv2_schema.ResourceData, handlers []HandlerAPIModel) []error { setValue := utilsdk.MkLens(d) - resource := domainSchemaLookup(currentSchemaVersion, false, webhookType)[webhookType]["handler"].Elem.(*schema.Resource) + resource := domainSchemaLookup(currentSchemaVersion, false, webhookType)[webhookType]["handler"].Elem.(*sdkv2_schema.Resource) var packedHandlers []interface{} for _, handler := range handlers { packedHandler := map[string]interface{}{ @@ -345,10 +648,10 @@ func ResourceArtifactoryWebhook(webhookType string) *schema.Resource { packedHandlers = append(packedHandlers, packedHandler) } - return setValue("handler", schema.NewSet(schema.HashResource(resource), packedHandlers)) + return setValue("handler", sdkv2_schema.NewSet(sdkv2_schema.HashResource(resource), packedHandlers)) } - var packWebhook = func(d *schema.ResourceData, webhook BaseAPIModel) diag.Diagnostics { + var packWebhook = func(d *sdkv2_schema.ResourceData, webhook WebhookAPIModel) sdkv2_diag.Diagnostics { setValue := utilsdk.MkLens(d) setValue("key", webhook.Key) @@ -361,16 +664,16 @@ func ResourceArtifactoryWebhook(webhookType string) *schema.Resource { errors = append(errors, packHandlers(d, webhook.Handlers)...) if len(errors) > 0 { - return diag.Errorf("failed to pack webhook %q", errors) + return sdkv2_diag.Errorf("failed to pack webhook %q", errors) } return nil } - var readWebhook = func(ctx context.Context, data *schema.ResourceData, m interface{}) diag.Diagnostics { + var readWebhook = func(ctx context.Context, data *sdkv2_schema.ResourceData, m interface{}) sdkv2_diag.Diagnostics { tflog.Debug(ctx, "tflog.Debug(ctx, \"readWebhook\")") - webhook := BaseAPIModel{} + webhook := WebhookAPIModel{} webhook.EventFilter.Criteria = domainCriteriaLookup[webhookType] @@ -382,7 +685,7 @@ func ResourceArtifactoryWebhook(webhookType string) *schema.Resource { Get(WebhookURL) if err != nil { - return diag.FromErr(err) + return sdkv2_diag.FromErr(err) } if resp.StatusCode() == http.StatusNotFound { @@ -391,18 +694,18 @@ func ResourceArtifactoryWebhook(webhookType string) *schema.Resource { } if resp.IsError() { - return diag.Errorf("%s", artifactoryError.String()) + return sdkv2_diag.Errorf("%s", artifactoryError.String()) } return packWebhook(data, webhook) } - var createWebhook = func(ctx context.Context, data *schema.ResourceData, m interface{}) diag.Diagnostics { + var createWebhook = func(ctx context.Context, data *sdkv2_schema.ResourceData, m interface{}) sdkv2_diag.Diagnostics { tflog.Debug(ctx, "createWebhook") webhook, err := unpackWebhook(data) if err != nil { - return diag.FromErr(err) + return sdkv2_diag.FromErr(err) } var artifactoryError artifactory.ArtifactoryErrorsResponse @@ -412,11 +715,11 @@ func ResourceArtifactoryWebhook(webhookType string) *schema.Resource { SetError(&artifactoryError). Post(webhooksURL) if err != nil { - return diag.FromErr(err) + return sdkv2_diag.FromErr(err) } if resp.IsError() { - return diag.Errorf("%s", artifactoryError.String()) + return sdkv2_diag.Errorf("%s", artifactoryError.String()) } data.SetId(webhook.Id()) @@ -424,12 +727,12 @@ func ResourceArtifactoryWebhook(webhookType string) *schema.Resource { return readWebhook(ctx, data, m) } - var updateWebhook = func(ctx context.Context, data *schema.ResourceData, m interface{}) diag.Diagnostics { + var updateWebhook = func(ctx context.Context, data *sdkv2_schema.ResourceData, m interface{}) sdkv2_diag.Diagnostics { tflog.Debug(ctx, "updateWebhook") webhook, err := unpackWebhook(data) if err != nil { - return diag.FromErr(err) + return sdkv2_diag.FromErr(err) } var artifactoryError artifactory.ArtifactoryErrorsResponse @@ -440,11 +743,11 @@ func ResourceArtifactoryWebhook(webhookType string) *schema.Resource { SetError(&artifactoryError). Put(WebhookURL) if err != nil { - return diag.FromErr(err) + return sdkv2_diag.FromErr(err) } if resp.IsError() { - return diag.Errorf("%s", artifactoryError.String()) + return sdkv2_diag.Errorf("%s", artifactoryError.String()) } data.SetId(webhook.Id()) @@ -452,7 +755,7 @@ func ResourceArtifactoryWebhook(webhookType string) *schema.Resource { return readWebhook(ctx, data, m) } - var deleteWebhook = func(ctx context.Context, data *schema.ResourceData, m interface{}) diag.Diagnostics { + var deleteWebhook = func(ctx context.Context, data *sdkv2_schema.ResourceData, m interface{}) sdkv2_diag.Diagnostics { tflog.Debug(ctx, "deleteWebhook") var artifactoryError artifactory.ArtifactoryErrorsResponse @@ -462,7 +765,7 @@ func ResourceArtifactoryWebhook(webhookType string) *schema.Resource { Delete(WebhookURL) if err != nil { - return diag.FromErr(err) + return sdkv2_diag.FromErr(err) } if resp.StatusCode() == http.StatusNotFound { @@ -471,16 +774,16 @@ func ResourceArtifactoryWebhook(webhookType string) *schema.Resource { } if resp.IsError() { - return diag.Errorf("%s", artifactoryError.String()) + return sdkv2_diag.Errorf("%s", artifactoryError.String()) } return nil } - var eventTypesDiff = func(ctx context.Context, diff *schema.ResourceDiff, v interface{}) error { + var eventTypesDiff = func(ctx context.Context, diff *sdkv2_schema.ResourceDiff, v interface{}) error { tflog.Debug(ctx, "eventTypesDiff") - eventTypes := diff.Get("event_types").(*schema.Set).List() + eventTypes := diff.Get("event_types").(*sdkv2_schema.Set).List() if len(eventTypes) == 0 { return nil } @@ -494,11 +797,11 @@ func ResourceArtifactoryWebhook(webhookType string) *schema.Resource { return nil } - var criteriaDiff = func(ctx context.Context, diff *schema.ResourceDiff, v interface{}) error { + var criteriaDiff = func(ctx context.Context, diff *sdkv2_schema.ResourceDiff, v interface{}) error { tflog.Debug(ctx, "criteriaDiff") if resource, ok := diff.GetOk("criteria"); ok { - criteria := resource.(*schema.Set).List() + criteria := resource.(*sdkv2_schema.Set).List() if len(criteria) == 0 { return nil } @@ -510,23 +813,23 @@ func ResourceArtifactoryWebhook(webhookType string) *schema.Resource { // 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{ + resourceSchemaV1 := &sdkv2_schema.Resource{ Schema: domainSchemaLookup(1, false, webhookType)[webhookType], } - rs := schema.Resource{ + rs := sdkv2_schema.Resource{ SchemaVersion: 2, CreateContext: createWebhook, ReadContext: readWebhook, UpdateContext: updateWebhook, DeleteContext: deleteWebhook, - Importer: &schema.ResourceImporter{ - StateContext: schema.ImportStatePassthroughContext, + Importer: &sdkv2_schema.ResourceImporter{ + StateContext: sdkv2_schema.ImportStatePassthroughContext, }, Schema: domainSchemaLookup(currentSchemaVersion, false, webhookType)[webhookType], - StateUpgraders: []schema.StateUpgrader{ + StateUpgraders: []sdkv2_schema.StateUpgrader{ { Type: resourceSchemaV1.CoreConfigSchema().ImpliedType(), Upgrade: ResourceStateUpgradeV1, diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_webhook_base.go b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_base.go index a40080205..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 BaseCriteriaAPIModel 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_repo.go b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_repo.go index 0945ecf28..3f36e0a03 100644 --- a/pkg/artifactory/resource/webhook/resource_artifactory_webhook_repo.go +++ b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_repo.go @@ -2,93 +2,497 @@ package webhook import ( "context" - "fmt" + "net/http" - "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-artifactory/v12/pkg/artifactory" + "github.com/jfrog/terraform-provider-shared/util" + utilfw "github.com/jfrog/terraform-provider-shared/util/fw" ) -type RepoCriteriaAPIModel struct { - BaseCriteriaAPIModel - 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: "artifactory_artifact_webhook", + 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: "artifactory_artifact_property_webhook", + 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.", + }, + } } -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", - }, - }), +func NewDockerWebhookResource() resource.Resource { + return &RepoWebhookResource{ + WebhookResource: WebhookResource{ + TypeName: "artifactory_docker_webhook", + 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) { + resp.TypeName = r.TypeName +} + +func (r *RepoWebhookResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + criteriaBlock := schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "include_patterns": schema.SetAttribute{ + ElementType: types.StringType, + Optional: true, + MarkdownDescription: "Simple comma separated wildcard patterns for repository artifact paths (with no leading slash).\nAnt-style path expressions are supported (*, **, ?).\nFor example: `org/apache/**`", + }, + "exclude_patterns": schema.SetAttribute{ + ElementType: types.StringType, + Optional: true, + MarkdownDescription: "Simple comma separated wildcard patterns for repository artifact paths (with no leading slash).\nAnt-style path expressions are supported (*, **, ?).\nFor example: `org/apache/**`", + }, + "any_local": schema.BoolAttribute{ + Required: true, + Description: "Trigger on any local repositories", + }, + "any_remote": schema.BoolAttribute{ + Required: true, + Description: "Trigger on any remote repositories", + }, + "any_federated": schema.BoolAttribute{ + Required: true, + Description: "Trigger on any federated repositories", + }, + "repo_keys": schema.SetAttribute{ + ElementType: types.StringType, + Required: true, + Description: "Trigger on this list of repository keys", + }, }, - 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 + } + + 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 + } + + if response.IsError() { + utilfw.UnableToCreateResourceError(resp, artifactoryError.String()) + return + } + + plan.ID = plan.Key + + // 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 + var artifactoryError artifactory.ArtifactoryErrorsResponse + + response, err := r.ProviderData.Client.R(). + SetPathParam("webhookKey", state.Key.ValueString()). + SetResult(&webhook). + SetError(&artifactoryError). + Get(WebhookURL) + if err != nil { + utilfw.UnableToRefreshResourceError(resp, err.Error()) + return + } + + if response.StatusCode() == http.StatusNotFound { + resp.State.RemoveResource(ctx) + return + } + + if response.IsError() { + utilfw.UnableToRefreshResourceError(resp, artifactoryError.String()) + 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 } - if v, ok := artifactoryCriteria["anyFederated"]; ok { - criteria["any_federated"] = v.(bool) + var artifactoryError artifactory.ArtifactoryErrorsResponse + response, err := r.ProviderData.Client.R(). + SetPathParam("webhookKey", plan.Key.ValueString()). + SetBody(webhook). + AddRetryCondition(retryOnProxyError). + SetError(&artifactoryError). + Put(WebhookURL) + if err != nil { + utilfw.UnableToUpdateResourceError(resp, err.Error()) + return } + if response.IsError() { + utilfw.UnableToUpdateResourceError(resp, artifactoryError.String()) + return + } + + plan.ID = plan.Key - return criteria + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) } -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, +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)...) + + var artifactoryError artifactory.ArtifactoryErrorsResponse + response, err := r.ProviderData.Client.R(). + SetPathParam("webhookKey", state.Key.ValueString()). + SetError(&artifactoryError). + Delete(WebhookURL) + + if err != nil { + utilfw.UnableToDeleteResourceError(resp, err.Error()) + return + } + + if response.StatusCode() == http.StatusNotFound { + resp.State.RemoveResource(ctx) + return } + + if response.IsError() { + utilfw.UnableToDeleteResourceError(resp, artifactoryError.String()) + 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 *RepoWebhookResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + r.WebhookResource.ImportState(ctx, req, resp) } -var repoCriteriaValidation = func(ctx context.Context, criteria map[string]interface{}) error { - tflog.Debug(ctx, "repoCriteriaValidation") +func (m RepoWebhookResourceModel) toAPIModel(ctx context.Context, domain string, apiModel *WebhookAPIModel) (diags diag.Diagnostics) { + critieriaObj := m.Criteria.Elements()[0].(types.Object) + critieriaAttrs := critieriaObj.Attributes() + + var includePatterns []string + d := critieriaAttrs["include_patterns"].(types.Set).ElementsAs(ctx, &includePatterns, false) + if d.HasError() { + diags.Append(d...) + } - anyLocal := criteria["any_local"].(bool) - anyRemote := criteria["any_remote"].(bool) - anyFederated := criteria["any_federated"].(bool) - repoKeys := criteria["repo_keys"].(*schema.Set).List() + var excludePatterns []string + d = critieriaAttrs["exclude_patterns"].(types.Set).ElementsAs(ctx, &excludePatterns, false) + 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") + var repoKeys []string + d = critieriaAttrs["repo_keys"].(types.Set).ElementsAs(ctx, &repoKeys, false) + if d.HasError() { + diags.Append(d...) } - return nil + criteriaAPIModel := RepoCriteriaAPIModel{ + BaseCriteriaAPIModel: BaseCriteriaAPIModel{ + IncludePatterns: includePatterns, + ExcludePatterns: excludePatterns, + }, + 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 criteriaSetResourceModelAttributeTypes = map[string]attr.Type{ + "include_patterns": types.SetType{ElemType: types.StringType}, + "exclude_patterns": types.SetType{ElemType: types.StringType}, + "any_local": types.BoolType, + "any_remote": types.BoolType, + "any_federated": types.BoolType, + "repo_keys": types.SetType{ElemType: types.StringType}, } + +var criteriaSetResourceModelElementTypes = types.ObjectType{ + AttrTypes: criteriaSetResourceModelAttributeTypes, +} + +func (m *RepoWebhookResourceModel) fromAPIModel(ctx context.Context, apiModel WebhookAPIModel, stateHandlers basetypes.SetValue) diag.Diagnostics { + diags := diag.Diagnostics{} + + criteriaAPIModel := apiModel.EventFilter.Criteria.(map[string]interface{}) + + includePatterns := types.SetNull(types.StringType) + if v, ok := criteriaAPIModel["includePatterns"]; ok && v != nil { + ps, d := types.SetValueFrom(ctx, types.StringType, v) + if d.HasError() { + diags.Append(d...) + } + + includePatterns = ps + } + + excludePatterns := types.SetNull(types.StringType) + if v, ok := criteriaAPIModel["excludePatterns"]; ok && v != nil { + ps, d := types.SetValueFrom(ctx, types.StringType, v) + if d.HasError() { + diags.Append(d...) + } + + excludePatterns = ps + } + + repoKeys := types.SetNull(types.StringType) + if v, ok := criteriaAPIModel["repoKeys"]; ok && v != nil { + ks, d := types.SetValueFrom(ctx, types.StringType, v) + if d.HasError() { + diags.Append(d...) + } + + repoKeys = ks + } + + criteria, d := types.ObjectValue( + criteriaSetResourceModelAttributeTypes, + map[string]attr.Value{ + "include_patterns": includePatterns, + "exclude_patterns": excludePatterns, + "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( + criteriaSetResourceModelElementTypes, + []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"` +} + +// +// var repoWebhookSchema = func(webhookType string, version int, isCustom bool) map[string]*sdkv2_schema.Schema { +// return utilsdk.MergeMaps(getBaseSchemaByVersion(webhookType, version, isCustom), map[string]*sdkv2_schema.Schema{ +// "criteria": { +// Type: sdkv2_schema.TypeSet, +// Required: true, +// MaxItems: 1, +// Elem: &sdkv2_schema.Resource{ +// Schema: utilsdk.MergeMaps(baseCriteriaSchema, map[string]*sdkv2_schema.Schema{ +// "any_local": { +// Type: sdkv2_schema.TypeBool, +// Required: true, +// Description: "Trigger on any local repositories", +// }, +// "any_remote": { +// Type: sdkv2_schema.TypeBool, +// Required: true, +// Description: "Trigger on any remote repositories", +// }, +// "any_federated": { +// Type: sdkv2_schema.TypeBool, +// Required: true, +// Description: "Trigger on any federated repositories", +// }, +// "repo_keys": { +// Type: sdkv2_schema.TypeSet, +// Required: true, +// Elem: &sdkv2_schema.Schema{Type: sdkv2_schema.TypeString}, +// Description: "Trigger on this list of repository keys", +// }, +// }), +// }, +// Description: "Specifies where the webhook will be applied on which repositories.", +// }, +// }) +// } +// +// var packRepoCriteria = func(artifactoryCriteria map[string]interface{}) map[string]interface{} { +// criteria := map[string]interface{}{ +// "any_local": artifactoryCriteria["anyLocal"].(bool), +// "any_remote": artifactoryCriteria["anyRemote"].(bool), +// "any_federated": false, +// "repo_keys": sdkv2_schema.NewSet(sdkv2_schema.HashString, artifactoryCriteria["repoKeys"].([]interface{})), +// } +// +// if v, ok := artifactoryCriteria["anyFederated"]; ok { +// criteria["any_federated"] = v.(bool) +// } +// +// return criteria +// } +// +// var unpackRepoCriteria = func(terraformCriteria map[string]interface{}, baseCriteria BaseCriteriaAPIModel) interface{} { +// return RepoCriteriaAPIModel{ +// AnyLocal: terraformCriteria["any_local"].(bool), +// AnyRemote: terraformCriteria["any_remote"].(bool), +// AnyFederated: terraformCriteria["any_federated"].(bool), +// RepoKeys: utilsdk.CastToStringArr(terraformCriteria["repo_keys"].(*sdkv2_schema.Set).List()), +// BaseCriteriaAPIModel: baseCriteria, +// } +// } +// +// var repoCriteriaValidation = func(ctx context.Context, criteria map[string]interface{}) error { +// tflog.Debug(ctx, "repoCriteriaValidation") +// +// anyLocal := criteria["any_local"].(bool) +// anyRemote := criteria["any_remote"].(bool) +// anyFederated := criteria["any_federated"].(bool) +// repoKeys := criteria["repo_keys"].(*sdkv2_schema.Set).List() +// +// if (!anyLocal && !anyRemote && !anyFederated) && len(repoKeys) == 0 { +// return fmt.Errorf("repo_keys cannot be empty when any_local, any_remote, and any_federated are false") +// } +// +// return nil +// } diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_webhook_test.go b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_test.go index 0a7005b4f..cadac0709 100644 --- a/pkg/artifactory/resource/webhook/resource_artifactory_webhook_test.go +++ b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_test.go @@ -97,7 +97,7 @@ var releaseBundleV2Template = ` ` func TestAccWebhook_CriteriaValidation(t *testing.T) { - for _, webhookType := range webhook.TypesSupported { + for _, webhookType := range webhook.DomainSupported { if !slices.Contains([]string{"user", "release_bundle_v2_promotion", "artifact_lifecycle"}, webhookType) { t.Run(webhookType, func(t *testing.T) { resource.Test(webhookCriteriaValidationTestCase(webhookType, t)) From 94b68c41db0d85ec422969dfc4703671e38ba705 Mon Sep 17 00:00:00 2001 From: Alex Hung Date: Thu, 26 Sep 2024 15:47:13 -0700 Subject: [PATCH 04/17] Migrate build webhook to Plugin Framework Refactor out common code --- pkg/artifactory/provider/framework.go | 1 + .../webhook/resource_artifactory_webhook.go | 193 ++++++++-- .../resource_artifactory_webhook_build.go | 341 ++++++++++++++++-- .../resource_artifactory_webhook_repo.go | 292 +++------------ 4 files changed, 524 insertions(+), 303 deletions(-) diff --git a/pkg/artifactory/provider/framework.go b/pkg/artifactory/provider/framework.go index bf6db17f3..3f9609b80 100644 --- a/pkg/artifactory/provider/framework.go +++ b/pkg/artifactory/provider/framework.go @@ -224,6 +224,7 @@ func (p *ArtifactoryProvider) Resources(ctx context.Context) []func() resource.R replication.NewRemoteRepositoryReplicationResource, webhook.NewArtifactWebhookResource, webhook.NewArtifactPropertyWebhookResource, + webhook.NewBuildWebhookResource, webhook.NewDockerWebhookResource, } } diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_webhook.go b/pkg/artifactory/resource/webhook/resource_artifactory_webhook.go index fa2bfc01e..b2769a377 100644 --- a/pkg/artifactory/resource/webhook/resource_artifactory_webhook.go +++ b/pkg/artifactory/resource/webhook/resource_artifactory_webhook.go @@ -28,6 +28,7 @@ import ( sdkv2_schema "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/jfrog/terraform-provider-artifactory/v12/pkg/artifactory" "github.com/jfrog/terraform-provider-shared/util" + utilfw "github.com/jfrog/terraform-provider-shared/util/fw" utilsdk "github.com/jfrog/terraform-provider-shared/util/sdk" validatorfw_string "github.com/jfrog/terraform-provider-shared/validator/fw/string" "github.com/samber/lo" @@ -58,7 +59,6 @@ const currentSchemaVersion = 2 var DomainSupported = []string{ ArtifactLifecycleDomain, ArtifactoryReleaseBundleDomain, - BuildDomain, DestinationDomain, DistributionDomain, ReleaseBundleDomain, @@ -89,6 +89,21 @@ type WebhookResource struct { Description string } +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, + }, + } +} + func (r *WebhookResource) schema(domain string, criteriaBlock schema.SetNestedBlock) schema.Schema { return schema.Schema{ Version: 2, @@ -184,6 +199,10 @@ func (r *WebhookResource) schema(domain string, criteriaBlock schema.SetNestedBl } } +func (r *WebhookResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = r.TypeName +} + 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 { @@ -192,6 +211,91 @@ func (r *WebhookResource) Configure(ctx context.Context, req resource.ConfigureR r.ProviderData = req.ProviderData.(util.ProviderMetadata) } +func (r *WebhookResource) Create(ctx context.Context, webhook WebhookAPIModel, req resource.CreateRequest, resp *resource.CreateResponse) { + 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 + } + + if response.IsError() { + utilfw.UnableToCreateResourceError(resp, artifactoryError.String()) + return + } +} + +func (r *WebhookResource) Read(ctx context.Context, key string, webhook *WebhookAPIModel, req resource.ReadRequest, resp *resource.ReadResponse) { + 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 + } + + if response.StatusCode() == http.StatusNotFound { + resp.State.RemoveResource(ctx) + return + } + + if response.IsError() { + utilfw.UnableToRefreshResourceError(resp, artifactoryError.String()) + return + } +} + +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 + } + + if response.IsError() { + utilfw.UnableToUpdateResourceError(resp, artifactoryError.String()) + return + } +} + +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) + + if err != nil { + utilfw.UnableToDeleteResourceError(resp, err.Error()) + return + } + + if response.StatusCode() == http.StatusNotFound { + resp.State.RemoveResource(ctx) + return + } + + if response.IsError() { + utilfw.UnableToDeleteResourceError(resp, artifactoryError.String()) + return + } +} + // 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) @@ -207,30 +311,9 @@ type WebhookResourceModel struct { Handlers types.Set `tfsdk:"handler"` } -func (m WebhookResourceModel) toAPIModel(ctx context.Context, domain string, toCriteriaAPIModel interface{}, apiModel *WebhookAPIModel) (diags diag.Diagnostics) { - critieriaObj := m.Criteria.Elements()[0].(types.Object) - critieriaAttrs := critieriaObj.Attributes() - - var includePatterns []string - d := critieriaAttrs["include_patterns"].(types.Set).ElementsAs(ctx, &includePatterns, false) - if d.HasError() { - diags.Append(d...) - } - - var excludePatterns []string - d = critieriaAttrs["exclude_patterns"].(types.Set).ElementsAs(ctx, &excludePatterns, false) - 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...) - } - +func (m WebhookResourceModel) toAPIModel(ctx context.Context, domain string, criteriaAPIModel interface{}, apiModel *WebhookAPIModel) (diags diag.Diagnostics) { var eventTypes []string - d = m.EventTypes.ElementsAs(ctx, &eventTypes, false) + d := m.EventTypes.ElementsAs(ctx, &eventTypes, false) if d.HasError() { diags.Append(d...) } @@ -268,7 +351,7 @@ func (m WebhookResourceModel) toAPIModel(ctx context.Context, domain string, toC EventFilter: EventFilterAPIModel{ Domain: domain, EventTypes: eventTypes, - Criteria: toCriteriaAPIModel, + Criteria: criteriaAPIModel, }, Handlers: handlers, } @@ -276,6 +359,32 @@ func (m WebhookResourceModel) toAPIModel(ctx context.Context, domain string, toC return } +func (m *WebhookResourceModel) toBaseCriteriaAPIModel(ctx context.Context, criteriaAttrs map[string]attr.Value) (BaseCriteriaAPIModel, diag.Diagnostics) { + diags := diag.Diagnostics{} + + var includePatterns []string + d := criteriaAttrs["include_patterns"].(types.Set).ElementsAs(ctx, &includePatterns, false) + if d.HasError() { + diags.Append(d...) + } + + var excludePatterns []string + d = criteriaAttrs["exclude_patterns"].(types.Set).ElementsAs(ctx, &excludePatterns, false) + if d.HasError() { + diags.Append(d...) + } + + return BaseCriteriaAPIModel{ + IncludePatterns: includePatterns, + ExcludePatterns: excludePatterns, + }, diags +} + +var patternsCriteriaSetResourceModelAttributeTypes = map[string]attr.Type{ + "include_patterns": types.SetType{ElemType: types.StringType}, + "exclude_patterns": types.SetType{ElemType: types.StringType}, +} + var handlerSetResourceModelAttributeTypes = map[string]attr.Type{ "url": types.StringType, "secret": types.StringType, @@ -288,6 +397,35 @@ var handlerSetResourceModelElementTypes = types.ObjectType{ AttrTypes: handlerSetResourceModelAttributeTypes, } +func (m *WebhookResourceModel) fromBaseCriteriaAPIModel(ctx context.Context, criteriaAPIModel map[string]interface{}) (map[string]attr.Value, diag.Diagnostics) { + diags := diag.Diagnostics{} + + includePatterns := types.SetNull(types.StringType) + if v, ok := criteriaAPIModel["includePatterns"]; ok && v != nil { + ps, d := types.SetValueFrom(ctx, types.StringType, v) + if d.HasError() { + diags.Append(d...) + } + + includePatterns = ps + } + + excludePatterns := types.SetNull(types.StringType) + if v, ok := criteriaAPIModel["excludePatterns"]; ok && v != nil { + ps, d := types.SetValueFrom(ctx, types.StringType, v) + if d.HasError() { + diags.Append(d...) + } + + excludePatterns = ps + } + + return map[string]attr.Value{ + "include_patterns": includePatterns, + "exclude_patterns": excludePatterns, + }, diags +} + func (m *WebhookResourceModel) fromAPIModel(ctx context.Context, apiModel WebhookAPIModel, stateHandlers basetypes.SetValue, criteriaSet basetypes.SetValue) diag.Diagnostics { diags := diag.Diagnostics{} @@ -441,7 +579,6 @@ var packKeyValuePair = func(keyValuePairs []KeyValuePairAPIModel) map[string]int } var domainCriteriaLookup = map[string]interface{}{ - BuildDomain: BuildWebhookCriteria{}, ReleaseBundleDomain: ReleaseBundleWebhookCriteria{}, DistributionDomain: ReleaseBundleWebhookCriteria{}, ArtifactoryReleaseBundleDomain: ReleaseBundleWebhookCriteria{}, @@ -453,7 +590,6 @@ var domainCriteriaLookup = map[string]interface{}{ } var domainPackLookup = map[string]func(map[string]interface{}) map[string]interface{}{ - BuildDomain: packBuildCriteria, ReleaseBundleDomain: packReleaseBundleCriteria, DistributionDomain: packReleaseBundleCriteria, ArtifactoryReleaseBundleDomain: packReleaseBundleCriteria, @@ -465,7 +601,6 @@ var domainPackLookup = map[string]func(map[string]interface{}) map[string]interf } var domainUnpackLookup = map[string]func(map[string]interface{}, BaseCriteriaAPIModel) interface{}{ - BuildDomain: unpackBuildCriteria, ReleaseBundleDomain: unpackReleaseBundleCriteria, DistributionDomain: unpackReleaseBundleCriteria, ArtifactoryReleaseBundleDomain: unpackReleaseBundleCriteria, @@ -478,7 +613,6 @@ var domainUnpackLookup = map[string]func(map[string]interface{}, BaseCriteriaAPI var domainSchemaLookup = func(version int, isCustom bool, webhookType string) map[string]map[string]*sdkv2_schema.Schema { return map[string]map[string]*sdkv2_schema.Schema{ - BuildDomain: buildWebhookSchema(webhookType, version, isCustom), ReleaseBundleDomain: releaseBundleWebhookSchema(webhookType, version, isCustom), DistributionDomain: releaseBundleWebhookSchema(webhookType, version, isCustom), ArtifactoryReleaseBundleDomain: releaseBundleWebhookSchema(webhookType, version, isCustom), @@ -532,7 +666,6 @@ var packCriteria = func(d *sdkv2_schema.ResourceData, webhookType string, criter } var domainCriteriaValidationLookup = map[string]func(context.Context, map[string]interface{}) error{ - BuildDomain: buildCriteriaValidation, ReleaseBundleDomain: releaseBundleCriteriaValidation, DistributionDomain: releaseBundleCriteriaValidation, ArtifactoryReleaseBundleDomain: releaseBundleCriteriaValidation, diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_webhook_build.go b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_build.go index e55cc5f04..f37ab9ff7 100644 --- a/pkg/artifactory/resource/webhook/resource_artifactory_webhook_build.go +++ b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_build.go @@ -2,35 +2,302 @@ package webhook import ( "context" - "fmt" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "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" + sdkv2_schema "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/jfrog/terraform-provider-shared/util" utilsdk "github.com/jfrog/terraform-provider-shared/util/sdk" + "github.com/samber/lo" ) -type BuildWebhookCriteria struct { +var _ resource.Resource = &BuildWebhookResource{} + +func NewBuildWebhookResource() resource.Resource { + return &BuildWebhookResource{ + WebhookResource: WebhookResource{ + TypeName: "artifactory_build_webhook", + 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) +} + +func (r *BuildWebhookResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + criteriaBlock := schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: lo.Assign( + patternsSchemaAttributes("Use Ant-style wildcard patterns to specify build names (i.e. artifact paths) in the build info repository (without a leading slash) that will be excluded from this permission target.\nAnt-style path expressions are supported (*, **, ?).\nFor example, an `apache/**` pattern will exclude the `apache` build info from the permission."), + map[string]schema.Attribute{ + "any_build": schema.BoolAttribute{ + Required: true, + Description: "Trigger on any builds", + }, + "selected_builds": schema.SetAttribute{ + ElementType: types.StringType, + Required: true, + Description: "Trigger on this list of build IDs", + }, + }, + ), + }, + 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) + + plan.ID = plan.Key + + // 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 + r.WebhookResource.Read(ctx, state.Key.ValueString(), &webhook, req, resp) + + 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) + + plan.ID = plan.Key + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +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 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 *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 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) + + 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...) + } + + 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"` } -var buildWebhookSchema = func(webhookType string, version int, isCustom bool) map[string]*schema.Schema { - return utilsdk.MergeMaps(getBaseSchemaByVersion(webhookType, version, isCustom), map[string]*schema.Schema{ +var buildWebhookSchema = func(webhookType string, version int, isCustom bool) map[string]*sdkv2_schema.Schema { + return utilsdk.MergeMaps(getBaseSchemaByVersion(webhookType, version, isCustom), map[string]*sdkv2_schema.Schema{ "criteria": { - Type: schema.TypeSet, + Type: sdkv2_schema.TypeSet, Required: true, MaxItems: 1, - Elem: &schema.Resource{ - Schema: utilsdk.MergeMaps(baseCriteriaSchema, map[string]*schema.Schema{ + Elem: &sdkv2_schema.Resource{ + Schema: utilsdk.MergeMaps(baseCriteriaSchema, map[string]*sdkv2_schema.Schema{ "any_build": { - Type: schema.TypeBool, + Type: sdkv2_schema.TypeBool, Required: true, Description: "Trigger on any builds", }, "selected_builds": { - Type: schema.TypeSet, + Type: sdkv2_schema.TypeSet, Required: true, - Elem: &schema.Schema{Type: schema.TypeString}, + Elem: &sdkv2_schema.Schema{Type: sdkv2_schema.TypeString}, Description: "Trigger on this list of build IDs", }, }), @@ -40,29 +307,29 @@ var buildWebhookSchema = func(webhookType string, version int, isCustom bool) ma }) } -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 BuildWebhookCriteria{ - AnyBuild: terraformCriteria["any_build"].(bool), - SelectedBuilds: utilsdk.CastToStringArr(terraformCriteria["selected_builds"].(*schema.Set).List()), - BaseCriteriaAPIModel: baseCriteria, - } -} - -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 packBuildCriteria = func(artifactoryCriteria map[string]interface{}) map[string]interface{} { +// return map[string]interface{}{ +// "any_build": artifactoryCriteria["anyBuild"].(bool), +// "selected_builds": sdkv2_schema.NewSet(sdkv2_schema.HashString, artifactoryCriteria["selectedBuilds"].([]interface{})), +// } +// } +// +// var unpackBuildCriteria = func(terraformCriteria map[string]interface{}, baseCriteria BaseCriteriaAPIModel) interface{} { +// return BuildCriteriaAPIModel{ +// AnyBuild: terraformCriteria["any_build"].(bool), +// SelectedBuilds: utilsdk.CastToStringArr(terraformCriteria["selected_builds"].(*sdkv2_schema.Set).List()), +// BaseCriteriaAPIModel: baseCriteria, +// } +// } +// +// var buildCriteriaValidation = func(ctx context.Context, criteria map[string]interface{}) error { +// anyBuild := criteria["any_build"].(bool) +// selectedBuilds := criteria["selected_builds"].(*sdkv2_schema.Set).List() +// includePatterns := criteria["include_patterns"].(*sdkv2_schema.Set).List() +// +// if !anyBuild && (len(selectedBuilds) == 0 && len(includePatterns) == 0) { +// return fmt.Errorf("selected_builds or include_patterns cannot be empty when any_build is false") +// } +// +// return nil +// } diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_webhook_repo.go b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_repo.go index 3f36e0a03..f3f16ff64 100644 --- a/pkg/artifactory/resource/webhook/resource_artifactory_webhook_repo.go +++ b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_repo.go @@ -2,7 +2,6 @@ package webhook import ( "context" - "net/http" "github.com/hashicorp/terraform-plugin-framework-validators/setvalidator" "github.com/hashicorp/terraform-plugin-framework/attr" @@ -13,9 +12,8 @@ import ( "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/jfrog/terraform-provider-artifactory/v12/pkg/artifactory" "github.com/jfrog/terraform-provider-shared/util" - utilfw "github.com/jfrog/terraform-provider-shared/util/fw" + "github.com/samber/lo" ) var _ resource.Resource = &RepoWebhookResource{} @@ -59,41 +57,34 @@ type RepoWebhookResource struct { } func (r *RepoWebhookResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = r.TypeName + r.WebhookResource.Metadata(ctx, req, resp) } func (r *RepoWebhookResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { criteriaBlock := schema.SetNestedBlock{ NestedObject: schema.NestedBlockObject{ - Attributes: map[string]schema.Attribute{ - "include_patterns": schema.SetAttribute{ - ElementType: types.StringType, - Optional: true, - MarkdownDescription: "Simple comma separated wildcard patterns for repository artifact paths (with no leading slash).\nAnt-style path expressions are supported (*, **, ?).\nFor example: `org/apache/**`", + Attributes: lo.Assign( + patternsSchemaAttributes("Simple comma separated wildcard patterns for repository artifact paths (with no leading slash).\nAnt-style path expressions are supported (*, **, ?).\nFor example: `org/apache/**`"), + map[string]schema.Attribute{ + "any_local": schema.BoolAttribute{ + Required: true, + Description: "Trigger on any local repositories", + }, + "any_remote": schema.BoolAttribute{ + Required: true, + Description: "Trigger on any remote repositories", + }, + "any_federated": schema.BoolAttribute{ + Required: true, + Description: "Trigger on any federated repositories", + }, + "repo_keys": schema.SetAttribute{ + ElementType: types.StringType, + Required: true, + Description: "Trigger on this list of repository keys", + }, }, - "exclude_patterns": schema.SetAttribute{ - ElementType: types.StringType, - Optional: true, - MarkdownDescription: "Simple comma separated wildcard patterns for repository artifact paths (with no leading slash).\nAnt-style path expressions are supported (*, **, ?).\nFor example: `org/apache/**`", - }, - "any_local": schema.BoolAttribute{ - Required: true, - Description: "Trigger on any local repositories", - }, - "any_remote": schema.BoolAttribute{ - Required: true, - Description: "Trigger on any remote repositories", - }, - "any_federated": schema.BoolAttribute{ - Required: true, - Description: "Trigger on any federated repositories", - }, - "repo_keys": schema.SetAttribute{ - ElementType: types.StringType, - Required: true, - Description: "Trigger on this list of repository keys", - }, - }, + ), }, Validators: []validator.Set{ setvalidator.SizeBetween(1, 1), @@ -150,22 +141,7 @@ func (r *RepoWebhookResource) Create(ctx context.Context, req resource.CreateReq return } - 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 - } - - if response.IsError() { - utilfw.UnableToCreateResourceError(resp, artifactoryError.String()) - return - } + r.WebhookResource.Create(ctx, webhook, req, resp) plan.ID = plan.Key @@ -185,27 +161,7 @@ func (r *RepoWebhookResource) Read(ctx context.Context, req resource.ReadRequest } var webhook WebhookAPIModel - var artifactoryError artifactory.ArtifactoryErrorsResponse - - response, err := r.ProviderData.Client.R(). - SetPathParam("webhookKey", state.Key.ValueString()). - SetResult(&webhook). - SetError(&artifactoryError). - Get(WebhookURL) - if err != nil { - utilfw.UnableToRefreshResourceError(resp, err.Error()) - return - } - - if response.StatusCode() == http.StatusNotFound { - resp.State.RemoveResource(ctx) - return - } - - if response.IsError() { - utilfw.UnableToRefreshResourceError(resp, artifactoryError.String()) - return - } + r.WebhookResource.Read(ctx, state.Key.ValueString(), &webhook, req, resp) resp.Diagnostics.Append(state.fromAPIModel(ctx, webhook, state.Handlers)...) if resp.Diagnostics.HasError() { @@ -233,21 +189,7 @@ func (r *RepoWebhookResource) Update(ctx context.Context, req resource.UpdateReq return } - var artifactoryError artifactory.ArtifactoryErrorsResponse - response, err := r.ProviderData.Client.R(). - SetPathParam("webhookKey", plan.Key.ValueString()). - SetBody(webhook). - AddRetryCondition(retryOnProxyError). - SetError(&artifactoryError). - Put(WebhookURL) - if err != nil { - utilfw.UnableToUpdateResourceError(resp, err.Error()) - return - } - if response.IsError() { - utilfw.UnableToUpdateResourceError(resp, artifactoryError.String()) - return - } + r.WebhookResource.Update(ctx, plan.Key.ValueString(), webhook, req, resp) plan.ID = plan.Key @@ -263,26 +205,7 @@ func (r *RepoWebhookResource) Delete(ctx context.Context, req resource.DeleteReq // Read Terraform prior state data into the model resp.Diagnostics.Append(req.State.Get(ctx, &state)...) - var artifactoryError artifactory.ArtifactoryErrorsResponse - response, err := r.ProviderData.Client.R(). - SetPathParam("webhookKey", state.Key.ValueString()). - SetError(&artifactoryError). - Delete(WebhookURL) - - if err != nil { - utilfw.UnableToDeleteResourceError(resp, err.Error()) - return - } - - if response.StatusCode() == http.StatusNotFound { - resp.State.RemoveResource(ctx) - return - } - - if response.IsError() { - utilfw.UnableToDeleteResourceError(resp, artifactoryError.String()) - return - } + r.WebhookResource.Delete(ctx, state.Key.ValueString(), req, resp) // If the logic reaches here, it implicitly succeeded and will remove // the resource from state if there are no other errors. @@ -297,14 +220,7 @@ func (m RepoWebhookResourceModel) toAPIModel(ctx context.Context, domain string, critieriaObj := m.Criteria.Elements()[0].(types.Object) critieriaAttrs := critieriaObj.Attributes() - var includePatterns []string - d := critieriaAttrs["include_patterns"].(types.Set).ElementsAs(ctx, &includePatterns, false) - if d.HasError() { - diags.Append(d...) - } - - var excludePatterns []string - d = critieriaAttrs["exclude_patterns"].(types.Set).ElementsAs(ctx, &excludePatterns, false) + baseCriteria, d := m.WebhookResourceModel.toBaseCriteriaAPIModel(ctx, critieriaAttrs) if d.HasError() { diags.Append(d...) } @@ -316,14 +232,11 @@ func (m RepoWebhookResourceModel) toAPIModel(ctx context.Context, domain string, } criteriaAPIModel := RepoCriteriaAPIModel{ - BaseCriteriaAPIModel: BaseCriteriaAPIModel{ - IncludePatterns: includePatterns, - ExcludePatterns: excludePatterns, - }, - AnyLocal: critieriaAttrs["any_local"].(types.Bool).ValueBool(), - AnyRemote: critieriaAttrs["any_remote"].(types.Bool).ValueBool(), - AnyFederated: critieriaAttrs["any_federated"].(types.Bool).ValueBool(), - RepoKeys: repoKeys, + 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) @@ -334,17 +247,18 @@ func (m RepoWebhookResourceModel) toAPIModel(ctx context.Context, domain string, return } -var criteriaSetResourceModelAttributeTypes = map[string]attr.Type{ - "include_patterns": types.SetType{ElemType: types.StringType}, - "exclude_patterns": types.SetType{ElemType: types.StringType}, - "any_local": types.BoolType, - "any_remote": types.BoolType, - "any_federated": types.BoolType, - "repo_keys": types.SetType{ElemType: types.StringType}, -} +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 criteriaSetResourceModelElementTypes = types.ObjectType{ - AttrTypes: criteriaSetResourceModelAttributeTypes, +var repoCriteriaSetResourceModelElementTypes = types.ObjectType{ + AttrTypes: repoCriteriaSetResourceModelAttributeTypes, } func (m *RepoWebhookResourceModel) fromAPIModel(ctx context.Context, apiModel WebhookAPIModel, stateHandlers basetypes.SetValue) diag.Diagnostics { @@ -352,25 +266,7 @@ func (m *RepoWebhookResourceModel) fromAPIModel(ctx context.Context, apiModel We criteriaAPIModel := apiModel.EventFilter.Criteria.(map[string]interface{}) - includePatterns := types.SetNull(types.StringType) - if v, ok := criteriaAPIModel["includePatterns"]; ok && v != nil { - ps, d := types.SetValueFrom(ctx, types.StringType, v) - if d.HasError() { - diags.Append(d...) - } - - includePatterns = ps - } - - excludePatterns := types.SetNull(types.StringType) - if v, ok := criteriaAPIModel["excludePatterns"]; ok && v != nil { - ps, d := types.SetValueFrom(ctx, types.StringType, v) - if d.HasError() { - diags.Append(d...) - } - - excludePatterns = ps - } + baseCriteriaAttrs, d := m.WebhookResourceModel.fromBaseCriteriaAPIModel(ctx, criteriaAPIModel) repoKeys := types.SetNull(types.StringType) if v, ok := criteriaAPIModel["repoKeys"]; ok && v != nil { @@ -383,21 +279,22 @@ func (m *RepoWebhookResourceModel) fromAPIModel(ctx context.Context, apiModel We } criteria, d := types.ObjectValue( - criteriaSetResourceModelAttributeTypes, - map[string]attr.Value{ - "include_patterns": includePatterns, - "exclude_patterns": excludePatterns, - "any_local": types.BoolValue(criteriaAPIModel["anyLocal"].(bool)), - "any_remote": types.BoolValue(criteriaAPIModel["anyRemote"].(bool)), - "any_federated": types.BoolValue(criteriaAPIModel["anyFederated"].(bool)), - "repo_keys": repoKeys, - }, + 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( - criteriaSetResourceModelElementTypes, + repoCriteriaSetResourceModelElementTypes, []attr.Value{criteria}, ) if d.HasError() { @@ -419,80 +316,3 @@ type RepoCriteriaAPIModel struct { AnyFederated bool `json:"anyFederated"` RepoKeys []string `json:"repoKeys"` } - -// -// var repoWebhookSchema = func(webhookType string, version int, isCustom bool) map[string]*sdkv2_schema.Schema { -// return utilsdk.MergeMaps(getBaseSchemaByVersion(webhookType, version, isCustom), map[string]*sdkv2_schema.Schema{ -// "criteria": { -// Type: sdkv2_schema.TypeSet, -// Required: true, -// MaxItems: 1, -// Elem: &sdkv2_schema.Resource{ -// Schema: utilsdk.MergeMaps(baseCriteriaSchema, map[string]*sdkv2_schema.Schema{ -// "any_local": { -// Type: sdkv2_schema.TypeBool, -// Required: true, -// Description: "Trigger on any local repositories", -// }, -// "any_remote": { -// Type: sdkv2_schema.TypeBool, -// Required: true, -// Description: "Trigger on any remote repositories", -// }, -// "any_federated": { -// Type: sdkv2_schema.TypeBool, -// Required: true, -// Description: "Trigger on any federated repositories", -// }, -// "repo_keys": { -// Type: sdkv2_schema.TypeSet, -// Required: true, -// Elem: &sdkv2_schema.Schema{Type: sdkv2_schema.TypeString}, -// Description: "Trigger on this list of repository keys", -// }, -// }), -// }, -// Description: "Specifies where the webhook will be applied on which repositories.", -// }, -// }) -// } -// -// var packRepoCriteria = func(artifactoryCriteria map[string]interface{}) map[string]interface{} { -// criteria := map[string]interface{}{ -// "any_local": artifactoryCriteria["anyLocal"].(bool), -// "any_remote": artifactoryCriteria["anyRemote"].(bool), -// "any_federated": false, -// "repo_keys": sdkv2_schema.NewSet(sdkv2_schema.HashString, artifactoryCriteria["repoKeys"].([]interface{})), -// } -// -// if v, ok := artifactoryCriteria["anyFederated"]; ok { -// criteria["any_federated"] = v.(bool) -// } -// -// return criteria -// } -// -// var unpackRepoCriteria = func(terraformCriteria map[string]interface{}, baseCriteria BaseCriteriaAPIModel) interface{} { -// return RepoCriteriaAPIModel{ -// AnyLocal: terraformCriteria["any_local"].(bool), -// AnyRemote: terraformCriteria["any_remote"].(bool), -// AnyFederated: terraformCriteria["any_federated"].(bool), -// RepoKeys: utilsdk.CastToStringArr(terraformCriteria["repo_keys"].(*sdkv2_schema.Set).List()), -// BaseCriteriaAPIModel: baseCriteria, -// } -// } -// -// var repoCriteriaValidation = func(ctx context.Context, criteria map[string]interface{}) error { -// tflog.Debug(ctx, "repoCriteriaValidation") -// -// anyLocal := criteria["any_local"].(bool) -// anyRemote := criteria["any_remote"].(bool) -// anyFederated := criteria["any_federated"].(bool) -// repoKeys := criteria["repo_keys"].(*sdkv2_schema.Set).List() -// -// if (!anyLocal && !anyRemote && !anyFederated) && len(repoKeys) == 0 { -// return fmt.Errorf("repo_keys cannot be empty when any_local, any_remote, and any_federated are false") -// } -// -// return nil -// } From b284326a75d0dd6cf466db36873908075d323156 Mon Sep 17 00:00:00 2001 From: Alex Hung Date: Fri, 27 Sep 2024 13:33:32 -0700 Subject: [PATCH 05/17] Migrate release bundle webhook resource Fix migration issue from SDKv2 Remove unused, old debug loggings --- pkg/artifactory/provider/framework.go | 1 + .../resource_artifactory_item_properties.go | 6 - ...rce_artifactory_distribution_public_key.go | 2 - .../resource_artifactory_scoped_token.go | 7 +- ...esource_artifactory_vault_configuration.go | 5 - .../resource_artifactory_custom_webhook.go | 13 - .../webhook/resource_artifactory_webhook.go | 118 +++---- .../resource_artifactory_webhook_build.go | 22 +- ...urce_artifactory_webhook_release_bundle.go | 312 +++++++++++++++++- ...e_artifactory_webhook_release_bundle_v2.go | 3 - .../resource_artifactory_webhook_repo.go | 22 +- .../resource_artifactory_webhook_test.go | 179 +++++++--- 12 files changed, 533 insertions(+), 157 deletions(-) diff --git a/pkg/artifactory/provider/framework.go b/pkg/artifactory/provider/framework.go index 3f9609b80..4c820b9c5 100644 --- a/pkg/artifactory/provider/framework.go +++ b/pkg/artifactory/provider/framework.go @@ -226,6 +226,7 @@ func (p *ArtifactoryProvider) Resources(ctx context.Context) []func() resource.R webhook.NewArtifactPropertyWebhookResource, webhook.NewBuildWebhookResource, webhook.NewDockerWebhookResource, + webhook.NewReleaseBundleWebhookResource, } } 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/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_custom_webhook.go b/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook.go index ef9b89384..aac638c54 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" @@ -260,8 +259,6 @@ 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.EventFilterAPIModel.Criteria = domainCriteriaLookup[webhookType] @@ -296,8 +293,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) @@ -323,8 +318,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) @@ -351,8 +344,6 @@ 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()). @@ -376,8 +367,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 +382,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 { diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_webhook.go b/pkg/artifactory/resource/webhook/resource_artifactory_webhook.go index b2769a377..024ebc540 100644 --- a/pkg/artifactory/resource/webhook/resource_artifactory_webhook.go +++ b/pkg/artifactory/resource/webhook/resource_artifactory_webhook.go @@ -16,13 +16,12 @@ import ( "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/stringdefault" "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/hashicorp/terraform-plugin-log/tflog" sdkv2_diag "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff" sdkv2_schema "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" @@ -61,7 +60,7 @@ var DomainSupported = []string{ ArtifactoryReleaseBundleDomain, DestinationDomain, DistributionDomain, - ReleaseBundleDomain, + // ReleaseBundleDomain, ReleaseBundleV2Domain, ReleaseBundleV2PromotionDomain, UserDomain, @@ -106,11 +105,8 @@ var patternsSchemaAttributes = func(description string) map[string]schema.Attrib func (r *WebhookResource) schema(domain string, criteriaBlock schema.SetNestedBlock) schema.Schema { return schema.Schema{ - Version: 2, + Version: currentSchemaVersion, Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ // for backward compatability - Computed: true, - }, "key": schema.StringAttribute{ Required: true, Validators: []validator.String{ @@ -163,15 +159,17 @@ func (r *WebhookResource) schema(domain string, criteriaBlock schema.SetNestedBl }, "secret": schema.StringAttribute{ Optional: true, - Computed: true, - Default: stringdefault.StaticString(""), // for backward compatability // 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, - Computed: true, - Default: booldefault.StaticBool(false), + 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{ @@ -180,6 +178,9 @@ func (r *WebhookResource) schema(domain string, criteriaBlock schema.SetNestedBl 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{ @@ -230,7 +231,7 @@ func (r *WebhookResource) Create(ctx context.Context, webhook WebhookAPIModel, r } } -func (r *WebhookResource) Read(ctx context.Context, key string, webhook *WebhookAPIModel, req resource.ReadRequest, resp *resource.ReadResponse) { +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). @@ -239,18 +240,20 @@ func (r *WebhookResource) Read(ctx context.Context, key string, webhook *Webhook Get(WebhookURL) if err != nil { utilfw.UnableToRefreshResourceError(resp, err.Error()) - return + return false } if response.StatusCode() == http.StatusNotFound { resp.State.RemoveResource(ctx) - return + return false } if response.IsError() { utilfw.UnableToRefreshResourceError(resp, artifactoryError.String()) - return + return false } + + return true } func (r *WebhookResource) Update(ctx context.Context, key string, webhook WebhookAPIModel, req resource.UpdateRequest, resp *resource.UpdateResponse) { @@ -302,7 +305,6 @@ func (r *WebhookResource) ImportState(ctx context.Context, req resource.ImportSt } type WebhookResourceModel struct { - ID types.String `tfsdk:"id"` Key types.String `tfsdk:"key"` Description types.String `tfsdk:"description"` Enabled types.Bool `tfsdk:"enabled"` @@ -336,9 +338,9 @@ func (m WebhookResourceModel) toAPIModel(ctx context.Context, domain string, cri return HandlerAPIModel{ HandlerType: "webhook", Url: attrs["url"].(types.String).ValueString(), - Secret: attrs["secret"].(types.String).ValueString(), - UseSecretForSigning: attrs["use_secret_for_signing"].(types.Bool).ValueBool(), - Proxy: attrs["proxy"].(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, } }, @@ -429,7 +431,6 @@ func (m *WebhookResourceModel) fromBaseCriteriaAPIModel(ctx context.Context, cri func (m *WebhookResourceModel) fromAPIModel(ctx context.Context, apiModel WebhookAPIModel, stateHandlers basetypes.SetValue, criteriaSet basetypes.SetValue) diag.Diagnostics { diags := diag.Diagnostics{} - m.ID = types.StringValue(apiModel.Key) m.Key = types.StringValue(apiModel.Key) description := types.StringNull() @@ -469,7 +470,9 @@ func (m *WebhookResourceModel) fromAPIModel(ctx context.Context, apiModel Webhoo customHttpHeaders = h } - secret := types.StringValue("") + secret := types.StringNull() + useSecretForSigning := types.BoolPointerValue(handler.UseSecretForSigning) + matchedHandler, found := lo.Find( stateHandlers.Elements(), func(elem attr.Value) bool { @@ -479,14 +482,17 @@ func (m *WebhookResourceModel) fromAPIModel(ctx context.Context, apiModel Webhoo ) if found { attrs := matchedHandler.(types.Object).Attributes() - if !attrs["secret"].(types.String).IsNull() { - secret = attrs["secret"].(types.String) + s := attrs["secret"].(types.String) + if !s.IsNull() && s.ValueString() != "" { + secret = s } - } - proxy := types.StringNull() - if handler.Proxy != "" { - proxy = types.StringValue(handler.Proxy) + // 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() + } } h, d := types.ObjectValue( @@ -494,8 +500,8 @@ func (m *WebhookResourceModel) fromAPIModel(ctx context.Context, apiModel Webhoo map[string]attr.Value{ "url": types.StringValue(handler.Url), "secret": secret, - "use_secret_for_signing": types.BoolValue(handler.UseSecretForSigning), - "proxy": proxy, + "use_secret_for_signing": useSecretForSigning, + "proxy": types.StringPointerValue(handler.Proxy), "custom_http_headers": customHttpHeaders, }, ) @@ -545,9 +551,9 @@ type BaseCriteriaAPIModel struct { type HandlerAPIModel 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"` + Secret *string `json:"secret"` + UseSecretForSigning *bool `json:"use_secret_for_signing,omitempty"` + Proxy *string `json:"proxy"` CustomHttpHeaders []KeyValuePairAPIModel `json:"custom_http_headers"` } @@ -579,10 +585,10 @@ var packKeyValuePair = func(keyValuePairs []KeyValuePairAPIModel) map[string]int } var domainCriteriaLookup = map[string]interface{}{ - ReleaseBundleDomain: ReleaseBundleWebhookCriteria{}, - DistributionDomain: ReleaseBundleWebhookCriteria{}, - ArtifactoryReleaseBundleDomain: ReleaseBundleWebhookCriteria{}, - DestinationDomain: ReleaseBundleWebhookCriteria{}, + ReleaseBundleDomain: ReleaseBundleCriteriaAPIModel{}, + DistributionDomain: ReleaseBundleCriteriaAPIModel{}, + ArtifactoryReleaseBundleDomain: ReleaseBundleCriteriaAPIModel{}, + DestinationDomain: ReleaseBundleCriteriaAPIModel{}, UserDomain: EmptyWebhookCriteria{}, ReleaseBundleV2Domain: ReleaseBundleV2WebhookCriteria{}, ReleaseBundleV2PromotionDomain: ReleaseBundleV2PromotionWebhookCriteria{}, @@ -724,15 +730,21 @@ func ResourceArtifactoryWebhook(webhookType string) *sdkv2_schema.Resource { } if v, ok := h["secret"]; ok { - webhookHandler.Secret = v.(string) + if s, ok := v.(string); ok { + webhookHandler.Secret = &s + } } if v, ok := h["use_secret_for_signing"]; ok { - webhookHandler.UseSecretForSigning = v.(bool) + if b, ok := v.(bool); ok { + webhookHandler.UseSecretForSigning = &b + } } if v, ok := h["proxy"]; ok { - webhookHandler.Proxy = v.(string) + if s, ok := v.(string); ok { + webhookHandler.Proxy = &s + } } if v, ok := h["custom_http_headers"]; ok { @@ -768,10 +780,16 @@ func ResourceArtifactoryWebhook(webhookType string) *sdkv2_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, + "url": handler.Url, + "secret": packSecret(d, handler.Url), + } + + if handler.UseSecretForSigning != nil { + packedHandler["use_secret_for_signing"] = *handler.UseSecretForSigning + } + + if handler.Proxy != nil { + packedHandler["proxy"] = *handler.Proxy } if handler.CustomHttpHeaders != nil { @@ -804,8 +822,6 @@ func ResourceArtifactoryWebhook(webhookType string) *sdkv2_schema.Resource { } var readWebhook = func(ctx context.Context, data *sdkv2_schema.ResourceData, m interface{}) sdkv2_diag.Diagnostics { - tflog.Debug(ctx, "tflog.Debug(ctx, \"readWebhook\")") - webhook := WebhookAPIModel{} webhook.EventFilter.Criteria = domainCriteriaLookup[webhookType] @@ -834,8 +850,6 @@ func ResourceArtifactoryWebhook(webhookType string) *sdkv2_schema.Resource { } var createWebhook = func(ctx context.Context, data *sdkv2_schema.ResourceData, m interface{}) sdkv2_diag.Diagnostics { - tflog.Debug(ctx, "createWebhook") - webhook, err := unpackWebhook(data) if err != nil { return sdkv2_diag.FromErr(err) @@ -861,8 +875,6 @@ func ResourceArtifactoryWebhook(webhookType string) *sdkv2_schema.Resource { } var updateWebhook = func(ctx context.Context, data *sdkv2_schema.ResourceData, m interface{}) sdkv2_diag.Diagnostics { - tflog.Debug(ctx, "updateWebhook") - webhook, err := unpackWebhook(data) if err != nil { return sdkv2_diag.FromErr(err) @@ -889,8 +901,6 @@ func ResourceArtifactoryWebhook(webhookType string) *sdkv2_schema.Resource { } var deleteWebhook = func(ctx context.Context, data *sdkv2_schema.ResourceData, m interface{}) sdkv2_diag.Diagnostics { - tflog.Debug(ctx, "deleteWebhook") - var artifactoryError artifactory.ArtifactoryErrorsResponse resp, err := m.(util.ProviderMetadata).Client.R(). SetPathParam("webhookKey", data.Id()). @@ -914,8 +924,6 @@ func ResourceArtifactoryWebhook(webhookType string) *sdkv2_schema.Resource { } var eventTypesDiff = func(ctx context.Context, diff *sdkv2_schema.ResourceDiff, v interface{}) error { - tflog.Debug(ctx, "eventTypesDiff") - eventTypes := diff.Get("event_types").(*sdkv2_schema.Set).List() if len(eventTypes) == 0 { return nil @@ -931,8 +939,6 @@ func ResourceArtifactoryWebhook(webhookType string) *sdkv2_schema.Resource { } var criteriaDiff = func(ctx context.Context, diff *sdkv2_schema.ResourceDiff, v interface{}) error { - tflog.Debug(ctx, "criteriaDiff") - if resource, ok := diff.GetOk("criteria"); ok { criteria := resource.(*sdkv2_schema.Set).List() if len(criteria) == 0 { diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_webhook_build.go b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_build.go index f37ab9ff7..3e0e93f60 100644 --- a/pkg/artifactory/resource/webhook/resource_artifactory_webhook_build.go +++ b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_build.go @@ -114,8 +114,9 @@ func (r *BuildWebhookResource) Create(ctx context.Context, req resource.CreateRe } r.WebhookResource.Create(ctx, webhook, req, resp) - - plan.ID = plan.Key + if resp.Diagnostics.HasError() { + return + } // Save data into Terraform state resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) @@ -133,7 +134,14 @@ func (r *BuildWebhookResource) Read(ctx context.Context, req resource.ReadReques } var webhook WebhookAPIModel - r.WebhookResource.Read(ctx, state.Key.ValueString(), &webhook, req, resp) + 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() { @@ -162,8 +170,9 @@ func (r *BuildWebhookResource) Update(ctx context.Context, req resource.UpdateRe } r.WebhookResource.Update(ctx, plan.Key.ValueString(), webhook, req, resp) - - plan.ID = plan.Key + if resp.Diagnostics.HasError() { + return + } // Save data into Terraform state resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) @@ -178,6 +187,9 @@ func (r *BuildWebhookResource) Delete(ctx context.Context, req resource.DeleteRe 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. 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 e3d5e31b9..cc66b1ac0 100644 --- a/pkg/artifactory/resource/webhook/resource_artifactory_webhook_release_bundle.go +++ b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_release_bundle.go @@ -4,34 +4,314 @@ import ( "context" "fmt" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "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" + sdkv2_schema "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/jfrog/terraform-provider-shared/util" utilsdk "github.com/jfrog/terraform-provider-shared/util/sdk" + "github.com/samber/lo" ) -type ReleaseBundleWebhookCriteria struct { +var _ resource.Resource = &ReleaseBundleWebhookResource{} + +func NewReleaseBundleWebhookResource() resource.Resource { + return &ReleaseBundleWebhookResource{ + WebhookResource: WebhookResource{ + TypeName: "artifactory_release_bundle_webhook", + Domain: BuildDomain, + 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", + }, + "selected_builds": schema.SetAttribute{ + ElementType: types.StringType, + Required: true, + Description: "Trigger on this list of release bundle names", + }, + }, + ), + }, + Validators: []validator.Set{ + setvalidator.SizeBetween(1, 1), + setvalidator.IsRequired(), + }, + Description: "Specifies where the webhook will be applied, on which release bundles or distributions.", + } + + resp.Schema = r.schema(r.Domain, criteriaBlock) +} + +func (r *ReleaseBundleWebhookResource) 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)...) +} + +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) +} + +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, +} + +func (m *ReleaseBundleWebhookResourceModel) fromAPIModel(ctx context.Context, apiModel WebhookAPIModel, stateHandlers basetypes.SetValue) diag.Diagnostics { + diags := diag.Diagnostics{} + + criteriaAPIModel := apiModel.EventFilter.Criteria.(map[string]interface{}) + + baseCriteriaAttrs, d := m.WebhookResourceModel.fromBaseCriteriaAPIModel(ctx, criteriaAPIModel) + + 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...) + } + + 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"` } -var releaseBundleWebhookSchema = func(webhookType string, version int, isCustom bool) map[string]*schema.Schema { - return utilsdk.MergeMaps(getBaseSchemaByVersion(webhookType, version, isCustom), map[string]*schema.Schema{ +var releaseBundleWebhookSchema = func(webhookType string, version int, isCustom bool) map[string]*sdkv2_schema.Schema { + return utilsdk.MergeMaps(getBaseSchemaByVersion(webhookType, version, isCustom), map[string]*sdkv2_schema.Schema{ "criteria": { - Type: schema.TypeSet, + Type: sdkv2_schema.TypeSet, Required: true, MaxItems: 1, - Elem: &schema.Resource{ - Schema: utilsdk.MergeMaps(baseCriteriaSchema, map[string]*schema.Schema{ + Elem: &sdkv2_schema.Resource{ + Schema: utilsdk.MergeMaps(baseCriteriaSchema, map[string]*sdkv2_schema.Schema{ "any_release_bundle": { - Type: schema.TypeBool, + Type: sdkv2_schema.TypeBool, Required: true, Description: "Trigger on any release bundles or distributions", }, "registered_release_bundle_names": { - Type: schema.TypeSet, + Type: sdkv2_schema.TypeSet, Required: true, - Elem: &schema.Schema{Type: schema.TypeString}, + Elem: &sdkv2_schema.Schema{Type: sdkv2_schema.TypeString}, Description: "Trigger on this list of release bundle names", }, }), @@ -44,23 +324,21 @@ var releaseBundleWebhookSchema = func(webhookType string, version int, isCustom var packReleaseBundleCriteria = func(artifactoryCriteria map[string]interface{}) map[string]interface{} { return map[string]interface{}{ "any_release_bundle": artifactoryCriteria["anyReleaseBundle"].(bool), - "registered_release_bundle_names": schema.NewSet(schema.HashString, artifactoryCriteria["registeredReleaseBundlesNames"].([]interface{})), + "registered_release_bundle_names": sdkv2_schema.NewSet(sdkv2_schema.HashString, artifactoryCriteria["registeredReleaseBundlesNames"].([]interface{})), } } var unpackReleaseBundleCriteria = func(terraformCriteria map[string]interface{}, baseCriteria BaseCriteriaAPIModel) interface{} { - return ReleaseBundleWebhookCriteria{ + return ReleaseBundleCriteriaAPIModel{ AnyReleaseBundle: terraformCriteria["any_release_bundle"].(bool), - RegisteredReleaseBundlesNames: utilsdk.CastToStringArr(terraformCriteria["registered_release_bundle_names"].(*schema.Set).List()), + RegisteredReleaseBundlesNames: utilsdk.CastToStringArr(terraformCriteria["registered_release_bundle_names"].(*sdkv2_schema.Set).List()), BaseCriteriaAPIModel: baseCriteria, } } var releaseBundleCriteriaValidation = func(ctx context.Context, criteria map[string]interface{}) error { - tflog.Debug(ctx, "releaseBundleCriteriaValidation") - anyReleaseBundle := criteria["any_release_bundle"].(bool) - registeredReleaseBundlesNames := criteria["registered_release_bundle_names"].(*schema.Set).List() + registeredReleaseBundlesNames := criteria["registered_release_bundle_names"].(*sdkv2_schema.Set).List() if !anyReleaseBundle && len(registeredReleaseBundlesNames) == 0 { return fmt.Errorf("registered_release_bundle_names cannot be empty when any_release_bundle is false") 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 a349dc3e0..1a3209864 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,7 +4,6 @@ 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" ) @@ -57,8 +56,6 @@ var unpackReleaseBundleV2Criteria = func(terraformCriteria map[string]interface{ } var releaseBundleV2CriteriaValidation = func(ctx context.Context, criteria map[string]interface{}) error { - tflog.Debug(ctx, "releaseBundleV2CriteriaValidation") - anyReleaseBundle := criteria["any_release_bundle"].(bool) selectedReleaseBundles := criteria["selected_release_bundles"].(*schema.Set).List() diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_webhook_repo.go b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_repo.go index f3f16ff64..cec98d145 100644 --- a/pkg/artifactory/resource/webhook/resource_artifactory_webhook_repo.go +++ b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_repo.go @@ -142,8 +142,9 @@ func (r *RepoWebhookResource) Create(ctx context.Context, req resource.CreateReq } r.WebhookResource.Create(ctx, webhook, req, resp) - - plan.ID = plan.Key + if resp.Diagnostics.HasError() { + return + } // Save data into Terraform state resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) @@ -161,7 +162,14 @@ func (r *RepoWebhookResource) Read(ctx context.Context, req resource.ReadRequest } var webhook WebhookAPIModel - r.WebhookResource.Read(ctx, state.Key.ValueString(), &webhook, req, resp) + 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() { @@ -190,8 +198,9 @@ func (r *RepoWebhookResource) Update(ctx context.Context, req resource.UpdateReq } r.WebhookResource.Update(ctx, plan.Key.ValueString(), webhook, req, resp) - - plan.ID = plan.Key + if resp.Diagnostics.HasError() { + return + } // Save data into Terraform state resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) @@ -206,6 +215,9 @@ func (r *RepoWebhookResource) Delete(ctx context.Context, req resource.DeleteReq 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. diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_webhook_test.go b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_test.go index cadac0709..6cfbad528 100644 --- a/pkg/artifactory/resource/webhook/resource_artifactory_webhook_test.go +++ b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_test.go @@ -10,12 +10,12 @@ import ( "github.com/go-resty/resty/v2" "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/plancheck" "github.com/jfrog/terraform-provider-artifactory/v12/pkg/acctest" "github.com/jfrog/terraform-provider-artifactory/v12/pkg/artifactory/resource/webhook" "github.com/jfrog/terraform-provider-shared/client" "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{ @@ -230,7 +230,6 @@ func TestAccWebhook_HandlerValidation_ProxyWithURL(t *testing.T) { params := map[string]interface{}{ "webhookName": name, - "proxy": fmt.Sprintf("test-proxy-%d", id), } webhookConfig := util.ExecuteTemplate("TestAccWebhookEventTypesValidation", ` @@ -324,15 +323,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 }}" { @@ -354,7 +352,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" @@ -384,7 +382,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" @@ -411,12 +408,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"), } @@ -433,12 +429,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"), } @@ -463,8 +458,8 @@ func webhookTestCase(webhookType string, t *testing.T) (*testing.T, resource.Tes { ResourceName: fqrn, ImportState: true, + ImportStateId: name, ImportStateVerify: true, - ImportStateCheck: validator.CheckImportState(name, "key"), ImportStateVerifyIdentifierAttribute: "key", ImportStateVerifyIgnore: []string{"handler.0.secret", "handler.1.secret"}, }, @@ -472,6 +467,123 @@ func webhookTestCase(webhookType string, t *testing.T) (*testing.T, resource.Tes } } +func TestAccWebhook_UpgradeFromSDKv2(t *testing.T) { + // Can only realistically test these 3 types of webhook since creating + // build, release_bundle, or distribution in test environment is almost impossible + for _, webhookType := range []string{"artifact", "artifact_property", "docker"} { + t.Run(webhookType, func(t *testing.T) { + resource.Test(webhookMigrateFromSDKv2TestCase(webhookType, t)) + }) + } +} + +func webhookMigrateFromSDKv2TestCase(webhookType string, t *testing.T) (*testing.T, resource.TestCase) { + id := testutil.RandomInt() + name := fmt.Sprintf("webhook-%d", id) + fqrn := fmt.Sprintf("artifactory_%s_webhook.%s", webhookType, name) + + repoType := domainRepoTypeLookup[webhookType] + repoName := fmt.Sprintf("%s-local-%d", webhookType, id) + eventTypes := webhook.DomainEventTypesSupported[webhookType] + + params := map[string]interface{}{ + "repoName": repoName, + "repoType": repoType, + "webhookType": webhookType, + "webhookName": name, + "eventTypes": eventTypes, + "anyLocal": testutil.RandBool(), + "anyRemote": testutil.RandBool(), + "anyFederated": testutil.RandBool(), + } + config := util.ExecuteTemplate("TestAccWebhook{{ .webhookType }}Type", ` + resource "artifactory_local_{{ .repoType }}_repository" "{{ .repoName }}" { + key = "{{ .repoName }}" + } + + resource "artifactory_{{ .webhookType }}_webhook" "{{ .webhookName }}" { + key = "{{ .webhookName }}" + description = "test description" + event_types = [{{ range $index, $eventType := .eventTypes}}{{if $index}},{{end}}"{{$eventType}}"{{end}}] + criteria { + any_local = {{ .anyLocal }} + any_remote = {{ .anyRemote }} + any_federated = {{ .anyFederated }} + repo_keys = [artifactory_local_{{ .repoType }}_repository.{{ .repoName }}.key] + include_patterns = ["foo/**"] + exclude_patterns = ["bar/**"] + } + handler { + url = "https://google.com" + secret = "fake-secret" + use_secret_for_signing = true + custom_http_headers = { + header-1 = "value-1" + header-2 = "value-2" + } + } + handler { + url = "https://tempurl.com" + } + } + `, params) + + testChecks := []resource.TestCheckFunc{ + resource.TestCheckResourceAttr(fqrn, "key", name), + resource.TestCheckResourceAttr(fqrn, "event_types.#", fmt.Sprintf("%d", len(eventTypes))), + resource.TestCheckResourceAttr(fqrn, "criteria.#", "1"), + resource.TestCheckResourceAttr(fqrn, "criteria.0.any_local", fmt.Sprintf("%t", params["anyLocal"])), + resource.TestCheckResourceAttr(fqrn, "criteria.0.any_remote", fmt.Sprintf("%t", params["anyRemote"])), + resource.TestCheckResourceAttr(fqrn, "criteria.0.any_federated", fmt.Sprintf("%t", params["anyFederated"])), + resource.TestCheckTypeSetElemAttr(fqrn, "criteria.0.repo_keys.*", repoName), + resource.TestCheckResourceAttr(fqrn, "criteria.0.include_patterns.#", "1"), + resource.TestCheckResourceAttr(fqrn, "criteria.0.include_patterns.0", "foo/**"), + resource.TestCheckResourceAttr(fqrn, "criteria.0.exclude_patterns.#", "1"), + resource.TestCheckResourceAttr(fqrn, "criteria.0.exclude_patterns.0", "bar/**"), + resource.TestCheckResourceAttr(fqrn, "handler.#", "2"), + resource.TestCheckResourceAttr(fqrn, "handler.0.url", "https://google.com"), + resource.TestCheckResourceAttr(fqrn, "handler.0.secret", "fake-secret"), + resource.TestCheckResourceAttr(fqrn, "handler.0.use_secret_for_signing", "true"), + resource.TestCheckResourceAttr(fqrn, "handler.0.custom_http_headers.%", "2"), + resource.TestCheckResourceAttr(fqrn, "handler.0.custom_http_headers.header-1", "value-1"), + resource.TestCheckResourceAttr(fqrn, "handler.0.custom_http_headers.header-2", "value-2"), + resource.TestCheckResourceAttr(fqrn, "handler.1.url", "https://tempurl.com"), + resource.TestCheckResourceAttr(fqrn, "handler.1.secret", ""), + resource.TestCheckNoResourceAttr(fqrn, "handler.1.custom_http_headers"), + } + + for _, eventType := range eventTypes { + eventTypeCheck := resource.TestCheckTypeSetElemAttr(fqrn, "event_types.*", eventType) + testChecks = append(testChecks, eventTypeCheck) + } + + return t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + CheckDestroy: acctest.VerifyDeleted(fqrn, "key", testCheckWebhook), + Steps: []resource.TestStep{ + { + Config: config, + ExternalProviders: map[string]resource.ExternalProvider{ + "artifactory": { + Source: "jfrog/artifactory", + VersionConstraint: "12.1.0", + }, + }, + Check: resource.ComposeTestCheckFunc(testChecks...), + }, + { + Config: config, + ProtoV6ProviderFactories: acctest.ProtoV6MuxProviderFactories, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectEmptyPlan(), + }, + }, + }, + }, + } +} + func testCheckWebhook(id string, request *resty.Request) (*resty.Response, error) { return request. SetPathParam("webhookKey", id). @@ -482,7 +594,7 @@ func testCheckWebhook(id string, request *resty.Request) (*resty.Response, error func TestAccWebhook_GH476WebHookChangeBearerSet0(t *testing.T) { _, fqrn, name := testutil.MkNames("test-webhook", "artifactory_artifact_webhook") - format := ` + temp := ` resource "artifactory_artifact_webhook" "{{ .webhookName }}" { key = "{{ .webhookName }}" @@ -508,7 +620,7 @@ func TestAccWebhook_GH476WebHookChangeBearerSet0(t *testing.T) { firstToken := testutil.RandomInt() config1 := util.ExecuteTemplate( "TestAccWebhook{{ .webhookName }}", - format, + temp, map[string]interface{}{ "webhookName": name, "token": firstToken, @@ -517,24 +629,16 @@ 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) }, - ProtoV6ProviderFactories: acctest.ProtoV6MuxProviderFactories, + ProtoV6ProviderFactories: acctest.ProtoV6ProviderFactories, CheckDestroy: acctest.VerifyDeleted(fqrn, "key", testCheckWebhook), Steps: []resource.TestStep{ { @@ -547,14 +651,11 @@ func TestAccWebhook_GH476WebHookChangeBearerSet0(t *testing.T) { 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", }, }, }) From 67460920cd73cf7def9e9109963b08f9a0700d49 Mon Sep 17 00:00:00 2001 From: Alex Hung Date: Fri, 27 Sep 2024 13:43:29 -0700 Subject: [PATCH 06/17] Migrate other release bundle like webhook resources --- pkg/artifactory/provider/framework.go | 3 ++ .../webhook/resource_artifactory_webhook.go | 24 --------------- ...urce_artifactory_webhook_release_bundle.go | 30 +++++++++++++++++++ 3 files changed, 33 insertions(+), 24 deletions(-) diff --git a/pkg/artifactory/provider/framework.go b/pkg/artifactory/provider/framework.go index 4c820b9c5..694f2554f 100644 --- a/pkg/artifactory/provider/framework.go +++ b/pkg/artifactory/provider/framework.go @@ -224,7 +224,10 @@ func (p *ArtifactoryProvider) Resources(ctx context.Context) []func() resource.R replication.NewRemoteRepositoryReplicationResource, webhook.NewArtifactWebhookResource, webhook.NewArtifactPropertyWebhookResource, + webhook.NewArtifactoryReleaseBundleWebhookResource, webhook.NewBuildWebhookResource, + webhook.NewDestinationWebhookResource, + webhook.NewDistributionWebhookResource, webhook.NewDockerWebhookResource, webhook.NewReleaseBundleWebhookResource, } diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_webhook.go b/pkg/artifactory/resource/webhook/resource_artifactory_webhook.go index 024ebc540..c0de47e99 100644 --- a/pkg/artifactory/resource/webhook/resource_artifactory_webhook.go +++ b/pkg/artifactory/resource/webhook/resource_artifactory_webhook.go @@ -57,10 +57,6 @@ const currentSchemaVersion = 2 var DomainSupported = []string{ ArtifactLifecycleDomain, - ArtifactoryReleaseBundleDomain, - DestinationDomain, - DistributionDomain, - // ReleaseBundleDomain, ReleaseBundleV2Domain, ReleaseBundleV2PromotionDomain, UserDomain, @@ -585,10 +581,6 @@ var packKeyValuePair = func(keyValuePairs []KeyValuePairAPIModel) map[string]int } var domainCriteriaLookup = map[string]interface{}{ - ReleaseBundleDomain: ReleaseBundleCriteriaAPIModel{}, - DistributionDomain: ReleaseBundleCriteriaAPIModel{}, - ArtifactoryReleaseBundleDomain: ReleaseBundleCriteriaAPIModel{}, - DestinationDomain: ReleaseBundleCriteriaAPIModel{}, UserDomain: EmptyWebhookCriteria{}, ReleaseBundleV2Domain: ReleaseBundleV2WebhookCriteria{}, ReleaseBundleV2PromotionDomain: ReleaseBundleV2PromotionWebhookCriteria{}, @@ -596,10 +588,6 @@ var domainCriteriaLookup = map[string]interface{}{ } var domainPackLookup = map[string]func(map[string]interface{}) map[string]interface{}{ - ReleaseBundleDomain: packReleaseBundleCriteria, - DistributionDomain: packReleaseBundleCriteria, - ArtifactoryReleaseBundleDomain: packReleaseBundleCriteria, - DestinationDomain: packReleaseBundleCriteria, UserDomain: packEmptyCriteria, ReleaseBundleV2Domain: packReleaseBundleV2Criteria, ReleaseBundleV2PromotionDomain: packReleaseBundleV2PromotionCriteria, @@ -607,10 +595,6 @@ var domainPackLookup = map[string]func(map[string]interface{}) map[string]interf } var domainUnpackLookup = map[string]func(map[string]interface{}, BaseCriteriaAPIModel) interface{}{ - ReleaseBundleDomain: unpackReleaseBundleCriteria, - DistributionDomain: unpackReleaseBundleCriteria, - ArtifactoryReleaseBundleDomain: unpackReleaseBundleCriteria, - DestinationDomain: unpackReleaseBundleCriteria, UserDomain: unpackEmptyCriteria, ReleaseBundleV2Domain: unpackReleaseBundleV2Criteria, ReleaseBundleV2PromotionDomain: unpackReleaseBundleV2PromotionCriteria, @@ -619,10 +603,6 @@ var domainUnpackLookup = map[string]func(map[string]interface{}, BaseCriteriaAPI var domainSchemaLookup = func(version int, isCustom bool, webhookType string) map[string]map[string]*sdkv2_schema.Schema { return map[string]map[string]*sdkv2_schema.Schema{ - ReleaseBundleDomain: releaseBundleWebhookSchema(webhookType, version, isCustom), - DistributionDomain: releaseBundleWebhookSchema(webhookType, version, isCustom), - ArtifactoryReleaseBundleDomain: releaseBundleWebhookSchema(webhookType, version, isCustom), - DestinationDomain: releaseBundleWebhookSchema(webhookType, version, isCustom), UserDomain: userWebhookSchema(webhookType, version, isCustom), ReleaseBundleV2Domain: releaseBundleV2WebhookSchema(webhookType, version, isCustom), ReleaseBundleV2PromotionDomain: releaseBundleV2PromotionWebhookSchema(webhookType, version, isCustom), @@ -672,10 +652,6 @@ var packCriteria = func(d *sdkv2_schema.ResourceData, webhookType string, criter } var domainCriteriaValidationLookup = map[string]func(context.Context, map[string]interface{}) error{ - ReleaseBundleDomain: releaseBundleCriteriaValidation, - DistributionDomain: releaseBundleCriteriaValidation, - ArtifactoryReleaseBundleDomain: releaseBundleCriteriaValidation, - DestinationDomain: releaseBundleCriteriaValidation, UserDomain: emptyCriteriaValidation, ReleaseBundleV2Domain: releaseBundleV2CriteriaValidation, ReleaseBundleV2PromotionDomain: emptyCriteriaValidation, 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 cc66b1ac0..cb2bb14f3 100644 --- a/pkg/artifactory/resource/webhook/resource_artifactory_webhook_release_bundle.go +++ b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_release_bundle.go @@ -21,6 +21,36 @@ import ( var _ resource.Resource = &ReleaseBundleWebhookResource{} +func NewArtifactoryReleaseBundleWebhookResource() resource.Resource { + return &ReleaseBundleWebhookResource{ + WebhookResource: WebhookResource{ + TypeName: "artifactory_artifactory_release_bundle3_webhook", + Domain: BuildDomain, + 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: "artifactory_destination_webhook", + Domain: BuildDomain, + 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: "artifactory_distribution_webhook", + Domain: BuildDomain, + 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 NewReleaseBundleWebhookResource() resource.Resource { return &ReleaseBundleWebhookResource{ WebhookResource: WebhookResource{ From 40fbbcd7252beb16e6aca31e2668c690c063ef50 Mon Sep 17 00:00:00 2001 From: Alex Hung Date: Fri, 27 Sep 2024 14:52:15 -0700 Subject: [PATCH 07/17] Migrate artifact lifecycle webhook resource --- pkg/artifactory/provider/framework.go | 1 + ...resource_artifactory_artifact_lifecycle.go | 166 +++++++++++++++++- ...rce_artifactory_artifact_lifecycle_test.go | 90 ++++++++-- .../webhook/resource_artifactory_webhook.go | 154 +++++++++------- .../resource_artifactory_webhook_build.go | 62 +------ ...urce_artifactory_webhook_release_bundle.go | 20 +-- .../resource_artifactory_webhook_repo.go | 11 +- 7 files changed, 348 insertions(+), 156 deletions(-) diff --git a/pkg/artifactory/provider/framework.go b/pkg/artifactory/provider/framework.go index 694f2554f..b9bf42922 100644 --- a/pkg/artifactory/provider/framework.go +++ b/pkg/artifactory/provider/framework.go @@ -223,6 +223,7 @@ func (p *ArtifactoryProvider) Resources(ctx context.Context) []func() resource.R replication.NewLocalRepositoryMultiReplicationResource, replication.NewRemoteRepositoryReplicationResource, webhook.NewArtifactWebhookResource, + webhook.NewArtifactLifecycleWebhookResource, webhook.NewArtifactPropertyWebhookResource, webhook.NewArtifactoryReleaseBundleWebhookResource, webhook.NewBuildWebhookResource, 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..04379fc65 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,83 @@ 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 +90,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 +100,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 +113,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_webhook.go b/pkg/artifactory/resource/webhook/resource_artifactory_webhook.go index c0de47e99..d41dca313 100644 --- a/pkg/artifactory/resource/webhook/resource_artifactory_webhook.go +++ b/pkg/artifactory/resource/webhook/resource_artifactory_webhook.go @@ -56,7 +56,6 @@ const ( const currentSchemaVersion = 2 var DomainSupported = []string{ - ArtifactLifecycleDomain, ReleaseBundleV2Domain, ReleaseBundleV2PromotionDomain, UserDomain, @@ -99,7 +98,68 @@ var patternsSchemaAttributes = func(description string) map[string]schema.Attrib } } -func (r *WebhookResource) schema(domain string, criteriaBlock schema.SetNestedBlock) schema.Schema { +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), + }, + }, + } + + if criteriaBlock != nil { + blocks = lo.Assign( + blocks, + map[string]schema.Block{ + "criteria": *criteriaBlock, + }, + ) + } + return schema.Schema{ Version: currentSchemaVersion, Attributes: map[string]schema.Attribute{ @@ -140,58 +200,7 @@ func (r *WebhookResource) schema(domain string, criteriaBlock schema.SetNestedBl "Allow values: %v", strings.Trim(strings.Join(DomainEventTypesSupported[ArtifactDomain], ", "), "[]")), }, }, - Blocks: map[string]schema.Block{ - "criteria": criteriaBlock, - "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), - }, - }, - }, + Blocks: blocks, MarkdownDescription: r.Description, } } @@ -300,16 +309,20 @@ func (r *WebhookResource) ImportState(ctx context.Context, req resource.ImportSt resource.ImportStatePassthroughID(ctx, path.Root("key"), req, resp) } -type WebhookResourceModel struct { +type WebhookNoCriteriaResourceModel struct { Key types.String `tfsdk:"key"` Description types.String `tfsdk:"description"` Enabled types.Bool `tfsdk:"enabled"` EventTypes types.Set `tfsdk:"event_types"` - Criteria types.Set `tfsdk:"criteria"` Handlers types.Set `tfsdk:"handler"` } -func (m WebhookResourceModel) toAPIModel(ctx context.Context, domain string, criteriaAPIModel interface{}, apiModel *WebhookAPIModel) (diags diag.Diagnostics) { +type WebhookResourceModel struct { + WebhookNoCriteriaResourceModel + Criteria types.Set `tfsdk:"criteria"` +} + +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() { @@ -349,7 +362,6 @@ func (m WebhookResourceModel) toAPIModel(ctx context.Context, domain string, cri EventFilter: EventFilterAPIModel{ Domain: domain, EventTypes: eventTypes, - Criteria: criteriaAPIModel, }, Handlers: handlers, } @@ -357,6 +369,14 @@ func (m WebhookResourceModel) toAPIModel(ctx context.Context, domain string, cri return } +func (m WebhookResourceModel) toAPIModel(ctx context.Context, domain string, criteriaAPIModel interface{}, apiModel *WebhookAPIModel) (diags diag.Diagnostics) { + d := m.WebhookNoCriteriaResourceModel.toAPIModel(ctx, domain, apiModel) + + apiModel.EventFilter.Criteria = criteriaAPIModel + + return d +} + func (m *WebhookResourceModel) toBaseCriteriaAPIModel(ctx context.Context, criteriaAttrs map[string]attr.Value) (BaseCriteriaAPIModel, diag.Diagnostics) { diags := diag.Diagnostics{} @@ -424,7 +444,7 @@ func (m *WebhookResourceModel) fromBaseCriteriaAPIModel(ctx context.Context, cri }, diags } -func (m *WebhookResourceModel) fromAPIModel(ctx context.Context, apiModel WebhookAPIModel, stateHandlers basetypes.SetValue, criteriaSet basetypes.SetValue) diag.Diagnostics { +func (m *WebhookNoCriteriaResourceModel) fromAPIModel(ctx context.Context, apiModel WebhookAPIModel, stateHandlers basetypes.SetValue) diag.Diagnostics { diags := diag.Diagnostics{} m.Key = types.StringValue(apiModel.Key) @@ -442,7 +462,6 @@ func (m *WebhookResourceModel) fromAPIModel(ctx context.Context, apiModel Webhoo diags.Append(d...) } m.EventTypes = eventTypes - m.Criteria = criteriaSet handlers := lo.Map( apiModel.Handlers, @@ -521,6 +540,14 @@ func (m *WebhookResourceModel) fromAPIModel(ctx context.Context, apiModel Webhoo return diags } +func (m *WebhookResourceModel) fromAPIModel(ctx context.Context, apiModel WebhookAPIModel, stateHandlers basetypes.SetValue, criteriaSet *basetypes.SetValue) diag.Diagnostics { + if criteriaSet != nil { + m.Criteria = *criteriaSet + } + + return m.WebhookNoCriteriaResourceModel.fromAPIModel(ctx, apiModel, stateHandlers) +} + type WebhookAPIModel struct { Key string `json:"key"` Description string `json:"description"` @@ -536,7 +563,7 @@ func (w WebhookAPIModel) Id() string { type EventFilterAPIModel struct { Domain string `json:"domain"` EventTypes []string `json:"event_types"` - Criteria interface{} `json:"criteria"` + Criteria interface{} `json:"criteria,omitempty"` } type BaseCriteriaAPIModel struct { @@ -584,21 +611,18 @@ var domainCriteriaLookup = map[string]interface{}{ UserDomain: EmptyWebhookCriteria{}, ReleaseBundleV2Domain: ReleaseBundleV2WebhookCriteria{}, ReleaseBundleV2PromotionDomain: ReleaseBundleV2PromotionWebhookCriteria{}, - ArtifactLifecycleDomain: EmptyWebhookCriteria{}, } var domainPackLookup = map[string]func(map[string]interface{}) map[string]interface{}{ UserDomain: packEmptyCriteria, ReleaseBundleV2Domain: packReleaseBundleV2Criteria, ReleaseBundleV2PromotionDomain: packReleaseBundleV2PromotionCriteria, - ArtifactLifecycleDomain: packEmptyCriteria, } var domainUnpackLookup = map[string]func(map[string]interface{}, BaseCriteriaAPIModel) interface{}{ UserDomain: unpackEmptyCriteria, ReleaseBundleV2Domain: unpackReleaseBundleV2Criteria, ReleaseBundleV2PromotionDomain: unpackReleaseBundleV2PromotionCriteria, - ArtifactLifecycleDomain: unpackEmptyCriteria, } var domainSchemaLookup = func(version int, isCustom bool, webhookType string) map[string]map[string]*sdkv2_schema.Schema { @@ -606,7 +630,6 @@ var domainSchemaLookup = func(version int, isCustom bool, webhookType string) ma UserDomain: userWebhookSchema(webhookType, version, isCustom), ReleaseBundleV2Domain: releaseBundleV2WebhookSchema(webhookType, version, isCustom), ReleaseBundleV2PromotionDomain: releaseBundleV2PromotionWebhookSchema(webhookType, version, isCustom), - ArtifactLifecycleDomain: artifactLifecycleWebhookSchema(webhookType, version, isCustom), } } @@ -655,7 +678,6 @@ var domainCriteriaValidationLookup = map[string]func(context.Context, map[string UserDomain: emptyCriteriaValidation, ReleaseBundleV2Domain: releaseBundleV2CriteriaValidation, ReleaseBundleV2PromotionDomain: emptyCriteriaValidation, - ArtifactLifecycleDomain: emptyCriteriaValidation, } var emptyCriteriaValidation = func(ctx context.Context, criteria map[string]interface{}) error { diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_webhook_build.go b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_build.go index 3e0e93f60..322aff332 100644 --- a/pkg/artifactory/resource/webhook/resource_artifactory_webhook_build.go +++ b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_build.go @@ -2,6 +2,7 @@ package webhook import ( "context" + "fmt" "github.com/hashicorp/terraform-plugin-framework-validators/setvalidator" "github.com/hashicorp/terraform-plugin-framework/attr" @@ -12,9 +13,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - sdkv2_schema "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/jfrog/terraform-provider-shared/util" - utilsdk "github.com/jfrog/terraform-provider-shared/util/sdk" "github.com/samber/lo" ) @@ -23,7 +22,7 @@ var _ resource.Resource = &BuildWebhookResource{} func NewBuildWebhookResource() resource.Resource { return &BuildWebhookResource{ WebhookResource: WebhookResource{ - TypeName: "artifactory_build_webhook", + 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.", }, @@ -67,7 +66,7 @@ func (r *BuildWebhookResource) Schema(ctx context.Context, req resource.SchemaRe Description: "Specifies where the webhook will be applied on which builds.", } - resp.Schema = r.schema(r.Domain, criteriaBlock) + resp.Schema = r.schema(r.Domain, &criteriaBlock) } func (r *BuildWebhookResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { @@ -279,7 +278,7 @@ func (m *BuildWebhookResourceModel) fromAPIModel(ctx context.Context, apiModel W diags.Append(d...) } - d = m.WebhookResourceModel.fromAPIModel(ctx, apiModel, stateHandlers, criteriaSet) + d = m.WebhookResourceModel.fromAPIModel(ctx, apiModel, stateHandlers, &criteriaSet) if d.HasError() { diags.Append(d...) } @@ -292,56 +291,3 @@ type BuildCriteriaAPIModel struct { AnyBuild bool `json:"anyBuild"` SelectedBuilds []string `json:"selectedBuilds"` } - -var buildWebhookSchema = func(webhookType string, version int, isCustom bool) map[string]*sdkv2_schema.Schema { - return utilsdk.MergeMaps(getBaseSchemaByVersion(webhookType, version, isCustom), map[string]*sdkv2_schema.Schema{ - "criteria": { - Type: sdkv2_schema.TypeSet, - Required: true, - MaxItems: 1, - Elem: &sdkv2_schema.Resource{ - Schema: utilsdk.MergeMaps(baseCriteriaSchema, map[string]*sdkv2_schema.Schema{ - "any_build": { - Type: sdkv2_schema.TypeBool, - Required: true, - Description: "Trigger on any builds", - }, - "selected_builds": { - Type: sdkv2_schema.TypeSet, - Required: true, - Elem: &sdkv2_schema.Schema{Type: sdkv2_schema.TypeString}, - Description: "Trigger on this list of build IDs", - }, - }), - }, - Description: "Specifies where the webhook will be applied on which builds.", - }, - }) -} - -// var packBuildCriteria = func(artifactoryCriteria map[string]interface{}) map[string]interface{} { -// return map[string]interface{}{ -// "any_build": artifactoryCriteria["anyBuild"].(bool), -// "selected_builds": sdkv2_schema.NewSet(sdkv2_schema.HashString, artifactoryCriteria["selectedBuilds"].([]interface{})), -// } -// } -// -// var unpackBuildCriteria = func(terraformCriteria map[string]interface{}, baseCriteria BaseCriteriaAPIModel) interface{} { -// return BuildCriteriaAPIModel{ -// AnyBuild: terraformCriteria["any_build"].(bool), -// SelectedBuilds: utilsdk.CastToStringArr(terraformCriteria["selected_builds"].(*sdkv2_schema.Set).List()), -// BaseCriteriaAPIModel: baseCriteria, -// } -// } -// -// var buildCriteriaValidation = func(ctx context.Context, criteria map[string]interface{}) error { -// anyBuild := criteria["any_build"].(bool) -// selectedBuilds := criteria["selected_builds"].(*sdkv2_schema.Set).List() -// includePatterns := criteria["include_patterns"].(*sdkv2_schema.Set).List() -// -// if !anyBuild && (len(selectedBuilds) == 0 && len(includePatterns) == 0) { -// return fmt.Errorf("selected_builds or include_patterns cannot be empty when any_build is false") -// } -// -// return nil -// } 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 cb2bb14f3..59900b624 100644 --- a/pkg/artifactory/resource/webhook/resource_artifactory_webhook_release_bundle.go +++ b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_release_bundle.go @@ -24,8 +24,8 @@ var _ resource.Resource = &ReleaseBundleWebhookResource{} func NewArtifactoryReleaseBundleWebhookResource() resource.Resource { return &ReleaseBundleWebhookResource{ WebhookResource: WebhookResource{ - TypeName: "artifactory_artifactory_release_bundle3_webhook", - Domain: BuildDomain, + 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.:", }, } @@ -34,8 +34,8 @@ func NewArtifactoryReleaseBundleWebhookResource() resource.Resource { func NewDestinationWebhookResource() resource.Resource { return &ReleaseBundleWebhookResource{ WebhookResource: WebhookResource{ - TypeName: "artifactory_destination_webhook", - Domain: BuildDomain, + 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.:", }, } @@ -44,8 +44,8 @@ func NewDestinationWebhookResource() resource.Resource { func NewDistributionWebhookResource() resource.Resource { return &ReleaseBundleWebhookResource{ WebhookResource: WebhookResource{ - TypeName: "artifactory_distribution_webhook", - Domain: BuildDomain, + 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.:", }, } @@ -54,8 +54,8 @@ func NewDistributionWebhookResource() resource.Resource { func NewReleaseBundleWebhookResource() resource.Resource { return &ReleaseBundleWebhookResource{ WebhookResource: WebhookResource{ - TypeName: "artifactory_release_bundle_webhook", - Domain: BuildDomain, + 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.", }, @@ -99,7 +99,7 @@ func (r *ReleaseBundleWebhookResource) Schema(ctx context.Context, req resource. Description: "Specifies where the webhook will be applied, on which release bundles or distributions.", } - resp.Schema = r.schema(r.Domain, criteriaBlock) + resp.Schema = r.schema(r.Domain, &criteriaBlock) } func (r *ReleaseBundleWebhookResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { @@ -311,7 +311,7 @@ func (m *ReleaseBundleWebhookResourceModel) fromAPIModel(ctx context.Context, ap diags.Append(d...) } - d = m.WebhookResourceModel.fromAPIModel(ctx, apiModel, stateHandlers, criteriaSet) + d = m.WebhookResourceModel.fromAPIModel(ctx, apiModel, stateHandlers, &criteriaSet) if d.HasError() { diags.Append(d...) } diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_webhook_repo.go b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_repo.go index cec98d145..fc607bb47 100644 --- a/pkg/artifactory/resource/webhook/resource_artifactory_webhook_repo.go +++ b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_repo.go @@ -2,6 +2,7 @@ package webhook import ( "context" + "fmt" "github.com/hashicorp/terraform-plugin-framework-validators/setvalidator" "github.com/hashicorp/terraform-plugin-framework/attr" @@ -21,7 +22,7 @@ var _ resource.Resource = &RepoWebhookResource{} func NewArtifactWebhookResource() resource.Resource { return &RepoWebhookResource{ WebhookResource: WebhookResource{ - TypeName: "artifactory_artifact_webhook", + 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.", }, @@ -31,7 +32,7 @@ func NewArtifactWebhookResource() resource.Resource { func NewArtifactPropertyWebhookResource() resource.Resource { return &RepoWebhookResource{ WebhookResource: WebhookResource{ - TypeName: "artifactory_artifact_property_webhook", + 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.", }, @@ -41,7 +42,7 @@ func NewArtifactPropertyWebhookResource() resource.Resource { func NewDockerWebhookResource() resource.Resource { return &RepoWebhookResource{ WebhookResource: WebhookResource{ - TypeName: "artifactory_docker_webhook", + 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.", }, @@ -93,7 +94,7 @@ func (r *RepoWebhookResource) Schema(ctx context.Context, req resource.SchemaReq Description: "Specifies where the webhook will be applied on which repositories.", } - resp.Schema = r.schema(r.Domain, criteriaBlock) + resp.Schema = r.schema(r.Domain, &criteriaBlock) } func (r *RepoWebhookResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { @@ -313,7 +314,7 @@ func (m *RepoWebhookResourceModel) fromAPIModel(ctx context.Context, apiModel We diags.Append(d...) } - d = m.WebhookResourceModel.fromAPIModel(ctx, apiModel, stateHandlers, criteriaSet) + d = m.WebhookResourceModel.fromAPIModel(ctx, apiModel, stateHandlers, &criteriaSet) if d.HasError() { diags.Append(d...) } From 6a2198761102e0d4e9b460cc37f4021cbcc6ffe4 Mon Sep 17 00:00:00 2001 From: Alex Hung Date: Fri, 27 Sep 2024 15:22:39 -0700 Subject: [PATCH 08/17] Migrate release bundle v2 webhook resource --- pkg/artifactory/provider/framework.go | 1 + .../webhook/resource_artifactory_webhook.go | 6 - ...urce_artifactory_webhook_release_bundle.go | 56 +--- ...e_artifactory_webhook_release_bundle_v2.go | 300 +++++++++++++++--- 4 files changed, 265 insertions(+), 98 deletions(-) diff --git a/pkg/artifactory/provider/framework.go b/pkg/artifactory/provider/framework.go index b9bf42922..800119e5c 100644 --- a/pkg/artifactory/provider/framework.go +++ b/pkg/artifactory/provider/framework.go @@ -231,6 +231,7 @@ func (p *ArtifactoryProvider) Resources(ctx context.Context) []func() resource.R webhook.NewDistributionWebhookResource, webhook.NewDockerWebhookResource, webhook.NewReleaseBundleWebhookResource, + webhook.NewReleaseBundleV2WebhookResource, } } diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_webhook.go b/pkg/artifactory/resource/webhook/resource_artifactory_webhook.go index d41dca313..8ed3e7c33 100644 --- a/pkg/artifactory/resource/webhook/resource_artifactory_webhook.go +++ b/pkg/artifactory/resource/webhook/resource_artifactory_webhook.go @@ -56,7 +56,6 @@ const ( const currentSchemaVersion = 2 var DomainSupported = []string{ - ReleaseBundleV2Domain, ReleaseBundleV2PromotionDomain, UserDomain, } @@ -609,26 +608,22 @@ var packKeyValuePair = func(keyValuePairs []KeyValuePairAPIModel) map[string]int var domainCriteriaLookup = map[string]interface{}{ UserDomain: EmptyWebhookCriteria{}, - ReleaseBundleV2Domain: ReleaseBundleV2WebhookCriteria{}, ReleaseBundleV2PromotionDomain: ReleaseBundleV2PromotionWebhookCriteria{}, } var domainPackLookup = map[string]func(map[string]interface{}) map[string]interface{}{ UserDomain: packEmptyCriteria, - ReleaseBundleV2Domain: packReleaseBundleV2Criteria, ReleaseBundleV2PromotionDomain: packReleaseBundleV2PromotionCriteria, } var domainUnpackLookup = map[string]func(map[string]interface{}, BaseCriteriaAPIModel) interface{}{ UserDomain: unpackEmptyCriteria, - ReleaseBundleV2Domain: unpackReleaseBundleV2Criteria, ReleaseBundleV2PromotionDomain: unpackReleaseBundleV2PromotionCriteria, } var domainSchemaLookup = func(version int, isCustom bool, webhookType string) map[string]map[string]*sdkv2_schema.Schema { return map[string]map[string]*sdkv2_schema.Schema{ UserDomain: userWebhookSchema(webhookType, version, isCustom), - ReleaseBundleV2Domain: releaseBundleV2WebhookSchema(webhookType, version, isCustom), ReleaseBundleV2PromotionDomain: releaseBundleV2PromotionWebhookSchema(webhookType, version, isCustom), } } @@ -676,7 +671,6 @@ var packCriteria = func(d *sdkv2_schema.ResourceData, webhookType string, criter var domainCriteriaValidationLookup = map[string]func(context.Context, map[string]interface{}) error{ UserDomain: emptyCriteriaValidation, - ReleaseBundleV2Domain: releaseBundleV2CriteriaValidation, ReleaseBundleV2PromotionDomain: emptyCriteriaValidation, } 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 59900b624..a1933dc14 100644 --- a/pkg/artifactory/resource/webhook/resource_artifactory_webhook_release_bundle.go +++ b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_release_bundle.go @@ -13,9 +13,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - sdkv2_schema "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/jfrog/terraform-provider-shared/util" - utilsdk "github.com/jfrog/terraform-provider-shared/util/sdk" "github.com/samber/lo" ) @@ -84,7 +82,7 @@ func (r *ReleaseBundleWebhookResource) Schema(ctx context.Context, req resource. Required: true, Description: "Trigger on any release bundles or distributions", }, - "selected_builds": schema.SetAttribute{ + "registered_release_bundle_names": schema.SetAttribute{ ElementType: types.StringType, Required: true, Description: "Trigger on this list of release bundle names", @@ -324,55 +322,3 @@ type ReleaseBundleCriteriaAPIModel struct { AnyReleaseBundle bool `json:"anyReleaseBundle"` RegisteredReleaseBundlesNames []string `json:"registeredReleaseBundlesNames"` } - -var releaseBundleWebhookSchema = func(webhookType string, version int, isCustom bool) map[string]*sdkv2_schema.Schema { - return utilsdk.MergeMaps(getBaseSchemaByVersion(webhookType, version, isCustom), map[string]*sdkv2_schema.Schema{ - "criteria": { - Type: sdkv2_schema.TypeSet, - Required: true, - MaxItems: 1, - Elem: &sdkv2_schema.Resource{ - Schema: utilsdk.MergeMaps(baseCriteriaSchema, map[string]*sdkv2_schema.Schema{ - "any_release_bundle": { - Type: sdkv2_schema.TypeBool, - Required: true, - Description: "Trigger on any release bundles or distributions", - }, - "registered_release_bundle_names": { - Type: sdkv2_schema.TypeSet, - Required: true, - Elem: &sdkv2_schema.Schema{Type: sdkv2_schema.TypeString}, - Description: "Trigger on this list of release bundle names", - }, - }), - }, - Description: "Specifies where the webhook will be applied, on which release bundles or distributions.", - }, - }) -} - -var packReleaseBundleCriteria = func(artifactoryCriteria map[string]interface{}) map[string]interface{} { - return map[string]interface{}{ - "any_release_bundle": artifactoryCriteria["anyReleaseBundle"].(bool), - "registered_release_bundle_names": sdkv2_schema.NewSet(sdkv2_schema.HashString, artifactoryCriteria["registeredReleaseBundlesNames"].([]interface{})), - } -} - -var unpackReleaseBundleCriteria = func(terraformCriteria map[string]interface{}, baseCriteria BaseCriteriaAPIModel) interface{} { - return ReleaseBundleCriteriaAPIModel{ - AnyReleaseBundle: terraformCriteria["any_release_bundle"].(bool), - RegisteredReleaseBundlesNames: utilsdk.CastToStringArr(terraformCriteria["registered_release_bundle_names"].(*sdkv2_schema.Set).List()), - BaseCriteriaAPIModel: baseCriteria, - } -} - -var releaseBundleCriteriaValidation = func(ctx context.Context, criteria map[string]interface{}) error { - anyReleaseBundle := criteria["any_release_bundle"].(bool) - registeredReleaseBundlesNames := criteria["registered_release_bundle_names"].(*sdkv2_schema.Set).List() - - if !anyReleaseBundle && len(registeredReleaseBundlesNames) == 0 { - return fmt.Errorf("registered_release_bundle_names cannot be empty when any_release_bundle is false") - } - - return nil -} 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 1a3209864..45ec792c0 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,64 +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 ReleaseBundleV2WebhookCriteria struct { - BaseCriteriaAPIModel - 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", }, - "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", + ) + } +} + +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)...) +} + +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 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) 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 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.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 unpackReleaseBundleV2Criteria = func(terraformCriteria map[string]interface{}, baseCriteria BaseCriteriaAPIModel) interface{} { - return ReleaseBundleV2WebhookCriteria{ - AnyReleaseBundle: terraformCriteria["any_release_bundle"].(bool), - SelectedReleaseBundles: utilsdk.CastToStringArr(terraformCriteria["selected_release_bundles"].(*schema.Set).List()), +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 releaseBundleV2CriteriaValidation = func(ctx context.Context, criteria map[string]interface{}) error { - anyReleaseBundle := criteria["any_release_bundle"].(bool) - selectedReleaseBundles := criteria["selected_release_bundles"].(*schema.Set).List() +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...) + } - if !anyReleaseBundle && len(selectedReleaseBundles) == 0 { - return fmt.Errorf("selected_release_bundles cannot be empty when any_release_bundle is false") + releaseBundleNames = rb } - return nil + 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"` } From ec84142025f81aa7785c46501db12e819185282b Mon Sep 17 00:00:00 2001 From: Alex Hung Date: Fri, 27 Sep 2024 16:09:49 -0700 Subject: [PATCH 09/17] Migrate release bundle v2 promotion webhook resource --- pkg/artifactory/provider/framework.go | 1 + .../webhook/resource_artifactory_webhook.go | 15 +- ...e_artifactory_webhook_release_bundle_v2.go | 2 +- ...ory_webhook_release_bundle_v2_promotion.go | 262 ++++++++++++++++-- ...ebhook_release_bundle_v2_promotion_test.go | 102 ++++++- 5 files changed, 332 insertions(+), 50 deletions(-) diff --git a/pkg/artifactory/provider/framework.go b/pkg/artifactory/provider/framework.go index 800119e5c..5ae328c1d 100644 --- a/pkg/artifactory/provider/framework.go +++ b/pkg/artifactory/provider/framework.go @@ -232,6 +232,7 @@ func (p *ArtifactoryProvider) Resources(ctx context.Context) []func() resource.R webhook.NewDockerWebhookResource, webhook.NewReleaseBundleWebhookResource, webhook.NewReleaseBundleV2WebhookResource, + webhook.NewReleaseBundleV2PromotionWebhookResource, } } diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_webhook.go b/pkg/artifactory/resource/webhook/resource_artifactory_webhook.go index 8ed3e7c33..481d982a4 100644 --- a/pkg/artifactory/resource/webhook/resource_artifactory_webhook.go +++ b/pkg/artifactory/resource/webhook/resource_artifactory_webhook.go @@ -56,7 +56,6 @@ const ( const currentSchemaVersion = 2 var DomainSupported = []string{ - ReleaseBundleV2PromotionDomain, UserDomain, } @@ -608,23 +607,20 @@ var packKeyValuePair = func(keyValuePairs []KeyValuePairAPIModel) map[string]int var domainCriteriaLookup = map[string]interface{}{ UserDomain: EmptyWebhookCriteria{}, - ReleaseBundleV2PromotionDomain: ReleaseBundleV2PromotionWebhookCriteria{}, + ReleaseBundleV2PromotionDomain: ReleaseBundleV2PromotionCriteriaAPIModel{}, } var domainPackLookup = map[string]func(map[string]interface{}) map[string]interface{}{ - UserDomain: packEmptyCriteria, - ReleaseBundleV2PromotionDomain: packReleaseBundleV2PromotionCriteria, + UserDomain: packEmptyCriteria, } var domainUnpackLookup = map[string]func(map[string]interface{}, BaseCriteriaAPIModel) interface{}{ - UserDomain: unpackEmptyCriteria, - ReleaseBundleV2PromotionDomain: unpackReleaseBundleV2PromotionCriteria, + UserDomain: unpackEmptyCriteria, } var domainSchemaLookup = func(version int, isCustom bool, webhookType string) map[string]map[string]*sdkv2_schema.Schema { return map[string]map[string]*sdkv2_schema.Schema{ - UserDomain: userWebhookSchema(webhookType, version, isCustom), - ReleaseBundleV2PromotionDomain: releaseBundleV2PromotionWebhookSchema(webhookType, version, isCustom), + UserDomain: userWebhookSchema(webhookType, version, isCustom), } } @@ -670,8 +666,7 @@ var packCriteria = func(d *sdkv2_schema.ResourceData, webhookType string, criter } var domainCriteriaValidationLookup = map[string]func(context.Context, map[string]interface{}) error{ - UserDomain: emptyCriteriaValidation, - ReleaseBundleV2PromotionDomain: emptyCriteriaValidation, + UserDomain: emptyCriteriaValidation, } var emptyCriteriaValidation = func(ctx context.Context, criteria map[string]interface{}) error { 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 45ec792c0..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 @@ -49,7 +49,7 @@ func (r *ReleaseBundleV2WebhookResource) Schema(ctx context.Context, req resourc 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": schema.SetAttribute{ ElementType: types.StringType, 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 ec5fa8d7f..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 BaseCriteriaAPIModel) 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"}, }}, }) } From dda5d11c772e2488531577da171d9095ebb61ebe Mon Sep 17 00:00:00 2001 From: Alex Hung Date: Fri, 27 Sep 2024 16:35:32 -0700 Subject: [PATCH 10/17] Migrate user webhook resource --- pkg/artifactory/provider/framework.go | 1 + pkg/artifactory/provider/resources.go | 2 - ...rce_artifactory_artifact_lifecycle_test.go | 3 +- .../resource_artifactory_custom_webhook.go | 399 +++++++++++++++++ .../webhook/resource_artifactory_webhook.go | 418 +----------------- .../resource_artifactory_webhook_test.go | 25 +- .../resource_artifactory_webhook_user.go | 164 ++++++- .../resource_artifactory_webhook_user_test.go | 86 +++- 8 files changed, 642 insertions(+), 456 deletions(-) diff --git a/pkg/artifactory/provider/framework.go b/pkg/artifactory/provider/framework.go index 5ae328c1d..692d3303b 100644 --- a/pkg/artifactory/provider/framework.go +++ b/pkg/artifactory/provider/framework.go @@ -233,6 +233,7 @@ func (p *ArtifactoryProvider) Resources(ctx context.Context) []func() resource.R webhook.NewReleaseBundleWebhookResource, webhook.NewReleaseBundleV2WebhookResource, webhook.NewReleaseBundleV2PromotionWebhookResource, + webhook.NewUserWebhookResource, } } diff --git a/pkg/artifactory/provider/resources.go b/pkg/artifactory/provider/resources.go index ee2e6fd28..0ce00edcf 100644 --- a/pkg/artifactory/provider/resources.go +++ b/pkg/artifactory/provider/resources.go @@ -125,8 +125,6 @@ func resourcesMap() map[string]*schema.Resource { } for _, webhookType := range webhook.DomainSupported { - webhookResourceName := fmt.Sprintf("artifactory_%s_webhook", webhookType) - resourcesMap[webhookResourceName] = webhook.ResourceArtifactoryWebhook(webhookType) webhookCustomResourceName := fmt.Sprintf("artifactory_%s_custom_webhook", webhookType) resourcesMap[webhookCustomResourceName] = webhook.ResourceArtifactoryCustomWebhook(webhookType) } 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 04379fc65..31c19aad9 100644 --- a/pkg/artifactory/resource/webhook/resource_artifactory_artifact_lifecycle_test.go +++ b/pkg/artifactory/resource/webhook/resource_artifactory_artifact_lifecycle_test.go @@ -69,7 +69,8 @@ func TestAccWebhook_ArtifactLifecycle_UpgradeFromSDKv2(t *testing.T) { plancheck.ExpectEmptyPlan(), }, }, - }}, + }, + }, }) } diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook.go b/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook.go index aac638c54..a8d00acdd 100644 --- a/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook.go +++ b/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook.go @@ -419,3 +419,402 @@ func ResourceArtifactoryCustomWebhook(webhookType string) *schema.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 +} + +var domainCriteriaLookup = map[string]interface{}{} + +var domainPackLookup = map[string]func(map[string]interface{}) map[string]interface{}{} + +var domainUnpackLookup = map[string]func(map[string]interface{}, BaseCriteriaAPIModel) interface{}{} + +var domainSchemaLookup = func(version int, isCustom bool, webhookType string) map[string]map[string]*schema.Schema { + return map[string]map[string]*schema.Schema{} +} + +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{ + UserDomain: emptyCriteriaValidation, +} + +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 +} + +func ResourceArtifactoryWebhook(webhookType string) *schema.Resource { + + var unpackWebhook = func(data *schema.ResourceData) (WebhookAPIModel, error) { + d := &utilsdk.ResourceData{ResourceData: data} + + var unpackHandlers = func(d *utilsdk.ResourceData) []HandlerAPIModel { + var webhookHandlers []HandlerAPIModel + + 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 := HandlerAPIModel{ + HandlerType: "webhook", + Url: h["url"].(string), + } + + if v, ok := h["secret"]; ok { + if s, ok := v.(string); ok { + webhookHandler.Secret = &s + } + } + + if v, ok := h["use_secret_for_signing"]; ok { + if b, ok := v.(bool); ok { + webhookHandler.UseSecretForSigning = &b + } + } + + if v, ok := h["proxy"]; ok { + if s, ok := v.(string); ok { + webhookHandler.Proxy = &s + } + } + + if v, ok := h["custom_http_headers"]; ok { + webhookHandler.CustomHttpHeaders = unpackKeyValuePair(v.(map[string]interface{})) + } + + webhookHandlers = append(webhookHandlers, webhookHandler) + } + } + } + + return webhookHandlers + } + + webhook := WebhookAPIModel{ + Key: d.GetString("key", false), + Description: d.GetString("description", false), + Enabled: d.GetBool("enabled", false), + EventFilter: EventFilterAPIModel{ + Domain: webhookType, + EventTypes: d.GetSet("event_types"), + Criteria: unpackCriteria(d, webhookType), + }, + Handlers: unpackHandlers(d), + } + + return webhook, nil + } + + var packHandlers = func(d *schema.ResourceData, handlers []HandlerAPIModel) []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), + } + + if handler.UseSecretForSigning != nil { + packedHandler["use_secret_for_signing"] = *handler.UseSecretForSigning + } + + if handler.Proxy != nil { + packedHandler["proxy"] = *handler.Proxy + } + + if handler.CustomHttpHeaders != nil { + packedHandler["custom_http_headers"] = packKeyValuePair(handler.CustomHttpHeaders) + } + + packedHandlers = append(packedHandlers, packedHandler) + } + + return setValue("handler", schema.NewSet(schema.HashResource(resource), packedHandlers)) + } + + var packWebhook = func(d *schema.ResourceData, webhook WebhookAPIModel) diag.Diagnostics { + setValue := utilsdk.MkLens(d) + + setValue("key", webhook.Key) + setValue("description", webhook.Description) + setValue("enabled", webhook.Enabled) + errors := setValue("event_types", webhook.EventFilter.EventTypes) + if webhook.EventFilter.Criteria != nil { + errors = append(errors, packCriteria(d, webhookType, webhook.EventFilter.Criteria.(map[string]interface{}))...) + } + errors = append(errors, packHandlers(d, webhook.Handlers)...) + + if len(errors) > 0 { + return diag.Errorf("failed to pack webhook %q", errors) + } + + return nil + } + + var readWebhook = func(ctx context.Context, data *schema.ResourceData, m interface{}) diag.Diagnostics { + webhook := WebhookAPIModel{} + + webhook.EventFilter.Criteria = domainCriteriaLookup[webhookType] + + var artifactoryError artifactory.ArtifactoryErrorsResponse + resp, err := m.(util.ProviderMetadata).Client.R(). + SetPathParam("webhookKey", data.Id()). + SetResult(&webhook). + SetError(&artifactoryError). + Get(WebhookURL) + + if err != nil { + return 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 createWebhook = func(ctx context.Context, data *schema.ResourceData, m interface{}) diag.Diagnostics { + webhook, err := unpackWebhook(data) + if err != nil { + return diag.FromErr(err) + } + + var artifactoryError artifactory.ArtifactoryErrorsResponse + resp, err := m.(util.ProviderMetadata).Client.R(). + SetBody(webhook). + AddRetryCondition(retryOnProxyError). + SetError(&artifactoryError). + Post(webhooksURL) + if err != nil { + return diag.FromErr(err) + } + + if resp.IsError() { + return diag.Errorf("%s", artifactoryError.String()) + } + + data.SetId(webhook.Id()) + + return readWebhook(ctx, data, m) + } + + var updateWebhook = func(ctx context.Context, data *schema.ResourceData, m interface{}) diag.Diagnostics { + webhook, err := unpackWebhook(data) + if err != nil { + return diag.FromErr(err) + } + + var artifactoryError artifactory.ArtifactoryErrorsResponse + resp, err := m.(util.ProviderMetadata).Client.R(). + SetPathParam("webhookKey", data.Id()). + SetBody(webhook). + AddRetryCondition(retryOnProxyError). + SetError(&artifactoryError). + Put(WebhookURL) + if err != nil { + return diag.FromErr(err) + } + + if resp.IsError() { + return diag.Errorf("%s", artifactoryError.String()) + } + + data.SetId(webhook.Id()) + + return readWebhook(ctx, data, m) + } + + var deleteWebhook = func(ctx context.Context, data *schema.ResourceData, m interface{}) diag.Diagnostics { + var artifactoryError artifactory.ArtifactoryErrorsResponse + resp, err := m.(util.ProviderMetadata).Client.R(). + SetPathParam("webhookKey", data.Id()). + SetError(&artifactoryError). + Delete(WebhookURL) + + if err != nil { + return diag.FromErr(err) + } + + if resp.StatusCode() == http.StatusNotFound { + data.SetId("") + return nil + } + + if resp.IsError() { + return diag.Errorf("%s", artifactoryError.String()) + } + + return nil + } + + var eventTypesDiff = func(ctx context.Context, diff *schema.ResourceDiff, v interface{}) error { + eventTypes := diff.Get("event_types").(*schema.Set).List() + if len(eventTypes) == 0 { + return nil + } + + eventTypesSupported := DomainEventTypesSupported[webhookType] + for _, eventType := range eventTypes { + if !slices.Contains(eventTypesSupported, eventType.(string)) { + return fmt.Errorf("event_type %s not supported for domain %s", eventType, webhookType) + } + } + return nil + } + + var criteriaDiff = func(ctx context.Context, diff *schema.ResourceDiff, v interface{}) error { + if resource, ok := diff.GetOk("criteria"); ok { + criteria := resource.(*schema.Set).List() + if len(criteria) == 0 { + return nil + } + return domainCriteriaValidationLookup[webhookType](ctx, criteria[0].(map[string]interface{})) + } + + return nil + } + + // 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], + } + + rs := schema.Resource{ + SchemaVersion: 2, + CreateContext: createWebhook, + ReadContext: readWebhook, + UpdateContext: updateWebhook, + DeleteContext: deleteWebhook, + + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Schema: domainSchemaLookup(currentSchemaVersion, false, webhookType)[webhookType], + StateUpgraders: []schema.StateUpgrader{ + { + Type: resourceSchemaV1.CoreConfigSchema().ImpliedType(), + Upgrade: ResourceStateUpgradeV1, + Version: 1, + }, + }, + + CustomizeDiff: customdiff.All( + eventTypesDiff, + criteriaDiff, + ), + Description: "Provides an Artifactory webhook resource", + } + + if webhookType == "artifactory_release_bundle" { + rs.DeprecationMessage = "This resource is being deprecated and replaced by artifactory_destination_webhook resource" + } + + return &rs +} + +// 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"], + }, + } + + delete(rawState, "url") + delete(rawState, "secret") + delete(rawState, "proxy") + delete(rawState, "custom_http_headers") + + return rawState, nil +} diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_webhook.go b/pkg/artifactory/resource/webhook/resource_artifactory_webhook.go index 481d982a4..1ed988f1d 100644 --- a/pkg/artifactory/resource/webhook/resource_artifactory_webhook.go +++ b/pkg/artifactory/resource/webhook/resource_artifactory_webhook.go @@ -22,17 +22,11 @@ import ( "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - sdkv2_diag "github.com/hashicorp/terraform-plugin-sdk/v2/diag" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff" - sdkv2_schema "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/jfrog/terraform-provider-artifactory/v12/pkg/artifactory" "github.com/jfrog/terraform-provider-shared/util" utilfw "github.com/jfrog/terraform-provider-shared/util/fw" - utilsdk "github.com/jfrog/terraform-provider-shared/util/sdk" validatorfw_string "github.com/jfrog/terraform-provider-shared/validator/fw/string" "github.com/samber/lo" - - "golang.org/x/exp/slices" ) const ( @@ -55,9 +49,7 @@ const ( const currentSchemaVersion = 2 -var DomainSupported = []string{ - UserDomain, -} +var DomainSupported = []string{} var DomainEventTypesSupported = map[string][]string{ ArtifactDomain: {"deployed", "deleted", "moved", "copied", "cached"}, @@ -583,416 +575,8 @@ type KeyValuePairAPIModel struct { Value string `json:"value"` } -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 -} - -var domainCriteriaLookup = map[string]interface{}{ - UserDomain: EmptyWebhookCriteria{}, - ReleaseBundleV2PromotionDomain: ReleaseBundleV2PromotionCriteriaAPIModel{}, -} - -var domainPackLookup = map[string]func(map[string]interface{}) map[string]interface{}{ - UserDomain: packEmptyCriteria, -} - -var domainUnpackLookup = map[string]func(map[string]interface{}, BaseCriteriaAPIModel) interface{}{ - UserDomain: unpackEmptyCriteria, -} - -var domainSchemaLookup = func(version int, isCustom bool, webhookType string) map[string]map[string]*sdkv2_schema.Schema { - return map[string]map[string]*sdkv2_schema.Schema{ - UserDomain: userWebhookSchema(webhookType, version, isCustom), - } -} - -var unpackCriteria = func(d *utilsdk.ResourceData, webhookType string) interface{} { - var webhookCriteria interface{} - - if v, ok := d.GetOk("criteria"); ok { - criteria := v.(*sdkv2_schema.Set).List() - if len(criteria) == 1 { - id := criteria[0].(map[string]interface{}) - - baseCriteria := BaseCriteriaAPIModel{ - IncludePatterns: utilsdk.CastToStringArr(id["include_patterns"].(*sdkv2_schema.Set).List()), - ExcludePatterns: utilsdk.CastToStringArr(id["exclude_patterns"].(*sdkv2_schema.Set).List()), - } - - webhookCriteria = domainUnpackLookup[webhookType](id, baseCriteria) - } - } - - return webhookCriteria -} - -var packCriteria = func(d *sdkv2_schema.ResourceData, webhookType string, criteria map[string]interface{}) []error { - setValue := utilsdk.MkLens(d) - - resource := domainSchemaLookup(currentSchemaVersion, false, webhookType)[webhookType]["criteria"].Elem.(*sdkv2_schema.Resource) - packedCriteria := domainPackLookup[webhookType](criteria) - - includePatterns := []interface{}{} - if v, ok := criteria["includePatterns"]; ok && v != nil { - includePatterns = v.([]interface{}) - } - packedCriteria["include_patterns"] = sdkv2_schema.NewSet(sdkv2_schema.HashString, includePatterns) - - excludePatterns := []interface{}{} - if v, ok := criteria["excludePatterns"]; ok && v != nil { - excludePatterns = v.([]interface{}) - } - packedCriteria["exclude_patterns"] = sdkv2_schema.NewSet(sdkv2_schema.HashString, excludePatterns) - - return setValue("criteria", sdkv2_schema.NewSet(sdkv2_schema.HashResource(resource), []interface{}{packedCriteria})) -} - -var domainCriteriaValidationLookup = map[string]func(context.Context, map[string]interface{}) error{ - UserDomain: emptyCriteriaValidation, -} - -var emptyCriteriaValidation = func(ctx context.Context, criteria map[string]interface{}) error { - return nil -} - -var packSecret = func(d *sdkv2_schema.ResourceData, url string) string { - // Get secret from TF state - var secret string - if v, ok := d.GetOk("handler"); ok { - handlers := v.(*sdkv2_schema.Set).List() - for _, handler := range handlers { - h := handler.(map[string]interface{}) - // if urls match, assign the secret value from the state - if h["url"].(string) == url { - secret = h["secret"].(string) - } - } - } - - return secret -} - var retryOnProxyError = func(response *resty.Response, _r error) bool { var proxyNotFoundRegex = regexp.MustCompile("proxy with key '.*' not found") return proxyNotFoundRegex.MatchString(string(response.Body()[:])) } - -func ResourceArtifactoryWebhook(webhookType string) *sdkv2_schema.Resource { - - var unpackWebhook = func(data *sdkv2_schema.ResourceData) (WebhookAPIModel, error) { - d := &utilsdk.ResourceData{ResourceData: data} - - var unpackHandlers = func(d *utilsdk.ResourceData) []HandlerAPIModel { - var webhookHandlers []HandlerAPIModel - - if v, ok := d.GetOk("handler"); ok { - handlers := v.(*sdkv2_schema.Set).List() - for _, handler := range handlers { - h := handler.(map[string]interface{}) - // use this to filter out weirdness with terraform adding an extra blank webhook in a set - // https://discuss.hashicorp.com/t/using-typeset-in-provider-always-adds-an-empty-element-on-update/18566/2 - if h["url"].(string) != "" { - webhookHandler := HandlerAPIModel{ - HandlerType: "webhook", - Url: h["url"].(string), - } - - if v, ok := h["secret"]; ok { - if s, ok := v.(string); ok { - webhookHandler.Secret = &s - } - } - - if v, ok := h["use_secret_for_signing"]; ok { - if b, ok := v.(bool); ok { - webhookHandler.UseSecretForSigning = &b - } - } - - if v, ok := h["proxy"]; ok { - if s, ok := v.(string); ok { - webhookHandler.Proxy = &s - } - } - - if v, ok := h["custom_http_headers"]; ok { - webhookHandler.CustomHttpHeaders = unpackKeyValuePair(v.(map[string]interface{})) - } - - webhookHandlers = append(webhookHandlers, webhookHandler) - } - } - } - - return webhookHandlers - } - - webhook := WebhookAPIModel{ - Key: d.GetString("key", false), - Description: d.GetString("description", false), - Enabled: d.GetBool("enabled", false), - EventFilter: EventFilterAPIModel{ - Domain: webhookType, - EventTypes: d.GetSet("event_types"), - Criteria: unpackCriteria(d, webhookType), - }, - Handlers: unpackHandlers(d), - } - - return webhook, nil - } - - var packHandlers = func(d *sdkv2_schema.ResourceData, handlers []HandlerAPIModel) []error { - setValue := utilsdk.MkLens(d) - resource := domainSchemaLookup(currentSchemaVersion, false, webhookType)[webhookType]["handler"].Elem.(*sdkv2_schema.Resource) - var packedHandlers []interface{} - for _, handler := range handlers { - packedHandler := map[string]interface{}{ - "url": handler.Url, - "secret": packSecret(d, handler.Url), - } - - if handler.UseSecretForSigning != nil { - packedHandler["use_secret_for_signing"] = *handler.UseSecretForSigning - } - - if handler.Proxy != nil { - packedHandler["proxy"] = *handler.Proxy - } - - if handler.CustomHttpHeaders != nil { - packedHandler["custom_http_headers"] = packKeyValuePair(handler.CustomHttpHeaders) - } - - packedHandlers = append(packedHandlers, packedHandler) - } - - return setValue("handler", sdkv2_schema.NewSet(sdkv2_schema.HashResource(resource), packedHandlers)) - } - - var packWebhook = func(d *sdkv2_schema.ResourceData, webhook WebhookAPIModel) sdkv2_diag.Diagnostics { - setValue := utilsdk.MkLens(d) - - setValue("key", webhook.Key) - setValue("description", webhook.Description) - setValue("enabled", webhook.Enabled) - errors := setValue("event_types", webhook.EventFilter.EventTypes) - if webhook.EventFilter.Criteria != nil { - errors = append(errors, packCriteria(d, webhookType, webhook.EventFilter.Criteria.(map[string]interface{}))...) - } - errors = append(errors, packHandlers(d, webhook.Handlers)...) - - if len(errors) > 0 { - return sdkv2_diag.Errorf("failed to pack webhook %q", errors) - } - - return nil - } - - var readWebhook = func(ctx context.Context, data *sdkv2_schema.ResourceData, m interface{}) sdkv2_diag.Diagnostics { - webhook := WebhookAPIModel{} - - webhook.EventFilter.Criteria = domainCriteriaLookup[webhookType] - - var artifactoryError artifactory.ArtifactoryErrorsResponse - resp, err := m.(util.ProviderMetadata).Client.R(). - SetPathParam("webhookKey", data.Id()). - SetResult(&webhook). - SetError(&artifactoryError). - Get(WebhookURL) - - if err != nil { - return sdkv2_diag.FromErr(err) - } - - if resp.StatusCode() == http.StatusNotFound { - data.SetId("") - return nil - } - - if resp.IsError() { - return sdkv2_diag.Errorf("%s", artifactoryError.String()) - } - - return packWebhook(data, webhook) - } - - var createWebhook = func(ctx context.Context, data *sdkv2_schema.ResourceData, m interface{}) sdkv2_diag.Diagnostics { - webhook, err := unpackWebhook(data) - if err != nil { - return sdkv2_diag.FromErr(err) - } - - var artifactoryError artifactory.ArtifactoryErrorsResponse - resp, err := m.(util.ProviderMetadata).Client.R(). - SetBody(webhook). - AddRetryCondition(retryOnProxyError). - SetError(&artifactoryError). - Post(webhooksURL) - if err != nil { - return sdkv2_diag.FromErr(err) - } - - if resp.IsError() { - return sdkv2_diag.Errorf("%s", artifactoryError.String()) - } - - data.SetId(webhook.Id()) - - return readWebhook(ctx, data, m) - } - - var updateWebhook = func(ctx context.Context, data *sdkv2_schema.ResourceData, m interface{}) sdkv2_diag.Diagnostics { - webhook, err := unpackWebhook(data) - if err != nil { - return sdkv2_diag.FromErr(err) - } - - var artifactoryError artifactory.ArtifactoryErrorsResponse - resp, err := m.(util.ProviderMetadata).Client.R(). - SetPathParam("webhookKey", data.Id()). - SetBody(webhook). - AddRetryCondition(retryOnProxyError). - SetError(&artifactoryError). - Put(WebhookURL) - if err != nil { - return sdkv2_diag.FromErr(err) - } - - if resp.IsError() { - return sdkv2_diag.Errorf("%s", artifactoryError.String()) - } - - data.SetId(webhook.Id()) - - return readWebhook(ctx, data, m) - } - - var deleteWebhook = func(ctx context.Context, data *sdkv2_schema.ResourceData, m interface{}) sdkv2_diag.Diagnostics { - var artifactoryError artifactory.ArtifactoryErrorsResponse - resp, err := m.(util.ProviderMetadata).Client.R(). - SetPathParam("webhookKey", data.Id()). - SetError(&artifactoryError). - Delete(WebhookURL) - - if err != nil { - return sdkv2_diag.FromErr(err) - } - - if resp.StatusCode() == http.StatusNotFound { - data.SetId("") - return nil - } - - if resp.IsError() { - return sdkv2_diag.Errorf("%s", artifactoryError.String()) - } - - return nil - } - - var eventTypesDiff = func(ctx context.Context, diff *sdkv2_schema.ResourceDiff, v interface{}) error { - eventTypes := diff.Get("event_types").(*sdkv2_schema.Set).List() - if len(eventTypes) == 0 { - return nil - } - - eventTypesSupported := DomainEventTypesSupported[webhookType] - for _, eventType := range eventTypes { - if !slices.Contains(eventTypesSupported, eventType.(string)) { - return fmt.Errorf("event_type %s not supported for domain %s", eventType, webhookType) - } - } - return nil - } - - var criteriaDiff = func(ctx context.Context, diff *sdkv2_schema.ResourceDiff, v interface{}) error { - if resource, ok := diff.GetOk("criteria"); ok { - criteria := resource.(*sdkv2_schema.Set).List() - if len(criteria) == 0 { - return nil - } - return domainCriteriaValidationLookup[webhookType](ctx, criteria[0].(map[string]interface{})) - } - - return nil - } - - // Previous version of the schema - // see example in https://www.terraform.io/plugin/sdkv2/resources/state-migration#terraform-v0-12-sdk-state-migrations - resourceSchemaV1 := &sdkv2_schema.Resource{ - Schema: domainSchemaLookup(1, false, webhookType)[webhookType], - } - - rs := sdkv2_schema.Resource{ - SchemaVersion: 2, - CreateContext: createWebhook, - ReadContext: readWebhook, - UpdateContext: updateWebhook, - DeleteContext: deleteWebhook, - - Importer: &sdkv2_schema.ResourceImporter{ - StateContext: sdkv2_schema.ImportStatePassthroughContext, - }, - - Schema: domainSchemaLookup(currentSchemaVersion, false, webhookType)[webhookType], - StateUpgraders: []sdkv2_schema.StateUpgrader{ - { - Type: resourceSchemaV1.CoreConfigSchema().ImpliedType(), - Upgrade: ResourceStateUpgradeV1, - Version: 1, - }, - }, - - CustomizeDiff: customdiff.All( - eventTypesDiff, - criteriaDiff, - ), - Description: "Provides an Artifactory webhook resource", - } - - if webhookType == "artifactory_release_bundle" { - rs.DeprecationMessage = "This resource is being deprecated and replaced by artifactory_destination_webhook resource" - } - - return &rs -} - -// 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"], - }, - } - - delete(rawState, "url") - delete(rawState, "secret") - delete(rawState, "proxy") - delete(rawState, "custom_http_headers") - - return rawState, nil -} diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_webhook_test.go b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_test.go index 6cfbad528..c4154905a 100644 --- a/pkg/artifactory/resource/webhook/resource_artifactory_webhook_test.go +++ b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_test.go @@ -5,7 +5,6 @@ import ( "fmt" "reflect" "regexp" - "slices" "testing" "github.com/go-resty/resty/v2" @@ -25,13 +24,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 +96,10 @@ var releaseBundleV2Template = ` ` func TestAccWebhook_CriteriaValidation(t *testing.T) { - for _, webhookType := range webhook.DomainSupported { - 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)) + }) } } diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_webhook_user.go b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_user.go index 9f02176b0..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 BaseCriteriaAPIModel) 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"}, }}, }) } From baed11ca1080618863fa01078568b65a1af8406f Mon Sep 17 00:00:00 2001 From: Alex Hung Date: Mon, 30 Sep 2024 10:46:55 -0700 Subject: [PATCH 11/17] Move SDKv2 webhook funcs to custom webhook package --- .../resource_artifactory_custom_webhook.go | 370 +++++++++++++++++- .../webhook/resource_artifactory_webhook.go | 2 - 2 files changed, 365 insertions(+), 7 deletions(-) diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook.go b/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook.go index a8d00acdd..53bbc074f 100644 --- a/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook.go +++ b/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook.go @@ -20,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": { @@ -106,6 +121,149 @@ 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"` @@ -442,14 +600,158 @@ var packKeyValuePair = func(keyValuePairs []KeyValuePairAPIModel) map[string]int return kvPairs } -var domainCriteriaLookup = map[string]interface{}{} +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{}{} +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{}{} +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{} + 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{} { @@ -494,7 +796,65 @@ var packCriteria = func(d *schema.ResourceData, webhookType string, criteria map } var domainCriteriaValidationLookup = map[string]func(context.Context, map[string]interface{}) error{ - UserDomain: emptyCriteriaValidation, + "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 { diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_webhook.go b/pkg/artifactory/resource/webhook/resource_artifactory_webhook.go index 1ed988f1d..36caea9a1 100644 --- a/pkg/artifactory/resource/webhook/resource_artifactory_webhook.go +++ b/pkg/artifactory/resource/webhook/resource_artifactory_webhook.go @@ -49,8 +49,6 @@ const ( const currentSchemaVersion = 2 -var DomainSupported = []string{} - var DomainEventTypesSupported = map[string][]string{ ArtifactDomain: {"deployed", "deleted", "moved", "copied", "cached"}, ArtifactPropertyDomain: {"added", "deleted"}, From ab898a9193e28cb73372d599c6efc832d0c6a0e7 Mon Sep 17 00:00:00 2001 From: Alex Hung Date: Mon, 30 Sep 2024 10:47:31 -0700 Subject: [PATCH 12/17] Fix incorrect migration test for remote generic repo --- .../resource_artifactory_remote_generic_repository.go | 11 +++++------ .../resource_artifactory_remote_repository_test.go | 9 ++++++--- 2 files changed, 11 insertions(+), 9 deletions(-) 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(), + }, + }, }, }, }) From 6bcfc598144bd62e4dce03cc03be965b431795bd Mon Sep 17 00:00:00 2001 From: Alex Hung Date: Mon, 30 Sep 2024 11:55:17 -0700 Subject: [PATCH 13/17] Remove unused funcs --- .../resource_artifactory_custom_webhook.go | 300 ------------------ .../resource_artifactory_webhook_test.go | 38 --- 2 files changed, 338 deletions(-) diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook.go b/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook.go index 53bbc074f..80528e83a 100644 --- a/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook.go +++ b/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook.go @@ -878,303 +878,3 @@ var packSecret = func(d *schema.ResourceData, url string) string { return secret } -func ResourceArtifactoryWebhook(webhookType string) *schema.Resource { - - var unpackWebhook = func(data *schema.ResourceData) (WebhookAPIModel, error) { - d := &utilsdk.ResourceData{ResourceData: data} - - var unpackHandlers = func(d *utilsdk.ResourceData) []HandlerAPIModel { - var webhookHandlers []HandlerAPIModel - - 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 := HandlerAPIModel{ - HandlerType: "webhook", - Url: h["url"].(string), - } - - if v, ok := h["secret"]; ok { - if s, ok := v.(string); ok { - webhookHandler.Secret = &s - } - } - - if v, ok := h["use_secret_for_signing"]; ok { - if b, ok := v.(bool); ok { - webhookHandler.UseSecretForSigning = &b - } - } - - if v, ok := h["proxy"]; ok { - if s, ok := v.(string); ok { - webhookHandler.Proxy = &s - } - } - - if v, ok := h["custom_http_headers"]; ok { - webhookHandler.CustomHttpHeaders = unpackKeyValuePair(v.(map[string]interface{})) - } - - webhookHandlers = append(webhookHandlers, webhookHandler) - } - } - } - - return webhookHandlers - } - - webhook := WebhookAPIModel{ - Key: d.GetString("key", false), - Description: d.GetString("description", false), - Enabled: d.GetBool("enabled", false), - EventFilter: EventFilterAPIModel{ - Domain: webhookType, - EventTypes: d.GetSet("event_types"), - Criteria: unpackCriteria(d, webhookType), - }, - Handlers: unpackHandlers(d), - } - - return webhook, nil - } - - var packHandlers = func(d *schema.ResourceData, handlers []HandlerAPIModel) []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), - } - - if handler.UseSecretForSigning != nil { - packedHandler["use_secret_for_signing"] = *handler.UseSecretForSigning - } - - if handler.Proxy != nil { - packedHandler["proxy"] = *handler.Proxy - } - - if handler.CustomHttpHeaders != nil { - packedHandler["custom_http_headers"] = packKeyValuePair(handler.CustomHttpHeaders) - } - - packedHandlers = append(packedHandlers, packedHandler) - } - - return setValue("handler", schema.NewSet(schema.HashResource(resource), packedHandlers)) - } - - var packWebhook = func(d *schema.ResourceData, webhook WebhookAPIModel) diag.Diagnostics { - setValue := utilsdk.MkLens(d) - - setValue("key", webhook.Key) - setValue("description", webhook.Description) - setValue("enabled", webhook.Enabled) - errors := setValue("event_types", webhook.EventFilter.EventTypes) - if webhook.EventFilter.Criteria != nil { - errors = append(errors, packCriteria(d, webhookType, webhook.EventFilter.Criteria.(map[string]interface{}))...) - } - errors = append(errors, packHandlers(d, webhook.Handlers)...) - - if len(errors) > 0 { - return diag.Errorf("failed to pack webhook %q", errors) - } - - return nil - } - - var readWebhook = func(ctx context.Context, data *schema.ResourceData, m interface{}) diag.Diagnostics { - webhook := WebhookAPIModel{} - - webhook.EventFilter.Criteria = domainCriteriaLookup[webhookType] - - var artifactoryError artifactory.ArtifactoryErrorsResponse - resp, err := m.(util.ProviderMetadata).Client.R(). - SetPathParam("webhookKey", data.Id()). - SetResult(&webhook). - SetError(&artifactoryError). - Get(WebhookURL) - - if err != nil { - return 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 createWebhook = func(ctx context.Context, data *schema.ResourceData, m interface{}) diag.Diagnostics { - webhook, err := unpackWebhook(data) - if err != nil { - return diag.FromErr(err) - } - - var artifactoryError artifactory.ArtifactoryErrorsResponse - resp, err := m.(util.ProviderMetadata).Client.R(). - SetBody(webhook). - AddRetryCondition(retryOnProxyError). - SetError(&artifactoryError). - Post(webhooksURL) - if err != nil { - return diag.FromErr(err) - } - - if resp.IsError() { - return diag.Errorf("%s", artifactoryError.String()) - } - - data.SetId(webhook.Id()) - - return readWebhook(ctx, data, m) - } - - var updateWebhook = func(ctx context.Context, data *schema.ResourceData, m interface{}) diag.Diagnostics { - webhook, err := unpackWebhook(data) - if err != nil { - return diag.FromErr(err) - } - - var artifactoryError artifactory.ArtifactoryErrorsResponse - resp, err := m.(util.ProviderMetadata).Client.R(). - SetPathParam("webhookKey", data.Id()). - SetBody(webhook). - AddRetryCondition(retryOnProxyError). - SetError(&artifactoryError). - Put(WebhookURL) - if err != nil { - return diag.FromErr(err) - } - - if resp.IsError() { - return diag.Errorf("%s", artifactoryError.String()) - } - - data.SetId(webhook.Id()) - - return readWebhook(ctx, data, m) - } - - var deleteWebhook = func(ctx context.Context, data *schema.ResourceData, m interface{}) diag.Diagnostics { - var artifactoryError artifactory.ArtifactoryErrorsResponse - resp, err := m.(util.ProviderMetadata).Client.R(). - SetPathParam("webhookKey", data.Id()). - SetError(&artifactoryError). - Delete(WebhookURL) - - if err != nil { - return diag.FromErr(err) - } - - if resp.StatusCode() == http.StatusNotFound { - data.SetId("") - return nil - } - - if resp.IsError() { - return diag.Errorf("%s", artifactoryError.String()) - } - - return nil - } - - var eventTypesDiff = func(ctx context.Context, diff *schema.ResourceDiff, v interface{}) error { - eventTypes := diff.Get("event_types").(*schema.Set).List() - if len(eventTypes) == 0 { - return nil - } - - eventTypesSupported := DomainEventTypesSupported[webhookType] - for _, eventType := range eventTypes { - if !slices.Contains(eventTypesSupported, eventType.(string)) { - return fmt.Errorf("event_type %s not supported for domain %s", eventType, webhookType) - } - } - return nil - } - - var criteriaDiff = func(ctx context.Context, diff *schema.ResourceDiff, v interface{}) error { - if resource, ok := diff.GetOk("criteria"); ok { - criteria := resource.(*schema.Set).List() - if len(criteria) == 0 { - return nil - } - return domainCriteriaValidationLookup[webhookType](ctx, criteria[0].(map[string]interface{})) - } - - return nil - } - - // 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], - } - - rs := schema.Resource{ - SchemaVersion: 2, - CreateContext: createWebhook, - ReadContext: readWebhook, - UpdateContext: updateWebhook, - DeleteContext: deleteWebhook, - - Importer: &schema.ResourceImporter{ - StateContext: schema.ImportStatePassthroughContext, - }, - - Schema: domainSchemaLookup(currentSchemaVersion, false, webhookType)[webhookType], - StateUpgraders: []schema.StateUpgrader{ - { - Type: resourceSchemaV1.CoreConfigSchema().ImpliedType(), - Upgrade: ResourceStateUpgradeV1, - Version: 1, - }, - }, - - CustomizeDiff: customdiff.All( - eventTypesDiff, - criteriaDiff, - ), - Description: "Provides an Artifactory webhook resource", - } - - if webhookType == "artifactory_release_bundle" { - rs.DeprecationMessage = "This resource is being deprecated and replaced by artifactory_destination_webhook resource" - } - - return &rs -} - -// 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"], - }, - } - - delete(rawState, "url") - delete(rawState, "secret") - delete(rawState, "proxy") - delete(rawState, "custom_http_headers") - - return rawState, nil -} diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_webhook_test.go b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_test.go index c4154905a..9e006741f 100644 --- a/pkg/artifactory/resource/webhook/resource_artifactory_webhook_test.go +++ b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_test.go @@ -1,9 +1,7 @@ package webhook_test import ( - "context" "fmt" - "reflect" "regexp" "testing" @@ -657,39 +655,3 @@ func TestAccWebhook_GH476WebHookChangeBearerSet0(t *testing.T) { }, }) } - -// 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) - } -} From 10e0313926cb5e6d3e249c2351769da202018b19 Mon Sep 17 00:00:00 2001 From: Alex Hung Date: Mon, 30 Sep 2024 11:55:33 -0700 Subject: [PATCH 14/17] Add webhook type check for deprecated resource --- .../resource/webhook/resource_artifactory_custom_webhook.go | 2 +- .../webhook/resource_artifactory_webhook_release_bundle.go | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook.go b/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook.go index 80528e83a..bd995e860 100644 --- a/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook.go +++ b/pkg/artifactory/resource/webhook/resource_artifactory_custom_webhook.go @@ -571,7 +571,7 @@ 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" } 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 a1933dc14..df4d6c38d 100644 --- a/pkg/artifactory/resource/webhook/resource_artifactory_webhook_release_bundle.go +++ b/pkg/artifactory/resource/webhook/resource_artifactory_webhook_release_bundle.go @@ -98,6 +98,9 @@ func (r *ReleaseBundleWebhookResource) Schema(ctx context.Context, req resource. } 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) { From b951124d164a6cc56b585aae2d2300ff82eb1ced Mon Sep 17 00:00:00 2001 From: Alex Hung Date: Mon, 30 Sep 2024 11:57:02 -0700 Subject: [PATCH 15/17] Update CHANGELOG --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0933a49f5..3e7398ef7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## 12.1.1 (October 1, 2024) + +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: From 20379daa51c2cebac137e609b0b1a8ac7360755d Mon Sep 17 00:00:00 2001 From: JFrog CI Date: Mon, 30 Sep 2024 21:01:04 +0000 Subject: [PATCH 16/17] JFrog Pipelines - Add Artifactory version to CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e7398ef7..95ec2b608 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -## 12.1.1 (October 1, 2024) +## 12.1.1 (October 1, 2024). Tested on Artifactory 7.90.13 with Terraform 1.9.6 and OpenTofu 1.8.2 IMPROVEMENTS: From 8072c7da1742f85739054cb98e3f87a7aa752013 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 26 Sep 2024 21:31:18 +0000 Subject: [PATCH 17/17] Bump github.com/go-resty/resty/v2 from 2.15.2 to 2.15.3 Bumps [github.com/go-resty/resty/v2](https://github.com/go-resty/resty) from 2.15.2 to 2.15.3. - [Release notes](https://github.com/go-resty/resty/releases) - [Commits](https://github.com/go-resty/resty/compare/v2.15.2...v2.15.3) --- updated-dependencies: - dependency-name: github.com/go-resty/resty/v2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) 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=