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 +}