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..88c9a1f --- /dev/null +++ b/docs/data-sources/user.md @@ -0,0 +1,33 @@ +--- +# 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 + +- `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/integration/integration_test.go b/integration/integration_test.go index a038274..8672e2e 100644 --- a/integration/integration_test.go +++ b/integration/integration_test.go @@ -49,10 +49,24 @@ 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) + assert.NoError(t, err) + _, err = c.CreateUser(ctx, codersdk.CreateUserRequest{ + Email: "test2@coder.com", + Username: "ethan", + Password: "SomeSecurePassword!", + UserLoginType: "password", + DisableLogin: false, + OrganizationID: me.OrganizationIDs[0], + }) + assert.NoError(t, err) + }, assertF: func(t testing.TB, c *codersdk.Client) { // Check user fields. user, err := c.User(ctx, "dean") @@ -102,6 +116,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..3b05004 100644 --- a/integration/user-test/main.tf +++ b/integration/user-test/main.tf @@ -16,3 +16,17 @@ resource "coderd_user" "dean" { password = "SomeSecurePassword!" suspended = false } + +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" + login_type = "${data.coderd_user.ethan.login_type}" + roles = data.coderd_user.ethan.roles + suspended = data.coderd_user.ethan.suspended +} + diff --git a/internal/provider/example_data_source.go b/internal/provider/example_data_source.go deleted file mode 100644 index 00fdb3f..0000000 --- a/internal/provider/example_data_source.go +++ /dev/null @@ -1,104 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package provider - -import ( - "context" - "fmt" - - "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 { - data *CoderdProviderData -} - -// 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.(*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 = 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 e29d806..0000000 --- a/internal/provider/example_data_source_test.go +++ /dev/null @@ -1,37 +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 = ` -provider coderd { - url = "https://dev.coder.com" - token = "iamnotarealtoken" -} - -data "coderd_example" "test" { - configurable_attribute = "example" -} -` diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 60bcc6f..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 ( @@ -112,7 +109,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..8254fe1 --- /dev/null +++ b/internal/provider/user_data_source.go @@ -0,0 +1,191 @@ +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"` + 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) { + 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, + }, + "name": schema.StringAttribute{ + MarkdownDescription: "Display name of the user.", + 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, + ElementType: types.StringType, + }, + "login_type": schema.StringAttribute{ + MarkdownDescription: "Type of login for the user. Valid types are 'none', 'password', 'github', and 'oidc'.", + Computed: true, + }, + "suspended": schema.BoolAttribute{ + 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, + }, + }, + } +} + +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.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)) + } + data.Roles = types.SetValueMust(types.StringType, roles) + 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)...) +} + +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..6c5c0df --- /dev/null +++ b/internal/provider/user_data_source_test.go @@ -0,0 +1,95 @@ +package provider + +/* +import ( + "html/template" + "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(t), + 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(t), + 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 { + URL string + Token string + + ID string + Username string +} + +func (c testAccUserDataSourceConfig) String(t *testing.T) string { + tpl := ` +provider coderd { + url = "{{.URL}}" + token = "{{.Token}}" +} + +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) + } + + return buf.String() +} +*/ diff --git a/internal/provider/user_resource.go b/internal/provider/user_resource.go index 01c4fd5..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 ( @@ -130,7 +127,7 @@ 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( @@ -141,7 +138,7 @@ func (r *UserResource) Configure(ctx context.Context, req resource.ConfigureRequ return } - r.data = client + r.data = data } func (r *UserResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { 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 /*