terraform/internal/command/jsonconfig/config.go

530 lines
16 KiB
Go

package jsonconfig
import (
"encoding/json"
"fmt"
"sort"
"github.com/zclconf/go-cty/cty"
ctyjson "github.com/zclconf/go-cty/cty/json"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/configs"
"github.com/hashicorp/terraform/internal/configs/configschema"
"github.com/hashicorp/terraform/internal/getproviders"
"github.com/hashicorp/terraform/internal/terraform"
)
// Config represents the complete configuration source
type config struct {
ProviderConfigs map[string]providerConfig `json:"provider_config,omitempty"`
RootModule module `json:"root_module,omitempty"`
}
// ProviderConfig describes all of the provider configurations throughout the
// configuration tree, flattened into a single map for convenience since
// provider configurations are the one concept in Terraform that can span across
// module boundaries.
type providerConfig struct {
Name string `json:"name,omitempty"`
FullName string `json:"full_name,omitempty"`
Alias string `json:"alias,omitempty"`
VersionConstraint string `json:"version_constraint,omitempty"`
ModuleAddress string `json:"module_address,omitempty"`
Expressions map[string]interface{} `json:"expressions,omitempty"`
parentKey string
}
type module struct {
Outputs map[string]output `json:"outputs,omitempty"`
// Resources are sorted in a user-friendly order that is undefined at this
// time, but consistent.
Resources []resource `json:"resources,omitempty"`
ModuleCalls map[string]moduleCall `json:"module_calls,omitempty"`
Variables variables `json:"variables,omitempty"`
}
type moduleCall struct {
Source string `json:"source,omitempty"`
Expressions map[string]interface{} `json:"expressions,omitempty"`
CountExpression *expression `json:"count_expression,omitempty"`
ForEachExpression *expression `json:"for_each_expression,omitempty"`
Module module `json:"module,omitempty"`
VersionConstraint string `json:"version_constraint,omitempty"`
DependsOn []string `json:"depends_on,omitempty"`
}
// variables is the JSON representation of the variables provided to the current
// plan.
type variables map[string]*variable
type variable struct {
Default json.RawMessage `json:"default,omitempty"`
Description string `json:"description,omitempty"`
Sensitive bool `json:"sensitive,omitempty"`
}
// Resource is the representation of a resource in the config
type resource struct {
// Address is the absolute resource address
Address string `json:"address,omitempty"`
// Mode can be "managed" or "data"
Mode string `json:"mode,omitempty"`
Type string `json:"type,omitempty"`
Name string `json:"name,omitempty"`
// ProviderConfigKey is the key into "provider_configs" (shown above) for
// the provider configuration that this resource is associated with.
//
// NOTE: If a given resource is in a ModuleCall, and the provider was
// configured outside of the module (in a higher level configuration file),
// the ProviderConfigKey will not match a key in the ProviderConfigs map.
ProviderConfigKey string `json:"provider_config_key,omitempty"`
// Provisioners is an optional field which describes any provisioners.
// Connection info will not be included here.
Provisioners []provisioner `json:"provisioners,omitempty"`
// Expressions" describes the resource-type-specific content of the
// configuration block.
Expressions map[string]interface{} `json:"expressions,omitempty"`
// SchemaVersion indicates which version of the resource type schema the
// "values" property conforms to.
SchemaVersion uint64 `json:"schema_version"`
// CountExpression and ForEachExpression describe the expressions given for
// the corresponding meta-arguments in the resource configuration block.
// These are omitted if the corresponding argument isn't set.
CountExpression *expression `json:"count_expression,omitempty"`
ForEachExpression *expression `json:"for_each_expression,omitempty"`
DependsOn []string `json:"depends_on,omitempty"`
}
type output struct {
Sensitive bool `json:"sensitive,omitempty"`
Expression expression `json:"expression,omitempty"`
DependsOn []string `json:"depends_on,omitempty"`
Description string `json:"description,omitempty"`
}
type provisioner struct {
Type string `json:"type,omitempty"`
Expressions map[string]interface{} `json:"expressions,omitempty"`
}
// Marshal returns the json encoding of terraform configuration.
func Marshal(c *configs.Config, schemas *terraform.Schemas) ([]byte, error) {
var output config
pcs := make(map[string]providerConfig)
marshalProviderConfigs(c, schemas, pcs)
rootModule, err := marshalModule(c, schemas, "")
if err != nil {
return nil, err
}
output.RootModule = rootModule
normalizeModuleProviderKeys(&rootModule, pcs)
for name, pc := range pcs {
if pc.parentKey != "" {
delete(pcs, name)
}
}
output.ProviderConfigs = pcs
ret, err := json.Marshal(output)
return ret, err
}
func marshalProviderConfigs(
c *configs.Config,
schemas *terraform.Schemas,
m map[string]providerConfig,
) {
if c == nil {
return
}
// We want to determine only the provider requirements from this module,
// ignoring any descendants. Disregard any diagnostics when determining
// requirements because we want this marshalling to succeed even if there
// are invalid constraints.
reqs, _ := c.ProviderRequirementsShallow()
// Add an entry for each provider configuration block in the module.
for k, pc := range c.Module.ProviderConfigs {
providerFqn := c.ProviderForConfigAddr(addrs.LocalProviderConfig{LocalName: pc.Name})
schema := schemas.ProviderConfig(providerFqn)
p := providerConfig{
Name: pc.Name,
FullName: providerFqn.String(),
Alias: pc.Alias,
ModuleAddress: c.Path.String(),
Expressions: marshalExpressions(pc.Config, schema),
}
// Store the fully resolved provider version constraint, rather than
// using the version argument in the configuration block. This is both
// future proof (for when we finish the deprecation of the provider config
// version argument) and more accurate (as it reflects the full set of
// constraints, in case there are multiple).
if vc, ok := reqs[providerFqn]; ok {
p.VersionConstraint = getproviders.VersionConstraintsString(vc)
}
key := opaqueProviderKey(k, c.Path.String())
m[key] = p
}
// Ensure that any required providers with no associated configuration
// block are included in the set.
for k, pr := range c.Module.ProviderRequirements.RequiredProviders {
// If a provider has aliases defined, process those first.
for _, alias := range pr.Aliases {
// If there exists a value for this provider, we have nothing to add
// to it, so skip.
key := opaqueProviderKey(alias.StringCompact(), c.Path.String())
if _, exists := m[key]; exists {
continue
}
// Given no provider configuration block exists, the only fields we can
// fill here are the local name, FQN, module address, and version
// constraints.
p := providerConfig{
Name: pr.Name,
FullName: pr.Type.String(),
ModuleAddress: c.Path.String(),
}
if vc, ok := reqs[pr.Type]; ok {
p.VersionConstraint = getproviders.VersionConstraintsString(vc)
}
m[key] = p
}
// If there exists a value for this provider, we have nothing to add
// to it, so skip.
key := opaqueProviderKey(k, c.Path.String())
if _, exists := m[key]; exists {
continue
}
// Given no provider configuration block exists, the only fields we can
// fill here are the local name, module address, and version
// constraints.
p := providerConfig{
Name: pr.Name,
FullName: pr.Type.String(),
ModuleAddress: c.Path.String(),
}
if vc, ok := reqs[pr.Type]; ok {
p.VersionConstraint = getproviders.VersionConstraintsString(vc)
}
m[key] = p
}
// Must also visit our child modules, recursively.
for name, mc := range c.Module.ModuleCalls {
// Keys in c.Children are guaranteed to match those in c.Module.ModuleCalls
cc := c.Children[name]
// Add provider config map entries for passed provider configs,
// pointing at the passed configuration
for _, ppc := range mc.Providers {
// These provider names include aliases, if set
moduleProviderName := ppc.InChild.String()
parentProviderName := ppc.InParent.String()
// Look up the provider FQN from the module context, using the non-aliased local name
providerFqn := cc.ProviderForConfigAddr(addrs.LocalProviderConfig{LocalName: ppc.InChild.Name})
// The presence of passed provider configs means that we cannot have
// any configuration expressions or version constraints here
p := providerConfig{
Name: moduleProviderName,
FullName: providerFqn.String(),
ModuleAddress: cc.Path.String(),
}
key := opaqueProviderKey(moduleProviderName, cc.Path.String())
parentKey := opaqueProviderKey(parentProviderName, cc.Parent.Path.String())
// Traverse up the module call tree until we find the provider
// configuration which has no linked parent config. This is then
// the source of the configuration used in this module call, so
// we link to it directly
for {
parent, exists := m[parentKey]
if !exists {
break
}
p.parentKey = parentKey
parentKey = parent.parentKey
if parentKey == "" {
break
}
}
m[key] = p
}
// Finally, marshal any other provider configs within the called module.
// It is safe to do this last because it is invalid to configure a
// provider which has passed provider configs in the module call.
marshalProviderConfigs(cc, schemas, m)
}
}
func marshalModule(c *configs.Config, schemas *terraform.Schemas, addr string) (module, error) {
var module module
var rs []resource
managedResources, err := marshalResources(c.Module.ManagedResources, schemas, addr)
if err != nil {
return module, err
}
dataResources, err := marshalResources(c.Module.DataResources, schemas, addr)
if err != nil {
return module, err
}
rs = append(managedResources, dataResources...)
module.Resources = rs
outputs := make(map[string]output)
for _, v := range c.Module.Outputs {
o := output{
Sensitive: v.Sensitive,
Expression: marshalExpression(v.Expr),
}
if v.Description != "" {
o.Description = v.Description
}
if len(v.DependsOn) > 0 {
dependencies := make([]string, len(v.DependsOn))
for i, d := range v.DependsOn {
ref, diags := addrs.ParseRef(d)
// we should not get an error here, because `terraform validate`
// would have complained well before this point, but if we do we'll
// silenty skip it.
if !diags.HasErrors() {
dependencies[i] = ref.Subject.String()
}
}
o.DependsOn = dependencies
}
outputs[v.Name] = o
}
module.Outputs = outputs
module.ModuleCalls = marshalModuleCalls(c, schemas)
if len(c.Module.Variables) > 0 {
vars := make(variables, len(c.Module.Variables))
for k, v := range c.Module.Variables {
var defaultValJSON []byte
if v.Default == cty.NilVal {
defaultValJSON = nil
} else {
defaultValJSON, err = ctyjson.Marshal(v.Default, v.Default.Type())
if err != nil {
return module, err
}
}
vars[k] = &variable{
Default: defaultValJSON,
Description: v.Description,
Sensitive: v.Sensitive,
}
}
module.Variables = vars
}
return module, nil
}
func marshalModuleCalls(c *configs.Config, schemas *terraform.Schemas) map[string]moduleCall {
ret := make(map[string]moduleCall)
for name, mc := range c.Module.ModuleCalls {
mcConfig := c.Children[name]
ret[name] = marshalModuleCall(mcConfig, mc, schemas)
}
return ret
}
func marshalModuleCall(c *configs.Config, mc *configs.ModuleCall, schemas *terraform.Schemas) moduleCall {
// It is possible to have a module call with a nil config.
if c == nil {
return moduleCall{}
}
ret := moduleCall{
// We're intentionally echoing back exactly what the user entered
// here, rather than the normalized version in SourceAddr, because
// historically we only _had_ the raw address and thus it would be
// a (admittedly minor) breaking change to start normalizing them
// now, in case consumers of this data are expecting a particular
// non-normalized syntax.
Source: mc.SourceAddrRaw,
VersionConstraint: mc.Version.Required.String(),
}
cExp := marshalExpression(mc.Count)
if !cExp.Empty() {
ret.CountExpression = &cExp
} else {
fExp := marshalExpression(mc.ForEach)
if !fExp.Empty() {
ret.ForEachExpression = &fExp
}
}
schema := &configschema.Block{}
schema.Attributes = make(map[string]*configschema.Attribute)
for _, variable := range c.Module.Variables {
schema.Attributes[variable.Name] = &configschema.Attribute{
Required: variable.Default == cty.NilVal,
}
}
ret.Expressions = marshalExpressions(mc.Config, schema)
module, _ := marshalModule(c, schemas, c.Path.String())
ret.Module = module
if len(mc.DependsOn) > 0 {
dependencies := make([]string, len(mc.DependsOn))
for i, d := range mc.DependsOn {
ref, diags := addrs.ParseRef(d)
// we should not get an error here, because `terraform validate`
// would have complained well before this point, but if we do we'll
// silenty skip it.
if !diags.HasErrors() {
dependencies[i] = ref.Subject.String()
}
}
ret.DependsOn = dependencies
}
return ret
}
func marshalResources(resources map[string]*configs.Resource, schemas *terraform.Schemas, moduleAddr string) ([]resource, error) {
var rs []resource
for _, v := range resources {
providerConfigKey := opaqueProviderKey(v.ProviderConfigAddr().StringCompact(), moduleAddr)
r := resource{
Address: v.Addr().String(),
Type: v.Type,
Name: v.Name,
ProviderConfigKey: providerConfigKey,
}
switch v.Mode {
case addrs.ManagedResourceMode:
r.Mode = "managed"
case addrs.DataResourceMode:
r.Mode = "data"
default:
return rs, fmt.Errorf("resource %s has an unsupported mode %s", r.Address, v.Mode.String())
}
cExp := marshalExpression(v.Count)
if !cExp.Empty() {
r.CountExpression = &cExp
} else {
fExp := marshalExpression(v.ForEach)
if !fExp.Empty() {
r.ForEachExpression = &fExp
}
}
schema, schemaVer := schemas.ResourceTypeConfig(
v.Provider,
v.Mode,
v.Type,
)
if schema == nil {
return nil, fmt.Errorf("no schema found for %s (in provider %s)", v.Addr().String(), v.Provider)
}
r.SchemaVersion = schemaVer
r.Expressions = marshalExpressions(v.Config, schema)
// Managed is populated only for Mode = addrs.ManagedResourceMode
if v.Managed != nil && len(v.Managed.Provisioners) > 0 {
var provisioners []provisioner
for _, p := range v.Managed.Provisioners {
schema := schemas.ProvisionerConfig(p.Type)
prov := provisioner{
Type: p.Type,
Expressions: marshalExpressions(p.Config, schema),
}
provisioners = append(provisioners, prov)
}
r.Provisioners = provisioners
}
if len(v.DependsOn) > 0 {
dependencies := make([]string, len(v.DependsOn))
for i, d := range v.DependsOn {
ref, diags := addrs.ParseRef(d)
// we should not get an error here, because `terraform validate`
// would have complained well before this point, but if we do we'll
// silenty skip it.
if !diags.HasErrors() {
dependencies[i] = ref.Subject.String()
}
}
r.DependsOn = dependencies
}
rs = append(rs, r)
}
sort.Slice(rs, func(i, j int) bool {
return rs[i].Address < rs[j].Address
})
return rs, nil
}
// Flatten all resource provider keys in a module and its descendents, such
// that any resources from providers using a configuration passed through the
// module call have a direct refernce to that provider configuration.
func normalizeModuleProviderKeys(m *module, pcs map[string]providerConfig) {
for i, r := range m.Resources {
if pc, exists := pcs[r.ProviderConfigKey]; exists {
if _, hasParent := pcs[pc.parentKey]; hasParent {
m.Resources[i].ProviderConfigKey = pc.parentKey
}
}
}
for _, mc := range m.ModuleCalls {
normalizeModuleProviderKeys(&mc.Module, pcs)
}
}
// opaqueProviderKey generates a unique absProviderConfig-like string from the module
// address and provider
func opaqueProviderKey(provider string, addr string) (key string) {
key = provider
if addr != "" {
key = fmt.Sprintf("%s:%s", addr, provider)
}
return key
}