diff --git a/CHANGELOG.md b/CHANGELOG.md index 99b415b3..d93e22cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## 2.7.0 (May 29, 2024). Tested on Artifactory 7.84.12 and Xray 3.96.1 + +FEATURES: + +* resource/xray_binary_manager_repos and resource/xray_binary_manager_builds: Add new resources to support adding repositories or builds to binary manager indexing configuration. PR: [#194](https://github.com/jfrog/terraform-provider-xray/pull/194) Issue: [#129](https://github.com/jfrog/terraform-provider-xray/issues/129) + ## 2.6.0 (May 6, 2024). Tested on Artifactory 7.84.11 and Xray 3.95.7 FEATURES: diff --git a/GNUmakefile b/GNUmakefile index d52afa93..3fce914f 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -59,7 +59,7 @@ fmt: @go fmt ./... doc: - rm docs/debug.md + rm -f docs/debug.md go generate .PHONY: build fmt diff --git a/docs/resources/binary_manager_builds.md b/docs/resources/binary_manager_builds.md new file mode 100644 index 00000000..d65f7004 --- /dev/null +++ b/docs/resources/binary_manager_builds.md @@ -0,0 +1,29 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "xray_binary_manager_builds Resource - terraform-provider-xray" +subcategory: "" +description: |- + Provides an Xray Binary Manager Builds Indexing configuration resource. See Indexing Xray Resources https://jfrog.com/help/r/jfrog-security-documentation/add-or-remove-resources-from-indexing and REST API https://jfrog.com/help/r/xray-rest-apis/update-builds-indexing-configuration for more details. +--- + +# xray_binary_manager_builds (Resource) + +Provides an Xray Binary Manager Builds Indexing configuration resource. See [Indexing Xray Resources](https://jfrog.com/help/r/jfrog-security-documentation/add-or-remove-resources-from-indexing) and [REST API](https://jfrog.com/help/r/xray-rest-apis/update-builds-indexing-configuration) for more details. + + + + +## Schema + +### Required + +- `id` (String) ID of the binary manager, e.g. 'default' +- `indexed_builds` (Set of String) Builds to be indexed. + +### Optional + +- `project_key` (String) For Xray version 3.21.2 and above with Projects, a Project Admin with Index Resources privilege can maintain the indexed and not indexed repositories in a given binary manger using this resource in the scope of a project. + +### Read-Only + +- `non_indexed_builds` (Set of String) Non-indexed builds for output. diff --git a/docs/resources/binary_manager_repos.md b/docs/resources/binary_manager_repos.md new file mode 100644 index 00000000..9676fad0 --- /dev/null +++ b/docs/resources/binary_manager_repos.md @@ -0,0 +1,48 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "xray_binary_manager_repos Resource - terraform-provider-xray" +subcategory: "" +description: |- + Provides an Xray Binary Manager Repository Indexing configuration resource. See Indexing Xray Resources https://jfrog.com/help/r/jfrog-security-documentation/add-or-remove-resources-from-indexing and REST API https://jfrog.com/help/r/xray-rest-apis/update-repos-indexing-configuration for more details. +--- + +# xray_binary_manager_repos (Resource) + +Provides an Xray Binary Manager Repository Indexing configuration resource. See [Indexing Xray Resources](https://jfrog.com/help/r/jfrog-security-documentation/add-or-remove-resources-from-indexing) and [REST API](https://jfrog.com/help/r/xray-rest-apis/update-repos-indexing-configuration) for more details. + + + + +## Schema + +### Required + +- `id` (String) ID of the binary manager, e.g. 'default' +- `indexed_repos` (Attributes Set) Repositories to be indexed. (see [below for nested schema](#nestedatt--indexed_repos)) + +### Optional + +- `project_key` (String) For Xray version 3.21.2 and above with Projects, a Project Admin with Index Resources privilege can maintain the indexed and not indexed repositories in a given binary manger using this resource in the scope of a project. + +### Read-Only + +- `non_indexed_repos` (Attributes Set) Non-indexed repositories for output. (see [below for nested schema](#nestedatt--non_indexed_repos)) + + +### Nested Schema for `indexed_repos` + +Required: + +- `name` (String) Name of the repository +- `package_type` (String) Artifactory package type. Valid value: Alpine, Bower, Cargo, Chef, Cocoapods, Composer, Conan, Conda, Cran, Debian, Docker, Gems, Generic, Gitlfs, Go, Gradle, Helm, Ivy, Maven, Npm, Nuget, Opkg, P2, Puppet, Pypi, Rpm, Sbt, Swift, Terraform, Terraformbackend, Vagrant, Vcs +- `type` (String) Repository type. Valid value: local, remote, federated + + + +### Nested Schema for `non_indexed_repos` + +Required: + +- `name` (String) +- `package_type` (String) +- `type` (String) diff --git a/go.mod b/go.mod index c90f14ac..7d09b0a9 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ module github.com/jfrog/terraform-provider-xray require ( github.com/go-resty/resty/v2 v2.13.1 - github.com/hashicorp/go-version v1.6.0 + github.com/hashicorp/go-version v1.7.0 github.com/hashicorp/terraform-plugin-docs v0.19.2 github.com/hashicorp/terraform-plugin-framework v1.8.0 github.com/hashicorp/terraform-plugin-framework-validators v0.12.0 @@ -14,9 +14,10 @@ require ( github.com/hashicorp/terraform-plugin-mux v0.16.0 github.com/hashicorp/terraform-plugin-sdk/v2 v2.34.0 github.com/hashicorp/terraform-plugin-testing v1.5.1 - github.com/jfrog/terraform-provider-shared v1.25.2 + github.com/jfrog/terraform-provider-shared v1.25.3 github.com/samber/lo v1.39.0 golang.org/x/exp v0.0.0-20230809150735-7b3493d9a819 + golang.org/x/text v0.15.0 ) require ( @@ -79,7 +80,6 @@ require ( golang.org/x/mod v0.16.0 // indirect golang.org/x/net v0.25.0 // indirect golang.org/x/sys v0.20.0 // indirect - golang.org/x/text v0.15.0 // indirect golang.org/x/tools v0.13.0 // indirect google.golang.org/appengine v1.6.8 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de // indirect diff --git a/go.sum b/go.sum index 91c7f796..7917b682 100644 --- a/go.sum +++ b/go.sum @@ -89,8 +89,8 @@ github.com/hashicorp/go-plugin v1.6.0/go.mod h1:lBS5MtSSBZk0SHc66KACcjjlU6WzEVP/ github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek= -github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= +github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/hc-install v0.6.4 h1:QLqlM56/+SIIGvGcfFiwMY3z5WGXT066suo/v9Km8e0= github.com/hashicorp/hc-install v0.6.4/go.mod h1:05LWLy8TD842OtgcfBbOT0WMoInBMUSHjmDx10zuBIA= github.com/hashicorp/hcl/v2 v2.20.1 h1:M6hgdyz7HYt1UN9e61j+qKJBqR3orTWbI1HKBJEdxtc= @@ -130,8 +130,8 @@ github.com/imdario/mergo v0.3.15 h1:M8XP7IuFNsqUx6VPK2P9OSmsYsI/YFaGil0uD21V3dM= github.com/imdario/mergo v0.3.15/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= -github.com/jfrog/terraform-provider-shared v1.25.2 h1:OdomF5La2wXEeI7k9muP6d2oclsZI5sgOldkEpVD2sc= -github.com/jfrog/terraform-provider-shared v1.25.2/go.mod h1:L987Z8XO4cuv7ys4Tw6sP/LESw7z0Dji0U2ysR8FUP4= +github.com/jfrog/terraform-provider-shared v1.25.3 h1:0P1H5WkNmhrXvXAo5pY1XkVn81CkZxcttDqZTESlKBw= +github.com/jfrog/terraform-provider-shared v1.25.3/go.mod h1:QthwPRUALElMt2RTGqoeB/3Vztx626YPBzIAoqEp0w0= github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c= github.com/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gavG9e0Q693nKo= github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= diff --git a/pkg/xray/provider/framework.go b/pkg/xray/provider/framework.go index 7394d5b8..37ae7f50 100644 --- a/pkg/xray/provider/framework.go +++ b/pkg/xray/provider/framework.go @@ -184,6 +184,8 @@ func (p *XrayProvider) Configure(ctx context.Context, req provider.ConfigureRequ // Resources satisfies the provider.Provider interface for ArtifactoryProvider. func (p *XrayProvider) Resources(ctx context.Context) []func() resource.Resource { return []func() resource.Resource{ + xray_resource.NewBinaryManagerReposResource, + xray_resource.NewBinaryManagerBuildsResource, xray_resource.NewSettingsResource, xray_resource.NewWebhookResource, xray_resource.NewWorkersCountResource, diff --git a/pkg/xray/resource/resource_xray_binary_manager_builds.go b/pkg/xray/resource/resource_xray_binary_manager_builds.go new file mode 100644 index 00000000..6d283a99 --- /dev/null +++ b/pkg/xray/resource/resource_xray_binary_manager_builds.go @@ -0,0 +1,322 @@ +package xray + +import ( + "context" + "strings" + + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "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/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" +) + +const BinaryManagerBuildsEndpoint = "xray/api/v1/binMgr/{id}/builds" + +var _ resource.Resource = &WebhookResource{} + +func NewBinaryManagerBuildsResource() resource.Resource { + return &BinaryManagerBuildsResource{} +} + +type BinaryManagerBuildsResource struct { + ProviderData util.ProviderMetadata + TypeName string +} + +func (r *BinaryManagerBuildsResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_binary_manager_builds" + r.TypeName = resp.TypeName +} + +type BinaryManagerBuildsResourceModel struct { + ID types.String `tfsdk:"id"` + ProjectKey types.String `tfsdk:"project_key"` + IndexedBuilds types.Set `tfsdk:"indexed_builds"` + NonIndexedBuilds types.Set `tfsdk:"non_indexed_builds"` +} + +func (m BinaryManagerBuildsResourceModel) toAPIModel(ctx context.Context, apiModel *BinaryManagerBuildsAPIModel) (ds diag.Diagnostics) { + var indexedBuilds []string + ds.Append(m.IndexedBuilds.ElementsAs(ctx, &indexedBuilds, false)...) + + *apiModel = BinaryManagerBuildsAPIModel{ + BinManagerID: m.ID.ValueString(), + IndexedBuilds: indexedBuilds, + } + + return +} + +func (m *BinaryManagerBuildsResourceModel) fromAPIModel(ctx context.Context, apiModel BinaryManagerBuildsAPIModel) (ds diag.Diagnostics) { + m.ID = types.StringValue(apiModel.BinManagerID) + + indexedBuilds, d := types.SetValueFrom(ctx, types.StringType, apiModel.IndexedBuilds) + if d != nil { + ds.Append(d...) + } + m.IndexedBuilds = indexedBuilds + + nonIndexedBuilds, d := types.SetValueFrom(ctx, types.StringType, apiModel.NonIndexedBuilds) + if d != nil { + ds.Append(d...) + } + m.NonIndexedBuilds = nonIndexedBuilds + + return +} + +type BinaryManagerBuildsAPIModel struct { + BinManagerID string `json:"bin_mgr_id"` + IndexedBuilds []string `json:"indexed_builds"` + NonIndexedBuilds []string `json:"non_indexed_builds"` +} + +func (r *BinaryManagerBuildsResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Required: true, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Description: "ID of the binary manager, e.g. 'default'", + }, + "project_key": schema.StringAttribute{ + Optional: true, + Validators: []validator.String{ + validatorfw_string.ProjectKey(), + }, + Description: "For Xray version 3.21.2 and above with Projects, a Project Admin with Index Resources privilege can maintain the indexed and not indexed repositories in a given binary manger using this resource in the scope of a project.", + }, + "indexed_builds": schema.SetAttribute{ + ElementType: types.StringType, + Required: true, + Description: "Builds to be indexed.", + }, + "non_indexed_builds": schema.SetAttribute{ + ElementType: types.StringType, + Computed: true, + Description: "Non-indexed builds for output.", + }, + }, + Description: "Provides an Xray Binary Manager Builds Indexing configuration resource. See [Indexing Xray Resources](https://jfrog.com/help/r/jfrog-security-documentation/add-or-remove-resources-from-indexing) " + + "and [REST API](https://jfrog.com/help/r/xray-rest-apis/update-builds-indexing-configuration) for more details.", + } +} + +func (r *BinaryManagerBuildsResource) 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 *BinaryManagerBuildsResource) 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 BinaryManagerBuildsResourceModel + + // Read Terraform plan data into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + request, err := getRestyRequest(r.ProviderData.Client, plan.ProjectKey.ValueString()) + if err != nil { + resp.Diagnostics.AddError( + "failed to get Resty client", + err.Error(), + ) + return + } + + var builds BinaryManagerBuildsAPIModel + resp.Diagnostics.Append(plan.toAPIModel(ctx, &builds)...) + if resp.Diagnostics.HasError() { + return + } + + response, err := request. + SetPathParam("id", plan.ID.ValueString()). + SetBody(builds). + Put(BinaryManagerBuildsEndpoint) + if err != nil { + utilfw.UnableToCreateResourceError(resp, err.Error()) + return + } + if response.IsError() { + utilfw.UnableToCreateResourceError(resp, response.String()) + return + } + + // get the indexed and non-indexed repos list since the PUT + // doesn't return the list + response, err = request. + SetPathParam("id", plan.ID.ValueString()). + SetResult(&builds). + Get(BinaryManagerBuildsEndpoint) + if err != nil { + utilfw.UnableToCreateResourceError(resp, err.Error()) + return + } + + if response.IsError() { + utilfw.UnableToCreateResourceError(resp, response.String()) + return + } + + resp.Diagnostics.Append(plan.fromAPIModel(ctx, builds)...) + if resp.Diagnostics.HasError() { + return + } + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *BinaryManagerBuildsResource) 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 BinaryManagerBuildsResourceModel + + // 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 builds BinaryManagerBuildsAPIModel + + response, err := request. + SetPathParam("id", state.ID.ValueString()). + SetResult(&builds). + Get(BinaryManagerBuildsEndpoint) + if err != nil { + utilfw.UnableToRefreshResourceError(resp, err.Error()) + return + } + + if response.IsError() { + utilfw.UnableToRefreshResourceError(resp, response.String()) + return + } + + resp.Diagnostics.Append(state.fromAPIModel(ctx, builds)...) + if resp.Diagnostics.HasError() { + return + } + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *BinaryManagerBuildsResource) 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 BinaryManagerBuildsResourceModel + + // Read Terraform plan data into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + request, err := getRestyRequest(r.ProviderData.Client, plan.ProjectKey.ValueString()) + if err != nil { + resp.Diagnostics.AddError( + "failed to get Resty client", + err.Error(), + ) + return + } + + var builds BinaryManagerBuildsAPIModel + resp.Diagnostics.Append(plan.toAPIModel(ctx, &builds)...) + if resp.Diagnostics.HasError() { + return + } + + response, err := request. + SetPathParam("id", plan.ID.ValueString()). + SetBody(builds). + Put(BinaryManagerBuildsEndpoint) + if err != nil { + utilfw.UnableToUpdateResourceError(resp, err.Error()) + return + } + if response.IsError() { + utilfw.UnableToUpdateResourceError(resp, response.String()) + return + } + + // get the indexed and non-indexed repos list since the PUT + // doesn't return the list + response, err = request. + SetPathParam("id", plan.ID.ValueString()). + SetResult(&builds). + Get(BinaryManagerBuildsEndpoint) + if err != nil { + utilfw.UnableToUpdateResourceError(resp, err.Error()) + return + } + + if response.IsError() { + utilfw.UnableToUpdateResourceError(resp, response.String()) + return + } + + resp.Diagnostics.Append(plan.fromAPIModel(ctx, builds)...) + if resp.Diagnostics.HasError() { + return + } + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *BinaryManagerBuildsResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + go util.SendUsageResourceDelete(ctx, r.ProviderData.Client.R(), r.ProviderData.ProductId, r.TypeName) + + resp.Diagnostics.AddWarning( + "Repository indexing configuration cannot be deleted", + "The resource is deleted from Terraform but the repository indexing configuration remains unchanged in Xray.", + ) + + // 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 *BinaryManagerBuildsResource) 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("id"), parts[0])...) + } + + if len(parts) == 2 && parts[1] != "" { + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_key"), parts[1])...) + } +} diff --git a/pkg/xray/resource/resource_xray_binary_manager_builds_test.go b/pkg/xray/resource/resource_xray_binary_manager_builds_test.go new file mode 100644 index 00000000..0083a819 --- /dev/null +++ b/pkg/xray/resource/resource_xray_binary_manager_builds_test.go @@ -0,0 +1,282 @@ +package xray_test + +import ( + "fmt" + "testing" + "time" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" + "github.com/jfrog/terraform-provider-shared/testutil" + "github.com/jfrog/terraform-provider-shared/util" + "github.com/jfrog/terraform-provider-xray/pkg/acctest" + "github.com/samber/lo" +) + +func uploadBuild(t *testing.T, name, number, projectKey string) error { + type Build struct { + Version string `json:"version"` + Name string `json:"name"` + Number string `json:"number"` + Started string `json:"started"` + } + + build := Build{ + Version: "1.0.1", + Name: name, + Number: number, + Started: time.Now().Format("2006-01-02T15:04:05.000Z0700"), + } + + restyClient := acctest.GetTestResty(t) + + req := restyClient.R() + + if projectKey != "" { + req.SetQueryParam("project", projectKey) + } + + res, err := req. + SetBody(build). + Put("artifactory/api/build") + + if err != nil { + return err + } + + if res.IsError() { + return fmt.Errorf("%s", res.String()) + } + + return nil +} + +func deleteBuild(t *testing.T, name, number, projectKey string) error { + type Build struct { + Name string `json:"buildName"` + BuildRepo string `json:"buildRepo"` + DeleteAll bool `json:"deleteAll"` + } + + build := Build{ + Name: name, + DeleteAll: true, + } + + restyClient := acctest.GetTestResty(t) + + req := restyClient.R() + + if projectKey != "" { + build.BuildRepo = fmt.Sprintf("%s-build-info", projectKey) + } + + res, err := req. + SetBody(build). + Post("artifactory/api/build/delete") + + if err != nil { + return err + } + + if res.IsError() { + return fmt.Errorf("%s", res.String()) + } + + return nil +} + +func TestAccBinaryManagerBuilds_full(t *testing.T) { + _, fqrn, resourceName := testutil.MkNames("test-bin-mgr-builds", "xray_binary_manager_builds") + + build1Name := fmt.Sprintf("test-build-%d", testutil.RandomInt()) + build2Name := fmt.Sprintf("test-build-%d", testutil.RandomInt()) + + const template = ` + resource "xray_binary_manager_builds" "{{ .name }}" { + id = "default" + indexed_builds = ["{{ .build1Name }}"] + } + ` + + testData := map[string]string{ + "name": resourceName, + "build1Name": build1Name, + } + + config := util.ExecuteTemplate("TestAccBinaryManagerBuilds_full", template, testData) + + const updateTemplate = ` + resource "xray_binary_manager_builds" "{{ .name }}" { + id = "default" + indexed_builds = ["{{ .build1Name }}", "{{ .build2Name }}"] + } + + ` + updatedTestData := map[string]string{ + "name": resourceName, + "build1Name": build1Name, + "build2Name": build2Name, + } + updatedConfig := util.ExecuteTemplate("TestAccBinaryManagerBuilds_full", updateTemplate, updatedTestData) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(t) + if err := uploadBuild(t, build1Name, "1", ""); err != nil { + t.Fatalf("failed to upload build: %s", err) + } + if err := uploadBuild(t, build2Name, "1", ""); err != nil { + t.Fatalf("failed to upload build: %s", err) + } + }, + ProtoV6ProviderFactories: acctest.ProtoV6ProviderFactories, + ExternalProviders: map[string]resource.ExternalProvider{ + "artifactory": { + Source: "jfrog/artifactory", + }, + }, + CheckDestroy: func(*terraform.State) error { + if err := deleteBuild(t, build1Name, "1", ""); err != nil { + return err + } + + if err := deleteBuild(t, build2Name, "1", ""); err != nil { + return nil + } + + return nil + }, + Steps: []resource.TestStep{ + { + Config: config, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(fqrn, "id", "default"), + resource.TestCheckResourceAttr(fqrn, "indexed_builds.#", "1"), + resource.TestCheckResourceAttr(fqrn, "indexed_builds.0", build1Name), + ), + }, + { + Config: updatedConfig, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(fqrn, "id", "default"), + resource.TestCheckResourceAttr(fqrn, "indexed_builds.#", "2"), + resource.TestCheckTypeSetElemAttr(fqrn, "indexed_builds.*", build1Name), + resource.TestCheckTypeSetElemAttr(fqrn, "indexed_builds.*", build2Name), + ), + }, + { + ResourceName: fqrn, + ImportState: true, + ImportStateId: resourceName, + ImportStateVerify: true, + ImportStateVerifyIdentifierAttribute: "id", + }, + }, + }) +} + +func TestAccBinaryManagerBuilds_project_full(t *testing.T) { + _, fqrn, resourceName := testutil.MkNames("test-bin-mgr-builds", "xray_binary_manager_builds") + + projectKey := lo.RandomString(6, lo.LowerCaseLettersCharset) + + build1Name := fmt.Sprintf("test-build-%d", testutil.RandomInt()) + build2Name := fmt.Sprintf("test-build-%d", testutil.RandomInt()) + + const template = ` + resource "xray_binary_manager_builds" "{{ .name }}" { + id = "default" + project_key = "{{ .projectKey }}" + indexed_builds = ["{{ .build1Name }}"] + } + ` + + testData := map[string]string{ + "name": resourceName, + "build1Name": build1Name, + "projectKey": projectKey, + } + + config := util.ExecuteTemplate("TestAccBinaryManagerBuilds_full", template, testData) + + const updateTemplate = ` + resource "xray_binary_manager_builds" "{{ .name }}" { + id = "default" + project_key = "{{ .projectKey }}" + indexed_builds = ["{{ .build1Name }}", "{{ .build2Name }}"] + } + + ` + updatedTestData := map[string]string{ + "name": resourceName, + "build1Name": build1Name, + "build2Name": build2Name, + "projectKey": projectKey, + } + + updatedConfig := util.ExecuteTemplate("TestAccBinaryManagerBuilds_full", updateTemplate, updatedTestData) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(t) + acctest.CreateProject(t, projectKey) + if err := uploadBuild(t, build1Name, "1", projectKey); err != nil { + t.Fatalf("failed to upload build: %s", err) + } + if err := uploadBuild(t, build2Name, "1", projectKey); err != nil { + t.Fatalf("failed to upload build: %s", err) + } + }, + ProtoV6ProviderFactories: acctest.ProtoV6ProviderFactories, + ExternalProviders: map[string]resource.ExternalProvider{ + "artifactory": { + Source: "jfrog/artifactory", + }, + "project": { + Source: "jfrog/project", + }, + }, + CheckDestroy: func(*terraform.State) error { + if err := deleteBuild(t, build1Name, "1", projectKey); err != nil { + return err + } + + if err := deleteBuild(t, build2Name, "1", projectKey); err != nil { + return nil + } + + acctest.DeleteProject(t, projectKey) + + return nil + }, + Steps: []resource.TestStep{ + { + Config: config, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(fqrn, "id", "default"), + resource.TestCheckResourceAttr(fqrn, "project_key", projectKey), + resource.TestCheckResourceAttr(fqrn, "indexed_builds.#", "1"), + resource.TestCheckResourceAttr(fqrn, "indexed_builds.0", build1Name), + ), + }, + { + Config: updatedConfig, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(fqrn, "id", "default"), + resource.TestCheckResourceAttr(fqrn, "project_key", projectKey), + resource.TestCheckResourceAttr(fqrn, "indexed_builds.#", "2"), + resource.TestCheckTypeSetElemAttr(fqrn, "indexed_builds.*", build1Name), + resource.TestCheckTypeSetElemAttr(fqrn, "indexed_builds.*", build2Name), + ), + }, + { + ResourceName: fqrn, + ImportState: true, + ImportStateId: fmt.Sprintf("%s:%s", resourceName, projectKey), + ImportStateVerify: true, + ImportStateVerifyIdentifierAttribute: "id", + }, + }, + }) +} diff --git a/pkg/xray/resource/resource_xray_binary_manager_repos.go b/pkg/xray/resource/resource_xray_binary_manager_repos.go new file mode 100644 index 00000000..99ff8d3b --- /dev/null +++ b/pkg/xray/resource/resource_xray_binary_manager_repos.go @@ -0,0 +1,422 @@ +package xray + +import ( + "context" + "fmt" + "strings" + + "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/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-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/text/cases" + "golang.org/x/text/language" +) + +const BinaryManagerReposEndpoint = "xray/api/v1/binMgr/{id}/repos" + +var _ resource.Resource = &WebhookResource{} + +func NewBinaryManagerReposResource() resource.Resource { + return &BinaryManagerReposResource{} +} + +type BinaryManagerReposResource struct { + ProviderData util.ProviderMetadata + TypeName string +} + +func (r *BinaryManagerReposResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_binary_manager_repos" + r.TypeName = resp.TypeName +} + +type BinaryManagerReposResourceModel struct { + ID types.String `tfsdk:"id"` + ProjectKey types.String `tfsdk:"project_key"` + IndexedRepos types.Set `tfsdk:"indexed_repos"` + NonIndexedRepos types.Set `tfsdk:"non_indexed_repos"` +} + +func (m BinaryManagerReposResourceModel) toAPIModel(apiModel *BinaryManagerReposAPIModel) diag.Diagnostics { + var mapRepo = func(elem attr.Value, _ int) BinaryManagerRepoAPIModel { + attrs := elem.(types.Object).Attributes() + + return BinaryManagerRepoAPIModel{ + Name: attrs["name"].(types.String).ValueString(), + Type: attrs["type"].(types.String).ValueString(), + PackageType: attrs["package_type"].(types.String).ValueString(), + } + } + + indexedRepos := lo.Map( + m.IndexedRepos.Elements(), + mapRepo, + ) + + *apiModel = BinaryManagerReposAPIModel{ + BinManagerID: m.ID.ValueString(), + IndexedRepos: indexedRepos, + } + + return nil +} + +var repoResourceModelAttributeTypes map[string]attr.Type = map[string]attr.Type{ + "name": types.StringType, + "type": types.StringType, + "package_type": types.StringType, +} + +var repoSetResourceModelAttributeTypes types.ObjectType = types.ObjectType{ + AttrTypes: repoResourceModelAttributeTypes, +} + +func (m *BinaryManagerReposResourceModel) fromAPIModel(apiModel BinaryManagerReposAPIModel) diag.Diagnostics { + diags := diag.Diagnostics{} + + m.ID = types.StringValue(apiModel.BinManagerID) + + indexedRepos, ds := m.fromRepoAPIModel(apiModel.IndexedRepos) + if ds != nil { + diags = append(diags, ds...) + } + m.IndexedRepos = indexedRepos + + nonIndexedRepos, ds := m.fromRepoAPIModel(apiModel.NonIndexedRepos) + if ds != nil { + diags = append(diags, ds...) + } + m.NonIndexedRepos = nonIndexedRepos + + return diags +} + +func (m BinaryManagerReposResourceModel) fromRepoAPIModel(repoAPIModels []BinaryManagerRepoAPIModel) (basetypes.SetValue, diag.Diagnostics) { + diags := diag.Diagnostics{} + + repos := lo.Map( + repoAPIModels, + func(property BinaryManagerRepoAPIModel, _ int) attr.Value { + repo, ds := types.ObjectValue( + repoResourceModelAttributeTypes, + map[string]attr.Value{ + "name": types.StringValue(property.Name), + "type": types.StringValue(property.Type), + "package_type": types.StringValue(property.PackageType), + }, + ) + + if ds != nil { + diags = append(diags, ds...) + } + + return repo + }, + ) + + return types.SetValue( + repoSetResourceModelAttributeTypes, + repos, + ) +} + +type BinaryManagerReposAPIModel struct { + BinManagerID string `json:"bin_mgr_id"` + IndexedRepos []BinaryManagerRepoAPIModel `json:"indexed_repos"` + NonIndexedRepos []BinaryManagerRepoAPIModel `json:"non_indexed_repos"` +} + +type BinaryManagerRepoAPIModel struct { + Name string `json:"name"` + Type string `json:"type"` + PackageType string `json:"pkg_type"` +} + +var validTitledPackageTypes = lo.Map(validPackageTypes, func(packageType string, _ int) string { + caser := cases.Title(language.AmericanEnglish, cases.NoLower) + return caser.String(packageType) +}) + +func (r *BinaryManagerReposResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Required: true, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Description: "ID of the binary manager, e.g. 'default'", + }, + "project_key": schema.StringAttribute{ + Optional: true, + Validators: []validator.String{ + validatorfw_string.ProjectKey(), + }, + Description: "For Xray version 3.21.2 and above with Projects, a Project Admin with Index Resources privilege can maintain the indexed and not indexed repositories in a given binary manger using this resource in the scope of a project.", + }, + "indexed_repos": schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "name": schema.StringAttribute{ + Required: true, + Validators: []validator.String{ + validatorfw_string.RepoKey(), + }, + Description: "Name of the repository", + }, + "type": schema.StringAttribute{ + Required: true, + Validators: []validator.String{ + stringvalidator.OneOf("local", "remote", "federated"), + }, + Description: "Repository type. Valid value: local, remote, federated", + }, + "package_type": schema.StringAttribute{ + Required: true, + Validators: []validator.String{ + stringvalidator.OneOf(validTitledPackageTypes...), + }, + Description: fmt.Sprintf("Artifactory package type. Valid value: %s", strings.Join(validTitledPackageTypes, ", ")), + }, + }, + }, + Required: true, + Description: "Repositories to be indexed.", + }, + "non_indexed_repos": schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "name": schema.StringAttribute{Required: true}, + "type": schema.StringAttribute{Required: true}, + "package_type": schema.StringAttribute{Required: true}, + }, + }, + Computed: true, + Description: "Non-indexed repositories for output.", + }, + }, + Description: "Provides an Xray Binary Manager Repository Indexing configuration resource. See [Indexing Xray Resources](https://jfrog.com/help/r/jfrog-security-documentation/add-or-remove-resources-from-indexing) " + + "and [REST API](https://jfrog.com/help/r/xray-rest-apis/update-repos-indexing-configuration) for more details.", + } +} + +func (r *BinaryManagerReposResource) 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 *BinaryManagerReposResource) 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 BinaryManagerReposResourceModel + + // Read Terraform plan data into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + request, err := getRestyRequest(r.ProviderData.Client, plan.ProjectKey.ValueString()) + if err != nil { + resp.Diagnostics.AddError( + "failed to get Resty client", + err.Error(), + ) + return + } + + var repos BinaryManagerReposAPIModel + resp.Diagnostics.Append(plan.toAPIModel(&repos)...) + if resp.Diagnostics.HasError() { + return + } + + response, err := request. + SetPathParam("id", plan.ID.ValueString()). + SetBody(repos). + Put(BinaryManagerReposEndpoint) + if err != nil { + utilfw.UnableToCreateResourceError(resp, err.Error()) + return + } + if response.IsError() { + utilfw.UnableToCreateResourceError(resp, response.String()) + return + } + + // get the indexed and non-indexed repos list since the PUT + // doesn't return the list + response, err = request. + SetPathParam("id", plan.ID.ValueString()). + SetResult(&repos). + Get(BinaryManagerReposEndpoint) + if err != nil { + utilfw.UnableToCreateResourceError(resp, err.Error()) + return + } + + if response.IsError() { + utilfw.UnableToCreateResourceError(resp, response.String()) + return + } + + resp.Diagnostics.Append(plan.fromAPIModel(repos)...) + if resp.Diagnostics.HasError() { + return + } + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *BinaryManagerReposResource) 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 BinaryManagerReposResourceModel + + // 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 repos BinaryManagerReposAPIModel + + response, err := request. + SetPathParam("id", state.ID.ValueString()). + SetResult(&repos). + Get(BinaryManagerReposEndpoint) + if err != nil { + utilfw.UnableToRefreshResourceError(resp, err.Error()) + return + } + + if response.IsError() { + utilfw.UnableToRefreshResourceError(resp, response.String()) + return + } + + resp.Diagnostics.Append(state.fromAPIModel(repos)...) + if resp.Diagnostics.HasError() { + return + } + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *BinaryManagerReposResource) 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 BinaryManagerReposResourceModel + + // Read Terraform plan data into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + request, err := getRestyRequest(r.ProviderData.Client, plan.ProjectKey.ValueString()) + if err != nil { + resp.Diagnostics.AddError( + "failed to get Resty client", + err.Error(), + ) + return + } + + var repos BinaryManagerReposAPIModel + resp.Diagnostics.Append(plan.toAPIModel(&repos)...) + if resp.Diagnostics.HasError() { + return + } + + response, err := request. + SetPathParam("id", plan.ID.ValueString()). + SetBody(repos). + Put(BinaryManagerReposEndpoint) + if err != nil { + utilfw.UnableToUpdateResourceError(resp, err.Error()) + return + } + if response.IsError() { + utilfw.UnableToUpdateResourceError(resp, response.String()) + return + } + + // get the indexed and non-indexed repos list since the PUT + // doesn't return the list + response, err = request. + SetPathParam("id", plan.ID.ValueString()). + SetResult(&repos). + Get(BinaryManagerReposEndpoint) + if err != nil { + utilfw.UnableToUpdateResourceError(resp, err.Error()) + return + } + + if response.IsError() { + utilfw.UnableToUpdateResourceError(resp, response.String()) + return + } + + resp.Diagnostics.Append(plan.fromAPIModel(repos)...) + if resp.Diagnostics.HasError() { + return + } + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *BinaryManagerReposResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + go util.SendUsageResourceDelete(ctx, r.ProviderData.Client.R(), r.ProviderData.ProductId, r.TypeName) + + resp.Diagnostics.AddWarning( + "Repository indexing configuration cannot be deleted", + "The resource is deleted from Terraform but the repository indexing configuration remains unchanged in Xray.", + ) + + // 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 *BinaryManagerReposResource) 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("id"), parts[0])...) + } + + if len(parts) == 2 && parts[1] != "" { + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_key"), parts[1])...) + } +} diff --git a/pkg/xray/resource/resource_xray_binary_manager_repos_test.go b/pkg/xray/resource/resource_xray_binary_manager_repos_test.go new file mode 100644 index 00000000..db6f1b51 --- /dev/null +++ b/pkg/xray/resource/resource_xray_binary_manager_repos_test.go @@ -0,0 +1,335 @@ +package xray_test + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/jfrog/terraform-provider-shared/testutil" + "github.com/jfrog/terraform-provider-shared/util" + "github.com/jfrog/terraform-provider-xray/pkg/acctest" + "github.com/samber/lo" +) + +func TestAccBinaryManagerRepos_full(t *testing.T) { + _, fqrn, resourceName := testutil.MkNames("test-bin-mgr-repos", "xray_binary_manager_repos") + _, _, repo1Name := testutil.MkNames("test-local-generic-repo", "artifactory_local_generic_repository") + _, _, repo2Name := testutil.MkNames("test-local-docker-repo", "artifactory_local_docker_repository") + _, _, repo3Name := testutil.MkNames("test-local-npm-repo", "artifactory_local_npm_repository") + + const template = ` + resource "artifactory_local_generic_repository" "{{ .repo1 }}" { + key = "{{ .repo1 }}" + xray_index = true + } + + resource "artifactory_local_docker_v2_repository" "{{ .repo2 }}" { + key = "{{ .repo2 }}" + xray_index = false + } + + resource "xray_binary_manager_repos" "{{ .name }}" { + id = "default" + indexed_repos = [ + { + name = artifactory_local_generic_repository.{{ .repo1 }}.key + type = "local" + package_type = "Generic" + } + ] + } + ` + + testData := map[string]string{ + "name": resourceName, + "repo1": repo1Name, + "repo2": repo2Name, + } + + config := util.ExecuteTemplate("TestAccBinaryManagerRepos_full", template, testData) + + const updateTemplate = ` + resource "artifactory_local_generic_repository" "{{ .repo1 }}" { + key = "{{ .repo1 }}" + xray_index = true + } + + resource "artifactory_local_docker_v2_repository" "{{ .repo2 }}" { + key = "{{ .repo2 }}" + xray_index = true + } + + resource "artifactory_local_npm_repository" "{{ .repo3 }}" { + key = "{{ .repo3 }}" + } + + resource "xray_binary_manager_repos" "{{ .name }}" { + id = "default" + indexed_repos = [ + { + name = artifactory_local_generic_repository.{{ .repo1 }}.key + type = "local" + package_type = "Generic" + }, + { + name = artifactory_local_docker_v2_repository.{{ .repo2 }}.key + type = "local" + package_type = "Docker" + } + ] + } + ` + updatedTestData := map[string]string{ + "name": resourceName, + "repo1": repo1Name, + "repo2": repo2Name, + "repo3": repo3Name, + } + updatedConfig := util.ExecuteTemplate("TestAccBinaryManagerRepos_full", updateTemplate, updatedTestData) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ProtoV6ProviderFactories: acctest.ProtoV6ProviderFactories, + ExternalProviders: map[string]resource.ExternalProvider{ + "artifactory": { + Source: "jfrog/artifactory", + }, + }, + Steps: []resource.TestStep{ + { + Config: config, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(fqrn, "id", "default"), + resource.TestCheckResourceAttr(fqrn, "indexed_repos.#", "1"), + resource.TestCheckResourceAttr(fqrn, "indexed_repos.0.name", repo1Name), + resource.TestCheckResourceAttr(fqrn, "indexed_repos.0.type", "local"), + resource.TestCheckResourceAttr(fqrn, "indexed_repos.0.package_type", "Generic"), + ), + }, + { + Config: updatedConfig, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(fqrn, "id", "default"), + resource.TestCheckResourceAttr(fqrn, "indexed_repos.#", "2"), + resource.TestCheckTypeSetElemNestedAttrs(fqrn, "indexed_repos.*", map[string]string{ + "name": repo1Name, + "type": "local", + "package_type": "Generic", + }), + resource.TestCheckTypeSetElemNestedAttrs(fqrn, "indexed_repos.*", map[string]string{ + "name": repo2Name, + "type": "local", + "package_type": "Docker", + }), + ), + ConfigPlanChecks: testutil.ConfigPlanChecks(""), + }, + { + ResourceName: fqrn, + ImportState: true, + ImportStateId: resourceName, + ImportStateVerify: true, + ImportStateVerifyIdentifierAttribute: "id", + }, + }, + }) +} + +func TestAccBinaryManagerRepos_project_full(t *testing.T) { + _, fqrn, resourceName := testutil.MkNames("test-bin-mgr-repos", "xray_binary_manager_repos") + _, _, repo1Name := testutil.MkNames("test-local-generic-repo", "artifactory_local_generic_repository") + _, _, repo2Name := testutil.MkNames("test-local-docker-repo", "artifactory_local_docker_repository") + _, _, repo3Name := testutil.MkNames("test-local-npm-repo", "artifactory_local_npm_repository") + _, _, projectName := testutil.MkNames("test-project", "project") + + projectKey := lo.RandomString(6, lo.LowerCaseLettersCharset) + + const template = ` + resource "artifactory_local_generic_repository" "{{ .repo1 }}" { + key = "{{ .repo1 }}" + xray_index = true + + lifecycle { + ignore_changes = ["project_key"] + } + } + + resource "artifactory_local_docker_v2_repository" "{{ .repo2 }}" { + key = "{{ .repo2 }}" + xray_index = false + + lifecycle { + ignore_changes = ["project_key"] + } + } + + resource "project" "{{ .projectName }}" { + key = "{{ .projectKey }}" + display_name = "{{ .projectName }}" + admin_privileges { + manage_members = true + manage_resources = true + index_resources = true + } + } + + resource "project_repository" "{{ .projectKey }}-{{ .repo1 }}" { + project_key = project.{{ .projectName }}.key + key = artifactory_local_generic_repository.{{ .repo1 }}.key + } + + resource "project_repository" "{{ .projectKey }}-{{ .repo2 }}" { + project_key = project.{{ .projectName }}.key + key = artifactory_local_docker_v2_repository.{{ .repo2 }}.key + } + + resource "xray_binary_manager_repos" "{{ .name }}" { + id = "default" + project_key = project.{{ .projectName }}.key + indexed_repos = [ + { + name = artifactory_local_generic_repository.{{ .repo1 }}.key + type = "local" + package_type = "Generic" + } + ] + + depends_on = [ + project_repository.{{ .projectKey }}-{{ .repo1 }}, + ] + } + ` + + testData := map[string]string{ + "name": resourceName, + "repo1": repo1Name, + "repo2": repo2Name, + "projectName": projectName, + "projectKey": projectKey, + } + + config := util.ExecuteTemplate("TestAccBinaryManagerRepos_full", template, testData) + + const updateTemplate = ` + resource "artifactory_local_generic_repository" "{{ .repo1 }}" { + key = "{{ .repo1 }}" + xray_index = true + + lifecycle { + ignore_changes = ["project_key"] + } + } + + resource "artifactory_local_docker_v2_repository" "{{ .repo2 }}" { + key = "{{ .repo2 }}" + xray_index = true + + lifecycle { + ignore_changes = ["project_key"] + } + } + + resource "project" "{{ .projectName }}" { + key = "{{ .projectKey }}" + display_name = "{{ .projectName }}" + admin_privileges { + manage_members = true + manage_resources = true + index_resources = true + } + } + + resource "project_repository" "{{ .projectKey }}-{{ .repo1 }}" { + project_key = project.{{ .projectName }}.key + key = artifactory_local_generic_repository.{{ .repo1 }}.key + } + + resource "project_repository" "{{ .projectKey }}-{{ .repo2 }}" { + project_key = project.{{ .projectName }}.key + key = artifactory_local_docker_v2_repository.{{ .repo2 }}.key + } + + resource "xray_binary_manager_repos" "{{ .name }}" { + id = "default" + project_key = project.{{ .projectName }}.key + indexed_repos = [ + { + name = artifactory_local_generic_repository.{{ .repo1 }}.key + type = "local" + package_type = "Generic" + }, + { + name = artifactory_local_docker_v2_repository.{{ .repo2 }}.key + type = "local" + package_type = "Docker" + } + ] + + depends_on = [ + project_repository.{{ .projectKey }}-{{ .repo1 }}, + project_repository.{{ .projectKey }}-{{ .repo2 }}, + ] + } + ` + updatedTestData := map[string]string{ + "name": resourceName, + "repo1": repo1Name, + "repo2": repo2Name, + "repo3": repo3Name, + "projectName": projectName, + "projectKey": projectKey, + } + updatedConfig := util.ExecuteTemplate("TestAccBinaryManagerRepos_full", updateTemplate, updatedTestData) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ProtoV6ProviderFactories: acctest.ProtoV6ProviderFactories, + ExternalProviders: map[string]resource.ExternalProvider{ + "artifactory": { + Source: "jfrog/artifactory", + }, + "project": { + Source: "jfrog/project", + }, + }, + Steps: []resource.TestStep{ + { + Config: config, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(fqrn, "id", "default"), + resource.TestCheckResourceAttr(fqrn, "project_key", projectKey), + resource.TestCheckResourceAttr(fqrn, "indexed_repos.#", "1"), + resource.TestCheckResourceAttr(fqrn, "indexed_repos.0.name", repo1Name), + resource.TestCheckResourceAttr(fqrn, "indexed_repos.0.type", "local"), + resource.TestCheckResourceAttr(fqrn, "indexed_repos.0.package_type", "Generic"), + ), + }, + { + Config: updatedConfig, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(fqrn, "id", "default"), + resource.TestCheckResourceAttr(fqrn, "project_key", projectKey), + resource.TestCheckResourceAttr(fqrn, "indexed_repos.#", "2"), + resource.TestCheckTypeSetElemNestedAttrs(fqrn, "indexed_repos.*", map[string]string{ + "name": repo1Name, + "type": "local", + "package_type": "Generic", + }), + resource.TestCheckTypeSetElemNestedAttrs(fqrn, "indexed_repos.*", map[string]string{ + "name": repo2Name, + "type": "local", + "package_type": "Docker", + }), + ), + ConfigPlanChecks: testutil.ConfigPlanChecks(""), + }, + { + ResourceName: fqrn, + ImportState: true, + ImportStateId: fmt.Sprintf("%s:%s", resourceName, projectKey), + ImportStateVerify: true, + ImportStateVerifyIdentifierAttribute: "id", + }, + }, + }) +}