diff --git a/addrs/project.go b/addrs/project.go index ea03057462..d40ecab8a9 100644 --- a/addrs/project.go +++ b/addrs/project.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hclsyntax" "github.com/hashicorp/terraform/tfdiags" ) @@ -51,6 +52,103 @@ type ProjectWorkspace struct { Key InstanceKey } +// MakeProjectWorkspace is a helper to compactly construct a project workspace +// address for a workspace in the current project. +func MakeProjectWorkspace(name string, key InstanceKey) ProjectWorkspace { + return ProjectWorkspace{ + Rel: ProjectWorkspaceCurrent, + Name: name, + Key: key, + } +} + +// MakeProjectWorkspaceUpstream is a helper to compactly construct a project +// workspace address for an upstream workspace. +func MakeProjectWorkspaceUpstream(name string, key InstanceKey) ProjectWorkspace { + return ProjectWorkspace{ + Rel: ProjectWorkspaceUpstream, + Name: name, + Key: key, + } +} + +// ParseProjectWorkspaceCompact parses a project workspace address as it +// appears in workspace-specific scenarios such as on the command line and +// in environment variables. +// +// The result is always a workspace in the current project, and never an +// upstream workspace or any other relationship. +// +// This notation is different than the addresses used within the project +// configuration file, exploiting the fact that it's implied that we're +// talking about workspaces in order to achieve a more compact representation. +// +// The returned address is invalid and should not be used if the returned +// diags contains errors. +func ParseProjectWorkspaceCompact(traversal hcl.Traversal) (ProjectWorkspace, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + if len(traversal) > 2 || len(traversal) < 1 { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid workspace address", + Detail: "A workspace address must be a workspace configuration name, optionally followed by a dot and then a workspace configuration instance key.", + Subject: traversal.SourceRange().Ptr(), + }) + return ProjectWorkspace{}, diags + } + + ret := ProjectWorkspace{ + Rel: ProjectWorkspaceCurrent, + Name: traversal.RootName(), + } + + if len(traversal) == 2 { + keyStep, ok := traversal[1].(hcl.TraverseAttr) + if !ok { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid workspace address", + Detail: "If a workspace instance key is provided, it must be given as an attribute name introduced with a dot.", + Subject: keyStep.SourceRange().Ptr(), + }) + return ProjectWorkspace{}, diags + } + + ret.Key = StringKey(keyStep.Name) + } + + return ret, diags +} + +// ParseProjectWorkspaceCompactStr is a wrapper around +// ParseProjectWorkspaceCompact that first parses the given string as an HCL +// traversal. +// +// This should be used only in specialized situations since it will cause the +// created references to not have any meaningful source location information. +// If a reference string is coming from a source that should be identified in +// error messages then the caller should instead parse it directly using a +// suitable function from the HCL API and pass the traversal itself to +// ParseRef. +// +// Error diagnostics are returned if either the parsing fails or the analysis +// of the traversal fails. There is no way for the caller to distinguish the +// two kinds of diagnostics programmatically. If error diagnostics are returned +// the returned reference may be nil or incomplete. +func ParseProjectWorkspaceCompactStr(str string) (ProjectWorkspace, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + + traversal, parseDiags := hclsyntax.ParseTraversalAbs([]byte(str), "", hcl.Pos{Line: 1, Column: 1}) + diags = diags.Append(parseDiags) + if parseDiags.HasErrors() { + return ProjectWorkspace{}, diags + } + + addr, targetDiags := ParseProjectWorkspaceCompact(traversal) + diags = diags.Append(targetDiags) + return addr, diags +} + // Config returns the address of the workspace configuration this instance // belongs to. func (w ProjectWorkspace) Config() ProjectWorkspaceConfig { @@ -84,6 +182,34 @@ func (w ProjectWorkspace) String() string { } } +// StringCompact returns the compact string representation of a workspace in +// the current project. This is the same format that +// ParseProjectWorkspaceCompact consumes. +// +// This should be used only in sitautions where it is clear from context that +// the result is a workspace address. This is not the form used within the +// project configuration language. +// +// StringCompact is valid to use only for workspaces in the current project. +// This method will panic if used with an upstream workspace or any other +// workspace relationship. +func (w ProjectWorkspace) StringCompact() string { + if w.Rel != ProjectWorkspaceCurrent { + panic("StringCompact on workspace address not in the current project") + } + + switch key := w.Key.(type) { + case nil: + return w.Name + case StringKey: + return fmt.Sprintf("%s.%s", w.Name, string(key)) + default: + // No other key types are valid for project workspaces, but we'll + // tolerate this anyway for robustness. + return fmt.Sprintf("%s%s", w.Name, key.String()) + } +} + // ProjectWorkspaceRelationship defines the relationship between the current // workspace and the referenced workspace. type ProjectWorkspaceRelationship int