From 2453025a1ab19780b8debc4cc3d18ff815d3c98d Mon Sep 17 00:00:00 2001 From: Martin Atkins Date: Wed, 9 Jun 2021 17:24:10 -0700 Subject: [PATCH 01/19] addrs: Reference.DisplayString method We've ended up implementing something approximately like this in a few places now, so this is a centralized version that we can consolidate on moving forward, gradually removing that duplication. --- internal/addrs/parse_ref.go | 39 +++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/internal/addrs/parse_ref.go b/internal/addrs/parse_ref.go index bb68429797ad..bd5bcc7c51ae 100644 --- a/internal/addrs/parse_ref.go +++ b/internal/addrs/parse_ref.go @@ -2,10 +2,12 @@ package addrs import ( "fmt" + "strings" "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclsyntax" "github.com/hashicorp/terraform/internal/tfdiags" + "github.com/zclconf/go-cty/cty" ) // Reference describes a reference to an address with source location @@ -16,6 +18,43 @@ type Reference struct { Remaining hcl.Traversal } +// DisplayString returns a string that approximates the subject and remaining +// traversal of the reciever in a way that resembles the Terraform language +// syntax that could've produced it. +// +// It's not guaranteed to actually be a valid Terraform language expression, +// since the intended use here is primarily for UI messages such as +// diagnostics. +func (r *Reference) DisplayString() string { + if len(r.Remaining) == 0 { + // Easy case: we can just return the subject's string. + return r.Subject.String() + } + + var ret strings.Builder + ret.WriteString(r.Subject.String()) + for _, step := range r.Remaining { + switch tStep := step.(type) { + case hcl.TraverseRoot: + ret.WriteString(tStep.Name) + case hcl.TraverseAttr: + ret.WriteByte('.') + ret.WriteString(tStep.Name) + case hcl.TraverseIndex: + ret.WriteByte('[') + switch tStep.Key.Type() { + case cty.String: + ret.WriteString(fmt.Sprintf("%q", tStep.Key.AsString())) + case cty.Number: + bf := tStep.Key.AsBigFloat() + ret.WriteString(bf.Text('g', 10)) + } + ret.WriteByte(']') + } + } + return ret.String() +} + // ParseRef attempts to extract a referencable address from the prefix of the // given traversal, which must be an absolute traversal or this function // will panic. From 14253743714f085b81cefe3e5af300bae7c15d55 Mon Sep 17 00:00:00 2001 From: Martin Atkins Date: Fri, 11 Jun 2021 11:59:49 -0700 Subject: [PATCH 02/19] providers: A type for all schemas for a particular provider Previously the "providers" package contained only a type for representing the schema of a particular object within a provider, and the terraform package had the responsibility of aggregating many of those together to describe the entire surface area of a provider. Here we move what was previously terraform.ProviderSchema to instead be providers.Schemas, retaining its existing API otherwise, and leave behind a type alias to allow us to gradually update other references over time. We've gradually been shrinking down the responsibilities of the "terraform" package to just representing the graph components and behaviors anyway, but the specific motivation for doing this _now_ is to allow for other packages to both be called by the terraform package _and_ work with provider schemas at the same time, without creating a package dependency cycle: instead, these other packages can just import the "providers" package and not need to import the "terraform" package at all. For now this does still leave the responsibility for _building_ a providers.Schemas object over in the "terraform" package, because it's currently doing that as part of some larger work that isn't easily separable, and so reorganizing that would be a more involved and riskier change than just moving the existing type elsewhere. --- internal/providers/provider.go | 8 ----- internal/providers/schemas.go | 62 ++++++++++++++++++++++++++++++++++ internal/terraform/schemas.go | 52 ++++++---------------------- 3 files changed, 73 insertions(+), 49 deletions(-) create mode 100644 internal/providers/schemas.go diff --git a/internal/providers/provider.go b/internal/providers/provider.go index 8d3914f00d7b..26d4345883e4 100644 --- a/internal/providers/provider.go +++ b/internal/providers/provider.go @@ -3,7 +3,6 @@ package providers import ( "github.com/zclconf/go-cty/cty" - "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/states" "github.com/hashicorp/terraform/internal/tfdiags" ) @@ -88,13 +87,6 @@ type GetProviderSchemaResponse struct { Diagnostics tfdiags.Diagnostics } -// Schema pairs a provider or resource schema with that schema's version. -// This is used to be able to upgrade the schema in UpgradeResourceState. -type Schema struct { - Version int64 - Block *configschema.Block -} - type ValidateProviderConfigRequest struct { // Config is the raw configuration value for the provider. Config cty.Value diff --git a/internal/providers/schemas.go b/internal/providers/schemas.go new file mode 100644 index 000000000000..213ff4f0e588 --- /dev/null +++ b/internal/providers/schemas.go @@ -0,0 +1,62 @@ +package providers + +import ( + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/configs/configschema" +) + +// Schemas is an overall container for all of the schemas for all configurable +// objects defined within a particular provider. +// +// The schema for each individual configurable object is represented by nested +// instances of type Schema (singular) within this data structure. +// +// This type used to be known as terraform.ProviderSchema, but moved out here +// as part of our ongoing efforts to shrink down the "terraform" package. +// There's still a type alias at the old name, but we should prefer using +// providers.Schema in new code. However, a consequence of this transitional +// situation is that the "terraform" package still has the responsibility for +// constructing a providers.Schemas object based on responses from the provider +// API; hopefully we'll continue this refactor later so that functions in this +// package totally encapsulate the unmarshalling and include this as part of +// providers.GetProviderSchemaResponse. +type Schemas struct { + Provider *configschema.Block + ProviderMeta *configschema.Block + ResourceTypes map[string]*configschema.Block + DataSources map[string]*configschema.Block + + ResourceTypeSchemaVersions map[string]uint64 +} + +// SchemaForResourceType attempts to find a schema for the given mode and type. +// Returns nil if no such schema is available. +func (ss *Schemas) SchemaForResourceType(mode addrs.ResourceMode, typeName string) (schema *configschema.Block, version uint64) { + switch mode { + case addrs.ManagedResourceMode: + return ss.ResourceTypes[typeName], ss.ResourceTypeSchemaVersions[typeName] + case addrs.DataResourceMode: + // Data resources don't have schema versions right now, since state is discarded for each refresh + return ss.DataSources[typeName], 0 + default: + // Shouldn't happen, because the above cases are comprehensive. + return nil, 0 + } +} + +// SchemaForResourceAddr attempts to find a schema for the mode and type from +// the given resource address. Returns nil if no such schema is available. +func (ss *Schemas) SchemaForResourceAddr(addr addrs.Resource) (schema *configschema.Block, version uint64) { + return ss.SchemaForResourceType(addr.Mode, addr.Type) +} + +// Schema pairs a provider or resource schema with that schema's version. +// This is used to be able to upgrade the schema in UpgradeResourceState. +// +// This describes the schema for a single object within a provider. Type +// "Schemas" (plural) instead represents the overall collection of schemas +// for everything within a particular provider. +type Schema struct { + Version int64 + Block *configschema.Block +} diff --git a/internal/terraform/schemas.go b/internal/terraform/schemas.go index d09cc2cb2533..24edeb85aaf9 100644 --- a/internal/terraform/schemas.go +++ b/internal/terraform/schemas.go @@ -12,10 +12,16 @@ import ( "github.com/hashicorp/terraform/internal/tfdiags" ) +// ProviderSchema is an alias for providers.Schemas, which is the new location +// for what we originally called terraform.ProviderSchema but which has +// moved out as part of ongoing refactoring to shrink down the main "terraform" +// package. +type ProviderSchema = providers.Schemas + // Schemas is a container for various kinds of schema that Terraform needs // during processing. type Schemas struct { - Providers map[addrs.Provider]*ProviderSchema + Providers map[addrs.Provider]*providers.Schemas Provisioners map[string]*configschema.Block } @@ -24,7 +30,7 @@ type Schemas struct { // // It's usually better to go use the more precise methods offered by type // Schemas to handle this detail automatically. -func (ss *Schemas) ProviderSchema(provider addrs.Provider) *ProviderSchema { +func (ss *Schemas) ProviderSchema(provider addrs.Provider) *providers.Schemas { if ss.Providers == nil { return nil } @@ -76,7 +82,7 @@ func (ss *Schemas) ProvisionerConfig(name string) *configschema.Block { // still valid but may be incomplete. func loadSchemas(config *configs.Config, state *states.State, plugins *contextPlugins) (*Schemas, error) { schemas := &Schemas{ - Providers: map[addrs.Provider]*ProviderSchema{}, + Providers: map[addrs.Provider]*providers.Schemas{}, Provisioners: map[string]*configschema.Block{}, } var diags tfdiags.Diagnostics @@ -89,7 +95,7 @@ func loadSchemas(config *configs.Config, state *states.State, plugins *contextPl return schemas, diags.Err() } -func loadProviderSchemas(schemas map[addrs.Provider]*ProviderSchema, config *configs.Config, state *states.State, plugins *contextPlugins) tfdiags.Diagnostics { +func loadProviderSchemas(schemas map[addrs.Provider]*providers.Schemas, config *configs.Config, state *states.State, plugins *contextPlugins) tfdiags.Diagnostics { var diags tfdiags.Diagnostics ensure := func(fqn addrs.Provider) { @@ -105,7 +111,7 @@ func loadProviderSchemas(schemas map[addrs.Provider]*ProviderSchema, config *con // We'll put a stub in the map so we won't re-attempt this on // future calls, which would then repeat the same error message // multiple times. - schemas[fqn] = &ProviderSchema{} + schemas[fqn] = &providers.Schemas{} diags = diags.Append( tfdiags.Sourceless( tfdiags.Error, @@ -179,39 +185,3 @@ func loadProvisionerSchemas(schemas map[string]*configschema.Block, config *conf return diags } - -// ProviderSchema represents the schema for a provider's own configuration -// and the configuration for some or all of its resources and data sources. -// -// The completeness of this structure depends on how it was constructed. -// When constructed for a configuration, it will generally include only -// resource types and data sources used by that configuration. -type ProviderSchema struct { - Provider *configschema.Block - ProviderMeta *configschema.Block - ResourceTypes map[string]*configschema.Block - DataSources map[string]*configschema.Block - - ResourceTypeSchemaVersions map[string]uint64 -} - -// SchemaForResourceType attempts to find a schema for the given mode and type. -// Returns nil if no such schema is available. -func (ps *ProviderSchema) SchemaForResourceType(mode addrs.ResourceMode, typeName string) (schema *configschema.Block, version uint64) { - switch mode { - case addrs.ManagedResourceMode: - return ps.ResourceTypes[typeName], ps.ResourceTypeSchemaVersions[typeName] - case addrs.DataResourceMode: - // Data resources don't have schema versions right now, since state is discarded for each refresh - return ps.DataSources[typeName], 0 - default: - // Shouldn't happen, because the above cases are comprehensive. - return nil, 0 - } -} - -// SchemaForResourceAddr attempts to find a schema for the mode and type from -// the given resource address. Returns nil if no such schema is available. -func (ps *ProviderSchema) SchemaForResourceAddr(addr addrs.Resource) (schema *configschema.Block, version uint64) { - return ps.SchemaForResourceType(addr.Mode, addr.Type) -} From 055c432f12a3ecbb70a4faa0f66ff993b45700eb Mon Sep 17 00:00:00 2001 From: Martin Atkins Date: Wed, 9 Jun 2021 12:11:44 -0700 Subject: [PATCH 03/19] lang/globalref: Global reference analysis utilities Our existing functionality for dealing with references generally only has to concern itself with one level of references at a time, and only within one module, because we use it to draw a dependency graph which then ends up reflecting the broader context. However, there are some situations where it's handy to be able to ask questions about the indirect contributions to a particular expression in the configuration, particularly for additional hints in the user interface where we're just providing some extra context rather than changing behavior. This new "globalref" package therefore aims to be the home for algorithms for use-cases like this. It introduces its own special "Reference" type that wraps addrs.Reference to annotate it also with the usually-implied context about where the references would be evaluated. With that building block we can therefore ask questions whose answers might involve discussing references in multiple packages at once, such as "which resources directly or indirectly contribute to this expression?", including indirect hops through input variables or output values which would therefore change the evaluation context. The current implementations of this are around mapping references onto the static configuration expressions that they refer to, which is a pretty broad and conservative approach that unfortunately therefore loses accuracy when confronted with complex expressions that might take dynamic actions on the contents of an object. My hunch is that this'll be good enough to get some initial small use-cases solved, though there's plenty room for improvement in accuracy. It's somewhat ironic that this sort of "what is this value built from?" question is the use-case I had in mind when I designed the "marks" feature in cty, yet we've ended up putting it to an unexpected but still valid use in Terraform for sensitivity analysis and our currently handling of that isn't really tight enough to permit other concurrent uses of marks for other use-cases. I expect we can address that later and so maybe we'll try for a more accurate version of these analyses at a later date, but my hunch is that this'll be good enough for us to still get some good use out of it in the near future, particular related to helping understand where unknown values came from and in tailoring our refresh results in plan output to deemphasize detected changes that couldn't possibly have contributed to the proposed plan. --- internal/lang/globalref/analyzer.go | 68 ++ .../analyzer_contributing_resources.go | 130 ++++ .../analyzer_contributing_resources_test.go | 96 +++ .../globalref/analyzer_meta_references.go | 586 ++++++++++++++++++ .../analyzer_meta_references_shortcuts.go | 87 +++ .../analyzer_meta_references_test.go | 163 +++++ internal/lang/globalref/analyzer_test.go | 98 +++ internal/lang/globalref/doc.go | 9 + internal/lang/globalref/reference.go | 136 ++++ .../testdata/assorted/assorted-root.tf | 47 ++ .../testdata/assorted/child/assorted-child.tf | 13 + .../compute/contributing-resources-compute.tf | 51 ++ .../contributing-resources-root.tf | 28 + .../network/contributing-resources-network.tf | 41 ++ 14 files changed, 1553 insertions(+) create mode 100644 internal/lang/globalref/analyzer.go create mode 100644 internal/lang/globalref/analyzer_contributing_resources.go create mode 100644 internal/lang/globalref/analyzer_contributing_resources_test.go create mode 100644 internal/lang/globalref/analyzer_meta_references.go create mode 100644 internal/lang/globalref/analyzer_meta_references_shortcuts.go create mode 100644 internal/lang/globalref/analyzer_meta_references_test.go create mode 100644 internal/lang/globalref/analyzer_test.go create mode 100644 internal/lang/globalref/doc.go create mode 100644 internal/lang/globalref/reference.go create mode 100644 internal/lang/globalref/testdata/assorted/assorted-root.tf create mode 100644 internal/lang/globalref/testdata/assorted/child/assorted-child.tf create mode 100644 internal/lang/globalref/testdata/contributing-resources/compute/contributing-resources-compute.tf create mode 100644 internal/lang/globalref/testdata/contributing-resources/contributing-resources-root.tf create mode 100644 internal/lang/globalref/testdata/contributing-resources/network/contributing-resources-network.tf diff --git a/internal/lang/globalref/analyzer.go b/internal/lang/globalref/analyzer.go new file mode 100644 index 000000000000..7a24d781ef39 --- /dev/null +++ b/internal/lang/globalref/analyzer.go @@ -0,0 +1,68 @@ +package globalref + +import ( + "fmt" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/configs" + "github.com/hashicorp/terraform/internal/providers" +) + +// Analyzer is the main component of this package, serving as a container for +// various state that the analysis algorithms depend on either for their core +// functionality or for producing results more quickly. +// +// Global reference analysis is currently intended only for "best effort" +// use-cases related to giving hints to the user or tailoring UI output. +// Avoid using it for anything that would cause changes to the analyzer being +// considered a breaking change under the v1 compatibility promises, because +// we expect to continue to refine and evolve these rules over time in ways +// that may cause us to detect either more or fewer references than today. +// Typically we will conservatively return more references than would be +// necessary dynamically, but that isn't guaranteed for all situations. +// +// In particular, we currently typically don't distinguish between multiple +// instances of the same module, and so we overgeneralize references from +// one instance of a module as references from the same location in all +// instances of that module. We may make this more precise in future, which +// would then remove various detected references from the analysis results. +// +// Each Analyzer works with a particular configs.Config object which it assumes +// represents the root module of a configuration. Config objects are typically +// immutable by convention anyway, but it's particularly important not to +// modify a configuration while it's attached to a live Analyzer, because +// the Analyzer contains caches derived from data in the configuration tree. +type Analyzer struct { + cfg *configs.Config + providerSchemas map[addrs.Provider]*providers.Schemas +} + +// NewAnalyzer constructs a new analyzer bound to the given configuration and +// provider schemas. +// +// The given object must represent a root module, or this function will panic. +// +// The given provider schemas must cover at least all of the providers used +// in the given configuration. If not then analysis results will be silently +// incomplete for any decision that requires checking schema. +func NewAnalyzer(cfg *configs.Config, providerSchemas map[addrs.Provider]*providers.Schemas) *Analyzer { + if !cfg.Path.IsRoot() { + panic(fmt.Sprintf("constructing an Analyzer with non-root module %s", cfg.Path)) + } + + ret := &Analyzer{ + cfg: cfg, + providerSchemas: providerSchemas, + } + return ret +} + +// ModuleConfig retrieves a module configuration from the configuration the +// analyzer belongs to, or nil if there is no module with the given address. +func (a *Analyzer) ModuleConfig(addr addrs.ModuleInstance) *configs.Module { + modCfg := a.cfg.DescendentForInstance(addr) + if modCfg == nil { + return nil + } + return modCfg.Module +} diff --git a/internal/lang/globalref/analyzer_contributing_resources.go b/internal/lang/globalref/analyzer_contributing_resources.go new file mode 100644 index 000000000000..4024bafd0c8d --- /dev/null +++ b/internal/lang/globalref/analyzer_contributing_resources.go @@ -0,0 +1,130 @@ +package globalref + +import ( + "sort" + + "github.com/hashicorp/terraform/internal/addrs" +) + +// ContributingResources analyzes all of the given references and +// for each one tries to walk backwards through any named values to find all +// resources whose values contributed either directly or indirectly to any of +// them. +// +// This is a wrapper around ContributingResourceReferences which simplifies +// the result to only include distinct resource addresses, not full references. +// If the configuration includes several different references to different +// parts of a resource, ContributingResources will not preserve that detail. +func (a *Analyzer) ContributingResources(refs ...Reference) []addrs.AbsResource { + retRefs := a.ContributingResourceReferences(refs...) + if len(retRefs) == 0 { + return nil + } + + uniq := make(map[string]addrs.AbsResource, len(refs)) + for _, ref := range retRefs { + if addr, ok := resourceForAddr(ref.LocalRef.Subject); ok { + moduleAddr := ref.ModuleAddr() + absAddr := addr.Absolute(moduleAddr) + uniq[absAddr.String()] = absAddr + } + } + ret := make([]addrs.AbsResource, 0, len(uniq)) + for _, addr := range uniq { + ret = append(ret, addr) + } + sort.Slice(ret, func(i, j int) bool { + // We only have a sorting function for resource _instances_, but + // it'll do well enough if we just pretend we have no-key instances. + return ret[i].Instance(addrs.NoKey).Less(ret[j].Instance(addrs.NoKey)) + }) + return ret +} + +// ContributingResourceReferences analyzes all of the given references and +// for each one tries to walk backwards through any named values to find all +// references to resource attributes that contributed either directly or +// indirectly to any of them. +// +// This is a global operation that can be potentially quite expensive for +// complex configurations. +func (a *Analyzer) ContributingResourceReferences(refs ...Reference) []Reference { + // Our methodology here is to keep digging through MetaReferences + // until we've visited everything we encounter directly or indirectly, + // and keep track of any resources we find along the way. + + // We'll aggregate our result here, using the string representations of + // the resources as keys to avoid returning the same one more than once. + found := make(map[referenceAddrKey]Reference) + + // We might encounter the same object multiple times as we walk, + // but we won't learn anything more by traversing them again and so we'll + // just skip them instead. + visitedObjects := make(map[referenceAddrKey]struct{}) + + // A queue of objects we still need to visit. + // Note that if we find multiple references to the same object then we'll + // just arbitrary choose any one of them, because for our purposes here + // it's immaterial which reference we actually followed. + pendingObjects := make(map[referenceAddrKey]Reference) + + // Initial state: identify any directly-mentioned resources and + // queue up any named values we refer to. + for _, ref := range refs { + if _, ok := resourceForAddr(ref.LocalRef.Subject); ok { + found[ref.addrKey()] = ref + } + pendingObjects[ref.addrKey()] = ref + } + + for len(pendingObjects) > 0 { + // Note: we modify this map while we're iterating over it, which means + // that anything we add might be either visited within a later + // iteration of the inner loop or in a later iteration of the outer + // loop, but we get the correct result either way because we keep + // working until we've fully depleted the queue. + for key, ref := range pendingObjects { + delete(pendingObjects, key) + + // We do this _before_ the visit below just in case this is an + // invalid config with a self-referential local value, in which + // case we'll just silently ignore the self reference for our + // purposes here, and thus still eventually converge (albeit + // with an incomplete answer). + visitedObjects[key] = struct{}{} + + moreRefs := a.MetaReferences(ref) + for _, newRef := range moreRefs { + if _, ok := resourceForAddr(newRef.LocalRef.Subject); ok { + found[newRef.addrKey()] = newRef + } + + newKey := newRef.addrKey() + if _, visited := visitedObjects[newKey]; !visited { + pendingObjects[newKey] = newRef + } + } + } + } + + if len(found) == 0 { + return nil + } + + ret := make([]Reference, 0, len(found)) + for _, ref := range found { + ret = append(ret, ref) + } + return ret +} + +func resourceForAddr(addr addrs.Referenceable) (addrs.Resource, bool) { + switch addr := addr.(type) { + case addrs.Resource: + return addr, true + case addrs.ResourceInstance: + return addr.Resource, true + default: + return addrs.Resource{}, false + } +} diff --git a/internal/lang/globalref/analyzer_contributing_resources_test.go b/internal/lang/globalref/analyzer_contributing_resources_test.go new file mode 100644 index 000000000000..038b3ed54399 --- /dev/null +++ b/internal/lang/globalref/analyzer_contributing_resources_test.go @@ -0,0 +1,96 @@ +package globalref + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform/internal/addrs" +) + +func TestAnalyzerContributingResources(t *testing.T) { + azr := testAnalyzer(t, "contributing-resources") + + tests := map[string]struct { + StartRefs func() []Reference + WantAddrs []string + }{ + "root output 'network'": { + func() []Reference { + return azr.ReferencesFromOutputValue( + addrs.OutputValue{Name: "network"}.Absolute(addrs.RootModuleInstance), + ) + }, + []string{ + `data.test_thing.environment`, + `module.network.test_thing.subnet`, + `module.network.test_thing.vpc`, + }, + }, + "root output 'c10s_url'": { + func() []Reference { + return azr.ReferencesFromOutputValue( + addrs.OutputValue{Name: "c10s_url"}.Absolute(addrs.RootModuleInstance), + ) + }, + []string{ + `data.test_thing.environment`, + `module.compute.test_thing.load_balancer`, + `module.network.test_thing.subnet`, + `module.network.test_thing.vpc`, + + // NOTE: module.compute.test_thing.controller isn't here + // because we can see statically that the output value refers + // only to the "string" attribute of + // module.compute.test_thing.load_balancer , and so we + // don't consider references inside the "list" blocks. + }, + }, + "module.compute.test_thing.load_balancer": { + func() []Reference { + return azr.ReferencesFromResourceInstance( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_thing", + Name: "load_balancer", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance.Child("compute", addrs.NoKey)), + ) + }, + []string{ + `data.test_thing.environment`, + `module.compute.test_thing.controller`, + `module.network.test_thing.subnet`, + `module.network.test_thing.vpc`, + }, + }, + "data.test_thing.environment": { + func() []Reference { + return azr.ReferencesFromResourceInstance( + addrs.Resource{ + Mode: addrs.DataResourceMode, + Type: "test_thing", + Name: "environment", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + ) + }, + []string{ + // Nothing! This one only refers to an input variable. + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + startRefs := test.StartRefs() + addrs := azr.ContributingResources(startRefs...) + + want := test.WantAddrs + got := make([]string, len(addrs)) + for i, addr := range addrs { + got[i] = addr.String() + } + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("wrong addresses\n%s", diff) + } + }) + } +} diff --git a/internal/lang/globalref/analyzer_meta_references.go b/internal/lang/globalref/analyzer_meta_references.go new file mode 100644 index 000000000000..b7c6db22ea1d --- /dev/null +++ b/internal/lang/globalref/analyzer_meta_references.go @@ -0,0 +1,586 @@ +package globalref + +import ( + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/configs/configschema" + "github.com/hashicorp/terraform/internal/lang" + "github.com/zclconf/go-cty/cty" + "github.com/zclconf/go-cty/cty/convert" + "github.com/zclconf/go-cty/cty/gocty" +) + +// MetaReferences inspects the configuration to find the references contained +// within the most specific object that the given address refers to. +// +// This finds only the direct references in that object, not any indirect +// references from those. This is a building block for some other Analyzer +// functions that can walk through multiple levels of reference. +// +// If the given reference refers to something that doesn't exist in the +// configuration we're analyzing then MetaReferences will return no +// meta-references at all, which is indistinguishable from an existing +// object that doesn't refer to anything. +func (a *Analyzer) MetaReferences(ref Reference) []Reference { + // This function is aiming to encapsulate the fact that a reference + // is actually quite a complex notion which includes both a specific + // object the reference is to, where each distinct object type has + // a very different representation in the configuration, and then + // also potentially an attribute or block within the definition of that + // object. Our goal is to make all of these different situations appear + // mostly the same to the caller, in that all of them can be reduced to + // a set of references regardless of which expression or expressions we + // derive those from. + + moduleAddr := ref.ModuleAddr() + remaining := ref.LocalRef.Remaining + + // Our first task then is to select an appropriate implementation based + // on which address type the reference refers to. + switch targetAddr := ref.LocalRef.Subject.(type) { + case addrs.InputVariable: + return a.metaReferencesInputVariable(moduleAddr, targetAddr, remaining) + case addrs.ModuleCallInstanceOutput: + return a.metaReferencesOutputValue(moduleAddr, targetAddr, remaining) + case addrs.ModuleCallInstance: + return a.metaReferencesModuleCall(moduleAddr, targetAddr, remaining) + case addrs.ModuleCall: + // TODO: It isn't really correct to say that a reference to a module + // call is a reference to its no-key instance. Really what we want to + // say here is that it's a reference to _all_ instances, or to an + // instance with an unknown key, but we don't have any representation + // of that. For the moment it's pretty immaterial since most of our + // other analysis ignores instance keys anyway, but maybe we'll revisit + // this latter to distingish these two cases better. + return a.metaReferencesModuleCall(moduleAddr, targetAddr.Instance(addrs.NoKey), remaining) + case addrs.CountAttr, addrs.ForEachAttr: + if resourceAddr, ok := ref.ResourceAddr(); ok { + return a.metaReferencesCountOrEach(resourceAddr) + } + return nil + case addrs.ResourceInstance: + return a.metaReferencesResourceInstance(moduleAddr, targetAddr, remaining) + case addrs.Resource: + // TODO: It isn't really correct to say that a reference to a resource + // is a reference to its no-key instance. Really what we want to say + // here is that it's a reference to _all_ instances, or to an instance + // with an unknown key, but we don't have any representation of that. + // For the moment it's pretty immaterial since most of our other + // analysis ignores instance keys anyway, but maybe we'll revisit this + // latter to distingish these two cases better. + return a.metaReferencesResourceInstance(moduleAddr, targetAddr.Instance(addrs.NoKey), remaining) + default: + // For anything we don't explicitly support we'll just return no + // references. This includes the reference types that don't really + // refer to configuration objects at all, like "path.module", + // and so which cannot possibly generate any references. + return nil + } +} + +func (a *Analyzer) metaReferencesInputVariable(calleeAddr addrs.ModuleInstance, addr addrs.InputVariable, remain hcl.Traversal) []Reference { + if calleeAddr.IsRoot() { + // A root module variable definition can never refer to anything, + // because it conceptually exists outside of any module. + return nil + } + + callerAddr, callAddr := calleeAddr.Call() + + // We need to find the module call inside the caller module. + callerCfg := a.ModuleConfig(callerAddr) + if callerCfg == nil { + return nil + } + call := callerCfg.ModuleCalls[callAddr.Name] + if call == nil { + return nil + } + + // Now we need to look for an attribute matching the variable name inside + // the module block body. + body := call.Config + schema := &hcl.BodySchema{ + Attributes: []hcl.AttributeSchema{ + {Name: addr.Name}, + }, + } + // We don't check for errors here because we'll make a best effort to + // analyze whatever partial result HCL is able to extract. + content, _, _ := body.PartialContent(schema) + attr := content.Attributes[addr.Name] + if attr == nil { + return nil + } + refs, _ := lang.ReferencesInExpr(attr.Expr) + return absoluteRefs(callerAddr, refs) +} + +func (a *Analyzer) metaReferencesOutputValue(callerAddr addrs.ModuleInstance, addr addrs.ModuleCallInstanceOutput, remain hcl.Traversal) []Reference { + calleeAddr := callerAddr.Child(addr.Call.Call.Name, addr.Call.Key) + + // We need to find the output value declaration inside the callee module. + calleeCfg := a.ModuleConfig(calleeAddr) + if calleeCfg == nil { + return nil + } + + oc := calleeCfg.Outputs[addr.Name] + if oc == nil { + return nil + } + + // We don't check for errors here because we'll make a best effort to + // analyze whatever partial result HCL is able to extract. + refs, _ := lang.ReferencesInExpr(oc.Expr) + return absoluteRefs(calleeAddr, refs) +} + +func (a *Analyzer) metaReferencesModuleCall(callerAddr addrs.ModuleInstance, addr addrs.ModuleCallInstance, remain hcl.Traversal) []Reference { + calleeAddr := callerAddr.Child(addr.Call.Name, addr.Key) + + // What we're really doing here is just rolling up all of the references + // from all of this module's output values. + calleeCfg := a.ModuleConfig(calleeAddr) + if calleeCfg == nil { + return nil + } + + var ret []Reference + for name := range calleeCfg.Outputs { + outputAddr := addrs.ModuleCallInstanceOutput{ + Call: addr, + Name: name, + } + moreRefs := a.metaReferencesOutputValue(callerAddr, outputAddr, nil) + ret = append(ret, moreRefs...) + } + return ret +} + +func (a *Analyzer) metaReferencesCountOrEach(resourceAddr addrs.AbsResource) []Reference { + return a.ReferencesFromResourceRepetition(resourceAddr) +} + +func (a *Analyzer) metaReferencesResourceInstance(moduleAddr addrs.ModuleInstance, addr addrs.ResourceInstance, remain hcl.Traversal) []Reference { + modCfg := a.ModuleConfig(moduleAddr) + if modCfg == nil { + return nil + } + + rc := modCfg.ResourceByAddr(addr.Resource) + if rc == nil { + return nil + } + + // In valid cases we should have the schema for this resource type + // available. In invalid cases we might be dealing with partial information, + // and so the schema might be nil so we won't be able to return reference + // information for this particular situation. + providerSchema := a.providerSchemas[rc.Provider] + if providerSchema == nil { + return nil + } + resourceTypeSchema, _ := providerSchema.SchemaForResourceAddr(addr.Resource) + if resourceTypeSchema == nil { + return nil + } + + // When analyzing the resource configuration to look for references, we'll + // make a best effort to narrow down to only a particular sub-portion of + // the configuration by following the remaining traversal steps. In the + // ideal case this will lead us to a specific expression, but as a + // compromise it might lead us to some nested blocks where at least we + // can limit our searching only to those. + bodies := []hcl.Body{rc.Config} + var exprs []hcl.Expression + schema := resourceTypeSchema + var steppingThrough *configschema.NestedBlock + var steppingThroughType string + nextStep := func(newBodies []hcl.Body, newExprs []hcl.Expression) { + // We append exprs but replace bodies because exprs represent extra + // expressions we collected on the path, such as dynamic block for_each, + // which can potentially contribute to the final evalcontext, but + // bodies never contribute any values themselves, and instead just + // narrow down where we're searching. + bodies = newBodies + exprs = append(exprs, newExprs...) + steppingThrough = nil + steppingThroughType = "" + // Caller must also update "schema" if necessary. + } + traverseInBlock := func(name string) ([]hcl.Body, []hcl.Expression) { + if attr := schema.Attributes[name]; attr != nil { + // When we reach a specific attribute we can't traverse any deeper, because attributes are the leaves of the schema. + schema = nil + return traverseAttr(bodies, name) + } else if blockType := schema.BlockTypes[name]; blockType != nil { + // We need to take a different action here depending on + // the nesting mode of the block type. Some require us + // to traverse in two steps in order to select a specific + // child block, while others we can just step through + // directly. + switch blockType.Nesting { + case configschema.NestingSingle, configschema.NestingGroup: + // There should be only zero or one blocks of this + // type, so we can traverse in only one step. + schema = &blockType.Block + return traverseNestedBlockSingle(bodies, name) + case configschema.NestingMap, configschema.NestingList, configschema.NestingSet: + steppingThrough = blockType + return bodies, exprs // Preserve current selections for the second step + default: + // The above should be exhaustive, but just in case + // we add something new in future we'll bail out + // here and conservatively return everything under + // the current traversal point. + schema = nil + return nil, nil + } + } + + // We'll get here if the given name isn't in the schema at all. If so, + // there's nothing else to be done here. + schema = nil + return nil, nil + } +Steps: + for _, step := range remain { + // If we filter out all of our bodies before we finish traversing then + // we know we won't find anything else, because all of our subsequent + // traversal steps won't have any bodies to search. + if len(bodies) == 0 { + return nil + } + // If we no longer have a schema then that suggests we've + // traversed as deep as what the schema covers (e.g. we reached + // a specific attribute) and so we'll stop early, assuming that + // any remaining steps are traversals into an attribute expression + // result. + if schema == nil { + break + } + + switch step := step.(type) { + + case hcl.TraverseAttr: + switch { + case steppingThrough != nil: + // If we're stepping through a NestingMap block then + // it's valid to use attribute syntax to select one of + // the blocks by its label. Other nesting types require + // TraverseIndex, so can never be valid. + if steppingThrough.Nesting != configschema.NestingMap { + nextStep(nil, nil) // bail out + continue + } + nextStep(traverseNestedBlockMap(bodies, steppingThroughType, step.Name)) + schema = &steppingThrough.Block + default: + nextStep(traverseInBlock(step.Name)) + if schema == nil { + // traverseInBlock determined that we've traversed as + // deep as we can with reference to schema, so we'll + // stop here and just process whatever's selected. + break Steps + } + } + case hcl.TraverseIndex: + switch { + case steppingThrough != nil: + switch steppingThrough.Nesting { + case configschema.NestingMap: + keyVal, err := convert.Convert(step.Key, cty.String) + if err != nil { // Invalid traversal, so can't have any refs + nextStep(nil, nil) // bail out + continue + } + nextStep(traverseNestedBlockMap(bodies, steppingThroughType, keyVal.AsString())) + schema = &steppingThrough.Block + case configschema.NestingList: + idxVal, err := convert.Convert(step.Key, cty.Number) + if err != nil { // Invalid traversal, so can't have any refs + nextStep(nil, nil) // bail out + continue + } + var idx int + err = gocty.FromCtyValue(idxVal, &idx) + if err != nil { // Invalid traversal, so can't have any refs + nextStep(nil, nil) // bail out + continue + } + nextStep(traverseNestedBlockList(bodies, steppingThroughType, idx)) + schema = &steppingThrough.Block + default: + // Note that NestingSet ends up in here because we don't + // actually allow traversing into set-backed block types, + // and so such a reference would be invalid. + nextStep(nil, nil) // bail out + continue + } + default: + // When indexing the contents of a block directly we always + // interpret the key as a string representing an attribute + // name. + nameVal, err := convert.Convert(step.Key, cty.String) + if err != nil { // Invalid traversal, so can't have any refs + nextStep(nil, nil) // bail out + continue + } + nextStep(traverseInBlock(nameVal.AsString())) + if schema == nil { + // traverseInBlock determined that we've traversed as + // deep as we can with reference to schema, so we'll + // stop here and just process whatever's selected. + break Steps + } + } + default: + // We shouldn't get here, because the above cases are exhaustive + // for all of the relative traversal types, but we'll be robust in + // case HCL adds more in future and just pretend the traversal + // ended a bit early if so. + break Steps + } + } + + if steppingThrough != nil { + // If we ended in the middle of "stepping through" then we'll conservatively + // use the bodies of _all_ nested blocks of the type we were stepping + // through, because the recipient of this value could refer to any + // of them dynamically. + var labelNames []string + if steppingThrough.Nesting == configschema.NestingMap { + labelNames = []string{"key"} + } + blocks := findBlocksInBodies(bodies, steppingThroughType, labelNames) + for _, block := range blocks { + bodies, exprs = blockParts(block) + } + } + + if len(bodies) == 0 && len(exprs) == 0 { + return nil + } + + var refs []*addrs.Reference + for _, expr := range exprs { + moreRefs, _ := lang.ReferencesInExpr(expr) + refs = append(refs, moreRefs...) + } + if schema != nil { + for _, body := range bodies { + moreRefs, _ := lang.ReferencesInBlock(body, schema) + refs = append(refs, moreRefs...) + } + } + return absoluteRefs(addr.Absolute(moduleAddr), refs) +} + +func traverseAttr(bodies []hcl.Body, name string) ([]hcl.Body, []hcl.Expression) { + if len(bodies) == 0 { + return nil, nil + } + schema := &hcl.BodySchema{ + Attributes: []hcl.AttributeSchema{ + {Name: name}, + }, + } + // We can find at most one expression per body, because attribute names + // are always unique within a body. + retExprs := make([]hcl.Expression, 0, len(bodies)) + for _, body := range bodies { + content, _, _ := body.PartialContent(schema) + if attr := content.Attributes[name]; attr != nil && attr.Expr != nil { + retExprs = append(retExprs, attr.Expr) + } + } + return nil, retExprs +} + +func traverseNestedBlockSingle(bodies []hcl.Body, typeName string) ([]hcl.Body, []hcl.Expression) { + if len(bodies) == 0 { + return nil, nil + } + + blocks := findBlocksInBodies(bodies, typeName, nil) + var retBodies []hcl.Body + var retExprs []hcl.Expression + for _, block := range blocks { + moreBodies, moreExprs := blockParts(block) + retBodies = append(retBodies, moreBodies...) + retExprs = append(retExprs, moreExprs...) + } + return retBodies, retExprs +} + +func traverseNestedBlockMap(bodies []hcl.Body, typeName string, key string) ([]hcl.Body, []hcl.Expression) { + if len(bodies) == 0 { + return nil, nil + } + + blocks := findBlocksInBodies(bodies, typeName, []string{"key"}) + var retBodies []hcl.Body + var retExprs []hcl.Expression + for _, block := range blocks { + switch block.Type { + case "dynamic": + // For dynamic blocks we allow the key to be chosen dynamically + // and so we'll just conservatively include all dynamic block + // bodies. However, we need to also look for references in some + // arguments of the dynamic block itself. + argExprs, contentBody := dynamicBlockParts(block.Body) + retExprs = append(retExprs, argExprs...) + if contentBody != nil { + retBodies = append(retBodies, contentBody) + } + case typeName: + if len(block.Labels) == 1 && block.Labels[0] == key && block.Body != nil { + retBodies = append(retBodies, block.Body) + } + } + } + return retBodies, retExprs +} + +func traverseNestedBlockList(bodies []hcl.Body, typeName string, idx int) ([]hcl.Body, []hcl.Expression) { + if len(bodies) == 0 { + return nil, nil + } + + schema := &hcl.BodySchema{ + Blocks: []hcl.BlockHeaderSchema{ + {Type: typeName, LabelNames: nil}, + {Type: "dynamic", LabelNames: []string{"type"}}, + }, + } + var retBodies []hcl.Body + var retExprs []hcl.Expression + for _, body := range bodies { + content, _, _ := body.PartialContent(schema) + blocks := content.Blocks + + // A tricky aspect of this scenario is that if there are any "dynamic" + // blocks then we can't statically predict how many concrete blocks they + // will generate, and so consequently we can't predict the indices of + // any statically-defined blocks that might appear after them. + firstDynamic := -1 // -1 means "no dynamic blocks" + for i, block := range blocks { + if block.Type == "dynamic" { + firstDynamic = i + break + } + } + + switch { + case firstDynamic >= 0 && idx >= firstDynamic: + // This is the unfortunate case where the selection could be + // any of the blocks from firstDynamic onwards, and so we + // need to conservatively include all of them in our result. + for _, block := range blocks[firstDynamic:] { + moreBodies, moreExprs := blockParts(block) + retBodies = append(retBodies, moreBodies...) + retExprs = append(retExprs, moreExprs...) + } + default: + // This is the happier case where we can select just a single + // static block based on idx. Note that this one is guaranteed + // to never be dynamic but we're using blockParts here just + // for consistency. + moreBodies, moreExprs := blockParts(blocks[idx]) + retBodies = append(retBodies, moreBodies...) + retExprs = append(retExprs, moreExprs...) + } + } + + return retBodies, retExprs +} + +func findBlocksInBodies(bodies []hcl.Body, typeName string, labelNames []string) []*hcl.Block { + // We need to look for both static blocks of the given type, and any + // dynamic blocks whose label gives the expected type name. + schema := &hcl.BodySchema{ + Blocks: []hcl.BlockHeaderSchema{ + {Type: typeName, LabelNames: labelNames}, + {Type: "dynamic", LabelNames: []string{"type"}}, + }, + } + var blocks []*hcl.Block + for _, body := range bodies { + // We ignore errors here because we'll just make a best effort to analyze + // whatever partial result HCL returns in that case. + content, _, _ := body.PartialContent(schema) + + for _, block := range content.Blocks { + switch block.Type { + case "dynamic": + if len(block.Labels) != 1 { // Invalid + continue + } + if block.Labels[0] == typeName { + blocks = append(blocks, block) + } + case typeName: + blocks = append(blocks, block) + } + } + } + + // NOTE: The caller still needs to check for dynamic vs. static in order + // to do further processing. The callers above all aim to encapsulate + // that. + return blocks +} + +func blockParts(block *hcl.Block) ([]hcl.Body, []hcl.Expression) { + switch block.Type { + case "dynamic": + exprs, contentBody := dynamicBlockParts(block.Body) + var bodies []hcl.Body + if contentBody != nil { + bodies = []hcl.Body{contentBody} + } + return bodies, exprs + default: + if block.Body == nil { + return nil, nil + } + return []hcl.Body{block.Body}, nil + } +} + +func dynamicBlockParts(body hcl.Body) ([]hcl.Expression, hcl.Body) { + if body == nil { + return nil, nil + } + + // This is a subset of the "dynamic" block schema defined by the HCL + // dynblock extension, covering only the two arguments that are allowed + // to be arbitrary expressions possibly referring elsewhere. + schema := &hcl.BodySchema{ + Attributes: []hcl.AttributeSchema{ + {Name: "for_each"}, + {Name: "labels"}, + }, + Blocks: []hcl.BlockHeaderSchema{ + {Type: "content"}, + }, + } + content, _, _ := body.PartialContent(schema) + var exprs []hcl.Expression + if len(content.Attributes) != 0 { + exprs = make([]hcl.Expression, 0, len(content.Attributes)) + } + for _, attr := range content.Attributes { + if attr.Expr != nil { + exprs = append(exprs, attr.Expr) + } + } + var contentBody hcl.Body + for _, block := range content.Blocks { + if block != nil && block.Type == "content" && block.Body != nil { + contentBody = block.Body + } + } + return exprs, contentBody +} diff --git a/internal/lang/globalref/analyzer_meta_references_shortcuts.go b/internal/lang/globalref/analyzer_meta_references_shortcuts.go new file mode 100644 index 000000000000..acfaa904c7b0 --- /dev/null +++ b/internal/lang/globalref/analyzer_meta_references_shortcuts.go @@ -0,0 +1,87 @@ +package globalref + +import ( + "fmt" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/lang" +) + +// ReferencesFromOutputValue returns all of the direct references from the +// value expression of the given output value. It doesn't include any indirect +// references. +func (a *Analyzer) ReferencesFromOutputValue(addr addrs.AbsOutputValue) []Reference { + mc := a.ModuleConfig(addr.Module) + if mc == nil { + return nil + } + oc := mc.Outputs[addr.OutputValue.Name] + if oc == nil { + return nil + } + refs, _ := lang.ReferencesInExpr(oc.Expr) + return absoluteRefs(addr.Module, refs) +} + +// ReferencesFromResource returns all of the direct references from the +// definition of the resource instance at the given address. It doesn't +// include any indirect references. +// +// The result doesn't directly include references from a "count" or "for_each" +// expression belonging to the associated resource, but it will include any +// references to count.index, each.key, or each.value that appear in the +// expressions which you can then, if you wish, resolve indirectly using +// Analyzer.MetaReferences. Alternatively, you can use +// Analyzer.ReferencesFromResourceRepetition to get that same result directly. +func (a *Analyzer) ReferencesFromResourceInstance(addr addrs.AbsResourceInstance) []Reference { + // Using MetaReferences for this is kinda overkill, since + // lang.ReferencesInBlock would be sufficient really, but + // this ensures we keep consistent in how we build the + // resulting absolute references and otherwise aside from + // some extra overhead this call boils down to a call to + // lang.ReferencesInBlock anyway. + fakeRef := Reference{ + ContainerAddr: addr.Module, + LocalRef: &addrs.Reference{ + Subject: addr.Resource, + }, + } + return a.MetaReferences(fakeRef) +} + +// ReferencesFromResourceRepetition returns the references from the given +// resource's for_each or count expression, or an empty set if the resource +// doesn't use repetition. +// +// This is a special-case sort of helper for use in situations where an +// expression might refer to count.index, each.key, or each.value, and thus +// we say that it depends indirectly on the repetition expression. +func (a *Analyzer) ReferencesFromResourceRepetition(addr addrs.AbsResource) []Reference { + modCfg := a.ModuleConfig(addr.Module) + if modCfg == nil { + return nil + } + rc := modCfg.ResourceByAddr(addr.Resource) + if rc == nil { + return nil + } + + // We're assuming here that resources can either have count or for_each, + // but never both, because that's a requirement enforced by the language + // decoder. But we'll assert it just to make sure we catch it if that + // changes for some reason. + if rc.ForEach != nil && rc.Count != nil { + panic(fmt.Sprintf("%s has both for_each and count", addr)) + } + + switch { + case rc.ForEach != nil: + refs, _ := lang.ReferencesInExpr(rc.ForEach) + return absoluteRefs(addr.Module, refs) + case rc.Count != nil: + refs, _ := lang.ReferencesInExpr(rc.Count) + return absoluteRefs(addr.Module, refs) + default: + return nil + } +} diff --git a/internal/lang/globalref/analyzer_meta_references_test.go b/internal/lang/globalref/analyzer_meta_references_test.go new file mode 100644 index 000000000000..340e8760f64f --- /dev/null +++ b/internal/lang/globalref/analyzer_meta_references_test.go @@ -0,0 +1,163 @@ +package globalref + +import ( + "sort" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform/internal/addrs" +) + +func TestAnalyzerMetaReferences(t *testing.T) { + tests := []struct { + InputContainer string + InputRef string + WantRefs []string + }{ + { + ``, + `local.a`, + nil, + }, + { + ``, + `test_thing.single`, + []string{ + "::local.a", + "::local.b", + }, + }, + { + ``, + `test_thing.single.string`, + []string{ + "::local.a", + }, + }, + { + ``, + `test_thing.for_each`, + []string{ + "::local.a", + "::test_thing.single.string", + }, + }, + { + ``, + `test_thing.for_each["whatever"]`, + []string{ + "::local.a", + "::test_thing.single.string", + }, + }, + { + ``, + `test_thing.for_each["whatever"].single`, + []string{ + "::test_thing.single.string", + }, + }, + { + ``, + `test_thing.for_each["whatever"].single.z`, + []string{ + "::test_thing.single.string", + }, + }, + { + ``, + `test_thing.count`, + []string{ + "::local.a", + }, + }, + { + ``, + `test_thing.count[0]`, + []string{ + "::local.a", + }, + }, + { + ``, + `module.single.a`, + []string{ + "module.single::test_thing.foo", + "module.single::var.a", + }, + }, + { + ``, + `module.for_each["whatever"].a`, + []string{ + `module.for_each["whatever"]::test_thing.foo`, + `module.for_each["whatever"]::var.a`, + }, + }, + { + ``, + `module.count[0].a`, + []string{ + `module.count[0]::test_thing.foo`, + `module.count[0]::var.a`, + }, + }, + { + `module.single`, + `var.a`, + []string{ + "::test_thing.single", + }, + }, + { + `module.single`, + `test_thing.foo`, + []string{ + "module.single::var.a", + }, + }, + } + + azr := testAnalyzer(t, "assorted") + + for _, test := range tests { + name := test.InputRef + if test.InputContainer != "" { + name = test.InputContainer + " " + test.InputRef + } + t.Run(name, func(t *testing.T) { + t.Logf("testing %s", name) + var containerAddr addrs.Targetable + containerAddr = addrs.RootModuleInstance + if test.InputContainer != "" { + moduleAddrTarget, diags := addrs.ParseTargetStr(test.InputContainer) + if diags.HasErrors() { + t.Fatalf("input module address is invalid: %s", diags.Err()) + } + containerAddr = moduleAddrTarget.Subject + } + + localRef, diags := addrs.ParseRefStr(test.InputRef) + if diags.HasErrors() { + t.Fatalf("input reference is invalid: %s", diags.Err()) + } + + ref := Reference{ + ContainerAddr: containerAddr, + LocalRef: localRef, + } + + refs := azr.MetaReferences(ref) + + want := test.WantRefs + var got []string + for _, ref := range refs { + got = append(got, ref.DebugString()) + } + sort.Strings(got) + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("wrong references\n%s", diff) + } + }) + } +} diff --git a/internal/lang/globalref/analyzer_test.go b/internal/lang/globalref/analyzer_test.go new file mode 100644 index 000000000000..0a66217e7d9a --- /dev/null +++ b/internal/lang/globalref/analyzer_test.go @@ -0,0 +1,98 @@ +package globalref + +import ( + "context" + "path/filepath" + "testing" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/configs/configload" + "github.com/hashicorp/terraform/internal/configs/configschema" + "github.com/hashicorp/terraform/internal/initwd" + "github.com/hashicorp/terraform/internal/providers" + "github.com/hashicorp/terraform/internal/registry" + "github.com/zclconf/go-cty/cty" +) + +func testAnalyzer(t *testing.T, fixtureName string) *Analyzer { + configDir := filepath.Join("testdata", fixtureName) + + loader, cleanup := configload.NewLoaderForTests(t) + defer cleanup() + + inst := initwd.NewModuleInstaller(loader.ModulesDir(), registry.NewClient(nil, nil)) + _, instDiags := inst.InstallModules(context.Background(), configDir, true, initwd.ModuleInstallHooksImpl{}) + if instDiags.HasErrors() { + t.Fatalf("unexpected module installation errors: %s", instDiags.Err().Error()) + } + if err := loader.RefreshModules(); err != nil { + t.Fatalf("failed to refresh modules after install: %s", err) + } + + cfg, loadDiags := loader.LoadConfig(configDir) + if loadDiags.HasErrors() { + t.Fatalf("unexpected configuration errors: %s", loadDiags.Error()) + } + + resourceTypeSchema := &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "string": {Type: cty.String, Optional: true}, + "number": {Type: cty.Number, Optional: true}, + "any": {Type: cty.DynamicPseudoType, Optional: true}, + }, + BlockTypes: map[string]*configschema.NestedBlock{ + "single": { + Nesting: configschema.NestingSingle, + Block: configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "z": {Type: cty.String, Optional: true}, + }, + }, + }, + "group": { + Nesting: configschema.NestingGroup, + Block: configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "z": {Type: cty.String, Optional: true}, + }, + }, + }, + "list": { + Nesting: configschema.NestingList, + Block: configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "z": {Type: cty.String, Optional: true}, + }, + }, + }, + "map": { + Nesting: configschema.NestingMap, + Block: configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "z": {Type: cty.String, Optional: true}, + }, + }, + }, + "set": { + Nesting: configschema.NestingSet, + Block: configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "z": {Type: cty.String, Optional: true}, + }, + }, + }, + }, + } + schemas := map[addrs.Provider]*providers.Schemas{ + addrs.MustParseProviderSourceString("hashicorp/test"): { + ResourceTypes: map[string]*configschema.Block{ + "test_thing": resourceTypeSchema, + }, + DataSources: map[string]*configschema.Block{ + "test_thing": resourceTypeSchema, + }, + }, + } + + return NewAnalyzer(cfg, schemas) +} diff --git a/internal/lang/globalref/doc.go b/internal/lang/globalref/doc.go new file mode 100644 index 000000000000..133a9e7f2ab2 --- /dev/null +++ b/internal/lang/globalref/doc.go @@ -0,0 +1,9 @@ +// Package globalref is home to some analysis algorithms that aim to answer +// questions about references between objects and object attributes across +// an entire configuration. +// +// This is a different problem than references within a single module, which +// we handle using some relatively simpler functions in the "lang" package +// in the parent directory. The globalref algorithms are often implemented +// in terms of those module-local reference-checking functions. +package globalref diff --git a/internal/lang/globalref/reference.go b/internal/lang/globalref/reference.go new file mode 100644 index 000000000000..71920c304651 --- /dev/null +++ b/internal/lang/globalref/reference.go @@ -0,0 +1,136 @@ +package globalref + +import ( + "fmt" + + "github.com/hashicorp/terraform/internal/addrs" +) + +// Reference combines an addrs.Reference with the address of the module +// instance or resource instance where it was found. +// +// Because of the design of the Terraform language, our main model of +// references only captures the module-local part of the reference and assumes +// that it's always clear from context which module a reference belongs to. +// That's not true for globalref because our whole purpose is to work across +// module boundaries, and so this package in particular has its own +// representation of references. +type Reference struct { + // ContainerAddr is always either addrs.ModuleInstance or + // addrs.AbsResourceInstance. The latter is required if LocalRef's + // subject is either an addrs.CountAddr or addrs.ForEachAddr, so + // we can know which resource's repetition expression it's + // referring to. + ContainerAddr addrs.Targetable + + // LocalRef is a reference that would be resolved in the context + // of the module instance or resource instance given in ContainerAddr. + LocalRef *addrs.Reference +} + +func absoluteRef(containerAddr addrs.Targetable, localRef *addrs.Reference) Reference { + ret := Reference{ + ContainerAddr: containerAddr, + LocalRef: localRef, + } + // For simplicity's sake, we always reduce the ContainerAddr to be + // just the module address unless it's a count.index, each.key, or + // each.value reference, because for anything else it's immaterial + // which resource it belongs to. + switch localRef.Subject.(type) { + case addrs.CountAttr, addrs.ForEachAttr: + // nothing to do + default: + ret.ContainerAddr = ret.ModuleAddr() + } + return ret +} + +func absoluteRefs(containerAddr addrs.Targetable, refs []*addrs.Reference) []Reference { + if len(refs) == 0 { + return nil + } + + ret := make([]Reference, len(refs)) + for i, ref := range refs { + ret[i] = absoluteRef(containerAddr, ref) + } + return ret +} + +// ModuleAddr returns the address of the module where the reference would +// be resolved. +// +// This is either ContainerAddr directly if it's already just a module +// instance, or the module instance part of it if it's a resource instance. +func (r Reference) ModuleAddr() addrs.ModuleInstance { + switch addr := r.ContainerAddr.(type) { + case addrs.ModuleInstance: + return addr + case addrs.AbsResourceInstance: + return addr.Module + default: + // NOTE: We're intentionally using only a subset of possible + // addrs.Targetable implementations here, so anything else + // is invalid. + panic(fmt.Sprintf("reference has invalid container address type %T", addr)) + } +} + +// ResourceAddr returns the address of the resource where the reference +// would be resolved, if there is one. +// +// Because not all references belong to resources, the extra boolean return +// value indicates whether the returned address is valid. +func (r Reference) ResourceAddr() (addrs.AbsResource, bool) { + switch addr := r.ContainerAddr.(type) { + case addrs.ModuleInstance: + return addrs.AbsResource{}, false + case addrs.AbsResourceInstance: + return addr.ContainingResource(), true + default: + // NOTE: We're intentionally using only a subset of possible + // addrs.Targetable implementations here, so anything else + // is invalid. + panic(fmt.Sprintf("reference has invalid container address type %T", addr)) + } +} + +// DebugString returns an internal (but still somewhat Terraform-language-like) +// compact string representation of the reciever, which isn't an address that +// any of our usual address parsers could accept but still captures the +// essence of what the reference represents. +// +// The DebugString result is not suitable for end-user-oriented messages. +// +// DebugString is also not suitable for use as a unique key for a reference, +// because it's ambiguous (between a no-key resource instance and a resource) +// and because it discards the source location information in the LocalRef. +func (r Reference) DebugString() string { + // As the doc comment insinuates, we don't have any real syntax for + // "absolute references": references are always local, and targets are + // always absolute but only include modules and resources. + return r.ContainerAddr.String() + "::" + r.LocalRef.DisplayString() +} + +// addrKey returns the referenceAddrKey value for the item that +// this reference refers to, discarding any source location information. +// +// See the referenceAddrKey doc comment for more information on what this +// is suitable for. +func (r Reference) addrKey() referenceAddrKey { + // This is a pretty arbitrary bunch of stuff. We include the type here + // just to differentiate between no-key resource instances and resources. + return referenceAddrKey(fmt.Sprintf("%s(%T)%s", r.ContainerAddr.String(), r.LocalRef.Subject, r.LocalRef.DisplayString())) +} + +// referenceAddrKey is a special string type which conventionally contains +// a unique string representation of the object that a reference refers to, +// although not of the reference itself because it ignores the information +// that would differentiate two different references to the same object. +// +// The actual content of a referenceAddrKey is arbitrary, for internal use +// only. and subject to change in future. We use a named type here only to +// make it easier to see when we're intentionally using strings to uniquely +// identify absolute reference addresses. +type referenceAddrKey string diff --git a/internal/lang/globalref/testdata/assorted/assorted-root.tf b/internal/lang/globalref/testdata/assorted/assorted-root.tf new file mode 100644 index 000000000000..d61297232760 --- /dev/null +++ b/internal/lang/globalref/testdata/assorted/assorted-root.tf @@ -0,0 +1,47 @@ + +locals { + a = "hello world" + b = 2 +} + +resource "test_thing" "single" { + string = local.a + number = local.b + +} + +resource "test_thing" "for_each" { + for_each = {"q": local.a} + + string = local.a + + single { + z = test_thing.single.string + } +} + +resource "test_thing" "count" { + for_each = length(local.a) + + string = local.a +} + +module "single" { + source = "./child" + + a = test_thing.single +} + +module "for_each" { + source = "./child" + for_each = {"q": test_thing.single} + + a = test_thing.single +} + +module "count" { + source = "./child" + count = length(test_thing.single.string) + + a = test_thing.single +} diff --git a/internal/lang/globalref/testdata/assorted/child/assorted-child.tf b/internal/lang/globalref/testdata/assorted/child/assorted-child.tf new file mode 100644 index 000000000000..e722fe8e1d10 --- /dev/null +++ b/internal/lang/globalref/testdata/assorted/child/assorted-child.tf @@ -0,0 +1,13 @@ +variable "a" { +} + +resource "test_thing" "foo" { + string = var.a +} + +output "a" { + value = { + a = var.a + foo = test_thing.foo + } +} diff --git a/internal/lang/globalref/testdata/contributing-resources/compute/contributing-resources-compute.tf b/internal/lang/globalref/testdata/contributing-resources/compute/contributing-resources-compute.tf new file mode 100644 index 000000000000..c83daffe8294 --- /dev/null +++ b/internal/lang/globalref/testdata/contributing-resources/compute/contributing-resources-compute.tf @@ -0,0 +1,51 @@ +variable "network" { + type = object({ + vpc_id = string + subnet_ids = map(string) + }) +} + +resource "test_thing" "controller" { + for_each = var.network.subnet_ids + + string = each.value +} + +locals { + workers = flatten([ + for k, id in var.network_subnet_ids : [ + for n in range(3) : { + unique_key = "${k}:${n}" + subnet_id = n + } + ] + ]) +} + +resource "test_thing" "worker" { + for_each = { for o in local.workers : o.unique_key => o.subnet_id } + + string = each.value + + dynamic "list" { + for_each = test_thing.controller + content { + z = list.value.string + } + } +} + +resource "test_thing" "load_balancer" { + string = var.network.vpc_id + + dynamic "list" { + for_each = test_thing.controller + content { + z = list.value.string + } + } +} + +output "compuneetees_api_url" { + value = test_thing.load_balancer.string +} diff --git a/internal/lang/globalref/testdata/contributing-resources/contributing-resources-root.tf b/internal/lang/globalref/testdata/contributing-resources/contributing-resources-root.tf new file mode 100644 index 000000000000..d6ec5c4815b5 --- /dev/null +++ b/internal/lang/globalref/testdata/contributing-resources/contributing-resources-root.tf @@ -0,0 +1,28 @@ +variable "environment" { + type = string +} + +data "test_thing" "environment" { + string = var.environment +} + +module "network" { + source = "./network" + + base_cidr_block = data.test_thing.environment.any.base_cidr_block + subnet_count = data.test_thing.environment.any.subnet_count +} + +module "compute" { + source = "./compute" + + network = module.network +} + +output "network" { + value = module.network +} + +output "c10s_url" { + value = module.compute.compuneetees_api_url +} diff --git a/internal/lang/globalref/testdata/contributing-resources/network/contributing-resources-network.tf b/internal/lang/globalref/testdata/contributing-resources/network/contributing-resources-network.tf new file mode 100644 index 000000000000..3a4c9dc1d39f --- /dev/null +++ b/internal/lang/globalref/testdata/contributing-resources/network/contributing-resources-network.tf @@ -0,0 +1,41 @@ +variable "base_cidr_block" { + type = string +} + +variable "subnet_count" { + type = number +} + +locals { + subnet_newbits = log(var.subnet_count, 2) + subnet_cidr_blocks = toset([ + for n in range(var.subnet_count) : cidrsubnet(var.base_cidr_block, local.subnet_newbits, n) + ]) +} + +resource "test_thing" "vpc" { + string = var.base_cidr_block +} + +resource "test_thing" "subnet" { + for_each = local.subnet_cidr_blocks + + string = test_thing.vpc.string + single { + z = each.value + } +} + +resource "test_thing" "route_table" { + for_each = local.subnet_cidr_blocks + + string = each.value +} + +output "vpc_id" { + value = test_thing.vpc.string +} + +output "subnet_ids" { + value = { for k, sn in test_thing.subnet : k => sn.string } +} From 6d33de8a9dd9a8d7955c5170c78f0cf90f28131d Mon Sep 17 00:00:00 2001 From: James Bardin Date: Sat, 29 Jan 2022 14:58:30 -0500 Subject: [PATCH 04/19] fixup analysis calls from rebase --- internal/plans/plan.go | 15 +++++++ internal/terraform/context_plan.go | 64 ++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+) diff --git a/internal/plans/plan.go b/internal/plans/plan.go index da824f24ab3e..0c90b3e7a2ce 100644 --- a/internal/plans/plan.go +++ b/internal/plans/plan.go @@ -36,6 +36,21 @@ type Plan struct { ForceReplaceAddrs []addrs.AbsResourceInstance Backend Backend + // RelevantResources is a set of resource addresses that are either + // directly affected by proposed changes or may have indirectly contributed + // to them via references in expressions. + // + // This is the result of a heuristic and is intended only as a hint to + // the UI layer in case it wants to emphasize or de-emphasize certain + // resources. Don't use this to drive any non-cosmetic behavior, especially + // including anything that would be subject to compatibility constraints. + // + // FIXME: This result currently doesn't survive round-tripping through a + // saved plan file, and so it'll be populated only for a freshly-created + // plan that has only existed in memory so far. When reloading a saved + // plan it will always appear as if there are no "relevant resources". + RelevantResources []addrs.AbsResource + // PrevRunState and PriorState both describe the situation that the plan // was derived from: // diff --git a/internal/terraform/context_plan.go b/internal/terraform/context_plan.go index 8366ffe90984..3d975b953447 100644 --- a/internal/terraform/context_plan.go +++ b/internal/terraform/context_plan.go @@ -12,6 +12,7 @@ import ( "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/instances" + "github.com/hashicorp/terraform/internal/lang/globalref" "github.com/hashicorp/terraform/internal/plans" "github.com/hashicorp/terraform/internal/refactoring" "github.com/hashicorp/terraform/internal/states" @@ -199,6 +200,7 @@ The -target option is not for routine use, and is provided only for exceptional panic("nil plan but no errors") } + plan.RelevantResources = c.relevantResourcesForPlan(config, plan) return plan, diags } @@ -285,6 +287,10 @@ func (c *Context) refreshOnlyPlan(config *configs.Config, prevRunState *states.S // objects that would need to be created. plan.PriorState.SyncWrapper().RemovePlannedResourceInstanceObjects() + // We don't populate RelevantResources for a refresh-only plan, because + // they never have any planned actions and so no resource can ever be + // "relevant" per the intended meaning of that field. + return plan, diags } @@ -357,6 +363,7 @@ func (c *Context) destroyPlan(config *configs.Config, prevRunState *states.State destroyPlan.PrevRunState = pendingPlan.PrevRunState } + destroyPlan.RelevantResources = c.relevantResourcesForPlan(config, destroyPlan) return destroyPlan, diags } @@ -736,3 +743,60 @@ func blockedMovesWarningDiag(results refactoring.MoveResults) tfdiags.Diagnostic ), ) } + +// ReferenceAnalyzer returns a globalref.Analyzer object to help with +// global analysis of references within the configuration that's attached +// to the receiving context. +func (c *Context) ReferenceAnalyzer(config *configs.Config, state *states.State) *globalref.Analyzer { + schemas, diags := c.Schemas(config, state) + if diags != nil { + // FIXME: we now have to deal with the diagnostics here + panic(diags.ErrWithWarnings().Error()) + } + + return globalref.NewAnalyzer(config, schemas.Providers) +} + +// relevantResourcesForPlan implements the heuristic we use to populate the +// RelevantResources field of returned plans. +func (c *Context) relevantResourcesForPlan(config *configs.Config, plan *plans.Plan) []addrs.AbsResource { + azr := c.ReferenceAnalyzer(config, plan.PriorState) + + // Our current strategy is that a resource is relevant if it either has + // a proposed change action directly, or if its attributes are used as + // any part of a resource that has a proposed change action. We don't + // consider individual changed attributes for now, because we can't + // really reason about any rules that providers might have about changes + // to one attribute implying a change to another. + + // We'll use the string representation of a resource address as a unique + // key so we can dedupe our results. + relevant := make(map[string]addrs.AbsResource) + + var refs []globalref.Reference + for _, change := range plan.Changes.Resources { + if change.Action == plans.NoOp { + continue + } + instAddr := change.Addr + addr := instAddr.ContainingResource() + relevant[addr.String()] = addr + + moreRefs := azr.ReferencesFromResourceInstance(instAddr) + refs = append(refs, moreRefs...) + } + + contributors := azr.ContributingResources(refs...) + for _, addr := range contributors { + relevant[addr.String()] = addr + } + + if len(relevant) == 0 { + return nil + } + ret := make([]addrs.AbsResource, 0, len(relevant)) + for _, addr := range relevant { + ret = append(ret, addr) + } + return ret +} From a151aaef05c6ea64d2199a8ed64a332cbe67927b Mon Sep 17 00:00:00 2001 From: James Bardin Date: Mon, 31 Jan 2022 17:35:13 -0500 Subject: [PATCH 05/19] only show drift when there are changes --- internal/command/views/plan.go | 75 +++++++++++++++++++--------------- 1 file changed, 43 insertions(+), 32 deletions(-) diff --git a/internal/command/views/plan.go b/internal/command/views/plan.go index b861ddcfdf3f..50b6977c13c1 100644 --- a/internal/command/views/plan.go +++ b/internal/command/views/plan.go @@ -96,37 +96,7 @@ func (v *PlanJSON) HelpPrompt() { // The plan renderer is used by the Operation view (for plan and apply // commands) and the Show view (for the show command). func renderPlan(plan *plans.Plan, schemas *terraform.Schemas, view *View) { - // In refresh-only mode, we show all resources marked as drifted, - // including those which have moved without other changes. In other plan - // modes, move-only changes will be rendered in the planned changes, so - // we skip them here. - var driftedResources []*plans.ResourceInstanceChangeSrc - if plan.UIMode == plans.RefreshOnlyMode { - driftedResources = plan.DriftedResources - } else { - for _, dr := range plan.DriftedResources { - if dr.Action != plans.NoOp { - driftedResources = append(driftedResources, dr) - } - } - } - - haveRefreshChanges := len(driftedResources) > 0 - if haveRefreshChanges { - renderChangesDetectedByRefresh(driftedResources, schemas, view) - switch plan.UIMode { - case plans.RefreshOnlyMode: - view.streams.Println(format.WordWrap( - "\nThis is a refresh-only plan, so Terraform will not take any actions to undo these. If you were expecting these changes then you can apply this plan to record the updated values in the Terraform state without changing any remote objects.", - view.outputColumns(), - )) - default: - view.streams.Println(format.WordWrap( - "\nUnless you have made equivalent changes to your configuration, or ignored the relevant attributes using ignore_changes, the following plan may include actions to undo or respond to these changes.", - view.outputColumns(), - )) - } - } + haveRefreshChanges := renderChangesDetectedByRefresh(plan, schemas, view) counts := map[plans.Action]int{} var rChanges []*plans.ResourceInstanceChangeSrc @@ -360,7 +330,47 @@ func renderPlan(plan *plans.Plan, schemas *terraform.Schemas, view *View) { // renderChangesDetectedByRefresh returns true if it produced at least one // line of output, and guarantees to always produce whole lines terminated // by newline characters. -func renderChangesDetectedByRefresh(drs []*plans.ResourceInstanceChangeSrc, schemas *terraform.Schemas, view *View) { +func renderChangesDetectedByRefresh(plan *plans.Plan, schemas *terraform.Schemas, view *View) (rendered bool) { + // In refresh-only mode, we show all resources marked as drifted, + // including those which have moved without other changes. In other plan + // modes, move-only changes will be rendered in the planned changes, so + // we skip them here. + var drs []*plans.ResourceInstanceChangeSrc + if plan.UIMode == plans.RefreshOnlyMode { + drs = plan.DriftedResources + } else { + for _, dr := range plan.DriftedResources { + if dr.Action != plans.NoOp { + drs = append(drs, dr) + } + } + } + + if len(drs) == 0 { + return false + } + + // In an empty plan, we don't show any outside changes, because nothing in + // the plan could have been affected by those changes. If a user wants to + // see all external changes, then a refresh-only plan should be executed + // instead. + if plan.Changes.Empty() && plan.UIMode != plans.RefreshOnlyMode { + return false + } + + switch plan.UIMode { + case plans.RefreshOnlyMode: + view.streams.Println(format.WordWrap( + "\nThis is a refresh-only plan, so Terraform will not take any actions to undo these. If you were expecting these changes then you can apply this plan to record the updated values in the Terraform state without changing any remote objects.", + view.outputColumns(), + )) + default: + view.streams.Println(format.WordWrap( + "\nUnless you have made equivalent changes to your configuration, or ignored the relevant attributes using ignore_changes, the following plan may include actions to undo or respond to these changes.", + view.outputColumns(), + )) + } + view.streams.Print( view.colorize.Color("[reset]\n[bold][cyan]Note:[reset][bold] Objects have changed outside of Terraform[reset]\n\n"), ) @@ -403,6 +413,7 @@ func renderChangesDetectedByRefresh(drs []*plans.ResourceInstanceChangeSrc, sche format.DiffLanguageDetectedDrift, )) } + return true } const planHeaderIntro = ` From c5c7045a8955c7892ebd4e3c4d5030d12268cb17 Mon Sep 17 00:00:00 2001 From: James Bardin Date: Wed, 2 Feb 2022 09:30:41 -0500 Subject: [PATCH 06/19] filter out non-relevant drift changes Only show drift changes which may have affected the plan output. --- internal/command/views/plan.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/internal/command/views/plan.go b/internal/command/views/plan.go index 50b6977c13c1..b70a2bd58b4a 100644 --- a/internal/command/views/plan.go +++ b/internal/command/views/plan.go @@ -331,6 +331,13 @@ func renderPlan(plan *plans.Plan, schemas *terraform.Schemas, view *View) { // line of output, and guarantees to always produce whole lines terminated // by newline characters. func renderChangesDetectedByRefresh(plan *plans.Plan, schemas *terraform.Schemas, view *View) (rendered bool) { + // If this is not a refresh-only plan, we will need to filter out any + // non-relevant changes to reduce plan output. + relevant := make(map[string]bool) + for _, r := range plan.RelevantResources { + relevant[r.String()] = true + } + // In refresh-only mode, we show all resources marked as drifted, // including those which have moved without other changes. In other plan // modes, move-only changes will be rendered in the planned changes, so @@ -340,7 +347,7 @@ func renderChangesDetectedByRefresh(plan *plans.Plan, schemas *terraform.Schemas drs = plan.DriftedResources } else { for _, dr := range plan.DriftedResources { - if dr.Action != plans.NoOp { + if dr.Action != plans.NoOp && relevant[dr.Addr.ContainingResource().String()] { drs = append(drs, dr) } } From a02d7cc96a069394272ea4faea5b30a4c51d1d52 Mon Sep 17 00:00:00 2001 From: James Bardin Date: Wed, 2 Feb 2022 13:10:39 -0500 Subject: [PATCH 07/19] account for diagnostics when fetching schemas Maybe we can ensure schemas are all loaded at this point, but we can tackle that later. --- internal/terraform/context_plan.go | 33 ++++++++++++++++++------------ 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/internal/terraform/context_plan.go b/internal/terraform/context_plan.go index 3d975b953447..dcfc8e254452 100644 --- a/internal/terraform/context_plan.go +++ b/internal/terraform/context_plan.go @@ -200,7 +200,10 @@ The -target option is not for routine use, and is provided only for exceptional panic("nil plan but no errors") } - plan.RelevantResources = c.relevantResourcesForPlan(config, plan) + relevantResources, rDiags := c.relevantResourcesForPlan(config, plan) + diags = diags.Append(rDiags) + + plan.RelevantResources = relevantResources return plan, diags } @@ -363,7 +366,10 @@ func (c *Context) destroyPlan(config *configs.Config, prevRunState *states.State destroyPlan.PrevRunState = pendingPlan.PrevRunState } - destroyPlan.RelevantResources = c.relevantResourcesForPlan(config, destroyPlan) + relevantResources, rDiags := c.relevantResourcesForPlan(config, destroyPlan) + diags = diags.Append(rDiags) + + destroyPlan.RelevantResources = relevantResources return destroyPlan, diags } @@ -744,23 +750,24 @@ func blockedMovesWarningDiag(results refactoring.MoveResults) tfdiags.Diagnostic ) } -// ReferenceAnalyzer returns a globalref.Analyzer object to help with +// referenceAnalyzer returns a globalref.Analyzer object to help with // global analysis of references within the configuration that's attached // to the receiving context. -func (c *Context) ReferenceAnalyzer(config *configs.Config, state *states.State) *globalref.Analyzer { +func (c *Context) referenceAnalyzer(config *configs.Config, state *states.State) (*globalref.Analyzer, tfdiags.Diagnostics) { schemas, diags := c.Schemas(config, state) - if diags != nil { - // FIXME: we now have to deal with the diagnostics here - panic(diags.ErrWithWarnings().Error()) + if diags.HasErrors() { + return nil, diags } - - return globalref.NewAnalyzer(config, schemas.Providers) + return globalref.NewAnalyzer(config, schemas.Providers), diags } // relevantResourcesForPlan implements the heuristic we use to populate the // RelevantResources field of returned plans. -func (c *Context) relevantResourcesForPlan(config *configs.Config, plan *plans.Plan) []addrs.AbsResource { - azr := c.ReferenceAnalyzer(config, plan.PriorState) +func (c *Context) relevantResourcesForPlan(config *configs.Config, plan *plans.Plan) ([]addrs.AbsResource, tfdiags.Diagnostics) { + azr, diags := c.referenceAnalyzer(config, plan.PriorState) + if diags.HasErrors() { + return nil, diags + } // Our current strategy is that a resource is relevant if it either has // a proposed change action directly, or if its attributes are used as @@ -792,11 +799,11 @@ func (c *Context) relevantResourcesForPlan(config *configs.Config, plan *plans.P } if len(relevant) == 0 { - return nil + return nil, diags } ret := make([]addrs.AbsResource, 0, len(relevant)) for _, addr := range relevant { ret = append(ret, addr) } - return ret + return ret, diags } From 4181b6e106377d15cb59fba710d98f6fcc59ba1e Mon Sep 17 00:00:00 2001 From: James Bardin Date: Thu, 3 Feb 2022 10:50:39 -0500 Subject: [PATCH 08/19] comment fix --- .../lang/globalref/analyzer_meta_references_shortcuts.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/lang/globalref/analyzer_meta_references_shortcuts.go b/internal/lang/globalref/analyzer_meta_references_shortcuts.go index acfaa904c7b0..580e99b360f5 100644 --- a/internal/lang/globalref/analyzer_meta_references_shortcuts.go +++ b/internal/lang/globalref/analyzer_meta_references_shortcuts.go @@ -23,9 +23,9 @@ func (a *Analyzer) ReferencesFromOutputValue(addr addrs.AbsOutputValue) []Refere return absoluteRefs(addr.Module, refs) } -// ReferencesFromResource returns all of the direct references from the -// definition of the resource instance at the given address. It doesn't -// include any indirect references. +// ReferencesFromResourceInstance returns all of the direct references from the +// definition of the resource instance at the given address. It doesn't include +// any indirect references. // // The result doesn't directly include references from a "count" or "for_each" // expression belonging to the associated resource, but it will include any From b3adcf06a1c41a332104d32f4766d093eb6cbc42 Mon Sep 17 00:00:00 2001 From: James Bardin Date: Thu, 3 Feb 2022 17:31:23 -0500 Subject: [PATCH 09/19] find meta references through locals --- .../globalref/analyzer_meta_references.go | 20 +++++++++++++++++++ .../analyzer_meta_references_test.go | 7 +++++++ .../testdata/assorted/assorted-root.tf | 2 +- .../compute/contributing-resources-compute.tf | 4 +++- 4 files changed, 31 insertions(+), 2 deletions(-) diff --git a/internal/lang/globalref/analyzer_meta_references.go b/internal/lang/globalref/analyzer_meta_references.go index b7c6db22ea1d..4646b55e5354 100644 --- a/internal/lang/globalref/analyzer_meta_references.go +++ b/internal/lang/globalref/analyzer_meta_references.go @@ -40,6 +40,8 @@ func (a *Analyzer) MetaReferences(ref Reference) []Reference { switch targetAddr := ref.LocalRef.Subject.(type) { case addrs.InputVariable: return a.metaReferencesInputVariable(moduleAddr, targetAddr, remaining) + case addrs.LocalValue: + return a.metaReferencesLocalValue(moduleAddr, targetAddr, remaining) case addrs.ModuleCallInstanceOutput: return a.metaReferencesOutputValue(moduleAddr, targetAddr, remaining) case addrs.ModuleCallInstance: @@ -136,6 +138,23 @@ func (a *Analyzer) metaReferencesOutputValue(callerAddr addrs.ModuleInstance, ad return absoluteRefs(calleeAddr, refs) } +func (a *Analyzer) metaReferencesLocalValue(moduleAddr addrs.ModuleInstance, addr addrs.LocalValue, remain hcl.Traversal) []Reference { + modCfg := a.ModuleConfig(moduleAddr) + if modCfg == nil { + return nil + } + + local := modCfg.Locals[addr.Name] + if local == nil { + return nil + } + + // We don't check for errors here because we'll make a best effort to + // analyze whatever partial result HCL is able to extract. + refs, _ := lang.ReferencesInExpr(local.Expr) + return absoluteRefs(moduleAddr, refs) +} + func (a *Analyzer) metaReferencesModuleCall(callerAddr addrs.ModuleInstance, addr addrs.ModuleCallInstance, remain hcl.Traversal) []Reference { calleeAddr := callerAddr.Child(addr.Call.Name, addr.Key) @@ -181,6 +200,7 @@ func (a *Analyzer) metaReferencesResourceInstance(moduleAddr addrs.ModuleInstanc if providerSchema == nil { return nil } + resourceTypeSchema, _ := providerSchema.SchemaForResourceAddr(addr.Resource) if resourceTypeSchema == nil { return nil diff --git a/internal/lang/globalref/analyzer_meta_references_test.go b/internal/lang/globalref/analyzer_meta_references_test.go index 340e8760f64f..c693890cf6d2 100644 --- a/internal/lang/globalref/analyzer_meta_references_test.go +++ b/internal/lang/globalref/analyzer_meta_references_test.go @@ -19,6 +19,13 @@ func TestAnalyzerMetaReferences(t *testing.T) { `local.a`, nil, }, + { + ``, + `local.single`, + []string{ + "::test_thing.single.id", + }, + }, { ``, `test_thing.single`, diff --git a/internal/lang/globalref/testdata/assorted/assorted-root.tf b/internal/lang/globalref/testdata/assorted/assorted-root.tf index d61297232760..09f730eee455 100644 --- a/internal/lang/globalref/testdata/assorted/assorted-root.tf +++ b/internal/lang/globalref/testdata/assorted/assorted-root.tf @@ -1,7 +1,7 @@ - locals { a = "hello world" b = 2 + single = test_thing.single.id } resource "test_thing" "single" { diff --git a/internal/lang/globalref/testdata/contributing-resources/compute/contributing-resources-compute.tf b/internal/lang/globalref/testdata/contributing-resources/compute/contributing-resources-compute.tf index c83daffe8294..a88ec466f988 100644 --- a/internal/lang/globalref/testdata/contributing-resources/compute/contributing-resources-compute.tf +++ b/internal/lang/globalref/testdata/contributing-resources/compute/contributing-resources-compute.tf @@ -20,6 +20,8 @@ locals { } ] ]) + + controllers = test_thing.controller } resource "test_thing" "worker" { @@ -39,7 +41,7 @@ resource "test_thing" "load_balancer" { string = var.network.vpc_id dynamic "list" { - for_each = test_thing.controller + for_each = local.controllers content { z = list.value.string } From f1e8aed48d747fc46cbde3618de647ec4258e22b Mon Sep 17 00:00:00 2001 From: James Bardin Date: Thu, 3 Feb 2022 18:01:32 -0500 Subject: [PATCH 10/19] output drift footer in the right place --- internal/command/views/plan.go | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/internal/command/views/plan.go b/internal/command/views/plan.go index b70a2bd58b4a..61168051fd9d 100644 --- a/internal/command/views/plan.go +++ b/internal/command/views/plan.go @@ -365,19 +365,6 @@ func renderChangesDetectedByRefresh(plan *plans.Plan, schemas *terraform.Schemas return false } - switch plan.UIMode { - case plans.RefreshOnlyMode: - view.streams.Println(format.WordWrap( - "\nThis is a refresh-only plan, so Terraform will not take any actions to undo these. If you were expecting these changes then you can apply this plan to record the updated values in the Terraform state without changing any remote objects.", - view.outputColumns(), - )) - default: - view.streams.Println(format.WordWrap( - "\nUnless you have made equivalent changes to your configuration, or ignored the relevant attributes using ignore_changes, the following plan may include actions to undo or respond to these changes.", - view.outputColumns(), - )) - } - view.streams.Print( view.colorize.Color("[reset]\n[bold][cyan]Note:[reset][bold] Objects have changed outside of Terraform[reset]\n\n"), ) @@ -420,6 +407,20 @@ func renderChangesDetectedByRefresh(plan *plans.Plan, schemas *terraform.Schemas format.DiffLanguageDetectedDrift, )) } + + switch plan.UIMode { + case plans.RefreshOnlyMode: + view.streams.Println(format.WordWrap( + "\nThis is a refresh-only plan, so Terraform will not take any actions to undo these. If you were expecting these changes then you can apply this plan to record the updated values in the Terraform state without changing any remote objects.", + view.outputColumns(), + )) + default: + view.streams.Println(format.WordWrap( + "\nUnless you have made equivalent changes to your configuration, or ignored the relevant attributes using ignore_changes, the following plan may include actions to undo or respond to these changes.", + view.outputColumns(), + )) + } + return true } From dc393cc6e0c65e6c5577e58e7da4815a90f04cf6 Mon Sep 17 00:00:00 2001 From: James Bardin Date: Fri, 4 Feb 2022 12:39:08 -0500 Subject: [PATCH 11/19] ResourceAddr may have resources in LocalRef --- internal/lang/globalref/reference.go | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/internal/lang/globalref/reference.go b/internal/lang/globalref/reference.go index 71920c304651..47f48c3c88bd 100644 --- a/internal/lang/globalref/reference.go +++ b/internal/lang/globalref/reference.go @@ -83,16 +83,28 @@ func (r Reference) ModuleAddr() addrs.ModuleInstance { // Because not all references belong to resources, the extra boolean return // value indicates whether the returned address is valid. func (r Reference) ResourceAddr() (addrs.AbsResource, bool) { - switch addr := r.ContainerAddr.(type) { + moduleInstance := addrs.RootModuleInstance + + switch container := r.ContainerAddr.(type) { case addrs.ModuleInstance: + moduleInstance = container + + switch ref := r.LocalRef.Subject.(type) { + case addrs.Resource: + return ref.Absolute(moduleInstance), true + case addrs.ResourceInstance: + return ref.ContainingResource().Absolute(moduleInstance), true + } + return addrs.AbsResource{}, false + case addrs.AbsResourceInstance: - return addr.ContainingResource(), true + return container.ContainingResource(), true default: // NOTE: We're intentionally using only a subset of possible // addrs.Targetable implementations here, so anything else // is invalid. - panic(fmt.Sprintf("reference has invalid container address type %T", addr)) + panic(fmt.Sprintf("reference has invalid container address type %T", container)) } } From 620caa983c280acfa705f125cdf7914099513074 Mon Sep 17 00:00:00 2001 From: James Bardin Date: Fri, 4 Feb 2022 12:58:07 -0500 Subject: [PATCH 12/19] globalref.Reference.ResourceAttr Convert a global reference to a specific AbsResource and attribute pair. The hcl.Traversal is converted to a cty.Path at this point because plan rendering is based on cty values. --- .../analyzer_contributing_resources_test.go | 94 +++++++++++++++++++ internal/lang/globalref/reference.go | 54 +++++++++++ 2 files changed, 148 insertions(+) diff --git a/internal/lang/globalref/analyzer_contributing_resources_test.go b/internal/lang/globalref/analyzer_contributing_resources_test.go index 038b3ed54399..79c441c4318f 100644 --- a/internal/lang/globalref/analyzer_contributing_resources_test.go +++ b/internal/lang/globalref/analyzer_contributing_resources_test.go @@ -1,6 +1,7 @@ package globalref import ( + "sort" "testing" "github.com/google/go-cmp/cmp" @@ -94,3 +95,96 @@ func TestAnalyzerContributingResources(t *testing.T) { }) } } + +func TestAnalyzerContributingResourceAttrs(t *testing.T) { + azr := testAnalyzer(t, "contributing-resources") + + tests := map[string]struct { + StartRefs func() []Reference + WantAttrs []string + }{ + "root output 'network'": { + func() []Reference { + return azr.ReferencesFromOutputValue( + addrs.OutputValue{Name: "network"}.Absolute(addrs.RootModuleInstance), + ) + }, + []string{ + `data.test_thing.environment.any.base_cidr_block`, + `data.test_thing.environment.any.subnet_count`, + `module.network.test_thing.subnet`, + `module.network.test_thing.vpc.string`, + }, + }, + "root output 'c10s_url'": { + func() []Reference { + return azr.ReferencesFromOutputValue( + addrs.OutputValue{Name: "c10s_url"}.Absolute(addrs.RootModuleInstance), + ) + }, + []string{ + `data.test_thing.environment.any.base_cidr_block`, + `data.test_thing.environment.any.subnet_count`, + `module.compute.test_thing.load_balancer.string`, + `module.network.test_thing.subnet`, + `module.network.test_thing.vpc.string`, + }, + }, + "module.compute.test_thing.load_balancer": { + func() []Reference { + return azr.ReferencesFromResourceInstance( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_thing", + Name: "load_balancer", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance.Child("compute", addrs.NoKey)), + ) + }, + []string{ + `data.test_thing.environment.any.base_cidr_block`, + `data.test_thing.environment.any.subnet_count`, + `module.compute.test_thing.controller`, + `module.network.test_thing.subnet`, + `module.network.test_thing.vpc.string`, + }, + }, + "data.test_thing.environment": { + func() []Reference { + return azr.ReferencesFromResourceInstance( + addrs.Resource{ + Mode: addrs.DataResourceMode, + Type: "test_thing", + Name: "environment", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + ) + }, + []string{ + // Nothing! This one only refers to an input variable. + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + startRefs := test.StartRefs() + refs := azr.ContributingResourceReferences(startRefs...) + + want := test.WantAttrs + got := make([]string, len(refs)) + for i, ref := range refs { + resAttr, ok := ref.ResourceAttr() + if !ok { + t.Errorf("%s is not a resource attr reference", resAttr.DebugString()) + continue + } + got[i] = resAttr.DebugString() + } + + sort.Strings(got) + + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("wrong addresses\n%s", diff) + } + }) + } +} diff --git a/internal/lang/globalref/reference.go b/internal/lang/globalref/reference.go index 47f48c3c88bd..4fc2f14a997b 100644 --- a/internal/lang/globalref/reference.go +++ b/internal/lang/globalref/reference.go @@ -3,7 +3,10 @@ package globalref import ( "fmt" + "github.com/hashicorp/hcl/v2" "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/tfdiags" + "github.com/zclconf/go-cty/cty" ) // Reference combines an addrs.Reference with the address of the module @@ -125,6 +128,45 @@ func (r Reference) DebugString() string { return r.ContainerAddr.String() + "::" + r.LocalRef.DisplayString() } +// ResourceAttr converts the Reference value to a more specific ResourceAttr +// value. +// +// Because not all references belong to resources, the extra boolean return +// value indicates whether the returned address is valid. +func (r Reference) ResourceAttr() (ResourceAttr, bool) { + res, ok := r.ResourceAddr() + if !ok { + return ResourceAttr{}, ok + } + + traversal := r.LocalRef.Remaining + + path := make(cty.Path, len(traversal)) + for si, step := range traversal { + switch ts := step.(type) { + case hcl.TraverseRoot: + path[si] = cty.GetAttrStep{ + Name: ts.Name, + } + case hcl.TraverseAttr: + path[si] = cty.GetAttrStep{ + Name: ts.Name, + } + case hcl.TraverseIndex: + path[si] = cty.IndexStep{ + Key: ts.Key, + } + default: + panic(fmt.Sprintf("unsupported traversal step %#v", step)) + } + } + + return ResourceAttr{ + Resource: res, + Attr: path, + }, true +} + // addrKey returns the referenceAddrKey value for the item that // this reference refers to, discarding any source location information. // @@ -146,3 +188,15 @@ func (r Reference) addrKey() referenceAddrKey { // make it easier to see when we're intentionally using strings to uniquely // identify absolute reference addresses. type referenceAddrKey string + +// ResourceAttr represents a global resource and attribute reference. +// This is a more specific form of the Reference type since it can only refer +// to a specific AbsResource and one of its attributes. +type ResourceAttr struct { + Resource addrs.AbsResource + Attr cty.Path +} + +func (r ResourceAttr) DebugString() string { + return r.Resource.String() + tfdiags.FormatCtyPath(r.Attr) +} From c02e8bc5b33bfed40fb8826ee52045da0619a208 Mon Sep 17 00:00:00 2001 From: James Bardin Date: Fri, 4 Feb 2022 13:34:43 -0500 Subject: [PATCH 13/19] change plan to store individual relevant attrs Storing individual contributing attributes will allow finer tuning of the plan rendering. add contributing to outputs --- internal/command/views/plan.go | 4 +-- internal/plans/plan.go | 14 +++----- internal/terraform/context_plan.go | 51 ++++++++++++------------------ 3 files changed, 28 insertions(+), 41 deletions(-) diff --git a/internal/command/views/plan.go b/internal/command/views/plan.go index 61168051fd9d..51226dbdfa0f 100644 --- a/internal/command/views/plan.go +++ b/internal/command/views/plan.go @@ -334,8 +334,8 @@ func renderChangesDetectedByRefresh(plan *plans.Plan, schemas *terraform.Schemas // If this is not a refresh-only plan, we will need to filter out any // non-relevant changes to reduce plan output. relevant := make(map[string]bool) - for _, r := range plan.RelevantResources { - relevant[r.String()] = true + for _, r := range plan.RelevantAttributes { + relevant[r.Resource.String()] = true } // In refresh-only mode, we show all resources marked as drifted, diff --git a/internal/plans/plan.go b/internal/plans/plan.go index 0c90b3e7a2ce..f7a2df1adc93 100644 --- a/internal/plans/plan.go +++ b/internal/plans/plan.go @@ -5,6 +5,7 @@ import ( "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/configs/configschema" + "github.com/hashicorp/terraform/internal/lang/globalref" "github.com/hashicorp/terraform/internal/states" "github.com/zclconf/go-cty/cty" ) @@ -36,20 +37,15 @@ type Plan struct { ForceReplaceAddrs []addrs.AbsResourceInstance Backend Backend - // RelevantResources is a set of resource addresses that are either - // directly affected by proposed changes or may have indirectly contributed - // to them via references in expressions. + // RelevantAttributes is a set of resource addresses and attributes that are + // either directly affected by proposed changes or may have indirectly + // contributed to them via references in expressions. // // This is the result of a heuristic and is intended only as a hint to // the UI layer in case it wants to emphasize or de-emphasize certain // resources. Don't use this to drive any non-cosmetic behavior, especially // including anything that would be subject to compatibility constraints. - // - // FIXME: This result currently doesn't survive round-tripping through a - // saved plan file, and so it'll be populated only for a freshly-created - // plan that has only existed in memory so far. When reloading a saved - // plan it will always appear as if there are no "relevant resources". - RelevantResources []addrs.AbsResource + RelevantAttributes []globalref.ResourceAttr // PrevRunState and PriorState both describe the situation that the plan // was derived from: diff --git a/internal/terraform/context_plan.go b/internal/terraform/context_plan.go index dcfc8e254452..e776e37e8ec4 100644 --- a/internal/terraform/context_plan.go +++ b/internal/terraform/context_plan.go @@ -200,10 +200,10 @@ The -target option is not for routine use, and is provided only for exceptional panic("nil plan but no errors") } - relevantResources, rDiags := c.relevantResourcesForPlan(config, plan) + relevantAttrs, rDiags := c.relevantResourceAttrsForPlan(config, plan) diags = diags.Append(rDiags) - plan.RelevantResources = relevantResources + plan.RelevantAttributes = relevantAttrs return plan, diags } @@ -366,10 +366,10 @@ func (c *Context) destroyPlan(config *configs.Config, prevRunState *states.State destroyPlan.PrevRunState = pendingPlan.PrevRunState } - relevantResources, rDiags := c.relevantResourcesForPlan(config, destroyPlan) + relevantAttrs, rDiags := c.relevantResourceAttrsForPlan(config, destroyPlan) diags = diags.Append(rDiags) - destroyPlan.RelevantResources = relevantResources + destroyPlan.RelevantAttributes = relevantAttrs return destroyPlan, diags } @@ -763,47 +763,38 @@ func (c *Context) referenceAnalyzer(config *configs.Config, state *states.State) // relevantResourcesForPlan implements the heuristic we use to populate the // RelevantResources field of returned plans. -func (c *Context) relevantResourcesForPlan(config *configs.Config, plan *plans.Plan) ([]addrs.AbsResource, tfdiags.Diagnostics) { +func (c *Context) relevantResourceAttrsForPlan(config *configs.Config, plan *plans.Plan) ([]globalref.ResourceAttr, tfdiags.Diagnostics) { azr, diags := c.referenceAnalyzer(config, plan.PriorState) if diags.HasErrors() { return nil, diags } - // Our current strategy is that a resource is relevant if it either has - // a proposed change action directly, or if its attributes are used as - // any part of a resource that has a proposed change action. We don't - // consider individual changed attributes for now, because we can't - // really reason about any rules that providers might have about changes - // to one attribute implying a change to another. - - // We'll use the string representation of a resource address as a unique - // key so we can dedupe our results. - relevant := make(map[string]addrs.AbsResource) - var refs []globalref.Reference for _, change := range plan.Changes.Resources { if change.Action == plans.NoOp { continue } - instAddr := change.Addr - addr := instAddr.ContainingResource() - relevant[addr.String()] = addr - moreRefs := azr.ReferencesFromResourceInstance(instAddr) + moreRefs := azr.ReferencesFromResourceInstance(change.Addr) refs = append(refs, moreRefs...) } - contributors := azr.ContributingResources(refs...) - for _, addr := range contributors { - relevant[addr.String()] = addr - } + for _, change := range plan.Changes.Outputs { + if change.Action == plans.NoOp { + continue + } - if len(relevant) == 0 { - return nil, diags + moreRefs := azr.ReferencesFromOutputValue(change.Addr) + refs = append(refs, moreRefs...) } - ret := make([]addrs.AbsResource, 0, len(relevant)) - for _, addr := range relevant { - ret = append(ret, addr) + + var contributors []globalref.ResourceAttr + + for _, ref := range azr.ContributingResourceReferences(refs...) { + if res, ok := ref.ResourceAttr(); ok { + contributors = append(contributors, res) + } } - return ret, diags + + return contributors, diags } From 0e7cec83db0e093faa0569444dea3bca6139a6ae Mon Sep 17 00:00:00 2001 From: James Bardin Date: Fri, 4 Feb 2022 14:24:36 -0500 Subject: [PATCH 14/19] decode change before creating diff This is functionally equivalent, but will allow us to filter the change values directly for reduced drift output. --- internal/command/format/diff.go | 21 ++------------------- internal/command/format/diff_test.go | 20 +++++--------------- internal/command/views/plan.go | 26 ++++++++++++++++++++++++-- internal/plans/changes.go | 4 ++++ 4 files changed, 35 insertions(+), 36 deletions(-) diff --git a/internal/command/format/diff.go b/internal/command/format/diff.go index 1c1da14f48a4..9f2da51b3a3b 100644 --- a/internal/command/format/diff.go +++ b/internal/command/format/diff.go @@ -45,7 +45,7 @@ const ( // If "color" is non-nil, it will be used to color the result. Otherwise, // no color codes will be included. func ResourceChange( - change *plans.ResourceInstanceChangeSrc, + change *plans.ResourceInstanceChange, schema *configschema.Block, color *colorstring.Colorize, language DiffLanguage, @@ -187,24 +187,7 @@ func ResourceChange( // structures. path := make(cty.Path, 0, 3) - changeV, err := change.Decode(schema.ImpliedType()) - if err != nil { - // Should never happen in here, since we've already been through - // loads of layers of encode/decode of the planned changes before now. - panic(fmt.Sprintf("failed to decode plan for %s while rendering diff: %s", addr, err)) - } - - // We currently have an opt-out that permits the legacy SDK to return values - // that defy our usual conventions around handling of nesting blocks. To - // avoid the rendering code from needing to handle all of these, we'll - // normalize first. - // (Ideally we'd do this as part of the SDK opt-out implementation in core, - // but we've added it here for now to reduce risk of unexpected impacts - // on other code in core.) - changeV.Change.Before = objchange.NormalizeObjectFromLegacySDK(changeV.Change.Before, schema) - changeV.Change.After = objchange.NormalizeObjectFromLegacySDK(changeV.Change.After, schema) - - result := p.writeBlockBodyDiff(schema, changeV.Before, changeV.After, 6, path) + result := p.writeBlockBodyDiff(schema, change.Before, change.After, 6, path) if result.bodyWritten { buf.WriteString("\n") buf.WriteString(strings.Repeat(" ", 4)) diff --git a/internal/command/format/diff_test.go b/internal/command/format/diff_test.go index 466e72fd85f0..19c57c82b008 100644 --- a/internal/command/format/diff_test.go +++ b/internal/command/format/diff_test.go @@ -4857,10 +4857,6 @@ func runTestCases(t *testing.T, testCases map[string]testCase) { case !beforeVal.IsKnown(): beforeVal = cty.UnknownVal(ty) // allow mistyped unknowns } - before, err := plans.NewDynamicValue(beforeVal, ty) - if err != nil { - t.Fatal(err) - } afterVal := tc.After switch { // Some fixups to make the test cases a little easier to write @@ -4869,10 +4865,6 @@ func runTestCases(t *testing.T, testCases map[string]testCase) { case !afterVal.IsKnown(): afterVal = cty.UnknownVal(ty) // allow mistyped unknowns } - after, err := plans.NewDynamicValue(afterVal, ty) - if err != nil { - t.Fatal(err) - } addr := addrs.Resource{ Mode: tc.Mode, @@ -4887,7 +4879,7 @@ func runTestCases(t *testing.T, testCases map[string]testCase) { prevRunAddr = addr } - change := &plans.ResourceInstanceChangeSrc{ + change := &plans.ResourceInstanceChange{ Addr: addr, PrevRunAddr: prevRunAddr, DeposedKey: tc.DeposedKey, @@ -4895,12 +4887,10 @@ func runTestCases(t *testing.T, testCases map[string]testCase) { Provider: addrs.NewDefaultProvider("test"), Module: addrs.RootModule, }, - ChangeSrc: plans.ChangeSrc{ - Action: tc.Action, - Before: before, - After: after, - BeforeValMarks: tc.BeforeValMarks, - AfterValMarks: tc.AfterValMarks, + Change: plans.Change{ + Action: tc.Action, + Before: beforeVal.MarkWithPaths(tc.BeforeValMarks), + After: afterVal.MarkWithPaths(tc.AfterValMarks), }, ActionReason: tc.ActionReason, RequiredReplace: tc.RequiredReplace, diff --git a/internal/command/views/plan.go b/internal/command/views/plan.go index 51226dbdfa0f..04355e7e7ca3 100644 --- a/internal/command/views/plan.go +++ b/internal/command/views/plan.go @@ -9,7 +9,9 @@ import ( "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/command/arguments" "github.com/hashicorp/terraform/internal/command/format" + "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/plans/objchange" "github.com/hashicorp/terraform/internal/terraform" "github.com/hashicorp/terraform/internal/tfdiags" ) @@ -274,7 +276,7 @@ func renderPlan(plan *plans.Plan, schemas *terraform.Schemas, view *View) { } view.streams.Println(format.ResourceChange( - rcs, + decodeChange(rcs, rSchema), rSchema, view.colorize, format.DiffLanguageProposedChange, @@ -401,7 +403,7 @@ func renderChangesDetectedByRefresh(plan *plans.Plan, schemas *terraform.Schemas } view.streams.Println(format.ResourceChange( - rcs, + decodeChange(rcs, rSchema), rSchema, view.colorize, format.DiffLanguageDetectedDrift, @@ -424,6 +426,26 @@ func renderChangesDetectedByRefresh(plan *plans.Plan, schemas *terraform.Schemas return true } +func decodeChange(change *plans.ResourceInstanceChangeSrc, schema *configschema.Block) *plans.ResourceInstanceChange { + changeV, err := change.Decode(schema.ImpliedType()) + if err != nil { + // Should never happen in here, since we've already been through + // loads of layers of encode/decode of the planned changes before now. + panic(fmt.Sprintf("failed to decode plan for %s while rendering diff: %s", change.Addr, err)) + } + + // We currently have an opt-out that permits the legacy SDK to return values + // that defy our usual conventions around handling of nesting blocks. To + // avoid the rendering code from needing to handle all of these, we'll + // normalize first. + // (Ideally we'd do this as part of the SDK opt-out implementation in core, + // but we've added it here for now to reduce risk of unexpected impacts + // on other code in core.) + changeV.Change.Before = objchange.NormalizeObjectFromLegacySDK(changeV.Change.Before, schema) + changeV.Change.After = objchange.NormalizeObjectFromLegacySDK(changeV.Change.After, schema) + return changeV +} + const planHeaderIntro = ` Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols: ` diff --git a/internal/plans/changes.go b/internal/plans/changes.go index 510eb2c7cfee..ae76a45dc09b 100644 --- a/internal/plans/changes.go +++ b/internal/plans/changes.go @@ -234,6 +234,10 @@ func (rc *ResourceInstanceChange) Encode(ty cty.Type) (*ResourceInstanceChangeSr }, err } +func (rc *ResourceInstanceChange) Moved() bool { + return !rc.Addr.Equal(rc.PrevRunAddr) +} + // Simplify will, where possible, produce a change with a simpler action than // the receiever given a flag indicating whether the caller is dealing with // a normal apply or a destroy. This flag deals with the fact that Terraform From 25f4c0d3dd006a71bcfc72257892898bc4a42cba Mon Sep 17 00:00:00 2001 From: James Bardin Date: Fri, 4 Feb 2022 16:39:31 -0500 Subject: [PATCH 15/19] filter attribute refresh changes from plan UI Filter the refresh changes from the normal plan UI at the attribute level. We do this by constructing fake plans.Change records for diff generation, reverting all attribute changes that do not match any of the plan's ContributingResourceReferences. --- internal/command/views/operation_test.go | 52 ++++++++++++- internal/command/views/plan.go | 97 ++++++++++++++++++++++-- 2 files changed, 141 insertions(+), 8 deletions(-) diff --git a/internal/command/views/operation_test.go b/internal/command/views/operation_test.go index aa86fe1445ac..eca2c3909162 100644 --- a/internal/command/views/operation_test.go +++ b/internal/command/views/operation_test.go @@ -8,6 +8,7 @@ import ( "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/command/arguments" + "github.com/hashicorp/terraform/internal/lang/globalref" "github.com/hashicorp/terraform/internal/plans" "github.com/hashicorp/terraform/internal/states" "github.com/hashicorp/terraform/internal/states/statefile" @@ -107,7 +108,7 @@ func TestOperation_planNoChanges(t *testing.T) { }, "No objects need to be destroyed.", }, - "drift detected in normal mode": { + "no drift detected in normal noop": { func(schemas *terraform.Schemas) *plans.Plan { addr := addrs.Resource{ Mode: addrs.ManagedResourceMode, @@ -146,7 +147,54 @@ func TestOperation_planNoChanges(t *testing.T) { DriftedResources: drs, } }, - "to update the Terraform state to match, create and apply a refresh-only plan", + "No changes", + }, + "drift detected in normal mode": { + func(schemas *terraform.Schemas) *plans.Plan { + addr := addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_resource", + Name: "somewhere", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance) + schema, _ := schemas.ResourceTypeConfig( + addrs.NewDefaultProvider("test"), + addr.Resource.Resource.Mode, + addr.Resource.Resource.Type, + ) + ty := schema.ImpliedType() + rc := &plans.ResourceInstanceChange{ + Addr: addr, + PrevRunAddr: addr, + ProviderAddr: addrs.RootModuleInstance.ProviderConfigDefault( + addrs.NewDefaultProvider("test"), + ), + Change: plans.Change{ + Action: plans.Update, + Before: cty.NullVal(ty), + After: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("1234"), + "foo": cty.StringVal("bar"), + }), + }, + } + rcs, err := rc.Encode(ty) + if err != nil { + panic(err) + } + drs := []*plans.ResourceInstanceChangeSrc{rcs} + changes := plans.NewChanges() + changes.Resources = drs + return &plans.Plan{ + UIMode: plans.NormalMode, + Changes: changes, + DriftedResources: drs, + RelevantAttributes: []globalref.ResourceAttr{{ + Resource: addr.ContainingResource(), + Attr: cty.GetAttrPath("id"), + }}, + } + }, + "Objects have changed outside of Terraform", }, "drift detected in refresh-only mode": { func(schemas *terraform.Schemas) *plans.Plan { diff --git a/internal/command/views/plan.go b/internal/command/views/plan.go index 04355e7e7ca3..11cfc872e59b 100644 --- a/internal/command/views/plan.go +++ b/internal/command/views/plan.go @@ -10,10 +10,12 @@ import ( "github.com/hashicorp/terraform/internal/command/arguments" "github.com/hashicorp/terraform/internal/command/format" "github.com/hashicorp/terraform/internal/configs/configschema" + "github.com/hashicorp/terraform/internal/lang/globalref" "github.com/hashicorp/terraform/internal/plans" "github.com/hashicorp/terraform/internal/plans/objchange" "github.com/hashicorp/terraform/internal/terraform" "github.com/hashicorp/terraform/internal/tfdiags" + "github.com/zclconf/go-cty/cty" ) // The Plan view is used for the plan command. @@ -340,16 +342,36 @@ func renderChangesDetectedByRefresh(plan *plans.Plan, schemas *terraform.Schemas relevant[r.Resource.String()] = true } + var changes []*plans.ResourceInstanceChange + for _, rcs := range plan.DriftedResources { + providerSchema := schemas.ProviderSchema(rcs.ProviderAddr.Provider) + if providerSchema == nil { + // Should never happen + view.streams.Printf("(schema missing for %s)\n\n", rcs.ProviderAddr) + continue + } + rSchema, _ := providerSchema.SchemaForResourceAddr(rcs.Addr.Resource.Resource) + if rSchema == nil { + // Should never happen + view.streams.Printf("(schema missing for %s)\n\n", rcs.Addr) + continue + } + + changes = append(changes, decodeChange(rcs, rSchema)) + } + // In refresh-only mode, we show all resources marked as drifted, // including those which have moved without other changes. In other plan // modes, move-only changes will be rendered in the planned changes, so // we skip them here. - var drs []*plans.ResourceInstanceChangeSrc + var drs []*plans.ResourceInstanceChange if plan.UIMode == plans.RefreshOnlyMode { - drs = plan.DriftedResources + drs = changes } else { - for _, dr := range plan.DriftedResources { - if dr.Action != plans.NoOp && relevant[dr.Addr.ContainingResource().String()] { + for _, dr := range changes { + change := filterRefreshChange(dr, plan.RelevantAttributes) + if change.Action != plans.NoOp { + dr.Change = change drs = append(drs, dr) } } @@ -371,7 +393,7 @@ func renderChangesDetectedByRefresh(plan *plans.Plan, schemas *terraform.Schemas view.colorize.Color("[reset]\n[bold][cyan]Note:[reset][bold] Objects have changed outside of Terraform[reset]\n\n"), ) view.streams.Print(format.WordWrap( - "Terraform detected the following changes made outside of Terraform since the last \"terraform apply\":\n\n", + "Terraform detected the following changes made outside of Terraform since the last \"terraform apply\" which may have affected this plan:\n\n", view.outputColumns(), )) @@ -403,7 +425,7 @@ func renderChangesDetectedByRefresh(plan *plans.Plan, schemas *terraform.Schemas } view.streams.Println(format.ResourceChange( - decodeChange(rcs, rSchema), + rcs, rSchema, view.colorize, format.DiffLanguageDetectedDrift, @@ -426,6 +448,69 @@ func renderChangesDetectedByRefresh(plan *plans.Plan, schemas *terraform.Schemas return true } +// Filter individual resource changes for display based on the attributes which +// may have contributed to the plan as a whole. In order to continue to use the +// existing diff renderer, we are going to create a fake change for display, +// only showing the attributes we're interested in. +// The resulting change will be a NoOp if it has nothing relevant to the plan. +func filterRefreshChange(change *plans.ResourceInstanceChange, contributing []globalref.ResourceAttr) plans.Change { + if change.Action == plans.NoOp { + return change.Change + } + + var relevantAttrs []cty.Path + resAddr := change.Addr.ContainingResource() + + for _, attr := range contributing { + if resAddr.Equal(attr.Resource) { + relevantAttrs = append(relevantAttrs, attr.Attr) + } + } + + // If no attributes are relevant in this resource, then we can turn this + // onto a NoOp change for display. + if len(relevantAttrs) == 0 { + return plans.Change{ + Action: plans.NoOp, + Before: change.Before, + After: change.Before, + } + } + + // We have some attributes in this change which were marked as relevant, so + // we are going to take the Before value and add in only those attributes + // from the After value which may have contributed to the plan. + before := change.Before + after, _ := cty.Transform(before, func(path cty.Path, v cty.Value) (cty.Value, error) { + for i, attrPath := range relevantAttrs { + // If the current value is null, but we are only a prefix of the + // affected path, we need to take the value from this point since + // we can't recurse any further into the object. This has the + // possibility of pulling in extra attribute changes we're not + // concerned with, but we can take this as "close enough" for now. + if (v.IsNull() && attrPath.HasPrefix(path)) || attrPath.Equals(path) { + // remove the path from further consideration + relevantAttrs = append(relevantAttrs[:i], relevantAttrs[i+1:]...) + + v, err := path.Apply(change.After) + return v, err + } + } + return v, nil + }) + + action := change.Action + if before.RawEquals(after) { + action = plans.NoOp + } + + return plans.Change{ + Action: action, + Before: before, + After: after, + } +} + func decodeChange(change *plans.ResourceInstanceChangeSrc, schema *configschema.Block) *plans.ResourceInstanceChange { changeV, err := change.Decode(schema.ImpliedType()) if err != nil { From e2b74247f2afc18f93d8250602536a1be4b15ebd Mon Sep 17 00:00:00 2001 From: James Bardin Date: Mon, 7 Feb 2022 09:39:14 -0500 Subject: [PATCH 16/19] track contributing instances Track individual instance drift rather than whole resources which contributed to the plan. This will allow the output to be more precise, and we can still use NoKey instances as a proxy for containing resources when needed. --- internal/command/views/operation_test.go | 2 +- internal/command/views/plan.go | 2 +- .../globalref/analyzer_meta_references.go | 4 ++-- internal/lang/globalref/reference.go | 20 +++++++++---------- internal/plans/plan.go | 6 +++--- 5 files changed, 16 insertions(+), 18 deletions(-) diff --git a/internal/command/views/operation_test.go b/internal/command/views/operation_test.go index eca2c3909162..9b0323a27684 100644 --- a/internal/command/views/operation_test.go +++ b/internal/command/views/operation_test.go @@ -189,7 +189,7 @@ func TestOperation_planNoChanges(t *testing.T) { Changes: changes, DriftedResources: drs, RelevantAttributes: []globalref.ResourceAttr{{ - Resource: addr.ContainingResource(), + Resource: addr, Attr: cty.GetAttrPath("id"), }}, } diff --git a/internal/command/views/plan.go b/internal/command/views/plan.go index 11cfc872e59b..c3c1283cfc80 100644 --- a/internal/command/views/plan.go +++ b/internal/command/views/plan.go @@ -459,7 +459,7 @@ func filterRefreshChange(change *plans.ResourceInstanceChange, contributing []gl } var relevantAttrs []cty.Path - resAddr := change.Addr.ContainingResource() + resAddr := change.Addr for _, attr := range contributing { if resAddr.Equal(attr.Resource) { diff --git a/internal/lang/globalref/analyzer_meta_references.go b/internal/lang/globalref/analyzer_meta_references.go index 4646b55e5354..9a2bb89920bf 100644 --- a/internal/lang/globalref/analyzer_meta_references.go +++ b/internal/lang/globalref/analyzer_meta_references.go @@ -56,8 +56,8 @@ func (a *Analyzer) MetaReferences(ref Reference) []Reference { // this latter to distingish these two cases better. return a.metaReferencesModuleCall(moduleAddr, targetAddr.Instance(addrs.NoKey), remaining) case addrs.CountAttr, addrs.ForEachAttr: - if resourceAddr, ok := ref.ResourceAddr(); ok { - return a.metaReferencesCountOrEach(resourceAddr) + if resourceAddr, ok := ref.ResourceInstance(); ok { + return a.metaReferencesCountOrEach(resourceAddr.ContainingResource()) } return nil case addrs.ResourceInstance: diff --git a/internal/lang/globalref/reference.go b/internal/lang/globalref/reference.go index 4fc2f14a997b..d47cecfa70c7 100644 --- a/internal/lang/globalref/reference.go +++ b/internal/lang/globalref/reference.go @@ -80,29 +80,27 @@ func (r Reference) ModuleAddr() addrs.ModuleInstance { } } -// ResourceAddr returns the address of the resource where the reference +// ResourceInstance returns the address of the resource where the reference // would be resolved, if there is one. // // Because not all references belong to resources, the extra boolean return // value indicates whether the returned address is valid. -func (r Reference) ResourceAddr() (addrs.AbsResource, bool) { - moduleInstance := addrs.RootModuleInstance - +func (r Reference) ResourceInstance() (addrs.AbsResourceInstance, bool) { switch container := r.ContainerAddr.(type) { case addrs.ModuleInstance: - moduleInstance = container + moduleInstance := container switch ref := r.LocalRef.Subject.(type) { case addrs.Resource: - return ref.Absolute(moduleInstance), true + return ref.Instance(addrs.NoKey).Absolute(moduleInstance), true case addrs.ResourceInstance: - return ref.ContainingResource().Absolute(moduleInstance), true + return ref.Absolute(moduleInstance), true } - return addrs.AbsResource{}, false + return addrs.AbsResourceInstance{}, false case addrs.AbsResourceInstance: - return container.ContainingResource(), true + return container, true default: // NOTE: We're intentionally using only a subset of possible // addrs.Targetable implementations here, so anything else @@ -134,7 +132,7 @@ func (r Reference) DebugString() string { // Because not all references belong to resources, the extra boolean return // value indicates whether the returned address is valid. func (r Reference) ResourceAttr() (ResourceAttr, bool) { - res, ok := r.ResourceAddr() + res, ok := r.ResourceInstance() if !ok { return ResourceAttr{}, ok } @@ -193,7 +191,7 @@ type referenceAddrKey string // This is a more specific form of the Reference type since it can only refer // to a specific AbsResource and one of its attributes. type ResourceAttr struct { - Resource addrs.AbsResource + Resource addrs.AbsResourceInstance Attr cty.Path } diff --git a/internal/plans/plan.go b/internal/plans/plan.go index f7a2df1adc93..766da885a17b 100644 --- a/internal/plans/plan.go +++ b/internal/plans/plan.go @@ -37,9 +37,9 @@ type Plan struct { ForceReplaceAddrs []addrs.AbsResourceInstance Backend Backend - // RelevantAttributes is a set of resource addresses and attributes that are - // either directly affected by proposed changes or may have indirectly - // contributed to them via references in expressions. + // RelevantAttributes is a set of resource instance addresses and + // attributes that are either directly affected by proposed changes or may + // have indirectly contributed to them via references in expressions. // // This is the result of a heuristic and is intended only as a hint to // the UI layer in case it wants to emphasize or de-emphasize certain From 773f5b02ec17cc83ae42c63cce772da8ac15e7ae Mon Sep 17 00:00:00 2001 From: James Bardin Date: Mon, 7 Feb 2022 11:03:52 -0500 Subject: [PATCH 17/19] round-trip relevant attributes through planfile --- .../plans/internal/planproto/planfile.pb.go | 342 +++++++++++------- .../plans/internal/planproto/planfile.proto | 9 + internal/plans/planfile/tfplan.go | 50 +++ internal/plans/planfile/tfplan_test.go | 11 + 4 files changed, 286 insertions(+), 126 deletions(-) diff --git a/internal/plans/internal/planproto/planfile.pb.go b/internal/plans/internal/planproto/planfile.pb.go index 756cfdabeb06..d1bb3fdb63a4 100644 --- a/internal/plans/internal/planproto/planfile.pb.go +++ b/internal/plans/internal/planproto/planfile.pb.go @@ -255,6 +255,9 @@ type Plan struct { // Backend is a description of the backend configuration and other related // settings at the time the plan was created. Backend *Backend `protobuf:"bytes,13,opt,name=backend,proto3" json:"backend,omitempty"` + // RelevantAttributes lists individual resource attributes from + // ResourceDrift which may have contributed to the plan changes. + RelevantAttributes []*PlanResourceAttr `protobuf:"bytes,15,rep,name=relevant_attributes,json=relevantAttributes,proto3" json:"relevant_attributes,omitempty"` } func (x *Plan) Reset() { @@ -359,6 +362,13 @@ func (x *Plan) GetBackend() *Backend { return nil } +func (x *Plan) GetRelevantAttributes() []*PlanResourceAttr { + if x != nil { + return x.RelevantAttributes + } + return nil +} + // Backend is a description of backend configuration and other related settings. type Backend struct { state protoimpl.MessageState @@ -820,6 +830,61 @@ func (x *Path) GetSteps() []*Path_Step { return nil } +type PlanResourceAttr struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Resource string `protobuf:"bytes,1,opt,name=resource,proto3" json:"resource,omitempty"` + Attr *Path `protobuf:"bytes,2,opt,name=attr,proto3" json:"attr,omitempty"` +} + +func (x *PlanResourceAttr) Reset() { + *x = PlanResourceAttr{} + if protoimpl.UnsafeEnabled { + mi := &file_planfile_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *PlanResourceAttr) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PlanResourceAttr) ProtoMessage() {} + +func (x *PlanResourceAttr) ProtoReflect() protoreflect.Message { + mi := &file_planfile_proto_msgTypes[8] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PlanResourceAttr.ProtoReflect.Descriptor instead. +func (*PlanResourceAttr) Descriptor() ([]byte, []int) { + return file_planfile_proto_rawDescGZIP(), []int{0, 1} +} + +func (x *PlanResourceAttr) GetResource() string { + if x != nil { + return x.Resource + } + return "" +} + +func (x *PlanResourceAttr) GetAttr() *Path { + if x != nil { + return x.Attr + } + return nil +} + type Path_Step struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -834,7 +899,7 @@ type Path_Step struct { func (x *Path_Step) Reset() { *x = Path_Step{} if protoimpl.UnsafeEnabled { - mi := &file_planfile_proto_msgTypes[8] + mi := &file_planfile_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -847,7 +912,7 @@ func (x *Path_Step) String() string { func (*Path_Step) ProtoMessage() {} func (x *Path_Step) ProtoReflect() protoreflect.Message { - mi := &file_planfile_proto_msgTypes[8] + mi := &file_planfile_proto_msgTypes[9] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -908,7 +973,7 @@ var File_planfile_proto protoreflect.FileDescriptor var file_planfile_proto_rawDesc = []byte{ 0x0a, 0x0e, 0x70, 0x6c, 0x61, 0x6e, 0x66, 0x69, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x12, 0x06, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x22, 0xd0, 0x04, 0x0a, 0x04, 0x50, 0x6c, 0x61, + 0x12, 0x06, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x22, 0xec, 0x05, 0x0a, 0x04, 0x50, 0x6c, 0x61, 0x6e, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x25, 0x0a, 0x07, 0x75, 0x69, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x18, 0x11, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x0c, 0x2e, 0x74, @@ -940,108 +1005,118 @@ var file_planfile_proto_rawDesc = []byte{ 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x29, 0x0a, 0x07, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x42, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x52, 0x07, - 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x1a, 0x52, 0x0a, 0x0e, 0x56, 0x61, 0x72, 0x69, 0x61, - 0x62, 0x6c, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x2a, 0x0a, 0x05, 0x76, - 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x74, 0x66, 0x70, - 0x6c, 0x61, 0x6e, 0x2e, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, - 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x69, 0x0a, 0x07, 0x42, - 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x2c, 0x0a, 0x06, 0x63, 0x6f, - 0x6e, 0x66, 0x69, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x74, 0x66, 0x70, - 0x6c, 0x61, 0x6e, 0x2e, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, - 0x52, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1c, 0x0a, 0x09, 0x77, 0x6f, 0x72, 0x6b, - 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x77, 0x6f, 0x72, - 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x22, 0xe4, 0x01, 0x0a, 0x06, 0x43, 0x68, 0x61, 0x6e, 0x67, - 0x65, 0x12, 0x26, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x0e, 0x32, 0x0e, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x41, 0x63, 0x74, 0x69, 0x6f, - 0x6e, 0x52, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x2c, 0x0a, 0x06, 0x76, 0x61, 0x6c, - 0x75, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x74, 0x66, 0x70, 0x6c, - 0x61, 0x6e, 0x2e, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, - 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x12, 0x42, 0x0a, 0x16, 0x62, 0x65, 0x66, 0x6f, 0x72, - 0x65, 0x5f, 0x73, 0x65, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x76, 0x65, 0x5f, 0x70, 0x61, 0x74, 0x68, - 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0c, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, - 0x2e, 0x50, 0x61, 0x74, 0x68, 0x52, 0x14, 0x62, 0x65, 0x66, 0x6f, 0x72, 0x65, 0x53, 0x65, 0x6e, - 0x73, 0x69, 0x74, 0x69, 0x76, 0x65, 0x50, 0x61, 0x74, 0x68, 0x73, 0x12, 0x40, 0x0a, 0x15, 0x61, - 0x66, 0x74, 0x65, 0x72, 0x5f, 0x73, 0x65, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x76, 0x65, 0x5f, 0x70, - 0x61, 0x74, 0x68, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0c, 0x2e, 0x74, 0x66, 0x70, - 0x6c, 0x61, 0x6e, 0x2e, 0x50, 0x61, 0x74, 0x68, 0x52, 0x13, 0x61, 0x66, 0x74, 0x65, 0x72, 0x53, - 0x65, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x76, 0x65, 0x50, 0x61, 0x74, 0x68, 0x73, 0x22, 0xd3, 0x02, - 0x0a, 0x16, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, - 0x63, 0x65, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x61, 0x64, 0x64, 0x72, - 0x18, 0x0d, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x61, 0x64, 0x64, 0x72, 0x12, 0x22, 0x0a, 0x0d, - 0x70, 0x72, 0x65, 0x76, 0x5f, 0x72, 0x75, 0x6e, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x18, 0x0e, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x0b, 0x70, 0x72, 0x65, 0x76, 0x52, 0x75, 0x6e, 0x41, 0x64, 0x64, 0x72, - 0x12, 0x1f, 0x0a, 0x0b, 0x64, 0x65, 0x70, 0x6f, 0x73, 0x65, 0x64, 0x5f, 0x6b, 0x65, 0x79, 0x18, - 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x64, 0x65, 0x70, 0x6f, 0x73, 0x65, 0x64, 0x4b, 0x65, - 0x79, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x18, 0x08, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, 0x26, 0x0a, - 0x06, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, - 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x52, 0x06, 0x63, - 0x68, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, - 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x12, - 0x37, 0x0a, 0x10, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x5f, 0x72, 0x65, 0x70, 0x6c, - 0x61, 0x63, 0x65, 0x18, 0x0b, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0c, 0x2e, 0x74, 0x66, 0x70, 0x6c, - 0x61, 0x6e, 0x2e, 0x50, 0x61, 0x74, 0x68, 0x52, 0x0f, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, - 0x64, 0x52, 0x65, 0x70, 0x6c, 0x61, 0x63, 0x65, 0x12, 0x49, 0x0a, 0x0d, 0x61, 0x63, 0x74, 0x69, - 0x6f, 0x6e, 0x5f, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0e, 0x32, - 0x24, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, - 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x52, 0x0c, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x61, - 0x73, 0x6f, 0x6e, 0x22, 0x68, 0x0a, 0x0c, 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x43, 0x68, 0x61, - 0x6e, 0x67, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x26, 0x0a, 0x06, 0x63, 0x68, 0x61, 0x6e, 0x67, - 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, - 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x52, 0x06, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x12, - 0x1c, 0x0a, 0x09, 0x73, 0x65, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x76, 0x65, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x08, 0x52, 0x09, 0x73, 0x65, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x76, 0x65, 0x22, 0x28, 0x0a, - 0x0c, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x18, 0x0a, - 0x07, 0x6d, 0x73, 0x67, 0x70, 0x61, 0x63, 0x6b, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, - 0x6d, 0x73, 0x67, 0x70, 0x61, 0x63, 0x6b, 0x22, 0xa5, 0x01, 0x0a, 0x04, 0x50, 0x61, 0x74, 0x68, - 0x12, 0x27, 0x0a, 0x05, 0x73, 0x74, 0x65, 0x70, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, - 0x11, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x50, 0x61, 0x74, 0x68, 0x2e, 0x53, 0x74, - 0x65, 0x70, 0x52, 0x05, 0x73, 0x74, 0x65, 0x70, 0x73, 0x1a, 0x74, 0x0a, 0x04, 0x53, 0x74, 0x65, - 0x70, 0x12, 0x27, 0x0a, 0x0e, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x5f, 0x6e, - 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x0d, 0x61, 0x74, 0x74, - 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x37, 0x0a, 0x0b, 0x65, 0x6c, - 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x14, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, - 0x56, 0x61, 0x6c, 0x75, 0x65, 0x48, 0x00, 0x52, 0x0a, 0x65, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, - 0x4b, 0x65, 0x79, 0x42, 0x0a, 0x0a, 0x08, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x2a, - 0x31, 0x0a, 0x04, 0x4d, 0x6f, 0x64, 0x65, 0x12, 0x0a, 0x0a, 0x06, 0x4e, 0x4f, 0x52, 0x4d, 0x41, - 0x4c, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x44, 0x45, 0x53, 0x54, 0x52, 0x4f, 0x59, 0x10, 0x01, - 0x12, 0x10, 0x0a, 0x0c, 0x52, 0x45, 0x46, 0x52, 0x45, 0x53, 0x48, 0x5f, 0x4f, 0x4e, 0x4c, 0x59, - 0x10, 0x02, 0x2a, 0x70, 0x0a, 0x06, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x08, 0x0a, 0x04, - 0x4e, 0x4f, 0x4f, 0x50, 0x10, 0x00, 0x12, 0x0a, 0x0a, 0x06, 0x43, 0x52, 0x45, 0x41, 0x54, 0x45, - 0x10, 0x01, 0x12, 0x08, 0x0a, 0x04, 0x52, 0x45, 0x41, 0x44, 0x10, 0x02, 0x12, 0x0a, 0x0a, 0x06, - 0x55, 0x50, 0x44, 0x41, 0x54, 0x45, 0x10, 0x03, 0x12, 0x0a, 0x0a, 0x06, 0x44, 0x45, 0x4c, 0x45, - 0x54, 0x45, 0x10, 0x05, 0x12, 0x16, 0x0a, 0x12, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x5f, 0x54, - 0x48, 0x45, 0x4e, 0x5f, 0x43, 0x52, 0x45, 0x41, 0x54, 0x45, 0x10, 0x06, 0x12, 0x16, 0x0a, 0x12, - 0x43, 0x52, 0x45, 0x41, 0x54, 0x45, 0x5f, 0x54, 0x48, 0x45, 0x4e, 0x5f, 0x44, 0x45, 0x4c, 0x45, - 0x54, 0x45, 0x10, 0x07, 0x2a, 0xa7, 0x02, 0x0a, 0x1c, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, - 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x12, 0x08, 0x0a, 0x04, 0x4e, 0x4f, 0x4e, 0x45, 0x10, 0x00, 0x12, - 0x1b, 0x0a, 0x17, 0x52, 0x45, 0x50, 0x4c, 0x41, 0x43, 0x45, 0x5f, 0x42, 0x45, 0x43, 0x41, 0x55, - 0x53, 0x45, 0x5f, 0x54, 0x41, 0x49, 0x4e, 0x54, 0x45, 0x44, 0x10, 0x01, 0x12, 0x16, 0x0a, 0x12, - 0x52, 0x45, 0x50, 0x4c, 0x41, 0x43, 0x45, 0x5f, 0x42, 0x59, 0x5f, 0x52, 0x45, 0x51, 0x55, 0x45, - 0x53, 0x54, 0x10, 0x02, 0x12, 0x21, 0x0a, 0x1d, 0x52, 0x45, 0x50, 0x4c, 0x41, 0x43, 0x45, 0x5f, - 0x42, 0x45, 0x43, 0x41, 0x55, 0x53, 0x45, 0x5f, 0x43, 0x41, 0x4e, 0x4e, 0x4f, 0x54, 0x5f, 0x55, - 0x50, 0x44, 0x41, 0x54, 0x45, 0x10, 0x03, 0x12, 0x25, 0x0a, 0x21, 0x44, 0x45, 0x4c, 0x45, 0x54, - 0x45, 0x5f, 0x42, 0x45, 0x43, 0x41, 0x55, 0x53, 0x45, 0x5f, 0x4e, 0x4f, 0x5f, 0x52, 0x45, 0x53, - 0x4f, 0x55, 0x52, 0x43, 0x45, 0x5f, 0x43, 0x4f, 0x4e, 0x46, 0x49, 0x47, 0x10, 0x04, 0x12, 0x23, - 0x0a, 0x1f, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x5f, 0x42, 0x45, 0x43, 0x41, 0x55, 0x53, 0x45, - 0x5f, 0x57, 0x52, 0x4f, 0x4e, 0x47, 0x5f, 0x52, 0x45, 0x50, 0x45, 0x54, 0x49, 0x54, 0x49, 0x4f, - 0x4e, 0x10, 0x05, 0x12, 0x1e, 0x0a, 0x1a, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x5f, 0x42, 0x45, - 0x43, 0x41, 0x55, 0x53, 0x45, 0x5f, 0x43, 0x4f, 0x55, 0x4e, 0x54, 0x5f, 0x49, 0x4e, 0x44, 0x45, - 0x58, 0x10, 0x06, 0x12, 0x1b, 0x0a, 0x17, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x5f, 0x42, 0x45, - 0x43, 0x41, 0x55, 0x53, 0x45, 0x5f, 0x45, 0x41, 0x43, 0x48, 0x5f, 0x4b, 0x45, 0x59, 0x10, 0x07, - 0x12, 0x1c, 0x0a, 0x18, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x5f, 0x42, 0x45, 0x43, 0x41, 0x55, - 0x53, 0x45, 0x5f, 0x4e, 0x4f, 0x5f, 0x4d, 0x4f, 0x44, 0x55, 0x4c, 0x45, 0x10, 0x08, 0x42, 0x42, - 0x5a, 0x40, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, - 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, - 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x70, 0x6c, 0x61, 0x6e, 0x73, 0x2f, - 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x70, 0x6c, 0x61, 0x6e, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x12, 0x4b, 0x0a, 0x13, 0x72, 0x65, 0x6c, 0x65, 0x76, + 0x61, 0x6e, 0x74, 0x5f, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x18, 0x0f, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x50, 0x6c, + 0x61, 0x6e, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x61, 0x74, 0x74, 0x72, + 0x52, 0x12, 0x72, 0x65, 0x6c, 0x65, 0x76, 0x61, 0x6e, 0x74, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, + 0x75, 0x74, 0x65, 0x73, 0x1a, 0x52, 0x0a, 0x0e, 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, + 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x2a, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, + 0x2e, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x05, 0x76, + 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x4d, 0x0a, 0x0d, 0x72, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x5f, 0x61, 0x74, 0x74, 0x72, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x20, 0x0a, 0x04, 0x61, 0x74, 0x74, 0x72, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x0c, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x50, 0x61, 0x74, + 0x68, 0x52, 0x04, 0x61, 0x74, 0x74, 0x72, 0x22, 0x69, 0x0a, 0x07, 0x42, 0x61, 0x63, 0x6b, 0x65, + 0x6e, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x2c, 0x0a, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x2e, + 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x06, 0x63, 0x6f, + 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1c, 0x0a, 0x09, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, + 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, + 0x63, 0x65, 0x22, 0xe4, 0x01, 0x0a, 0x06, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x26, 0x0a, + 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x0e, 0x2e, + 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x06, 0x61, + 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x2c, 0x0a, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, + 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x44, + 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x06, 0x76, 0x61, 0x6c, + 0x75, 0x65, 0x73, 0x12, 0x42, 0x0a, 0x16, 0x62, 0x65, 0x66, 0x6f, 0x72, 0x65, 0x5f, 0x73, 0x65, + 0x6e, 0x73, 0x69, 0x74, 0x69, 0x76, 0x65, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x73, 0x18, 0x03, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x0c, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x50, 0x61, 0x74, + 0x68, 0x52, 0x14, 0x62, 0x65, 0x66, 0x6f, 0x72, 0x65, 0x53, 0x65, 0x6e, 0x73, 0x69, 0x74, 0x69, + 0x76, 0x65, 0x50, 0x61, 0x74, 0x68, 0x73, 0x12, 0x40, 0x0a, 0x15, 0x61, 0x66, 0x74, 0x65, 0x72, + 0x5f, 0x73, 0x65, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x76, 0x65, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x73, + 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0c, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x2e, + 0x50, 0x61, 0x74, 0x68, 0x52, 0x13, 0x61, 0x66, 0x74, 0x65, 0x72, 0x53, 0x65, 0x6e, 0x73, 0x69, + 0x74, 0x69, 0x76, 0x65, 0x50, 0x61, 0x74, 0x68, 0x73, 0x22, 0xd3, 0x02, 0x0a, 0x16, 0x52, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x43, 0x68, + 0x61, 0x6e, 0x67, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x61, 0x64, 0x64, 0x72, 0x18, 0x0d, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x04, 0x61, 0x64, 0x64, 0x72, 0x12, 0x22, 0x0a, 0x0d, 0x70, 0x72, 0x65, 0x76, + 0x5f, 0x72, 0x75, 0x6e, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0b, 0x70, 0x72, 0x65, 0x76, 0x52, 0x75, 0x6e, 0x41, 0x64, 0x64, 0x72, 0x12, 0x1f, 0x0a, 0x0b, + 0x64, 0x65, 0x70, 0x6f, 0x73, 0x65, 0x64, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x07, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x0a, 0x64, 0x65, 0x70, 0x6f, 0x73, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x12, 0x1a, 0x0a, + 0x08, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x08, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, 0x26, 0x0a, 0x06, 0x63, 0x68, 0x61, + 0x6e, 0x67, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x74, 0x66, 0x70, 0x6c, + 0x61, 0x6e, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x52, 0x06, 0x63, 0x68, 0x61, 0x6e, 0x67, + 0x65, 0x12, 0x18, 0x0a, 0x07, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x18, 0x0a, 0x20, 0x01, + 0x28, 0x0c, 0x52, 0x07, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x12, 0x37, 0x0a, 0x10, 0x72, + 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x5f, 0x72, 0x65, 0x70, 0x6c, 0x61, 0x63, 0x65, 0x18, + 0x0b, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0c, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x50, + 0x61, 0x74, 0x68, 0x52, 0x0f, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x52, 0x65, 0x70, + 0x6c, 0x61, 0x63, 0x65, 0x12, 0x49, 0x0a, 0x0d, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x72, + 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x24, 0x2e, 0x74, 0x66, + 0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x6e, 0x73, + 0x74, 0x61, 0x6e, 0x63, 0x65, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x61, 0x73, 0x6f, + 0x6e, 0x52, 0x0c, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x22, + 0x68, 0x0a, 0x0c, 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x12, + 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, + 0x61, 0x6d, 0x65, 0x12, 0x26, 0x0a, 0x06, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x43, 0x68, 0x61, + 0x6e, 0x67, 0x65, 0x52, 0x06, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x73, + 0x65, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x76, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, + 0x73, 0x65, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x76, 0x65, 0x22, 0x28, 0x0a, 0x0c, 0x44, 0x79, 0x6e, + 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x73, 0x67, + 0x70, 0x61, 0x63, 0x6b, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x6d, 0x73, 0x67, 0x70, + 0x61, 0x63, 0x6b, 0x22, 0xa5, 0x01, 0x0a, 0x04, 0x50, 0x61, 0x74, 0x68, 0x12, 0x27, 0x0a, 0x05, + 0x73, 0x74, 0x65, 0x70, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x74, 0x66, + 0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x50, 0x61, 0x74, 0x68, 0x2e, 0x53, 0x74, 0x65, 0x70, 0x52, 0x05, + 0x73, 0x74, 0x65, 0x70, 0x73, 0x1a, 0x74, 0x0a, 0x04, 0x53, 0x74, 0x65, 0x70, 0x12, 0x27, 0x0a, + 0x0e, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x0d, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, + 0x74, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x37, 0x0a, 0x0b, 0x65, 0x6c, 0x65, 0x6d, 0x65, 0x6e, + 0x74, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x74, 0x66, + 0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, + 0x65, 0x48, 0x00, 0x52, 0x0a, 0x65, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x4b, 0x65, 0x79, 0x42, + 0x0a, 0x0a, 0x08, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x2a, 0x31, 0x0a, 0x04, 0x4d, + 0x6f, 0x64, 0x65, 0x12, 0x0a, 0x0a, 0x06, 0x4e, 0x4f, 0x52, 0x4d, 0x41, 0x4c, 0x10, 0x00, 0x12, + 0x0b, 0x0a, 0x07, 0x44, 0x45, 0x53, 0x54, 0x52, 0x4f, 0x59, 0x10, 0x01, 0x12, 0x10, 0x0a, 0x0c, + 0x52, 0x45, 0x46, 0x52, 0x45, 0x53, 0x48, 0x5f, 0x4f, 0x4e, 0x4c, 0x59, 0x10, 0x02, 0x2a, 0x70, + 0x0a, 0x06, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x08, 0x0a, 0x04, 0x4e, 0x4f, 0x4f, 0x50, + 0x10, 0x00, 0x12, 0x0a, 0x0a, 0x06, 0x43, 0x52, 0x45, 0x41, 0x54, 0x45, 0x10, 0x01, 0x12, 0x08, + 0x0a, 0x04, 0x52, 0x45, 0x41, 0x44, 0x10, 0x02, 0x12, 0x0a, 0x0a, 0x06, 0x55, 0x50, 0x44, 0x41, + 0x54, 0x45, 0x10, 0x03, 0x12, 0x0a, 0x0a, 0x06, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x10, 0x05, + 0x12, 0x16, 0x0a, 0x12, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x5f, 0x54, 0x48, 0x45, 0x4e, 0x5f, + 0x43, 0x52, 0x45, 0x41, 0x54, 0x45, 0x10, 0x06, 0x12, 0x16, 0x0a, 0x12, 0x43, 0x52, 0x45, 0x41, + 0x54, 0x45, 0x5f, 0x54, 0x48, 0x45, 0x4e, 0x5f, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x10, 0x07, + 0x2a, 0xa7, 0x02, 0x0a, 0x1c, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x6e, 0x73, + 0x74, 0x61, 0x6e, 0x63, 0x65, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x61, 0x73, 0x6f, + 0x6e, 0x12, 0x08, 0x0a, 0x04, 0x4e, 0x4f, 0x4e, 0x45, 0x10, 0x00, 0x12, 0x1b, 0x0a, 0x17, 0x52, + 0x45, 0x50, 0x4c, 0x41, 0x43, 0x45, 0x5f, 0x42, 0x45, 0x43, 0x41, 0x55, 0x53, 0x45, 0x5f, 0x54, + 0x41, 0x49, 0x4e, 0x54, 0x45, 0x44, 0x10, 0x01, 0x12, 0x16, 0x0a, 0x12, 0x52, 0x45, 0x50, 0x4c, + 0x41, 0x43, 0x45, 0x5f, 0x42, 0x59, 0x5f, 0x52, 0x45, 0x51, 0x55, 0x45, 0x53, 0x54, 0x10, 0x02, + 0x12, 0x21, 0x0a, 0x1d, 0x52, 0x45, 0x50, 0x4c, 0x41, 0x43, 0x45, 0x5f, 0x42, 0x45, 0x43, 0x41, + 0x55, 0x53, 0x45, 0x5f, 0x43, 0x41, 0x4e, 0x4e, 0x4f, 0x54, 0x5f, 0x55, 0x50, 0x44, 0x41, 0x54, + 0x45, 0x10, 0x03, 0x12, 0x25, 0x0a, 0x21, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x5f, 0x42, 0x45, + 0x43, 0x41, 0x55, 0x53, 0x45, 0x5f, 0x4e, 0x4f, 0x5f, 0x52, 0x45, 0x53, 0x4f, 0x55, 0x52, 0x43, + 0x45, 0x5f, 0x43, 0x4f, 0x4e, 0x46, 0x49, 0x47, 0x10, 0x04, 0x12, 0x23, 0x0a, 0x1f, 0x44, 0x45, + 0x4c, 0x45, 0x54, 0x45, 0x5f, 0x42, 0x45, 0x43, 0x41, 0x55, 0x53, 0x45, 0x5f, 0x57, 0x52, 0x4f, + 0x4e, 0x47, 0x5f, 0x52, 0x45, 0x50, 0x45, 0x54, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x10, 0x05, 0x12, + 0x1e, 0x0a, 0x1a, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x5f, 0x42, 0x45, 0x43, 0x41, 0x55, 0x53, + 0x45, 0x5f, 0x43, 0x4f, 0x55, 0x4e, 0x54, 0x5f, 0x49, 0x4e, 0x44, 0x45, 0x58, 0x10, 0x06, 0x12, + 0x1b, 0x0a, 0x17, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x5f, 0x42, 0x45, 0x43, 0x41, 0x55, 0x53, + 0x45, 0x5f, 0x45, 0x41, 0x43, 0x48, 0x5f, 0x4b, 0x45, 0x59, 0x10, 0x07, 0x12, 0x1c, 0x0a, 0x18, + 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x5f, 0x42, 0x45, 0x43, 0x41, 0x55, 0x53, 0x45, 0x5f, 0x4e, + 0x4f, 0x5f, 0x4d, 0x4f, 0x44, 0x55, 0x4c, 0x45, 0x10, 0x08, 0x42, 0x42, 0x5a, 0x40, 0x67, 0x69, + 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, + 0x72, 0x70, 0x2f, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x2f, 0x69, 0x6e, 0x74, + 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x70, 0x6c, 0x61, 0x6e, 0x73, 0x2f, 0x69, 0x6e, 0x74, 0x65, + 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x70, 0x6c, 0x61, 0x6e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -1057,7 +1132,7 @@ func file_planfile_proto_rawDescGZIP() []byte { } var file_planfile_proto_enumTypes = make([]protoimpl.EnumInfo, 3) -var file_planfile_proto_msgTypes = make([]protoimpl.MessageInfo, 9) +var file_planfile_proto_msgTypes = make([]protoimpl.MessageInfo, 10) var file_planfile_proto_goTypes = []interface{}{ (Mode)(0), // 0: tfplan.Mode (Action)(0), // 1: tfplan.Action @@ -1070,7 +1145,8 @@ var file_planfile_proto_goTypes = []interface{}{ (*DynamicValue)(nil), // 8: tfplan.DynamicValue (*Path)(nil), // 9: tfplan.Path nil, // 10: tfplan.Plan.VariablesEntry - (*Path_Step)(nil), // 11: tfplan.Path.Step + (*PlanResourceAttr)(nil), // 11: tfplan.Plan.resource_attr + (*Path_Step)(nil), // 12: tfplan.Path.Step } var file_planfile_proto_depIdxs = []int32{ 0, // 0: tfplan.Plan.ui_mode:type_name -> tfplan.Mode @@ -1079,23 +1155,25 @@ var file_planfile_proto_depIdxs = []int32{ 6, // 3: tfplan.Plan.resource_drift:type_name -> tfplan.ResourceInstanceChange 7, // 4: tfplan.Plan.output_changes:type_name -> tfplan.OutputChange 4, // 5: tfplan.Plan.backend:type_name -> tfplan.Backend - 8, // 6: tfplan.Backend.config:type_name -> tfplan.DynamicValue - 1, // 7: tfplan.Change.action:type_name -> tfplan.Action - 8, // 8: tfplan.Change.values:type_name -> tfplan.DynamicValue - 9, // 9: tfplan.Change.before_sensitive_paths:type_name -> tfplan.Path - 9, // 10: tfplan.Change.after_sensitive_paths:type_name -> tfplan.Path - 5, // 11: tfplan.ResourceInstanceChange.change:type_name -> tfplan.Change - 9, // 12: tfplan.ResourceInstanceChange.required_replace:type_name -> tfplan.Path - 2, // 13: tfplan.ResourceInstanceChange.action_reason:type_name -> tfplan.ResourceInstanceActionReason - 5, // 14: tfplan.OutputChange.change:type_name -> tfplan.Change - 11, // 15: tfplan.Path.steps:type_name -> tfplan.Path.Step - 8, // 16: tfplan.Plan.VariablesEntry.value:type_name -> tfplan.DynamicValue - 8, // 17: tfplan.Path.Step.element_key:type_name -> tfplan.DynamicValue - 18, // [18:18] is the sub-list for method output_type - 18, // [18:18] is the sub-list for method input_type - 18, // [18:18] is the sub-list for extension type_name - 18, // [18:18] is the sub-list for extension extendee - 0, // [0:18] is the sub-list for field type_name + 11, // 6: tfplan.Plan.relevant_attributes:type_name -> tfplan.Plan.resource_attr + 8, // 7: tfplan.Backend.config:type_name -> tfplan.DynamicValue + 1, // 8: tfplan.Change.action:type_name -> tfplan.Action + 8, // 9: tfplan.Change.values:type_name -> tfplan.DynamicValue + 9, // 10: tfplan.Change.before_sensitive_paths:type_name -> tfplan.Path + 9, // 11: tfplan.Change.after_sensitive_paths:type_name -> tfplan.Path + 5, // 12: tfplan.ResourceInstanceChange.change:type_name -> tfplan.Change + 9, // 13: tfplan.ResourceInstanceChange.required_replace:type_name -> tfplan.Path + 2, // 14: tfplan.ResourceInstanceChange.action_reason:type_name -> tfplan.ResourceInstanceActionReason + 5, // 15: tfplan.OutputChange.change:type_name -> tfplan.Change + 12, // 16: tfplan.Path.steps:type_name -> tfplan.Path.Step + 8, // 17: tfplan.Plan.VariablesEntry.value:type_name -> tfplan.DynamicValue + 9, // 18: tfplan.Plan.resource_attr.attr:type_name -> tfplan.Path + 8, // 19: tfplan.Path.Step.element_key:type_name -> tfplan.DynamicValue + 20, // [20:20] is the sub-list for method output_type + 20, // [20:20] is the sub-list for method input_type + 20, // [20:20] is the sub-list for extension type_name + 20, // [20:20] is the sub-list for extension extendee + 0, // [0:20] is the sub-list for field type_name } func init() { file_planfile_proto_init() } @@ -1189,6 +1267,18 @@ func file_planfile_proto_init() { } } file_planfile_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*PlanResourceAttr); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_planfile_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*Path_Step); i { case 0: return &v.state @@ -1201,7 +1291,7 @@ func file_planfile_proto_init() { } } } - file_planfile_proto_msgTypes[8].OneofWrappers = []interface{}{ + file_planfile_proto_msgTypes[9].OneofWrappers = []interface{}{ (*Path_Step_AttributeName)(nil), (*Path_Step_ElementKey)(nil), } @@ -1211,7 +1301,7 @@ func file_planfile_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_planfile_proto_rawDesc, NumEnums: 3, - NumMessages: 9, + NumMessages: 10, NumExtensions: 0, NumServices: 0, }, diff --git a/internal/plans/internal/planproto/planfile.proto b/internal/plans/internal/planproto/planfile.proto index 752abd77ac5a..659c419efa68 100644 --- a/internal/plans/internal/planproto/planfile.proto +++ b/internal/plans/internal/planproto/planfile.proto @@ -61,6 +61,15 @@ message Plan { // Backend is a description of the backend configuration and other related // settings at the time the plan was created. Backend backend = 13; + + message resource_attr { + string resource = 1; + Path attr= 2; + }; + + // RelevantAttributes lists individual resource attributes from + // ResourceDrift which may have contributed to the plan changes. + repeated resource_attr relevant_attributes = 15; } // Mode describes the planning mode that created the plan. diff --git a/internal/plans/planfile/tfplan.go b/internal/plans/planfile/tfplan.go index 47f95a2a3260..768c4a532f70 100644 --- a/internal/plans/planfile/tfplan.go +++ b/internal/plans/planfile/tfplan.go @@ -8,6 +8,7 @@ import ( "google.golang.org/protobuf/proto" "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/lang/globalref" "github.com/hashicorp/terraform/internal/lang/marks" "github.com/hashicorp/terraform/internal/plans" "github.com/hashicorp/terraform/internal/plans/internal/planproto" @@ -107,6 +108,14 @@ func readTfplan(r io.Reader) (*plans.Plan, error) { plan.DriftedResources = append(plan.DriftedResources, change) } + for _, rawRA := range rawPlan.RelevantAttributes { + ra, err := resourceAttrFromTfplan(rawRA) + if err != nil { + return nil, err + } + plan.RelevantAttributes = append(plan.RelevantAttributes, ra) + } + for _, rawTargetAddr := range rawPlan.TargetAddrs { target, diags := addrs.ParseTargetStr(rawTargetAddr) if diags.HasErrors() { @@ -407,6 +416,14 @@ func writeTfplan(plan *plans.Plan, w io.Writer) error { rawPlan.ResourceDrift = append(rawPlan.ResourceDrift, rawRC) } + for _, ra := range plan.RelevantAttributes { + rawRA, err := resourceAttrToTfplan(ra) + if err != nil { + return err + } + rawPlan.RelevantAttributes = append(rawPlan.RelevantAttributes, rawRA) + } + for _, targetAddr := range plan.TargetAddrs { rawPlan.TargetAddrs = append(rawPlan.TargetAddrs, targetAddr.String()) } @@ -445,6 +462,39 @@ func writeTfplan(plan *plans.Plan, w io.Writer) error { return nil } +func resourceAttrToTfplan(ra globalref.ResourceAttr) (*planproto.PlanResourceAttr, error) { + res := &planproto.PlanResourceAttr{} + + res.Resource = ra.Resource.String() + attr, err := pathToTfplan(ra.Attr) + if err != nil { + return res, err + } + res.Attr = attr + return res, nil +} + +func resourceAttrFromTfplan(ra *planproto.PlanResourceAttr) (globalref.ResourceAttr, error) { + var res globalref.ResourceAttr + if ra.Resource == "" { + return res, fmt.Errorf("missing resource address from relevant attribute") + } + + instAddr, diags := addrs.ParseAbsResourceInstanceStr(ra.Resource) + if diags.HasErrors() { + return res, fmt.Errorf("invalid resource instance address %q in relevant attributes: %w", ra.Resource, diags.Err()) + } + + res.Resource = instAddr + path, err := pathFromTfplan(ra.Attr) + if err != nil { + return res, fmt.Errorf("invalid path in %q relevant attribute: %s", res.Resource, err) + } + + res.Attr = path + return res, nil +} + func resourceChangeToTfplan(change *plans.ResourceInstanceChangeSrc) (*planproto.ResourceInstanceChange, error) { ret := &planproto.ResourceInstanceChange{} diff --git a/internal/plans/planfile/tfplan_test.go b/internal/plans/planfile/tfplan_test.go index 7d5be4dc5713..962e1de0f1e4 100644 --- a/internal/plans/planfile/tfplan_test.go +++ b/internal/plans/planfile/tfplan_test.go @@ -8,6 +8,7 @@ import ( "github.com/zclconf/go-cty/cty" "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/lang/globalref" "github.com/hashicorp/terraform/internal/lang/marks" "github.com/hashicorp/terraform/internal/plans" ) @@ -158,6 +159,16 @@ func TestTFPlanRoundTrip(t *testing.T) { }, }, }, + RelevantAttributes: []globalref.ResourceAttr{ + { + Resource: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_thing", + Name: "woot", + }.Instance(addrs.IntKey(0)).Absolute(addrs.RootModuleInstance), + Attr: cty.GetAttrPath("boop").Index(cty.NumberIntVal(1)), + }, + }, TargetAddrs: []addrs.Targetable{ addrs.Resource{ Mode: addrs.ManagedResourceMode, From f0cd8be66f33a30082d000bef6963eb8db33f931 Mon Sep 17 00:00:00 2001 From: James Bardin Date: Mon, 7 Feb 2022 14:31:45 -0500 Subject: [PATCH 18/19] add whole resource references --- internal/command/views/plan.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/internal/command/views/plan.go b/internal/command/views/plan.go index c3c1283cfc80..37bd7dfa6621 100644 --- a/internal/command/views/plan.go +++ b/internal/command/views/plan.go @@ -454,6 +454,7 @@ func renderChangesDetectedByRefresh(plan *plans.Plan, schemas *terraform.Schemas // only showing the attributes we're interested in. // The resulting change will be a NoOp if it has nothing relevant to the plan. func filterRefreshChange(change *plans.ResourceInstanceChange, contributing []globalref.ResourceAttr) plans.Change { + if change.Action == plans.NoOp { return change.Change } @@ -462,7 +463,13 @@ func filterRefreshChange(change *plans.ResourceInstanceChange, contributing []gl resAddr := change.Addr for _, attr := range contributing { - if resAddr.Equal(attr.Resource) { + if !resAddr.ContainingResource().Equal(attr.Resource.ContainingResource()) { + continue + } + + // If the contributing address has no instance key, then the + // contributing reference applies to all instances. + if attr.Resource.Resource.Key == addrs.NoKey || resAddr.Equal(attr.Resource) { relevantAttrs = append(relevantAttrs, attr.Attr) } } From 8c5e11d41a9282881cf2622c3772a8f482c4ec6e Mon Sep 17 00:00:00 2001 From: James Bardin Date: Tue, 8 Feb 2022 10:58:10 -0500 Subject: [PATCH 19/19] add relevant_attributes to the json plan format Add the resource instances and individual attributes which may have contributed to the planned changes to the json format of the plan. We use the existing path encoding for individual attributes, which is already used in the replace_paths change field. --- internal/command/jsonplan/plan.go | 79 +++++++++++++++++++++---------- 1 file changed, 54 insertions(+), 25 deletions(-) diff --git a/internal/command/jsonplan/plan.go b/internal/command/jsonplan/plan.go index c68cfe8021c0..38a93dedffdc 100644 --- a/internal/command/jsonplan/plan.go +++ b/internal/command/jsonplan/plan.go @@ -33,11 +33,12 @@ type plan struct { PlannedValues stateValues `json:"planned_values,omitempty"` // ResourceDrift and ResourceChanges are sorted in a user-friendly order // that is undefined at this time, but consistent. - ResourceDrift []resourceChange `json:"resource_drift,omitempty"` - ResourceChanges []resourceChange `json:"resource_changes,omitempty"` - OutputChanges map[string]change `json:"output_changes,omitempty"` - PriorState json.RawMessage `json:"prior_state,omitempty"` - Config json.RawMessage `json:"configuration,omitempty"` + ResourceDrift []resourceChange `json:"resource_drift,omitempty"` + ResourceChanges []resourceChange `json:"resource_changes,omitempty"` + OutputChanges map[string]change `json:"output_changes,omitempty"` + PriorState json.RawMessage `json:"prior_state,omitempty"` + Config json.RawMessage `json:"configuration,omitempty"` + RelevantAttributes []resourceAttr `json:"relevant_attributes,omitempty"` } func newPlan() *plan { @@ -46,6 +47,13 @@ func newPlan() *plan { } } +// resourceAttr contains the address and attribute of an external for the +// RelevantAttributes in the plan. +type resourceAttr struct { + Resource string `json:"resource"` + Attr json.RawMessage `json:"attribute"` +} + // Change is the representation of a proposed change for an object. type change struct { // Actions are the actions that will be taken on the object selected by the @@ -151,6 +159,10 @@ func Marshal( } } + if err := output.marshalRelevantAttrs(p); err != nil { + return nil, fmt.Errorf("error marshaling relevant attributes for external changes: %s", err) + } + // output.ResourceChanges if p.Changes != nil { output.ResourceChanges, err = output.marshalResourceChanges(p.Changes.Resources, schemas) @@ -482,6 +494,19 @@ func (p *plan) marshalPlannedValues(changes *plans.Changes, schemas *terraform.S return nil } +func (p *plan) marshalRelevantAttrs(plan *plans.Plan) error { + for _, ra := range plan.RelevantAttributes { + addr := ra.Resource.String() + path, err := encodePath(ra.Attr) + if err != nil { + return err + } + + p.RelevantAttributes = append(p.RelevantAttributes, resourceAttr{addr, path}) + } + return nil +} + // omitUnknowns recursively walks the src cty.Value and returns a new cty.Value, // omitting any unknowns. // @@ -655,26 +680,7 @@ func encodePaths(pathSet cty.PathSet) (json.RawMessage, error) { jsonPaths := make([]json.RawMessage, 0, len(pathList)) for _, path := range pathList { - steps := make([]json.RawMessage, 0, len(path)) - for _, step := range path { - switch s := step.(type) { - case cty.IndexStep: - key, err := ctyjson.Marshal(s.Key, s.Key.Type()) - if err != nil { - return nil, fmt.Errorf("Failed to marshal index step key %#v: %s", s.Key, err) - } - steps = append(steps, key) - case cty.GetAttrStep: - name, err := json.Marshal(s.Name) - if err != nil { - return nil, fmt.Errorf("Failed to marshal get attr step name %#v: %s", s.Name, err) - } - steps = append(steps, name) - default: - return nil, fmt.Errorf("Unsupported path step %#v (%t)", step, step) - } - } - jsonPath, err := json.Marshal(steps) + jsonPath, err := encodePath(path) if err != nil { return nil, err } @@ -683,3 +689,26 @@ func encodePaths(pathSet cty.PathSet) (json.RawMessage, error) { return json.Marshal(jsonPaths) } + +func encodePath(path cty.Path) (json.RawMessage, error) { + steps := make([]json.RawMessage, 0, len(path)) + for _, step := range path { + switch s := step.(type) { + case cty.IndexStep: + key, err := ctyjson.Marshal(s.Key, s.Key.Type()) + if err != nil { + return nil, fmt.Errorf("Failed to marshal index step key %#v: %s", s.Key, err) + } + steps = append(steps, key) + case cty.GetAttrStep: + name, err := json.Marshal(s.Name) + if err != nil { + return nil, fmt.Errorf("Failed to marshal get attr step name %#v: %s", s.Name, err) + } + steps = append(steps, name) + default: + return nil, fmt.Errorf("Unsupported path step %#v (%t)", step, step) + } + } + return json.Marshal(steps) +}