projects/projectconfigs: Decode workspace block contents

f-workspaces2-prototype
Martin Atkins 6 years ago
parent 283c0127e7
commit edd27f4ec2

@ -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)
}
})
}

@ -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 = {}
}

@ -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"}},
},
}

Loading…
Cancel
Save