projects: Load individual workspaces, evaluating their configuration

f-workspaces2-prototype
Martin Atkins 7 years ago
parent 36919a4867
commit 294b9ae045

@ -12,7 +12,13 @@ import (
// belongs to, or an error if the given directory does not seem to belong to
// a project.
func (m *Meta) findProjectForDir(dir string) (*projects.Project, tfdiags.Diagnostics) {
return projects.FindProject(dir)
project, diags := projects.FindProject(dir)
if project != nil {
// Make project configuration source code available for diagnostic
// messages, in case diags contains any configuration errors/warnings.
m.configLoader.ImportSources(project.ConfigSources())
}
return project, diags
}
// findCurrentProject finds the project that the current working directory
@ -35,3 +41,17 @@ func (m *Meta) findCurrentProject() (*projects.Project, tfdiags.Diagnostics) {
}
return projects.FindProject(dir)
}
// findCurrentProjectManager wraps findCurrentProject and annotates the
// resulting project with the current set of project context variables to
// produce a ProjectManager object.
func (m *Meta) findCurrentProjectManager() (*projects.ProjectManager, tfdiags.Diagnostics) {
project, diags := m.findCurrentProject()
if project == nil {
return nil, diags
}
// TODO: Once we have updated "terraform init" to be able to accept
// context values and stash them somewhere, we'll load them here and
// pass them in to NewManager. For now we just assume no context values.
return project.NewManager(nil), diags
}

