Skip to content

Commit eb43b9f

Browse files
authored
feat: add coder_workspace_owner datasource (#230)
- Adds a coder_workspace_owner data source populated from fields of coder_workspace prefix with `owner_`. - Adds `coder_workspace_owner.ssh_{public,private}_key`. - Deprecates same fields of coder_workspace.
1 parent 7e5a28b commit eb43b9f

File tree

7 files changed

+299
-13
lines changed

7 files changed

+299
-13
lines changed

docs/data-sources/workspace.md

+7-7
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,13 @@ resource "kubernetes_pod" "dev" {
3030
- `access_url` (String) The access URL of the Coder deployment provisioning this workspace.
3131
- `id` (String) UUID of the workspace.
3232
- `name` (String) Name of the workspace.
33-
- `owner` (String) Username of the workspace owner.
34-
- `owner_email` (String) Email address of the workspace owner.
35-
- `owner_groups` (List of String) List of groups the workspace owner belongs to.
36-
- `owner_id` (String) UUID of the workspace owner.
37-
- `owner_name` (String) Name of the workspace owner.
38-
- `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.
39-
- `owner_session_token` (String) Session token for authenticating with a Coder deployment. It is regenerated everytime a workspace is started.
33+
- `owner` (String, Deprecated) Username of the workspace owner.
34+
- `owner_email` (String, Deprecated) Email address of the workspace owner.
35+
- `owner_groups` (List of String, Deprecated) List of groups the workspace owner belongs to.
36+
- `owner_id` (String, Deprecated) UUID of the workspace owner.
37+
- `owner_name` (String, Deprecated) Name of the workspace owner.
38+
- `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.
39+
- `owner_session_token` (String, Deprecated) Session token for authenticating with a Coder deployment. It is regenerated everytime a workspace is started.
4040
- `start_count` (Number) A computed count based on "transition" state. If "start", count will equal 1.
4141
- `template_id` (String) ID of the workspace's template.
4242
- `template_name` (String) Name of the workspace's template.

docs/data-sources/workspace_owner.md

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
---
2+
# generated by https://github.com/hashicorp/terraform-plugin-docs
3+
page_title: "coder_workspace_owner Data Source - terraform-provider-coder"
4+
subcategory: ""
5+
description: |-
6+
Use this data source to fetch information about the workspace owner.
7+
---
8+
9+
# coder_workspace_owner (Data Source)
10+
11+
Use this data source to fetch information about the workspace owner.
12+
13+
14+
15+
<!-- schema generated by tfplugindocs -->
16+
## Schema
17+
18+
### Read-Only
19+
20+
- `email` (String) The email address of the user.
21+
- `full_name` (String) The full name of the user.
22+
- `groups` (List of String) The groups of which the user is a member.
23+
- `id` (String) The UUID of the workspace owner.
24+
- `name` (String) The username of the user.
25+
- `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.
26+
- `session_token` (String) Session token for authenticating with a Coder deployment. It is regenerated every time a workspace is started.
27+
- `ssh_private_key` (String, Sensitive) The user's generated SSH private key.
28+
- `ssh_public_key` (String) The user's generated SSH public key.

provider/provider.go

+7-6
Original file line numberDiff line numberDiff line change
@@ -68,12 +68,13 @@ func New() *schema.Provider {
6868
}, nil
6969
},
7070
DataSourcesMap: map[string]*schema.Resource{
71-
"coder_workspace": workspaceDataSource(),
72-
"coder_workspace_tags": workspaceTagDataSource(),
73-
"coder_provisioner": provisionerDataSource(),
74-
"coder_parameter": parameterDataSource(),
75-
"coder_git_auth": gitAuthDataSource(),
76-
"coder_external_auth": externalAuthDataSource(),
71+
"coder_workspace": workspaceDataSource(),
72+
"coder_workspace_tags": workspaceTagDataSource(),
73+
"coder_provisioner": provisionerDataSource(),
74+
"coder_parameter": parameterDataSource(),
75+
"coder_git_auth": gitAuthDataSource(),
76+
"coder_external_auth": externalAuthDataSource(),
77+
"coder_workspace_owner": workspaceOwnerDataSource(),
7778
},
7879
ResourcesMap: map[string]*schema.Resource{
7980
"coder_agent": agentResource(),

provider/workspace.go

+7
Original file line numberDiff line numberDiff line change
@@ -135,28 +135,33 @@ func workspaceDataSource() *schema.Resource {
135135
Type: schema.TypeString,
136136
Computed: true,
137137
Description: "Username of the workspace owner.",
138+
Deprecated: "Use `coder_workspace_owner.name` instead.",
138139
},
139140
"owner_email": {
140141
Type: schema.TypeString,
141142
Computed: true,
142143
Description: "Email address of the workspace owner.",
144+
Deprecated: "Use `coder_workspace_owner.email` instead.",
143145
},
144146
"owner_id": {
145147
Type: schema.TypeString,
146148
Computed: true,
147149
Description: "UUID of the workspace owner.",
150+
Deprecated: "Use `coder_workspace_owner.id` instead.",
148151
},
149152
"owner_name": {
150153
Type: schema.TypeString,
151154
Computed: true,
152155
Description: "Name of the workspace owner.",
156+
Deprecated: "Use `coder_workspace_owner.full_name` instead.",
153157
},
154158
"owner_oidc_access_token": {
155159
Type: schema.TypeString,
156160
Computed: true,
157161
Description: "A valid OpenID Connect access token of the workspace owner. " +
158162
"This is only available if the workspace owner authenticated with OpenID Connect. " +
159163
"If a valid token cannot be obtained, this value will be an empty string.",
164+
Deprecated: "Use `coder_workspace_owner.oidc_access_token` instead.",
160165
},
161166
"owner_groups": {
162167
Type: schema.TypeList,
@@ -165,6 +170,7 @@ func workspaceDataSource() *schema.Resource {
165170
},
166171
Computed: true,
167172
Description: "List of groups the workspace owner belongs to.",
173+
Deprecated: "Use `coder_workspace_owner.groups` instead.",
168174
},
169175
"id": {
170176
Type: schema.TypeString,
@@ -180,6 +186,7 @@ func workspaceDataSource() *schema.Resource {
180186
Type: schema.TypeString,
181187
Computed: true,
182188
Description: "Session token for authenticating with a Coder deployment. It is regenerated everytime a workspace is started.",
189+
Deprecated: "Use `coder_workspace_owner.session_token` instead.",
183190
},
184191
"template_id": {
185192
Type: schema.TypeString,

provider/workspace_owner.go

+127
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
package provider
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"os"
7+
"strings"
8+
9+
"github.com/google/uuid"
10+
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
11+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
12+
)
13+
14+
type Role struct {
15+
Name string `json:"name"`
16+
DisplayName string `json:"display-name"`
17+
}
18+
19+
func workspaceOwnerDataSource() *schema.Resource {
20+
return &schema.Resource{
21+
Description: "Use this data source to fetch information about the workspace owner.",
22+
ReadContext: func(ctx context.Context, rd *schema.ResourceData, i interface{}) diag.Diagnostics {
23+
if idStr, ok := os.LookupEnv("CODER_WORKSPACE_OWNER_ID"); ok {
24+
rd.SetId(idStr)
25+
} else {
26+
rd.SetId(uuid.NewString())
27+
}
28+
29+
if username, ok := os.LookupEnv("CODER_WORKSPACE_OWNER"); ok {
30+
_ = rd.Set("name", username)
31+
} else {
32+
_ = rd.Set("name", "default")
33+
}
34+
35+
if fullname, ok := os.LookupEnv("CODER_WORKSPACE_OWNER_NAME"); ok {
36+
_ = rd.Set("full_name", fullname)
37+
} else { // compat: field can be blank, fill in default
38+
_ = rd.Set("full_name", "default")
39+
}
40+
41+
if email, ok := os.LookupEnv("CODER_WORKSPACE_OWNER_EMAIL"); ok {
42+
_ = rd.Set("email", email)
43+
} else {
44+
_ = rd.Set("email", "[email protected]")
45+
}
46+
47+
if sshPubKey, ok := os.LookupEnv("CODER_WORKSPACE_OWNER_SSH_PUBLIC_KEY"); ok {
48+
_ = rd.Set("ssh_public_key", sshPubKey)
49+
}
50+
51+
if sshPrivKey, ok := os.LookupEnv("CODER_WORKSPACE_OWNER_SSH_PRIVATE_KEY"); ok {
52+
_ = rd.Set("ssh_private_key", sshPrivKey)
53+
}
54+
55+
var groups []string
56+
if groupsRaw, ok := os.LookupEnv("CODER_WORKSPACE_OWNER_GROUPS"); ok {
57+
if err := json.NewDecoder(strings.NewReader(groupsRaw)).Decode(&groups); err != nil {
58+
return diag.Errorf("invalid user groups: %s", err.Error())
59+
}
60+
_ = rd.Set("groups", groups)
61+
}
62+
63+
if tok, ok := os.LookupEnv("CODER_WORKSPACE_OWNER_SESSION_TOKEN"); ok {
64+
_ = rd.Set("session_token", tok)
65+
}
66+
67+
if tok, ok := os.LookupEnv("CODER_WORKSPACE_OWNER_OIDC_ACCESS_TOKEN"); ok {
68+
_ = rd.Set("oidc_access_token", tok)
69+
}
70+
71+
return nil
72+
},
73+
Schema: map[string]*schema.Schema{
74+
"id": {
75+
Type: schema.TypeString,
76+
Computed: true,
77+
Description: "The UUID of the workspace owner.",
78+
},
79+
"name": {
80+
Type: schema.TypeString,
81+
Computed: true,
82+
Description: "The username of the user.",
83+
},
84+
"full_name": {
85+
Type: schema.TypeString,
86+
Computed: true,
87+
Description: "The full name of the user.",
88+
},
89+
"email": {
90+
Type: schema.TypeString,
91+
Computed: true,
92+
Description: "The email address of the user.",
93+
},
94+
"ssh_public_key": {
95+
Type: schema.TypeString,
96+
Computed: true,
97+
Description: "The user's generated SSH public key.",
98+
},
99+
"ssh_private_key": {
100+
Type: schema.TypeString,
101+
Computed: true,
102+
Description: "The user's generated SSH private key.",
103+
Sensitive: true,
104+
},
105+
"groups": {
106+
Type: schema.TypeList,
107+
Elem: &schema.Schema{
108+
Type: schema.TypeString,
109+
},
110+
Computed: true,
111+
Description: "The groups of which the user is a member.",
112+
},
113+
"session_token": {
114+
Type: schema.TypeString,
115+
Computed: true,
116+
Description: "Session token for authenticating with a Coder deployment. It is regenerated every time a workspace is started.",
117+
},
118+
"oidc_access_token": {
119+
Type: schema.TypeString,
120+
Computed: true,
121+
Description: "A valid OpenID Connect access token of the workspace owner. " +
122+
"This is only available if the workspace owner authenticated with OpenID Connect. " +
123+
"If a valid token cannot be obtained, this value will be an empty string.",
124+
},
125+
},
126+
}
127+
}

provider/workspace_owner_test.go

+119
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
package provider_test
2+
3+
import (
4+
"os"
5+
"testing"
6+
7+
"github.com/coder/terraform-provider-coder/provider"
8+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
9+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
10+
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
11+
"github.com/stretchr/testify/assert"
12+
"github.com/stretchr/testify/require"
13+
)
14+
15+
const (
16+
testSSHEd25519PublicKey = `ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJeNcdBMtd4Jo9f2W8RZef0ld7Ypye5zTQEf0vUXa/Eq owner123@host456`
17+
// nolint:gosec // This key was generated specifically for this purpose.
18+
testSSHEd25519PrivateKey = `-----BEGIN OPENSSH PRIVATE KEY-----
19+
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
20+
QyNTUxOQAAACCXjXHQTLXeCaPX9lvEWXn9JXe2Kcnuc00BH9L1F2vxKgAAAJgp3mfQKd5n
21+
0AAAAAtzc2gtZWQyNTUxOQAAACCXjXHQTLXeCaPX9lvEWXn9JXe2Kcnuc00BH9L1F2vxKg
22+
AAAEBia7mAQFoLBILlvTJroTkOUomzfcPY9ckpViQOjYFkAZeNcdBMtd4Jo9f2W8RZef0l
23+
d7Ypye5zTQEf0vUXa/EqAAAAE3ZzY29kZUAzY2Y4MWY5YmM3MmQBAg==
24+
-----END OPENSSH PRIVATE KEY-----`
25+
)
26+
27+
func TestWorkspaceOwnerDatasource(t *testing.T) {
28+
t.Run("OK", func(t *testing.T) {
29+
t.Setenv("CODER_WORKSPACE_OWNER_ID", "11111111-1111-1111-1111-111111111111")
30+
t.Setenv("CODER_WORKSPACE_OWNER", "owner123")
31+
t.Setenv("CODER_WORKSPACE_OWNER_NAME", "Mr Owner")
32+
t.Setenv("CODER_WORKSPACE_OWNER_EMAIL", "[email protected]")
33+
t.Setenv("CODER_WORKSPACE_OWNER_SSH_PUBLIC_KEY", testSSHEd25519PublicKey)
34+
t.Setenv("CODER_WORKSPACE_OWNER_SSH_PRIVATE_KEY", testSSHEd25519PrivateKey)
35+
t.Setenv("CODER_WORKSPACE_OWNER_GROUPS", `["group1", "group2"]`)
36+
t.Setenv("CODER_WORKSPACE_OWNER_SESSION_TOKEN", `supersecret`)
37+
t.Setenv("CODER_WORKSPACE_OWNER_OIDC_ACCESS_TOKEN", `alsosupersecret`)
38+
39+
resource.Test(t, resource.TestCase{
40+
Providers: map[string]*schema.Provider{
41+
"coder": provider.New(),
42+
},
43+
IsUnitTest: true,
44+
Steps: []resource.TestStep{{
45+
Config: `
46+
provider "coder" {}
47+
data "coder_workspace_owner" "me" {}
48+
`,
49+
Check: func(s *terraform.State) error {
50+
require.Len(t, s.Modules, 1)
51+
require.Len(t, s.Modules[0].Resources, 1)
52+
resource := s.Modules[0].Resources["data.coder_workspace_owner.me"]
53+
require.NotNil(t, resource)
54+
55+
attrs := resource.Primary.Attributes
56+
assert.Equal(t, "11111111-1111-1111-1111-111111111111", attrs["id"])
57+
assert.Equal(t, "owner123", attrs["name"])
58+
assert.Equal(t, "Mr Owner", attrs["full_name"])
59+
assert.Equal(t, "[email protected]", attrs["email"])
60+
assert.Equal(t, testSSHEd25519PublicKey, attrs["ssh_public_key"])
61+
assert.Equal(t, testSSHEd25519PrivateKey, attrs["ssh_private_key"])
62+
assert.Equal(t, `group1`, attrs["groups.0"])
63+
assert.Equal(t, `group2`, attrs["groups.1"])
64+
assert.Equal(t, `supersecret`, attrs["session_token"])
65+
assert.Equal(t, `alsosupersecret`, attrs["oidc_access_token"])
66+
return nil
67+
},
68+
}},
69+
})
70+
})
71+
72+
t.Run("Defaults", func(t *testing.T) {
73+
for _, v := range []string{
74+
"CODER_WORKSPACE_OWNER",
75+
"CODER_WORKSPACE_OWNER_ID",
76+
"CODER_WORKSPACE_OWNER_EMAIL",
77+
"CODER_WORKSPACE_OWNER_NAME",
78+
"CODER_WORKSPACE_OWNER_SESSION_TOKEN",
79+
"CODER_WORKSPACE_OWNER_GROUPS",
80+
"CODER_WORKSPACE_OWNER_OIDC_ACCESS_TOKEN",
81+
"CODER_WORKSPACE_OWNER_SSH_PUBLIC_KEY",
82+
"CODER_WORKSPACE_OWNER_SSH_PRIVATE_KEY",
83+
} { // https://github.com/golang/go/issues/52817
84+
t.Setenv(v, "")
85+
os.Unsetenv(v)
86+
}
87+
88+
resource.Test(t, resource.TestCase{
89+
Providers: map[string]*schema.Provider{
90+
"coder": provider.New(),
91+
},
92+
IsUnitTest: true,
93+
Steps: []resource.TestStep{{
94+
Config: `
95+
provider "coder" {}
96+
data "coder_workspace_owner" "me" {}
97+
`,
98+
Check: func(s *terraform.State) error {
99+
require.Len(t, s.Modules, 1)
100+
require.Len(t, s.Modules[0].Resources, 1)
101+
resource := s.Modules[0].Resources["data.coder_workspace_owner.me"]
102+
require.NotNil(t, resource)
103+
104+
attrs := resource.Primary.Attributes
105+
assert.NotEmpty(t, attrs["id"])
106+
assert.Equal(t, "default", attrs["name"])
107+
assert.Equal(t, "default", attrs["full_name"])
108+
assert.Equal(t, "[email protected]", attrs["email"])
109+
assert.Empty(t, attrs["ssh_public_key"])
110+
assert.Empty(t, attrs["ssh_private_key"])
111+
assert.Empty(t, attrs["groups.0"])
112+
assert.Empty(t, attrs["session_token"])
113+
assert.Empty(t, attrs["oidc_access_token"])
114+
return nil
115+
},
116+
}},
117+
})
118+
})
119+
}

provider/workspace_test.go

+4
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,12 @@ import (
1414

1515
func TestWorkspace(t *testing.T) {
1616
t.Setenv("CODER_WORKSPACE_OWNER", "owner123")
17+
t.Setenv("CODER_WORKSPACE_OWNER_ID", "11111111-1111-1111-1111-111111111111")
1718
t.Setenv("CODER_WORKSPACE_OWNER_NAME", "Mr Owner")
1819
t.Setenv("CODER_WORKSPACE_OWNER_EMAIL", "[email protected]")
1920
t.Setenv("CODER_WORKSPACE_OWNER_SESSION_TOKEN", "abc123")
2021
t.Setenv("CODER_WORKSPACE_OWNER_GROUPS", `["group1", "group2"]`)
22+
t.Setenv("CODER_WORKSPACE_OWNER_OIDC_ACCESS_TOKEN", "supersecret")
2123
t.Setenv("CODER_WORKSPACE_TEMPLATE_ID", "templateID")
2224
t.Setenv("CODER_WORKSPACE_TEMPLATE_NAME", "template123")
2325
t.Setenv("CODER_WORKSPACE_TEMPLATE_VERSION", "v1.2.3")
@@ -47,13 +49,15 @@ func TestWorkspace(t *testing.T) {
4749
assert.Equal(t, "https://example.com:8080", attribs["access_url"])
4850
assert.Equal(t, "8080", attribs["access_port"])
4951
assert.Equal(t, "owner123", attribs["owner"])
52+
assert.Equal(t, "11111111-1111-1111-1111-111111111111", attribs["owner_id"])
5053
assert.Equal(t, "Mr Owner", attribs["owner_name"])
5154
assert.Equal(t, "[email protected]", attribs["owner_email"])
5255
assert.Equal(t, "group1", attribs["owner_groups.0"])
5356
assert.Equal(t, "group2", attribs["owner_groups.1"])
5457
assert.Equal(t, "templateID", attribs["template_id"])
5558
assert.Equal(t, "template123", attribs["template_name"])
5659
assert.Equal(t, "v1.2.3", attribs["template_version"])
60+
assert.Equal(t, "supersecret", attribs["owner_oidc_access_token"])
5761
return nil
5862
},
5963
}},

0 commit comments

Comments
 (0)