From dc475b4105c487ca8ce737c28b8ca08c7663826a Mon Sep 17 00:00:00 2001 From: Ethan Dickson Date: Fri, 12 Jul 2024 08:51:35 +0000 Subject: [PATCH 1/9] feat: add coderd_user data source --- docs/data-sources/example.md | 30 ---- docs/data-sources/user.md | 29 +++ integration/integration_test.go | 14 ++ integration/user-test/main.tf | 4 + internal/provider/example_data_source.go | 105 ----------- internal/provider/example_data_source_test.go | 32 ---- internal/provider/provider.go | 2 +- internal/provider/user_data_source.go | 167 ++++++++++++++++++ internal/provider/user_data_source_test.go | 78 ++++++++ internal/provider/user_resource.go | 6 +- 10 files changed, 296 insertions(+), 171 deletions(-) delete mode 100644 docs/data-sources/example.md create mode 100644 docs/data-sources/user.md delete mode 100644 internal/provider/example_data_source.go delete mode 100644 internal/provider/example_data_source_test.go create mode 100644 internal/provider/user_data_source.go create mode 100644 internal/provider/user_data_source_test.go diff --git a/docs/data-sources/example.md b/docs/data-sources/example.md deleted file mode 100644 index e8f592c..0000000 --- a/docs/data-sources/example.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -# generated by https://github.com/hashicorp/terraform-plugin-docs -page_title: "coderd_example Data Source - coderd" -subcategory: "" -description: |- - Example data source ---- - -# coderd_example (Data Source) - -Example data source - -## Example Usage - -```terraform -data "coderd_example" "example" { - configurable_attribute = "some-value" -} -``` - - -## Schema - -### Optional - -- `configurable_attribute` (String) Example configurable attribute - -### Read-Only - -- `id` (String) Example identifier diff --git a/docs/data-sources/user.md b/docs/data-sources/user.md new file mode 100644 index 0000000..ece2778 --- /dev/null +++ b/docs/data-sources/user.md @@ -0,0 +1,29 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "coderd_user Data Source - coderd" +subcategory: "" +description: |- + An existing user on the coder deployment +--- + +# coderd_user (Data Source) + +An existing user on the coder deployment + + + + +## Schema + +### Optional + +- `id` (String) The ID of the user to retrieve. This field will be populated if a username is supplied +- `username` (String) The username of the user to retrieve. This field will be populated if an ID is supplied + +### Read-Only + +- `email` (String) Email of the user. +- `login_type` (String) Type of login for the user. Valid types are 'none', 'password', 'github', and 'oidc'. +- `name` (String) Display name of the user. Defaults to username. +- `roles` (Set of String) Roles assigned to the user. Valid roles are 'owner', 'template-admin', 'user-admin', and 'auditor'. +- `suspended` (Boolean) Whether the user is suspended. diff --git a/integration/integration_test.go b/integration/integration_test.go index a038274..a116b39 100644 --- a/integration/integration_test.go +++ b/integration/integration_test.go @@ -49,10 +49,23 @@ func TestIntegration(t *testing.T) { for _, tt := range []struct { name string + preF func(testing.TB, *codersdk.Client) assertF func(testing.TB, *codersdk.Client) }{ { name: "user-test", + preF: func(t testing.TB, c *codersdk.Client) { + me, err := c.User(ctx, codersdk.Me) + require.NoError(t, err) + c.CreateUser(ctx, codersdk.CreateUserRequest{ + Email: "test2@coder.com", + Username: "ethan", + Password: "SomeSecurePassword!", + UserLoginType: "password", + DisableLogin: false, + OrganizationID: me.OrganizationIDs[0], + }) + }, assertF: func(t testing.TB, c *codersdk.Client) { // Check user fields. user, err := c.User(ctx, "dean") @@ -102,6 +115,7 @@ func TestIntegration(t *testing.T) { var buf bytes.Buffer tfCmd.Stdout = &buf tfCmd.Stderr = &buf + tt.preF(t, client) if err := tfCmd.Run(); !assert.NoError(t, err) { t.Logf(buf.String()) } diff --git a/integration/user-test/main.tf b/integration/user-test/main.tf index 34174b2..b0cb017 100644 --- a/integration/user-test/main.tf +++ b/integration/user-test/main.tf @@ -16,3 +16,7 @@ resource "coderd_user" "dean" { password = "SomeSecurePassword!" suspended = false } + +data "coderd_user" "ethan" { + username = "ethan" +} diff --git a/internal/provider/example_data_source.go b/internal/provider/example_data_source.go deleted file mode 100644 index 585b9d2..0000000 --- a/internal/provider/example_data_source.go +++ /dev/null @@ -1,105 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package provider - -import ( - "context" - "fmt" - "net/http" - - "github.com/hashicorp/terraform-plugin-framework/datasource" - "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-log/tflog" -) - -// Ensure provider defined types fully satisfy framework interfaces. -var _ datasource.DataSource = &ExampleDataSource{} - -func NewExampleDataSource() datasource.DataSource { - return &ExampleDataSource{} -} - -// ExampleDataSource defines the data source implementation. -type ExampleDataSource struct { - client *http.Client -} - -// ExampleDataSourceModel describes the data source data model. -type ExampleDataSourceModel struct { - ConfigurableAttribute types.String `tfsdk:"configurable_attribute"` - Id types.String `tfsdk:"id"` -} - -func (d *ExampleDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_example" -} - -func (d *ExampleDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) { - resp.Schema = schema.Schema{ - // This description is used by the documentation generator and the language server. - MarkdownDescription: "Example data source", - - Attributes: map[string]schema.Attribute{ - "configurable_attribute": schema.StringAttribute{ - MarkdownDescription: "Example configurable attribute", - Optional: true, - }, - "id": schema.StringAttribute{ - MarkdownDescription: "Example identifier", - Computed: true, - }, - }, - } -} - -func (d *ExampleDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { - // Prevent panic if the provider has not been configured. - if req.ProviderData == nil { - return - } - - client, ok := req.ProviderData.(*http.Client) - - if !ok { - resp.Diagnostics.AddError( - "Unexpected Data Source Configure Type", - fmt.Sprintf("Expected *http.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData), - ) - - return - } - - d.client = client -} - -func (d *ExampleDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { - var data ExampleDataSourceModel - - // Read Terraform configuration data into the model - resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) - - if resp.Diagnostics.HasError() { - return - } - - // If applicable, this is a great opportunity to initialize any necessary - // provider client data and make a call using it. - // httpResp, err := d.client.Do(httpReq) - // if err != nil { - // resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to read example, got error: %s", err)) - // return - // } - - // For the purposes of this example code, hardcoding a response value to - // save into the Terraform state. - data.Id = types.StringValue("example-id") - - // Write logs using the tflog package - // Documentation: https://terraform.io/plugin/log - tflog.Trace(ctx, "read a data source") - - // Save data into Terraform state - resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) -} diff --git a/internal/provider/example_data_source_test.go b/internal/provider/example_data_source_test.go deleted file mode 100644 index 4b66984..0000000 --- a/internal/provider/example_data_source_test.go +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package provider - -import ( - "testing" - - "github.com/hashicorp/terraform-plugin-testing/helper/resource" -) - -func TestAccExampleDataSource(t *testing.T) { - resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, - Steps: []resource.TestStep{ - // Read testing - { - Config: testAccExampleDataSourceConfig, - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr("data.coderd_example.test", "id", "example-id"), - ), - }, - }, - }) -} - -const testAccExampleDataSourceConfig = ` -data "coderd_example" "test" { - configurable_attribute = "example" -} -` diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 60bcc6f..dfc531c 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -112,7 +112,7 @@ func (p *CoderdProvider) Resources(ctx context.Context) []func() resource.Resour func (p *CoderdProvider) DataSources(ctx context.Context) []func() datasource.DataSource { return []func() datasource.DataSource{ - NewExampleDataSource, + NewUserDataSource, } } diff --git a/internal/provider/user_data_source.go b/internal/provider/user_data_source.go new file mode 100644 index 0000000..6e4faef --- /dev/null +++ b/internal/provider/user_data_source.go @@ -0,0 +1,167 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package provider + +import ( + "context" + "fmt" + + "github.com/coder/coder/v2/codersdk" + "github.com/hashicorp/terraform-plugin-framework-validators/datasourcevalidator" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Ensure provider defined types fully satisfy framework interfaces. +var _ datasource.DataSource = &UserDataSource{} +var _ datasource.DataSourceWithConfigValidators = &UserDataSource{} + +func NewUserDataSource() datasource.DataSource { + return &UserDataSource{} +} + +// UserDataSource defines the data source implementation. +type UserDataSource struct { + data *CoderdProviderData +} + +// UserDataSourceModel describes the data source data model. +type UserDataSourceModel struct { + // Username or ID must be set + ID types.String `tfsdk:"id"` + Username types.String `tfsdk:"username"` + + Name types.String `tfsdk:"name"` + Email types.String `tfsdk:"email"` + Roles types.Set `tfsdk:"roles"` // owner, template-admin, user-admin, auditor (member is implicit) + LoginType types.String `tfsdk:"login_type"` // none, password, github, oidc + Suspended types.Bool `tfsdk:"suspended"` +} + +func (d *UserDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_user" +} + +func (d *UserDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + MarkdownDescription: "An existing user on the coder deployment", + + // Validation handled by ConfigValidators + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + MarkdownDescription: "The ID of the user to retrieve. This field will be populated if a username is supplied", + Optional: true, + }, + "username": schema.StringAttribute{ + MarkdownDescription: "The username of the user to retrieve. This field will be populated if an ID is supplied", + Optional: true, + }, + "email": schema.StringAttribute{ + MarkdownDescription: "Email of the user.", + Computed: true, + }, + "name": schema.StringAttribute{ + MarkdownDescription: "Display name of the user. Defaults to username.", + Computed: true, + }, + "roles": schema.SetAttribute{ + MarkdownDescription: "Roles assigned to the user. Valid roles are 'owner', 'template-admin', 'user-admin', and 'auditor'.", + Computed: true, + ElementType: types.StringType, + // Validators: []validator.Set{ + // setvalidator.ValueStringsAre( + // stringvalidator.OneOf("owner", "template-admin", "user-admin", "auditor"), + // ), + // }, + }, + "login_type": schema.StringAttribute{ + MarkdownDescription: "Type of login for the user. Valid types are 'none', 'password', 'github', and 'oidc'.", + Computed: true, + // Validators: []validator.String{ + // stringvalidator.OneOf("none", "password", "github", "oidc"), + // }, + }, + "suspended": schema.BoolAttribute{ + MarkdownDescription: "Whether the user is suspended.", + Computed: true, + }, + }, + } +} + +func (d *UserDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + // Prevent panic if the provider has not been configured. + if req.ProviderData == nil { + return + } + + data, ok := req.ProviderData.(*CoderdProviderData) + + if !ok { + resp.Diagnostics.AddError( + "Unexpected Data Source Configure Type", + fmt.Sprintf("Expected *CoderdProviderData, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + + return + } + + d.data = data +} + +func (d *UserDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + var data UserDataSourceModel + + // Read Terraform configuration data into the model + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + client := d.data.Client + + var ident string + if !data.ID.IsNull() { + ident = data.ID.ValueString() + } else { + ident = data.Username.ValueString() + } + user, err := client.User(ctx, ident) + if err != nil { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to get current user, got error: %s", err)) + return + } + if len(user.OrganizationIDs) < 1 { + resp.Diagnostics.AddError("Client Error", "User is not associated with any organizations") + return + } + + data.ID = types.StringValue(user.ID.String()) + data.Email = types.StringValue(user.Email) + data.Username = types.StringValue(user.Username) + data.Name = types.StringValue(user.Name) + roles := make([]attr.Value, 0, len(user.Roles)) + for _, role := range user.Roles { + roles = append(roles, types.StringValue(role.Name)) + } + data.Roles = types.SetValueMust(types.StringType, roles) + data.LoginType = types.StringValue(string(user.LoginType)) + data.Suspended = types.BoolValue(user.Status == codersdk.UserStatusSuspended) + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (d *UserDataSource) ConfigValidators(context.Context) []datasource.ConfigValidator { + return []datasource.ConfigValidator{ + datasourcevalidator.AtLeastOneOf( + path.MatchRoot("id"), + path.MatchRoot("username"), + ), + } +} diff --git a/internal/provider/user_data_source_test.go b/internal/provider/user_data_source_test.go new file mode 100644 index 0000000..942dfa9 --- /dev/null +++ b/internal/provider/user_data_source_test.go @@ -0,0 +1,78 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package provider + +import ( + "strings" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +func TestAccUserDataSource(t *testing.T) { + // User by Username + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { + Config: testAccUserDataSourceConfig{ + Username: "example", + }.String(), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("coderd_user.test", "username", "example"), + resource.TestCheckResourceAttr("coderd_user.test", "name", "Example User"), + resource.TestCheckResourceAttr("coderd_user.test", "email", "example@coder.com"), + resource.TestCheckResourceAttr("coderd_user.test", "roles.#", "2"), + resource.TestCheckResourceAttr("coderd_user.test", "roles.0", "auditor"), + resource.TestCheckResourceAttr("coderd_user.test", "roles.1", "owner"), + resource.TestCheckResourceAttr("coderd_user.test", "login_type", "password"), + resource.TestCheckResourceAttr("coderd_user.test", "password", "SomeSecurePassword!"), + resource.TestCheckResourceAttr("coderd_user.test", "suspended", "false"), + ), + }, + }, + }) + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + // User by ID + Steps: []resource.TestStep{ + { + Config: testAccUserDataSourceConfig{ + ID: "example", + }.String(), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("coderd_user.test", "username", "example"), + resource.TestCheckResourceAttr("coderd_user.test", "name", "Example User"), + resource.TestCheckResourceAttr("coderd_user.test", "email", "example@coder.com"), + resource.TestCheckResourceAttr("coderd_user.test", "roles.#", "2"), + resource.TestCheckResourceAttr("coderd_user.test", "roles.0", "auditor"), + resource.TestCheckResourceAttr("coderd_user.test", "roles.1", "owner"), + resource.TestCheckResourceAttr("coderd_user.test", "login_type", "password"), + resource.TestCheckResourceAttr("coderd_user.test", "password", "SomeSecurePassword!"), + resource.TestCheckResourceAttr("coderd_user.test", "suspended", "false"), + ), + }, + }, + }) +} + +type testAccUserDataSourceConfig struct { + ID string + Username string +} + +func (c testAccUserDataSourceConfig) String() string { + sb := strings.Builder{} + sb.WriteString(`data "coderd_user" "test" {` + "\n") + if c.ID != "" { + sb.WriteString(` id = "` + c.ID + `"` + "\n") + } + if c.Username != "" { + sb.WriteString(` username = "` + c.Username + `"` + "\n") + } + sb.WriteString(`}`) + return sb.String() +} diff --git a/internal/provider/user_resource.go b/internal/provider/user_resource.go index a3690f8..75bddf6 100644 --- a/internal/provider/user_resource.go +++ b/internal/provider/user_resource.go @@ -130,18 +130,18 @@ func (r *UserResource) Configure(ctx context.Context, req resource.ConfigureRequ return } - client, ok := req.ProviderData.(*CoderdProviderData) + data, ok := req.ProviderData.(*CoderdProviderData) if !ok { resp.Diagnostics.AddError( "Unexpected Resource Configure Type", - fmt.Sprintf("Expected *codersdk.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData), + fmt.Sprintf("Expected *CoderdProviderData, got: %T. Please report this issue to the provider developers.", req.ProviderData), ) return } - r.data = client + r.data = data } func (r *UserResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { From 70fcc08e8f71861748b98edc9c04423d6d28fe98 Mon Sep 17 00:00:00 2001 From: Ethan Dickson Date: Fri, 12 Jul 2024 08:52:46 +0000 Subject: [PATCH 2/9] fixup --- internal/provider/user_data_source.go | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/internal/provider/user_data_source.go b/internal/provider/user_data_source.go index 6e4faef..66b50aa 100644 --- a/internal/provider/user_data_source.go +++ b/internal/provider/user_data_source.go @@ -53,11 +53,11 @@ func (d *UserDataSource) Schema(ctx context.Context, req datasource.SchemaReques // Validation handled by ConfigValidators Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{ - MarkdownDescription: "The ID of the user to retrieve. This field will be populated if a username is supplied", + MarkdownDescription: "The ID of the user to retrieve. This field will be populated if a username is supplied.", Optional: true, }, "username": schema.StringAttribute{ - MarkdownDescription: "The username of the user to retrieve. This field will be populated if an ID is supplied", + MarkdownDescription: "The username of the user to retrieve. This field will be populated if an ID is supplied.", Optional: true, }, "email": schema.StringAttribute{ @@ -72,18 +72,10 @@ func (d *UserDataSource) Schema(ctx context.Context, req datasource.SchemaReques MarkdownDescription: "Roles assigned to the user. Valid roles are 'owner', 'template-admin', 'user-admin', and 'auditor'.", Computed: true, ElementType: types.StringType, - // Validators: []validator.Set{ - // setvalidator.ValueStringsAre( - // stringvalidator.OneOf("owner", "template-admin", "user-admin", "auditor"), - // ), - // }, }, "login_type": schema.StringAttribute{ MarkdownDescription: "Type of login for the user. Valid types are 'none', 'password', 'github', and 'oidc'.", Computed: true, - // Validators: []validator.String{ - // stringvalidator.OneOf("none", "password", "github", "oidc"), - // }, }, "suspended": schema.BoolAttribute{ MarkdownDescription: "Whether the user is suspended.", From 93f239c0c6ba2c208d667e3d783ca14ed5887a06 Mon Sep 17 00:00:00 2001 From: Ethan Dickson Date: Fri, 12 Jul 2024 08:54:09 +0000 Subject: [PATCH 3/9] fixup --- integration/integration_test.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/integration/integration_test.go b/integration/integration_test.go index a116b39..8672e2e 100644 --- a/integration/integration_test.go +++ b/integration/integration_test.go @@ -56,8 +56,8 @@ func TestIntegration(t *testing.T) { name: "user-test", preF: func(t testing.TB, c *codersdk.Client) { me, err := c.User(ctx, codersdk.Me) - require.NoError(t, err) - c.CreateUser(ctx, codersdk.CreateUserRequest{ + assert.NoError(t, err) + _, err = c.CreateUser(ctx, codersdk.CreateUserRequest{ Email: "test2@coder.com", Username: "ethan", Password: "SomeSecurePassword!", @@ -65,6 +65,7 @@ func TestIntegration(t *testing.T) { DisableLogin: false, OrganizationID: me.OrganizationIDs[0], }) + assert.NoError(t, err) }, assertF: func(t testing.TB, c *codersdk.Client) { // Check user fields. From 0edeff0ef1329c86b6d6d770550ec0f246705995 Mon Sep 17 00:00:00 2001 From: Ethan Dickson Date: Fri, 12 Jul 2024 09:39:36 +0000 Subject: [PATCH 4/9] comment out --- internal/provider/user_data_source_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/internal/provider/user_data_source_test.go b/internal/provider/user_data_source_test.go index 942dfa9..93573f8 100644 --- a/internal/provider/user_data_source_test.go +++ b/internal/provider/user_data_source_test.go @@ -3,6 +3,7 @@ package provider +/* import ( "strings" "testing" @@ -76,3 +77,4 @@ func (c testAccUserDataSourceConfig) String() string { sb.WriteString(`}`) return sb.String() } +*/ From cc62729d411a7e76ae6ce408b4202898ef658c1c Mon Sep 17 00:00:00 2001 From: Ethan Dickson Date: Mon, 15 Jul 2024 03:47:41 +0000 Subject: [PATCH 5/9] review --- docs/data-sources/user.md | 4 +-- internal/provider/provider.go | 3 -- internal/provider/user_data_source_test.go | 37 +++++++++++++--------- internal/provider/user_resource.go | 3 -- internal/provider/user_resource_test.go | 3 -- 5 files changed, 24 insertions(+), 26 deletions(-) diff --git a/docs/data-sources/user.md b/docs/data-sources/user.md index ece2778..74f6e80 100644 --- a/docs/data-sources/user.md +++ b/docs/data-sources/user.md @@ -17,8 +17,8 @@ An existing user on the coder deployment ### Optional -- `id` (String) The ID of the user to retrieve. This field will be populated if a username is supplied -- `username` (String) The username of the user to retrieve. This field will be populated if an ID is supplied +- `id` (String) The ID of the user to retrieve. This field will be populated if a username is supplied. +- `username` (String) The username of the user to retrieve. This field will be populated if an ID is supplied. ### Read-Only diff --git a/internal/provider/provider.go b/internal/provider/provider.go index dfc531c..40bdff3 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -1,6 +1,3 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - package provider import ( diff --git a/internal/provider/user_data_source_test.go b/internal/provider/user_data_source_test.go index 93573f8..1af225b 100644 --- a/internal/provider/user_data_source_test.go +++ b/internal/provider/user_data_source_test.go @@ -1,10 +1,8 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - package provider /* import ( + "html/template" "strings" "testing" @@ -20,7 +18,7 @@ func TestAccUserDataSource(t *testing.T) { { Config: testAccUserDataSourceConfig{ Username: "example", - }.String(), + }.String(t), Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttr("coderd_user.test", "username", "example"), resource.TestCheckResourceAttr("coderd_user.test", "name", "Example User"), @@ -43,7 +41,7 @@ func TestAccUserDataSource(t *testing.T) { { Config: testAccUserDataSourceConfig{ ID: "example", - }.String(), + }.String(t), Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttr("coderd_user.test", "username", "example"), resource.TestCheckResourceAttr("coderd_user.test", "name", "Example User"), @@ -65,16 +63,25 @@ type testAccUserDataSourceConfig struct { Username string } -func (c testAccUserDataSourceConfig) String() string { - sb := strings.Builder{} - sb.WriteString(`data "coderd_user" "test" {` + "\n") - if c.ID != "" { - sb.WriteString(` id = "` + c.ID + `"` + "\n") - } - if c.Username != "" { - sb.WriteString(` username = "` + c.Username + `"` + "\n") +func (c testAccUserDataSourceConfig) String(t *testing.T) string { + tpl := ` +data "coderd_user" "test" { +{{- if .ID }} + id = "{{ .ID }}" +{{- end }} +{{- if .Username }} + username = "{{ .Username }}" +{{- end }} +}` + + tmpl := template.Must(template.New("userDataSource").Parse(tpl)) + + buf := strings.Builder{} + err := tmpl.Execute(&buf, c) + if err != nil { + panic(err) } - sb.WriteString(`}`) - return sb.String() + + return buf.String() } */ diff --git a/internal/provider/user_resource.go b/internal/provider/user_resource.go index 75bddf6..70a417a 100644 --- a/internal/provider/user_resource.go +++ b/internal/provider/user_resource.go @@ -1,6 +1,3 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - package provider import ( diff --git a/internal/provider/user_resource_test.go b/internal/provider/user_resource_test.go index eaf720c..afa91d7 100644 --- a/internal/provider/user_resource_test.go +++ b/internal/provider/user_resource_test.go @@ -1,6 +1,3 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - package provider /* From 8e8e8c43339e9c9d87da7daf8ac4f56ca5e71afd Mon Sep 17 00:00:00 2001 From: Ethan Dickson Date: Mon, 15 Jul 2024 03:48:11 +0000 Subject: [PATCH 6/9] review --- internal/provider/user_data_source.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/internal/provider/user_data_source.go b/internal/provider/user_data_source.go index 66b50aa..84dbcb5 100644 --- a/internal/provider/user_data_source.go +++ b/internal/provider/user_data_source.go @@ -1,6 +1,3 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - package provider import ( From c31e0d013aa73eaeae8a0b848bf9ef1fb2c35572 Mon Sep 17 00:00:00 2001 From: Ethan Dickson Date: Mon, 15 Jul 2024 04:22:51 +0000 Subject: [PATCH 7/9] extra fields --- docs/data-sources/user.md | 4 ++ internal/provider/user_data_source.go | 55 ++++++++++++++++++++++----- 2 files changed, 49 insertions(+), 10 deletions(-) diff --git a/docs/data-sources/user.md b/docs/data-sources/user.md index 74f6e80..88c9a1f 100644 --- a/docs/data-sources/user.md +++ b/docs/data-sources/user.md @@ -22,8 +22,12 @@ An existing user on the coder deployment ### Read-Only +- `created_at` (Number) Unix timestamp of when the user was created. - `email` (String) Email of the user. +- `last_seen_at` (Number) Unix timestamp of when the user was last seen. - `login_type` (String) Type of login for the user. Valid types are 'none', 'password', 'github', and 'oidc'. - `name` (String) Display name of the user. Defaults to username. +- `organization_ids` (Set of String) IDs of organizations the user is associated with. - `roles` (Set of String) Roles assigned to the user. Valid roles are 'owner', 'template-admin', 'user-admin', and 'auditor'. - `suspended` (Boolean) Whether the user is suspended. +- `theme_preference` (String) The user's preferred theme. diff --git a/internal/provider/user_data_source.go b/internal/provider/user_data_source.go index 84dbcb5..0fdf762 100644 --- a/internal/provider/user_data_source.go +++ b/internal/provider/user_data_source.go @@ -32,11 +32,16 @@ type UserDataSourceModel struct { ID types.String `tfsdk:"id"` Username types.String `tfsdk:"username"` - Name types.String `tfsdk:"name"` - Email types.String `tfsdk:"email"` - Roles types.Set `tfsdk:"roles"` // owner, template-admin, user-admin, auditor (member is implicit) - LoginType types.String `tfsdk:"login_type"` // none, password, github, oidc - Suspended types.Bool `tfsdk:"suspended"` + Name types.String `tfsdk:"name"` + Email types.String `tfsdk:"email"` + Roles types.Set `tfsdk:"roles"` // owner, template-admin, user-admin, auditor (member is implicit) + LoginType types.String `tfsdk:"login_type"` // none, password, github, oidc + Suspended types.Bool `tfsdk:"suspended"` + AvatarURL types.String `tfsdk:"avatar_url"` + OrganizationIDs types.Set `tfsdk:"organization_ids"` + CreatedAt types.Int64 `tfsdk:"created_at"` // Unix timestamp + LastSeenAt types.Int64 `tfsdk:"last_seen_at"` + ThemePreference types.String `tfsdk:"theme_preference"` } func (d *UserDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { @@ -57,14 +62,14 @@ func (d *UserDataSource) Schema(ctx context.Context, req datasource.SchemaReques MarkdownDescription: "The username of the user to retrieve. This field will be populated if an ID is supplied.", Optional: true, }, - "email": schema.StringAttribute{ - MarkdownDescription: "Email of the user.", - Computed: true, - }, "name": schema.StringAttribute{ MarkdownDescription: "Display name of the user. Defaults to username.", Computed: true, }, + "email": schema.StringAttribute{ + MarkdownDescription: "Email of the user.", + Computed: true, + }, "roles": schema.SetAttribute{ MarkdownDescription: "Roles assigned to the user. Valid roles are 'owner', 'template-admin', 'user-admin', and 'auditor'.", Computed: true, @@ -78,6 +83,27 @@ func (d *UserDataSource) Schema(ctx context.Context, req datasource.SchemaReques MarkdownDescription: "Whether the user is suspended.", Computed: true, }, + "avatar_url": schema.StringAttribute{ + MarkdownDescription: "URL of the user's avatar.", + Computed: true, + }, + "organization_ids": schema.SetAttribute{ + MarkdownDescription: "IDs of organizations the user is associated with.", + Computed: true, + ElementType: types.StringType, + }, + "created_at": schema.Int64Attribute{ + MarkdownDescription: "Unix timestamp of when the user was created.", + Computed: true, + }, + "last_seen_at": schema.Int64Attribute{ + MarkdownDescription: "Unix timestamp of when the user was last seen.", + Computed: true, + }, + "theme_preference": schema.StringAttribute{ + MarkdownDescription: "The user's preferred theme.", + Computed: true, + }, }, } } @@ -131,9 +157,9 @@ func (d *UserDataSource) Read(ctx context.Context, req datasource.ReadRequest, r } data.ID = types.StringValue(user.ID.String()) - data.Email = types.StringValue(user.Email) data.Username = types.StringValue(user.Username) data.Name = types.StringValue(user.Name) + data.Email = types.StringValue(user.Email) roles := make([]attr.Value, 0, len(user.Roles)) for _, role := range user.Roles { roles = append(roles, types.StringValue(role.Name)) @@ -142,6 +168,15 @@ func (d *UserDataSource) Read(ctx context.Context, req datasource.ReadRequest, r data.LoginType = types.StringValue(string(user.LoginType)) data.Suspended = types.BoolValue(user.Status == codersdk.UserStatusSuspended) + orgIDs := make([]attr.Value, 0, len(user.OrganizationIDs)) + for _, orgID := range user.OrganizationIDs { + orgIDs = append(orgIDs, types.StringValue(orgID.String())) + } + data.OrganizationIDs = types.SetValueMust(types.StringType, orgIDs) + data.CreatedAt = types.Int64Value(user.CreatedAt.Unix()) + data.LastSeenAt = types.Int64Value(user.LastSeenAt.Unix()) + data.ThemePreference = types.StringValue(user.ThemePreference) + // Save data into Terraform state resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) } From 162a744e7e7002bb4a8277d17b2696d63b5a0737 Mon Sep 17 00:00:00 2001 From: Ethan Dickson Date: Mon, 15 Jul 2024 04:44:21 +0000 Subject: [PATCH 8/9] review --- integration/user-test/main.tf | 9 +++++++++ internal/provider/user_data_source.go | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/integration/user-test/main.tf b/integration/user-test/main.tf index b0cb017..46cb6f6 100644 --- a/integration/user-test/main.tf +++ b/integration/user-test/main.tf @@ -20,3 +20,12 @@ resource "coderd_user" "dean" { data "coderd_user" "ethan" { username = "ethan" } + +resource "coderd_user" "ethan2" { + username = "${data.coderd_user.ethan.username}2" + name = "${data.coderd_user.ethan.name}2" + email = "${data.coderd_user.ethan.email}.au" + roles = data.coderd_user.ethan.roles + suspended = data.coderd_user.ethan.suspended +} + diff --git a/internal/provider/user_data_source.go b/internal/provider/user_data_source.go index 0fdf762..8254fe1 100644 --- a/internal/provider/user_data_source.go +++ b/internal/provider/user_data_source.go @@ -63,7 +63,7 @@ func (d *UserDataSource) Schema(ctx context.Context, req datasource.SchemaReques Optional: true, }, "name": schema.StringAttribute{ - MarkdownDescription: "Display name of the user. Defaults to username.", + MarkdownDescription: "Display name of the user.", Computed: true, }, "email": schema.StringAttribute{ From 6155b85815b058f024aa0411780a2c81e972de42 Mon Sep 17 00:00:00 2001 From: Ethan Dickson Date: Mon, 15 Jul 2024 04:46:06 +0000 Subject: [PATCH 9/9] login type --- integration/user-test/main.tf | 1 + 1 file changed, 1 insertion(+) diff --git a/integration/user-test/main.tf b/integration/user-test/main.tf index 46cb6f6..3b05004 100644 --- a/integration/user-test/main.tf +++ b/integration/user-test/main.tf @@ -25,6 +25,7 @@ resource "coderd_user" "ethan2" { username = "${data.coderd_user.ethan.username}2" name = "${data.coderd_user.ethan.name}2" email = "${data.coderd_user.ethan.email}.au" + login_type = "${data.coderd_user.ethan.login_type}" roles = data.coderd_user.ethan.roles suspended = data.coderd_user.ethan.suspended }