@ -1,8 +1,10 @@
package command
import (
"fmt"
"strings"
"github.com/hashicorp/terraform/tfdiags"
"github.com/posener/complete"
)
@ -11,26 +13,50 @@ type WorkspaceShowCommand struct {
}
func (c *WorkspaceShowCommand) Run(args []string) int {
c.Ui.Error("not yet updated for workspaces2 prototype")
return 1
/*
args, err := c.Meta.process(args, true)
if err != nil {
return 1
}
cmdFlags := c.Meta.extendedFlagSet("workspace show")
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
if err := cmdFlags.Parse(args); err != nil {
c.Ui.Error(fmt.Sprintf("Error parsing command-line flags: %s\n", err.Error()))
return 1
}
workspace := c.Workspace()
c.Ui.Output(workspace)
return 0
*/
args, err := c.Meta.process(args, true)
if err != nil {
return 1
}
cmdFlags := c.Meta.defaultFlagSet("workspace show")
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
if err := cmdFlags.Parse(args); err != nil {
c.Ui.Error(fmt.Sprintf("Error parsing command-line flags: %s\n", err.Error()))
return 1
}
var diags tfdiags.Diagnostics
projectMgr, moreDiags := c.findCurrentProjectManager()
diags = diags.Append(moreDiags)
if diags.HasErrors() {
c.showDiagnostics(diags)
return 1
}
workspaceAddr := c.Workspace()
workspace, moreDiags := projectMgr.LoadWorkspace(workspaceAddr)
diags = diags.Append(moreDiags)
if diags.HasErrors() {
c.showDiagnostics(diags)
return 1
}
depAddrs := projectMgr.Project().WorkspaceDependencies(workspaceAddr)
fmt.Printf("Workspace %s\n\n", workspaceAddr.StringCompact())
fmt.Printf("Configuration Root: %s\n", workspace.ConfigDir())
fmt.Printf("Input Variables:\n")
for addr, val := range workspace.InputVariables() {
fmt.Printf(" %s = %#v\n", addr.Name, val)
}
fmt.Printf("Dependencies:\n")
for _, addr := range depAddrs {
fmt.Printf(" %s\n", addr)
}
fmt.Println("")
return 0
}
func (c *WorkspaceShowCommand) AutocompleteArgs() complete.Predictor {

@ -0,0 +1,53 @@
package projects
import (
"github.com/hashicorp/hcl/v2"
"github.com/zclconf/go-cty/cty"
"github.com/hashicorp/terraform/addrs"
"github.com/hashicorp/terraform/projects/projectconfigs"
"github.com/hashicorp/terraform/projects/projectlang"
)
type dynamicEvalData struct {
config *projectconfigs.Config
workspaceOutputs map[addrs.ProjectWorkspace]map[addrs.OutputValue]cty.Value
contextValues map[addrs.ProjectContextValue]cty.Value
}
var _ projectlang.DynamicEvaluateData = (*dynamicEvalData)(nil)
func (d *dynamicEvalData) BaseDir() string {
return d.config.ProjectRoot
}
func (d *dynamicEvalData) LocalValueExpr(addr addrs.LocalValue) hcl.Expression {
return d.config.Locals[addr.Name].Value
}
func (d *dynamicEvalData) ContextValue(addr addrs.ProjectContextValue) cty.Value {
return d.contextValues[addr]
}
func (d *dynamicEvalData) WorkspaceConfigValue(addr addrs.ProjectWorkspaceConfig) cty.Value {
noKeyAddr := addr.Instance(addrs.NoKey)
if noKeyOutputs, exists := d.workspaceOutputs[noKeyAddr]; exists {
attrs := make(map[string]cty.Value, len(noKeyOutputs))
for outputAddr, v := range noKeyOutputs {
attrs[outputAddr.Name] = v
}
return cty.ObjectVal(attrs)
}
objs := make(map[string]cty.Value)
for instAddr, outputs := range d.workspaceOutputs {
if instAddr.Config() != addr {
continue
}
attrs := make(map[string]cty.Value, len(outputs))
for outputAddr, v := range outputs {
attrs[outputAddr.Name] = v
}
objs[string(instAddr.Key.(addrs.StringKey))] = cty.ObjectVal(attrs)
}
return cty.ObjectVal(objs)
}

@ -0,0 +1,42 @@
package projects
import (
"github.com/hashicorp/terraform/addrs"
"github.com/zclconf/go-cty/cty"
)
// ProjectManager is a wrapper around Project that associates a project with
// the context it is being run in.
//
// A Project object represents the project itself, while a ProjectManager
// represents that project being used in a particular context: a specific set
// of context values, a means to access output values from upstream projects,
// and any other similar context-specific annotations that are required to
// run operations against a particular project.
type ProjectManager struct {
project *Project
contextValues map[addrs.ProjectContextValue]cty.Value
}
// NewManager creates a ProjectManager object that binds the recieving project
// to a particular set of context values and other contextual context that
// will allow running operations against workspaces in the project.
func (p *Project) NewManager(contextValues map[addrs.ProjectContextValue]cty.Value) *ProjectManager {
return &ProjectManager{
project: p,
contextValues: contextValues,
}
}
// Project returns the project that the receiving ProjectManager is managing.
func (m *ProjectManager) Project() *Project {
return m.project
}
// ContextValues returns the configured context values for this project manager.
//
// The caller must treat the returned map as immutable, even though the Go
// type system cannot enforce that.
func (m *ProjectManager) ContextValues() map[addrs.ProjectContextValue]cty.Value {
return m.contextValues
}

@ -0,0 +1,276 @@
package projectlang
import (
"fmt"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/terraform/addrs"
"github.com/hashicorp/terraform/lang"
"github.com/hashicorp/terraform/tfdiags"
"github.com/zclconf/go-cty/cty"
)
// DynamicEvaluateData is an interface used during dynamic evaluation operations
// to interrogate information from the configuration and other value sources.
//
// DynamicEvaluateData is used only in situations where it's assume that
// references in the expressions were pre-validated to ensure that they
// refer to items in the configuration, so DynamicEvaluateData implementations
// can assume that all requested objects should exist in configuration, and
// panic if that does not hold in practice.
type DynamicEvaluateData interface {
// BaseDir returns the directory that should be considered as the base
// directory for any relative filesystem paths that appear in expressions.
BaseDir() string
// LocalValueExpr returns the expression associated with the given named
// local value.
//
// While the caller is responsible for determining final values for most
// other referenceable objects exposed from DynamicEvaluateData, local
// values are treated as part of the language itself and their expressions
// are evaluated by the language runtime in the projectlang package to
// ensure that each one is only evaluated once while processing a single
// expression evaluation call.
LocalValueExpr(addrs.LocalValue) hcl.Expression
// ContextValue returns the value associated with the given context key
// in the current project execution context.
ContextValue(addrs.ProjectContextValue) cty.Value
// WorkspaceConfigValue returns a value representing a particular workspace
// configuration when accessed in expressions.
//
// For a workspace configuration block that does not have for_each set,
// the return value is an object whose attributes are the output values
// of the workspace in question.
//
// For a workspace configuration block that does have for_each set, there
// result has an extra nesting level where the top-level object attributes
// are the workspace instance keys and each instance's output values
// appear in nested objects.
WorkspaceConfigValue(addrs.ProjectWorkspaceConfig) cty.Value
}
// DynamicEvaluateEach represents the current "each" repetition when evaluating
// expressions.
//
// If evaluating in a context where no "for_each" is active, use
// projectlang.NoEach as a placeholder value.
type DynamicEvaluateEach struct {
Key addrs.InstanceKey
Value cty.Value
}
// ValueObj returns the "each" object value that should represent the reciever
// in HCL expression evaluation.
func (e DynamicEvaluateEach) ValueObj() cty.Value {
switch k := e.Key.(type) {
case nil:
return cty.NullVal(cty.Object(map[string]cty.Type{
"key": cty.DynamicPseudoType,
"value": cty.DynamicPseudoType,
}))
case addrs.StringKey:
return cty.ObjectVal(map[string]cty.Value{
"key": cty.StringVal(string(k)),
"value": e.Value,
})
case addrs.IntKey:
return cty.ObjectVal(map[string]cty.Value{
"key": cty.NumberIntVal(int64(k)),
"value": e.Value,
})
default:
panic(fmt.Sprintf("unsupported key type %T", e.Key))
}
}
// NoEach is the zero value of DynamicEvaluateEach and is used to represent
// situations where no "each" object is available.
var NoEach = DynamicEvaluateEach{
Key: addrs.NoKey,
Value: cty.NilVal,
}
// DynamicEvaluateExprs is the full expression evaluation pass that supports
// references to any of the object types in the project configuration language.
//
// The given DynamicEvaluateData and DynamicEvaluateEach values together
// provide the data that will be available for use in reference expressions.
// For expressions where the "each" object should not be available, set
// that argument to projectlang.NoEach.
//
// Grouping multiple evaluations together both allows us to avoid re-evaluating
// common local values multiple times and, more importantly, ensures that we'll
// only report errors for each expression once rather than repeating them once
// per expression. Callers should therefore prefer to gather together all of
// their dynamic evaluation expressions into a single call and avoid combining
// diagnostics from separate calls to DynamicEvaluateExprs in the same output.
func DynamicEvaluateExprs(exprs []hcl.Expression, data DynamicEvaluateData, each DynamicEvaluateEach) ([]cty.Value, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
dependents := make(map[addrs.LocalValue][]addrs.LocalValue)
inDegree := make(map[addrs.LocalValue]int)
var queue []addrs.LocalValue
// We'll seed our queue with the references in the given expressions themselves.
for _, expr := range exprs {
for _, traversal := range expr.Variables() {
ref, moreDiags := addrs.ParseProjectConfigRef(traversal)
diags = diags.Append(moreDiags)
if moreDiags.HasErrors() {
continue
}
addr, ok := ref.Subject.(addrs.LocalValue)
if ok {
queue = append(queue, addr)
dependents[addr] = nil
inDegree[addr] = 0
}
}
}
// Now we'll work our way through the graph of locals to find all of
// the ones we directly and indirectly depend on.
localValues := make(map[string]cty.Value)
for i := 0; i < len(queue); i++ { // queue length will grow during iteration
addr := queue[i]
if _, exists := localValues[addr.Name]; exists {
// We already dealt with this one via another path through the graph.
continue
}
// We'll make a placeholder element for now, just so we know we've visited
// this one, and then overwrite it with a real value later.
localValues[addr.Name] = cty.NilVal
expr := data.LocalValueExpr(addr)
if expr == nil {
// Should never happen because references should be validated by our caller.
panic(fmt.Sprintf("no expression available for %s", addr))
}
for _, traversal := range expr.Variables() {
ref, moreDiags := addrs.ParseProjectConfigRef(traversal)
diags = diags.Append(moreDiags)
if moreDiags.HasErrors() {
continue
}
refAddr, ok := ref.Subject.(addrs.LocalValue)
if !ok {
// If it refers to anything other than local values then
// it's a dynamic local value and so we can't use it
// in static evaluation.
localValues[addr.Name] = cty.DynamicVal
continue
}
inDegree[refAddr]++
dependents[addr] = append(dependents[addr], refAddr)
queue = append(queue, refAddr)
}
}
// We now need to re-visit all of the addresses in topological order,
// evaluating the locals as we go. We'll re-use the backing buffer of
// our queue above, since we know it has sufficient capacity for all
// of the local values involved.
queue = queue[:0]
for addr := range dependents { // Seed queue with locals that have no dependencies
if inDegree[addr] == 0 {
queue = append(queue, addr)
}
}
for len(queue) > 0 {
var addr addrs.LocalValue
addr, queue = queue[0], queue[1:] // dequeue next item
if val := localValues[addr.Name]; val != cty.NilVal {
continue // Already dealt with this one
}
delete(inDegree, addr)
expr := data.LocalValueExpr(addr)
if expr == nil {
// Should never happen because references should be validated by our caller.
panic(fmt.Sprintf("no expression available for %s", addr))
}
val, moreDiags := expr.Value(staticEvalContext(data.BaseDir(), localValues))
diags = diags.Append(moreDiags)
localValues[addr.Name] = val
for _, referrerAddr := range dependents[addr] {
inDegree[referrerAddr]--
if inDegree[referrerAddr] < 1 {
queue = append(queue, referrerAddr)
}
}
}
if len(inDegree) > 0 {
// TODO: This error needs to be _much_ better
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Dependency cycle in project configuration",
Detail: "There is at least one dependency cycle between the local values in the project configuration.",
})
return nil, diags
}
// Finally, with all of the local values evaluated, we can evaluate the
// expressions we were given.
ret := make([]cty.Value, len(exprs))
for i, expr := range exprs {
refs := findReferencesInExpr(expr)
ctx, moreDiags := dynamicEvalContext(refs, data, each, localValues)
diags = diags.Append(moreDiags)
if moreDiags.HasErrors() {
ret[i] = cty.DynamicVal
continue
}
val, hclDiags := expr.Value(ctx)
diags = diags.Append(hclDiags)
ret[i] = val
}
return ret, diags
}
func dynamicEvalContext(refs []*addrs.ProjectConfigReference, data DynamicEvaluateData, each DynamicEvaluateEach, localValues map[string]cty.Value) (*hcl.EvalContext, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
currentWorkspaces := map[string]cty.Value{}
upstreamWorkspaces := map[string]cty.Value{}
contextValues := map[string]cty.Value{}
eachVal := each.ValueObj()
for _, ref := range refs {
switch addr := ref.Subject.(type) {
case addrs.ProjectWorkspaceConfig:
obj := data.WorkspaceConfigValue(addr)
switch addr.Rel {
case addrs.ProjectWorkspaceCurrent:
currentWorkspaces[addr.Name] = obj
case addrs.ProjectWorkspaceUpstream:
upstreamWorkspaces[addr.Name] = obj
}
case addrs.ProjectContextValue:
contextValues[addr.Name] = data.ContextValue(addr)
case addrs.LocalValue:
// Nothing to do for these because they should already be in
// localValues.
case addrs.ForEachAttr:
// Nothing to do for these because we've already populated
// eachVal above.
default:
panic(fmt.Sprintf("unsupported reference type %T", addr))
}
}
return &hcl.EvalContext{
Variables: map[string]cty.Value{
"local": cty.ObjectVal(localValues),
"workspace": cty.ObjectVal(currentWorkspaces),
"upstream": cty.ObjectVal(upstreamWorkspaces),
"context": cty.ObjectVal(contextValues),
"each": eachVal,
},
Functions: lang.Functions(false, data.BaseDir()),
}, diags
}

