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 }