From ff992238c15e996bcd0f374d03734ce00b4e45a7 Mon Sep 17 00:00:00 2001 From: Martin Atkins Date: Fri, 1 Sep 2023 17:16:15 -0700 Subject: [PATCH] core: Allow passing in externally-configured provider instances Normally Terraform Core expects what it sees as the root module to be a self-contained root module which defines all of the provider configurations it needs. This commit is the start of making it valid for the apparent root module to actually be a shared module, which means it'd expect to receive provider configurations from its caller rather than defining them itself. The caller of the "root" module is the Terraform Core caller written in Go, so this is essentially a Go equivalent of the "providers" argument in a "module" block in the Terraform language. --- internal/configs/config.go | 46 ++++--- internal/configs/provider_validation.go | 48 ++++--- internal/terraform/context_apply.go | 53 +++++-- internal/terraform/context_apply2_test.go | 2 +- .../terraform/context_apply_overrides_test.go | 4 +- internal/terraform/context_plan.go | 43 ++++-- internal/terraform/context_walk.go | 40 ++++-- internal/terraform/eval_context_builtin.go | 22 +++ internal/terraform/graph_walk_context.go | 72 +++++----- internal/terraform/providers.go | 129 ++++++++++++++++++ 10 files changed, 354 insertions(+), 105 deletions(-) create mode 100644 internal/terraform/providers.go diff --git a/internal/configs/config.go b/internal/configs/config.go index e21b3ca105..806a19c797 100644 --- a/internal/configs/config.go +++ b/internal/configs/config.go @@ -885,7 +885,8 @@ func (c *Config) ProviderForConfigAddr(addr addrs.LocalProviderConfig) addrs.Pro // EffectiveRequiredProviderConfigs returns a set of all of the provider // configurations this config's direct module expects to have passed in -// (explicitly or implicitly) by its caller. +// (explicitly or implicitly) by its caller. This method only makes sense +// to call on the object representing the root module. // // This includes both provider configurations declared explicitly using // configuration_aliases in the required_providers block _and_ configurations @@ -906,7 +907,7 @@ func (c *Config) ProviderForConfigAddr(addr addrs.LocalProviderConfig) addrs.Pro // // This function assumes that the configuration is valid. It may produce under- // or over-constrained results if called on an invalid configuration. -func (c *Config) EffectiveRequiredProviderConfigs() addrs.Set[addrs.AbsProviderConfig] { +func (c *Config) EffectiveRequiredProviderConfigs() addrs.Set[addrs.RootProviderConfig] { // The Terraform language has accumulated so many different ways to imply // the need for a provider configuration that answering this is quite a // complicated process that ends up potentially needing to visit the @@ -914,22 +915,25 @@ func (c *Config) EffectiveRequiredProviderConfigs() addrs.Set[addrs.AbsProviderC // about the current node's requirements. In the happy explicit case we // can avoid any recursion, but that case is rare in practice. + if c == nil { + return nil + } + // We'll start by visiting all of the "provider" blocks in the module and // figuring out which provider configuration address they each declare. Any // configuration addresses we find here cannot be "required" provider // configs because the module instantiates them itself. - selfConfigured := addrs.MakeSet[addrs.AbsProviderConfig]() + selfConfigured := addrs.MakeSet[addrs.RootProviderConfig]() for _, pc := range c.Module.ProviderConfigs { localAddr := pc.Addr() sourceAddr := c.Module.ProviderForLocalConfig(localAddr) - selfConfigured.Add(addrs.AbsProviderConfig{ - Module: c.Path, + selfConfigured.Add(addrs.RootProviderConfig{ Provider: sourceAddr, Alias: localAddr.Alias, }) } - ret := addrs.MakeSet[addrs.AbsProviderConfig]() - maybeAddAbs := func(addr addrs.AbsProviderConfig) { + ret := addrs.MakeSet[addrs.RootProviderConfig]() + maybeAdd := func(addr addrs.RootProviderConfig) { if !selfConfigured.Has(addr) { ret.Add(addr) } @@ -939,16 +943,17 @@ func (c *Config) EffectiveRequiredProviderConfigs() addrs.Set[addrs.AbsProviderC // in the _current_ module c.Module. It will produce incorrect results // if used for addresses from any child module. sourceAddr := c.Module.ProviderForLocalConfig(addr) - maybeAddAbs(addrs.AbsProviderConfig{ - Module: c.Path, + maybeAdd(addrs.RootProviderConfig{ Provider: sourceAddr, Alias: addr.Alias, }) } - for _, req := range c.Module.ProviderRequirements.RequiredProviders { - for _, addr := range req.Aliases { - maybeAddLocal(addr) + if c.Module.ProviderRequirements != nil { + for _, req := range c.Module.ProviderRequirements.RequiredProviders { + for _, addr := range req.Aliases { + maybeAddLocal(addr) + } } } for _, rc := range c.Module.ManagedResources { @@ -958,10 +963,16 @@ func (c *Config) EffectiveRequiredProviderConfigs() addrs.Set[addrs.AbsProviderC maybeAddLocal(rc.ProviderConfigAddr()) } for _, ic := range c.Module.Import { - maybeAddLocal(addrs.LocalProviderConfig{ - LocalName: ic.ProviderConfigRef.Name, - Alias: ic.ProviderConfigRef.Alias, - }) + if ic.ProviderConfigRef != nil { + maybeAddLocal(addrs.LocalProviderConfig{ + LocalName: ic.ProviderConfigRef.Name, + Alias: ic.ProviderConfigRef.Alias, + }) + } else { + maybeAdd(addrs.RootProviderConfig{ + Provider: ic.Provider, + }) + } } for _, mc := range c.Module.ModuleCalls { for _, pp := range mc.Providers { @@ -984,8 +995,7 @@ func (c *Config) EffectiveRequiredProviderConfigs() addrs.Set[addrs.AbsProviderC } // We must reinterpret the child address to appear as // if written in its parent (our current module). - maybeAddAbs(addrs.AbsProviderConfig{ - Module: c.Path, + maybeAdd(addrs.RootProviderConfig{ Provider: childReq.Provider, }) } diff --git a/internal/configs/provider_validation.go b/internal/configs/provider_validation.go index 02c4ee2416..f21d78b098 100644 --- a/internal/configs/provider_validation.go +++ b/internal/configs/provider_validation.go @@ -282,13 +282,20 @@ func validateProviderConfigsForTests(cfg *Config) (diags hcl.Diagnostics) { // however will generate an error if a suitable provider configuration is not // passed in through the module call. // -// The call argument is the ModuleCall for the provided Config cfg. The +// The parentCall argument is the ModuleCall for the provided Config cfg. The // noProviderConfigRange argument is passed down the call stack, indicating // that the module call, or a parent module call, has used a feature (at the // specified source location) that precludes providers from being configured at // all within the module. +// +// Set parentCall to nil when analyzing the root module. In that case the +// given configuration is allowed to require passed-in provider configurations +// without that being an error at this layer, although Terraform Core itself +// will raise an error if asked to plan such a configuration without the caller +// passing in suitable pre-configured providers to use. func validateProviderConfigs(parentCall *ModuleCall, cfg *Config, noProviderConfigRange *hcl.Range) (diags hcl.Diagnostics) { mod := cfg.Module + analyzingRootModule := (parentCall == nil) for name, child := range cfg.Children { mc := mod.ModuleCalls[name] @@ -566,25 +573,30 @@ func validateProviderConfigs(parentCall *ModuleCall, cfg *Config, noProviderConf } // A declared alias requires either a matching configuration within the - // module, or one must be passed in. - for name, providerAddr := range configAliases { - _, confOk := configured[name] - _, passedOk := passedIn[name] + // module, or one must be passed in, unless we're analyzing the root + // module. For the root module it's up to Terraform Core to check if + // it's being given the required provider configurations as part of the + // options when creating a plan. + if !analyzingRootModule { + for name, providerAddr := range configAliases { + _, confOk := configured[name] + _, passedOk := passedIn[name] + + if confOk || passedOk { + continue + } - if confOk || passedOk { - continue + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Missing required provider configuration", + Detail: fmt.Sprintf( + "The child module requires an additional configuration for provider %s, with the local name %q.\n\nRefer to the module's documentation to understand the intended purpose of this additional provider configuration, and then add an entry for %s in the \"providers\" meta-argument in the module block to choose which provider configuration the module should use for that purpose.", + providerAddr.Provider.ForDisplay(), name, + name, + ), + Subject: &parentCall.DeclRange, + }) } - - diags = append(diags, &hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Missing required provider configuration", - Detail: fmt.Sprintf( - "The child module requires an additional configuration for provider %s, with the local name %q.\n\nRefer to the module's documentation to understand the intended purpose of this additional provider configuration, and then add an entry for %s in the \"providers\" meta-argument in the module block to choose which provider configuration the module should use for that purpose.", - providerAddr.Provider.ForDisplay(), name, - name, - ), - Subject: &parentCall.DeclRange, - }) } // You cannot pass in a provider that cannot be used diff --git a/internal/terraform/context_apply.go b/internal/terraform/context_apply.go index 392fe70cea..a843b391a6 100644 --- a/internal/terraform/context_apply.go +++ b/internal/terraform/context_apply.go @@ -12,6 +12,7 @@ import ( "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/states" "github.com/hashicorp/terraform/internal/tfdiags" ) @@ -19,6 +20,28 @@ import ( // ApplyOpts are options that affect the details of how Terraform will apply a // previously-generated plan. type ApplyOpts struct { + // ExternalProviders is a set of pre-configured provider instances with + // the same purpose as [PlanOpts.ExternalProviders]. + // + // Callers must pass providers that are configured in a similar way as + // the providers that were passed when creating the plan that's being + // applied, or the results will be erratic. + ExternalProviders map[addrs.RootProviderConfig]providers.Interface +} + +// ApplyOpts creates an [ApplyOpts] with copies of all of the elements that +// are expected to propagate from plan to apply when planning and applying +// in the same process. +// +// In practice planning and applying are often separated into two different +// executions, in which case callers must retain enough information between +// plan and apply to construct an equivalent [ApplyOpts] themselves without +// using this function. This is here mainly for convenient internal use such +// as in test cases. +func (po *PlanOpts) ApplyOpts() *ApplyOpts { + return &ApplyOpts{ + ExternalProviders: po.ExternalProviders, + } } // Apply performs the actions described by the given Plan object and returns @@ -37,7 +60,11 @@ type ApplyOpts struct { // certain combinations of plan-time options. func (c *Context) Apply(plan *plans.Plan, config *configs.Config, opts *ApplyOpts) (*states.State, tfdiags.Diagnostics) { defer c.acquireRun("apply")() + var diags tfdiags.Diagnostics + if plan == nil { + panic("cannot apply nil plan") + } log.Printf("[DEBUG] Building and walking apply graph for %s plan", plan.UIMode) if opts == nil { @@ -67,17 +94,25 @@ func (c *Context) Apply(plan *plans.Plan, config *configs.Config, opts *ApplyOpt } } - graph, operation, diags := c.applyGraph(plan, config, true) - if diags.HasErrors() { + graph, operation, moreDiags := c.applyGraph(plan, config, opts, true) + diags = diags.Append(moreDiags) + if moreDiags.HasErrors() { + return nil, diags + } + + moreDiags = checkExternalProviders(config, opts.ExternalProviders) + diags = diags.Append(moreDiags) + if moreDiags.HasErrors() { return nil, diags } workingState := plan.PriorState.DeepCopy() walker, walkDiags := c.walk(graph, operation, &graphWalkOpts{ - Config: config, - InputState: workingState, - Changes: plan.Changes, - Overrides: plan.Overrides, + Config: config, + InputState: workingState, + Changes: plan.Changes, + Overrides: plan.Overrides, + ExternalProviderConfigs: opts.ExternalProviders, // We need to propagate the check results from the plan phase, // because that will tell us which checkable objects we're expecting @@ -133,7 +168,7 @@ Note that the -target option is not suitable for routine use, and is provided on return newState, diags } -func (c *Context) applyGraph(plan *plans.Plan, config *configs.Config, validate bool) (*Graph, walkOperation, tfdiags.Diagnostics) { +func (c *Context) applyGraph(plan *plans.Plan, config *configs.Config, opts *ApplyOpts, validate bool) (*Graph, walkOperation, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics variables := InputValues{} @@ -211,14 +246,14 @@ func (c *Context) applyGraph(plan *plans.Plan, config *configs.Config, validate // The result of this is intended only for rendering ot the user as a dot // graph, and so may change in future in order to make the result more useful // in that context, even if drifts away from the physical graph that Terraform -// Core currently uses as an implementation detail of planning. +// Core currently uses as an implementation detail of applying. func (c *Context) ApplyGraphForUI(plan *plans.Plan, config *configs.Config) (*Graph, tfdiags.Diagnostics) { // For now though, this really is just the internal graph, confusing // implementation details and all. var diags tfdiags.Diagnostics - graph, _, moreDiags := c.applyGraph(plan, config, false) + graph, _, moreDiags := c.applyGraph(plan, config, nil, false) diags = diags.Append(moreDiags) return graph, diags } diff --git a/internal/terraform/context_apply2_test.go b/internal/terraform/context_apply2_test.go index 35a17e4cf3..b5aca988ef 100644 --- a/internal/terraform/context_apply2_test.go +++ b/internal/terraform/context_apply2_test.go @@ -2433,7 +2433,7 @@ resource "test_object" "foo" { t.Fatalf("expected no errors, but got %s", diags) } - state, diags := ctx.Apply(plan, m) + state, diags := ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("expected no errors, but got %s", diags) } diff --git a/internal/terraform/context_apply_overrides_test.go b/internal/terraform/context_apply_overrides_test.go index 541ff7c93c..0ee525d83a 100644 --- a/internal/terraform/context_apply_overrides_test.go +++ b/internal/terraform/context_apply_overrides_test.go @@ -591,7 +591,7 @@ output "id" { t.Fatal(diags.Err()) } - state, diags := ctx.Apply(plan, cfg) + state, diags := ctx.Apply(plan, cfg, nil) if diags.HasErrors() { t.Fatal(diags.Err()) } @@ -622,7 +622,7 @@ output "id" { t.Fatal(diags.Err()) } - _, diags = ctx.Apply(destroyPlan, cfg) + _, diags = ctx.Apply(destroyPlan, cfg, nil) if diags.HasErrors() { t.Fatal(diags.Err()) } diff --git a/internal/terraform/context_plan.go b/internal/terraform/context_plan.go index 7d50aa5bd1..8310f36a73 100644 --- a/internal/terraform/context_plan.go +++ b/internal/terraform/context_plan.go @@ -19,6 +19,7 @@ import ( "github.com/hashicorp/terraform/internal/lang/globalref" "github.com/hashicorp/terraform/internal/moduletest/mocking" "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/refactoring" "github.com/hashicorp/terraform/internal/states" "github.com/hashicorp/terraform/internal/tfdiags" @@ -93,6 +94,18 @@ type PlanOpts struct { // // If empty, then no config will be generated. GenerateConfigPath string + + // ExternalProviders are clients for pre-configured providers that are + // treated as being passed into the root module from the caller. This + // is equivalent to writing a "providers" argument inside a "module" + // block in the Terraform language, but for the root module the caller + // is written in Go rather than the Terraform language. + // + // Terraform Core will NOT call ValidateProviderConfig or ConfigureProvider + // on any providers in this map; it's the caller's responsibility to + // configure these providers based on information outside the scope of + // the root module. + ExternalProviders map[addrs.RootProviderConfig]providers.Interface } // Plan generates an execution plan by comparing the given configuration @@ -138,6 +151,12 @@ func (c *Context) Plan(config *configs.Config, prevRunState *states.State, opts return nil, diags } + providerCfgDiags := checkExternalProviders(config, opts.ExternalProviders) + diags = diags.Append(providerCfgDiags) + if providerCfgDiags.HasErrors() { + return nil, diags + } + switch opts.Mode { case plans.NormalMode, plans.DestroyMode: // OK @@ -259,7 +278,7 @@ The -target option is not for routine use, and is provided only for exceptional return plan, diags } - diags = diags.Append(c.checkApplyGraph(plan, config)) + diags = diags.Append(c.checkApplyGraph(plan, config, opts)) return plan, diags } @@ -268,13 +287,13 @@ The -target option is not for routine use, and is provided only for exceptional // check for any errors that may arise once the planned changes are added to // the graph. This allows terraform to report errors (mostly cycles) during // plan that would otherwise only crop up during apply -func (c *Context) checkApplyGraph(plan *plans.Plan, config *configs.Config) tfdiags.Diagnostics { +func (c *Context) checkApplyGraph(plan *plans.Plan, config *configs.Config, opts *PlanOpts) tfdiags.Diagnostics { if plan.Changes.Empty() { log.Println("[DEBUG] no planned changes, skipping apply graph check") return nil } log.Println("[DEBUG] building apply graph to check for errors") - _, _, diags := c.applyGraph(plan, config, true) + _, _, diags := c.applyGraph(plan, config, opts.ApplyOpts(), true) return diags } @@ -566,16 +585,22 @@ func (c *Context) planWalk(config *configs.Config, prevRunState *states.State, o timestamp := time.Now().UTC() + var externalProviderConfigs map[addrs.RootProviderConfig]providers.Interface + if opts != nil { + externalProviderConfigs = opts.ExternalProviders + } + // If we get here then we should definitely have a non-nil "graph", which // we can now walk. changes := plans.NewChanges() walker, walkDiags := c.walk(graph, walkOp, &graphWalkOpts{ - Config: config, - InputState: prevRunState, - Changes: changes, - MoveResults: moveResults, - PlanTimeTimestamp: timestamp, - Overrides: opts.Overrides, + Config: config, + InputState: prevRunState, + ExternalProviderConfigs: externalProviderConfigs, + Changes: changes, + MoveResults: moveResults, + Overrides: opts.Overrides, + PlanTimeTimestamp: timestamp, }) diags = diags.Append(walker.NonFatalDiagnostics) diags = diags.Append(walkDiags) diff --git a/internal/terraform/context_walk.go b/internal/terraform/context_walk.go index db371d52fa..30ed5547a5 100644 --- a/internal/terraform/context_walk.go +++ b/internal/terraform/context_walk.go @@ -7,11 +7,13 @@ import ( "log" "time" + "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/checks" "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/instances" "github.com/hashicorp/terraform/internal/moduletest/mocking" "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/refactoring" "github.com/hashicorp/terraform/internal/states" "github.com/hashicorp/terraform/internal/tfdiags" @@ -29,6 +31,17 @@ type graphWalkOpts struct { Changes *plans.Changes Config *configs.Config + // ExternalProviderConfigs is used for walks that make use of configured + // providers (e.g. plan and apply) to satisfy situations where the root + // module itself declares that it expects to have providers passed in + // from outside. + // + // This should not be populated for walks that use only unconfigured + // providers, such as validate. Populating it for those walks might cause + // strange things to happen, because our graph walking machinery doesn't + // always take into account what walk type it's dealing with. + ExternalProviderConfigs map[addrs.RootProviderConfig]providers.Interface + // PlanTimeCheckResults should be populated during the apply phase with // the snapshot of check results that was generated during the plan step. // @@ -143,18 +156,19 @@ func (c *Context) graphWalker(operation walkOperation, opts *graphWalkOpts) *Con } return &ContextGraphWalker{ - Context: c, - State: state, - Config: opts.Config, - RefreshState: refreshState, - PrevRunState: prevRunState, - Changes: changes.SyncWrapper(), - Checks: checkState, - InstanceExpander: instances.NewExpander(), - MoveResults: opts.MoveResults, - Operation: operation, - StopContext: c.runContext, - PlanTimestamp: opts.PlanTimeTimestamp, - Overrides: opts.Overrides, + Context: c, + State: state, + Config: opts.Config, + RefreshState: refreshState, + Overrides: opts.Overrides, + PrevRunState: prevRunState, + Changes: changes.SyncWrapper(), + Checks: checkState, + InstanceExpander: instances.NewExpander(), + ExternalProviderConfigs: opts.ExternalProviderConfigs, + MoveResults: opts.MoveResults, + Operation: operation, + StopContext: c.runContext, + PlanTimestamp: opts.PlanTimeTimestamp, } } diff --git a/internal/terraform/eval_context_builtin.go b/internal/terraform/eval_context_builtin.go index e8252dc5c5..3acc61b578 100644 --- a/internal/terraform/eval_context_builtin.go +++ b/internal/terraform/eval_context_builtin.go @@ -61,6 +61,13 @@ type BuiltinEvalContext struct { // available for use during a graph walk. Plugins *contextPlugins + // ExternalProviderConfigs are pre-configured provider instances passed + // in by the caller, for situations like Stack components where the + // root module isn't designed to be planned and applied in isolation and + // instead expects to recieve certain provider configurations from the + // stack configuration. + ExternalProviderConfigs map[addrs.RootProviderConfig]providers.Interface + Hooks []Hook InputValue UIInput ProviderCache map[string]providers.Interface @@ -134,6 +141,21 @@ func (ctx *BuiltinEvalContext) InitProvider(addr addrs.AbsProviderConfig, config key := addr.String() + if addr.Module.IsRoot() { + rootAddr := addrs.RootProviderConfig{ + Provider: addr.Provider, + Alias: addr.Alias, + } + if external, isExternal := ctx.ExternalProviderConfigs[rootAddr]; isExternal { + // External providers should always be pre-configured by the + // external caller, and so we'll wrap them in a type that + // makes operations like ConfigureProvider and Close be no-op. + wrapped := externalProviderWrapper{external} + ctx.ProviderCache[key] = wrapped + return wrapped, nil + } + } + p, err := ctx.Plugins.NewProviderInstance(addr.Provider) if err != nil { return nil, err diff --git a/internal/terraform/graph_walk_context.go b/internal/terraform/graph_walk_context.go index 1d1b205b89..2171a26b8f 100644 --- a/internal/terraform/graph_walk_context.go +++ b/internal/terraform/graph_walk_context.go @@ -30,21 +30,22 @@ type ContextGraphWalker struct { NullGraphWalker // Configurable values - Context *Context - State *states.SyncState // Used for safe concurrent access to state - RefreshState *states.SyncState // Used for safe concurrent access to state - PrevRunState *states.SyncState // Used for safe concurrent access to state - Changes *plans.ChangesSync // Used for safe concurrent writes to changes - Checks *checks.State // Used for safe concurrent writes of checkable objects and their check results - InstanceExpander *instances.Expander // Tracks our gradual expansion of module and resource instances - Imports []configs.Import - MoveResults refactoring.MoveResults // Read-only record of earlier processing of move statements - Operation walkOperation - StopContext context.Context - RootVariableValues InputValues - Config *configs.Config - PlanTimestamp time.Time - Overrides *mocking.Overrides + Context *Context + State *states.SyncState // Used for safe concurrent access to state + RefreshState *states.SyncState // Used for safe concurrent access to state + PrevRunState *states.SyncState // Used for safe concurrent access to state + Changes *plans.ChangesSync // Used for safe concurrent writes to changes + Checks *checks.State // Used for safe concurrent writes of checkable objects and their check results + InstanceExpander *instances.Expander // Tracks our gradual expansion of module and resource instances + Imports []configs.Import + MoveResults refactoring.MoveResults // Read-only record of earlier processing of move statements + Operation walkOperation + StopContext context.Context + RootVariableValues InputValues + ExternalProviderConfigs map[addrs.RootProviderConfig]providers.Interface + Config *configs.Config + PlanTimestamp time.Time + Overrides *mocking.Overrides // This is an output. Do not set this, nor read it while a graph walk // is in progress. @@ -97,26 +98,27 @@ func (w *ContextGraphWalker) EvalContext() EvalContext { } ctx := &BuiltinEvalContext{ - StopContext: w.StopContext, - Hooks: w.Context.hooks, - InputValue: w.Context.uiInput, - InstanceExpanderValue: w.InstanceExpander, - Plugins: w.Context.plugins, - MoveResultsValue: w.MoveResults, - ProviderCache: w.providerCache, - ProviderInputConfig: w.Context.providerInputConfig, - ProviderLock: &w.providerLock, - ProvisionerCache: w.provisionerCache, - ProvisionerLock: &w.provisionerLock, - ChangesValue: w.Changes, - ChecksValue: w.Checks, - StateValue: w.State, - RefreshStateValue: w.RefreshState, - PrevRunStateValue: w.PrevRunState, - Evaluator: evaluator, - VariableValues: w.variableValues, - VariableValuesLock: &w.variableValuesLock, - OverrideValues: w.Overrides, + StopContext: w.StopContext, + Hooks: w.Context.hooks, + InputValue: w.Context.uiInput, + InstanceExpanderValue: w.InstanceExpander, + Plugins: w.Context.plugins, + ExternalProviderConfigs: w.ExternalProviderConfigs, + MoveResultsValue: w.MoveResults, + ProviderCache: w.providerCache, + ProviderInputConfig: w.Context.providerInputConfig, + ProviderLock: &w.providerLock, + ProvisionerCache: w.provisionerCache, + ProvisionerLock: &w.provisionerLock, + ChangesValue: w.Changes, + ChecksValue: w.Checks, + StateValue: w.State, + RefreshStateValue: w.RefreshState, + PrevRunStateValue: w.PrevRunState, + Evaluator: evaluator, + VariableValues: w.variableValues, + VariableValuesLock: &w.variableValuesLock, + OverrideValues: w.Overrides, } return ctx diff --git a/internal/terraform/providers.go b/internal/terraform/providers.go new file mode 100644 index 0000000000..7708ffd8df --- /dev/null +++ b/internal/terraform/providers.go @@ -0,0 +1,129 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package terraform + +import ( + "fmt" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/configs" + "github.com/hashicorp/terraform/internal/providers" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +// checkExternalProviders verifies that all of the explicitly-declared +// external provider configuration requirements in the root module are +// satisfied by the given instances, and also that all of the given +// instances belong to providers that the overall configuration at least +// uses somewhere. +// +// At the moment we only use external provider configurations for module +// trees acting as Stack components and most other use will not offer any +// externally-configured providers at all, and so the errors returned +// here are somewhat vague to accommodate being used both to describe +// an invalid component configuration and the problem of trying to plan and +// apply a module that wasn't intended to be a root module. +func checkExternalProviders(rootCfg *configs.Config, got map[addrs.RootProviderConfig]providers.Interface) tfdiags.Diagnostics { + var diags tfdiags.Diagnostics + + allowedProviders := map[addrs.Provider]struct{}{} + for _, addr := range rootCfg.ProviderTypes() { + allowedProviders[addr] = struct{}{} + } + requiredConfigs := rootCfg.EffectiveRequiredProviderConfigs() + + // Passed-in provider configurations can only be for providers that this + // configuration actually contains some use of. + // (This is an imprecise way of rejecting undeclared provider configs; + // we can't be precise because Terraform permits implicit default provider + // configurations.) + for cfgAddr := range got { + if _, allowed := allowedProviders[cfgAddr.Provider]; !allowed { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Unexpected provider configuration", + fmt.Sprintf("The plan options include a configuration for provider %s, which is not used anywhere in this configuration.", cfgAddr.Provider), + )) + } else if cfgAddr.Alias != "" && !requiredConfigs.Has(cfgAddr) { + // Additional (aliased) provider configurations must always be + // explicitly declared. + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Unexpected provider configuration", + fmt.Sprintf("The plan options include a configuration for provider %s with alias %q, which is not declared by the root module.", cfgAddr.Provider, cfgAddr.Alias), + )) + } + } + + // The caller _must_ pass external provider configurations for any address + // that's been explicitly declared as required in the required_providers + // block. + for _, cfgAddr := range requiredConfigs { + if _, defined := got[cfgAddr]; !defined { + if cfgAddr.Alias == "" { + // We can't actually return an error here because it's valid + // to leave a default provider configuration implied as long + // as the provider itself will accept an all-null configuration, + // which we won't know until we actually start evaluating. + continue + } else { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Undefined provider configuration", + fmt.Sprintf( + "The root module declares that it requires the caller to pass a configuration for provider %s with alias %q.", + cfgAddr.Provider, cfgAddr.Alias, + ), + )) + } + } + } + + // It isn't valid to pass in a provider for an address that is associated + // with an explicit "provider" block in the root module, since that would + // make it ambiguous whether we're using the passed in one or the declared + // one. + for _, pc := range rootCfg.Module.ProviderConfigs { + absAddr := rootCfg.ResolveAbsProviderAddr(pc.Addr(), addrs.RootModule) + rootAddr := addrs.RootProviderConfig{ + Provider: absAddr.Provider, + Alias: absAddr.Alias, + } + if _, defined := got[rootAddr]; defined { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Unexpected provider configuration", + fmt.Sprintf("The plan options include provider configuration %s, but that conflicts with the explicitly-defined provider configuration at %s.", rootAddr, pc.DeclRange.String()), + )) + } + } + + return diags +} + +// externalProviderWrapper is a wrapper around a provider instance that +// intercepts methods that don't make sense to call on a provider instance +// passed in by an external caller which we assume is owned by the caller +// and pre-configured. +// +// This is a kinda-hacky way to deal with the fact that Terraform Core +// logic tends to assume it is responsible for the full lifecycle of a +// provider instance, which isn't true for externally-provided ones. +type externalProviderWrapper struct { + providers.Interface +} + +var _ providers.Interface = externalProviderWrapper{} + +// ConfigureProvider does nothing because external providers are supposed to +// be pre-configured before passing them to Terraform Core. +func (pw externalProviderWrapper) ConfigureProvider(providers.ConfigureProviderRequest) providers.ConfigureProviderResponse { + return providers.ConfigureProviderResponse{} +} + +// Close does nothing because the caller which provided an external provider +// client is the one responsible for eventually closing it. +func (pw externalProviderWrapper) Close() error { + return nil +}