Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

jsonconfig: Improve provider configuration output #30138

Merged
merged 3 commits into from Feb 10, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
110 changes: 106 additions & 4 deletions internal/command/jsonconfig/config.go
Expand Up @@ -27,10 +27,12 @@ type config struct {
// 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 {
Expand Down Expand Up @@ -120,14 +122,22 @@ func Marshal(c *configs.Config, schemas *terraform.Schemas) ([]byte, error) {

pcs := make(map[string]providerConfig)
marshalProviderConfigs(c, schemas, pcs)
output.ProviderConfigs = 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
}
Expand All @@ -154,6 +164,7 @@ func marshalProviderConfigs(

p := providerConfig{
Name: pc.Name,
FullName: providerFqn.String(),
Alias: pc.Alias,
ModuleAddress: c.Path.String(),
Expressions: marshalExpressions(pc.Config, schema),
Expand All @@ -176,6 +187,30 @@ func marshalProviderConfigs(
// 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())
Expand All @@ -188,6 +223,7 @@ func marshalProviderConfigs(
// constraints.
p := providerConfig{
Name: pr.Name,
FullName: pr.Type.String(),
ModuleAddress: c.Path.String(),
}

Expand All @@ -199,7 +235,53 @@ func marshalProviderConfigs(
}

// Must also visit our child modules, recursively.
for _, cc := range c.Children {
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)
}
}
Expand Down Expand Up @@ -319,7 +401,9 @@ func marshalModuleCall(c *configs.Config, mc *configs.ModuleCall, schemas *terra
}

ret.Expressions = marshalExpressions(mc.Config, schema)
module, _ := marshalModule(c, schemas, mc.Name)

module, _ := marshalModule(c, schemas, c.Path.String())

ret.Module = module

if len(mc.DependsOn) > 0 {
Expand All @@ -342,11 +426,12 @@ func marshalModuleCall(c *configs.Config, mc *configs.ModuleCall, schemas *terra
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: opaqueProviderKey(v.ProviderConfigAddr().StringCompact(), moduleAddr),
ProviderConfigKey: providerConfigKey,
}

switch v.Mode {
Expand Down Expand Up @@ -416,6 +501,23 @@ func marshalResources(resources map[string]*configs.Resource, schemas *terraform
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) {
Expand Down
2 changes: 1 addition & 1 deletion internal/command/jsonplan/plan.go
Expand Up @@ -22,7 +22,7 @@ import (
// FormatVersion represents the version of the json format and will be
// incremented for any change to this format that requires changes to a
// consuming parser.
const FormatVersion = "1.0"
const FormatVersion = "1.1"

// Plan is the top-level representation of the json format of a plan. It includes
// the complete config and current state.
Expand Down
6 changes: 6 additions & 0 deletions internal/command/show_test.go
Expand Up @@ -576,6 +576,9 @@ func TestShow_json_output(t *testing.T) {
}
json.Unmarshal([]byte(byteValue), &want)

// Disregard format version to reduce needless test fixture churn
want.FormatVersion = got.FormatVersion

if !cmp.Equal(got, want) {
t.Fatalf("wrong result:\n %v\n", cmp.Diff(got, want))
}
Expand Down Expand Up @@ -667,6 +670,9 @@ func TestShow_json_output_sensitive(t *testing.T) {
}
json.Unmarshal([]byte(byteValue), &want)

// Disregard format version to reduce needless test fixture churn
want.FormatVersion = got.FormatVersion

if !cmp.Equal(got, want) {
t.Fatalf("wrong result:\n %v\n", cmp.Diff(got, want))
}
Expand Down
1 change: 1 addition & 0 deletions internal/command/testdata/show-json-sensitive/output.json
Expand Up @@ -164,6 +164,7 @@
"provider_config": {
"test": {
"name": "test",
"full_name": "registry.terraform.io/hashicorp/test",
"expressions": {
"region": {
"constant_value": "somewhere"
Expand Down
Expand Up @@ -152,6 +152,7 @@
"provider_config": {
"test": {
"name": "test",
"full_name": "registry.terraform.io/hashicorp/test",
"expressions": {
"region": {
"constant_value": "somewhere"
Expand Down
7 changes: 4 additions & 3 deletions internal/command/testdata/show-json/modules/output.json
Expand Up @@ -224,7 +224,7 @@
"mode": "managed",
"type": "test_instance",
"name": "test",
"provider_config_key": "module_test_bar:test",
"provider_config_key": "module.module_test_bar:test",
"expressions": {
"ami": {
"references": [
Expand Down Expand Up @@ -265,7 +265,7 @@
"mode": "managed",
"type": "test_instance",
"name": "test",
"provider_config_key": "module_test_foo:test",
"provider_config_key": "module.module_test_foo:test",
"expressions": {
"ami": {
"references": [
Expand All @@ -291,7 +291,8 @@
"provider_config": {
"module.module_test_foo:test": {
"module_address": "module.module_test_foo",
"name": "test"
"name": "test",
"full_name": "registry.terraform.io/hashicorp/test"
}
}
}
Expand Down
Expand Up @@ -68,7 +68,7 @@
"mode": "managed",
"type": "test_instance",
"name": "test",
"provider_config_key": "more:test",
"provider_config_key": "module.my_module.module.more:test",
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here is an example of how the previous version of these opaque keys were incorrect. If two cousin modules were named "more", these values would clash, and either way there would never be an entry in the provider config map for "more:test".

"expressions": {
"ami": {
"references": [
Expand Down
@@ -0,0 +1,26 @@
terraform {
required_providers {
test = {
source = "hashicorp/test"
configuration_aliases = [test, test.second]
}
}
}

resource "test_instance" "test_primary" {
ami = "primary"
provider = test
}

resource "test_instance" "test_secondary" {
ami = "secondary"
provider = test.second
}

module "grandchild" {
source = "./nested"
providers = {
test = test
test.alt = test.second
}
}
@@ -0,0 +1,18 @@
terraform {
required_providers {
test = {
source = "hashicorp/test"
configuration_aliases = [test, test.alt]
}
}
}

resource "test_instance" "test_main" {
ami = "main"
provider = test
}

resource "test_instance" "test_alternate" {
ami = "secondary"
provider = test.alt
}
34 changes: 34 additions & 0 deletions internal/command/testdata/show-json/provider-aliasing/main.tf
@@ -0,0 +1,34 @@
provider "test" {
region = "somewhere"
}

provider "test" {
alias = "backup"
region = "elsewhere"
}

resource "test_instance" "test" {
ami = "foo"
provider = test
}

resource "test_instance" "test_backup" {
ami = "foo-backup"
provider = test.backup
}

module "child" {
source = "./child"
providers = {
test = test
test.second = test.backup
}
}

module "sibling" {
source = "./child"
providers = {
test = test
test.second = test
}
}