Skip to content

Commit f4cd152

Browse files
committed
feat: use preview to compute workspace tags from terraform
1 parent f0dd768 commit f4cd152

File tree

10 files changed

+1020
-87
lines changed

10 files changed

+1020
-87
lines changed

archive/fs/zip.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package archivefs
2+
3+
import (
4+
"archive/zip"
5+
"io"
6+
"io/fs"
7+
8+
"github.com/spf13/afero"
9+
"github.com/spf13/afero/zipfs"
10+
)
11+
12+
// FromZipReader creates a read-only in-memory FS
13+
func FromZipReader(r io.ReaderAt, size int64) (fs.FS, error) {
14+
zr, err := zip.NewReader(r, size)
15+
if err != nil {
16+
return nil, err
17+
}
18+
return afero.NewIOFS(zipfs.New(zr)), nil
19+
}

coderd/dynamicparameters/error.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,17 @@ import (
1010
"github.com/coder/coder/v2/codersdk"
1111
)
1212

13-
func ParameterValidationError(diags hcl.Diagnostics) *DiagnosticError {
13+
func parameterValidationError(diags hcl.Diagnostics) *DiagnosticError {
1414
return &DiagnosticError{
1515
Message: "Unable to validate parameters",
1616
Diagnostics: diags,
1717
KeyedDiagnostics: make(map[string]hcl.Diagnostics),
1818
}
1919
}
2020

21-
func TagValidationError(diags hcl.Diagnostics) *DiagnosticError {
21+
func tagValidationError(diags hcl.Diagnostics) *DiagnosticError {
2222
return &DiagnosticError{
23-
Message: "Failed to parse workspace tags",
23+
Message: "Failed to parse provisioner tags",
2424
Diagnostics: diags,
2525
KeyedDiagnostics: make(map[string]hcl.Diagnostics),
2626
}

coderd/dynamicparameters/render.go

Lines changed: 38 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,28 @@ func (r *dynamicRenderer) getWorkspaceOwnerData(ctx context.Context, ownerID uui
243243
return nil // already fetched
244244
}
245245

246-
user, err := r.db.GetUserByID(ctx, ownerID)
246+
owner, err := WorkspaceOwner(ctx, r.db, r.data.templateVersion.OrganizationID, ownerID)
247+
if err != nil {
248+
return err
249+
}
250+
251+
r.currentOwner = owner
252+
return nil
253+
}
254+
255+
func (r *dynamicRenderer) Close() {
256+
r.once.Do(r.close)
257+
}
258+
259+
func ProvisionerVersionSupportsDynamicParameters(version string) bool {
260+
major, minor, err := apiversion.Parse(version)
261+
// If the api version is not valid or less than 1.6, we need to use the static parameters
262+
useStaticParams := err != nil || major < 1 || (major == 1 && minor < 6)
263+
return !useStaticParams
264+
}
265+
266+
func WorkspaceOwner(ctx context.Context, db database.Store, org uuid.UUID, ownerID uuid.UUID) (*previewtypes.WorkspaceOwner, error) {
267+
user, err := db.GetUserByID(ctx, ownerID)
247268
if err != nil {
248269
// If the user failed to read, we also try to read the user from their
249270
// organization member. You only need to be able to read the organization member
@@ -252,37 +273,37 @@ func (r *dynamicRenderer) getWorkspaceOwnerData(ctx context.Context, ownerID uui
252273
// Only the terraform files can therefore leak more information than the
253274
// caller should have access to. All this info should be public assuming you can
254275
// read the user though.
255-
mem, err := database.ExpectOne(r.db.OrganizationMembers(ctx, database.OrganizationMembersParams{
256-
OrganizationID: r.data.templateVersion.OrganizationID,
276+
mem, err := database.ExpectOne(db.OrganizationMembers(ctx, database.OrganizationMembersParams{
277+
OrganizationID: org,
257278
UserID: ownerID,
258279
IncludeSystem: true,
259280
}))
260281
if err != nil {
261-
return xerrors.Errorf("fetch user: %w", err)
282+
return nil, xerrors.Errorf("fetch user: %w", err)
262283
}
263284

264285
// Org member fetched, so use the provisioner context to fetch the user.
265286
//nolint:gocritic // Has the correct permissions, and matches the provisioning flow.
266-
user, err = r.db.GetUserByID(dbauthz.AsProvisionerd(ctx), mem.OrganizationMember.UserID)
287+
user, err = db.GetUserByID(dbauthz.AsProvisionerd(ctx), mem.OrganizationMember.UserID)
267288
if err != nil {
268-
return xerrors.Errorf("fetch user: %w", err)
289+
return nil, xerrors.Errorf("fetch user: %w", err)
269290
}
270291
}
271292

272293
// nolint:gocritic // This is kind of the wrong query to use here, but it
273294
// matches how the provisioner currently works. We should figure out
274295
// something that needs less escalation but has the correct behavior.
275-
row, err := r.db.GetAuthorizationUserRoles(dbauthz.AsProvisionerd(ctx), ownerID)
296+
row, err := db.GetAuthorizationUserRoles(dbauthz.AsProvisionerd(ctx), ownerID)
276297
if err != nil {
277-
return xerrors.Errorf("user roles: %w", err)
298+
return nil, xerrors.Errorf("user roles: %w", err)
278299
}
279300
roles, err := row.RoleNames()
280301
if err != nil {
281-
return xerrors.Errorf("expand roles: %w", err)
302+
return nil, xerrors.Errorf("expand roles: %w", err)
282303
}
283304
ownerRoles := make([]previewtypes.WorkspaceOwnerRBACRole, 0, len(roles))
284305
for _, it := range roles {
285-
if it.OrganizationID != uuid.Nil && it.OrganizationID != r.data.templateVersion.OrganizationID {
306+
if it.OrganizationID != uuid.Nil && it.OrganizationID != org {
286307
continue
287308
}
288309
var orgID string
@@ -298,28 +319,28 @@ func (r *dynamicRenderer) getWorkspaceOwnerData(ctx context.Context, ownerID uui
298319
// The correct public key has to be sent. This will not be leaked
299320
// unless the template leaks it.
300321
// nolint:gocritic
301-
key, err := r.db.GetGitSSHKey(dbauthz.AsProvisionerd(ctx), ownerID)
322+
key, err := db.GetGitSSHKey(dbauthz.AsProvisionerd(ctx), ownerID)
302323
if err != nil && !xerrors.Is(err, sql.ErrNoRows) {
303-
return xerrors.Errorf("ssh key: %w", err)
324+
return nil, xerrors.Errorf("ssh key: %w", err)
304325
}
305326

306327
// The groups need to be sent to preview. These groups are not exposed to the
307328
// user, unless the template does it through the parameters. Regardless, we need
308329
// the correct groups, and a user might not have read access.
309330
// nolint:gocritic
310-
groups, err := r.db.GetGroups(dbauthz.AsProvisionerd(ctx), database.GetGroupsParams{
311-
OrganizationID: r.data.templateVersion.OrganizationID,
331+
groups, err := db.GetGroups(dbauthz.AsProvisionerd(ctx), database.GetGroupsParams{
332+
OrganizationID: org,
312333
HasMemberID: ownerID,
313334
})
314335
if err != nil {
315-
return xerrors.Errorf("groups: %w", err)
336+
return nil, xerrors.Errorf("groups: %w", err)
316337
}
317338
groupNames := make([]string, 0, len(groups))
318339
for _, it := range groups {
319340
groupNames = append(groupNames, it.Group.Name)
320341
}
321342

322-
r.currentOwner = &previewtypes.WorkspaceOwner{
343+
return &previewtypes.WorkspaceOwner{
323344
ID: user.ID.String(),
324345
Name: user.Username,
325346
FullName: user.Name,
@@ -328,17 +349,5 @@ func (r *dynamicRenderer) getWorkspaceOwnerData(ctx context.Context, ownerID uui
328349
RBACRoles: ownerRoles,
329350
SSHPublicKey: key.PublicKey,
330351
Groups: groupNames,
331-
}
332-
return nil
333-
}
334-
335-
func (r *dynamicRenderer) Close() {
336-
r.once.Do(r.close)
337-
}
338-
339-
func ProvisionerVersionSupportsDynamicParameters(version string) bool {
340-
major, minor, err := apiversion.Parse(version)
341-
// If the api version is not valid or less than 1.6, we need to use the static parameters
342-
useStaticParams := err != nil || major < 1 || (major == 1 && minor < 6)
343-
return !useStaticParams
352+
}, nil
344353
}

coderd/dynamicparameters/resolver.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ func ResolveParameters(
7373
// always be valid. If there is a case where this is not true, then this has to
7474
// be changed to allow the build to continue with a different set of values.
7575

76-
return nil, ParameterValidationError(diags)
76+
return nil, parameterValidationError(diags)
7777
}
7878

7979
// The user's input now needs to be validated against the parameters.
@@ -113,13 +113,13 @@ func ResolveParameters(
113113
// are fatal. Additional validation for immutability has to be done manually.
114114
output, diags = renderer.Render(ctx, ownerID, values.ValuesMap())
115115
if diags.HasErrors() {
116-
return nil, ParameterValidationError(diags)
116+
return nil, parameterValidationError(diags)
117117
}
118118

119119
// parameterNames is going to be used to remove any excess values that were left
120120
// around without a parameter.
121121
parameterNames := make(map[string]struct{}, len(output.Parameters))
122-
parameterError := ParameterValidationError(nil)
122+
parameterError := parameterValidationError(nil)
123123
for _, parameter := range output.Parameters {
124124
parameterNames[parameter.Name] = struct{}{}
125125

coderd/dynamicparameters/tags.go

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
package dynamicparameters
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/hashicorp/hcl/v2"
7+
8+
"github.com/coder/preview"
9+
previewtypes "github.com/coder/preview/types"
10+
)
11+
12+
func CheckTags(output *preview.Output, diags hcl.Diagnostics) *DiagnosticError {
13+
de := tagValidationError(diags)
14+
failedTags := output.WorkspaceTags.UnusableTags()
15+
if len(failedTags) == 0 && !de.HasError() {
16+
return nil // No errors, all is good!
17+
}
18+
19+
for _, tag := range failedTags {
20+
name := tag.KeyString()
21+
if name == previewtypes.UnknownStringValue {
22+
name = "unknown" // Best effort to get a name for the tag
23+
}
24+
de.Extend(name, failedTagDiagnostic(tag))
25+
}
26+
return de
27+
}
28+
29+
// failedTagDiagnostic is a helper function that takes an invalid tag and
30+
// returns an appropriate hcl diagnostic for it.
31+
func failedTagDiagnostic(tag previewtypes.Tag) hcl.Diagnostics {
32+
const (
33+
key = "key"
34+
value = "value"
35+
)
36+
37+
diags := hcl.Diagnostics{}
38+
39+
// TODO: It would be really nice to pull out the variable references to help identify the source of
40+
// the unknown or invalid tag.
41+
unknownErr := "Tag %s is not known, it likely refers to a variable that is not set or has no default."
42+
invalidErr := "Tag %s is not valid, it must be a non-null string value."
43+
44+
if !tag.Key.Value.IsWhollyKnown() {
45+
diags = diags.Append(&hcl.Diagnostic{
46+
Severity: hcl.DiagError,
47+
Summary: fmt.Sprintf(unknownErr, key),
48+
})
49+
} else if !tag.Key.Valid() {
50+
diags = diags.Append(&hcl.Diagnostic{
51+
Severity: hcl.DiagError,
52+
Summary: fmt.Sprintf(invalidErr, key),
53+
})
54+
}
55+
56+
if !tag.Value.Value.IsWhollyKnown() {
57+
diags = diags.Append(&hcl.Diagnostic{
58+
Severity: hcl.DiagError,
59+
Summary: fmt.Sprintf(unknownErr, value),
60+
})
61+
} else if !tag.Value.Valid() {
62+
diags = diags.Append(&hcl.Diagnostic{
63+
Severity: hcl.DiagError,
64+
Summary: fmt.Sprintf(invalidErr, value),
65+
})
66+
}
67+
68+
if diags.HasErrors() {
69+
// Stop here if there are diags, as the diags manually created above are more
70+
// informative than the original tag's diagnostics.
71+
return diags
72+
}
73+
74+
// If we reach here, decorate the original tag's diagnostics
75+
diagErr := "Tag %s: %s"
76+
if tag.Key.ValueDiags.HasErrors() {
77+
// add 'Tag key' prefix to each diagnostic
78+
for _, d := range tag.Key.ValueDiags {
79+
d.Summary = fmt.Sprintf(diagErr, key, d.Summary)
80+
}
81+
}
82+
diags = diags.Extend(tag.Key.ValueDiags)
83+
84+
if tag.Value.ValueDiags.HasErrors() {
85+
// add 'Tag value' prefix to each diagnostic
86+
for _, d := range tag.Value.ValueDiags {
87+
d.Summary = fmt.Sprintf(diagErr, value, d.Summary)
88+
}
89+
}
90+
diags = diags.Extend(tag.Value.ValueDiags)
91+
92+
if !diags.HasErrors() {
93+
diags = diags.Append(&hcl.Diagnostic{
94+
Severity: hcl.DiagError,
95+
Summary: "Tag is invalid for some unknown reason. Please check the tag's value and key.",
96+
})
97+
}
98+
99+
return diags
100+
}

0 commit comments

Comments
 (0)