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,