Skip to content

Commit c2837a6

Browse files
authored
feat: evaluate provisioner tags (coder#13333)
1 parent fa9edc1 commit c2837a6

File tree

2 files changed

+252
-20
lines changed

2 files changed

+252
-20
lines changed

coderd/wsbuilder/wsbuilder.go

Lines changed: 130 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ import (
1010
"net/http"
1111
"time"
1212

13+
"github.com/hashicorp/hcl/v2"
14+
"github.com/hashicorp/hcl/v2/hclsyntax"
15+
"github.com/zclconf/go-cty/cty"
16+
1317
"github.com/coder/coder/v2/coderd/rbac/policy"
1418
"github.com/coder/coder/v2/provisionersdk"
1519

@@ -55,14 +59,17 @@ type Builder struct {
5559
store database.Store
5660

5761
// cache of objects, so we only fetch once
58-
template *database.Template
59-
templateVersion *database.TemplateVersion
60-
templateVersionJob *database.ProvisionerJob
61-
templateVersionParameters *[]database.TemplateVersionParameter
62-
lastBuild *database.WorkspaceBuild
63-
lastBuildErr *error
64-
lastBuildParameters *[]database.WorkspaceBuildParameter
65-
lastBuildJob *database.ProvisionerJob
62+
template *database.Template
63+
templateVersion *database.TemplateVersion
64+
templateVersionJob *database.ProvisionerJob
65+
templateVersionParameters *[]database.TemplateVersionParameter
66+
templateVersionWorkspaceTags *[]database.TemplateVersionWorkspaceTag
67+
lastBuild *database.WorkspaceBuild
68+
lastBuildErr *error
69+
lastBuildParameters *[]database.WorkspaceBuildParameter
70+
lastBuildJob *database.ProvisionerJob
71+
parameterNames *[]string
72+
parameterValues *[]string
6673

6774
verifyNoLegacyParametersOnce bool
6875
}
@@ -297,7 +304,11 @@ func (b *Builder) buildTx(authFunc func(action policy.Action, object rbac.Object
297304
if err != nil {
298305
return nil, nil, BuildError{http.StatusInternalServerError, "marshal metadata", err}
299306
}
300-
tags := provisionersdk.MutateTags(b.workspace.OwnerID, templateVersionJob.Tags)
307+
308+
tags, err := b.getProvisionerTags()
309+
if err != nil {
310+
return nil, nil, err // already wrapped BuildError
311+
}
301312

302313
now := dbtime.Now()
303314
provisionerJob, err := b.store.InsertProvisionerJob(b.ctx, database.InsertProvisionerJobParams{
@@ -364,6 +375,7 @@ func (b *Builder) buildTx(authFunc func(action policy.Action, object rbac.Object
364375
// getParameters already wraps errors in BuildError
365376
return err
366377
}
378+
367379
err = store.InsertWorkspaceBuildParameters(b.ctx, database.InsertWorkspaceBuildParametersParams{
368380
WorkspaceBuildID: workspaceBuildID,
369381
Name: names,
@@ -502,6 +514,10 @@ func (b *Builder) getState() ([]byte, error) {
502514
}
503515

504516
func (b *Builder) getParameters() (names, values []string, err error) {
517+
if b.parameterNames != nil {
518+
return *b.parameterNames, *b.parameterValues, nil
519+
}
520+
505521
templateVersionParameters, err := b.getTemplateVersionParameters()
506522
if err != nil {
507523
return nil, nil, BuildError{http.StatusInternalServerError, "failed to fetch template version parameters", err}
@@ -535,6 +551,9 @@ func (b *Builder) getParameters() (names, values []string, err error) {
535551
names = append(names, templateVersionParameter.Name)
536552
values = append(values, value)
537553
}
554+
555+
b.parameterNames = &names
556+
b.parameterValues = &values
538557
return names, values, nil
539558
}
540559

@@ -632,6 +651,108 @@ func (b *Builder) getLastBuildJob() (*database.ProvisionerJob, error) {
632651
return b.lastBuildJob, nil
633652
}
634653

654+
func (b *Builder) getProvisionerTags() (map[string]string, error) {
655+
// Step 1: Mutate template version tags
656+
templateVersionJob, err := b.getTemplateVersionJob()
657+
if err != nil {
658+
return nil, BuildError{http.StatusInternalServerError, "failed to fetch template version job", err}
659+
}
660+
annotationTags := provisionersdk.MutateTags(b.workspace.OwnerID, templateVersionJob.Tags)
661+
662+
tags := map[string]string{}
663+
for name, value := range annotationTags {
664+
tags[name] = value
665+
}
666+
667+
// Step 2: Mutate workspace tags
668+
workspaceTags, err := b.getTemplateVersionWorkspaceTags()
669+
if err != nil {
670+
return nil, BuildError{http.StatusInternalServerError, "failed to fetch template version workspace tags", err}
671+
}
672+
parameterNames, parameterValues, err := b.getParameters()
673+
if err != nil {
674+
return nil, err // already wrapped BuildError
675+
}
676+
677+
evalCtx := buildParametersEvalContext(parameterNames, parameterValues)
678+
for _, workspaceTag := range workspaceTags {
679+
expr, diags := hclsyntax.ParseExpression([]byte(workspaceTag.Value), "expression.hcl", hcl.InitialPos)
680+
if diags.HasErrors() {
681+
return nil, BuildError{http.StatusBadRequest, "failed to parse workspace tag value", xerrors.Errorf(diags.Error())}
682+
}
683+
684+
val, diags := expr.Value(evalCtx)
685+
if diags.HasErrors() {
686+
return nil, BuildError{http.StatusBadRequest, "failed to evaluate workspace tag value", xerrors.Errorf(diags.Error())}
687+
}
688+
689+
// Do not use "val.AsString()" as it can panic
690+
str, err := ctyValueString(val)
691+
if err != nil {
692+
return nil, BuildError{http.StatusBadRequest, "failed to marshal cty.Value as string", err}
693+
}
694+
tags[workspaceTag.Key] = str
695+
}
696+
return tags, nil
697+
}
698+
699+
func buildParametersEvalContext(names, values []string) *hcl.EvalContext {
700+
m := map[string]cty.Value{}
701+
for i, name := range names {
702+
m[name] = cty.MapVal(map[string]cty.Value{
703+
"value": cty.StringVal(values[i]),
704+
})
705+
}
706+
707+
if len(m) == 0 {
708+
return nil // otherwise, panic: must not call MapVal with empty map
709+
}
710+
711+
return &hcl.EvalContext{
712+
Variables: map[string]cty.Value{
713+
"data": cty.MapVal(map[string]cty.Value{
714+
"coder_parameter": cty.MapVal(m),
715+
}),
716+
},
717+
}
718+
}
719+
720+
func ctyValueString(val cty.Value) (string, error) {
721+
switch val.Type() {
722+
case cty.Bool:
723+
if val.True() {
724+
return "true", nil
725+
} else {
726+
return "false", nil
727+
}
728+
case cty.Number:
729+
return val.AsBigFloat().String(), nil
730+
case cty.String:
731+
return val.AsString(), nil
732+
default:
733+
return "", xerrors.Errorf("only primitive types are supported - bool, number, and string")
734+
}
735+
}
736+
737+
func (b *Builder) getTemplateVersionWorkspaceTags() ([]database.TemplateVersionWorkspaceTag, error) {
738+
if b.templateVersionWorkspaceTags != nil {
739+
return *b.templateVersionWorkspaceTags, nil
740+
}
741+
742+
templateVersion, err := b.getTemplateVersion()
743+
if err != nil {
744+
return nil, xerrors.Errorf("get template version: %w", err)
745+
}
746+
747+
workspaceTags, err := b.store.GetTemplateVersionWorkspaceTags(b.ctx, templateVersion.ID)
748+
if err != nil && !xerrors.Is(err, sql.ErrNoRows) {
749+
return nil, xerrors.Errorf("get template version workspace tags: %w", err)
750+
}
751+
752+
b.templateVersionWorkspaceTags = &workspaceTags
753+
return *b.templateVersionWorkspaceTags, nil
754+
}
755+
635756
// authorize performs build authorization pre-checks using the provided authFunc
636757
func (b *Builder) authorize(authFunc func(action policy.Action, object rbac.Objecter) bool) error {
637758
// Doing this up front saves a lot of work if the user doesn't have permission.

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