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