@@ -10,6 +10,10 @@ import (
10
10
"net/http"
11
11
"time"
12
12
13
+ "github.com/hashicorp/hcl/v2"
14
+ "github.com/hashicorp/hcl/v2/hclsyntax"
15
+ "github.com/zclconf/go-cty/cty"
16
+
13
17
"github.com/coder/coder/v2/coderd/rbac/policy"
14
18
"github.com/coder/coder/v2/provisionersdk"
15
19
@@ -55,14 +59,17 @@ type Builder struct {
55
59
store database.Store
56
60
57
61
// 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
66
73
67
74
verifyNoLegacyParametersOnce bool
68
75
}
@@ -297,7 +304,11 @@ func (b *Builder) buildTx(authFunc func(action policy.Action, object rbac.Object
297
304
if err != nil {
298
305
return nil ,nil ,BuildError {http .StatusInternalServerError ,"marshal metadata" ,err }
299
306
}
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
+ }
301
312
302
313
now := dbtime .Now ()
303
314
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
364
375
// getParameters already wraps errors in BuildError
365
376
return err
366
377
}
378
+
367
379
err = store .InsertWorkspaceBuildParameters (b .ctx , database.InsertWorkspaceBuildParametersParams {
368
380
WorkspaceBuildID :workspaceBuildID ,
369
381
Name :names ,
@@ -502,6 +514,10 @@ func (b *Builder) getState() ([]byte, error) {
502
514
}
503
515
504
516
func (b * Builder )getParameters () (names ,values []string ,err error ) {
517
+ if b .parameterNames != nil {
518
+ return * b .parameterNames ,* b .parameterValues ,nil
519
+ }
520
+
505
521
templateVersionParameters ,err := b .getTemplateVersionParameters ()
506
522
if err != nil {
507
523
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) {
535
551
names = append (names ,templateVersionParameter .Name )
536
552
values = append (values ,value )
537
553
}
554
+
555
+ b .parameterNames = & names
556
+ b .parameterValues = & values
538
557
return names ,values ,nil
539
558
}
540
559
@@ -632,6 +651,108 @@ func (b *Builder) getLastBuildJob() (*database.ProvisionerJob, error) {
632
651
return b .lastBuildJob ,nil
633
652
}
634
653
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
+
635
756
// authorize performs build authorization pre-checks using the provided authFunc
636
757
func (b * Builder )authorize (authFunc func (action policy.Action ,object rbac.Objecter )bool )error {
637
758
// Doing this up front saves a lot of work if the user doesn't have permission.