@ -185,7 +185,7 @@ func StaticEvaluateExprs(exprs []hcl.Expression, data StaticEvaluateData) ([]cty
func staticEvalContext(baseDir string, localValues map[string]cty.Value) *hcl.EvalContext {
return &hcl.EvalContext{
Variables: map[string]cty.Value{
"local": cty.MapVal(localValues),
"local": cty.ObjectVal(localValues),
// All of the other top-level objects are just placeholders here
// so we can still do partial type checking of derived expressions.
@ -193,6 +193,6 @@ func staticEvalContext(baseDir string, localValues map[string]cty.Value) *hcl.Ev
"upstream": cty.DynamicVal,
"context": cty.DynamicVal,
},
Functions: lang.Functions(true, "."),
Functions: lang.Functions(true, baseDir),
}
}

@ -2,6 +2,8 @@ package projects
import (
"fmt"
"os"
"path/filepath"
"sort"
"github.com/hashicorp/hcl/v2"
@ -153,6 +155,33 @@ Vals:
}, diags
}
// Config returns te configuration that was used to define this project.
//
// Callers must treat this value as immutable, even though the Go type system
// cannot enforce that.
func (p *Project) Config() *projectconfigs.Config {
return p.config
}
// ConfigSources returns a map from filenames to source buffers for the
// configuration files that were used in the definition of this project.
//
// This is exposed so that callers can register the source buffers in a
// source registry for use to create snippets in future diagnostics messages.
func (p *Project) ConfigSources() map[string][]byte {
fn := p.config.ConfigFile
// Config sources are conventionally given relative to the current working
// directory, so we'll make a best-effort attempt to transform it as such.
if cwd, err := os.Getwd(); err == nil {
if relFn, err := filepath.Rel(cwd, fn); err == nil {
fn = relFn
}
}
return map[string][]byte{
fn: p.config.Source,
}
}
// AllWorkspaceAddrs returns the addresses of all of the workspaces defined
// in the project.
//

