diff --git a/.github/workflows/acceptance-tests.yml b/.github/workflows/acceptance-tests.yml index c1896f4..efb021f 100644 --- a/.github/workflows/acceptance-tests.yml +++ b/.github/workflows/acceptance-tests.yml @@ -31,7 +31,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v5 with: - go-version: 1.21 + go-version: 1.22 - name: Install Helm uses: azure/setup-helm@v4.2.0 - name: Install Terraform CLI diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d1c47ef..ba57d09 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -27,7 +27,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v5 with: - go-version: 1.21 + go-version: 1.22 - name: Import GPG key id: import_gpg uses: crazy-max/ghaction-import-gpg@v6 diff --git a/CHANGELOG.md b/CHANGELOG.md index f1b4841..cd757f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ -## 1.0.0 (July 9, 2024). Tested on Artifactory 7.84.17 with Terraform 1.9.1 and OpenTofu 1.7.3 +## 1.0.0 (July 16, 2024). Tested on Artifactory 7.84.17 with Terraform 1.9.2 and OpenTofu 1.7.3 FEATURES: * **New Resource:** `missioncontrol_license_bucket` PR: [#2](https://github.com/jfrog/terraform-provider-mission-control/pull/2) -* **New Resource:** `missioncontrol_jpd` PR: [#3](https://github.com/jfrog/terraform-provider-mission-control/pull/3) \ No newline at end of file +* **New Resource:** `missioncontrol_jpd` PR: [#3](https://github.com/jfrog/terraform-provider-mission-control/pull/3) +* **New Resource:** `missioncontrol_access_federation_star` and `missioncontrol_access_federation_mesh` PR: [#8](https://github.com/jfrog/terraform-provider-mission-control/pull/8) \ No newline at end of file diff --git a/docs/resources/access_federation_mesh.md b/docs/resources/access_federation_mesh.md new file mode 100644 index 0000000..670609b --- /dev/null +++ b/docs/resources/access_federation_mesh.md @@ -0,0 +1,44 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "missioncontrol_access_federation_mesh Resource - missioncontrol" +subcategory: "" +description: |- + Provides a JFrog Access Federation https://jfrog.com/help/r/jfrog-platform-administration-documentation/access-federation resource to setup Mesh Topology. + ~>The source and targets must have been configured properly for Access Federation https://jfrog.com/help/r/jfrog-platform-administration-documentation/access-federation. + ~>Deletion is currently not supported via REST API. This must be done using JFrog UI. +--- + +# missioncontrol_access_federation_mesh (Resource) + +Provides a [JFrog Access Federation](https://jfrog.com/help/r/jfrog-platform-administration-documentation/access-federation) resource to setup Mesh Topology. +~>The source and targets must have been configured properly for [Access Federation](https://jfrog.com/help/r/jfrog-platform-administration-documentation/access-federation). +~>**Deletion** is currently not supported via REST API. This must be done using JFrog UI. + +## Example Usage + +```terraform +resource "missioncontrol_access_federation_mesh" "my-mesh" { + ids = ["JPD-1", "JPD-2"] + entities = ["USERS", "GROUPS", "PERMISSIONS"] +} +``` + + +## Schema + +### Required + +- `entities` (Set of String) Entity types to sync. Allow values: `USERS`, `GROUPS`, `PERMISSIONS`, `TOKENS` +- `ids` (Set of String) IDs for the source Platform Deployment. Use [Get Access Federation Candidate API](https://jfrog.com/help/r/jfrog-rest-apis/get-access-federation-candidates) to get a list of ID. Must have at least 2 items. + +### Read-Only + +- `id` (String) The ID of this resource. + +## Import + +Import is supported using the following syntax: + +```shell +terraform import missioncontrol_access_federation_mesh.my-mesh JPD-1:JPD-2 +``` diff --git a/docs/resources/access_federation_star.md b/docs/resources/access_federation_star.md new file mode 100644 index 0000000..669cb7e --- /dev/null +++ b/docs/resources/access_federation_star.md @@ -0,0 +1,70 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "missioncontrol_access_federation_star Resource - missioncontrol" +subcategory: "" +description: |- + Provides a JFrog Access Federation https://jfrog.com/help/r/jfrog-platform-administration-documentation/access-federation resource to setup Star Topology. + ~>The source and targets must have been configured properly for Access Federation https://jfrog.com/help/r/jfrog-platform-administration-documentation/access-federation. + ~>Deletion is currently not supported via REST API. This must be done using JFrog UI. +--- + +# missioncontrol_access_federation_star (Resource) + +Provides a [JFrog Access Federation](https://jfrog.com/help/r/jfrog-platform-administration-documentation/access-federation) resource to setup Star Topology. +~>The source and targets must have been configured properly for [Access Federation](https://jfrog.com/help/r/jfrog-platform-administration-documentation/access-federation). +~>**Deletion** is currently not supported via REST API. This must be done using JFrog UI. + +## Example Usage + +```terraform +resource "missioncontrol_access_federation_star" "my-star" { + id = "JPD-1" + entities = ["USERS", "GROUPS", "PERMISSIONS"] + targets = [ + { + id = "JPD-2" + url = "http://myartifactory-2.jfrog.io/access" + permission_filters = { + include_patterns = ["some-regex"] + } + }, + ] +} +``` + + +## Schema + +### Required + +- `entities` (Set of String) Entity types to sync. Allow values: `USERS`, `GROUPS`, `PERMISSIONS`, `TOKENS` +- `id` (String) ID for the source Platform Deployment. Use [Get Access Federation Candidate API](https://jfrog.com/help/r/jfrog-rest-apis/get-access-federation-candidates) to get a list of ID. +- `targets` (Attributes Set) Target JPD (see [below for nested schema](#nestedatt--targets)) + + +### Nested Schema for `targets` + +Required: + +- `id` (String) ID of the targeted Platform Deployment +- `url` (String) Target Platform deployment URL: http://:/access; for example: http://myplatformserver:8082/access. + +Optional: + +- `permission_filters` (Attributes) When assigning entity types to targets, you can assign specific permissions to be synchronized using the `include_patterns`/`exclude_patterns` regular expressions. (see [below for nested schema](#nestedatt--targets--permission_filters)) + + +### Nested Schema for `targets.permission_filters` + +Optional: + +- `exclude_patterns` (Set of String) +- `include_patterns` (Set of String) + +## Import + +Import is supported using the following syntax: + +```shell +terraform import missioncontrol_access_federation_star.my-star JPD-1 +``` diff --git a/docs/resources/jpd.md b/docs/resources/jpd.md new file mode 100644 index 0000000..40a2024 --- /dev/null +++ b/docs/resources/jpd.md @@ -0,0 +1,91 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "missioncontrol_jpd Resource - missioncontrol" +subcategory: "" +description: |- + Provides a JFrog Platform Deployment https://jfrog.com/help/r/jfrog-platform-administration-documentation/manage-platform-deployments resource to manage JPD. + ~>Supported on the Self-Hosted platform, with an Enterprise X or Enterprise+ license. +--- + +# missioncontrol_jpd (Resource) + +Provides a [JFrog Platform Deployment](https://jfrog.com/help/r/jfrog-platform-administration-documentation/manage-platform-deployments) resource to manage JPD. +~>Supported on the Self-Hosted platform, with an Enterprise X or Enterprise+ license. + + + + +## Schema + +### Required + +- `location` (Attributes) The geographical location of the Platform Deployment to be displayed on a global Platform Deployment view (see [below for nested schema](#nestedatt--location)) +- `name` (String) A unique logical name for this Platform Deployment +- `url` (String) The Platform deployment URL: http://:/; for example: http://myplatformserver:8082/. Note: For legacy instances, version 6.x and lower, the URL should contain the instance root context: http://://; for example http://myv6server:8081/artifactory/. URL must ends with trailing slash. + +### Optional + +- `password` (String, Sensitive) Admin password for legacy JPD (Artifactory 6.x). +- `tags` (Set of String) Add labels to be applied for filtering Platform Deployments according to categories for example, location, dedicated centers - dev, testing, production +- `token` (String, Sensitive) JPD join key +- `username` (String) Admin username for legacy JPD (Artifactory 6.x). + +### Read-Only + +- `base_url` (String) +- `cold_storage_jpd` (String) +- `id` (String) The ID of this resource. +- `is_cold_storage` (Boolean) +- `licenses` (Attributes Set) (see [below for nested schema](#nestedatt--licenses)) +- `local` (Boolean) +- `services` (Attributes Set) (see [below for nested schema](#nestedatt--services)) +- `status` (Attributes) (see [below for nested schema](#nestedatt--status)) + + +### Nested Schema for `location` + +Required: + +- `city_name` (String) +- `country_code` (String) 2 letters ISO-3166-2 country code +- `latitude` (Number) +- `longitude` (Number) + + + +### Nested Schema for `licenses` + +Read-Only: + +- `expired` (Boolean) +- `license_hash` (String) +- `licensed_to` (String) +- `type` (String) +- `valid_through` (String) + + + +### Nested Schema for `services` + +Read-Only: + +- `status` (Attributes) (see [below for nested schema](#nestedatt--services--status)) +- `type` (String) + + +### Nested Schema for `services.status` + +Read-Only: + +- `code` (String) + + + + +### Nested Schema for `status` + +Read-Only: + +- `code` (String) +- `message` (String) +- `warnings` (Set of String) diff --git a/examples/resources/missioncontrol_access_federation_mesh/import.sh b/examples/resources/missioncontrol_access_federation_mesh/import.sh new file mode 100644 index 0000000..d19ef35 --- /dev/null +++ b/examples/resources/missioncontrol_access_federation_mesh/import.sh @@ -0,0 +1 @@ +terraform import missioncontrol_access_federation_mesh.my-mesh JPD-1:JPD-2 \ No newline at end of file diff --git a/examples/resources/missioncontrol_access_federation_mesh/resource.tf b/examples/resources/missioncontrol_access_federation_mesh/resource.tf new file mode 100644 index 0000000..0525e94 --- /dev/null +++ b/examples/resources/missioncontrol_access_federation_mesh/resource.tf @@ -0,0 +1,4 @@ +resource "missioncontrol_access_federation_mesh" "my-mesh" { + ids = ["JPD-1", "JPD-2"] + entities = ["USERS", "GROUPS", "PERMISSIONS"] +} \ No newline at end of file diff --git a/examples/resources/missioncontrol_access_federation_star/import.sh b/examples/resources/missioncontrol_access_federation_star/import.sh new file mode 100644 index 0000000..5a5d296 --- /dev/null +++ b/examples/resources/missioncontrol_access_federation_star/import.sh @@ -0,0 +1 @@ +terraform import missioncontrol_access_federation_star.my-star JPD-1 \ No newline at end of file diff --git a/examples/resources/missioncontrol_access_federation_star/resource.tf b/examples/resources/missioncontrol_access_federation_star/resource.tf new file mode 100644 index 0000000..f6d5fa8 --- /dev/null +++ b/examples/resources/missioncontrol_access_federation_star/resource.tf @@ -0,0 +1,13 @@ +resource "missioncontrol_access_federation_star" "my-star" { + id = "JPD-1" + entities = ["USERS", "GROUPS", "PERMISSIONS"] + targets = [ + { + id = "JPD-2" + url = "http://myartifactory-2.jfrog.io/access" + permission_filters = { + include_patterns = ["some-regex"] + } + }, + ] +} \ No newline at end of file diff --git a/go.mod b/go.mod index 5d354e4..d8a3de5 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/jfrog/terraform-provider-mission-control -go 1.21.5 +go 1.22.5 require ( github.com/go-resty/resty/v2 v2.13.1 @@ -8,6 +8,7 @@ require ( github.com/hashicorp/terraform-plugin-framework v1.10.0 github.com/hashicorp/terraform-plugin-framework-validators v0.13.0 github.com/hashicorp/terraform-plugin-go v0.23.0 + github.com/hashicorp/terraform-plugin-log v0.9.0 github.com/hashicorp/terraform-plugin-testing v1.9.0 github.com/jfrog/terraform-provider-shared v1.25.5 github.com/samber/lo v1.45.0 diff --git a/pkg/missioncontrol/provider.go b/pkg/missioncontrol/provider.go index 1cf973a..dee4088 100644 --- a/pkg/missioncontrol/provider.go +++ b/pkg/missioncontrol/provider.go @@ -148,6 +148,8 @@ func (p *MissionControlProvider) Resources(ctx context.Context) []func() resourc return []func() resource.Resource{ NewLicenseBucketResource, NewJPDResource, + NewAccessFederationStarResource, + NewAccessFederationMeshResource, } } diff --git a/pkg/missioncontrol/resource_access_federation_mesh.go b/pkg/missioncontrol/resource_access_federation_mesh.go new file mode 100644 index 0000000..9dac564 --- /dev/null +++ b/pkg/missioncontrol/resource_access_federation_mesh.go @@ -0,0 +1,349 @@ +package missioncontrol + +import ( + "context" + "fmt" + "strings" + + "github.com/hashicorp/terraform-plugin-framework-validators/setvalidator" + "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/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/jfrog/terraform-provider-shared/util" + utilfw "github.com/jfrog/terraform-provider-shared/util/fw" + "github.com/samber/lo" +) + +const ( + accessFederationsEndpoint = "mc/api/v1/federation" + accessFederationMeshEndpoint = "mc/api/v1/federation/create_mesh" +) + +var _ resource.Resource = &accessFederationMeshResource{} + +type accessFederationMeshResource struct { + ProviderData util.ProviderMetadata + TypeName string +} + +func NewAccessFederationMeshResource() resource.Resource { + return &accessFederationMeshResource{ + TypeName: "missioncontrol_access_federation_mesh", + } +} + +func (r *accessFederationMeshResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = r.TypeName +} + +func (r *accessFederationMeshResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ // this is necessary because TF can't use SetAttribute for state comparison + Computed: true, + }, + "ids": schema.SetAttribute{ + ElementType: types.StringType, + Required: true, + Validators: []validator.Set{ + setvalidator.SizeAtLeast(2), + setvalidator.ValueStringsAre( + stringvalidator.LengthAtLeast(1), + ), + }, + Description: "IDs for the source Platform Deployment. Use [Get Access Federation Candidate API](https://jfrog.com/help/r/jfrog-rest-apis/get-access-federation-candidates) to get a list of ID. Must have at least 2 items.", + }, + "entities": schema.SetAttribute{ + ElementType: types.StringType, + Required: true, + Validators: []validator.Set{ + setvalidator.ValueStringsAre( + stringvalidator.OneOf("USERS", "GROUPS", "PERMISSIONS", "TOKENS"), + ), + }, + Description: "Entity types to sync. Allow values: `USERS`, `GROUPS`, `PERMISSIONS`, `TOKENS`", + }, + }, + MarkdownDescription: "Provides a [JFrog Access Federation](https://jfrog.com/help/r/jfrog-platform-administration-documentation/access-federation) resource to setup Mesh Topology.\n" + + "~>The source and targets must have been configured properly for [Access Federation](https://jfrog.com/help/r/jfrog-platform-administration-documentation/access-federation).\n" + + "~>**Deletion** is currently not supported via REST API. This must be done using JFrog UI.", + } +} + +type accessFederationMeshResourceModel struct { + ID types.String `tfsdk:"id"` + IDs types.Set `tfsdk:"ids"` + Entities types.Set `tfsdk:"entities"` +} + +func (r *accessFederationMeshResourceModel) fromAPIModel(ctx context.Context, apiModel *accessFederationGetAllResponseAPIModel) (ds diag.Diagnostics) { + var ids []string + + ids = append(ids, apiModel.Source) + + targetIDs := lo.Map( + apiModel.Targets, + func(target accessFederationTargetGetAllAPIModel, _ int) string { + return target.ID + }, + ) + ids = append(ids, targetIDs...) + + idsSet, d := types.SetValueFrom(ctx, types.StringType, ids) + if d.HasError() { + ds.Append(d...) + } + r.IDs = idsSet + + r.ID = types.StringValue(strings.Join(ids, ":")) + + entitiesNested := lo.Map( + apiModel.Targets, + func(target accessFederationTargetGetAllAPIModel, _ int) []string { + return target.Entities + }, + ) + entities := lo.Uniq(lo.Flatten(entitiesNested)) + + entitiesSet, d := types.SetValueFrom(ctx, types.StringType, entities) + if d.HasError() { + ds.Append(d...) + } + r.Entities = entitiesSet + + return +} + +func (r accessFederationMeshResourceModel) toAPIModel(ctx context.Context, apiModel *accessFederationMeshRequestAPIModel) diag.Diagnostics { + ds := diag.Diagnostics{} + + var ids []string + ds.Append(r.IDs.ElementsAs(ctx, &ids, false)...) + + var entities []string + ds.Append(r.Entities.ElementsAs(ctx, &entities, false)...) + + *apiModel = accessFederationMeshRequestAPIModel{ + IDs: ids, + Entities: entities, + } + + return ds +} + +type accessFederationMeshRequestAPIModel struct { + IDs []string `json:"jpd_ids"` + Entities []string `json:"entities"` +} + +type accessFederationGetAllResponseAPIModel struct { + Source string `json:"source"` + Targets []accessFederationTargetGetAllAPIModel `json:"targets"` +} + +type accessFederationTargetGetAllAPIModel struct { + accessFederationTargetAPIModel + Entities []string `json:"entities"` +} + +func (r *accessFederationMeshResource) 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 *accessFederationMeshResource) 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 accessFederationMeshResourceModel + + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + var accessFederation accessFederationMeshRequestAPIModel + resp.Diagnostics.Append(plan.toAPIModel(ctx, &accessFederation)...) + if resp.Diagnostics.HasError() { + return + } + + var results []accessFederationResponseAPIModel + response, err := r.ProviderData.Client.R(). + SetBody(accessFederation). + SetResult(&results). + Post(accessFederationMeshEndpoint) + + if err != nil { + utilfw.UnableToCreateResourceError(resp, err.Error()) + return + } + + if response.IsError() { + utilfw.UnableToCreateResourceError(resp, response.String()) + return + } + + for _, result := range results { + tflog.Info(ctx, "Create result", map[string]interface{}{ + "label": result.Label, + "status": result.Status, + }) + } + + var ids []string + resp.Diagnostics.Append(plan.IDs.ElementsAs(ctx, &ids, false)...) + if resp.Diagnostics.HasError() { + return + } + plan.ID = types.StringValue(strings.Join(ids, ":")) + + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *accessFederationMeshResource) 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 accessFederationMeshResourceModel + + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + var accessFederations []accessFederationGetAllResponseAPIModel + response, err := r.ProviderData.Client.R(). + SetQueryParam("includeNonConfiguredJPDs", "false"). + SetResult(&accessFederations). + Get(accessFederationsEndpoint) + + if err != nil { + utilfw.UnableToRefreshResourceError(resp, err.Error()) + return + } + + if response.IsError() { + utilfw.UnableToRefreshResourceError(resp, response.String()) + return + } + + var jpdIDs []string + resp.Diagnostics.Append(state.IDs.ElementsAs(ctx, &jpdIDs, false)...) + if resp.Diagnostics.HasError() { + return + } + + sourceAccessFederation, found := lo.Find( + accessFederations, + func(accessFederation accessFederationGetAllResponseAPIModel) bool { + targetIDs := lo.Map( + accessFederation.Targets, + func(target accessFederationTargetGetAllAPIModel, _ int) string { + return target.ID + }, + ) + + return lo.Contains(jpdIDs, accessFederation.Source) && lo.Every(jpdIDs, targetIDs) + }, + ) + + if !found { + utilfw.UnableToRefreshResourceError( + resp, + fmt.Sprintf("unabled to find Access Federation Configurations for JPDs: %s", strings.Join(jpdIDs, ", ")), + ) + return + } + + // Convert from the API data model to the Terraform data model + // and refresh any attribute values. + resp.Diagnostics.Append(state.fromAPIModel(ctx, &sourceAccessFederation)...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *accessFederationMeshResource) 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 accessFederationMeshResourceModel + + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + var accessFederation accessFederationMeshRequestAPIModel + resp.Diagnostics.Append(plan.toAPIModel(ctx, &accessFederation)...) + if resp.Diagnostics.HasError() { + return + } + + var results []accessFederationResponseAPIModel + response, err := r.ProviderData.Client.R(). + SetBody(accessFederation). + SetResult(&results). + Post(accessFederationMeshEndpoint) + + if err != nil { + utilfw.UnableToUpdateResourceError(resp, err.Error()) + return + } + + if response.IsError() { + utilfw.UnableToUpdateResourceError(resp, response.String()) + return + } + + for _, result := range results { + tflog.Info(ctx, "Update result", map[string]interface{}{ + "label": result.Label, + "status": result.Status, + }) + } + + var ids []string + resp.Diagnostics.Append(plan.IDs.ElementsAs(ctx, &ids, false)...) + if resp.Diagnostics.HasError() { + return + } + plan.ID = types.StringValue(strings.Join(ids, ":")) + + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *accessFederationMeshResource) 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( + "Access Federation deletion not supported", + " The resource has be deleted from Terraform state. To delete Access Federation relationship, please use the JFrog UI.", + ) + + // 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 *accessFederationMeshResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + parts := strings.Split(req.ID, ":") + if len(parts) <= 1 { + resp.Diagnostics.AddError( + "Unexpected Import Identifier", + "Expected at least one JPD ID in the form of: jpd_id_1:jpd_id_2:...", + ) + return + } + + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), req.ID)...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("ids"), parts)...) +} diff --git a/pkg/missioncontrol/resource_access_federation_mesh_test.go b/pkg/missioncontrol/resource_access_federation_mesh_test.go new file mode 100644 index 0000000..087e5de --- /dev/null +++ b/pkg/missioncontrol/resource_access_federation_mesh_test.go @@ -0,0 +1,85 @@ +package missioncontrol_test + +import ( + "os" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/jfrog/terraform-provider-shared/testutil" + "github.com/jfrog/terraform-provider-shared/util" +) + +// To execute this test, you need setup second Artifactory instance with circle-of-trust. +// Then set them as env vars before running the test +func TestAccAccessFederationMesh_full(t *testing.T) { + var skipTest = func() (bool, string) { + if len(os.Getenv("ARTIFACTORY_URL_2")) > 0 { + return false, "Env var `ARTIFACTORY_URL_2` is set. Executing test." + } + + return true, "Env var `ARTIFACTORY_URL_2` is not set. Skipping test." + } + + if skip, reason := skipTest(); skip { + t.Skipf(reason) + } + + _, fqrn, resourceName := testutil.MkNames("test-access-federation", "missioncontrol_access_federation_mesh") + + temp := ` + resource "missioncontrol_access_federation_mesh" "{{ .name }}" { + ids = ["JPD-1", "JPD-2"] + entities = ["USERS", "GROUPS", "PERMISSIONS", "TOKENS"] + }` + + testData := map[string]string{ + "name": resourceName, + } + + config := util.ExecuteTemplate(resourceName, temp, testData) + + updatedTemp := ` + resource "missioncontrol_access_federation_mesh" "{{ .name }}" { + ids = ["JPD-1", "JPD-2"] + entities = ["USERS", "GROUPS", "PERMISSIONS"] + }` + updatedConfig := util.ExecuteTemplate(resourceName, updatedTemp, testData) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProviders(), + Steps: []resource.TestStep{ + { + Config: config, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(fqrn, "ids.#", "2"), + resource.TestCheckTypeSetElemAttr(fqrn, "ids.*", "JPD-1"), + resource.TestCheckTypeSetElemAttr(fqrn, "ids.*", "JPD-2"), + resource.TestCheckResourceAttr(fqrn, "entities.#", "4"), + resource.TestCheckTypeSetElemAttr(fqrn, "entities.*", "USERS"), + resource.TestCheckTypeSetElemAttr(fqrn, "entities.*", "GROUPS"), + resource.TestCheckTypeSetElemAttr(fqrn, "entities.*", "PERMISSIONS"), + resource.TestCheckTypeSetElemAttr(fqrn, "entities.*", "TOKENS"), + ), + }, + { + Config: updatedConfig, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(fqrn, "ids.#", "2"), + resource.TestCheckTypeSetElemAttr(fqrn, "ids.*", "JPD-1"), + resource.TestCheckTypeSetElemAttr(fqrn, "ids.*", "JPD-2"), + resource.TestCheckResourceAttr(fqrn, "entities.#", "3"), + resource.TestCheckTypeSetElemAttr(fqrn, "entities.*", "USERS"), + resource.TestCheckTypeSetElemAttr(fqrn, "entities.*", "GROUPS"), + resource.TestCheckTypeSetElemAttr(fqrn, "entities.*", "PERMISSIONS"), + ), + }, + { + ResourceName: fqrn, + ImportState: true, + ImportStateId: "JPD-1:JPD-2", + ImportStateVerify: true, + }, + }, + }) +} diff --git a/pkg/missioncontrol/resource_access_federation_star.go b/pkg/missioncontrol/resource_access_federation_star.go new file mode 100644 index 0000000..e5948a8 --- /dev/null +++ b/pkg/missioncontrol/resource_access_federation_star.go @@ -0,0 +1,411 @@ +package missioncontrol + +import ( + "context" + "regexp" + + "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/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/jfrog/terraform-provider-shared/util" + utilfw "github.com/jfrog/terraform-provider-shared/util/fw" + validator_string "github.com/jfrog/terraform-provider-shared/validator/fw/string" + "github.com/samber/lo" +) + +const accessFederationEndpoint = "mc/api/v1/federation/{id}" + +var _ resource.Resource = &accessFederationStarResource{} + +type accessFederationStarResource struct { + ProviderData util.ProviderMetadata + TypeName string +} + +func NewAccessFederationStarResource() resource.Resource { + return &accessFederationStarResource{ + TypeName: "missioncontrol_access_federation_star", + } +} + +func (r *accessFederationStarResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = r.TypeName +} + +func (r *accessFederationStarResource) 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), + }, + Description: "ID for the source Platform Deployment. Use [Get Access Federation Candidate API](https://jfrog.com/help/r/jfrog-rest-apis/get-access-federation-candidates) to get a list of ID.", + }, + "entities": schema.SetAttribute{ + ElementType: types.StringType, + Required: true, + Validators: []validator.Set{ + setvalidator.ValueStringsAre( + stringvalidator.OneOf("USERS", "GROUPS", "PERMISSIONS", "TOKENS"), + ), + }, + Description: "Entity types to sync. Allow values: `USERS`, `GROUPS`, `PERMISSIONS`, `TOKENS`", + }, + "targets": schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Required: true, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + Description: "ID of the targeted Platform Deployment", + }, + "url": schema.StringAttribute{ + Required: true, + Validators: []validator.String{ + validator_string.IsURLHttpOrHttps(), + stringvalidator.RegexMatches(regexp.MustCompile(`^.+/access$`), "must end in '/access'"), + }, + Description: "Target Platform deployment URL: http://:/access; for example: http://myplatformserver:8082/access.", + }, + "permission_filters": schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "include_patterns": schema.SetAttribute{ + ElementType: types.StringType, + Optional: true, + }, + "exclude_patterns": schema.SetAttribute{ + ElementType: types.StringType, + Optional: true, + }, + }, + Optional: true, + Description: "When assigning entity types to targets, you can assign specific permissions to be synchronized using the `include_patterns`/`exclude_patterns` regular expressions.", + }, + }, + }, + Required: true, + Description: "Target JPD", + }, + }, + MarkdownDescription: "Provides a [JFrog Access Federation](https://jfrog.com/help/r/jfrog-platform-administration-documentation/access-federation) resource to setup Star Topology.\n" + + "~>The source and targets must have been configured properly for [Access Federation](https://jfrog.com/help/r/jfrog-platform-administration-documentation/access-federation).\n" + + "~>**Deletion** is currently not supported via REST API. This must be done using JFrog UI.", + } +} + +type accessFederationStarResourceModel struct { + ID types.String `tfsdk:"id"` + Entities types.Set `tfsdk:"entities"` + Targets types.Set `tfsdk:"targets"` +} + +var targetAttributeTypes = map[string]attr.Type{ + "id": types.StringType, + "url": types.StringType, + "permission_filters": types.ObjectType{AttrTypes: permissionFilterAttributeTypes}, +} + +var targetsElmementType = types.ObjectType{ + AttrTypes: targetAttributeTypes, +} + +var permissionFilterAttributeTypes = map[string]attr.Type{ + "include_patterns": types.SetType{ElemType: types.StringType}, + "exclude_patterns": types.SetType{ElemType: types.StringType}, +} + +func (r *accessFederationStarResourceModel) fromAPIModel(ctx context.Context, apiModel *accessFederationGetResponseAPIModel) (ds diag.Diagnostics) { + r.Targets = types.SetNull(targetsElmementType) + + if len(apiModel.Targets) > 0 { + targets := lo.Map( + apiModel.Targets, + func(target accessFederationTargetAPIModel, _ int) attr.Value { + includePatterns := types.SetNull(types.StringType) + if len(target.PermissionFilters.IncludePatterns) > 0 { + p, d := types.SetValueFrom(ctx, types.StringType, target.PermissionFilters.IncludePatterns) + if d.HasError() { + ds.Append(d...) + } + includePatterns = p + } + + excludePatterns := types.SetNull(types.StringType) + if len(target.PermissionFilters.ExcludePatterns) > 0 { + p, d := types.SetValueFrom(ctx, types.StringType, target.PermissionFilters.ExcludePatterns) + if d.HasError() { + ds.Append(d...) + } + excludePatterns = p + } + + permissionFilters, d := types.ObjectValue( + permissionFilterAttributeTypes, + map[string]attr.Value{ + "include_patterns": includePatterns, + "exclude_patterns": excludePatterns, + }, + ) + if d.HasError() { + ds.Append(d...) + } + + t, d := types.ObjectValue( + targetAttributeTypes, + map[string]attr.Value{ + "id": types.StringValue(target.ID), + "url": types.StringValue(target.URL), + "permission_filters": permissionFilters, + }, + ) + if d.HasError() { + ds.Append(d...) + } + + return t + }, + ) + targetsSet, d := types.SetValue(targetsElmementType, targets) + if d.HasError() { + ds.Append(d...) + } + r.Targets = targetsSet + } + + entities, d := types.SetValueFrom(ctx, types.StringType, apiModel.Entities) + if d.HasError() { + ds.Append(d...) + } + r.Entities = entities + + return +} + +func (r accessFederationStarResourceModel) toAPIModel(ctx context.Context, apiModel *accessFederationRequestAPIModel) diag.Diagnostics { + ds := diag.Diagnostics{} + + var entities []string + ds.Append(r.Entities.ElementsAs(ctx, &entities, false)...) + + targets := lo.Map( + r.Targets.Elements(), + func(elem attr.Value, _ int) accessFederationTargetAPIModel { + attrs := elem.(types.Object).Attributes() + + permissionFiltersAttrs := attrs["permission_filters"].(types.Object).Attributes() + + var includePatterns []string + d := permissionFiltersAttrs["include_patterns"].(types.Set).ElementsAs(ctx, &includePatterns, false) + if d.HasError() { + ds.Append(d...) + } + + var excludePatterns []string + d = permissionFiltersAttrs["exclude_patterns"].(types.Set).ElementsAs(ctx, &excludePatterns, false) + if d.HasError() { + ds.Append(d...) + } + + return accessFederationTargetAPIModel{ + ID: attrs["id"].(types.String).ValueString(), + URL: attrs["url"].(types.String).ValueString(), + PermissionFilters: accessFederationPermissionFiltersAPIModel{ + IncludePatterns: includePatterns, + ExcludePatterns: excludePatterns, + }, + } + }, + ) + + *apiModel = accessFederationRequestAPIModel{ + ID: r.ID.ValueString(), + Entities: entities, + Targets: targets, + } + + return ds +} + +type accessFederationRequestAPIModel struct { + ID string `json:"id"` + Entities []string `json:"entities"` + Targets []accessFederationTargetAPIModel `json:"targets"` +} + +type accessFederationTargetAPIModel struct { + ID string `json:"id"` + URL string `json:"url"` + PermissionFilters accessFederationPermissionFiltersAPIModel `json:"permission_filters"` +} + +type accessFederationPermissionFiltersAPIModel struct { + IncludePatterns []string `json:"include_patterns"` + ExcludePatterns []string `json:"exclude_patterns"` +} + +type accessFederationResponseAPIModel struct { + Label string `json:"label"` + Status string `json:"status"` +} + +type accessFederationGetResponseAPIModel struct { + Entities []string `json:"entities"` + Targets []accessFederationTargetAPIModel `json:"targets"` +} + +func (r *accessFederationStarResource) 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 *accessFederationStarResource) 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 accessFederationStarResourceModel + + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + var accessFederation accessFederationRequestAPIModel + resp.Diagnostics.Append(plan.toAPIModel(ctx, &accessFederation)...) + if resp.Diagnostics.HasError() { + return + } + + var results []accessFederationResponseAPIModel + response, err := r.ProviderData.Client.R(). + SetPathParam("id", plan.ID.ValueString()). + SetBody(accessFederation). + SetResult(&results). + Put(accessFederationEndpoint) + + if err != nil { + utilfw.UnableToCreateResourceError(resp, err.Error()) + return + } + + if response.IsError() { + utilfw.UnableToCreateResourceError(resp, response.String()) + return + } + + for _, result := range results { + tflog.Info(ctx, "Create result", map[string]interface{}{ + "label": result.Label, + "status": result.Status, + }) + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *accessFederationStarResource) 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 accessFederationStarResourceModel + + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + var accessFederation accessFederationGetResponseAPIModel + response, err := r.ProviderData.Client.R(). + SetPathParam("id", state.ID.ValueString()). + SetResult(&accessFederation). + Get(accessFederationEndpoint) + + if err != nil { + utilfw.UnableToRefreshResourceError(resp, err.Error()) + return + } + + if response.IsError() { + utilfw.UnableToRefreshResourceError(resp, response.String()) + return + } + + // Convert from the API data model to the Terraform data model + // and refresh any attribute values. + resp.Diagnostics.Append(state.fromAPIModel(ctx, &accessFederation)...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *accessFederationStarResource) 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 accessFederationStarResourceModel + + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + var accessFederation accessFederationRequestAPIModel + resp.Diagnostics.Append(plan.toAPIModel(ctx, &accessFederation)...) + if resp.Diagnostics.HasError() { + return + } + + var results []accessFederationResponseAPIModel + response, err := r.ProviderData.Client.R(). + SetPathParam("id", plan.ID.ValueString()). + SetBody(accessFederation). + SetResult(&results). + Put(accessFederationEndpoint) + + if err != nil { + utilfw.UnableToUpdateResourceError(resp, err.Error()) + return + } + + if response.IsError() { + utilfw.UnableToUpdateResourceError(resp, response.String()) + return + } + + for _, result := range results { + tflog.Info(ctx, "Update result", map[string]interface{}{ + "label": result.Label, + "status": result.Status, + }) + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *accessFederationStarResource) 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( + "Access Federation deletion not supported", + " The resource has be deleted from Terraform state. To delete Access Federation relationship, please use the JFrog UI.", + ) + + // 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 *accessFederationStarResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} diff --git a/pkg/missioncontrol/resource_access_federation_star_test.go b/pkg/missioncontrol/resource_access_federation_star_test.go new file mode 100644 index 0000000..506e05e --- /dev/null +++ b/pkg/missioncontrol/resource_access_federation_star_test.go @@ -0,0 +1,114 @@ +package missioncontrol_test + +import ( + "os" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/jfrog/terraform-provider-shared/testutil" + "github.com/jfrog/terraform-provider-shared/util" +) + +// To execute this test, you need setup second Artifactory instance with circle-of-trust. +// Then set them as env vars before running the test +func TestAccAccessFederationStar_full(t *testing.T) { + var skipTest = func() (bool, string) { + if len(os.Getenv("ARTIFACTORY_URL_2")) > 0 { + return false, "Env var `ARTIFACTORY_URL_2` is set. Executing test." + } + + return true, "Env var `ARTIFACTORY_URL_2` is not set. Skipping test." + } + + if skip, reason := skipTest(); skip { + t.Skipf(reason) + } + + _, fqrn, resourceName := testutil.MkNames("test-access-federation", "missioncontrol_access_federation_star") + + temp := ` + resource "missioncontrol_access_federation_star" "{{ .name }}" { + id = "JPD-1" + entities = ["USERS", "GROUPS", "PERMISSIONS", "TOKENS"] + targets = [ + { + id = "JPD-2" + url = "http://host.docker.internal:9082/access" + permission_filters = { + include_patterns = ["foo", "bar"] + exclude_patterns = ["fizz", "buzz"] + } + }, + ] + }` + + testData := map[string]string{ + "name": resourceName, + } + + config := util.ExecuteTemplate(resourceName, temp, testData) + + updatedTemp := ` + resource "missioncontrol_access_federation_star" "{{ .name }}" { + id = "JPD-1" + entities = ["USERS", "GROUPS", "PERMISSIONS"] + targets = [ + { + id = "JPD-2" + url = "http://host.docker.internal:9082/access" + permission_filters = { + include_patterns = ["foo"] + } + }, + ] + }` + updatedConfig := util.ExecuteTemplate(resourceName, updatedTemp, testData) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProviders(), + Steps: []resource.TestStep{ + { + Config: config, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(fqrn, "id", "JPD-1"), + resource.TestCheckResourceAttr(fqrn, "entities.#", "4"), + resource.TestCheckTypeSetElemAttr(fqrn, "entities.*", "USERS"), + resource.TestCheckTypeSetElemAttr(fqrn, "entities.*", "GROUPS"), + resource.TestCheckTypeSetElemAttr(fqrn, "entities.*", "PERMISSIONS"), + resource.TestCheckTypeSetElemAttr(fqrn, "entities.*", "TOKENS"), + resource.TestCheckResourceAttr(fqrn, "targets.#", "1"), + resource.TestCheckResourceAttr(fqrn, "targets.0.id", "JPD-2"), + resource.TestCheckResourceAttr(fqrn, "targets.0.url", "http://host.docker.internal:9082/access"), + resource.TestCheckResourceAttr(fqrn, "targets.0.permission_filters.include_patterns.#", "2"), + resource.TestCheckTypeSetElemAttr(fqrn, "targets.0.permission_filters.include_patterns.*", "foo"), + resource.TestCheckTypeSetElemAttr(fqrn, "targets.0.permission_filters.include_patterns.*", "bar"), + resource.TestCheckResourceAttr(fqrn, "targets.0.permission_filters.exclude_patterns.#", "2"), + resource.TestCheckTypeSetElemAttr(fqrn, "targets.0.permission_filters.exclude_patterns.*", "fizz"), + resource.TestCheckTypeSetElemAttr(fqrn, "targets.0.permission_filters.exclude_patterns.*", "buzz"), + ), + }, + { + Config: updatedConfig, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(fqrn, "id", "JPD-1"), + resource.TestCheckResourceAttr(fqrn, "entities.#", "3"), + resource.TestCheckTypeSetElemAttr(fqrn, "entities.*", "USERS"), + resource.TestCheckTypeSetElemAttr(fqrn, "entities.*", "GROUPS"), + resource.TestCheckTypeSetElemAttr(fqrn, "entities.*", "PERMISSIONS"), + resource.TestCheckResourceAttr(fqrn, "targets.#", "1"), + resource.TestCheckResourceAttr(fqrn, "targets.0.id", "JPD-2"), + resource.TestCheckResourceAttr(fqrn, "targets.0.url", "http://host.docker.internal:9082/access"), + resource.TestCheckResourceAttr(fqrn, "targets.0.permission_filters.include_patterns.#", "1"), + resource.TestCheckTypeSetElemAttr(fqrn, "targets.0.permission_filters.include_patterns.*", "foo"), + resource.TestCheckNoResourceAttr(fqrn, "targets.0.permission_filters.exclude_patterns"), + ), + }, + { + ResourceName: fqrn, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} diff --git a/pkg/missioncontrol/resource_jpd_test.go b/pkg/missioncontrol/resource_jpd_test.go index 69e85e7..2c03e7b 100644 --- a/pkg/missioncontrol/resource_jpd_test.go +++ b/pkg/missioncontrol/resource_jpd_test.go @@ -10,15 +10,15 @@ import ( ) // To make tests work runs ./scripts/run-artifactory-2.sh which will export env var `ARTIFACTORY_URL_2` -func skipTest() (bool, string) { - if len(os.Getenv("ARTIFACTORY_URL_2")) > 0 && len(os.Getenv("ARTIFACTORY_JOIN_KEY")) > 0 { - return false, "Env var `ARTIFACTORY_URL_2` and `ARTIFACTORY_JOIN_KEY` are set. Executing test." - } +func TestAccJpd_full(t *testing.T) { + var skipTest = func() (bool, string) { + if len(os.Getenv("ARTIFACTORY_URL_2")) > 0 && len(os.Getenv("ARTIFACTORY_JOIN_KEY")) > 0 { + return false, "Env var `ARTIFACTORY_URL_2` and `ARTIFACTORY_JOIN_KEY` are set. Executing test." + } - return true, "Env var `ARTIFACTORY_URL_2` or `ARTIFACTORY_JOIN_KEY` are not set. Skipping test." -} + return true, "Env var `ARTIFACTORY_URL_2` or `ARTIFACTORY_JOIN_KEY` are not set. Skipping test." + } -func TestAccJpd_full(t *testing.T) { if skip, reason := skipTest(); skip { t.Skipf(reason) } diff --git a/scripts/run-artifactory-2.sh b/scripts/run-artifactory-2.sh index fdcf857..0921a66 100755 --- a/scripts/run-artifactory-2.sh +++ b/scripts/run-artifactory-2.sh @@ -14,7 +14,7 @@ sudo rm -rf ${SCRIPT_DIR}/artifactory-2/ mkdir -p ${SCRIPT_DIR}/artifactory-2/extra_conf mkdir -p ${SCRIPT_DIR}/artifactory-2/var/etc/access -cp ${SCRIPT_DIR}/artifactory.lic ${SCRIPT_DIR}/artifactory-2/extra_conf +cp ${SCRIPT_DIR}/artifactory-2.lic ${SCRIPT_DIR}/artifactory-2/extra_conf cp ${SCRIPT_DIR}/system.yaml ${SCRIPT_DIR}/artifactory-2/var/etc/ cp ${SCRIPT_DIR}/access.config.patch.yml ${SCRIPT_DIR}/artifactory-2/var/etc/access diff --git a/scripts/setup-circle-of-trust.sh b/scripts/setup-circle-of-trust.sh new file mode 100755 index 0000000..f9f7001 --- /dev/null +++ b/scripts/setup-circle-of-trust.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash + +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" > /dev/null && pwd )" + +export ARTIFACTORY_VERSION=${ARTIFACTORY_VERSION:-7.84.15} +echo "ARTIFACTORY_VERSION=${ARTIFACTORY_VERSION}" > /dev/stderr + +# docker cp doesn't support copying files between containers so copy to local disk first +CONTAINER_ID_1=$(docker ps -q --filter "ancestor=releases-docker.jfrog.io/jfrog/artifactory-pro:${ARTIFACTORY_VERSION}" --filter publish=8082) +CONTAINER_ID_2=$(docker ps -q --filter "ancestor=releases-docker.jfrog.io/jfrog/artifactory-pro:${ARTIFACTORY_VERSION}" --filter publish=9082) + +echo "Fetching root certificates" +docker cp "${CONTAINER_ID_1}":/opt/jfrog/artifactory/var/etc/access/keys/root.crt "${SCRIPT_DIR}/artifactory-1.crt" \ + && chmod go+rw "${SCRIPT_DIR}"/artifactory-1.crt +docker cp "${CONTAINER_ID_2}":/opt/jfrog/artifactory/var/etc/access/keys/root.crt "${SCRIPT_DIR}/artifactory-2.crt" \ + && chmod go+rw "${SCRIPT_DIR}"/artifactory-2.crt + +echo "Uploading root certificates" +docker cp "${SCRIPT_DIR}/artifactory-1.crt" "${CONTAINER_ID_2}:/opt/jfrog/artifactory/var/etc/access/keys/trusted/artifactory-1.crt" +docker cp "${SCRIPT_DIR}/artifactory-2.crt" "${CONTAINER_ID_1}:/opt/jfrog/artifactory/var/etc/access/keys/trusted/artifactory-2.crt" + +echo "Circle-of-Trust is setup between artifactory-1 and artifactory-2 instances" \ No newline at end of file