From edd27f4ec2baba9a5327ad33fa585b96145a7be1 Mon Sep 17 00:00:00 2001 From: Martin Atkins Date: Fri, 27 Sep 2019 16:46:54 -0700 Subject: [PATCH] projects/projectconfigs: Decode workspace block contents --- .../projectconfigs/projectconfigs_test.go | 160 ++++++++++++++++++ .../workspaces/.terraform-project.hcl | 15 ++ projects/projectconfigs/workspace.go | 93 +++++++++- 3 files changed, 260 insertions(+), 8 deletions(-) create mode 100644 projects/projectconfigs/testdata/workspaces/.terraform-project.hcl diff --git a/projects/projectconfigs/projectconfigs_test.go b/projects/projectconfigs/projectconfigs_test.go index 843ece9030..507eea6fea 100644 --- a/projects/projectconfigs/projectconfigs_test.go +++ b/projects/projectconfigs/projectconfigs_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" "github.com/hashicorp/hcl2/hcl" "github.com/hashicorp/hcl2/hcl/hclsyntax" "github.com/hashicorp/terraform/tfdiags" @@ -140,6 +141,165 @@ func TestLoad(t *testing.T) { t.Errorf("unexpected result\n%s", diff) } }) + t.Run("workspaces", func(t *testing.T) { + cfg, diags := Load("testdata/workspaces") + if diags.HasErrors() { + t.Fatalf("Unexpected problems: %s", diags.Err().Error()) + } + + got := cfg.Workspaces + want := map[string]*Workspace{ + "local": { + Name: "local", + ForEach: &hclsyntax.ObjectConsExpr{ + SrcRange: hcl.Range{ + Filename: "testdata/workspaces/.terraform-project.hcl", + Start: hcl.Pos{Line: 2, Column: 14, Byte: 33}, + End: hcl.Pos{Line: 2, Column: 16, Byte: 35}, + }, + OpenRange: hcl.Range{ + Filename: "testdata/workspaces/.terraform-project.hcl", + Start: hcl.Pos{Line: 2, Column: 14, Byte: 33}, + End: hcl.Pos{Line: 2, Column: 15, Byte: 34}, + }, + }, + Variables: &hclsyntax.ObjectConsExpr{ + SrcRange: hcl.Range{ + Filename: "testdata/workspaces/.terraform-project.hcl", + Start: hcl.Pos{Line: 5, Column: 15, Byte: 73}, + End: hcl.Pos{Line: 5, Column: 17, Byte: 75}, + }, + OpenRange: hcl.Range{ + Filename: "testdata/workspaces/.terraform-project.hcl", + Start: hcl.Pos{Line: 5, Column: 15, Byte: 73}, + End: hcl.Pos{Line: 5, Column: 16, Byte: 74}, + }, + }, + ConfigSource: &hclsyntax.TemplateExpr{ + Parts: []hclsyntax.Expression{ + &hclsyntax.LiteralValueExpr{ + Val: cty.StringVal("./foo"), + SrcRange: hcl.Range{ + Filename: "testdata/workspaces/.terraform-project.hcl", + Start: hcl.Pos{Line: 4, Column: 16, Byte: 52}, + End: hcl.Pos{Line: 4, Column: 21, Byte: 57}, + }, + }, + }, + SrcRange: hcl.Range{ + Filename: "testdata/workspaces/.terraform-project.hcl", + Start: hcl.Pos{Line: 4, Column: 15, Byte: 51}, + End: hcl.Pos{Line: 4, Column: 22, Byte: 58}, + }, + }, + StateStorage: &StateStorage{ + TypeName: "local", + Config: &hclsyntax.Body{ + Attributes: hclsyntax.Attributes{}, + Blocks: hclsyntax.Blocks{}, + SrcRange: hcl.Range{ + Filename: "testdata/workspaces/.terraform-project.hcl", + Start: hcl.Pos{Line: 7, Column: 25, Byte: 101}, + End: hcl.Pos{Line: 8, Column: 4, Byte: 106}, + }, + EndRange: hcl.Range{ + Filename: "testdata/workspaces/.terraform-project.hcl", + Start: hcl.Pos{Line: 8, Column: 4, Byte: 106}, + End: hcl.Pos{Line: 8, Column: 4, Byte: 106}, + }, + }, + DeclRange: tfdiags.SourceRange{ + Filename: "testdata/workspaces/.terraform-project.hcl", + Start: tfdiags.SourcePos{Line: 7, Column: 3, Byte: 79}, + End: tfdiags.SourcePos{Line: 7, Column: 24, Byte: 100}, + }, + TypeNameRange: tfdiags.SourceRange{ + Filename: "testdata/workspaces/.terraform-project.hcl", + Start: tfdiags.SourcePos{Line: 7, Column: 17, Byte: 93}, + End: tfdiags.SourcePos{Line: 7, Column: 24, Byte: 100}, + }, + }, + DeclRange: tfdiags.SourceRange{ + Filename: "testdata/workspaces/.terraform-project.hcl", + Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, + End: tfdiags.SourcePos{Line: 1, Column: 18, Byte: 17}, + }, + NameRange: tfdiags.SourceRange{ + Filename: "testdata/workspaces/.terraform-project.hcl", + Start: tfdiags.SourcePos{Line: 1, Column: 11, Byte: 10}, + End: tfdiags.SourcePos{Line: 1, Column: 18, Byte: 17}, + }, + }, + "remote": { + Name: "remote", + Variables: &hclsyntax.ObjectConsExpr{ + SrcRange: hcl.Range{ + Filename: "testdata/workspaces/.terraform-project.hcl", + Start: hcl.Pos{Line: 14, Column: 15, Byte: 206}, + End: hcl.Pos{Line: 14, Column: 17, Byte: 208}, + }, + OpenRange: hcl.Range{ + Filename: "testdata/workspaces/.terraform-project.hcl", + Start: hcl.Pos{Line: 14, Column: 15, Byte: 206}, + End: hcl.Pos{Line: 14, Column: 16, Byte: 207}, + }, + }, + ConfigSource: &hclsyntax.TemplateExpr{ + Parts: []hclsyntax.Expression{ + &hclsyntax.LiteralValueExpr{ + Val: cty.StringVal("./foo"), + SrcRange: hcl.Range{ + Filename: "testdata/workspaces/.terraform-project.hcl", + Start: hcl.Pos{Line: 13, Column: 16, Byte: 185}, + End: hcl.Pos{Line: 13, Column: 21, Byte: 190}, + }, + }, + }, + SrcRange: hcl.Range{ + Filename: "testdata/workspaces/.terraform-project.hcl", + Start: hcl.Pos{Line: 13, Column: 15, Byte: 184}, + End: hcl.Pos{Line: 13, Column: 22, Byte: 191}, + }, + }, + Remote: &hclsyntax.TemplateExpr{ + Parts: []hclsyntax.Expression{ + &hclsyntax.LiteralValueExpr{ + Val: cty.StringVal("tf.example.com/foo/bar"), + SrcRange: hcl.Range{ + Filename: "testdata/workspaces/.terraform-project.hcl", + Start: hcl.Pos{Line: 12, Column: 16, Byte: 146}, + End: hcl.Pos{Line: 12, Column: 38, Byte: 168}, + }, + }, + }, + SrcRange: hcl.Range{ + Filename: "testdata/workspaces/.terraform-project.hcl", + Start: hcl.Pos{Line: 12, Column: 15, Byte: 145}, + End: hcl.Pos{Line: 12, Column: 39, Byte: 169}, + }, + }, + DeclRange: tfdiags.SourceRange{ + Filename: "testdata/workspaces/.terraform-project.hcl", + Start: tfdiags.SourcePos{Line: 11, Column: 1, Byte: 110}, + End: tfdiags.SourcePos{Line: 11, Column: 19, Byte: 128}, + }, + NameRange: tfdiags.SourceRange{ + Filename: "testdata/workspaces/.terraform-project.hcl", + Start: tfdiags.SourcePos{Line: 11, Column: 11, Byte: 120}, + End: tfdiags.SourcePos{Line: 11, Column: 19, Byte: 128}, + }, + }, + } + diff := cmp.Diff( + want, got, + cmp.Comparer(cty.Type.Equals), + cmp.Comparer(cty.Value.RawEquals), + cmpopts.IgnoreUnexported(hclsyntax.Body{}), + ) + if diff != "" { + t.Errorf("unexpected result\n%s", diff) + } + }) } diff --git a/projects/projectconfigs/testdata/workspaces/.terraform-project.hcl b/projects/projectconfigs/testdata/workspaces/.terraform-project.hcl new file mode 100644 index 0000000000..2feb963a96 --- /dev/null +++ b/projects/projectconfigs/testdata/workspaces/.terraform-project.hcl @@ -0,0 +1,15 @@ +workspace "local" { + for_each = {} + + config = "./foo" + variables = {} + + state_storage "local" { + } +} + +workspace "remote" { + remote = "tf.example.com/foo/bar" + config = "./foo" + variables = {} +} diff --git a/projects/projectconfigs/workspace.go b/projects/projectconfigs/workspace.go index 9cc4232f60..b697eedece 100644 --- a/projects/projectconfigs/workspace.go +++ b/projects/projectconfigs/workspace.go @@ -27,14 +27,21 @@ type Workspace struct { // if that argument wasn't set. Variables hcl.Expression - // ConfigSource and StateStorage are set for local-operations-only - // workspaces and reflect the "config" argument and the "state_storage" - // block respectively. Both are nil for remote workspaces. - Config hcl.Expression + // ConfigSource is the expression representing the location of the root + // module of the configuration for this workspace, relative to the + // project root. + ConfigSource hcl.Expression + + // StateStorage represents the contents of a state_storage block, or nil + // for a remote workspace. + // + // StateStorage and Remote are mutually exclusive StateStorage *StateStorage // Remote is the expression given in the "remote" argument for a remote // workspace, or nil for local workspaces. + // + // Remote and StateStorage are mutually exclusive. Remote hcl.Expression // DeclRange is the source range of the block header of this block, @@ -60,6 +67,47 @@ func decodeWorkspaceBlock(block *hcl.Block) (*Workspace, tfdiags.Diagnostics) { }) } + content, hclDiags := block.Body.Content(workspaceSchema) + diags = diags.Append(hclDiags) + + if attr, ok := content.Attributes["for_each"]; ok { + ws.ForEach = attr.Expr + } + + if attr, ok := content.Attributes["variables"]; ok { + ws.Variables = attr.Expr + } + + if attr, ok := content.Attributes["config"]; ok { + ws.ConfigSource = attr.Expr + } + + if attr, ok := content.Attributes["remote"]; ok { + ws.Remote = attr.Expr + } + + for _, block := range content.Blocks { + switch block.Type { + case "state_storage": + if ws.StateStorage != nil { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Duplicate state_storage block", + Detail: fmt.Sprintf("A workspace configuration block may contain at most one state_storage block. State storage was already configured at %s.", ws.StateStorage.DeclRange.StartString()), + Subject: block.TypeRange.Ptr(), + }) + continue + } + + ss, moreDiags := decodeStateStorageBlock(block) + diags = diags.Append(moreDiags) + ws.StateStorage = ss + default: + // There are no other block types in our schema + panic(fmt.Sprintf("unexpected nested block type %q", block.Type)) + } + } + return ws, diags } @@ -78,11 +126,40 @@ type StateStorage struct { Config hcl.Body // DeclRange is the source range of the block header of this block, - // for use in diagnostic messages. NameRange is the range of the - // Name string specifically. - DeclRange, NameRange tfdiags.SourceRange + // for use in diagnostic messages. TypeNameRange is the range of the + // TypeName string specifically. + DeclRange, TypeNameRange tfdiags.SourceRange } func decodeStateStorageBlock(block *hcl.Block) (*StateStorage, tfdiags.Diagnostics) { - return nil, nil + var diags tfdiags.Diagnostics + ss := &StateStorage{ + TypeName: block.Labels[0], + Config: block.Body, + DeclRange: tfdiags.SourceRangeFromHCL(block.DefRange), + TypeNameRange: tfdiags.SourceRangeFromHCL(block.LabelRanges[0]), + } + + if !hclsyntax.ValidIdentifier(ss.TypeName) { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid state storage type", + Detail: fmt.Sprintf("The name %q is not a valid state storage type. Must start with a letter, followed by zero or more letters, digits, and underscores.", ss.TypeName), + Subject: block.LabelRanges[0].Ptr(), + }) + } + + return ss, diags +} + +var workspaceSchema = &hcl.BodySchema{ + Attributes: []hcl.AttributeSchema{ + {Name: "for_each"}, + {Name: "variables"}, + {Name: "config"}, + {Name: "remote"}, + }, + Blocks: []hcl.BlockHeaderSchema{ + {Type: "state_storage", LabelNames: []string{"type"}}, + }, }