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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy