diff --git a/CHANGELOG.md b/CHANGELOG.md index 2eb6cb1..a843cfe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +## 1.17.0 (November 15, 2024). Tested on Artifactory 7.98.8 with Terraform 1.9.8 and OpenTofu 1.8.5 + +FEATURES: + +**New Resource:** + +* `platform_group` - Resource to manage Group, using [Platform API](https://jfrog.com/help/r/jfrog-rest-apis/groups). This replaces the `artifactory_group` resource in [Artifactory provider](https://registry.terraform.io/providers/jfrog/artifactory/latest/docs/resources/group), which uses the (deprecated) Artifactory Security API. PR: [#155](https://github.com/jfrog/terraform-provider-platform/pull/155) + ## 1.16.0 (November 1, 2024). Tested on Artifactory 7.98.7 with Terraform 1.9.8 and OpenTofu 1.8.4 IMPROVEMENTS: diff --git a/docs/resources/group.md b/docs/resources/group.md new file mode 100644 index 0000000..381526b --- /dev/null +++ b/docs/resources/group.md @@ -0,0 +1,54 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "platform_group Resource - terraform-provider-platform" +subcategory: "" +description: |- + Provides a group resource. This can be used to create and manage groups. A group represents a role in the system and is assigned a set of permissions. +--- + +# platform_group (Resource) + +Provides a group resource. This can be used to create and manage groups. A group represents a role in the system and is assigned a set of permissions. + +## Example Usage + +```terraform +resource "platform_group" "my-group" { + name = "my-group" + description = "My group" + external_id = "My Azure ID" + auto_join = true + admin_privileges = false + members = [ + "admin" + ] +} +``` + + +## Schema + +### Required + +- `name` (String) Name of the group. + +### Optional + +- `admin_privileges` (Boolean) Any users added to this group will automatically be assigned with admin privileges in the system. +- `auto_join` (Boolean) When this parameter is set, any new users defined in the system are automatically assigned to this group. +- `description` (String) A description for the group. +- `external_id` (String) New external group ID used to configure the corresponding group in Azure AD. +- `members` (Set of String) List of users assigned to the group. + +### Read-Only + +- `realm` (String) The realm for the group. +- `realm_attributes` (String) The realm for the group. + +## Import + +Import is supported using the following syntax: + +```shell +terraform import platform_group.my-group my-group +``` diff --git a/examples/resources/platform_group/import.sh b/examples/resources/platform_group/import.sh new file mode 100644 index 0000000..e417b28 --- /dev/null +++ b/examples/resources/platform_group/import.sh @@ -0,0 +1 @@ +terraform import platform_group.my-group my-group \ No newline at end of file diff --git a/examples/resources/platform_group/resource.tf b/examples/resources/platform_group/resource.tf new file mode 100644 index 0000000..144bbb2 --- /dev/null +++ b/examples/resources/platform_group/resource.tf @@ -0,0 +1,10 @@ +resource "platform_group" "my-group" { + name = "my-group" + description = "My group" + external_id = "My Azure ID" + auto_join = true + admin_privileges = false + members = [ + "admin" + ] +} \ No newline at end of file diff --git a/go.mod b/go.mod index 62055b6..3595967 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( github.com/hashicorp/terraform-plugin-framework-validators v0.15.0 github.com/hashicorp/terraform-plugin-go v0.25.0 github.com/hashicorp/terraform-plugin-testing v1.10.0 - github.com/jfrog/terraform-provider-shared v1.26.0 + github.com/jfrog/terraform-provider-shared v1.27.0 github.com/samber/lo v1.47.0 ) diff --git a/go.sum b/go.sum index c8b01c2..f08047f 100644 --- a/go.sum +++ b/go.sum @@ -130,8 +130,8 @@ github.com/imdario/mergo v0.3.15 h1:M8XP7IuFNsqUx6VPK2P9OSmsYsI/YFaGil0uD21V3dM= github.com/imdario/mergo v0.3.15/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= -github.com/jfrog/terraform-provider-shared v1.26.0 h1:xfJfKcgejlFkIyo6VLJPzNtEVfbTYIiGKD2PWysdgw4= -github.com/jfrog/terraform-provider-shared v1.26.0/go.mod h1:IPwXN48K3uzJNDmT2x6zFGa5IS0KG2AK7jnQR2H4G1A= +github.com/jfrog/terraform-provider-shared v1.27.0 h1:ivXga2hsXnIJF/gevPZyXXqDm82v5PQ3sy/x3kDk+nA= +github.com/jfrog/terraform-provider-shared v1.27.0/go.mod h1:LpjcFuDzCW5+gdQs1LAjgMESKuYd3ZqJazoIGt0uv9Q= github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c= github.com/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gavG9e0Q693nKo= github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= diff --git a/pkg/platform/provider.go b/pkg/platform/provider.go index 89f6d83..677c23a 100644 --- a/pkg/platform/provider.go +++ b/pkg/platform/provider.go @@ -212,6 +212,7 @@ func (p *PlatformProvider) Resources(ctx context.Context) []func() resource.Reso NewAWSIAMRoleResource, NewLicenseResource, NewGlobalRoleResource, + NewGroupResource, NewOIDCConfigurationResource, NewOIDCIdentityMappingResource, NewMyJFrogIPAllowListResource, diff --git a/pkg/platform/resource_group.go b/pkg/platform/resource_group.go new file mode 100644 index 0000000..e287f7a --- /dev/null +++ b/pkg/platform/resource_group.go @@ -0,0 +1,406 @@ +package platform + +import ( + "context" + "net/http" + + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/setplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/jfrog/terraform-provider-shared/util" + utilfw "github.com/jfrog/terraform-provider-shared/util/fw" + validatorfw "github.com/jfrog/terraform-provider-shared/validator/fw" + "github.com/samber/lo" +) + +var _ resource.Resource = (*groupResource)(nil) + +type groupResource struct { + util.JFrogResource +} + +func NewGroupResource() resource.Resource { + return &groupResource{ + JFrogResource: util.JFrogResource{ + TypeName: "platform_group", + ValidArtifactoryVersion: "7.49.3", + CollectionEndpoint: "access/api/v2/groups", + DocumentEndpoint: "access/api/v2/groups/{name}", + }, + } +} + +func (r *groupResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "name": schema.StringAttribute{ + Required: true, + Validators: []validator.String{ + stringvalidator.LengthBetween(1, 64), + }, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + MarkdownDescription: "Name of the group.", + }, + "description": schema.StringAttribute{ + Optional: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + MarkdownDescription: "A description for the group.", + }, + "external_id": schema.StringAttribute{ + Optional: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + MarkdownDescription: "New external group ID used to configure the corresponding group in Azure AD.", + }, + "auto_join": schema.BoolAttribute{ + Optional: true, + Computed: true, + Default: booldefault.StaticBool(false), + Validators: []validator.Bool{ + validatorfw.BoolConflict(true, path.Expressions{ + path.MatchRelative().AtParent().AtName("admin_privileges"), + }...), + }, + MarkdownDescription: "When this parameter is set, any new users defined in the system are automatically assigned to this group.", + }, + "admin_privileges": schema.BoolAttribute{ + Optional: true, + Computed: true, + Default: booldefault.StaticBool(false), + MarkdownDescription: "Any users added to this group will automatically be assigned with admin privileges in the system.", + }, + "members": schema.SetAttribute{ + ElementType: types.StringType, + Optional: true, + PlanModifiers: []planmodifier.Set{ + setplanmodifier.UseStateForUnknown(), + }, + MarkdownDescription: "List of users assigned to the group.", + }, + "realm": schema.StringAttribute{ + Computed: true, + MarkdownDescription: "The realm for the group.", + }, + "realm_attributes": schema.StringAttribute{ + Computed: true, + MarkdownDescription: "The realm for the group.", + }, + }, + MarkdownDescription: "Provides a group resource to create and manage groups, and manages membership. A group represents a role and is used with RBAC (Role-Based Access Control) rules. See [JFrog documentation](https://jfrog.com/help/r/jfrog-platform-administration-documentation/create-and-edit-groups) for more details.", + } +} + +type groupResourceModel struct { + Name types.String `tfsdk:"name"` + Description types.String `tfsdk:"description"` + ExternalId types.String `tfsdk:"external_id"` + AutoJoin types.Bool `tfsdk:"auto_join"` + AdminPrivileges types.Bool `tfsdk:"admin_privileges"` + Members types.Set `tfsdk:"members"` + Realm types.String `tfsdk:"realm"` + RealmAttributes types.String `tfsdk:"realm_attributes"` +} + +func (r *groupResourceModel) toAPIModel(ctx context.Context, apiModel *groupAPIModel) (ds diag.Diagnostics) { + + var members []string + ds.Append(r.Members.ElementsAs(ctx, &members, false)...) + if ds.HasError() { + return + } + + *apiModel = groupAPIModel{ + Name: r.Name.ValueString(), + Description: r.Description.ValueStringPointer(), + ExternalId: r.ExternalId.ValueStringPointer(), + AutoJoin: r.AutoJoin.ValueBoolPointer(), + AdminPrivileges: r.AdminPrivileges.ValueBoolPointer(), + Members: members, + } + + return nil +} + +func (r *groupResourceModel) fromAPIModel(ctx context.Context, apiModel groupAPIModel, ignoreMembers bool) diag.Diagnostics { + diags := diag.Diagnostics{} + + r.Name = types.StringValue(apiModel.Name) + r.Description = types.StringPointerValue(apiModel.Description) + r.ExternalId = types.StringPointerValue(apiModel.ExternalId) + r.AutoJoin = types.BoolPointerValue(apiModel.AutoJoin) + r.AdminPrivileges = types.BoolPointerValue(apiModel.AdminPrivileges) + r.Realm = types.StringPointerValue(apiModel.Realm) + r.RealmAttributes = types.StringPointerValue(apiModel.RealmAttributes) + + if r.Members.IsUnknown() { + r.Members = types.SetNull(types.StringType) + } + + if !ignoreMembers && len(apiModel.Members) > 0 { + members, d := types.SetValueFrom(ctx, types.StringType, apiModel.Members) + if d != nil { + diags.Append(d...) + return diags + } + + r.Members = members + } + + return diags +} + +type groupAPIModel struct { + Name string `json:"name"` + Description *string `json:"description,omitempty"` + ExternalId *string `json:"external_id,omitempty"` + AutoJoin *bool `json:"auto_join,omitempty"` + AdminPrivileges *bool `json:"admin_privileges,omitempty"` + Members []string `json:"members,omitempty"` // only for create + Realm *string `json:"realm,omitempty"` // read only + RealmAttributes *string `json:"realm_attributes,omitempty"` // read only +} + +type groupMembersRequestAPIModel struct { + Add []string `json:"add"` + Remove []string `json:"remove"` +} + +type groupMembersResponseAPIModel struct { + Members []string `json:"members"` +} + +func (r *groupResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + // Prevent panic if the provider has not been configured. + if req.ProviderData == nil { + return + } + m := req.ProviderData.(PlatformProviderMetadata).ProviderMetadata + r.ProviderData = &m +} + +func (r *groupResource) 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 *groupResourceModel + // Read Terraform plan data into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + var group groupAPIModel + resp.Diagnostics.Append(plan.toAPIModel(ctx, &group)...) + if resp.Diagnostics.HasError() { + return + } + + var newGroup groupAPIModel + var apiErrs util.JFrogErrors + response, err := r.ProviderData.Client.R(). + SetBody(group). + SetResult(&newGroup). + SetError(&apiErrs). + Post(r.JFrogResource.CollectionEndpoint) + + if err != nil { + utilfw.UnableToCreateResourceError(resp, err.Error()) + return + } + + if response.StatusCode() != http.StatusCreated { + utilfw.UnableToCreateResourceError(resp, apiErrs.String()) + return + } + + plan.Realm = types.StringPointerValue(newGroup.Realm) + plan.RealmAttributes = types.StringPointerValue(newGroup.RealmAttributes) + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *groupResource) 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 *groupResourceModel + // Read Terraform prior state data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + var group groupAPIModel + var apiErrs util.JFrogErrors + response, err := r.ProviderData.Client.R(). + SetPathParam("name", state.Name.ValueString()). + SetResult(&group). + SetError(&apiErrs). + Get(r.JFrogResource.DocumentEndpoint) + + if err != nil { + utilfw.UnableToRefreshResourceError(resp, err.Error()) + return + } + + // Treat HTTP 404 Not Found status as a signal to recreate resource + // and return early + if response.StatusCode() == http.StatusNotFound { + resp.State.RemoveResource(ctx) + return + } + + if response.IsError() { + utilfw.UnableToRefreshResourceError(resp, apiErrs.String()) + return + } + + // Convert from the API data model to the Terraform data model + // and refresh any attribute values. + resp.Diagnostics.Append(state.fromAPIModel(ctx, group, false)...) + if resp.Diagnostics.HasError() { + return + } + + // Save updated data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *groupResource) 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 groupResourceModel + var state groupResourceModel + + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + var group groupAPIModel + resp.Diagnostics.Append(plan.toAPIModel(ctx, &group)...) + if resp.Diagnostics.HasError() { + return + } + + var updatedGroup groupAPIModel + var apiErrs util.JFrogErrors + response, err := r.ProviderData.Client.R(). + SetPathParam("name", plan.Name.ValueString()). + SetBody(group). + SetResult(&updatedGroup). + SetError(&apiErrs). + Patch(r.JFrogResource.DocumentEndpoint) + if err != nil { + utilfw.UnableToUpdateResourceError(resp, err.Error()) + return + } + + if response.IsError() { + utilfw.UnableToUpdateResourceError(resp, apiErrs.String()) + return + } + + resp.Diagnostics.Append(plan.fromAPIModel(ctx, updatedGroup, true)...) + if resp.Diagnostics.HasError() { + return + } + + var planMembers []string + resp.Diagnostics.Append(plan.Members.ElementsAs(ctx, &planMembers, false)...) + if resp.Diagnostics.HasError() { + return + } + + var stateMembers []string + resp.Diagnostics.Append(state.Members.ElementsAs(ctx, &stateMembers, false)...) + if resp.Diagnostics.HasError() { + return + } + + memebersToAdd, membersToRemove := lo.Difference(planMembers, stateMembers) + membersReq := groupMembersRequestAPIModel{ + Add: memebersToAdd, + Remove: membersToRemove, + } + + if len(memebersToAdd) > 0 || len(membersToRemove) > 0 { + var membersRes groupMembersResponseAPIModel + response, err = r.ProviderData.Client.R(). + SetPathParam("name", plan.Name.ValueString()). + SetBody(membersReq). + SetResult(&membersRes). + SetError(&apiErrs). + Patch(r.JFrogResource.DocumentEndpoint + "/members") + if err != nil { + utilfw.UnableToUpdateResourceError(resp, err.Error()) + return + } + + if response.IsError() { + utilfw.UnableToUpdateResourceError(resp, apiErrs.String()) + return + } + + ms, d := types.SetValueFrom(ctx, types.StringType, membersRes.Members) + if d != nil { + resp.Diagnostics.Append(d...) + return + } + + plan.Members = ms + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *groupResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + go util.SendUsageResourceDelete(ctx, r.ProviderData.Client.R(), r.ProviderData.ProductId, r.TypeName) + + var state groupResourceModel + + // Read Terraform prior state data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + + var apiErrs util.JFrogErrors + response, err := r.ProviderData.Client.R(). + SetPathParam("name", state.Name.ValueString()). + SetError(&apiErrs). + Delete(r.JFrogResource.DocumentEndpoint) + + if err != nil { + utilfw.UnableToDeleteResourceError(resp, err.Error()) + return + } + + // Return error if the HTTP status code is not 204 No Content or 404 Not Found + if response.StatusCode() != http.StatusNotFound && response.StatusCode() != http.StatusNoContent { + utilfw.UnableToDeleteResourceError(resp, apiErrs.String()) + return + } + + // If the logic reaches here, it implicitly succeeded and will remove + // the resource from state if there are no other errors. +} + +// ImportState imports the resource into the Terraform state. +func (r *groupResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("name"), req, resp) +} diff --git a/pkg/platform/resource_group_test.go b/pkg/platform/resource_group_test.go new file mode 100644 index 0000000..0b439b6 --- /dev/null +++ b/pkg/platform/resource_group_test.go @@ -0,0 +1,204 @@ +package platform_test + +import ( + "fmt" + "regexp" + "strings" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/plancheck" + "github.com/jfrog/terraform-provider-shared/testutil" + "github.com/jfrog/terraform-provider-shared/util" +) + +func TestAccGroup_full(t *testing.T) { + _, fqrn, groupName := testutil.MkNames("test-group", "platform_group") + + temp := ` + resource "platform_group" "{{ .groupName }}" { + name = "{{ .groupName }}" + description = "Test group" + external_id = "externalID" + auto_join = {{ .autoJoin }} + admin_privileges = false + members = {{ .members }} + } + ` + + testData := map[string]string{ + "groupName": groupName, + "autoJoin": fmt.Sprintf("%t", testutil.RandBool()), + "members": "[\"anonymous\", \"admin\"]", + } + + config := util.ExecuteTemplate(groupName, temp, testData) + + updatedTestData := map[string]string{ + "groupName": groupName, + "autoJoin": fmt.Sprintf("%t", testutil.RandBool()), + "members": "[\"admin\"]", + } + + updatedConfig := util.ExecuteTemplate(groupName, temp, updatedTestData) + + updated2TestData := map[string]string{ + "groupName": groupName, + "autoJoin": fmt.Sprintf("%t", testutil.RandBool()), + "adminPrivileges": fmt.Sprintf("%t", testutil.RandBool()), + "members": "[\"anonymous\"]", + } + + updated2Config := util.ExecuteTemplate(groupName, temp, updated2TestData) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProviders(), + Steps: []resource.TestStep{ + { + Config: config, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(fqrn, "name", testData["groupName"]), + resource.TestCheckResourceAttr(fqrn, "description", "Test group"), + resource.TestCheckResourceAttr(fqrn, "external_id", "externalID"), + resource.TestCheckResourceAttr(fqrn, "auto_join", testData["autoJoin"]), + resource.TestCheckResourceAttr(fqrn, "admin_privileges", "false"), + resource.TestCheckResourceAttrSet(fqrn, "realm"), + resource.TestCheckNoResourceAttr(fqrn, "realm_attributes"), + resource.TestCheckResourceAttr(fqrn, "members.#", "2"), + resource.TestCheckResourceAttr(fqrn, "members.0", "admin"), + resource.TestCheckResourceAttr(fqrn, "members.1", "anonymous"), + ), + }, + { + Config: updatedConfig, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(fqrn, "name", updatedTestData["groupName"]), + resource.TestCheckResourceAttr(fqrn, "description", "Test group"), + resource.TestCheckResourceAttr(fqrn, "external_id", "externalID"), + resource.TestCheckResourceAttr(fqrn, "auto_join", updatedTestData["autoJoin"]), + resource.TestCheckResourceAttr(fqrn, "admin_privileges", "false"), + resource.TestCheckResourceAttrSet(fqrn, "realm"), + resource.TestCheckNoResourceAttr(fqrn, "realm_attributes"), + resource.TestCheckResourceAttr(fqrn, "members.#", "1"), + resource.TestCheckResourceAttr(fqrn, "members.0", "admin"), + ), + }, + { + Config: updated2Config, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(fqrn, "name", updated2TestData["groupName"]), + resource.TestCheckResourceAttr(fqrn, "description", "Test group"), + resource.TestCheckResourceAttr(fqrn, "external_id", "externalID"), + resource.TestCheckResourceAttr(fqrn, "auto_join", updated2TestData["autoJoin"]), + resource.TestCheckResourceAttr(fqrn, "admin_privileges", "false"), + resource.TestCheckResourceAttrSet(fqrn, "realm"), + resource.TestCheckNoResourceAttr(fqrn, "realm_attributes"), + resource.TestCheckResourceAttr(fqrn, "members.#", "1"), + resource.TestCheckResourceAttr(fqrn, "members.0", "anonymous"), + ), + }, + { + ResourceName: fqrn, + ImportState: true, + ImportStateId: updated2TestData["groupName"], + ImportStateVerify: true, + ImportStateVerifyIdentifierAttribute: "name", + }, + }, + }) +} + +func TestAccGroup_auto_join_conflict(t *testing.T) { + _, _, groupName := testutil.MkNames("test-group", "platform_group") + temp := ` + resource "platform_group" "{{ .groupName }}" { + name = "{{ .groupName }}" + description = "Test group" + external_id = "externalID" + auto_join = true + admin_privileges = true + } + ` + + config := util.ExecuteTemplate(groupName, temp, map[string]string{"groupName": groupName}) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProviders(), + Steps: []resource.TestStep{ + { + Config: config, + ExpectError: regexp.MustCompile(".*can not be set to.*"), + }, + }, + }) +} + +func TestAccGroup_name_too_long(t *testing.T) { + _, _, groupName := testutil.MkNames("test-group", "platform_group") + + groupName = fmt.Sprintf("%s%s", groupName, strings.Repeat("X", 60)) + temp := ` + resource "platform_group" "{{ .groupName }}" { + name = "{{ .groupName }}" + description = "Test group" + external_id = "externalID" + auto_join = true + admin_privileges = false + } + ` + + config := util.ExecuteTemplate(groupName, temp, map[string]string{"groupName": groupName}) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProviders(), + Steps: []resource.TestStep{ + { + Config: config, + ExpectError: regexp.MustCompile(".*Attribute name string length must be between 1 and 64.*"), + }, + }, + }) +} + +func TestAccGroup_update_name(t *testing.T) { + _, fqrn, groupName := testutil.MkNames("test-group-name-", "platform_group") + + temp := ` + resource "platform_group" "{{ .groupName }}" { + name = "{{ .groupName }}" + } + ` + config := util.ExecuteTemplate(groupName, temp, map[string]string{"groupName": groupName}) + + updatedTemp := ` + resource "platform_group" "{{ .groupName }}" { + name = "{{ .groupName }}-updated" + } + ` + + updatedConfig := util.ExecuteTemplate(groupName, updatedTemp, map[string]string{"groupName": groupName}) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProviders(), + Steps: []resource.TestStep{ + { + Config: config, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(fqrn, "name", groupName), + ), + }, + { + Config: updatedConfig, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(fqrn, plancheck.ResourceActionDestroyBeforeCreate), + }, + }, + }, + }, + }) +} diff --git a/pkg/platform/resource_workers_service.go b/pkg/platform/resource_workers_service.go index 7713d90..c0d3869 100644 --- a/pkg/platform/resource_workers_service.go +++ b/pkg/platform/resource_workers_service.go @@ -424,7 +424,7 @@ func (r *workersServiceResource) Update(ctx context.Context, req resource.Update return } - planSecrets := lo.Map[attr.Value]( + planSecrets := lo.Map( plan.Secrets.Elements(), func(elem attr.Value, index int) secretAPIModel { attrs := elem.(types.Object).Attributes() @@ -434,15 +434,15 @@ func (r *workersServiceResource) Update(ctx context.Context, req resource.Update }, ) - stateSecrets := lo.Map[attr.Value](state.Secrets.Elements(), func(elem attr.Value, index int) secretAPIModel { + stateSecrets := lo.Map(state.Secrets.Elements(), func(elem attr.Value, index int) secretAPIModel { attrs := elem.(types.Object).Attributes() return secretAPIModel{ Key: attrs["key"].(types.String).ValueString(), } }) - _, secretsToBeRemoved := lo.Difference[secretAPIModel](planSecrets, stateSecrets) - secretKeysToBeRemovedKeys := lo.Map[secretAPIModel]( + _, secretsToBeRemoved := lo.Difference(planSecrets, stateSecrets) + secretKeysToBeRemovedKeys := lo.Map( secretsToBeRemoved, func(x secretAPIModel, index int) string { return x.Key