From 00a350453fd1fd5bd0063ae1969fd917107df79f Mon Sep 17 00:00:00 2001 From: Alex Hung Date: Fri, 21 Jun 2024 09:26:03 -0700 Subject: [PATCH 1/5] Migrate xray_watch resource to Plugin Framework --- pkg/xray/provider/framework.go | 1 + pkg/xray/provider/sdkv2.go | 1 - pkg/xray/resource/resource_xray_watch.go | 1115 ++++++++++++++--- pkg/xray/resource/resource_xray_watch_test.go | 253 ++-- pkg/xray/resource/watches.go | 555 -------- 5 files changed, 1105 insertions(+), 820 deletions(-) delete mode 100644 pkg/xray/resource/watches.go diff --git a/pkg/xray/provider/framework.go b/pkg/xray/provider/framework.go index e47bf866..973bca18 100644 --- a/pkg/xray/provider/framework.go +++ b/pkg/xray/provider/framework.go @@ -189,6 +189,7 @@ func (p *XrayProvider) Resources(ctx context.Context) []func() resource.Resource xray_resource.NewCustomIssueResource, xray_resource.NewIgnoreRuleResource, xray_resource.NewSettingsResource, + xray_resource.NewWatchResource, xray_resource.NewWebhookResource, xray_resource.NewWorkersCountResource, } diff --git a/pkg/xray/provider/sdkv2.go b/pkg/xray/provider/sdkv2.go index 136e0c8a..5b08cff0 100644 --- a/pkg/xray/provider/sdkv2.go +++ b/pkg/xray/provider/sdkv2.go @@ -58,7 +58,6 @@ func SdkV2() *schema.Provider { "xray_security_policy": xray.ResourceXraySecurityPolicyV2(), "xray_license_policy": xray.ResourceXrayLicensePolicyV2(), "xray_operational_risk_policy": xray.ResourceXrayOperationalRiskPolicy(), - "xray_watch": xray.ResourceXrayWatch(), "xray_repository_config": xray.ResourceXrayRepositoryConfig(), "xray_vulnerabilities_report": xray.ResourceXrayVulnerabilitiesReport(), "xray_licenses_report": xray.ResourceXrayLicensesReport(), diff --git a/pkg/xray/resource/resource_xray_watch.go b/pkg/xray/resource/resource_xray_watch.go index 0bcdba76..a62c7dde 100644 --- a/pkg/xray/resource/resource_xray_watch.go +++ b/pkg/xray/resource/resource_xray_watch.go @@ -1,12 +1,35 @@ package xray import ( + "context" + "encoding/json" "fmt" + "net/http" + "strconv" "strings" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - "github.com/jfrog/terraform-provider-shared/util/sdk" - "github.com/jfrog/terraform-provider-shared/validator" + "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/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/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" + "golang.org/x/exp/slices" +) + +const ( + WatchesEndpoint = "xray/api/v2/watches" + WatchEndpoint = "xray/api/v2/watches/{name}" ) var supportedResourceTypes = []string{ @@ -22,206 +45,958 @@ var supportedResourceTypes = []string{ "all-releaseBundlesV2", } -func ResourceXrayWatch() *schema.Resource { - return &schema.Resource{ - CreateContext: resourceXrayWatchCreate, - ReadContext: resourceXrayWatchRead, - UpdateContext: resourceXrayWatchUpdate, - DeleteContext: resourceXrayWatchDelete, - Description: "Provides an Xray watch resource.", +var _ resource.Resource = &WatchResource{} + +func NewWatchResource() resource.Resource { + return &WatchResource{} +} + +type WatchResource struct { + ProviderData util.ProviderMetadata + TypeName string +} + +func (r *WatchResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_watch" + r.TypeName = resp.TypeName +} + +type WatchResourceModel struct { + Name types.String `tfsdk:"name"` + ProjectKey types.String `tfsdk:"project_key"` + Description types.String `tfsdk:"description"` + Active types.Bool `tfsdk:"active"` + WatchResource types.Set `tfsdk:"watch_resource"` + AssignedPolicies types.Set `tfsdk:"assigned_policy"` + WatchRecipients types.Set `tfsdk:"watch_recipients"` +} + +func unpackAntFilter(ctx context.Context, filterType string, ds *diag.Diagnostics) func(elem attr.Value, _ int) WatchFilterAPIModel { + return func(elem attr.Value, _ int) WatchFilterAPIModel { + attrs := elem.(types.Object).Attributes() + + var includePatterns []string + ds.Append(attrs["include_patterns"].(types.List).ElementsAs(ctx, &includePatterns, false)...) + + var excludePatterns []string + ds.Append(attrs["exclude_patterns"].(types.List).ElementsAs(ctx, &excludePatterns, false)...) + + filterValue, err := json.Marshal( + WatchFilterAntValueAPIModel{ + IncludePatterns: includePatterns, + ExcludePatterns: excludePatterns, + }, + ) + if err != nil { + ds.AddError( + "failed to marshal ant filter", + err.Error(), + ) + } + + return WatchFilterAPIModel{ + Type: filterType, + Value: json.RawMessage(filterValue), + } + } +} + +func unpackKVFilter(_ context.Context, _ *diag.Diagnostics) func(elem attr.Value, _ int) WatchFilterAPIModel { + return func(elem attr.Value, _ int) WatchFilterAPIModel { + attrs := elem.(types.Object).Attributes() + + filterValue := fmt.Sprintf( + `{"key": "%s", "value": "%s"}`, + attrs["key"].(types.String).ValueString(), + attrs["value"].(types.String).ValueString(), + ) + + return WatchFilterAPIModel{ + Type: attrs["type"].(types.String).ValueString(), + Value: json.RawMessage(filterValue), + } + } +} + +func (m WatchResourceModel) toAPIModel(ctx context.Context, apiModel *WatchAPIModel) (ds diag.Diagnostics) { + projectResources := lo.Map( + m.WatchResource.Elements(), + func(elem attr.Value, _ int) WatchProjectResourceAPIModel { + attrs := elem.(types.Object).Attributes() + + var filters []WatchFilterAPIModel + + fs := lo.Map( + attrs["filter"].(types.Set).Elements(), + func(elem attr.Value, _ int) WatchFilterAPIModel { + attrs := elem.(types.Object).Attributes() + return WatchFilterAPIModel{ + Type: attrs["type"].(types.String).ValueString(), + Value: json.RawMessage(strconv.Quote(attrs["value"].(types.String).ValueString())), + } + }, + ) + filters = append(filters, fs...) + + antFilters := lo.Map( + attrs["ant_filter"].(types.Set).Elements(), + unpackAntFilter(ctx, "ant-patterns", &ds), + ) + filters = append(filters, antFilters...) + + pathAntFilters := lo.Map( + attrs["path_ant_filter"].(types.Set).Elements(), + unpackAntFilter(ctx, "path-ant-patterns", &ds), + ) + filters = append(filters, pathAntFilters...) + + kvFilters := lo.Map( + attrs["kv_filter"].(types.Set).Elements(), + unpackKVFilter(ctx, &ds), + ) + filters = append(filters, kvFilters...) + + return WatchProjectResourceAPIModel{ + Type: attrs["type"].(types.String).ValueString(), + BinaryManagerId: attrs["bin_mgr_id"].(types.String).ValueString(), + Name: attrs["name"].(types.String).ValueString(), + RepoType: attrs["repo_type"].(types.String).ValueString(), + Filters: filters, + } + }, + ) + + assignedPolicies := lo.Map( + m.AssignedPolicies.Elements(), + func(elem attr.Value, _ int) WatchAssignedPolicyAPIModel { + attrs := elem.(types.Object).Attributes() + return WatchAssignedPolicyAPIModel{ + Name: attrs["name"].(types.String).ValueString(), + Type: attrs["type"].(types.String).ValueString(), + } + }, + ) + + var recipients []string + ds.Append(m.WatchRecipients.ElementsAs(ctx, &recipients, false)...) + + *apiModel = WatchAPIModel{ + GeneralData: WatchGeneralDataAPIModel{ + Name: m.Name.ValueString(), + Description: m.Description.ValueString(), + Active: m.Active.ValueBool(), + }, + ProjectResources: WatchProjectResourcesAPIModel{ + Resources: projectResources, + }, + AssignedPolicies: assignedPolicies, + WatchRecipients: recipients, + } + + return +} + +var filterResourceModelAttributeTypes = map[string]attr.Type{ + "type": types.StringType, + "value": types.StringType, +} + +var filterObjectResourceModelAttributeTypes types.ObjectType = types.ObjectType{ + AttrTypes: filterResourceModelAttributeTypes, +} + +var antFilterResourceModelAttributeTypes = map[string]attr.Type{ + "include_patterns": types.ListType{ElemType: types.StringType}, + "exclude_patterns": types.ListType{ElemType: types.StringType}, +} + +var antFilterObjectResourceModelAttributeTypes types.ObjectType = types.ObjectType{ + AttrTypes: antFilterResourceModelAttributeTypes, +} + +var kvFilterResourceModelAttributeTypes = map[string]attr.Type{ + "type": types.StringType, + "key": types.StringType, + "value": types.StringType, +} + +var kvFilterObjectResourceModelAttributeTypes types.ObjectType = types.ObjectType{ + AttrTypes: kvFilterResourceModelAttributeTypes, +} + +var watchResourceResourceModelAttributeTypes = map[string]attr.Type{ + "type": types.StringType, + "name": types.StringType, + "bin_mgr_id": types.StringType, + "repo_type": types.StringType, + "filter": types.SetType{ + ElemType: filterObjectResourceModelAttributeTypes, + }, + "ant_filter": types.SetType{ + ElemType: antFilterObjectResourceModelAttributeTypes, + }, + "path_ant_filter": types.SetType{ + ElemType: antFilterObjectResourceModelAttributeTypes, + }, + "kv_filter": types.SetType{ + ElemType: kvFilterObjectResourceModelAttributeTypes, + }, +} + +var watchResourceObjectResourceModelAttributeTypes types.ObjectType = types.ObjectType{ + AttrTypes: watchResourceResourceModelAttributeTypes, +} + +var assignedPolicyResourceModelAttributeTypes = map[string]attr.Type{ + "name": types.StringType, + "type": types.StringType, +} + +var assignedPolicyObjectResourceModelAttributeTypes types.ObjectType = types.ObjectType{ + AttrTypes: assignedPolicyResourceModelAttributeTypes, +} + +// type PackFilterFunc func(ctx context.Context, filter WatchFilterAPIModel) (attr.Value, diag.Diagnostics) + +func packStringFilter(ctx context.Context, filter WatchFilterAPIModel) (attr.Value, diag.Diagnostics) { + var value string + err := json.Unmarshal(filter.Value, &value) + if err != nil { + return nil, diag.Diagnostics{ + diag.NewErrorDiagnostic("failed to pack KV filter", err.Error()), + } + } + + return types.ObjectValue( + filterResourceModelAttributeTypes, + map[string]attr.Value{ + "type": types.StringValue(filter.Type), + "value": types.StringValue(value), + }, + ) +} + +func packAntFilter(ctx context.Context, filter WatchFilterAPIModel) (attr.Value, diag.Diagnostics) { + var value WatchFilterAntValueAPIModel + err := json.Unmarshal(filter.Value, &value) + if err != nil { + return nil, diag.Diagnostics{ + diag.NewErrorDiagnostic("failed to pack Ant filter", err.Error()), + } + } + + diags := diag.Diagnostics{} + excludedPatterns := types.ListNull(types.StringType) + if len(value.ExcludePatterns) > 0 { + ps, d := types.ListValueFrom(ctx, types.StringType, value.ExcludePatterns) + if d != nil { + diags.Append(d...) + } + excludedPatterns = ps + } + + includedPatterns := types.ListNull(types.StringType) + if len(value.IncludePatterns) > 0 { + ps, d := types.ListValueFrom(ctx, types.StringType, value.IncludePatterns) + if d != nil { + diags.Append(d...) + } + includedPatterns = ps + } - Importer: &schema.ResourceImporter{ - StateContext: resourceImporterForProjectKey, + antFilter, d := types.ObjectValue( + antFilterResourceModelAttributeTypes, + map[string]attr.Value{ + "exclude_patterns": excludedPatterns, + "include_patterns": includedPatterns, }, + ) + if d != nil { + diags.Append(d...) + } + + return antFilter, diags +} - CustomizeDiff: watchResourceDiff, - - Schema: sdk.MergeMaps( - getProjectKeySchema(false, "Support repository and build watch resource types. When specifying individual repository or build they must be already assigned to the project. Build must be added as indexed resources."), - map[string]*schema.Schema{ - "name": { - Type: schema.TypeString, - Required: true, - ForceNew: true, - Description: "Name of the watch (must be unique)", - ValidateDiagFunc: validator.StringIsNotEmpty, +func packKvFilter(ctx context.Context, filter WatchFilterAPIModel) (attr.Value, diag.Diagnostics) { + var kvValue WatchFilterKvValueAPIModel + err := json.Unmarshal(filter.Value, &kvValue) + if err != nil { + return nil, diag.Diagnostics{ + diag.NewErrorDiagnostic("failed to pack KV filter", err.Error()), + } + } + + return types.ObjectValue( + kvFilterResourceModelAttributeTypes, + map[string]attr.Value{ + "type": types.StringValue(filter.Type), + "key": types.StringValue(kvValue.Key), + "value": types.StringValue(kvValue.Value), + }, + ) +} + +var packFilterMap = map[string]map[string]interface{}{ + "regex": { + "func": packStringFilter, + "attributeName": "filter", + }, + "path-regex": { + "func": packStringFilter, + "attributeName": "filter", + }, + "package-type": { + "func": packStringFilter, + "attributeName": "filter", + }, + "mime-type": { + "func": packStringFilter, + "attributeName": "filter", + }, + "ant-patterns": { + "func": packAntFilter, + "attributeName": "ant_filter", + }, + "path-ant-patterns": { + "func": packAntFilter, + "attributeName": "path_ant_filter", + }, + "property": { + "func": packKvFilter, + "attributeName": "kv_filter", + }, +} + +var allTypes = []string{"all-repos", "all-builds", "all-projects"} + +func (m *WatchResourceModel) fromAPIModel(ctx context.Context, apiModel WatchAPIModel) diag.Diagnostics { + diags := diag.Diagnostics{} + + m.Name = types.StringValue(apiModel.GeneralData.Name) + m.Description = types.StringValue(apiModel.GeneralData.Description) + m.Active = types.BoolValue(apiModel.GeneralData.Active) + + watchResources := lo.Map( + apiModel.ProjectResources.Resources, + func(property WatchProjectResourceAPIModel, _ int) attr.Value { + resources := make(map[string][]attr.Value) + + for _, filter := range property.Filters { + packFilterAttribute, ok := packFilterMap[filter.Type] + if !ok { + diags.AddError( + "invalid filter.Type", + filter.Type, + ) + } + + packedFilter, d := packFilterAttribute["func"].(func(ctx context.Context, filter WatchFilterAPIModel) (attr.Value, diag.Diagnostics))(ctx, filter) + if d != nil && d.HasError() { + diags.Append(d...) + } else { + attributeName := packFilterAttribute["attributeName"].(string) + resources[attributeName] = append(resources[attributeName], packedFilter) + } + } + + name := types.StringNull() + if len(property.Name) > 0 && !slices.Contains(allTypes, property.Type) { + name = types.StringValue(property.Name) + } + + repoType := types.StringNull() + if len(property.RepoType) > 0 { + repoType = types.StringValue(property.RepoType) + } + + watchResource, ds := types.ObjectValue( + watchResourceResourceModelAttributeTypes, + map[string]attr.Value{ + "type": types.StringValue(property.Type), + "name": name, + "bin_mgr_id": types.StringValue(property.BinaryManagerId), + "repo_type": repoType, + "filter": types.SetValueMust(filterObjectResourceModelAttributeTypes, resources["filter"]), + "ant_filter": types.SetValueMust(antFilterObjectResourceModelAttributeTypes, resources["ant_filter"]), + "path_ant_filter": types.SetValueMust(antFilterObjectResourceModelAttributeTypes, resources["path_ant_filter"]), + "kv_filter": types.SetValueMust(kvFilterObjectResourceModelAttributeTypes, resources["kv_filter"]), }, - "description": { - Type: schema.TypeString, - Optional: true, - Description: "Description of the watch", + ) + + if ds != nil { + diags.Append(ds...) + } + + return watchResource + }, + ) + watchResourceSet, d := types.SetValue( + watchResourceObjectResourceModelAttributeTypes, + watchResources, + ) + if d != nil { + diags.Append(d...) + } + m.WatchResource = watchResourceSet + + assignedPolicies := lo.Map( + apiModel.AssignedPolicies, + func(property WatchAssignedPolicyAPIModel, _ int) attr.Value { + assignedPolicy, ds := types.ObjectValue( + assignedPolicyResourceModelAttributeTypes, + map[string]attr.Value{ + "name": types.StringValue(property.Name), + "type": types.StringValue(property.Type), }, - "active": { - Type: schema.TypeBool, - Optional: true, - Description: "Whether or not the watch is active", + ) + + if ds != nil { + diags.Append(ds...) + } + + return assignedPolicy + }, + ) + assignedPoliciesSet, d := types.SetValue( + assignedPolicyObjectResourceModelAttributeTypes, + assignedPolicies, + ) + if d != nil { + diags.Append(d...) + } + m.AssignedPolicies = assignedPoliciesSet + + watchRecipients, d := types.SetValueFrom(ctx, types.StringType, apiModel.WatchRecipients) + if d != nil { + diags.Append(d...) + } + m.WatchRecipients = watchRecipients + + return diags +} + +type WatchGeneralDataAPIModel struct { + Name string `json:"name"` + Description string `json:"description"` + Active bool `json:"active"` +} + +type WatchFilterAPIModel struct { + Type string `json:"type"` + Value json.RawMessage `json:"value"` +} + +type WatchFilterAntValueAPIModel struct { + ExcludePatterns []string `json:"ExcludePatterns,omitempty"` + IncludePatterns []string `json:"IncludePatterns,omitempty"` +} + +type WatchFilterKvValueAPIModel struct { + Key string `json:"key"` + Value string `json:"value"` +} + +type WatchProjectResourceAPIModel struct { + Type string `json:"type"` + BinaryManagerId string `json:"bin_mgr_id"` + Filters []WatchFilterAPIModel `json:"filters,omitempty"` + Name string `json:"name,omitempty"` + BuildRepo string `json:"build_repo,omitempty"` + RepoType string `json:"repo_type,omitempty"` +} + +type WatchProjectResourcesAPIModel struct { + Resources []WatchProjectResourceAPIModel `json:"resources"` +} + +type WatchAssignedPolicyAPIModel struct { + Name string `json:"name"` + Type string `json:"type"` +} + +type WatchAPIModel struct { + GeneralData WatchGeneralDataAPIModel `json:"general_data"` + ProjectResources WatchProjectResourcesAPIModel `json:"project_resources"` + AssignedPolicies []WatchAssignedPolicyAPIModel `json:"assigned_policies"` + WatchRecipients []string `json:"watch_recipients"` +} + +func (r *WatchResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "name": schema.StringAttribute{ + Required: true, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), }, - "watch_resource": { - Type: schema.TypeSet, - Required: true, - Description: "Nested argument describing the resources to be watched. Defined below.", - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "type": { - Type: schema.TypeString, - Required: true, - Description: fmt.Sprintf("Type of resource to be watched. Options: %s.", strings.Join(supportedResourceTypes, ", ")), - ValidateDiagFunc: validator.StringInSlice(true, supportedResourceTypes...), - }, - "bin_mgr_id": { - Type: schema.TypeString, - Optional: true, - Default: "default", - Description: "The ID number of a binary manager resource. Default value is `default`. To check the list of available binary managers, use the API call `${JFROG_URL}/xray/api/v1/binMgr` as an admin user, use `binMgrId` value. More info [here](https://www.jfrog.com/confluence/display/JFROG/Xray+REST+API#XrayRESTAPI-GetBinaryManager)", - }, - "name": { - Type: schema.TypeString, - Optional: true, - Description: "The name of the build, repository, project, or release bundle. Xray indexing must be enabled on the repository, build, or release bundle.", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Description: "Name of the watch", + }, + "project_key": schema.StringAttribute{ + Optional: true, + Validators: []validator.String{ + validatorfw_string.ProjectKey(), + }, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Description: "Project key for assigning this resource to. Must be 2 - 10 lowercase alphanumeric and hyphen characters. Support repository and build watch resource types. When specifying individual repository or build they must be already assigned to the project. Build must be added as indexed resources.", + }, + "description": schema.StringAttribute{ + Optional: true, + Description: "Description of the watch", + }, + "active": schema.BoolAttribute{ + Optional: true, + Description: "Whether or not the watch is active", + }, + "watch_recipients": schema.SetAttribute{ + ElementType: types.StringType, + Optional: true, + Validators: []validator.Set{ + setvalidator.ValueStringsAre( + validatorfw_string.IsEmail(), + ), + }, + Description: "A list of email addressed that will get emailed when a violation is triggered.", + }, + }, + Blocks: map[string]schema.Block{ + "watch_resource": schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "type": schema.StringAttribute{ + Required: true, + Validators: []validator.String{ + stringvalidator.OneOfCaseInsensitive(supportedResourceTypes...), }, - "repo_type": { - Type: schema.TypeString, - Optional: true, - ValidateDiagFunc: validator.StringInSlice(true, "local", "remote"), - Description: "Type of repository. Only applicable when `type` is `repository`. Options: `local` or `remote`.", + Description: fmt.Sprintf("Type of resource to be watched. Options: %s.", strings.Join(supportedResourceTypes, ", ")), + }, + "bin_mgr_id": schema.StringAttribute{ + Optional: true, + Computed: true, + Default: stringdefault.StaticString("default"), + Description: "The ID number of a binary manager resource. Default value is `default`. To check the list of available binary managers, use the API call `${JFROG_URL}/xray/api/v1/binMgr` as an admin user, use `binMgrId` value. More info [here](https://www.jfrog.com/confluence/display/JFROG/Xray+REST+API#XrayRESTAPI-GetBinaryManager)", + }, + "name": schema.StringAttribute{ + Optional: true, + Description: "The name of the build, repository, project, or release bundle. Xray indexing must be enabled on the repository, build, or release bundle.", + }, + "repo_type": schema.StringAttribute{ + Optional: true, + Validators: []validator.String{ + stringvalidator.OneOfCaseInsensitive("local", "remote"), }, - "filter": { - Type: schema.TypeSet, - Optional: true, - MinItems: 1, - Description: "Filter for `regex`, `package-type` and `mime-type` type. Works for `repository` and `all-repos` watch_resource.type", - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "type": { - Type: schema.TypeString, - Required: true, - Description: "The type of filter, such as `regex`, `path-regex`, `package-type`, or `mime-type`", - ValidateDiagFunc: validator.StringInSlice(true, "regex", "path-regex", "package-type", "mime-type"), + Description: "Type of repository. Only applicable when `type` is `repository`. Options: `local` or `remote`.", + }, + }, + Blocks: map[string]schema.Block{ + "filter": schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "type": schema.StringAttribute{ + Required: true, + Validators: []validator.String{ + stringvalidator.OneOfCaseInsensitive("regex", "path-regex", "package-type", "mime-type"), }, - "value": { - Type: schema.TypeString, - Required: true, - Description: "The value of the filter, such as the text of the regex, name of the package type, or mime type.", - ValidateDiagFunc: validator.StringIsNotEmpty, + Description: "The type of filter, such as `regex`, `path-regex`, `package-type`, or `mime-type`", + }, + "value": schema.StringAttribute{ + Required: true, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), }, + Description: "The value of the filter, such as the text of the regex, name of the package type, or mime type.", }, }, }, - "ant_filter": { - Type: schema.TypeSet, - Optional: true, - MinItems: 1, - Description: "`ant-patterns` filter for `all-builds` and `all-projects` watch_resource.type", - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "include_patterns": { - Type: schema.TypeList, - Elem: &schema.Schema{ - Type: schema.TypeString, - }, - Optional: true, - Description: "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 included in this watch. Projects are supported too. Ant-style path expressions are supported (*, **, ?). For example, an 'apache/**' pattern will include the 'apache' build info in the watch.", - }, - "exclude_patterns": { - Type: schema.TypeList, - Elem: &schema.Schema{ - Type: schema.TypeString, - }, - Optional: true, - Description: "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 in this watch. Projects are supported too. Ant-style path expressions are supported (*, **, ?). For example, an 'apache/**' pattern will exclude the 'apache' build info in the watch.", - }, + Validators: []validator.Set{ + setvalidator.SizeAtLeast(1), + }, + Description: "Filter for `regex`, `package-type` and `mime-type` type. Works for `repository` and `all-repos` watch_resource.type", + }, + "ant_filter": schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "include_patterns": schema.ListAttribute{ + ElementType: types.StringType, + Optional: true, + Description: "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 included in this watch. Projects are supported too. Ant-style path expressions are supported (*, **, ?). For example, an 'apache/**' pattern will include the 'apache' build info in the watch.", + }, + "exclude_patterns": schema.ListAttribute{ + ElementType: types.StringType, + Optional: true, + Description: "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 in this watch. Projects are supported too. Ant-style path expressions are supported (*, **, ?). For example, an 'apache/**' pattern will exclude the 'apache' build info in the watch.", }, }, }, - "path_ant_filter": { - Type: schema.TypeSet, - Optional: true, - MinItems: 1, - Description: "`path-ant-patterns` filter for `repository` and `all-repos` watch_resource.type", - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "include_patterns": { - Type: schema.TypeList, - Elem: &schema.Schema{ - Type: schema.TypeString, - }, - Optional: true, - Description: "The pattern will apply to the selected repositories. Simple comma separated wildcard patterns for repository artifact paths (with no leading slash). Ant-style path expressions are supported (*, **, ?). For example: 'org/apache/**'", - }, - "exclude_patterns": { - Type: schema.TypeList, - Elem: &schema.Schema{ - Type: schema.TypeString, - }, - Optional: true, - Description: "The pattern will apply to the selected repositories. Simple comma separated wildcard patterns for repository artifact paths (with no leading slash). Ant-style path expressions are supported (*, **, ?). For example: 'org/apache/**'", - }, + Validators: []validator.Set{ + setvalidator.SizeAtLeast(1), + }, + Description: "`ant-patterns` filter for `all-builds` and `all-projects` watch_resource.type", + }, + "path_ant_filter": schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "include_patterns": schema.ListAttribute{ + ElementType: types.StringType, + Optional: true, + Description: "The pattern will apply to the selected repositories. Simple comma separated wildcard patterns for repository artifact paths (with no leading slash). Ant-style path expressions are supported (*, **, ?). For example: 'org/apache/**'", + }, + "exclude_patterns": schema.ListAttribute{ + ElementType: types.StringType, + Optional: true, + Description: "The pattern will apply to the selected repositories. Simple comma separated wildcard patterns for repository artifact paths (with no leading slash). Ant-style path expressions are supported (*, **, ?). For example: 'org/apache/**'", }, }, }, - "kv_filter": { - Type: schema.TypeSet, - Optional: true, - MinItems: 1, - Description: "Filter for `property` type. Works for `repository` and `all-repos` watch_resource.type.", - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "type": { - Type: schema.TypeString, - Required: true, - Description: "The type of filter. Currently only support `property`", - ValidateDiagFunc: validator.StringInSlice(true, "property"), + Validators: []validator.Set{ + setvalidator.SizeAtLeast(1), + }, + Description: "`path-ant-patterns` filter for `repository` and `all-repos` watch_resource.type", + }, + "kv_filter": schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "type": schema.StringAttribute{ + Required: true, + Validators: []validator.String{ + stringvalidator.OneOfCaseInsensitive("property"), }, - "key": { - Type: schema.TypeString, - Required: true, - Description: "The value of the filter, such as the property name of the artifact.", - ValidateDiagFunc: validator.StringIsNotEmpty, + Description: "The type of filter. Currently only support `property`", + }, + "key": schema.StringAttribute{ + Required: true, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), }, - "value": { - Type: schema.TypeString, - Required: true, - Description: "The value of the filter, such as the property value of the artifact.", - ValidateDiagFunc: validator.StringIsNotEmpty, + Description: "The value of the filter, such as the property name of the artifact.", + }, + "value": schema.StringAttribute{ + Required: true, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), }, + Description: "The value of the filter, such as the property value of the artifact.", }, }, }, + Validators: []validator.Set{ + setvalidator.SizeAtLeast(1), + }, + Description: "Filter for `property` type. Works for `repository` and `all-repos` watch_resource.type.", }, }, }, - // Key is "assigned_policies" in the API call body. Plural is used for better reflection of the - // actual functionality (see HCL examples) - "assigned_policy": { - Type: schema.TypeSet, - Required: true, - Description: "Nested argument describing policies that will be applied. Defined below.", - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "name": { - Type: schema.TypeString, - Required: true, - Description: "The name of the policy that will be applied", - }, - "type": { - Type: schema.TypeString, - Required: true, - Description: "The type of the policy - security, license or operational risk", - ValidateDiagFunc: validator.StringInSlice(true, "security", "license", "operational_risk"), + }, + "assigned_policy": schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "name": schema.StringAttribute{ + Required: true, + Description: "The name of the policy that will be applied", + }, + "type": schema.StringAttribute{ + Required: true, + Validators: []validator.String{ + stringvalidator.OneOfCaseInsensitive("security", "license", "operational_risk"), }, + + Description: "The type of the policy - security, license or operational risk", }, }, }, - "watch_recipients": { - Type: schema.TypeSet, - Optional: true, - Description: "A list of email addressed that will get emailed when a violation is triggered.", - Elem: &schema.Schema{ - Type: schema.TypeString, - ValidateDiagFunc: validator.IsEmail, - }, + Validators: []validator.Set{ + setvalidator.SizeAtLeast(1), }, + Description: "Nested argument describing policies that will be applied. Defined below.", }, - ), + }, + } +} + +func (r *WatchResource) 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 *WatchResource) 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 WatchResourceModel + + // Read Terraform plan data into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + projectKey := plan.ProjectKey.ValueString() + request, err := getRestyRequest(r.ProviderData.Client, projectKey) + if err != nil { + resp.Diagnostics.AddError( + "failed to get Resty client", + err.Error(), + ) + return + } + + var watch WatchAPIModel + resp.Diagnostics.Append(plan.toAPIModel(ctx, &watch)...) + if resp.Diagnostics.HasError() { + return + } + + // add 'build_repo' to resource if project_key is specified. + // undocumented Xray API structure that is required! + if len(plan.ProjectKey.ValueString()) > 0 { + for idx, resource := range watch.ProjectResources.Resources { + if resource.Type == "build" { + watch.ProjectResources.Resources[idx].BuildRepo = fmt.Sprintf("%s-build-info", projectKey) + } + } + } + + response, err := request. + SetBody(watch). + Post(WatchesEndpoint) + if err != nil { + utilfw.UnableToCreateResourceError(resp, err.Error()) + return + } + if response.IsError() { + utilfw.UnableToCreateResourceError(resp, response.String()) + return + } + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *WatchResource) 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 WatchResourceModel + + // Read Terraform plan data into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + projectKey := plan.ProjectKey.ValueString() + request, err := getRestyRequest(r.ProviderData.Client, projectKey) + if err != nil { + resp.Diagnostics.AddError( + "failed to get Resty client", + err.Error(), + ) + return + } + + var watch WatchAPIModel + resp.Diagnostics.Append(plan.toAPIModel(ctx, &watch)...) + if resp.Diagnostics.HasError() { + return + } + + // add 'build_repo' to resource if project_key is specified. + // undocumented Xray API structure that is required! + if len(plan.ProjectKey.ValueString()) > 0 { + for idx, resource := range watch.ProjectResources.Resources { + if resource.Type == "build" { + watch.ProjectResources.Resources[idx].BuildRepo = fmt.Sprintf("%s-build-info", projectKey) + } + } + } + + response, err := request. + SetPathParam("name", plan.Name.ValueString()). + SetBody(watch). + Put(WatchEndpoint) + if err != nil { + utilfw.UnableToUpdateResourceError(resp, err.Error()) + return + } + if response.IsError() { + utilfw.UnableToUpdateResourceError(resp, response.String()) + return + } + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *WatchResource) 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 WatchResourceModel + + // Read Terraform state data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + request, err := getRestyRequest(r.ProviderData.Client, state.ProjectKey.ValueString()) + if err != nil { + resp.Diagnostics.AddError( + "failed to get Resty client", + err.Error(), + ) + return + } + + var watch WatchAPIModel + + response, err := request. + SetPathParam("name", state.Name.ValueString()). + SetResult(&watch). + Get(WatchEndpoint) + 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, response.String()) + return + } + + resp.Diagnostics.Append(state.fromAPIModel(ctx, watch)...) + if resp.Diagnostics.HasError() { + return + } + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *WatchResource) 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 WatchResourceModel + + // Read Terraform prior state data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + + request, err := getRestyRequest(r.ProviderData.Client, state.ProjectKey.ValueString()) + if err != nil { + resp.Diagnostics.AddError( + "failed to get Resty client", + err.Error(), + ) + return + } + + response, err := request. + SetPathParam("name", state.Name.ValueString()). + Delete(WatchEndpoint) + + 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, response.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 *WatchResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + parts := strings.SplitN(req.ID, ":", 2) + + if len(parts) > 0 && parts[0] != "" { + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("name"), parts[0])...) + } + + if len(parts) == 2 && parts[1] != "" { + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_key"), parts[1])...) + } +} + +func (r WatchResource) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) { + var config WatchResourceModel + + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + + // If watch_resource is not configured, return without warning. + if config.WatchResource.IsNull() || config.WatchResource.IsUnknown() { + return + } + + repositoryResourceTypes := []string{"repository", "all-repos"} + + for idx, elem := range config.WatchResource.Elements() { + attrs := elem.(types.Object).Attributes() + + resourceType := attrs["type"].(types.String).ValueString() + + // validate repo_type + repoType := attrs["repo_type"].(types.String).ValueString() + if resourceType == "repository" && len(repoType) == 0 { + resp.Diagnostics.AddAttributeError( + path.Root("watch_resources").AtListIndex(idx).AtName("repo_type"), + "Invalid attribute values combination", + "Attribute 'repo_type' not set when 'watch_resource.type' is set to 'repository'", + ) + return + } + + // validate type with filter and ant_filter + antFilters := attrs["ant_filter"].(types.Set).Elements() + antPatternsResourceTypes := []string{"all-builds", "all-projects", "all-releaseBundles", "all-releaseBundlesV2"} + if !slices.Contains(antPatternsResourceTypes, resourceType) && len(antFilters) > 0 { + resp.Diagnostics.AddAttributeError( + path.Root("watch_resources").AtListIndex(idx).AtName("ant_filter"), + "Invalid attribute values combination", + "attribute 'ant_filter' is set when 'watch_resource.type' is not set to 'all-builds', 'all-projects', 'all-releaseBundles', or 'all-releaseBundlesV2'", + ) + return + } + + pathAntFilters := attrs["path_ant_filter"].(types.Set).Elements() + if !slices.Contains(repositoryResourceTypes, resourceType) && len(pathAntFilters) > 0 { + resp.Diagnostics.AddAttributeError( + path.Root("watch_resources").AtListIndex(idx).AtName("path_ant_filter"), + "Invalid attribute values combination", + "attribute 'path_ant_filter' is set when 'watch_resource.type' is not set to 'repository' or 'all-repos'", + ) + return + } + + kvFilters := attrs["kv_filter"].(types.Set).Elements() + if !slices.Contains(repositoryResourceTypes, resourceType) && len(kvFilters) > 0 { + resp.Diagnostics.AddAttributeError( + path.Root("watch_resources").AtListIndex(idx).AtName("kv_filter"), + "Invalid attribute values combination", + "attribute 'kv_filter' is set when 'watch_resource.type' is not set to 'repository' or 'all-repos'", + ) + return + } } } diff --git a/pkg/xray/resource/resource_xray_watch_test.go b/pkg/xray/resource/resource_xray_watch_test.go index be1a15fc..468f53be 100644 --- a/pkg/xray/resource/resource_xray_watch_test.go +++ b/pkg/xray/resource/resource_xray_watch_test.go @@ -5,7 +5,6 @@ import ( "math/rand" "regexp" "testing" - "time" "github.com/go-resty/resty/v2" "github.com/hashicorp/terraform-plugin-testing/helper/resource" @@ -34,6 +33,39 @@ var testDataWatch = map[string]string{ "watch_recipient_1": "test@email.com", } +func TestAccWatch_UpgradeFromSDKv2(t *testing.T) { + _, fqrn, resourceName := testutil.MkNames("watch-", "xray_watch") + testData := sdk.MergeMaps(testDataWatch) + + testData["resource_name"] = resourceName + testData["watch_name"] = fmt.Sprintf("xray-watch-%d", testutil.RandomInt()) + testData["policy_name_0"] = fmt.Sprintf("xray-policy-%d", testutil.RandomInt()) + + config := util.ExecuteTemplate(fqrn, allReposSinglePolicyWatchTemplate, testData) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + CheckDestroy: acctest.VerifyDeleted(fqrn, "name", testCheckWatch), + Steps: []resource.TestStep{ + { + ExternalProviders: map[string]resource.ExternalProvider{ + "xray": { + VersionConstraint: "2.8.1", + Source: "jfrog/xray", + }, + }, + Config: config, + Check: verifyXrayWatch(fqrn, testData), + }, + { + ProtoV6ProviderFactories: acctest.ProtoV6MuxProviderFactories, + Config: config, + ConfigPlanChecks: testutil.ConfigPlanChecks(""), + }, + }, + }) +} + func TestAccWatch_allReposSinglePolicy(t *testing.T) { _, fqrn, resourceName := testutil.MkNames("watch-", "xray_watch") testData := sdk.MergeMaps(testDataWatch) @@ -44,7 +76,7 @@ func TestAccWatch_allReposSinglePolicy(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(t) }, - CheckDestroy: acctest.VerifyDeleted(fqrn, "", func(id string, request *resty.Request) (*resty.Response, error) { + CheckDestroy: acctest.VerifyDeleted(fqrn, "name", func(id string, request *resty.Request) (*resty.Response, error) { acctest.CheckPolicyDeleted(testData["policy_name_0"], t, request) resp, err := testCheckWatch(id, request) return resp, err @@ -56,9 +88,11 @@ func TestAccWatch_allReposSinglePolicy(t *testing.T) { Check: verifyXrayWatch(fqrn, testData), }, { - ResourceName: fqrn, - ImportState: true, - ImportStateVerify: true, + ResourceName: fqrn, + ImportState: true, + ImportStateId: testData["watch_name"], + ImportStateVerify: true, + ImportStateVerifyIdentifierAttribute: "name", }, }, }) @@ -76,7 +110,7 @@ func TestAccWatch_allReposPathAntFilter(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(t) }, - CheckDestroy: acctest.VerifyDeleted(fqrn, "", func(id string, request *resty.Request) (*resty.Response, error) { + CheckDestroy: acctest.VerifyDeleted(fqrn, "name", func(id string, request *resty.Request) (*resty.Response, error) { acctest.CheckPolicyDeleted(testData["policy_name_0"], t, request) resp, err := testCheckWatch(id, request) return resp, err @@ -88,9 +122,11 @@ func TestAccWatch_allReposPathAntFilter(t *testing.T) { Check: verifyXrayWatch(fqrn, testData), }, { - ResourceName: fqrn, - ImportState: true, - ImportStateVerify: true, + ResourceName: fqrn, + ImportState: true, + ImportStateId: testData["watch_name"], + ImportStateVerify: true, + ImportStateVerifyIdentifierAttribute: "name", }, }, }) @@ -108,7 +144,7 @@ func TestAccWatch_allReposKvFilter(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(t) }, - CheckDestroy: acctest.VerifyDeleted(fqrn, "", func(id string, request *resty.Request) (*resty.Response, error) { + CheckDestroy: acctest.VerifyDeleted(fqrn, "name", func(id string, request *resty.Request) (*resty.Response, error) { acctest.CheckPolicyDeleted(testData["policy_name_0"], t, request) resp, err := testCheckWatch(id, request) return resp, err @@ -120,9 +156,11 @@ func TestAccWatch_allReposKvFilter(t *testing.T) { Check: verifyXrayWatch(fqrn, testData), }, { - ResourceName: fqrn, - ImportState: true, - ImportStateVerify: true, + ResourceName: fqrn, + ImportState: true, + ImportStateId: testData["watch_name"], + ImportStateVerify: true, + ImportStateVerifyIdentifierAttribute: "name", }, }, }) @@ -197,7 +235,7 @@ func TestAccWatch_allReposWithProjectKey(t *testing.T) { acctest.PreCheck(t) acctest.CreateProject(t, projectKey) }, - CheckDestroy: acctest.VerifyDeleted(fqrn, "", func(id string, request *resty.Request) (*resty.Response, error) { + CheckDestroy: acctest.VerifyDeleted(fqrn, "name", func(id string, request *resty.Request) (*resty.Response, error) { acctest.DeleteProject(t, projectKey) resp, err := testCheckWatch(id, request) return resp, err @@ -216,10 +254,11 @@ func TestAccWatch_allReposWithProjectKey(t *testing.T) { Check: verifyXrayWatch(fqrn, updatedTestData), }, { - ResourceName: fqrn, - ImportState: true, - ImportStateId: fmt.Sprintf("%s:%s", testData["watch_name"], projectKey), - ImportStateVerify: true, + ResourceName: fqrn, + ImportState: true, + ImportStateId: fmt.Sprintf("%s:%s", testData["watch_name"], projectKey), + ImportStateVerify: true, + ImportStateVerifyIdentifierAttribute: "name", }, }, }) @@ -237,7 +276,7 @@ func TestAccWatch_allReposMultiplePolicies(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(t) }, - CheckDestroy: acctest.VerifyDeleted(fqrn, "", func(id string, request *resty.Request) (*resty.Response, error) { + CheckDestroy: acctest.VerifyDeleted(fqrn, "name", func(id string, request *resty.Request) (*resty.Response, error) { acctest.CheckPolicyDeleted(testData["policy_name_0"], t, request) acctest.CheckPolicyDeleted(testData["policy_name_1"], t, request) acctest.CheckPolicyDeleted(testData["policy_name_2"], t, request) @@ -274,9 +313,11 @@ func TestAccWatch_allReposMultiplePolicies(t *testing.T) { ), }, { - ResourceName: fqrn, - ImportState: true, - ImportStateVerify: true, + ResourceName: fqrn, + ImportState: true, + ImportStateId: testData["watch_name"], + ImportStateVerify: true, + ImportStateVerifyIdentifierAttribute: "name", }, }, }) @@ -298,7 +339,7 @@ func makeSingleRepositoryTestCase(repoType string, t *testing.T) (*testing.T, re acctest.PreCheck(t) acctest.CreateRepos(t, testData["repo0"], repoType, "", "") }, - CheckDestroy: acctest.VerifyDeleted(fqrn, "", func(id string, request *resty.Request) (*resty.Response, error) { + CheckDestroy: acctest.VerifyDeleted(fqrn, "name", func(id string, request *resty.Request) (*resty.Response, error) { acctest.DeleteRepo(t, testData["repo0"]) acctest.CheckPolicyDeleted(testData["policy_name_0"], t, request) resp, err := testCheckWatch(id, request) @@ -318,9 +359,11 @@ func makeSingleRepositoryTestCase(repoType string, t *testing.T) (*testing.T, re ), }, { - ResourceName: fqrn, - ImportState: true, - ImportStateVerify: true, + ResourceName: fqrn, + ImportState: true, + ImportStateId: testData["watch_name"], + ImportStateVerify: true, + ImportStateVerifyIdentifierAttribute: "name", }, }, } @@ -405,7 +448,7 @@ func TestAccWatch_singleRepositoryWithProjectKey(t *testing.T) { acctest.CreateProject(t, projectKey) acctest.CreateRepos(t, repoKey, "local", projectKey, "") }, - CheckDestroy: acctest.VerifyDeleted(fqrn, "", func(id string, request *resty.Request) (*resty.Response, error) { + CheckDestroy: acctest.VerifyDeleted(fqrn, "name", func(id string, request *resty.Request) (*resty.Response, error) { acctest.DeleteRepo(t, repoKey) acctest.DeleteProject(t, projectKey) resp, err := testCheckWatch(id, request) @@ -422,10 +465,11 @@ func TestAccWatch_singleRepositoryWithProjectKey(t *testing.T) { ), }, { - ResourceName: fqrn, - ImportState: true, - ImportStateId: fmt.Sprintf("%s:%s", testData["watch_name"], projectKey), - ImportStateVerify: true, + ResourceName: fqrn, + ImportState: true, + ImportStateId: fmt.Sprintf("%s:%s", testData["watch_name"], projectKey), + ImportStateVerify: true, + ImportStateVerifyIdentifierAttribute: "name", }, }, }) @@ -451,7 +495,7 @@ func TestAccWatch_singleRepoMimeTypeFilter(t *testing.T) { acctest.PreCheck(t) acctest.CreateRepos(t, testData["repo0"], repoType, "", "") }, - CheckDestroy: acctest.VerifyDeleted(fqrn, "", func(id string, request *resty.Request) (*resty.Response, error) { + CheckDestroy: acctest.VerifyDeleted(fqrn, "name", func(id string, request *resty.Request) (*resty.Response, error) { acctest.DeleteRepo(t, testData["repo0"]) acctest.CheckPolicyDeleted(testData["policy_name_0"], t, request) resp, err := testCheckWatch(id, request) @@ -496,7 +540,7 @@ func TestAccWatch_singleRepoKvFilter(t *testing.T) { acctest.PreCheck(t) acctest.CreateRepos(t, testData["repo0"], repoType, "", "") }, - CheckDestroy: acctest.VerifyDeleted(fqrn, "", func(id string, request *resty.Request) (*resty.Response, error) { + CheckDestroy: acctest.VerifyDeleted(fqrn, "name", func(id string, request *resty.Request) (*resty.Response, error) { acctest.DeleteRepo(t, testData["repo0"]) acctest.CheckPolicyDeleted(testData["policy_name_0"], t, request) return testCheckWatch(id, request) @@ -538,7 +582,7 @@ func TestAccWatch_repositoryMissingRepoType(t *testing.T) { acctest.PreCheck(t) acctest.CreateRepos(t, testData["repo0"], "local", "", "") }, - CheckDestroy: acctest.VerifyDeleted(fqrn, "", func(id string, request *resty.Request) (*resty.Response, error) { + CheckDestroy: acctest.VerifyDeleted(fqrn, "name", func(id string, request *resty.Request) (*resty.Response, error) { acctest.DeleteRepo(t, testData["repo0"]) acctest.CheckPolicyDeleted(testData["policy_name_0"], t, request) resp, err := testCheckWatch(id, request) @@ -549,7 +593,7 @@ func TestAccWatch_repositoryMissingRepoType(t *testing.T) { Steps: []resource.TestStep{ { Config: util.ExecuteTemplate(fqrn, singleRepositoryInvalidWatchTemplate, testData), - ExpectError: regexp.MustCompile(`attribute 'repo_type' not set when 'watch_resource\.type' is set to 'repository'`), + ExpectError: regexp.MustCompile(`.*Attribute 'repo_type' not set when 'watch_resource\.type' is set to.*`), }, }, }) @@ -573,7 +617,7 @@ func TestAccWatch_multipleRepositories(t *testing.T) { acctest.CreateRepos(t, testData["repo0"], "local", "", "") acctest.CreateRepos(t, testData["repo1"], "local", "", "") }, - CheckDestroy: acctest.VerifyDeleted(fqrn, "", func(id string, request *resty.Request) (*resty.Response, error) { + CheckDestroy: acctest.VerifyDeleted(fqrn, "name", func(id string, request *resty.Request) (*resty.Response, error) { acctest.DeleteRepo(t, testData["repo0"]) acctest.DeleteRepo(t, testData["repo1"]) acctest.CheckPolicyDeleted(testData["policy_name_0"], t, request) @@ -587,9 +631,11 @@ func TestAccWatch_multipleRepositories(t *testing.T) { Check: verifyXrayWatch(fqrn, testData), }, { - ResourceName: fqrn, - ImportState: true, - ImportStateVerify: true, + ResourceName: fqrn, + ImportState: true, + ImportStateId: testData["watch_name"], + ImportStateVerify: true, + ImportStateVerifyIdentifierAttribute: "name", }, }, }) @@ -619,7 +665,7 @@ func TestAccWatch_multipleRepositoriesPathAntPatterns(t *testing.T) { acctest.CreateRepos(t, testData["repo1"], "local", "", "") acctest.CreateRepos(t, testData["repo2"], "local", "", "") }, - CheckDestroy: acctest.VerifyDeleted(fqrn, "", func(id string, request *resty.Request) (*resty.Response, error) { + CheckDestroy: acctest.VerifyDeleted(fqrn, "name", func(id string, request *resty.Request) (*resty.Response, error) { acctest.DeleteRepo(t, testData["repo0"]) acctest.DeleteRepo(t, testData["repo1"]) acctest.DeleteRepo(t, testData["repo2"]) @@ -643,9 +689,11 @@ func TestAccWatch_multipleRepositoriesPathAntPatterns(t *testing.T) { ), }, { - ResourceName: fqrn, - ImportState: true, - ImportStateVerify: true, + ResourceName: fqrn, + ImportState: true, + ImportStateId: testData["watch_name"], + ImportStateVerify: true, + ImportStateVerifyIdentifierAttribute: "name", }, }, }) @@ -673,7 +721,7 @@ func TestAccWatch_PathAntPatternsError(t *testing.T) { acctest.CreateRepos(t, testData["repo0"], "local", "", "") acctest.CreateRepos(t, testData["repo1"], "local", "", "") }, - CheckDestroy: acctest.VerifyDeleted(fqrn, "", func(id string, request *resty.Request) (*resty.Response, error) { + CheckDestroy: acctest.VerifyDeleted(fqrn, "name", func(id string, request *resty.Request) (*resty.Response, error) { acctest.DeleteRepo(t, testData["repo0"]) acctest.DeleteRepo(t, testData["repo1"]) acctest.CheckPolicyDeleted(testData["policy_name_0"], t, request) @@ -684,7 +732,7 @@ func TestAccWatch_PathAntPatternsError(t *testing.T) { Steps: []resource.TestStep{ { Config: util.ExecuteTemplate(fqrn, pathAntPatterns, testData), - ExpectError: regexp.MustCompile("attribute 'path_ant_filter' is set when 'watch_resource.type' is not set to 'repository' or 'all-repos'"), + ExpectError: regexp.MustCompile(".*attribute 'path_ant_filter' is set when 'watch_resource.type' is not set to.*"), }, }, }) @@ -713,7 +761,7 @@ func TestAccWatch_multipleRepositoriesKvFilter(t *testing.T) { acctest.CreateRepos(t, testData["repo0"], "local", "", "") acctest.CreateRepos(t, testData["repo1"], "local", "", "") }, - CheckDestroy: acctest.VerifyDeleted(fqrn, "", func(id string, request *resty.Request) (*resty.Response, error) { + CheckDestroy: acctest.VerifyDeleted(fqrn, "name", func(id string, request *resty.Request) (*resty.Response, error) { acctest.DeleteRepo(t, testData["repo0"]) acctest.DeleteRepo(t, testData["repo1"]) acctest.CheckPolicyDeleted(testData["policy_name_0"], t, request) @@ -742,9 +790,11 @@ func TestAccWatch_multipleRepositoriesKvFilter(t *testing.T) { ), }, { - ResourceName: fqrn, - ImportState: true, - ImportStateVerify: true, + ResourceName: fqrn, + ImportState: true, + ImportStateId: testData["watch_name"], + ImportStateVerify: true, + ImportStateVerifyIdentifierAttribute: "name", }, }, }) @@ -769,7 +819,7 @@ func TestAccWatch_KvFilterError(t *testing.T) { acctest.PreCheck(t) acctest.CreateRepos(t, testData["repo0"], "local", "", "") }, - CheckDestroy: acctest.VerifyDeleted(fqrn, "", func(id string, request *resty.Request) (*resty.Response, error) { + CheckDestroy: acctest.VerifyDeleted(fqrn, "name", func(id string, request *resty.Request) (*resty.Response, error) { acctest.DeleteRepo(t, testData["repo0"]) acctest.CheckPolicyDeleted(testData["policy_name_0"], t, request) resp, err := testCheckWatch(id, request) @@ -779,7 +829,7 @@ func TestAccWatch_KvFilterError(t *testing.T) { Steps: []resource.TestStep{ { Config: util.ExecuteTemplate(fqrn, kvFilters, testData), - ExpectError: regexp.MustCompile("attribute 'kv_filter' is set when 'watch_resource.type' is not set to 'repository' or 'all-repos'"), + ExpectError: regexp.MustCompile(".*attribute 'kv_filter' is set when 'watch_resource.type' is not set to.*"), }, }, }) @@ -802,7 +852,7 @@ func TestAccWatch_build(t *testing.T) { acctest.PreCheck(t) acctest.CreateBuilds(t, builds, "") }, - CheckDestroy: acctest.VerifyDeleted(fqrn, "", testCheckWatch), + CheckDestroy: acctest.VerifyDeleted(fqrn, "name", testCheckWatch), ProtoV6ProviderFactories: acctest.ProtoV6MuxProviderFactories, Steps: []resource.TestStep{ { @@ -810,9 +860,11 @@ func TestAccWatch_build(t *testing.T) { Check: verifyXrayWatch(fqrn, testData), }, { - ResourceName: fqrn, - ImportState: true, - ImportStateVerify: true, + ResourceName: fqrn, + ImportState: true, + ImportStateId: testData["watch_name"], + ImportStateVerify: true, + ImportStateVerifyIdentifierAttribute: "name", }, }, }) @@ -882,7 +934,7 @@ func TestAccWatch_buildWithProjectKey(t *testing.T) { acctest.CreateProject(t, projectKey) acctest.CreateBuilds(t, []string{testData["build_name0"]}, projectKey) }, - CheckDestroy: acctest.VerifyDeleted(fqrn, "", func(id string, request *resty.Request) (*resty.Response, error) { + CheckDestroy: acctest.VerifyDeleted(fqrn, "name", func(id string, request *resty.Request) (*resty.Response, error) { acctest.DeleteProject(t, projectKey) resp, err := testCheckWatch(id, request) return resp, err @@ -897,10 +949,11 @@ func TestAccWatch_buildWithProjectKey(t *testing.T) { ), }, { - ResourceName: fqrn, - ImportState: true, - ImportStateId: fmt.Sprintf("%s:%s", testData["watch_name"], projectKey), - ImportStateVerify: true, + ResourceName: fqrn, + ImportState: true, + ImportStateId: fmt.Sprintf("%s:%s", testData["watch_name"], projectKey), + ImportStateVerify: true, + ImportStateVerifyIdentifierAttribute: "name", }, }, }) @@ -980,7 +1033,7 @@ func TestAccWatch_allBuildsWithProjectKey(t *testing.T) { acctest.PreCheck(t) acctest.CreateProject(t, projectKey) }, - CheckDestroy: acctest.VerifyDeleted(fqrn, "", func(id string, request *resty.Request) (*resty.Response, error) { + CheckDestroy: acctest.VerifyDeleted(fqrn, "name", func(id string, request *resty.Request) (*resty.Response, error) { acctest.DeleteProject(t, projectKey) resp, err := testCheckWatch(id, request) return resp, err @@ -999,10 +1052,11 @@ func TestAccWatch_allBuildsWithProjectKey(t *testing.T) { Check: verifyXrayWatch(fqrn, updatedTestData), }, { - ResourceName: fqrn, - ImportState: true, - ImportStateId: fmt.Sprintf("%s:%s", testData["watch_name"], projectKey), - ImportStateVerify: true, + ResourceName: fqrn, + ImportState: true, + ImportStateId: fmt.Sprintf("%s:%s", testData["watch_name"], projectKey), + ImportStateVerify: true, + ImportStateVerifyIdentifierAttribute: "name", }, }, }) @@ -1025,7 +1079,7 @@ func TestAccWatch_multipleBuilds(t *testing.T) { acctest.PreCheck(t) acctest.CreateBuilds(t, builds, "") }, - CheckDestroy: acctest.VerifyDeleted(fqrn, "", testCheckWatch), + CheckDestroy: acctest.VerifyDeleted(fqrn, "name", testCheckWatch), ProtoV6ProviderFactories: acctest.ProtoV6MuxProviderFactories, Steps: []resource.TestStep{ { @@ -1033,9 +1087,11 @@ func TestAccWatch_multipleBuilds(t *testing.T) { Check: verifyXrayWatch(fqrn, testData), }, { - ResourceName: fqrn, - ImportState: true, - ImportStateVerify: true, + ResourceName: fqrn, + ImportState: true, + ImportStateId: testData["watch_name"], + ImportStateVerify: true, + ImportStateVerifyIdentifierAttribute: "name", }, }, }) @@ -1054,7 +1110,7 @@ func TestAccWatch_allBuilds(t *testing.T) { PreCheck: func() { acctest.PreCheck(t) }, - CheckDestroy: acctest.VerifyDeleted(fqrn, "", testCheckWatch), + CheckDestroy: acctest.VerifyDeleted(fqrn, "name", testCheckWatch), ProtoV6ProviderFactories: acctest.ProtoV6MuxProviderFactories, Steps: []resource.TestStep{ { @@ -1070,9 +1126,11 @@ func TestAccWatch_allBuilds(t *testing.T) { ), }, { - ResourceName: fqrn, - ImportState: true, - ImportStateVerify: true, + ResourceName: fqrn, + ImportState: true, + ImportStateId: testData["watch_name"], + ImportStateVerify: true, + ImportStateVerifyIdentifierAttribute: "name", }, }, }) @@ -1090,12 +1148,12 @@ func TestAccWatch_invalidBuildFilter(t *testing.T) { PreCheck: func() { acctest.PreCheck(t) }, - CheckDestroy: acctest.VerifyDeleted(fqrn, "", testCheckWatch), + CheckDestroy: acctest.VerifyDeleted(fqrn, "name", testCheckWatch), ProtoV6ProviderFactories: acctest.ProtoV6MuxProviderFactories, Steps: []resource.TestStep{ { Config: util.ExecuteTemplate(fqrn, invalidBuildsWatchFilterTemplate, testData), - ExpectError: regexp.MustCompile(`attribute 'ant_filter' is set when 'watch_resource.type' is not set to 'all-builds', 'all-projects', 'all-releaseBundles', or 'all-releaseBundlesV2'`), + ExpectError: regexp.MustCompile(`.*attribute 'ant_filter' is set when 'watch_resource.type' is not set to.*`), }, }, }) @@ -1114,7 +1172,7 @@ func TestAccWatch_allProjects(t *testing.T) { PreCheck: func() { acctest.PreCheck(t) }, - CheckDestroy: acctest.VerifyDeleted(fqrn, "", testCheckWatch), + CheckDestroy: acctest.VerifyDeleted(fqrn, "name", testCheckWatch), ProtoV6ProviderFactories: acctest.ProtoV6MuxProviderFactories, Steps: []resource.TestStep{ { @@ -1127,9 +1185,11 @@ func TestAccWatch_allProjects(t *testing.T) { ), }, { - ResourceName: fqrn, - ImportState: true, - ImportStateVerify: true, + ResourceName: fqrn, + ImportState: true, + ImportStateId: testData["watch_name"], + ImportStateVerify: true, + ImportStateVerifyIdentifierAttribute: "name", }, }, }) @@ -1154,7 +1214,7 @@ func TestAccWatch_singleProject(t *testing.T) { acctest.CreateProject(t, testData["project_key_0"]) acctest.CreateProject(t, testData["project_key_1"]) }, - CheckDestroy: acctest.VerifyDeleted(fqrn, "", func(id string, request *resty.Request) (*resty.Response, error) { + CheckDestroy: acctest.VerifyDeleted(fqrn, "name", func(id string, request *resty.Request) (*resty.Response, error) { acctest.DeleteProject(t, testData["project_key_0"]) acctest.DeleteProject(t, testData["project_key_1"]) //watch created by TF, so it will be automatically deleted by DeleteContext function @@ -1169,9 +1229,11 @@ func TestAccWatch_singleProject(t *testing.T) { Check: verifyXrayWatch(fqrn, testData), }, { - ResourceName: fqrn, - ImportState: true, - ImportStateVerify: true, + ResourceName: fqrn, + ImportState: true, + ImportStateId: testData["watch_name"], + ImportStateVerify: true, + ImportStateVerifyIdentifierAttribute: "name", }, }, }) @@ -1190,13 +1252,13 @@ func TestAccWatch_invalidProjectFilter(t *testing.T) { PreCheck: func() { acctest.PreCheck(t) }, - CheckDestroy: acctest.VerifyDeleted(fqrn, "", testCheckWatch), + CheckDestroy: acctest.VerifyDeleted(fqrn, "name", testCheckWatch), ProtoV6ProviderFactories: acctest.ProtoV6MuxProviderFactories, Steps: []resource.TestStep{ { Config: util.ExecuteTemplate(fqrn, invalidProjectWatchFilterTemplate, testData), - ExpectError: regexp.MustCompile(`attribute 'ant_filter' is set when 'watch_resource.type' is not set to 'all-builds', 'all-projects', 'all-releaseBundles', or 'all-releaseBundlesV2'`), + ExpectError: regexp.MustCompile(`attribute 'ant_filter' is set when 'watch_resource.type' is not set to.*`), }, }, }) @@ -1223,7 +1285,7 @@ func allReleaseBundleTestCase(watchType string, t *testing.T) (*testing.T, resou PreCheck: func() { acctest.PreCheck(t) }, - CheckDestroy: acctest.VerifyDeleted(fqrn, "", testCheckWatch), + CheckDestroy: acctest.VerifyDeleted(fqrn, "name", testCheckWatch), ProtoV6ProviderFactories: acctest.ProtoV6MuxProviderFactories, Steps: []resource.TestStep{ { @@ -1236,9 +1298,11 @@ func allReleaseBundleTestCase(watchType string, t *testing.T) (*testing.T, resou ), }, { - ResourceName: fqrn, - ImportState: true, - ImportStateVerify: true, + ResourceName: fqrn, + ImportState: true, + ImportStateId: testData["watch_name"], + ImportStateVerify: true, + ImportStateVerifyIdentifierAttribute: "name", }, }, } @@ -1258,7 +1322,7 @@ func TestAccWatch_singleReleaseBundle(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(t) }, - CheckDestroy: acctest.VerifyDeleted(fqrn, "", testCheckWatch), + CheckDestroy: acctest.VerifyDeleted(fqrn, "name", testCheckWatch), ProtoV6ProviderFactories: acctest.ProtoV6MuxProviderFactories, Steps: []resource.TestStep{ { @@ -1266,9 +1330,11 @@ func TestAccWatch_singleReleaseBundle(t *testing.T) { Check: verifyXrayWatch(fqrn, testData), }, { - ResourceName: fqrn, - ImportState: true, - ImportStateVerify: true, + ResourceName: fqrn, + ImportState: true, + ImportStateId: testData["watch_name"], + ImportStateVerify: true, + ImportStateVerifyIdentifierAttribute: "name", }, }, }) @@ -2338,6 +2404,5 @@ func testCheckWatch(id string, request *resty.Request) (*resty.Response, error) } func RandomProjectName() string { - rand.Seed(time.Now().UnixNano()) return fmt.Sprintf("testproj%d", rand.Intn(100)) } diff --git a/pkg/xray/resource/watches.go b/pkg/xray/resource/watches.go deleted file mode 100644 index 34288e69..00000000 --- a/pkg/xray/resource/watches.go +++ /dev/null @@ -1,555 +0,0 @@ -package xray - -import ( - "context" - "encoding/json" - "fmt" - "net/http" - "strconv" - - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - "github.com/jfrog/terraform-provider-shared/util" - "github.com/jfrog/terraform-provider-shared/util/sdk" - "golang.org/x/exp/slices" -) - -type WatchGeneralData struct { - Name string `json:"name"` - Description string `json:"description"` - Active bool `json:"active"` -} - -type WatchFilter struct { - Type string `json:"type"` - Value json.RawMessage `json:"value"` -} - -type WatchFilterAntValue struct { - ExcludePatterns []string `json:"ExcludePatterns"` - IncludePatterns []string `json:"IncludePatterns"` -} - -type WatchFilterKvValue struct { - Key string `json:"key"` - Value string `json:"value"` -} - -type WatchProjectResource struct { - Type string `json:"type"` - BinaryManagerId string `json:"bin_mgr_id"` - Filters []WatchFilter `json:"filters,omitempty"` - Name string `json:"name,omitempty"` - BuildRepo string `json:"build_repo,omitempty"` - RepoType string `json:"repo_type,omitempty"` -} - -type WatchProjectResources struct { - Resources []WatchProjectResource `json:"resources"` -} - -type WatchAssignedPolicy struct { - Name string `json:"name"` - Type string `json:"type"` -} - -type Watch struct { - ProjectKey string `json:"-"` - GeneralData WatchGeneralData `json:"general_data"` - ProjectResources WatchProjectResources `json:"project_resources"` - AssignedPolicies []WatchAssignedPolicy `json:"assigned_policies"` - WatchRecipients []string `json:"watch_recipients"` -} - -func unpackWatch(d *schema.ResourceData) Watch { - watch := Watch{} - - if v, ok := d.GetOk("project_key"); ok { - watch.ProjectKey = v.(string) - } - - generalData := WatchGeneralData{ - Name: d.Get("name").(string), - } - if v, ok := d.GetOk("description"); ok { - generalData.Description = v.(string) - } - if v, ok := d.GetOk("active"); ok { - generalData.Active = v.(bool) - } - watch.GeneralData = generalData - - projectResources := WatchProjectResources{} - if v, ok := d.GetOk("watch_resource"); ok { - var r []WatchProjectResource - for _, res := range v.(*schema.Set).List() { - r = append(r, unpackProjectResource(res)) - } - projectResources.Resources = r - } - watch.ProjectResources = projectResources - - var assignedPolicies []WatchAssignedPolicy - if v, ok := d.GetOk("assigned_policy"); ok { - policies := v.(*schema.Set).List() - for _, pol := range policies { - assignedPolicies = append(assignedPolicies, unpackAssignedPolicy(pol)) - } - } - watch.AssignedPolicies = assignedPolicies - - var watchRecipients []string - if v, ok := d.GetOk("watch_recipients"); ok { - recipients := v.(*schema.Set).List() - for _, watchRec := range recipients { - watchRecipients = append(watchRecipients, watchRec.(string)) - } - } - watch.WatchRecipients = watchRecipients - - return watch -} - -func unpackProjectResource(rawCfg interface{}) WatchProjectResource { - resource := WatchProjectResource{} - - cfg := rawCfg.(map[string]interface{}) - resource.Type = cfg["type"].(string) - - if v, ok := cfg["bin_mgr_id"]; ok { - resource.BinaryManagerId = v.(string) - } - - if v, ok := cfg["name"]; ok { - resource.Name = v.(string) - } - - if v, ok := cfg["repo_type"]; ok { - resource.RepoType = v.(string) - } - - if v, ok := cfg["filter"]; ok { - filters := unpackFilters(v.(*schema.Set)) - resource.Filters = append(resource.Filters, filters...) - } - - if v, ok := cfg["ant_filter"]; ok { - antFilters := unpackAntFilters(v.(*schema.Set), "ant-patterns") - resource.Filters = append(resource.Filters, antFilters...) - } - - if v, ok := cfg["path_ant_filter"]; ok { - antFilters := unpackAntFilters(v.(*schema.Set), "path-ant-patterns") - resource.Filters = append(resource.Filters, antFilters...) - } - - if v, ok := cfg["kv_filter"]; ok { - kvFilters := unpackKvFilters(v.(*schema.Set)) - resource.Filters = append(resource.Filters, kvFilters...) - } - - return resource -} - -func unpackFilters(d *schema.Set) []WatchFilter { - tfFilters := d.List() - - var filters []WatchFilter - - for _, raw := range tfFilters { - f := raw.(map[string]interface{}) - filter := WatchFilter{ - Type: f["type"].(string), - Value: json.RawMessage(strconv.Quote(f["value"].(string))), - } - filters = append(filters, filter) - } - - return filters -} - -func unpackAntFilters(d *schema.Set, filterType string) []WatchFilter { - tfFilters := d.List() - - var filters []WatchFilter - - type antFilterValue struct { - ExcludePatterns []string `json:"ExcludePatterns"` - IncludePatterns []string `json:"IncludePatterns"` - } - - for _, raw := range tfFilters { - antValue := raw.(map[string]interface{}) - - // create JSON string from slice: - // from []string{"a", "b"} to `["ExcludePatterns": ["a", "b"]]` - filterValue, _ := json.Marshal( - &antFilterValue{ - ExcludePatterns: sdk.CastToStringArr(antValue["exclude_patterns"].([]interface{})), - IncludePatterns: sdk.CastToStringArr(antValue["include_patterns"].([]interface{})), - }, - ) - - filter := WatchFilter{ - Type: filterType, - Value: json.RawMessage(filterValue), - } - filters = append(filters, filter) - } - - return filters -} - -func unpackKvFilters(d *schema.Set) []WatchFilter { - tfFilters := d.List() - - var filters []WatchFilter - - for _, raw := range tfFilters { - kv := raw.(map[string]interface{}) - - filterJsonString := fmt.Sprintf( - `{"key": "%s", "value": "%s"}`, - kv["key"].(string), - kv["value"].(string), - ) - - filter := WatchFilter{ - Type: kv["type"].(string), - Value: json.RawMessage(filterJsonString), - } - filters = append(filters, filter) - } - - return filters -} - -func unpackAssignedPolicy(rawCfg interface{}) WatchAssignedPolicy { - policy := WatchAssignedPolicy{} - - cfg := rawCfg.(map[string]interface{}) - policy.Name = cfg["name"].(string) - policy.Type = cfg["type"].(string) - - return policy -} - -var allTypes = []string{"all-repos", "all-builds", "all-projects"} - -func packProjectResources(ctx context.Context, resources WatchProjectResources) []interface{} { - var resourceMaps []interface{} - - for _, res := range resources.Resources { - resourceMap := map[string]interface{}{} - resourceMap["type"] = res.Type - // only pack watch resource name if type isn't for all-* - // Xray API returns a generated name for all-* type which will - // cause TF to want to update the resource since it doesn't match - // the configuration. - if len(res.Name) > 0 && !slices.Contains(allTypes, res.Type) { - resourceMap["name"] = res.Name - } - if len(res.BinaryManagerId) > 0 { - resourceMap["bin_mgr_id"] = res.BinaryManagerId - } - if len(res.RepoType) > 0 { - resourceMap["repo_type"] = res.RepoType - } - - resourceMap, errors := packFilters(res.Filters, resourceMap) - if len(errors) > 0 { - tflog.Error(ctx, fmt.Sprintf(`failed to pack filters: %v`, errors)) - } - - resourceMaps = append(resourceMaps, resourceMap) - } - - return resourceMaps -} - -type PackFilterFunc func(filter WatchFilter) (map[string]interface{}, error) - -func packStringFilter(filter WatchFilter) (map[string]interface{}, error) { - var value string - err := json.Unmarshal(filter.Value, &value) - if err != nil { - return nil, err - } - - return map[string]interface{}{ - "type": filter.Type, - "value": value, - }, nil -} - -func packAntFilter(filter WatchFilter) (map[string]interface{}, error) { - var value WatchFilterAntValue - err := json.Unmarshal(filter.Value, &value) - m := map[string]interface{}{ - "exclude_patterns": value.ExcludePatterns, - "include_patterns": value.IncludePatterns, - } - return m, err -} - -func packKvFilter(filter WatchFilter) (map[string]interface{}, error) { - var kvValue WatchFilterKvValue - err := json.Unmarshal(filter.Value, &kvValue) - if err != nil { - return nil, err - } - - return map[string]interface{}{ - "type": filter.Type, - "key": kvValue.Key, - "value": kvValue.Value, - }, nil -} - -var packFilterMap = map[string]map[string]interface{}{ - "regex": { - "func": packStringFilter, - "attributeName": "filter", - }, - "path-regex": { - "func": packStringFilter, - "attributeName": "filter", - }, - "package-type": { - "func": packStringFilter, - "attributeName": "filter", - }, - "mime-type": { - "func": packStringFilter, - "attributeName": "filter", - }, - "ant-patterns": { - "func": packAntFilter, - "attributeName": "ant_filter", - }, - "path-ant-patterns": { - "func": packAntFilter, - "attributeName": "path_ant_filter", - }, - "property": { - "func": packKvFilter, - "attributeName": "kv_filter", - }, -} - -func packFilters(filters []WatchFilter, resources map[string]interface{}) (map[string]interface{}, []error) { - resources["filter"] = []map[string]interface{}{} - resources["ant_filter"] = []map[string]interface{}{} - resources["path_ant_filter"] = []map[string]interface{}{} - resources["kv_filter"] = []map[string]interface{}{} - var errors []error - - for _, filter := range filters { - packFilterAttribute, ok := packFilterMap[filter.Type] - if !ok { - return nil, []error{fmt.Errorf("invalid filter.Type: %s", filter.Type)} - } - - packedFilter, err := packFilterAttribute["func"].(func(WatchFilter) (map[string]interface{}, error))(filter) - if err != nil { - errors = append(errors, err) - } else { - attributeName := packFilterAttribute["attributeName"].(string) - resources[attributeName] = append(resources[attributeName].([]map[string]interface{}), packedFilter) - } - } - - return resources, errors -} - -func packAssignedPolicies(policies []WatchAssignedPolicy) []interface{} { - var assignedPolicies []interface{} - for _, p := range policies { - assignedPolicy := map[string]interface{}{ - "name": p.Name, - "type": p.Type, - } - assignedPolicies = append(assignedPolicies, assignedPolicy) - } - - return assignedPolicies -} - -func packWatch(ctx context.Context, watch Watch, d *schema.ResourceData) diag.Diagnostics { - if err := d.Set("name", watch.GeneralData.Name); err != nil { - return diag.FromErr(err) - } - if err := d.Set("description", watch.GeneralData.Description); err != nil { - return diag.FromErr(err) - } - if err := d.Set("active", watch.GeneralData.Active); err != nil { - return diag.FromErr(err) - } - if err := d.Set("watch_resource", packProjectResources(ctx, watch.ProjectResources)); err != nil { - return diag.FromErr(err) - } - if err := d.Set("assigned_policy", packAssignedPolicies(watch.AssignedPolicies)); err != nil { - return diag.FromErr(err) - } - if err := d.Set("watch_recipients", watch.WatchRecipients); err != nil { - return diag.FromErr(err) - } - - return nil -} - -func resourceXrayWatchCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { - watch := unpackWatch(d) - - req, err := getRestyRequest(m.(util.ProviderMetadata).Client, watch.ProjectKey) - if err != nil { - return diag.FromErr(err) - } - - // add 'build_repo' to resource if project_key is specified. - // undocumented Xray API structure that is required! - if len(watch.ProjectKey) > 0 { - for idx, resource := range watch.ProjectResources.Resources { - if resource.Type == "build" { - watch.ProjectResources.Resources[idx].BuildRepo = fmt.Sprintf("%s-build-info", watch.ProjectKey) - } - } - } - - resp, err := req. - SetBody(watch). - Post("xray/api/v2/watches") - if err != nil { - return diag.FromErr(err) - } - if resp.IsError() { - return diag.Errorf("%s", resp.String()) - } - - d.SetId(watch.GeneralData.Name) - return resourceXrayWatchRead(ctx, d, m) -} - -func resourceXrayWatchRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { - watch := Watch{} - - projectKey := d.Get("project_key").(string) - req, err := getRestyRequest(m.(util.ProviderMetadata).Client, projectKey) - if err != nil { - return diag.FromErr(err) - } - - resp, err := req. - SetResult(&watch). - SetPathParams(map[string]string{ - "name": d.Id(), - }). - Get("xray/api/v2/watches/{name}") - if err != nil { - return diag.FromErr(err) - } - if resp.StatusCode() == http.StatusNotFound { - d.SetId("") - return diag.Errorf("watch (%s) not found, removing from state", d.Id()) - } - if resp.IsError() { - return diag.Errorf("%s", resp.String()) - } - - return packWatch(ctx, watch, d) -} - -func resourceXrayWatchUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { - watch := unpackWatch(d) - - req, err := getRestyRequest(m.(util.ProviderMetadata).Client, watch.ProjectKey) - if err != nil { - return diag.FromErr(err) - } - - resp, err := req. - SetBody(watch). - SetPathParams(map[string]string{ - "name": d.Id(), - }). - Put("xray/api/v2/watches/{name}") - if err != nil { - return diag.FromErr(err) - } - if resp.StatusCode() == http.StatusNotFound { - d.SetId("") - return diag.Errorf("watch (%s) not found, removing from state", d.Id()) - } - if resp.IsError() { - return diag.Errorf("%s", resp.String()) - } - - d.SetId(watch.GeneralData.Name) - return resourceXrayWatchRead(ctx, d, m) -} - -func resourceXrayWatchDelete(_ context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { - watch := unpackWatch(d) - - req, err := getRestyRequest(m.(util.ProviderMetadata).Client, watch.ProjectKey) - if err != nil { - return diag.FromErr(err) - } - - resp, err := req. - SetPathParams(map[string]string{ - "name": d.Id(), - }). - Delete("xray/api/v2/watches/{name}") - if err != nil { - return diag.FromErr(err) - } - if resp.StatusCode() == http.StatusNotFound { - d.SetId("") - } - if resp.IsError() { - return diag.Errorf("%s", resp.String()) - } - - d.SetId("") - - return nil -} - -func watchResourceDiff(_ context.Context, diff *schema.ResourceDiff, v interface{}) error { - watchResources := diff.Get("watch_resource").(*schema.Set).List() - if len(watchResources) == 0 { - return nil - } - for _, watchResource := range watchResources { - r := watchResource.(map[string]interface{}) - resourceType := r["type"].(string) - - // validate repo_type - repoType := r["repo_type"].(string) - if resourceType == "repository" && len(repoType) == 0 { - return fmt.Errorf("attribute 'repo_type' not set when 'watch_resource.type' is set to 'repository'") - } - - // validate type with filter and ant_filter - antFilters := r["ant_filter"].(*schema.Set).List() - antPatternsResourceTypes := []string{"all-builds", "all-projects", "all-releaseBundles", "all-releaseBundlesV2"} - if !slices.Contains(antPatternsResourceTypes, resourceType) && len(antFilters) > 0 { - return fmt.Errorf("attribute 'ant_filter' is set when 'watch_resource.type' is not set to 'all-builds', 'all-projects', 'all-releaseBundles', or 'all-releaseBundlesV2'") - } - - repositoryResourceTypes := []string{"repository", "all-repos"} - - pathAntFilters := r["path_ant_filter"].(*schema.Set).List() - if !slices.Contains(repositoryResourceTypes, resourceType) && len(pathAntFilters) > 0 { - return fmt.Errorf("attribute 'path_ant_filter' is set when 'watch_resource.type' is not set to 'repository' or 'all-repos'") - } - - kvFilters := r["kv_filter"].(*schema.Set).List() - if !slices.Contains(repositoryResourceTypes, resourceType) && len(kvFilters) > 0 { - return fmt.Errorf("attribute 'kv_filter' is set when 'watch_resource.type' is not set to 'repository' or 'all-repos'") - } - } - return nil -} From 51ecf5c8d5b413d192e8f6410232bf30436912df Mon Sep 17 00:00:00 2001 From: Alex Hung Date: Fri, 21 Jun 2024 09:27:26 -0700 Subject: [PATCH 2/5] Update CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 83b387aa..c4b0746a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ * resource/xray_custom_issue: Migrate from SDKv2 to Plugin Framework. PR: [#207](https://github.com/jfrog/terraform-provider-xray/pull/207) * resource/xray_ignore_rule: Migrate from SDKv2 to Plugin Framework. PR: [#209](https://github.com/jfrog/terraform-provider-xray/pull/209) +* resource/xray_watch: Migrate from SDKv2 to Plugin Framework. PR: [#210](https://github.com/jfrog/terraform-provider-xray/pull/210) ## 2.8.1 (June 14, 2024). Tested on Artifactory 7.84.14 and Xray 3.96.1 with Terraform 1.8.5 and OpenTofu 1.7.2 From bed1a0cff4597d39b3cdb1337aa0ae891553eab6 Mon Sep 17 00:00:00 2001 From: Alex Hung Date: Fri, 21 Jun 2024 09:27:53 -0700 Subject: [PATCH 3/5] Update CHANGELOG --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c4b0746a..b5c895de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -## 2.8.2 (June 19, 2024) +## 2.8.2 (June 21, 2024) * resource/xray_custom_issue: Migrate from SDKv2 to Plugin Framework. PR: [#207](https://github.com/jfrog/terraform-provider-xray/pull/207) * resource/xray_ignore_rule: Migrate from SDKv2 to Plugin Framework. PR: [#209](https://github.com/jfrog/terraform-provider-xray/pull/209) From 4804addff590537ad418d60c09e64e48fbf3d67e Mon Sep 17 00:00:00 2001 From: Alex Hung Date: Fri, 21 Jun 2024 10:13:45 -0700 Subject: [PATCH 4/5] Ensure optional description doesn't cause state drift --- pkg/xray/resource/resource_xray_watch.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pkg/xray/resource/resource_xray_watch.go b/pkg/xray/resource/resource_xray_watch.go index a62c7dde..92d87478 100644 --- a/pkg/xray/resource/resource_xray_watch.go +++ b/pkg/xray/resource/resource_xray_watch.go @@ -374,7 +374,10 @@ func (m *WatchResourceModel) fromAPIModel(ctx context.Context, apiModel WatchAPI diags := diag.Diagnostics{} m.Name = types.StringValue(apiModel.GeneralData.Name) - m.Description = types.StringValue(apiModel.GeneralData.Description) + m.Description = types.StringNull() + if len(apiModel.GeneralData.Description) > 0 { + m.Description = types.StringValue(apiModel.GeneralData.Description) + } m.Active = types.BoolValue(apiModel.GeneralData.Active) watchResources := lo.Map( From 10e0355bbe3877274cdeef37d6625b85bcaf4fc2 Mon Sep 17 00:00:00 2001 From: JFrog CI Date: Fri, 21 Jun 2024 17:45:21 +0000 Subject: [PATCH 5/5] 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 b5c895de..722bfe98 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -## 2.8.2 (June 21, 2024) +## 2.8.2 (June 21, 2024). Tested on Artifactory 7.84.15 and Xray 3.96.1 with Terraform 1.8.5 and OpenTofu 1.7.2 * resource/xray_custom_issue: Migrate from SDKv2 to Plugin Framework. PR: [#207](https://github.com/jfrog/terraform-provider-xray/pull/207) * resource/xray_ignore_rule: Migrate from SDKv2 to Plugin Framework. PR: [#209](https://github.com/jfrog/terraform-provider-xray/pull/209)