diff --git a/internal/stacks/stackconfig/component.go b/internal/stacks/stackconfig/component.go new file mode 100644 index 0000000000..9d71ff49df --- /dev/null +++ b/internal/stacks/stackconfig/component.go @@ -0,0 +1,51 @@ +package stackconfig + +import ( + "github.com/apparentlymart/go-versions/versions/constraints" + "github.com/hashicorp/go-slug/sourceaddrs" + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/terraform/internal/addrs" +) + +// Component represents the declaration of a single component within a +// particular [Stack]. +// +// Components are the most important object in a stack configuration, just as +// resources are the most important object in a Terraform module: each one +// refers to a Terraform module that describes the infrastructure that the +// component is "made of". +type Component struct { + Name string + + SourceAddr sourceaddrs.Source + AllowedVersions constraints.IntersectionSpec + + ForEach hcl.Expression + + // Inputs is an expression that should produce a value that can convert + // to an object type derived from the component's input variable + // declarations, and whose attribute values will then be used to populate + // those input variables. + Inputs hcl.Expression + + // ProviderConfigs describes the mapping between the static provider + // configuration slots declared in the component's root module and the + // dynamic provider configuration objects in scope in the calling + // stack configuration. + // + // This map deals with the slight schism between the stacks language's + // treatment of provider configurations as regular values of a special + // data type vs. the main Terraform language's treatment of provider + // configurations as something special passed out of band from the + // input variables. The overall structure and the map keys are fixed + // statically during decoding, but the final provider configuration objects + // are determined only at runtime by normal expression evaluation. + // + // The keys of this map refer to provider configuration slots inside + // the module being called, but use the local names defined in the + // calling stack configuration. The stacks language runtime will + // translate the caller's local names into the callee's declared provider + // configurations by using the stack configuration's table of local + // provider names. + ProviderConfigs map[addrs.LocalProviderConfig]hcl.Expression +} diff --git a/internal/stacks/stackconfig/config.go b/internal/stacks/stackconfig/config.go new file mode 100644 index 0000000000..099835b624 --- /dev/null +++ b/internal/stacks/stackconfig/config.go @@ -0,0 +1,18 @@ +package stackconfig + +// Config represents a node in a tree of stacks that are to be planned and +// applied together. +// +// A fully-resolved stack configuration has a root node of this type, which +// can have zero or more child nodes that are also of this type, and so on +// to arbitrary levels of nesting. +type Config struct { + // Stack is the definition of this node in the stack tree. + Stack *Stack + + // Children describes all of the embedded stacks nested directly beneath + // this node in the stack tree. The keys match the labels on the "stack" + // blocks in the configuration that [Config.Stack] was built from, and + // so also match the keys in the EmbeddedStacks field of that Stack. + Children map[string]*Stack +} diff --git a/internal/stacks/stackconfig/doc.go b/internal/stacks/stackconfig/doc.go new file mode 100644 index 0000000000..4c07ef6c8f --- /dev/null +++ b/internal/stacks/stackconfig/doc.go @@ -0,0 +1,13 @@ +// Package stackconfig deals with decoding and some static validation of the +// Terraform Stack language, which uses files with the suffixes .tfstack.hcl +// and .tfstack.json to describe a set of components to be planned and applied +// together. +// +// The Stack language has some elements that are intentionally similar to the +// main Terraform language (used to describe individual modules), but is +// currently implemented separately so they can evolve independently while +// the stacks language is still relatively new. Over time it might make sense +// to refactor so that there's only one implementation of each of the common +// elements, but we'll wait to see how similar things are once this language +// has been in real use for some time. +package stackconfig diff --git a/internal/stacks/stackconfig/embedded_stack.go b/internal/stacks/stackconfig/embedded_stack.go new file mode 100644 index 0000000000..defa12c975 --- /dev/null +++ b/internal/stacks/stackconfig/embedded_stack.go @@ -0,0 +1,34 @@ +package stackconfig + +import ( + "github.com/apparentlymart/go-versions/versions/constraints" + "github.com/hashicorp/go-slug/sourceaddrs" + "github.com/hashicorp/hcl/v2" +) + +// EmbeddedStack describes a call to another stack configuration whose +// declarations should be included as part of the overall stack configuration +// tree. +// +// An embedded stack exists only as a child of another stack and doesn't have +// its own independent identity outside of that calling stack. +// +// Terraform Cloud offers a related concept of "linked stacks" where the +// deployment configuration for one stack can refer to the outputs of another, +// while the other stack retains its own independent identity and lifecycle, +// but that concept only makes sense in an environment like Terraform Cloud +// where the stack outputs can be published for external consumption. +type EmbeddedStack struct { + Name string + + SourceAddr sourceaddrs.Source + AllowedVersions constraints.IntersectionSpec + + ForEach hcl.Expression + + // Inputs is an expression that should produce a value that can convert + // to an object type derived from the child stack's input variable + // declarations, and whose attribute values will then be used to populate + // those input variables. + Inputs hcl.Expression +} diff --git a/internal/stacks/stackconfig/file.go b/internal/stacks/stackconfig/file.go new file mode 100644 index 0000000000..458cf261f6 --- /dev/null +++ b/internal/stacks/stackconfig/file.go @@ -0,0 +1,22 @@ +package stackconfig + +import "github.com/hashicorp/go-slug/sourceaddrs" + +// File represents the content of a single .tfstack.hcl or .tfstack.json file +// before it's been merged with its siblings in the same directory to produce +// the overall [Stack] object. +type File struct { + // SourceAddr is the source location for this particular file, meaning + // that the "sub-path" portion of the address should always be populated + // and refer to a particular file rather than to a directory. + SourceAddr sourceaddrs.Source + + // The remaining fields in here correspond to the fields of the same name in + // [Stack]. + Components map[string]*Component + EmbeddedStacks map[string]*EmbeddedStack + InputVariables map[string]*InputVariable + LocalValues map[string]*LocalValue + OutputValues map[string]*OutputValue + ProviderConfigs map[string]*ProviderConfig +} diff --git a/internal/stacks/stackconfig/named_values.go b/internal/stacks/stackconfig/named_values.go new file mode 100644 index 0000000000..c96d889b9a --- /dev/null +++ b/internal/stacks/stackconfig/named_values.go @@ -0,0 +1,26 @@ +package stackconfig + +import ( + "github.com/zclconf/go-cty/cty" +) + +// InputVariable is a declaration of an input variable within a stack +// configuration. Callers must provide the values for these variables. +type InputVariable struct { + Name string + TypeConstraint cty.Type +} + +// LocalValue is a declaration of a private local value within a particular +// stack configuration. These are visible only within the scope of a particular +// [Stack]. +type LocalValue struct { + Name string +} + +// OutputValue is a declaration of a result from a stack configuration, which +// can be read by the stack's caller. +type OutputValue struct { + Name string + TypeConstraint cty.Type +} diff --git a/internal/stacks/stackconfig/provider_config.go b/internal/stacks/stackconfig/provider_config.go new file mode 100644 index 0000000000..53304d4e61 --- /dev/null +++ b/internal/stacks/stackconfig/provider_config.go @@ -0,0 +1,20 @@ +package stackconfig + +import ( + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/terraform/internal/addrs" +) + +// ProviderConfig is a provider configuration declared within a [Stack]. +type ProviderConfig struct { + Provider addrs.Provider + Name string + + // TODO: Figure out how we're going to retain the relevant subset of + // a provider configuration in the state so that we still have what + // we need to destroy any associated objects when a provider is removed + // from the configuration. + ForEach hcl.Expression + + Config hcl.Body +} diff --git a/internal/stacks/stackconfig/stack.go b/internal/stacks/stackconfig/stack.go new file mode 100644 index 0000000000..3bb3c0d769 --- /dev/null +++ b/internal/stacks/stackconfig/stack.go @@ -0,0 +1,35 @@ +package stackconfig + +import ( + "github.com/hashicorp/go-slug/sourceaddrs" + "github.com/hashicorp/terraform/internal/addrs" +) + +// Stack represents a single stack, which can potentially call other +// "embedded stacks" in a similar manner to how Terraform modules can call +// other modules. +type Stack struct { + SourceAddr sourceaddrs.Source + + // EmbeddedStacks are calls to other stack configurations that should + // be treated as a part of the overall desired state produced from this + // stack. These are declared with "stack" blocks in the stack language. + EmbeddedStacks map[string]*EmbeddedStack + + // Components are calls to trees of Terraform modules that represent the + // real infrastructure described by a stack. + Components map[string]*Component + + // InputVariables, LocalValues, and OutputValues together represent all + // of the "named values" in the stack configuration, which are just glue + // to pass values between scopes or to factor out common expressions for + // reuse in multiple locations. + InputVariables map[string]*InputVariable + LocalValues map[string]*LocalValue + OutputValues map[string]*OutputValue + + // ProviderConfigs are the provider configurations declared in this + // particular stack configuration. Other stack configurations in the + // overall tree might have their own provider configurations. + ProviderConfigs map[addrs.Provider]map[string]*ProviderConfig +}