From 28bab5a548fbdc020a965de140c49a3806ff8d9f Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Thu, 23 May 2024 21:40:03 +0000 Subject: [PATCH 1/5] feat: add coder_user datasource --- docs/data-sources/user.md | 26 +++++++++ provider/provider.go | 1 + provider/user.go | 113 ++++++++++++++++++++++++++++++++++++++ provider/user_test.go | 65 ++++++++++++++++++++++ 4 files changed, 205 insertions(+) create mode 100644 docs/data-sources/user.md create mode 100644 provider/user.go create mode 100644 provider/user_test.go diff --git a/docs/data-sources/user.md b/docs/data-sources/user.md new file mode 100644 index 00000000..b02077ac --- /dev/null +++ b/docs/data-sources/user.md @@ -0,0 +1,26 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "coder_user Data Source - terraform-provider-coder" +subcategory: "" +description: |- + Use this data source to fetch information about a user. +--- + +# coder_user (Data Source) + +Use this data source to fetch information about a user. + + + + +## Schema + +### Read-Only + +- `email` (String) The email address of the user. +- `full_name` (String) The full name of the user. +- `groups` (List of String) The groups of which the user is a member. +- `id` (String) The UUID of the user. +- `name` (String) The username of the user. +- `ssh_private_key` (String, Sensitive) The user's generated SSH private key. +- `ssh_public_key` (String) The user's generated SSH public key. diff --git a/provider/provider.go b/provider/provider.go index 4ea75ba8..2137dc75 100644 --- a/provider/provider.go +++ b/provider/provider.go @@ -74,6 +74,7 @@ func New() *schema.Provider { "coder_parameter": parameterDataSource(), "coder_git_auth": gitAuthDataSource(), "coder_external_auth": externalAuthDataSource(), + "coder_user": userDataSource(), }, ResourcesMap: map[string]*schema.Resource{ "coder_agent": agentResource(), diff --git a/provider/user.go b/provider/user.go new file mode 100644 index 00000000..317739cf --- /dev/null +++ b/provider/user.go @@ -0,0 +1,113 @@ +package provider + +import ( + "context" + "encoding/json" + "os" + "strings" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +type Role struct { + Name string `json:"name"` + DisplayName string `json:"display-name"` +} + +func userDataSource() *schema.Resource { + return &schema.Resource{ + Description: "Use this data source to fetch information about a user.", + ReadContext: func(ctx context.Context, rd *schema.ResourceData, i interface{}) diag.Diagnostics { + if idStr, ok := os.LookupEnv("CODER_USER_ID"); !ok { + return diag.Errorf("missing user id") + } else { + rd.SetId(idStr) + } + + if username, ok := os.LookupEnv("CODER_USER_NAME"); !ok { + return diag.Errorf("missing user username") + } else { + _ = rd.Set("name", username) + } + + if fullname, ok := os.LookupEnv("CODER_USER_FULL_NAME"); !ok { + _ = rd.Set("name", "default") // compat + } else { + _ = rd.Set("full_name", fullname) + } + + if email, ok := os.LookupEnv("CODER_USER_EMAIL"); !ok { + return diag.Errorf("missing user email") + } else { + _ = rd.Set("email", email) + } + + if sshPubKey, ok := os.LookupEnv("CODER_USER_SSH_PUBLIC_KEY"); !ok { + return diag.Errorf("missing user ssh_public_key") + } else { + _ = rd.Set("ssh_public_key", sshPubKey) + } + + if sshPrivKey, ok := os.LookupEnv("CODER_USER_SSH_PRIVATE_KEY"); !ok { + return diag.Errorf("missing user ssh_private_key") + } else { + _ = rd.Set("ssh_private_key", sshPrivKey) + } + + groupsRaw, ok := os.LookupEnv("CODER_USER_GROUPS") + if !ok { + return diag.Errorf("missing user groups") + } + var groups []string + if err := json.NewDecoder(strings.NewReader(groupsRaw)).Decode(&groups); err != nil { + return diag.Errorf("invalid user groups: %s", err.Error()) + } else { + _ = rd.Set("groups", groups) + } + + return nil + }, + Schema: map[string]*schema.Schema{ + "id": { + Type: schema.TypeString, + Computed: true, + Description: "The UUID of the user.", + }, + "name": { + Type: schema.TypeString, + Computed: true, + Description: "The username of the user.", + }, + "full_name": { + Type: schema.TypeString, + Computed: true, + Description: "The full name of the user.", + }, + "email": { + Type: schema.TypeString, + Computed: true, + Description: "The email address of the user.", + }, + "ssh_public_key": { + Type: schema.TypeString, + Computed: true, + Description: "The user's generated SSH public key.", + }, + "ssh_private_key": { + Type: schema.TypeString, + Computed: true, + Description: "The user's generated SSH private key.", + Sensitive: true, + }, + "groups": { + Type: schema.TypeList, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Computed: true, + Description: "The groups of which the user is a member.", + }, + }, + } +} diff --git a/provider/user_test.go b/provider/user_test.go new file mode 100644 index 00000000..8aeb2187 --- /dev/null +++ b/provider/user_test.go @@ -0,0 +1,65 @@ +package provider_test + +import ( + "testing" + + "github.com/coder/terraform-provider-coder/provider" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const ( + testSSHEd25519PublicKey = `ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJeNcdBMtd4Jo9f2W8RZef0ld7Ypye5zTQEf0vUXa/Eq owner123@host456` + // nolint:gosec // This key was generated specifically for this purpose. + testSSHEd25519PrivateKey = `-----BEGIN OPENSSH PRIVATE KEY----- + b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW + QyNTUxOQAAACCXjXHQTLXeCaPX9lvEWXn9JXe2Kcnuc00BH9L1F2vxKgAAAJgp3mfQKd5n + 0AAAAAtzc2gtZWQyNTUxOQAAACCXjXHQTLXeCaPX9lvEWXn9JXe2Kcnuc00BH9L1F2vxKg + AAAEBia7mAQFoLBILlvTJroTkOUomzfcPY9ckpViQOjYFkAZeNcdBMtd4Jo9f2W8RZef0l + d7Ypye5zTQEf0vUXa/EqAAAAE3ZzY29kZUAzY2Y4MWY5YmM3MmQBAg== + -----END OPENSSH PRIVATE KEY-----` +) + +func TestUserDatasource(t *testing.T) { + t.Setenv("CODER_USER_ID", "11111111-1111-1111-1111-111111111111") + t.Setenv("CODER_USER_NAME", "owner123") + t.Setenv("CODER_USER_AVATAR_URL", "https://example.com/avatar.png") + t.Setenv("CODER_USER_FULL_NAME", "Mr Owner") + t.Setenv("CODER_USER_EMAIL", "owner123@example.com") + t.Setenv("CODER_USER_SSH_PUBLIC_KEY", testSSHEd25519PublicKey) + t.Setenv("CODER_USER_SSH_PRIVATE_KEY", testSSHEd25519PrivateKey) + t.Setenv("CODER_USER_GROUPS", `["group1", "group2"]`) + + resource.Test(t, resource.TestCase{ + Providers: map[string]*schema.Provider{ + "coder": provider.New(), + }, + IsUnitTest: true, + Steps: []resource.TestStep{{ + Config: ` + provider "coder" {} + data "coder_user" "me" {} + `, + Check: func(s *terraform.State) error { + require.Len(t, s.Modules, 1) + require.Len(t, s.Modules[0].Resources, 1) + resource := s.Modules[0].Resources["data.coder_user.me"] + require.NotNil(t, resource) + + attrs := resource.Primary.Attributes + assert.Equal(t, "11111111-1111-1111-1111-111111111111", attrs["id"]) + assert.Equal(t, "owner123", attrs["name"]) + assert.Equal(t, "Mr Owner", attrs["full_name"]) + assert.Equal(t, "owner123@example.com", attrs["email"]) + assert.Equal(t, testSSHEd25519PublicKey, attrs["ssh_public_key"]) + assert.Equal(t, testSSHEd25519PrivateKey, attrs["ssh_private_key"]) + assert.Equal(t, `group1`, attrs["groups.0"]) + assert.Equal(t, `group2`, attrs["groups.1"]) + return nil + }, + }}, + }) +} From 1fc56bb32672fc7f70a33a573b16e00daf3f470f Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Fri, 24 May 2024 08:50:01 +0000 Subject: [PATCH 2/5] compat --- docs/data-sources/user.md | 1 + provider/user.go | 70 ++++++++++++++++++-------- provider/user_test.go | 103 ++++++++++++++++++++++++++------------ 3 files changed, 121 insertions(+), 53 deletions(-) diff --git a/docs/data-sources/user.md b/docs/data-sources/user.md index b02077ac..78cf06f4 100644 --- a/docs/data-sources/user.md +++ b/docs/data-sources/user.md @@ -22,5 +22,6 @@ Use this data source to fetch information about a user. - `groups` (List of String) The groups of which the user is a member. - `id` (String) The UUID of the user. - `name` (String) The username of the user. +- `session_token` (String) Session token for authenticating with a Coder deployment. It is regenerated every time a workspace is started. - `ssh_private_key` (String, Sensitive) The user's generated SSH private key. - `ssh_public_key` (String) The user's generated SSH public key. diff --git a/provider/user.go b/provider/user.go index 317739cf..d9e761f8 100644 --- a/provider/user.go +++ b/provider/user.go @@ -6,6 +6,7 @@ import ( "os" "strings" + "github.com/google/uuid" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) @@ -20,50 +21,70 @@ func userDataSource() *schema.Resource { Description: "Use this data source to fetch information about a user.", ReadContext: func(ctx context.Context, rd *schema.ResourceData, i interface{}) diag.Diagnostics { if idStr, ok := os.LookupEnv("CODER_USER_ID"); !ok { - return diag.Errorf("missing user id") + rd.SetId(uuid.NewString()) } else { rd.SetId(idStr) } - if username, ok := os.LookupEnv("CODER_USER_NAME"); !ok { - return diag.Errorf("missing user username") - } else { + if username, ok := os.LookupEnv("CODER_USER_NAME"); ok { _ = rd.Set("name", username) + } else if altUsername, ok := os.LookupEnv("CODER_WORKSPACE_OWNER"); ok { + _ = rd.Set("name", altUsername) + } else { + return diag.Errorf("missing user name") } - if fullname, ok := os.LookupEnv("CODER_USER_FULL_NAME"); !ok { - _ = rd.Set("name", "default") // compat - } else { + if fullname, ok := os.LookupEnv("CODER_USER_FULL_NAME"); ok { _ = rd.Set("full_name", fullname) + } else if altFullname, ok := os.LookupEnv("CODER_WORKSPACE_OWNER_NAME"); ok { + // Compatibility: read from CODER_WORKSPACE_OWNER_NAME + _ = rd.Set("full_name", altFullname) + } else { // fallback + return diag.Errorf("missing user full_name") } - if email, ok := os.LookupEnv("CODER_USER_EMAIL"); !ok { - return diag.Errorf("missing user email") - } else { + if email, ok := os.LookupEnv("CODER_USER_EMAIL"); ok { _ = rd.Set("email", email) + } else if altEmail, ok := os.LookupEnv("CODER_WORKSPACE_OWNER_EMAIL"); ok { + _ = rd.Set("email", altEmail) + } else { + return diag.Errorf("missing user email") } - if sshPubKey, ok := os.LookupEnv("CODER_USER_SSH_PUBLIC_KEY"); !ok { - return diag.Errorf("missing user ssh_public_key") - } else { + if sshPubKey, ok := os.LookupEnv("CODER_USER_SSH_PUBLIC_KEY"); ok { _ = rd.Set("ssh_public_key", sshPubKey) + } else { + // Compat: do not error + _ = rd.Set("ssh_public_key", "missing") } - if sshPrivKey, ok := os.LookupEnv("CODER_USER_SSH_PRIVATE_KEY"); !ok { - return diag.Errorf("missing user ssh_private_key") - } else { + if sshPrivKey, ok := os.LookupEnv("CODER_USER_SSH_PRIVATE_KEY"); ok { _ = rd.Set("ssh_private_key", sshPrivKey) + } else { + // Compat: do not error + _ = rd.Set("ssh_private_key", "missing") } - groupsRaw, ok := os.LookupEnv("CODER_USER_GROUPS") - if !ok { + var groups []string + if groupsRaw, ok := os.LookupEnv("CODER_USER_GROUPS"); ok { + if err := json.NewDecoder(strings.NewReader(groupsRaw)).Decode(&groups); err != nil { + return diag.Errorf("invalid user groups: %s", err.Error()) + } + } else if altGroupsRaw, ok := os.LookupEnv("CODER_WORKSPACE_OWNER_GROUPS"); ok { + if err := json.NewDecoder(strings.NewReader(altGroupsRaw)).Decode(&groups); err != nil { + return diag.Errorf("invalid workspace owner groups: %s", err.Error()) + } + } else { return diag.Errorf("missing user groups") } - var groups []string - if err := json.NewDecoder(strings.NewReader(groupsRaw)).Decode(&groups); err != nil { - return diag.Errorf("invalid user groups: %s", err.Error()) + _ = rd.Set("groups", groups) + + if tok, ok := os.LookupEnv("CODER_USER_SESSION_TOKEN"); ok { + _ = rd.Set("session_token", tok) + } else if altTok, ok := os.LookupEnv("CODER_WORKSPACE_OWNER_SESSION_TOKEN"); ok { + _ = rd.Set("session_token", altTok) } else { - _ = rd.Set("groups", groups) + return diag.Errorf("missing user session_token") } return nil @@ -108,6 +129,11 @@ func userDataSource() *schema.Resource { Computed: true, Description: "The groups of which the user is a member.", }, + "session_token": { + Type: schema.TypeString, + Computed: true, + Description: "Session token for authenticating with a Coder deployment. It is regenerated every time a workspace is started.", + }, }, } } diff --git a/provider/user_test.go b/provider/user_test.go index 8aeb2187..91a5e084 100644 --- a/provider/user_test.go +++ b/provider/user_test.go @@ -24,42 +24,83 @@ const ( ) func TestUserDatasource(t *testing.T) { - t.Setenv("CODER_USER_ID", "11111111-1111-1111-1111-111111111111") - t.Setenv("CODER_USER_NAME", "owner123") - t.Setenv("CODER_USER_AVATAR_URL", "https://example.com/avatar.png") - t.Setenv("CODER_USER_FULL_NAME", "Mr Owner") - t.Setenv("CODER_USER_EMAIL", "owner123@example.com") - t.Setenv("CODER_USER_SSH_PUBLIC_KEY", testSSHEd25519PublicKey) - t.Setenv("CODER_USER_SSH_PRIVATE_KEY", testSSHEd25519PrivateKey) - t.Setenv("CODER_USER_GROUPS", `["group1", "group2"]`) + t.Run("OK", func(t *testing.T) { + t.Setenv("CODER_USER_ID", "11111111-1111-1111-1111-111111111111") + t.Setenv("CODER_USER_NAME", "owner123") + t.Setenv("CODER_USER_FULL_NAME", "Mr Owner") + t.Setenv("CODER_USER_EMAIL", "owner123@example.com") + t.Setenv("CODER_USER_SSH_PUBLIC_KEY", testSSHEd25519PublicKey) + t.Setenv("CODER_USER_SSH_PRIVATE_KEY", testSSHEd25519PrivateKey) + t.Setenv("CODER_USER_GROUPS", `["group1", "group2"]`) + t.Setenv("CODER_USER_SESSION_TOKEN", `supersecret`) - resource.Test(t, resource.TestCase{ - Providers: map[string]*schema.Provider{ - "coder": provider.New(), - }, - IsUnitTest: true, - Steps: []resource.TestStep{{ - Config: ` + resource.Test(t, resource.TestCase{ + Providers: map[string]*schema.Provider{ + "coder": provider.New(), + }, + IsUnitTest: true, + Steps: []resource.TestStep{{ + Config: ` provider "coder" {} data "coder_user" "me" {} `, - Check: func(s *terraform.State) error { - require.Len(t, s.Modules, 1) - require.Len(t, s.Modules[0].Resources, 1) - resource := s.Modules[0].Resources["data.coder_user.me"] - require.NotNil(t, resource) + Check: func(s *terraform.State) error { + require.Len(t, s.Modules, 1) + require.Len(t, s.Modules[0].Resources, 1) + resource := s.Modules[0].Resources["data.coder_user.me"] + require.NotNil(t, resource) + + attrs := resource.Primary.Attributes + assert.Equal(t, "11111111-1111-1111-1111-111111111111", attrs["id"]) + assert.Equal(t, "owner123", attrs["name"]) + assert.Equal(t, "Mr Owner", attrs["full_name"]) + assert.Equal(t, "owner123@example.com", attrs["email"]) + assert.Equal(t, testSSHEd25519PublicKey, attrs["ssh_public_key"]) + assert.Equal(t, testSSHEd25519PrivateKey, attrs["ssh_private_key"]) + assert.Equal(t, `group1`, attrs["groups.0"]) + assert.Equal(t, `group2`, attrs["groups.1"]) + assert.Equal(t, `supersecret`, attrs["session_token"]) + return nil + }, + }}, + }) + }) + + t.Run("Compat", func(t *testing.T) { + t.Setenv("CODER_WORKSPACE_OWNER", "owner123") + t.Setenv("CODER_WORKSPACE_OWNER_NAME", "Mr Owner") + t.Setenv("CODER_WORKSPACE_OWNER_EMAIL", "owner123@example.com") + t.Setenv("CODER_WORKSPACE_OWNER_GROUPS", `["group1", "group2"]`) + t.Setenv("CODER_WORKSPACE_OWNER_SESSION_TOKEN", `supersecret`) - attrs := resource.Primary.Attributes - assert.Equal(t, "11111111-1111-1111-1111-111111111111", attrs["id"]) - assert.Equal(t, "owner123", attrs["name"]) - assert.Equal(t, "Mr Owner", attrs["full_name"]) - assert.Equal(t, "owner123@example.com", attrs["email"]) - assert.Equal(t, testSSHEd25519PublicKey, attrs["ssh_public_key"]) - assert.Equal(t, testSSHEd25519PrivateKey, attrs["ssh_private_key"]) - assert.Equal(t, `group1`, attrs["groups.0"]) - assert.Equal(t, `group2`, attrs["groups.1"]) - return nil + resource.Test(t, resource.TestCase{ + Providers: map[string]*schema.Provider{ + "coder": provider.New(), }, - }}, + IsUnitTest: true, + Steps: []resource.TestStep{{ + Config: ` + provider "coder" {} + data "coder_user" "me" {} + `, + Check: func(s *terraform.State) error { + require.Len(t, s.Modules, 1) + require.Len(t, s.Modules[0].Resources, 1) + resource := s.Modules[0].Resources["data.coder_user.me"] + require.NotNil(t, resource) + + attrs := resource.Primary.Attributes + assert.NotEmpty(t, attrs["id"]) + assert.Equal(t, "owner123", attrs["name"]) + assert.Equal(t, "Mr Owner", attrs["full_name"]) + assert.Equal(t, "owner123@example.com", attrs["email"]) + assert.Equal(t, "missing", attrs["ssh_public_key"]) + assert.Equal(t, "missing", attrs["ssh_private_key"]) + assert.Equal(t, `group1`, attrs["groups.0"]) + assert.Equal(t, `group2`, attrs["groups.1"]) + return nil + }, + }}, + }) }) } From 748a714ff5331e232ba4dd339fda4bda401c30c9 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Fri, 24 May 2024 13:25:56 +0000 Subject: [PATCH 3/5] rename to coder_workspace_owner --- docs/data-sources/user.md | 7 +- docs/data-sources/workspace.md | 14 ++-- provider/provider.go | 2 +- provider/workspace.go | 7 ++ provider/{user.go => workspace_owner.go} | 70 ++++++++----------- .../{user_test.go => workspace_owner_test.go} | 57 +++++++++------ provider/workspace_test.go | 4 ++ 7 files changed, 87 insertions(+), 74 deletions(-) rename provider/{user.go => workspace_owner.go} (55%) rename provider/{user_test.go => workspace_owner_test.go} (69%) diff --git a/docs/data-sources/user.md b/docs/data-sources/user.md index 78cf06f4..abf717cc 100644 --- a/docs/data-sources/user.md +++ b/docs/data-sources/user.md @@ -3,12 +3,12 @@ page_title: "coder_user Data Source - terraform-provider-coder" subcategory: "" description: |- - Use this data source to fetch information about a user. + Use this data source to fetch information about the workspace owner. --- # coder_user (Data Source) -Use this data source to fetch information about a user. +Use this data source to fetch information about the workspace owner. @@ -20,8 +20,9 @@ Use this data source to fetch information about a user. - `email` (String) The email address of the user. - `full_name` (String) The full name of the user. - `groups` (List of String) The groups of which the user is a member. -- `id` (String) The UUID of the user. +- `id` (String) The UUID of the workspace owner. - `name` (String) The username of the user. +- `oidc_access_token` (String) A valid OpenID Connect access token of the workspace owner. This is only available if the workspace owner authenticated with OpenID Connect. If a valid token cannot be obtained, this value will be an empty string. - `session_token` (String) Session token for authenticating with a Coder deployment. It is regenerated every time a workspace is started. - `ssh_private_key` (String, Sensitive) The user's generated SSH private key. - `ssh_public_key` (String) The user's generated SSH public key. diff --git a/docs/data-sources/workspace.md b/docs/data-sources/workspace.md index 2ed7c63d..2a813722 100644 --- a/docs/data-sources/workspace.md +++ b/docs/data-sources/workspace.md @@ -30,13 +30,13 @@ resource "kubernetes_pod" "dev" { - `access_url` (String) The access URL of the Coder deployment provisioning this workspace. - `id` (String) UUID of the workspace. - `name` (String) Name of the workspace. -- `owner` (String) Username of the workspace owner. -- `owner_email` (String) Email address of the workspace owner. -- `owner_groups` (List of String) List of groups the workspace owner belongs to. -- `owner_id` (String) UUID of the workspace owner. -- `owner_name` (String) Name of the workspace owner. -- `owner_oidc_access_token` (String) A valid OpenID Connect access token of the workspace owner. This is only available if the workspace owner authenticated with OpenID Connect. If a valid token cannot be obtained, this value will be an empty string. -- `owner_session_token` (String) Session token for authenticating with a Coder deployment. It is regenerated everytime a workspace is started. +- `owner` (String, Deprecated) Username of the workspace owner. +- `owner_email` (String, Deprecated) Email address of the workspace owner. +- `owner_groups` (List of String, Deprecated) List of groups the workspace owner belongs to. +- `owner_id` (String, Deprecated) UUID of the workspace owner. +- `owner_name` (String, Deprecated) Name of the workspace owner. +- `owner_oidc_access_token` (String, Deprecated) A valid OpenID Connect access token of the workspace owner. This is only available if the workspace owner authenticated with OpenID Connect. If a valid token cannot be obtained, this value will be an empty string. +- `owner_session_token` (String, Deprecated) Session token for authenticating with a Coder deployment. It is regenerated everytime a workspace is started. - `start_count` (Number) A computed count based on "transition" state. If "start", count will equal 1. - `template_id` (String) ID of the workspace's template. - `template_name` (String) Name of the workspace's template. diff --git a/provider/provider.go b/provider/provider.go index 2137dc75..e59238f2 100644 --- a/provider/provider.go +++ b/provider/provider.go @@ -74,7 +74,7 @@ func New() *schema.Provider { "coder_parameter": parameterDataSource(), "coder_git_auth": gitAuthDataSource(), "coder_external_auth": externalAuthDataSource(), - "coder_user": userDataSource(), + "coder_user": workspaceOwnerDataSource(), }, ResourcesMap: map[string]*schema.Resource{ "coder_agent": agentResource(), diff --git a/provider/workspace.go b/provider/workspace.go index b8ff1684..098d64cc 100644 --- a/provider/workspace.go +++ b/provider/workspace.go @@ -135,21 +135,25 @@ func workspaceDataSource() *schema.Resource { Type: schema.TypeString, Computed: true, Description: "Username of the workspace owner.", + Deprecated: "Use `coder_workspace_owner.name` instead.", }, "owner_email": { Type: schema.TypeString, Computed: true, Description: "Email address of the workspace owner.", + Deprecated: "Use `coder_workspace_owner.email` instead.", }, "owner_id": { Type: schema.TypeString, Computed: true, Description: "UUID of the workspace owner.", + Deprecated: "Use `coder_workspace_owner.id` instead.", }, "owner_name": { Type: schema.TypeString, Computed: true, Description: "Name of the workspace owner.", + Deprecated: "Use `coder_workspace_owner.full_name` instead.", }, "owner_oidc_access_token": { Type: schema.TypeString, @@ -157,6 +161,7 @@ func workspaceDataSource() *schema.Resource { Description: "A valid OpenID Connect access token of the workspace owner. " + "This is only available if the workspace owner authenticated with OpenID Connect. " + "If a valid token cannot be obtained, this value will be an empty string.", + Deprecated: "Use `coder_workspace_owner.oidc_access_token` instead.", }, "owner_groups": { Type: schema.TypeList, @@ -165,6 +170,7 @@ func workspaceDataSource() *schema.Resource { }, Computed: true, Description: "List of groups the workspace owner belongs to.", + Deprecated: "Use `coder_workspace_owner.groups` instead.", }, "id": { Type: schema.TypeString, @@ -180,6 +186,7 @@ func workspaceDataSource() *schema.Resource { Type: schema.TypeString, Computed: true, Description: "Session token for authenticating with a Coder deployment. It is regenerated everytime a workspace is started.", + Deprecated: "Use `coder_workspace_owner.session_token` instead.", }, "template_id": { Type: schema.TypeString, diff --git a/provider/user.go b/provider/workspace_owner.go similarity index 55% rename from provider/user.go rename to provider/workspace_owner.go index d9e761f8..5721b5c5 100644 --- a/provider/user.go +++ b/provider/workspace_owner.go @@ -16,75 +16,56 @@ type Role struct { DisplayName string `json:"display-name"` } -func userDataSource() *schema.Resource { +func workspaceOwnerDataSource() *schema.Resource { return &schema.Resource{ - Description: "Use this data source to fetch information about a user.", + Description: "Use this data source to fetch information about the workspace owner.", ReadContext: func(ctx context.Context, rd *schema.ResourceData, i interface{}) diag.Diagnostics { - if idStr, ok := os.LookupEnv("CODER_USER_ID"); !ok { - rd.SetId(uuid.NewString()) - } else { + if idStr, ok := os.LookupEnv("CODER_WORKSPACE_OWNER_ID"); ok { rd.SetId(idStr) + } else { + rd.SetId(uuid.NewString()) } - if username, ok := os.LookupEnv("CODER_USER_NAME"); ok { + if username, ok := os.LookupEnv("CODER_WORKSPACE_OWNER"); ok { _ = rd.Set("name", username) - } else if altUsername, ok := os.LookupEnv("CODER_WORKSPACE_OWNER"); ok { - _ = rd.Set("name", altUsername) } else { - return diag.Errorf("missing user name") + _ = rd.Set("name", "default") } - if fullname, ok := os.LookupEnv("CODER_USER_FULL_NAME"); ok { + if fullname, ok := os.LookupEnv("CODER_WORKSPACE_OWNER_NAME"); ok { _ = rd.Set("full_name", fullname) - } else if altFullname, ok := os.LookupEnv("CODER_WORKSPACE_OWNER_NAME"); ok { - // Compatibility: read from CODER_WORKSPACE_OWNER_NAME - _ = rd.Set("full_name", altFullname) - } else { // fallback - return diag.Errorf("missing user full_name") + } else { // compat: field can be blank, fill in default + _ = rd.Set("full_name", "default") } - if email, ok := os.LookupEnv("CODER_USER_EMAIL"); ok { + if email, ok := os.LookupEnv("CODER_WORKSPACE_OWNER_EMAIL"); ok { _ = rd.Set("email", email) - } else if altEmail, ok := os.LookupEnv("CODER_WORKSPACE_OWNER_EMAIL"); ok { - _ = rd.Set("email", altEmail) } else { - return diag.Errorf("missing user email") + _ = rd.Set("email", "default@example.com") } - if sshPubKey, ok := os.LookupEnv("CODER_USER_SSH_PUBLIC_KEY"); ok { + if sshPubKey, ok := os.LookupEnv("CODER_WORKSPACE_OWNER_SSH_PUBLIC_KEY"); ok { _ = rd.Set("ssh_public_key", sshPubKey) - } else { - // Compat: do not error - _ = rd.Set("ssh_public_key", "missing") } - if sshPrivKey, ok := os.LookupEnv("CODER_USER_SSH_PRIVATE_KEY"); ok { + if sshPrivKey, ok := os.LookupEnv("CODER_WORKSPACE_OWNER_SSH_PRIVATE_KEY"); ok { _ = rd.Set("ssh_private_key", sshPrivKey) - } else { - // Compat: do not error - _ = rd.Set("ssh_private_key", "missing") } var groups []string - if groupsRaw, ok := os.LookupEnv("CODER_USER_GROUPS"); ok { + if groupsRaw, ok := os.LookupEnv("CODER_WORKSPACE_OWNER_GROUPS"); ok { if err := json.NewDecoder(strings.NewReader(groupsRaw)).Decode(&groups); err != nil { return diag.Errorf("invalid user groups: %s", err.Error()) } - } else if altGroupsRaw, ok := os.LookupEnv("CODER_WORKSPACE_OWNER_GROUPS"); ok { - if err := json.NewDecoder(strings.NewReader(altGroupsRaw)).Decode(&groups); err != nil { - return diag.Errorf("invalid workspace owner groups: %s", err.Error()) - } - } else { - return diag.Errorf("missing user groups") + _ = rd.Set("groups", groups) } - _ = rd.Set("groups", groups) - if tok, ok := os.LookupEnv("CODER_USER_SESSION_TOKEN"); ok { + if tok, ok := os.LookupEnv("CODER_WORKSPACE_OWNER_SESSION_TOKEN"); ok { _ = rd.Set("session_token", tok) - } else if altTok, ok := os.LookupEnv("CODER_WORKSPACE_OWNER_SESSION_TOKEN"); ok { - _ = rd.Set("session_token", altTok) - } else { - return diag.Errorf("missing user session_token") + } + + if tok, ok := os.LookupEnv("CODER_WORKSPACE_OWNER_OIDC_ACCESS_TOKEN"); ok { + _ = rd.Set("oidc_access_token", tok) } return nil @@ -93,7 +74,7 @@ func userDataSource() *schema.Resource { "id": { Type: schema.TypeString, Computed: true, - Description: "The UUID of the user.", + Description: "The UUID of the workspace owner.", }, "name": { Type: schema.TypeString, @@ -134,6 +115,13 @@ func userDataSource() *schema.Resource { Computed: true, Description: "Session token for authenticating with a Coder deployment. It is regenerated every time a workspace is started.", }, + "oidc_access_token": { + Type: schema.TypeString, + Computed: true, + Description: "A valid OpenID Connect access token of the workspace owner. " + + "This is only available if the workspace owner authenticated with OpenID Connect. " + + "If a valid token cannot be obtained, this value will be an empty string.", + }, }, } } diff --git a/provider/user_test.go b/provider/workspace_owner_test.go similarity index 69% rename from provider/user_test.go rename to provider/workspace_owner_test.go index 91a5e084..b15c8efb 100644 --- a/provider/user_test.go +++ b/provider/workspace_owner_test.go @@ -1,6 +1,7 @@ package provider_test import ( + "os" "testing" "github.com/coder/terraform-provider-coder/provider" @@ -23,16 +24,17 @@ const ( -----END OPENSSH PRIVATE KEY-----` ) -func TestUserDatasource(t *testing.T) { +func TestWorkspaceOwnerDatasource(t *testing.T) { t.Run("OK", func(t *testing.T) { - t.Setenv("CODER_USER_ID", "11111111-1111-1111-1111-111111111111") - t.Setenv("CODER_USER_NAME", "owner123") - t.Setenv("CODER_USER_FULL_NAME", "Mr Owner") - t.Setenv("CODER_USER_EMAIL", "owner123@example.com") - t.Setenv("CODER_USER_SSH_PUBLIC_KEY", testSSHEd25519PublicKey) - t.Setenv("CODER_USER_SSH_PRIVATE_KEY", testSSHEd25519PrivateKey) - t.Setenv("CODER_USER_GROUPS", `["group1", "group2"]`) - t.Setenv("CODER_USER_SESSION_TOKEN", `supersecret`) + t.Setenv("CODER_WORKSPACE_OWNER_ID", "11111111-1111-1111-1111-111111111111") + t.Setenv("CODER_WORKSPACE_OWNER", "owner123") + t.Setenv("CODER_WORKSPACE_OWNER_NAME", "Mr Owner") + t.Setenv("CODER_WORKSPACE_OWNER_EMAIL", "owner123@example.com") + t.Setenv("CODER_WORKSPACE_OWNER_SSH_PUBLIC_KEY", testSSHEd25519PublicKey) + t.Setenv("CODER_WORKSPACE_OWNER_SSH_PRIVATE_KEY", testSSHEd25519PrivateKey) + t.Setenv("CODER_WORKSPACE_OWNER_GROUPS", `["group1", "group2"]`) + t.Setenv("CODER_WORKSPACE_OWNER_SESSION_TOKEN", `supersecret`) + t.Setenv("CODER_WORKSPACE_OWNER_OIDC_ACCESS_TOKEN", `alsosupersecret`) resource.Test(t, resource.TestCase{ Providers: map[string]*schema.Provider{ @@ -60,18 +62,28 @@ func TestUserDatasource(t *testing.T) { assert.Equal(t, `group1`, attrs["groups.0"]) assert.Equal(t, `group2`, attrs["groups.1"]) assert.Equal(t, `supersecret`, attrs["session_token"]) + assert.Equal(t, `alsosupersecret`, attrs["oidc_access_token"]) return nil }, }}, }) }) - t.Run("Compat", func(t *testing.T) { - t.Setenv("CODER_WORKSPACE_OWNER", "owner123") - t.Setenv("CODER_WORKSPACE_OWNER_NAME", "Mr Owner") - t.Setenv("CODER_WORKSPACE_OWNER_EMAIL", "owner123@example.com") - t.Setenv("CODER_WORKSPACE_OWNER_GROUPS", `["group1", "group2"]`) - t.Setenv("CODER_WORKSPACE_OWNER_SESSION_TOKEN", `supersecret`) + t.Run("Defaults", func(t *testing.T) { + for _, v := range []string{ + "CODER_WORKSPACE_OWNER", + "CODER_WORKSPACE_OWNER_ID", + "CODER_WORKSPACE_OWNER_EMAIL", + "CODER_WORKSPACE_OWNER_NAME", + "CODER_WORKSPACE_OWNER_SESSION_TOKEN", + "CODER_WORKSPACE_OWNER_GROUPS", + "CODER_WORKSPACE_OWNER_OIDC_ACCESS_TOKEN", + "CODER_WORKSPACE_OWNER_SSH_PUBLIC_KEY", + "CODER_WORKSPACE_OWNER_SSH_PRIVATE_KEY", + } { // https://github.com/golang/go/issues/52817 + t.Setenv(v, "") + os.Unsetenv(v) + } resource.Test(t, resource.TestCase{ Providers: map[string]*schema.Provider{ @@ -91,13 +103,14 @@ func TestUserDatasource(t *testing.T) { attrs := resource.Primary.Attributes assert.NotEmpty(t, attrs["id"]) - assert.Equal(t, "owner123", attrs["name"]) - assert.Equal(t, "Mr Owner", attrs["full_name"]) - assert.Equal(t, "owner123@example.com", attrs["email"]) - assert.Equal(t, "missing", attrs["ssh_public_key"]) - assert.Equal(t, "missing", attrs["ssh_private_key"]) - assert.Equal(t, `group1`, attrs["groups.0"]) - assert.Equal(t, `group2`, attrs["groups.1"]) + assert.Equal(t, "default", attrs["name"]) + assert.Equal(t, "default", attrs["full_name"]) + assert.Equal(t, "default@example.com", attrs["email"]) + assert.Empty(t, attrs["ssh_public_key"]) + assert.Empty(t, attrs["ssh_private_key"]) + assert.Empty(t, attrs["groups.0"]) + assert.Empty(t, attrs["session_token"]) + assert.Empty(t, attrs["oidc_access_token"]) return nil }, }}, diff --git a/provider/workspace_test.go b/provider/workspace_test.go index d5866af5..d285b30c 100644 --- a/provider/workspace_test.go +++ b/provider/workspace_test.go @@ -14,10 +14,12 @@ import ( func TestWorkspace(t *testing.T) { t.Setenv("CODER_WORKSPACE_OWNER", "owner123") + t.Setenv("CODER_WORKSPACE_OWNER_ID", "11111111-1111-1111-1111-111111111111") t.Setenv("CODER_WORKSPACE_OWNER_NAME", "Mr Owner") t.Setenv("CODER_WORKSPACE_OWNER_EMAIL", "owner123@example.com") t.Setenv("CODER_WORKSPACE_OWNER_SESSION_TOKEN", "abc123") t.Setenv("CODER_WORKSPACE_OWNER_GROUPS", `["group1", "group2"]`) + t.Setenv("CODER_WORKSPACE_OWNER_OIDC_ACCESS_TOKEN", "supersecret") t.Setenv("CODER_WORKSPACE_TEMPLATE_ID", "templateID") t.Setenv("CODER_WORKSPACE_TEMPLATE_NAME", "template123") t.Setenv("CODER_WORKSPACE_TEMPLATE_VERSION", "v1.2.3") @@ -47,6 +49,7 @@ func TestWorkspace(t *testing.T) { assert.Equal(t, "https://example.com:8080", attribs["access_url"]) assert.Equal(t, "8080", attribs["access_port"]) assert.Equal(t, "owner123", attribs["owner"]) + assert.Equal(t, "11111111-1111-1111-1111-111111111111", attribs["owner_id"]) assert.Equal(t, "Mr Owner", attribs["owner_name"]) assert.Equal(t, "owner123@example.com", attribs["owner_email"]) assert.Equal(t, "group1", attribs["owner_groups.0"]) @@ -54,6 +57,7 @@ func TestWorkspace(t *testing.T) { assert.Equal(t, "templateID", attribs["template_id"]) assert.Equal(t, "template123", attribs["template_name"]) assert.Equal(t, "v1.2.3", attribs["template_version"]) + assert.Equal(t, "supersecret", attribs["owner_oidc_access_token"]) return nil }, }}, From 8696892af909bf7f4b83b201f077221854c4235a Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Fri, 24 May 2024 13:41:05 +0000 Subject: [PATCH 4/5] fixup! rename to coder_workspace_owner --- provider/provider.go | 14 +++++++------- provider/workspace_owner_test.go | 8 ++++---- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/provider/provider.go b/provider/provider.go index e59238f2..c1991a26 100644 --- a/provider/provider.go +++ b/provider/provider.go @@ -68,13 +68,13 @@ func New() *schema.Provider { }, nil }, DataSourcesMap: map[string]*schema.Resource{ - "coder_workspace": workspaceDataSource(), - "coder_workspace_tags": workspaceTagDataSource(), - "coder_provisioner": provisionerDataSource(), - "coder_parameter": parameterDataSource(), - "coder_git_auth": gitAuthDataSource(), - "coder_external_auth": externalAuthDataSource(), - "coder_user": workspaceOwnerDataSource(), + "coder_workspace": workspaceDataSource(), + "coder_workspace_tags": workspaceTagDataSource(), + "coder_provisioner": provisionerDataSource(), + "coder_parameter": parameterDataSource(), + "coder_git_auth": gitAuthDataSource(), + "coder_external_auth": externalAuthDataSource(), + "coder_workspace_owner": workspaceOwnerDataSource(), }, ResourcesMap: map[string]*schema.Resource{ "coder_agent": agentResource(), diff --git a/provider/workspace_owner_test.go b/provider/workspace_owner_test.go index b15c8efb..90839cfc 100644 --- a/provider/workspace_owner_test.go +++ b/provider/workspace_owner_test.go @@ -44,12 +44,12 @@ func TestWorkspaceOwnerDatasource(t *testing.T) { Steps: []resource.TestStep{{ Config: ` provider "coder" {} - data "coder_user" "me" {} + data "coder_workspace_owner" "me" {} `, Check: func(s *terraform.State) error { require.Len(t, s.Modules, 1) require.Len(t, s.Modules[0].Resources, 1) - resource := s.Modules[0].Resources["data.coder_user.me"] + resource := s.Modules[0].Resources["data.coder_workspace_owner.me"] require.NotNil(t, resource) attrs := resource.Primary.Attributes @@ -93,12 +93,12 @@ func TestWorkspaceOwnerDatasource(t *testing.T) { Steps: []resource.TestStep{{ Config: ` provider "coder" {} - data "coder_user" "me" {} + data "coder_workspace_owner" "me" {} `, Check: func(s *terraform.State) error { require.Len(t, s.Modules, 1) require.Len(t, s.Modules[0].Resources, 1) - resource := s.Modules[0].Resources["data.coder_user.me"] + resource := s.Modules[0].Resources["data.coder_workspace_owner.me"] require.NotNil(t, resource) attrs := resource.Primary.Attributes From 04de18d0b6dc68f06b2cd9512ea91af5800bdea2 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Fri, 24 May 2024 13:44:44 +0000 Subject: [PATCH 5/5] make gen --- docs/data-sources/{user.md => workspace_owner.md} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename docs/data-sources/{user.md => workspace_owner.md} (90%) diff --git a/docs/data-sources/user.md b/docs/data-sources/workspace_owner.md similarity index 90% rename from docs/data-sources/user.md rename to docs/data-sources/workspace_owner.md index abf717cc..646b1340 100644 --- a/docs/data-sources/user.md +++ b/docs/data-sources/workspace_owner.md @@ -1,12 +1,12 @@ --- # generated by https://github.com/hashicorp/terraform-plugin-docs -page_title: "coder_user Data Source - terraform-provider-coder" +page_title: "coder_workspace_owner Data Source - terraform-provider-coder" subcategory: "" description: |- Use this data source to fetch information about the workspace owner. --- -# coder_user (Data Source) +# coder_workspace_owner (Data Source) Use this data source to fetch information about the workspace owner.