@@ -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.
0 commit comments