diff --git a/docs/data-sources/users.md b/docs/data-sources/users.md
index ca9f963dd..05fe88f50 100644
--- a/docs/data-sources/users.md
+++ b/docs/data-sources/users.md
@@ -25,48 +25,56 @@ data "octopusdeploy_users" "example" {
### Optional
-- `filter` (String) A filter with which to search.
+- `filter` (String) A filter search by username, display name or email
- `ids` (List of String) A filter to search by a list of IDs.
- `skip` (Number) A filter to specify the number of items to skip in the response.
-- `space_id` (String) A Space ID to filter by. Will revert what is specified on the provider if not set.
+- `space_id` (String, Deprecated) The space ID associated with this user.
- `take` (Number) A filter to specify the number of items to take (or return) in the response.
### Read-Only
-- `id` (String) An auto-generated identifier that includes the timestamp when this data source was last modified.
-- `users` (List of Object) A list of users that match the filter(s). (see [below for nested schema](#nestedatt--users))
+- `id` (String) The unique ID for this resource.
+- `users` (Attributes List) (see [below for nested schema](#nestedatt--users))
### Nested Schema for `users`
+Required:
+
+- `display_name` (String) The display name of this resource.
+- `username` (String) The username associated with this resource.
+
+Optional:
+
+- `can_password_be_edited` (Boolean) Specifies whether or not the password can be edited.
+- `email_address` (String) The email address of this resource.
+- `identity` (Attributes Set) The identities associated with the user. (see [below for nested schema](#nestedatt--users--identity))
+- `is_active` (Boolean) Specifies whether or not the user is active.
+- `is_requestor` (Boolean) Specifies whether or not the user is the requestor.
+- `is_service` (Boolean) Specifies whether or not the user is a service account.
+
Read-Only:
-- `can_password_be_edited` (Boolean)
-- `display_name` (String)
-- `email_address` (String)
-- `id` (String)
-- `identity` (Set of Object) (see [below for nested schema](#nestedobjatt--users--identity))
-- `is_active` (Boolean)
-- `is_requestor` (Boolean)
-- `is_service` (Boolean)
-- `password` (String)
-- `username` (String)
-
-
+- `id` (String) The unique ID for this resource.
+
+
### Nested Schema for `users.identity`
Read-Only:
-- `claim` (Set of Object) (see [below for nested schema](#nestedobjatt--users--identity--claim))
-- `provider` (String)
+- `claim` (Attributes Set) The claim associated with the identity. (see [below for nested schema](#nestedatt--users--identity--claim))
+- `provider` (String) The identity provider.
-
+
### Nested Schema for `users.identity.claim`
-Read-Only:
+Required:
+
+- `name` (String) The name of this resource.
+- `value` (String) The value of this resource.
+
+Optional:
-- `is_identifying_claim` (Boolean)
-- `name` (String)
-- `value` (String)
+- `is_identifying_claim` (Boolean) Specifies whether or not the claim is an identifying claim.
diff --git a/octopusdeploy/data_source_users.go b/octopusdeploy/data_source_users.go
deleted file mode 100644
index c541d86d7..000000000
--- a/octopusdeploy/data_source_users.go
+++ /dev/null
@@ -1,46 +0,0 @@
-package octopusdeploy
-
-import (
- "context"
- "time"
-
- "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/client"
- "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/users"
- "github.com/hashicorp/terraform-plugin-sdk/v2/diag"
- "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
-)
-
-func dataSourceUsers() *schema.Resource {
- return &schema.Resource{
- Description: "Provides information about existing users.",
- ReadContext: dataSourceUsersRead,
- Schema: getUserDataSchema(),
- }
-}
-
-func dataSourceUsersRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
- query := users.UsersQuery{
- Filter: d.Get("filter").(string),
- IDs: expandArray(d.Get("ids").([]interface{})),
- Skip: d.Get("skip").(int),
- Take: d.Get("take").(int),
- }
-
- spaceID := d.Get("space_id").(string)
-
- client := meta.(*client.Client)
- existingUsers, err := users.Get(client, spaceID, query)
- if err != nil {
- return diag.FromErr(err)
- }
-
- flattenedUsers := []interface{}{}
- for _, user := range existingUsers.Items {
- flattenedUsers = append(flattenedUsers, flattenUser(user))
- }
-
- d.Set("users", flattenedUsers)
- d.SetId("Users " + time.Now().UTC().String())
-
- return nil
-}
diff --git a/octopusdeploy/provider.go b/octopusdeploy/provider.go
index 5f1a0822f..786ce3e1c 100644
--- a/octopusdeploy/provider.go
+++ b/octopusdeploy/provider.go
@@ -28,7 +28,6 @@ func Provider() *schema.Provider {
"octopusdeploy_polling_tentacle_deployment_targets": dataSourcePollingTentacleDeploymentTargets(),
"octopusdeploy_ssh_connection_deployment_targets": dataSourceSSHConnectionDeploymentTargets(),
"octopusdeploy_teams": dataSourceTeams(),
- "octopusdeploy_users": dataSourceUsers(),
"octopusdeploy_user_roles": dataSourceUserRoles(),
"octopusdeploy_worker_pools": dataSourceWorkerPools(),
},
diff --git a/octopusdeploy_framework/datasource_users.go b/octopusdeploy_framework/datasource_users.go
new file mode 100644
index 000000000..c030f4ca2
--- /dev/null
+++ b/octopusdeploy_framework/datasource_users.go
@@ -0,0 +1,80 @@
+package octopusdeploy_framework
+
+import (
+ "context"
+ "fmt"
+ "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/users"
+ "github.com/OctopusDeploy/terraform-provider-octopusdeploy/octopusdeploy_framework/schemas"
+ "github.com/OctopusDeploy/terraform-provider-octopusdeploy/octopusdeploy_framework/util"
+ "github.com/hashicorp/terraform-plugin-framework/datasource"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+ "github.com/hashicorp/terraform-plugin-log/tflog"
+ "time"
+)
+
+type userDataSource struct {
+ *Config
+}
+
+type usersDataSourceModel struct {
+ ID types.String `tfsdk:"id"`
+ SpaceID types.String `tfsdk:"space_id"`
+ IDs types.List `tfsdk:"ids"`
+ Filter types.String `tfsdk:"filter"`
+ Skip types.Int64 `tfsdk:"skip"`
+ Take types.Int64 `tfsdk:"take"`
+ Users types.List `tfsdk:"users"`
+}
+
+func NewUsersDataSource() datasource.DataSource {
+ return &userDataSource{}
+}
+
+func (u *userDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
+ resp.TypeName = util.GetTypeName("users")
+}
+
+func (u *userDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) {
+ resp.Schema = schemas.UserSchema{}.GetDatasourceSchema()
+}
+
+func (u *userDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
+ u.Config = DataSourceConfiguration(req, resp)
+}
+
+func (u *userDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
+ var err error
+ var data usersDataSourceModel
+ resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ query := users.UsersQuery{
+ IDs: util.GetIds(data.IDs),
+ Filter: data.Filter.ValueString(),
+ Skip: util.GetNumber(data.Skip),
+ Take: util.GetNumber(data.Take),
+ }
+
+ util.DatasourceReading(ctx, "users", query)
+
+ existingUsers, err := users.Get(u.Client, data.SpaceID.ValueString(), query)
+ if err != nil {
+ resp.Diagnostics.AddError("unable to load users", err.Error())
+ return
+ }
+
+ mappedUsers := []schemas.UserTypeResourceModel{}
+ tflog.Debug(ctx, fmt.Sprintf("users returned from API: %#v", existingUsers))
+ for _, user := range existingUsers.Items {
+ mappedUsers = append(mappedUsers, schemas.MapFromUser(user))
+ }
+
+ util.DatasourceResultCount(ctx, "users", len(mappedUsers))
+
+ data.Users, _ = types.ListValueFrom(ctx, types.ObjectType{AttrTypes: schemas.UserObjectType()}, mappedUsers)
+ data.ID = types.StringValue("Users " + time.Now().UTC().String())
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
diff --git a/octopusdeploy/data_source_users_test.go b/octopusdeploy_framework/datasource_users_test.go
similarity index 79%
rename from octopusdeploy/data_source_users_test.go
rename to octopusdeploy_framework/datasource_users_test.go
index e022c30ce..d0984cc70 100644
--- a/octopusdeploy/data_source_users_test.go
+++ b/octopusdeploy_framework/datasource_users_test.go
@@ -1,29 +1,28 @@
-package octopusdeploy
+package octopusdeploy_framework
import (
"fmt"
+ "github.com/hashicorp/terraform-plugin-testing/helper/acctest"
+ "github.com/hashicorp/terraform-plugin-testing/helper/resource"
+ "github.com/hashicorp/terraform-plugin-testing/terraform"
"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 TestAccDataSourceUsers(t *testing.T) {
localName := acctest.RandStringFromCharSet(20, acctest.CharSetAlpha)
name := fmt.Sprintf("data.octopusdeploy_users.%s", localName)
username := "d"
-
resource.Test(t, resource.TestCase{
- PreCheck: func() { testAccPreCheck(t) },
ProtoV6ProviderFactories: ProtoV6ProviderFactories(),
+ PreCheck: func() { TestAccPreCheck(t) },
Steps: []resource.TestStep{
{
- Config: testAccDataSourceUsersConfig(localName, username),
Check: resource.ComposeTestCheckFunc(
testAccCheckUsersDataSourceID(name),
resource.TestCheckResourceAttrSet(name, "users.#"),
- )},
+ ),
+ Config: testAccDataSourceUsersConfig(localName, username),
+ },
},
})
}
diff --git a/octopusdeploy_framework/framework_provider.go b/octopusdeploy_framework/framework_provider.go
index 265eeac96..2da08dea3 100644
--- a/octopusdeploy_framework/framework_provider.go
+++ b/octopusdeploy_framework/framework_provider.go
@@ -74,6 +74,7 @@ func (p *octopusDeployFrameworkProvider) DataSources(ctx context.Context) []func
NewTagSetsDataSource,
NewScriptModuleDataSource,
NewTenantProjectDataSource,
+ NewUsersDataSource,
}
}
diff --git a/octopusdeploy_framework/schemas/schema.go b/octopusdeploy_framework/schemas/schema.go
index 3160e01ab..0a38087bf 100644
--- a/octopusdeploy_framework/schemas/schema.go
+++ b/octopusdeploy_framework/schemas/schema.go
@@ -126,6 +126,40 @@ func GetReadonlyDescriptionDatasourceSchema(resourceDescription string) datasour
}
}
+func GetUsernameDatasourceSchema(isRequired bool) datasourceSchema.Attribute {
+ s := datasourceSchema.StringAttribute{
+ Description: "The username associated with this resource.",
+ Validators: []validator.String{
+ stringvalidator.LengthAtLeast(1),
+ },
+ }
+
+ if isRequired {
+ s.Required = true
+ } else {
+ s.Optional = true
+ }
+
+ return s
+}
+
+func GetValueDatasourceSchema(isRequired bool) datasourceSchema.Attribute {
+ s := datasourceSchema.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 GetIdResourceSchema() resourceSchema.Attribute {
return resourceSchema.StringAttribute{
Description: "The unique ID for this resource.",
diff --git a/octopusdeploy_framework/schemas/user.go b/octopusdeploy_framework/schemas/user.go
new file mode 100644
index 000000000..5333f018e
--- /dev/null
+++ b/octopusdeploy_framework/schemas/user.go
@@ -0,0 +1,206 @@
+package schemas
+
+import (
+ "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/users"
+ "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
+ "github.com/hashicorp/terraform-plugin-framework/attr"
+ datasourceSchema "github.com/hashicorp/terraform-plugin-framework/datasource/schema"
+ resourceSchema "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/schema/validator"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+const (
+ UserResourceDescription = "user"
+)
+
+type UserSchema struct{}
+
+var _ EntitySchema = UserSchema{}
+
+func UserObjectType() map[string]attr.Type {
+ return map[string]attr.Type{
+ "id": types.StringType,
+ "username": types.StringType,
+ "can_password_be_edited": types.BoolType,
+ "display_name": types.StringType,
+ "email_address": types.StringType,
+ "is_active": types.BoolType,
+ "is_requestor": types.BoolType,
+ "is_service": types.BoolType,
+ "identity": types.SetType{
+ ElemType: types.ObjectType{AttrTypes: IdentityObjectType()},
+ },
+ }
+}
+
+func (u UserSchema) GetDatasourceSchema() datasourceSchema.Schema {
+ return datasourceSchema.Schema{
+ Description: "Provides information about existing users.",
+ Attributes: map[string]datasourceSchema.Attribute{
+ //request
+ "ids": GetQueryIDsDatasourceSchema(),
+ "space_id": GetUserSpaceIdDatasourceSchema(),
+ "filter": GetFilterDatasourceSchema(),
+ "skip": GetQuerySkipDatasourceSchema(),
+ "take": GetQueryTakeDatasourceSchema(),
+
+ //response
+ "id": GetIdDatasourceSchema(true),
+ "users": datasourceSchema.ListNestedAttribute{
+ Computed: true,
+ Optional: false,
+ NestedObject: datasourceSchema.NestedAttributeObject{
+ Attributes: u.GetDatasourceSchemaAttributes(),
+ },
+ },
+ },
+ }
+}
+
+func (u UserSchema) GetDatasourceSchemaAttributes() map[string]datasourceSchema.Attribute {
+ return map[string]datasourceSchema.Attribute{
+ "id": GetIdDatasourceSchema(true),
+ "username": GetUsernameDatasourceSchema(true),
+ "can_password_be_edited": GetBooleanDatasourceAttribute("Specifies whether or not the password can be edited.", true),
+ "display_name": GetDisplayNameDatasourceSchema(),
+ "email_address": GetEmailAddressDatasourceSchema(),
+ "is_active": GetBooleanDatasourceAttribute("Specifies whether or not the user is active.", true),
+ "is_requestor": GetBooleanDatasourceAttribute("Specifies whether or not the user is the requestor.", true),
+ "is_service": GetBooleanDatasourceAttribute("Specifies whether or not the user is a service account.", true),
+ "identity": datasourceSchema.SetNestedAttribute{
+ Description: "The identities associated with the user.",
+ Optional: true,
+ NestedObject: datasourceSchema.NestedAttributeObject{
+ Attributes: map[string]datasourceSchema.Attribute{
+ "provider": datasourceSchema.StringAttribute{
+ Description: "The identity provider.",
+ Computed: true,
+ },
+ "claim": datasourceSchema.SetNestedAttribute{
+ Description: "The claim associated with the identity.",
+ Computed: true,
+ NestedObject: datasourceSchema.NestedAttributeObject{
+ Attributes: map[string]datasourceSchema.Attribute{
+ "name": GetNameDatasourceSchema(true),
+ "is_identifying_claim": GetBooleanDatasourceAttribute("Specifies whether or not the claim is an identifying claim.", true),
+ "value": GetValueDatasourceSchema(true),
+ },
+ },
+ },
+ },
+ },
+ },
+ }
+}
+
+func GetUserSpaceIdDatasourceSchema() datasourceSchema.Attribute {
+ return datasourceSchema.StringAttribute{
+ Description: "The space ID associated with this user.",
+ Optional: true,
+ DeprecationMessage: "This attribute is deprecated and will be removed in a future release. Users are not scoped to spaces, meaning providing a space ID will not affect the result.",
+ }
+}
+
+func GetFilterDatasourceSchema() datasourceSchema.Attribute {
+ return datasourceSchema.StringAttribute{
+ Description: "A filter search by username, display name or email",
+ Optional: true,
+ }
+}
+
+func GetDisplayNameDatasourceSchema() datasourceSchema.Attribute {
+ s := datasourceSchema.StringAttribute{
+ Description: "The display name of this resource.",
+ Validators: []validator.String{
+ stringvalidator.LengthAtLeast(1),
+ },
+ Required: true,
+ }
+
+ return s
+}
+
+func GetEmailAddressDatasourceSchema() datasourceSchema.Attribute {
+ s := datasourceSchema.StringAttribute{
+ Description: "The email address of this resource.",
+ Validators: []validator.String{
+ stringvalidator.LengthAtLeast(1),
+ },
+ Optional: true,
+ }
+
+ return s
+}
+
+func IdentityObjectType() map[string]attr.Type {
+ return map[string]attr.Type{
+ "provider": types.StringType,
+ "claim": types.SetType{ElemType: types.ObjectType{AttrTypes: IdentityClaimObjectType()}},
+ }
+}
+
+func IdentityClaimObjectType() map[string]attr.Type {
+ return map[string]attr.Type{
+ "name": types.StringType,
+ "is_identifying_claim": types.BoolType,
+ "value": types.StringType,
+ }
+}
+
+func (u UserSchema) GetResourceSchema() resourceSchema.Schema {
+ return resourceSchema.Schema{}
+}
+
+func MapIdentityClaims(claims map[string]users.IdentityClaim) []attr.Value {
+ claimsList := make([]attr.Value, 0, len(claims))
+ for key, claim := range claims {
+ claimMap := map[string]attr.Value{
+ "is_identifying_claim": types.BoolValue(claim.IsIdentifyingClaim),
+ "name": types.StringValue(key),
+ "value": types.StringValue(claim.Value),
+ }
+ claimsList = append(claimsList, types.ObjectValueMust(IdentityClaimObjectType(), claimMap))
+ }
+ return claimsList
+}
+
+func MapIdentities(identities []users.Identity) []attr.Value {
+ identitiesList := make([]attr.Value, 0, len(identities))
+ for _, identity := range identities {
+ identityMap := map[string]attr.Value{
+ "provider": types.StringValue(identity.IdentityProviderName),
+ "claim": types.SetValueMust(types.ObjectType{AttrTypes: IdentityClaimObjectType()}, MapIdentityClaims(identity.Claims)),
+ }
+ identitiesList = append(identitiesList, types.ObjectValueMust(IdentityObjectType(), identityMap))
+ }
+ return identitiesList
+}
+
+func MapFromUser(u *users.User) UserTypeResourceModel {
+ var user UserTypeResourceModel
+ user.ID = types.StringValue(u.ID)
+ user.Username = types.StringValue(u.Username)
+ user.CanPasswordBeEdited = types.BoolValue(u.CanPasswordBeEdited)
+ user.DisplayName = types.StringValue(u.DisplayName)
+ user.EmailAddress = types.StringValue(u.EmailAddress)
+ user.IsActive = types.BoolValue(u.IsActive)
+ user.IsRequestor = types.BoolValue(u.IsRequestor)
+ user.IsService = types.BoolValue(u.IsService)
+ user.Identity = types.SetValueMust(types.ObjectType{AttrTypes: IdentityObjectType()}, MapIdentities(u.Identities))
+
+ return user
+}
+
+type UserTypeResourceModel struct {
+ Username types.String `tfsdk:"username"`
+ CanPasswordBeEdited types.Bool `tfsdk:"can_password_be_edited"`
+ DisplayName types.String `tfsdk:"display_name"`
+ EmailAddress types.String `tfsdk:"email_address"`
+ IsActive types.Bool `tfsdk:"is_active"`
+ IsRequestor types.Bool `tfsdk:"is_requestor"`
+ IsService types.Bool `tfsdk:"is_service"`
+ Identity types.Set `tfsdk:"identity"`
+
+ ResourceModel
+}