From 062ebe83e94ad638ca9887a7bb973227292bf366 Mon Sep 17 00:00:00 2001 From: Nelson Susanto Date: Mon, 14 Oct 2024 10:41:06 +1100 Subject: [PATCH] feat: migrate user resource to tf framework (#796) * wip user resource * User resource with create * wip read * wip test * Update test to use new get function * Fixes and remove old test * Working user resource in framework, remove SDK version * Docs updates * Remove old schema and update docs again * Docs again * fix test * is_active doesn't actually do anything * Update docs * Revert is_active since it does do something on update * Docs * Update the user if they're expected to be inactive * Try this * Fill email address if there's a value --------- Co-authored-by: domenicsim1 --- docs/resources/user.md | 20 +- octopusdeploy/provider.go | 1 - octopusdeploy/resource_space_test.go | 25 +++ octopusdeploy/resource_user.go | 94 --------- octopusdeploy/schema_user.go | 132 ------------ octopusdeploy_framework/datasource_users.go | 4 +- octopusdeploy_framework/framework_provider.go | 1 + octopusdeploy_framework/resource_user.go | 197 ++++++++++++++++++ .../resource_user_test.go | 30 ++- octopusdeploy_framework/schemas/schema.go | 35 +++- octopusdeploy_framework/schemas/user.go | 100 ++++++++- octopusdeploy_framework/schemas/variable.go | 10 +- 12 files changed, 382 insertions(+), 267 deletions(-) delete mode 100644 octopusdeploy/resource_user.go delete mode 100644 octopusdeploy/schema_user.go create mode 100644 octopusdeploy_framework/resource_user.go rename {octopusdeploy => octopusdeploy_framework}/resource_user_test.go (90%) diff --git a/docs/resources/user.md b/docs/resources/user.md index ea802907e..df9daaeeb 100644 --- a/docs/resources/user.md +++ b/docs/resources/user.md @@ -48,33 +48,33 @@ resource "octopusdeploy_user" "example" { ### Optional - `email_address` (String) The email address of this resource. -- `id` (String) The unique ID for this resource. -- `identity` (Block Set) (see [below for nested schema](#nestedblock--identity)) -- `is_active` (Boolean) -- `is_service` (Boolean) +- `identity` (Block Set) The identities associated with the user. (see [below for nested schema](#nestedblock--identity)) +- `is_active` (Boolean) Specifies whether or not the user is active. +- `is_service` (Boolean) Specifies whether or not the user is a service account. - `password` (String, Sensitive) The password associated with this resource. ### Read-Only -- `can_password_be_edited` (Boolean) -- `is_requestor` (Boolean) +- `can_password_be_edited` (Boolean) Specifies whether or not the password can be edited. +- `id` (String) The unique ID for this resource. +- `is_requestor` (Boolean) Specifies whether or not the user is the requestor. ### Nested Schema for `identity` Optional: -- `claim` (Block Set) (see [below for nested schema](#nestedblock--identity--claim)) -- `provider` (String) +- `claim` (Block Set) The claim associated with the identity. (see [below for nested schema](#nestedblock--identity--claim)) +- `provider` (String) The identity provider. ### Nested Schema for `identity.claim` Required: -- `is_identifying_claim` (Boolean) +- `is_identifying_claim` (Boolean) Specifies whether or not the claim is an identifying claim. - `name` (String) The name of this resource. -- `value` (String) +- `value` (String) The value of this resource. ## Import diff --git a/octopusdeploy/provider.go b/octopusdeploy/provider.go index 786ce3e1c..58a85a553 100644 --- a/octopusdeploy/provider.go +++ b/octopusdeploy/provider.go @@ -64,7 +64,6 @@ func Provider() *schema.Provider { "octopusdeploy_static_worker_pool": resourceStaticWorkerPool(), "octopusdeploy_team": resourceTeam(), "octopusdeploy_token_account": resourceTokenAccount(), - "octopusdeploy_user": resourceUser(), "octopusdeploy_user_role": resourceUserRole(), }, Schema: map[string]*schema.Schema{ diff --git a/octopusdeploy/resource_space_test.go b/octopusdeploy/resource_space_test.go index 3cca37024..f39443778 100644 --- a/octopusdeploy/resource_space_test.go +++ b/octopusdeploy/resource_space_test.go @@ -78,6 +78,31 @@ func testSpaceDataSource(localName string, name string, slug string) string { }`, localName, name) } +func testAccUserBasic(localName string, displayName string, isActive bool, isService bool, password string, username string, emailAddress string) string { + return fmt.Sprintf(`resource "octopusdeploy_user" "%s" { + display_name = "%s" + email_address = "%s" + is_active = %v + is_service = %v + password = "%s" + username = "%s" + + identity { + provider = "Octopus ID" + claim { + name = "email" + is_identifying_claim = true + value = "%s" + } + claim { + name = "dn" + is_identifying_claim = false + value = "%s" + } + } + }`, localName, displayName, emailAddress, isActive, isService, password, username, emailAddress, displayName) +} + func testSpaceBasic(localName string, name string, slug string) string { userLocalName := acctest.RandStringFromCharSet(20, acctest.CharSetAlpha) userDisplayName := acctest.RandStringFromCharSet(20, acctest.CharSetAlpha) diff --git a/octopusdeploy/resource_user.go b/octopusdeploy/resource_user.go deleted file mode 100644 index 0a7a5f599..000000000 --- a/octopusdeploy/resource_user.go +++ /dev/null @@ -1,94 +0,0 @@ -package octopusdeploy - -import ( - "context" - "log" - - "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/client" - "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/users" - "github.com/OctopusDeploy/terraform-provider-octopusdeploy/internal/errors" - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" -) - -func resourceUser() *schema.Resource { - return &schema.Resource{ - CreateContext: resourceUserCreate, - DeleteContext: resourceUserDelete, - Description: "This resource manages users in Octopus Deploy.", - Importer: getImporter(), - ReadContext: resourceUserRead, - Schema: getUserSchema(), - UpdateContext: resourceUserUpdate, - } -} - -func resourceUserCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { - user := expandUser(d) - - log.Printf("[DEBUG] creating user") - - client := m.(*client.Client) - createdUser, err := users.Add(client, user) - if err != nil { - return diag.FromErr(err) - } - - if err := setUser(ctx, d, createdUser); err != nil { - return diag.FromErr(err) - } - - d.SetId(createdUser.GetID()) - - log.Printf("[DEBUG] user created (%s)", d.Id()) - return nil -} - -func resourceUserDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { - log.Printf("[INFO] deleting user (%s)", d.Id()) - - client := m.(*client.Client) - if err := users.DeleteByID(client, d.Id()); err != nil { - return diag.FromErr(err) - } - - d.SetId("") - - log.Printf("[INFO] user deleted") - return nil -} - -func resourceUserRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { - log.Printf("[INFO] reading user (%s)", d.Id()) - - client := m.(*client.Client) - user, err := users.GetByID(client, d.Id()) - if err != nil { - return errors.ProcessApiError(ctx, d, err, "user") - } - - if err := setUser(ctx, d, user); err != nil { - return diag.FromErr(err) - } - - log.Printf("[INFO] user read (%s)", d.Id()) - return nil -} - -func resourceUserUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { - log.Printf("[INFO] updating user (%s)", d.Id()) - - user := expandUser(d) - client := m.(*client.Client) - updatedUser, err := users.Update(client, user) - if err != nil { - return diag.FromErr(err) - } - - if err := setUser(ctx, d, updatedUser); err != nil { - return diag.FromErr(err) - } - - log.Printf("[INFO] user updated (%s)", d.Id()) - return nil -} diff --git a/octopusdeploy/schema_user.go b/octopusdeploy/schema_user.go deleted file mode 100644 index 620c3db37..000000000 --- a/octopusdeploy/schema_user.go +++ /dev/null @@ -1,132 +0,0 @@ -package octopusdeploy - -import ( - "context" - "fmt" - - "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/users" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" -) - -func expandUser(d *schema.ResourceData) *users.User { - username := d.Get("username").(string) - displayName := d.Get("display_name").(string) - - user := users.NewUser(username, displayName) - user.ID = d.Id() - - if v, ok := d.GetOk("email_address"); ok { - user.EmailAddress = v.(string) - } - - if v, ok := d.GetOk("identity"); ok { - user.Identities = expandIdentities(v.(*schema.Set).List()) - } - - if v, ok := d.GetOk("is_active"); ok { - user.IsActive = v.(bool) - } - - if v, ok := d.GetOk("is_requestor"); ok { - user.IsRequestor = v.(bool) - } - - if v, ok := d.GetOk("is_service"); ok { - user.IsService = v.(bool) - } - - if v, ok := d.GetOk("password"); ok { - user.Password = v.(string) - } - - return user -} - -func flattenUser(user *users.User) map[string]interface{} { - if user == nil { - return nil - } - - return map[string]interface{}{ - "can_password_be_edited": user.CanPasswordBeEdited, - "display_name": user.DisplayName, - "email_address": user.EmailAddress, - "id": user.GetID(), - "identity": flattenIdentities(user.Identities), - "is_active": user.IsActive, - "is_service": user.IsService, - "username": user.Username, - } -} - -func getUserDataSchema() map[string]*schema.Schema { - dataSchema := getUserSchema() - setDataSchema(&dataSchema) - - return map[string]*schema.Schema{ - "filter": getQueryFilter(), - "id": getDataSchemaID(), - "ids": getQueryIDs(), - "skip": getQuerySkip(), - "take": getQueryTake(), - "space_id": getQuerySpaceID(), - "users": { - Computed: true, - Description: "A list of users that match the filter(s).", - Elem: &schema.Resource{Schema: dataSchema}, - Optional: false, - Type: schema.TypeList, - }, - } -} - -func getUserSchema() map[string]*schema.Schema { - return map[string]*schema.Schema{ - "can_password_be_edited": { - Computed: true, - Type: schema.TypeBool, - }, - "display_name": getDisplayNameSchema(true), - "email_address": getEmailAddressSchema(false), - "id": getIDSchema(), - "identity": { - Optional: true, - Elem: &schema.Resource{Schema: getIdentitySchema()}, - Type: schema.TypeSet, - }, - "is_active": { - Optional: true, - Type: schema.TypeBool, - }, - "is_requestor": { - Computed: true, - Type: schema.TypeBool, - }, - "is_service": { - Optional: true, - Type: schema.TypeBool, - }, - "username": getUsernameSchema(true), - "password": getPasswordSchema(false), - } -} - -func setUser(ctx context.Context, d *schema.ResourceData, user *users.User) error { - d.Set("can_password_be_edited", user.CanPasswordBeEdited) - d.Set("display_name", user.DisplayName) - d.Set("email_address", user.EmailAddress) - d.Set("id", user.GetID()) - - if err := d.Set("identity", flattenIdentities(user.Identities)); err != nil { - return fmt.Errorf("error setting identity: %s", err) - } - - d.Set("is_active", user.IsActive) - d.Set("is_requestor", user.IsRequestor) - d.Set("is_service", user.IsService) - d.Set("username", user.Username) - - d.SetId(user.GetID()) - - return nil -} diff --git a/octopusdeploy_framework/datasource_users.go b/octopusdeploy_framework/datasource_users.go index c030f4ca2..42860be92 100644 --- a/octopusdeploy_framework/datasource_users.go +++ b/octopusdeploy_framework/datasource_users.go @@ -65,10 +65,10 @@ func (u *userDataSource) Read(ctx context.Context, req datasource.ReadRequest, r return } - mappedUsers := []schemas.UserTypeResourceModel{} + mappedUsers := []schemas.UserTypeDatasourceModel{} tflog.Debug(ctx, fmt.Sprintf("users returned from API: %#v", existingUsers)) for _, user := range existingUsers.Items { - mappedUsers = append(mappedUsers, schemas.MapFromUser(user)) + mappedUsers = append(mappedUsers, schemas.MapToUserDatasourceModel(user)) } util.DatasourceResultCount(ctx, "users", len(mappedUsers)) diff --git a/octopusdeploy_framework/framework_provider.go b/octopusdeploy_framework/framework_provider.go index 2da08dea3..a4a6ba9d6 100644 --- a/octopusdeploy_framework/framework_provider.go +++ b/octopusdeploy_framework/framework_provider.go @@ -105,6 +105,7 @@ func (p *octopusDeployFrameworkProvider) Resources(ctx context.Context) []func() NewTenantResource, NewTentacleCertificateResource, NewScriptModuleResource, + NewUserResource, } } diff --git a/octopusdeploy_framework/resource_user.go b/octopusdeploy_framework/resource_user.go new file mode 100644 index 000000000..63e370dc6 --- /dev/null +++ b/octopusdeploy_framework/resource_user.go @@ -0,0 +1,197 @@ +package octopusdeploy_framework + +import ( + "context" + "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/users" + "github.com/OctopusDeploy/terraform-provider-octopusdeploy/internal/errors" + "github.com/OctopusDeploy/terraform-provider-octopusdeploy/octopusdeploy_framework/schemas" + "github.com/OctopusDeploy/terraform-provider-octopusdeploy/octopusdeploy_framework/util" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +var _ resource.ResourceWithImportState = &userTypeResource{} + +type userTypeResource struct { + *Config +} + +func NewUserResource() resource.Resource { return &userTypeResource{} } + +func (r *userTypeResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = util.GetTypeName("user") +} + +func (r *userTypeResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schemas.UserSchema{}.GetResourceSchema() +} + +func (r *userTypeResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + r.Config = ResourceConfiguration(req, resp) +} + +func (r *userTypeResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (r *userTypeResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data schemas.UserTypeResourceModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + newUser := users.NewUser(data.Username.ValueString(), data.DisplayName.ValueString()) + newUser.Password = data.Password.ValueString() + newUser.EmailAddress = data.EmailAddress.ValueString() + newUser.IsActive = data.IsActive.ValueBool() + newUser.IsRequestor = data.IsRequestor.ValueBool() + newUser.IsService = data.IsService.ValueBool() + if len(data.Identity.Elements()) > 0 { + newUser.Identities = mapIdentities(data.Identity) + } + + user, err := users.Add(r.Config.Client, newUser) + if err != nil { + resp.Diagnostics.AddError("Unable to create user", err.Error()) + return + } + + // Octopus doesn't allow creating inactive users. To mimic creating an inactive user, we need to update the newly created user. + if !data.IsActive.ValueBool() { + user.IsActive = data.IsActive.ValueBool() + user, err = users.Update(r.Config.Client, user) + } + + updateUser(&data, user) + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *userTypeResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var data schemas.UserTypeResourceModel + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + user, err := users.GetByID(r.Config.Client, data.ID.ValueString()) + if err != nil { + if err := errors.ProcessApiErrorV2(ctx, resp, data, err, "user"); err != nil { + resp.Diagnostics.AddError("unable to load user", err.Error()) + } + return + } + + updateUser(&data, user) + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *userTypeResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var data, state schemas.UserTypeResourceModel + + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + user, err := users.GetByID(r.Config.Client, data.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("unable to load user", err.Error()) + return + } + + updatedUser := users.NewUser(data.Username.ValueString(), data.DisplayName.ValueString()) + updatedUser.ID = user.ID + updatedUser.Password = data.Password.ValueString() + updatedUser.EmailAddress = data.EmailAddress.ValueString() + updatedUser.IsActive = data.IsActive.ValueBool() + updatedUser.IsRequestor = data.IsRequestor.ValueBool() + updatedUser.IsService = data.IsService.ValueBool() + if len(data.Identity.Elements()) > 0 { + updatedUser.Identities = mapIdentities(data.Identity) + } + + updatedUser, err = users.Update(r.Config.Client, updatedUser) + if err != nil { + resp.Diagnostics.AddError("unable to update user", err.Error()) + return + } + + updateUser(&data, updatedUser) + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *userTypeResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var data schemas.UserTypeResourceModel + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + if err := users.DeleteByID(r.Config.Client, data.ID.ValueString()); err != nil { + resp.Diagnostics.AddError("unable to delete user", err.Error()) + return + } +} + +func updateUser(data *schemas.UserTypeResourceModel, user *users.User) { + data.ID = types.StringValue(user.ID) + data.Username = types.StringValue(user.Username) + data.CanPasswordBeEdited = types.BoolValue(user.CanPasswordBeEdited) + data.DisplayName = types.StringValue(user.DisplayName) + if user.EmailAddress != "" { + data.EmailAddress = types.StringValue(user.EmailAddress) + } + data.IsRequestor = types.BoolValue(user.IsRequestor) + data.IsActive = types.BoolValue(user.IsActive) + data.IsService = types.BoolValue(user.IsService) + data.Identity = types.SetValueMust(types.ObjectType{AttrTypes: schemas.IdentityObjectType()}, schemas.MapIdentities(user.Identities)) +} + +func mapIdentities(identities types.Set) []users.Identity { + result := make([]users.Identity, 0, len(identities.Elements())) + for _, identityElem := range identities.Elements() { + identityObj := identityElem.(types.Object) + identityAttrs := identityObj.Attributes() + + identity := users.Identity{} + if v, ok := identityAttrs["provider"].(types.String); ok && !v.IsNull() { + identity.IdentityProviderName = v.ValueString() + } + + if v, ok := identityAttrs["claim"].(types.Set); ok && !v.IsNull() { + identity.Claims = mapIdentityClaims(v) + } + result = append(result, identity) + } + + return result +} + +func mapIdentityClaims(identityClaims types.Set) map[string]users.IdentityClaim { + result := map[string]users.IdentityClaim{} + for _, identityClaimElem := range identityClaims.Elements() { + identityClaimObj := identityClaimElem.(types.Object) + identityClaimAttrs := identityClaimObj.Attributes() + + identityClaim := users.IdentityClaim{} + var name string + if v, ok := identityClaimAttrs["name"].(types.String); ok && !v.IsNull() { + name = v.ValueString() + } + + if v, ok := identityClaimAttrs["is_identifying_claim"].(types.Bool); ok && !v.IsNull() { + identityClaim.IsIdentifyingClaim = v.ValueBool() + } + + if v, ok := identityClaimAttrs["value"].(types.String); ok && !v.IsNull() { + identityClaim.Value = v.ValueString() + } + + result[name] = identityClaim + } + + return result +} diff --git a/octopusdeploy/resource_user_test.go b/octopusdeploy_framework/resource_user_test.go similarity index 90% rename from octopusdeploy/resource_user_test.go rename to octopusdeploy_framework/resource_user_test.go index e4e65cd68..b18952f79 100644 --- a/octopusdeploy/resource_user_test.go +++ b/octopusdeploy_framework/resource_user_test.go @@ -1,4 +1,4 @@ -package octopusdeploy +package octopusdeploy_framework import ( "fmt" @@ -6,13 +6,12 @@ import ( "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/users" "github.com/OctopusSolutionsEngineering/OctopusTerraformTestFramework/octoclient" "github.com/OctopusSolutionsEngineering/OctopusTerraformTestFramework/test" + "github.com/hashicorp/terraform-plugin-testing/helper/acctest" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" "path/filepath" "strconv" "testing" - - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" - "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" ) func TestAccUserImportBasic(t *testing.T) { @@ -26,7 +25,7 @@ func TestAccUserImportBasic(t *testing.T) { resource.Test(t, resource.TestCase{ CheckDestroy: testAccUserCheckDestroy, - PreCheck: func() { testAccPreCheck(t) }, + PreCheck: func() { TestAccPreCheck(t) }, ProtoV6ProviderFactories: ProtoV6ProviderFactories(), Steps: []resource.TestStep{ { @@ -55,7 +54,7 @@ func TestAccUserBasic(t *testing.T) { resource.Test(t, resource.TestCase{ CheckDestroy: testAccUserCheckDestroy, - PreCheck: func() { testAccPreCheck(t) }, + PreCheck: func() { TestAccPreCheck(t) }, ProtoV6ProviderFactories: ProtoV6ProviderFactories(), Steps: []resource.TestStep{ { @@ -80,6 +79,17 @@ func TestAccUserBasic(t *testing.T) { }) } +func testAccUserImportStateIdFunc(resourceName string) resource.ImportStateIdFunc { + return func(s *terraform.State) (string, error) { + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return "", fmt.Errorf("Not found: %s", resourceName) + } + + return rs.Primary.ID, nil + } +} + func testAccUserImport(localName string, username string) string { return fmt.Sprintf(`resource "octopusdeploy_user" "%s" {}`, localName) } @@ -112,7 +122,7 @@ func testAccUserBasic(localName string, displayName string, isActive bool, isSer func testUserExists(prefix string) resource.TestCheckFunc { return func(s *terraform.State) error { userID := s.RootModule().Resources[prefix].Primary.ID - if _, err := octoClient.Users.GetByID(userID); err != nil { + if _, err := users.GetByID(octoClient, userID); err != nil { return err } @@ -126,7 +136,7 @@ func testAccUserCheckDestroy(s *terraform.State) error { continue } - _, err := octoClient.Users.GetByID(rs.Primary.ID) + _, err := users.GetByID(octoClient, rs.Primary.ID) if err == nil { return fmt.Errorf("user (%s) still exists", rs.Primary.ID) } @@ -207,7 +217,7 @@ func TestUsersAndTeams(t *testing.T) { Take: 1, } - resources, err := client.Users.Get(query) + resources, err := users.Get(client, "", query) if err != nil { return err } diff --git a/octopusdeploy_framework/schemas/schema.go b/octopusdeploy_framework/schemas/schema.go index 0a38087bf..c4b433942 100644 --- a/octopusdeploy_framework/schemas/schema.go +++ b/octopusdeploy_framework/schemas/schema.go @@ -333,11 +333,42 @@ func GetDownloadRetryBackoffSecondsResourceSchema() resourceSchema.Attribute { } } -func GetBooleanResourceAttribute(description string, defaultValue bool, isOptional bool) resourceSchema.Attribute { +func GetValueResourceSchema(isRequired bool) resourceSchema.Attribute { + s := resourceSchema.StringAttribute{ + Description: "The value of this resource.", + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + } + + if isRequired { + s.Required = true + } else { + s.Optional = true + } + + return s +} + +func GetOptionalBooleanResourceAttribute(description string, defaultValue bool) resourceSchema.Attribute { return resourceSchema.BoolAttribute{ Default: booldefault.StaticBool(defaultValue), Description: description, - Optional: isOptional, + Optional: true, + Computed: true, + } +} + +func GetRequiredBooleanResourceAttribute(description string) resourceSchema.Attribute { + return resourceSchema.BoolAttribute{ + Description: description, + Required: true, + } +} + +func GetReadonlyBooleanResourceAttribute(description string) resourceSchema.Attribute { + return resourceSchema.BoolAttribute{ + Description: description, Computed: true, } } diff --git a/octopusdeploy_framework/schemas/user.go b/octopusdeploy_framework/schemas/user.go index 5333f018e..0fac5d2f0 100644 --- a/octopusdeploy_framework/schemas/user.go +++ b/octopusdeploy_framework/schemas/user.go @@ -2,6 +2,7 @@ package schemas import ( "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/users" + "github.com/OctopusDeploy/terraform-provider-octopusdeploy/octopusdeploy_framework/util" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/attr" datasourceSchema "github.com/hashicorp/terraform-plugin-framework/datasource/schema" @@ -73,10 +74,7 @@ func (u UserSchema) GetDatasourceSchemaAttributes() map[string]datasourceSchema. Optional: true, NestedObject: datasourceSchema.NestedAttributeObject{ Attributes: map[string]datasourceSchema.Attribute{ - "provider": datasourceSchema.StringAttribute{ - Description: "The identity provider.", - Computed: true, - }, + "provider": GetProviderDatasourceSchema(), "claim": datasourceSchema.SetNestedAttribute{ Description: "The claim associated with the identity.", Computed: true, @@ -133,6 +131,15 @@ func GetEmailAddressDatasourceSchema() datasourceSchema.Attribute { return s } +func GetProviderDatasourceSchema() datasourceSchema.Attribute { + s := datasourceSchema.StringAttribute{ + Description: "The identity provider.", + Computed: true, + } + + return s +} + func IdentityObjectType() map[string]attr.Type { return map[string]attr.Type{ "provider": types.StringType, @@ -149,7 +156,78 @@ func IdentityClaimObjectType() map[string]attr.Type { } func (u UserSchema) GetResourceSchema() resourceSchema.Schema { - return resourceSchema.Schema{} + return resourceSchema.Schema{ + Description: util.GetResourceSchemaDescription(UserResourceDescription), + Attributes: map[string]resourceSchema.Attribute{ + "id": GetIdResourceSchema(), + "username": GetUsernameResourceSchema(true), + "password": GetPasswordResourceSchema(false), + "display_name": GetDisplayNameResourceSchema(), + "can_password_be_edited": GetReadonlyBooleanResourceAttribute("Specifies whether or not the password can be edited."), + "email_address": GetEmailAddressResourceSchema(), + "is_active": GetOptionalBooleanResourceAttribute("Specifies whether or not the user is active.", true), + "is_requestor": GetReadonlyBooleanResourceAttribute("Specifies whether or not the user is the requestor."), + "is_service": GetOptionalBooleanResourceAttribute("Specifies whether or not the user is a service account.", false), + }, + Blocks: map[string]resourceSchema.Block{ + "identity": resourceSchema.SetNestedBlock{ + Description: "The identities associated with the user.", + NestedObject: resourceSchema.NestedBlockObject{ + Attributes: map[string]resourceSchema.Attribute{ + "provider": GetProviderResourceSchema(), + }, + Blocks: map[string]resourceSchema.Block{ + "claim": resourceSchema.SetNestedBlock{ + Description: "The claim associated with the identity.", + NestedObject: resourceSchema.NestedBlockObject{ + Attributes: map[string]resourceSchema.Attribute{ + "name": GetNameResourceSchema(true), + "is_identifying_claim": GetRequiredBooleanResourceAttribute("Specifies whether or not the claim is an identifying claim."), + "value": GetValueResourceSchema(true), + }, + }, + }, + }, + }, + }, + }, + } +} + +func GetDisplayNameResourceSchema() resourceSchema.Attribute { + s := resourceSchema.StringAttribute{ + Description: "The display name of this resource.", + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + Required: true, + } + + return s +} + +func GetEmailAddressResourceSchema() datasourceSchema.Attribute { + s := datasourceSchema.StringAttribute{ + Description: "The email address of this resource.", + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + Optional: true, + } + + return s +} + +func GetProviderResourceSchema() resourceSchema.Attribute { + s := resourceSchema.StringAttribute{ + Description: "The identity provider.", + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + }, + Optional: true, + } + + return s } func MapIdentityClaims(claims map[string]users.IdentityClaim) []attr.Value { @@ -177,8 +255,8 @@ func MapIdentities(identities []users.Identity) []attr.Value { return identitiesList } -func MapFromUser(u *users.User) UserTypeResourceModel { - var user UserTypeResourceModel +func MapToUserDatasourceModel(u *users.User) UserTypeDatasourceModel { + var user UserTypeDatasourceModel user.ID = types.StringValue(u.ID) user.Username = types.StringValue(u.Username) user.CanPasswordBeEdited = types.BoolValue(u.CanPasswordBeEdited) @@ -192,7 +270,7 @@ func MapFromUser(u *users.User) UserTypeResourceModel { return user } -type UserTypeResourceModel struct { +type UserTypeDatasourceModel struct { Username types.String `tfsdk:"username"` CanPasswordBeEdited types.Bool `tfsdk:"can_password_be_edited"` DisplayName types.String `tfsdk:"display_name"` @@ -204,3 +282,9 @@ type UserTypeResourceModel struct { ResourceModel } + +type UserTypeResourceModel struct { + Password types.String `tfsdk:"password"` + + UserTypeDatasourceModel +} diff --git a/octopusdeploy_framework/schemas/variable.go b/octopusdeploy_framework/schemas/variable.go index 9d2ffb6c4..ab2a30963 100644 --- a/octopusdeploy_framework/schemas/variable.go +++ b/octopusdeploy_framework/schemas/variable.go @@ -157,14 +157,8 @@ func (v VariableSchema) GetResourceSchema() resourceSchema.Schema { stringvalidator.ConflictsWith(path.MatchRelative().AtParent().AtName(VariableSchemaAttributeNames.OwnerID)), }, }, - VariableSchemaAttributeNames.IsEditable: GetBooleanResourceAttribute( - "Indicates whether or not this variable is considered editable.", - true, - true), - VariableSchemaAttributeNames.IsSensitive: GetBooleanResourceAttribute( - "Indicates whether or not this resource is considered sensitive and should be kept secret.", - false, - true), + VariableSchemaAttributeNames.IsEditable: GetOptionalBooleanResourceAttribute("Indicates whether or not this variable is considered editable.", true), + VariableSchemaAttributeNames.IsSensitive: GetOptionalBooleanResourceAttribute("Indicates whether or not this resource is considered sensitive and should be kept secret.", false), VariableSchemaAttributeNames.SensitiveValue: resourceSchema.StringAttribute{ Optional: true, Sensitive: true,