@ -1,11 +1,345 @@
package projects
import (
"fmt"
"log"
"sort"
"github.com/hashicorp/hcl/v2"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/gocty"
"github.com/hashicorp/terraform/addrs"
"github.com/hashicorp/terraform/projects/projectconfigs"
"github.com/hashicorp/terraform/projects/projectlang"
"github.com/hashicorp/terraform/states/statemgr"
"github.com/hashicorp/terraform/tfdiags"
)
// Workspace represents a single selectable workspace, each of which has its
// own configuration directory, state, and input variables.
type Workspace struct {
project *Project
addr addrs.ProjectWorkspace
configDir string
variables map[addrs.InputVariable]cty.Value
// TODO: Also the remote config or state storage config, but for now
// we're just forcing local state as a prototype.
}
// LoadWorkspace instantiates a specific workspace from the receiving project.
//
// In order to do so, it resolves any references in the workspace configuration
// that the workspace belongs to, which might involve retrieving the output
// values from other workspaces, evaluating local values, etc.
//
// This can be a relatively expensive operation in a project that has lots of
// remote workspaces, so it should be used carefully. A caller that needs to
// work with many workspaces at once might be better off using
// LoadAllWorkspaces, which is able to optimize its work to ensure that each
// workspace is only accessed once, and also avoids producing the same errors
// multiple times if a given expression in the configuration contributes to
// multiple workspaces.
func (m *ProjectManager) LoadWorkspace(addr addrs.ProjectWorkspace) (*Workspace, tfdiags.Diagnostics) {
wss, diags := m.loadWorkspaces([]addrs.ProjectWorkspace{addr})
return wss[addr], diags
}
// LoadAllWorkspaces is like LoadWorkspace but loads all of the project's
// workspaces at once, as efficiently as possible.
//
// This is a better alternative to LoadWorkspace for callers that need to work
// with all or most of the workspaces in a project at once, because it's able
// to optimize its work and avoid duplicate calls. However, it's not suitable
// for callers that intend to work only with a single workspace because it is
// likely to fetch more data than necessary and will fail if the current user
// does not have access to any of the workspace outputs, even if those
// outputs would not normally be needed to process a particular selected
// workspace.
func (m *ProjectManager) LoadAllWorkspaces() (map[addrs.ProjectWorkspace]*Workspace, tfdiags.Diagnostics) {
return m.loadWorkspaces(m.project.AllWorkspaceAddrs())
}
// loadWorkspaces is the common implementation of both LoadWorkspace and
// LoadAllWorkspaces, which loads all of the workspaces requested in the
// given address slice, and as a side-effect fetches outputs for any other
// workspaces that the given ones depend on, fetching each workspace at most
// once.
//
// The resulting map includes Workspace objects only for the requested
// addresses, even if others needed to be fetched in order to complete the
// operation.
func (m *ProjectManager) loadWorkspaces(wantedAddrs []addrs.ProjectWorkspace) (map[addrs.ProjectWorkspace]*Workspace, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
if len(wantedAddrs) == 0 {
return nil, nil
}
ret := make(map[addrs.ProjectWorkspace]*Workspace, len(wantedAddrs))
// First we'll follow the dependencies of what's requested to expand out
// our full list of workspaces to fetch. Along the way we'll keep track
// of the dependencies we found so we can use them for a topological sort
// below. The keys in "deps" after this loop represent our full set of
// needed workspaces.
deps := make(map[addrs.ProjectWorkspace][]addrs.ProjectWorkspace)
for _, needAddr := range wantedAddrs {
deps[needAddr] = m.project.WorkspaceDependencies(needAddr)
}
for {
// We'll keep iterating until we stop finding new workspaces.
// This must converge eventually because the number of workspaces
// is finite itself, and so the worst case is that we load all of
// the workspaces.
new := false
for _, needAddrs := range deps {
for _, needAddr := range needAddrs {
if _, exists := deps[needAddr]; exists {
continue
}
new = true
deps[needAddr] = m.project.WorkspaceDependencies(needAddr)
}
}
if !new {
break
}
}
// NOTE: Strictly speaking we only need to fetch the outputs for the
// workspaces that are referenced by the ones requested, not for the
// ones requested directly. However, for the sake of simplicity we'll
// fetch all of them here. In a real implementation we'd likely want to
// optimize this in a number of ways, including avoiding fetching things
// we don't need to fetch _and_ doing our fetches as concurrently as
// possible.
workspaces := make(map[addrs.ProjectWorkspace]*Workspace)
workspaceOutputs := make(map[addrs.ProjectWorkspace]map[addrs.OutputValue]cty.Value)
dependents := make(map[addrs.ProjectWorkspace][]addrs.ProjectWorkspace)
inDegree := make(map[addrs.ProjectWorkspace]int)
for addr, needAddrs := range deps {
for _, needAddr := range needAddrs {
inDegree[addr]++
dependents[needAddr] = append(dependents[needAddr], addr)
}
}
var queue []addrs.ProjectWorkspace
for addr := range deps {
if inDegree[addr] == 0 {
queue = append(queue, addr)
}
}
for len(queue) > 0 {
var addr addrs.ProjectWorkspace
addr, queue = queue[0], queue[1:] // dequeue next item
if val := workspaces[addr]; val != nil {
continue // Already dealt with this one
}
delete(inDegree, addr)
log.Printf("[TRACE] projects.ProjectManager.loadWorkspaces: evaluating configuration for workspace %s", addr)
switch addr.Rel {
case addrs.ProjectWorkspaceCurrent:
cfg := m.project.config.Workspaces[addr.Name]
if cfg == nil {
panic(fmt.Sprintf("no config for %s", addr))
}
workspace, moreDiags := m.initCurrentProjectWorkspace(addr, cfg, workspaceOutputs)
diags = diags.Append(moreDiags)
if moreDiags.HasErrors() {
// Downstream references are likely to fail too if we failed
// to init this project, so we'll just bail out early.
return nil, diags
}
workspaces[addr] = workspace
outputs, moreDiags := workspace.LatestOutputValues()
diags = diags.Append(moreDiags)
if moreDiags.HasErrors() {
return nil, diags
}
workspaceOutputs[addr] = outputs
case addrs.ProjectWorkspaceUpstream:
// TODO: Skipping this for now, since we're just prototyping.
panic("upstream workspaces not yet supported")
default:
panic("unsupported workspace relationship")
}
for _, referrerAddr := range dependents[addr] {
inDegree[referrerAddr]--
if inDegree[referrerAddr] < 1 {
queue = append(queue, referrerAddr)
}
}
}
for _, wantAddr := range wantedAddrs {
ret[wantAddr] = workspaces[wantAddr]
}
return ret, diags
}
func (m *ProjectManager) initCurrentProjectWorkspace(addr addrs.ProjectWorkspace, cfg *projectconfigs.Workspace, otherWorkspaceOutputs map[addrs.ProjectWorkspace]map[addrs.OutputValue]cty.Value) (*Workspace, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
ret := &Workspace{
addr: addr,
project: m.project,
}
// This is a little more convoluted than we'd ideally like because we want
// to evaluate all of our expressions in a single call into projectlang so
// that we don't produce redundant diagnostic messages, but we don't always
// have explicit values for all of the arguments.
const configExpr = 0
const variablesExpr = 1
exprs := []hcl.Expression{
configExpr: hcl.StaticExpr(cty.NullVal(cty.String), cfg.DeclRange.ToHCL()),
variablesExpr: hcl.StaticExpr(cty.EmptyObjectVal, cfg.DeclRange.ToHCL()),
}
if cfg.ConfigSource != nil {
exprs[configExpr] = cfg.ConfigSource
}
if cfg.Variables != nil {
exprs[variablesExpr] = cfg.Variables
}
data := &dynamicEvalData{
config: m.project.config,
workspaceOutputs: otherWorkspaceOutputs,
contextValues: m.ContextValues(),
}
each := projectlang.NoEach
if addr.Key != addrs.NoKey {
each.Key = addr.Key
each.Value = m.project.workspaceEachVals[addr]
}
vals, moreDiags := projectlang.DynamicEvaluateExprs(
exprs,
data, each,
)
diags = diags.Append(moreDiags)
if moreDiags.HasErrors() {
return nil, diags
}
if !vals[configExpr].IsNull() {
err := gocty.FromCtyValue(vals[configExpr], &ret.configDir)
if err != nil {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid workspace configuration root",
Detail: fmt.Sprintf("Invalid root module path for workspace: %s.", tfdiags.FormatError(err)),
Subject: exprs[configExpr].Range().Ptr(),
})
}
} else {
ret.configDir = "."
}
if obj := vals[variablesExpr]; !obj.IsNull() {
if !(obj.Type().IsObjectType() || obj.Type().IsMapType()) {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid workspace variables",
Detail: "Invalid value for \"variables\" argument: must be a mapping from variable name to corresponding variable value.",
Subject: exprs[variablesExpr].Range().Ptr(),
})
} else {
raw := obj.AsValueMap()
ret.variables = make(map[addrs.InputVariable]cty.Value, len(raw))
for n, v := range raw {
ret.variables[addrs.InputVariable{Name: n}] = v
}
}
}
// TODO: Also the remote state storage config
return ret, diags
}
// Addr returns the address of the workspace represented by the reciever.
func (w *Workspace) Addr() addrs.ProjectWorkspace {
return w.addr
}
// Project returns the project object that this workspace belongs to.
func (w *Workspace) Project() *Project {
return w.project
}
// ConfigDir returns the path to the directory containing the root module
// for this workspace, relative to the project's root directory.
func (w *Workspace) ConfigDir() string {
return w.configDir
}
// InputVariables returns the configured input variable values for the
// workspace.
func (w *Workspace) InputVariables() map[addrs.InputVariable]cty.Value {
return w.variables
}
// StateMgr returns a configured state manager object for this workspace,
// or returns user-oriented diagnistics messages explaining why it cannot.
//
// The configuration for the state storage is validated for syntax as part of
// instantiating the workspace, so errors from this method will generally
// describe "dynamic" problems, such as being unable to connect to a remote
// server identified in the configuration.
func (w *Workspace) StateMgr() (statemgr.Full, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"StateMgr not yet implemented",
"Workspace.StateMgr isn't implemented yet",
))
return nil, diags
}
// LatestOutputValues returns the output values recorded at the end of the
// most recent operation against this workspace.
//
// The returned diagnostics might contain errors if the storage for the output
// values is currently unreachable for some reason. In that case, the
// returned map is invalid and must not be used.
func (w *Workspace) LatestOutputValues() (map[addrs.OutputValue]cty.Value, tfdiags.Diagnostics) {
// For the moment we're still getting outputs from the state directly.
// Ideally we'd switch to using a specialized API for this when the
// state is mastered in Terraform Cloud or Enterprise, so that we can
// apply separate access controls to whole state vs. outputs.
var diags tfdiags.Diagnostics
stateMgr, moreDiags := w.StateMgr()
diags = diags.Append(moreDiags)
if moreDiags.HasErrors() {
return nil, diags
}
err := stateMgr.RefreshState()
if err != nil {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Failed to fetch workspace state",
fmt.Sprintf("Could not retrieve the latest state snapshot for workspace %s in order to determine its latest output values: %s.", w.addr.StringCompact(), err),
))
return nil, diags
}
state := stateMgr.State()
raw := state.RootModule().OutputValues
ret := make(map[addrs.OutputValue]cty.Value, len(raw))
for k, v := range raw {
ret[addrs.OutputValue{Name: k}] = v.Value
}
return ret, nil
}
type sortWorkspaceAddrs []addrs.ProjectWorkspace
var _ sort.Interface = sortWorkspaceAddrs(nil)

Loading…
Cancel
Save