Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit c2837a6

Browse files
authored
feat: evaluate provisioner tags (#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

+130-9
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)