Split out provider schemas vs instances in new engine (#3530)

Signed-off-by: Christian Mesh <christianmesh1@gmail.com>
This commit is contained in:
Christian Mesh
2025-12-01 13:09:58 -05:00
committed by GitHub
parent 5e7397b8a3
commit ffc9c4d556
22 changed files with 429 additions and 379 deletions

View File

@@ -7,28 +7,23 @@ package local
import (
"context"
"errors"
"fmt"
"log"
"os"
"path/filepath"
"sync"
"sync/atomic"
"github.com/apparentlymart/go-versions/versions"
"github.com/davecgh/go-spew/spew"
"github.com/zclconf/go-cty/cty"
"github.com/opentofu/opentofu/internal/addrs"
"github.com/opentofu/opentofu/internal/backend"
"github.com/opentofu/opentofu/internal/configs"
"github.com/opentofu/opentofu/internal/configs/configload"
"github.com/opentofu/opentofu/internal/configs/configschema"
"github.com/opentofu/opentofu/internal/engine/planning"
"github.com/opentofu/opentofu/internal/engine/plugins"
"github.com/opentofu/opentofu/internal/lang/eval"
"github.com/opentofu/opentofu/internal/plans"
"github.com/opentofu/opentofu/internal/providers"
"github.com/opentofu/opentofu/internal/provisioners"
"github.com/opentofu/opentofu/internal/states"
"github.com/opentofu/opentofu/internal/states/statemgr"
"github.com/opentofu/opentofu/internal/tfdiags"
@@ -151,10 +146,7 @@ func (b *Local) opPlanWithExperimentalRuntime(stopCtx context.Context, cancelCtx
prevRoundState = states.NewState() // this is the first round, starting with an empty state
}
plugins := &newRuntimePlugins{
providers: b.ContextOpts.Providers,
provisioners: b.ContextOpts.Provisioners,
}
plugins := plugins.NewRuntimePlugins(b.ContextOpts.Providers, b.ContextOpts.Provisioners)
evalCtx := &eval.EvalContext{
RootModuleDir: op.ConfigDir,
OriginalWorkingDir: b.ContextOpts.Meta.OriginalWorkingDir,
@@ -206,7 +198,7 @@ func (b *Local) opPlanWithExperimentalRuntime(stopCtx context.Context, cancelCtx
return
}
plan, moreDiags := planning.PlanChanges(ctx, prevRoundState, configInst)
plan, moreDiags := planning.PlanChanges(ctx, prevRoundState, configInst, plugins)
diags = diags.Append(moreDiags)
// We intentionally continue with errors here because we make a best effort
// to render a partial plan output even when we have errors, in case
@@ -321,254 +313,3 @@ func (n *newRuntimeModules) ModuleConfig(ctx context.Context, source addrs.Modul
return eval.PrepareTofu2024Module(source, mod), diags
}
type newRuntimePlugins struct {
providers map[addrs.Provider]providers.Factory
provisioners map[string]provisioners.Factory
// unconfiguredInsts is all of the provider instances we've created for
// unconfigured uses such as schema fetching and validation, which we
// currently just leave running for the remainder of the life of this
// object though perhaps we'll do something more clever eventually.
//
// Must hold a lock on mu throughout any access to this map.
unconfiguredInsts map[addrs.Provider]providers.Unconfigured
mu sync.Mutex
}
var _ eval.Providers = (*newRuntimePlugins)(nil)
var _ eval.Provisioners = (*newRuntimePlugins)(nil)
// NewConfiguredProvider implements evalglue.Providers.
func (n *newRuntimePlugins) NewConfiguredProvider(ctx context.Context, provider addrs.Provider, configVal cty.Value) (providers.Configured, tfdiags.Diagnostics) {
inst, diags := n.newProviderInst(ctx, provider)
if diags.HasErrors() {
return nil, diags
}
resp := inst.ConfigureProvider(ctx, providers.ConfigureProviderRequest{
Config: configVal,
// We aren't actually Terraform, so we'll just pretend to be a
// Terraform version that has roughly the same functionality that
// OpenTofu currently has, since providers are permitted to use this to
// adapt their behavior for older versions of Terraform.
TerraformVersion: "1.13.0",
})
diags = diags.Append(resp.Diagnostics)
if resp.Diagnostics.HasErrors() {
return nil, diags
}
return inst, diags
}
// ProviderConfigSchema implements evalglue.Providers.
func (n *newRuntimePlugins) ProviderConfigSchema(ctx context.Context, provider addrs.Provider) (*providers.Schema, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
inst, moreDiags := n.unconfiguredProviderInst(ctx, provider)
diags = diags.Append(moreDiags)
if moreDiags.HasErrors() {
return nil, diags
}
resp := inst.GetProviderSchema(ctx)
diags = diags.Append(resp.Diagnostics)
if resp.Diagnostics.HasErrors() {
return nil, diags
}
return &resp.Provider, diags
}
// ResourceTypeSchema implements evalglue.Providers.
func (n *newRuntimePlugins) ResourceTypeSchema(ctx context.Context, provider addrs.Provider, mode addrs.ResourceMode, typeName string) (*providers.Schema, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
inst, moreDiags := n.unconfiguredProviderInst(ctx, provider)
diags = diags.Append(moreDiags)
if moreDiags.HasErrors() {
return nil, diags
}
resp := inst.GetProviderSchema(ctx)
diags = diags.Append(resp.Diagnostics)
if resp.Diagnostics.HasErrors() {
return nil, diags
}
// NOTE: Callers expect us to return nil if we successfully fetch the
// provider schema and then find there is no matching resource type, because
// the caller is typically in a better position to return a useful error
// message than we are.
var types map[string]providers.Schema
switch mode {
case addrs.ManagedResourceMode:
types = resp.ResourceTypes
case addrs.DataResourceMode:
types = resp.DataSources
case addrs.EphemeralResourceMode:
types = resp.EphemeralResources
default:
// We don't support any other modes, so we'll just treat these as
// a request for a resource type that doesn't exist at all.
return nil, nil
}
ret, ok := types[typeName]
if !ok {
return nil, diags
}
return &ret, diags
}
// ValidateProviderConfig implements evalglue.Providers.
func (n *newRuntimePlugins) ValidateProviderConfig(ctx context.Context, provider addrs.Provider, configVal cty.Value) tfdiags.Diagnostics {
var diags tfdiags.Diagnostics
inst, moreDiags := n.unconfiguredProviderInst(ctx, provider)
diags = diags.Append(moreDiags)
if moreDiags.HasErrors() {
return diags
}
resp := inst.ValidateProviderConfig(ctx, providers.ValidateProviderConfigRequest{
Config: configVal,
})
diags = diags.Append(resp.Diagnostics)
return diags
}
// ValidateResourceConfig implements evalglue.Providers.
func (n *newRuntimePlugins) ValidateResourceConfig(ctx context.Context, provider addrs.Provider, mode addrs.ResourceMode, typeName string, configVal cty.Value) tfdiags.Diagnostics {
var diags tfdiags.Diagnostics
inst, moreDiags := n.unconfiguredProviderInst(ctx, provider)
diags = diags.Append(moreDiags)
if moreDiags.HasErrors() {
return diags
}
switch mode {
case addrs.ManagedResourceMode:
resp := inst.ValidateResourceConfig(ctx, providers.ValidateResourceConfigRequest{
TypeName: typeName,
Config: configVal,
})
diags = diags.Append(resp.Diagnostics)
case addrs.DataResourceMode:
resp := inst.ValidateDataResourceConfig(ctx, providers.ValidateDataResourceConfigRequest{
TypeName: typeName,
Config: configVal,
})
diags = diags.Append(resp.Diagnostics)
case addrs.EphemeralResourceMode:
resp := inst.ValidateEphemeralConfig(ctx, providers.ValidateEphemeralConfigRequest{
TypeName: typeName,
Config: configVal,
})
diags = diags.Append(resp.Diagnostics)
default:
// If we get here then it's a bug because the cases above should
// cover all valid values of [addrs.ResourceMode].
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Unsupported resource mode",
fmt.Sprintf("Attempted to validate resource of unsupported mode %s; this is a bug in OpenTofu.", mode),
))
}
return diags
}
func (m *newRuntimePlugins) unconfiguredProviderInst(ctx context.Context, provider addrs.Provider) (providers.Unconfigured, tfdiags.Diagnostics) {
m.mu.Lock()
defer m.mu.Unlock()
if running, ok := m.unconfiguredInsts[provider]; ok {
return running, nil
}
inst, diags := m.newProviderInst(ctx, provider)
if diags.HasErrors() {
return nil, diags
}
if m.unconfiguredInsts == nil {
m.unconfiguredInsts = make(map[addrs.Provider]providers.Unconfigured)
}
m.unconfiguredInsts[provider] = inst
return inst, diags
}
// newProviderInst creates a new instance of the given provider.
//
// The result is not retained anywhere inside the receiver. Each call to this
// function returns a new object. A successful result is always an unconfigured
// provider, but we return [providers.Interface] in case the caller would like
// to subsequently configure the result before returning it as
// [providers.Configured].
//
// If you intend to use the resulting instance only for "unconfigured"
// operations like fetching schema, use
// [newRuntimePlugins.unconfiguredProviderInst] instead to potentially reuse
// an already-active instance of the same provider.
func (m *newRuntimePlugins) newProviderInst(_ context.Context, provider addrs.Provider) (providers.Interface, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
factory, ok := m.providers[provider]
if !ok {
// FIXME: If this error remains reachable in the final version of this
// code (i.e. if some caller isn't already guaranteeing that all
// providers from the configuration and state are included here) then
// we should make this error message more actionable.
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Provider unavailable",
fmt.Sprintf("This configuration requires provider %q, but it isn't installed.", provider),
))
return nil, diags
}
inst, err := factory()
if err != nil {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Provider failed to start",
fmt.Sprintf("Failed to launch provider %q: %s.", provider, tfdiags.FormatError(err)),
))
return nil, diags
}
return inst, diags
}
// ProvisionerConfigSchema implements evalglue.Provisioners.
func (n *newRuntimePlugins) ProvisionerConfigSchema(ctx context.Context, typeName string) (*configschema.Block, tfdiags.Diagnostics) {
// TODO: Implement this in terms of [newRuntimePlugins.provisioners].
// But provisioners aren't in scope for our "walking skeleton" phase of
// development, so we'll skip this for now.
var diags tfdiags.Diagnostics
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Cannot use providers in new runtime codepath",
fmt.Sprintf("Can't use provisioner %q: new runtime codepath doesn't know how to instantiate provisioners yet", typeName),
))
return nil, diags
}
// Close terminates any plugins that are managed by this object and are still
// running.
func (n *newRuntimePlugins) Close(ctx context.Context) error {
n.mu.Lock()
defer n.mu.Unlock()
var errs error
for addr, p := range n.unconfiguredInsts {
err := p.Close(ctx)
if err != nil {
errs = errors.Join(errs, fmt.Errorf("closing provider %q: %w", addr, err))
}
}
n.unconfiguredInsts = nil // discard all of the memoized instances
return errs
}

View File

@@ -9,6 +9,7 @@ import (
"context"
"fmt"
"github.com/opentofu/opentofu/internal/engine/plugins"
"github.com/opentofu/opentofu/internal/lang/eval"
"github.com/opentofu/opentofu/internal/lang/grapheval"
"github.com/opentofu/opentofu/internal/plans"
@@ -45,10 +46,10 @@ import (
// implementation of how it decides what to plan and how to plan it, and less
// on where it gets the information to make those decisions and how it
// represents those decisions in its return value.
func PlanChanges(ctx context.Context, prevRoundState *states.State, configInst *eval.ConfigInstance) (*plans.Plan, tfdiags.Diagnostics) {
func PlanChanges(ctx context.Context, prevRoundState *states.State, configInst *eval.ConfigInstance, providers plugins.Providers) (*plans.Plan, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
planCtx := newPlanContext(configInst.EvalContext(), prevRoundState)
planCtx := newPlanContext(configInst.EvalContext(), prevRoundState, providers)
// This configInst.DrivePlanning call blocks until the evaluator has
// visited all expressions in the configuration and calls

View File

@@ -10,6 +10,7 @@ import (
"github.com/opentofu/opentofu/internal/addrs"
"github.com/opentofu/opentofu/internal/engine/lifecycle"
"github.com/opentofu/opentofu/internal/engine/plugins"
"github.com/opentofu/opentofu/internal/lang/eval"
"github.com/opentofu/opentofu/internal/plans"
"github.com/opentofu/opentofu/internal/states"
@@ -40,12 +41,14 @@ type planContext struct {
providerInstances *providerInstances
providers plugins.Providers
// TODO: something to track which ephemeral resource instances are currently
// open? (Do we actually need that, or can we just rely on a background
// goroutine to babysit those based on the completion tracker?)
}
func newPlanContext(evalCtx *eval.EvalContext, prevRoundState *states.State) *planContext {
func newPlanContext(evalCtx *eval.EvalContext, prevRoundState *states.State, providers plugins.Providers) *planContext {
if prevRoundState == nil {
prevRoundState = states.NewState()
}
@@ -61,6 +64,7 @@ func newPlanContext(evalCtx *eval.EvalContext, prevRoundState *states.State) *pl
refreshedState: refreshedState.SyncWrapper(),
completion: completion,
providerInstances: newProviderInstances(completion),
providers: providers,
}
}

View File

@@ -23,6 +23,12 @@ func (p *planGlue) planDesiredDataResourceInstance(ctx context.Context, inst *ev
defer p.planCtx.reportResourceInstancePlanCompletion(inst.Addr)
var diags tfdiags.Diagnostics
validateDiags := p.planCtx.providers.ValidateResourceConfig(ctx, inst.Provider, inst.Addr.Resource.Resource.Mode, inst.Addr.Resource.Resource.Type, inst.ConfigVal)
diags = diags.Append(validateDiags)
if diags.HasErrors() {
return cty.DynamicVal, diags
}
if inst.ProviderInstance == nil {
// TODO: Record that this was deferred because we don't yet know which
// provider instance it belongs to.

View File

@@ -16,6 +16,13 @@ import (
func (p *planGlue) planDesiredEphemeralResourceInstance(ctx context.Context, inst *eval.DesiredResourceInstance) (cty.Value, tfdiags.Diagnostics) {
// Regardless of outcome we'll always report that we completed planning.
defer p.planCtx.reportResourceInstancePlanCompletion(inst.Addr)
var diags tfdiags.Diagnostics
validateDiags := p.planCtx.providers.ValidateResourceConfig(ctx, inst.Provider, inst.Addr.Resource.Resource.Mode, inst.Addr.Resource.Resource.Type, inst.ConfigVal)
diags = diags.Append(validateDiags)
if diags.HasErrors() {
return cty.DynamicVal, diags
}
// TODO: Implement
panic("unimplemented")

View File

@@ -35,6 +35,11 @@ type planGlue struct {
var _ eval.PlanGlue = (*planGlue)(nil)
// I'm not sure that this belongs here
func (p *planGlue) ValidateProviderConfig(ctx context.Context, provider addrs.Provider, configVal cty.Value) tfdiags.Diagnostics {
return p.planCtx.providers.ValidateProviderConfig(ctx, provider, configVal)
}
// PlanDesiredResourceInstance implements eval.PlanGlue.
//
// This is called each time the evaluation system discovers a new resource

View File

@@ -66,6 +66,12 @@ func (p *planGlue) planDesiredManagedResourceInstance(ctx context.Context, inst
return cty.DynamicVal, diags
}
validateDiags := p.planCtx.providers.ValidateResourceConfig(ctx, inst.Provider, inst.Addr.Resource.Resource.Mode, inst.Addr.Resource.Resource.Type, inst.ConfigVal)
diags = diags.Append(validateDiags)
if diags.HasErrors() {
return cty.DynamicVal, diags
}
var prevRoundVal cty.Value
var prevRoundPrivate []byte
prevRoundState := p.planCtx.prevRoundState.ResourceInstance(inst.Addr)

View File

@@ -71,7 +71,6 @@ func (pi *providerInstances) ProviderClient(ctx context.Context, addr addrs.AbsP
oracle := planGlue.oracle
once := pi.active.Get(addr)
return once.Do(ctx, func(ctx context.Context) (providers.Configured, tfdiags.Diagnostics) {
evalCtx := oracle.EvalContext(ctx)
configVal := oracle.ProviderInstanceConfig(ctx, addr)
if configVal == cty.NilVal {
// This suggests that the provider instance has an invalid
@@ -93,7 +92,7 @@ func (pi *providerInstances) ProviderClient(ctx context.Context, addr addrs.AbsP
// then this should return "nil, nil" in the error case so that the
// caller will treat it the same as a "configuration not valid enough"
// problem.
ret, diags := evalCtx.Providers.NewConfiguredProvider(ctx, addr.Config.Config.Provider, configVal)
ret, diags := planGlue.planCtx.providers.NewConfiguredProvider(ctx, addr.Config.Config.Provider, configVal)
// This background goroutine deals with closing the provider once it's
// no longer needed, and with asking it to gracefully stop if our

View File

@@ -0,0 +1,338 @@
// Copyright (c) The OpenTofu Authors
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package plugins
import (
"context"
"errors"
"fmt"
"sync"
"github.com/opentofu/opentofu/internal/addrs"
"github.com/opentofu/opentofu/internal/configs/configschema"
"github.com/opentofu/opentofu/internal/lang/eval"
"github.com/opentofu/opentofu/internal/providers"
"github.com/opentofu/opentofu/internal/provisioners"
"github.com/opentofu/opentofu/internal/tfdiags"
"github.com/zclconf/go-cty/cty"
)
type Plugins interface {
Providers
Provisioners
}
// Providers is implemented by callers of this package to provide access
// to the providers needed by a configuration without this package needing
// to know anything about how provider plugins work, or whether plugins are
// even being used.
type Providers interface {
eval.ProvidersSchema
// ValidateProviderConfig runs provider-specific logic to check whether
// the given configuration is valid. Returns at least one error diagnostic
// if the configuration is not valid, and may also return warning
// diagnostics regardless of whether the configuration is valid.
//
// The given config value is guaranteed to be an object conforming to
// the schema returned by a previous call to ProviderConfigSchema for
// the same provider.
ValidateProviderConfig(ctx context.Context, provider addrs.Provider, configVal cty.Value) tfdiags.Diagnostics
// ValidateResourceConfig runs provider-specific logic to check whether
// the given configuration is valid. Returns at least one error diagnostic
// if the configuration is not valid, and may also return warning
// diagnostics regardless of whether the configuration is valid.
//
// The given config value is guaranteed to be an object conforming to
// the schema returned by a previous call to ResourceTypeSchema for
// the same resource type.
ValidateResourceConfig(ctx context.Context, provider addrs.Provider, mode addrs.ResourceMode, typeName string, configVal cty.Value) tfdiags.Diagnostics
// NewConfiguredProvider starts a _configured_ instance of the given
// provider using the given configuration value.
//
// The evaluation system itself makes no use of configured providers, but
// higher-level processes wrapping it (e.g. the plan and apply engines)
// need to use configured providers for actions related to resources, etc,
// and so this is for their benefit to help ensure that they are definitely
// creating a configured instance of the same provider that other methods
// would be using to return schema information and validation results.
//
// It's the caller's responsibility to ensure that the given configuration
// value is valid according to the provider's schema and validation rules.
// That's usually achieved by taking a value provided by the evaluation
// system, which would then have already been processed using the results
// from [Providers.ProviderConfigSchema] and
// [Providers.ValidateProviderConfig]. If the returned diagnostics contains
// errors then the [providers.Configured] result is invalid and must not be
// used.
NewConfiguredProvider(ctx context.Context, provider addrs.Provider, configVal cty.Value) (providers.Configured, tfdiags.Diagnostics)
Close(ctx context.Context) error
}
type Provisioners interface {
eval.ProvisionersSchema
}
type newRuntimePlugins struct {
providers map[addrs.Provider]providers.Factory
provisioners map[string]provisioners.Factory
// unconfiguredInsts is all of the provider instances we've created for
// unconfigured uses such as schema fetching and validation, which we
// currently just leave running for the remainder of the life of this
// object though perhaps we'll do something more clever eventually.
//
// Must hold a lock on mu throughout any access to this map.
unconfiguredInsts map[addrs.Provider]providers.Unconfigured
mu sync.Mutex
}
var _ Providers = (*newRuntimePlugins)(nil)
var _ Provisioners = (*newRuntimePlugins)(nil)
func NewRuntimePlugins(providers map[addrs.Provider]providers.Factory, provisioners map[string]provisioners.Factory) Plugins {
return &newRuntimePlugins{
providers: providers,
provisioners: provisioners,
}
}
// NewConfiguredProvider implements evalglue.Providers.
func (n *newRuntimePlugins) NewConfiguredProvider(ctx context.Context, provider addrs.Provider, configVal cty.Value) (providers.Configured, tfdiags.Diagnostics) {
inst, diags := n.newProviderInst(ctx, provider)
if diags.HasErrors() {
return nil, diags
}
resp := inst.ConfigureProvider(ctx, providers.ConfigureProviderRequest{
Config: configVal,
// We aren't actually Terraform, so we'll just pretend to be a
// Terraform version that has roughly the same functionality that
// OpenTofu currently has, since providers are permitted to use this to
// adapt their behavior for older versions of Terraform.
TerraformVersion: "1.13.0",
})
diags = diags.Append(resp.Diagnostics)
if resp.Diagnostics.HasErrors() {
return nil, diags
}
return inst, diags
}
// ProviderConfigSchema implements evalglue.Providers.
func (n *newRuntimePlugins) ProviderConfigSchema(ctx context.Context, provider addrs.Provider) (*providers.Schema, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
inst, moreDiags := n.unconfiguredProviderInst(ctx, provider)
diags = diags.Append(moreDiags)
if moreDiags.HasErrors() {
return nil, diags
}
resp := inst.GetProviderSchema(ctx)
diags = diags.Append(resp.Diagnostics)
if resp.Diagnostics.HasErrors() {
return nil, diags
}
return &resp.Provider, diags
}
// ResourceTypeSchema implements evalglue.Providers.
func (n *newRuntimePlugins) ResourceTypeSchema(ctx context.Context, provider addrs.Provider, mode addrs.ResourceMode, typeName string) (*providers.Schema, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
inst, moreDiags := n.unconfiguredProviderInst(ctx, provider)
diags = diags.Append(moreDiags)
if moreDiags.HasErrors() {
return nil, diags
}
resp := inst.GetProviderSchema(ctx)
diags = diags.Append(resp.Diagnostics)
if resp.Diagnostics.HasErrors() {
return nil, diags
}
// NOTE: Callers expect us to return nil if we successfully fetch the
// provider schema and then find there is no matching resource type, because
// the caller is typically in a better position to return a useful error
// message than we are.
var types map[string]providers.Schema
switch mode {
case addrs.ManagedResourceMode:
types = resp.ResourceTypes
case addrs.DataResourceMode:
types = resp.DataSources
case addrs.EphemeralResourceMode:
types = resp.EphemeralResources
default:
// We don't support any other modes, so we'll just treat these as
// a request for a resource type that doesn't exist at all.
return nil, nil
}
ret, ok := types[typeName]
if !ok {
return nil, diags
}
return &ret, diags
}
// ValidateProviderConfig implements evalglue.Providers.
func (n *newRuntimePlugins) ValidateProviderConfig(ctx context.Context, provider addrs.Provider, configVal cty.Value) tfdiags.Diagnostics {
var diags tfdiags.Diagnostics
inst, moreDiags := n.unconfiguredProviderInst(ctx, provider)
diags = diags.Append(moreDiags)
if moreDiags.HasErrors() {
return diags
}
resp := inst.ValidateProviderConfig(ctx, providers.ValidateProviderConfigRequest{
Config: configVal,
})
diags = diags.Append(resp.Diagnostics)
return diags
}
// ValidateResourceConfig implements evalglue.Providers.
func (n *newRuntimePlugins) ValidateResourceConfig(ctx context.Context, provider addrs.Provider, mode addrs.ResourceMode, typeName string, configVal cty.Value) tfdiags.Diagnostics {
var diags tfdiags.Diagnostics
inst, moreDiags := n.unconfiguredProviderInst(ctx, provider)
diags = diags.Append(moreDiags)
if moreDiags.HasErrors() {
return diags
}
switch mode {
case addrs.ManagedResourceMode:
resp := inst.ValidateResourceConfig(ctx, providers.ValidateResourceConfigRequest{
TypeName: typeName,
Config: configVal,
})
diags = diags.Append(resp.Diagnostics)
case addrs.DataResourceMode:
resp := inst.ValidateDataResourceConfig(ctx, providers.ValidateDataResourceConfigRequest{
TypeName: typeName,
Config: configVal,
})
diags = diags.Append(resp.Diagnostics)
case addrs.EphemeralResourceMode:
resp := inst.ValidateEphemeralConfig(ctx, providers.ValidateEphemeralConfigRequest{
TypeName: typeName,
Config: configVal,
})
diags = diags.Append(resp.Diagnostics)
default:
// If we get here then it's a bug because the cases above should
// cover all valid values of [addrs.ResourceMode].
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Unsupported resource mode",
fmt.Sprintf("Attempted to validate resource of unsupported mode %s; this is a bug in OpenTofu.", mode),
))
}
return diags
}
func (m *newRuntimePlugins) unconfiguredProviderInst(ctx context.Context, provider addrs.Provider) (providers.Unconfigured, tfdiags.Diagnostics) {
m.mu.Lock()
defer m.mu.Unlock()
if running, ok := m.unconfiguredInsts[provider]; ok {
return running, nil
}
inst, diags := m.newProviderInst(ctx, provider)
if diags.HasErrors() {
return nil, diags
}
if m.unconfiguredInsts == nil {
m.unconfiguredInsts = make(map[addrs.Provider]providers.Unconfigured)
}
m.unconfiguredInsts[provider] = inst
return inst, diags
}
// newProviderInst creates a new instance of the given provider.
//
// The result is not retained anywhere inside the receiver. Each call to this
// function returns a new object. A successful result is always an unconfigured
// provider, but we return [providers.Interface] in case the caller would like
// to subsequently configure the result before returning it as
// [providers.Configured].
//
// If you intend to use the resulting instance only for "unconfigured"
// operations like fetching schema, use
// [newRuntimePlugins.unconfiguredProviderInst] instead to potentially reuse
// an already-active instance of the same provider.
func (m *newRuntimePlugins) newProviderInst(_ context.Context, provider addrs.Provider) (providers.Interface, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
factory, ok := m.providers[provider]
if !ok {
// FIXME: If this error remains reachable in the final version of this
// code (i.e. if some caller isn't already guaranteeing that all
// providers from the configuration and state are included here) then
// we should make this error message more actionable.
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Provider unavailable",
fmt.Sprintf("This configuration requires provider %q, but it isn't installed.", provider),
))
return nil, diags
}
inst, err := factory()
if err != nil {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Provider failed to start",
fmt.Sprintf("Failed to launch provider %q: %s.", provider, tfdiags.FormatError(err)),
))
return nil, diags
}
return inst, diags
}
// ProvisionerConfigSchema implements evalglue.Provisioners.
func (n *newRuntimePlugins) ProvisionerConfigSchema(ctx context.Context, typeName string) (*configschema.Block, tfdiags.Diagnostics) {
// TODO: Implement this in terms of [newRuntimePlugins.provisioners].
// But provisioners aren't in scope for our "walking skeleton" phase of
// development, so we'll skip this for now.
var diags tfdiags.Diagnostics
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Cannot use providers in new runtime codepath",
fmt.Sprintf("Can't use provisioner %q: new runtime codepath doesn't know how to instantiate provisioners yet", typeName),
))
return nil, diags
}
// Close terminates any plugins that are managed by this object and are still
// running.
func (n *newRuntimePlugins) Close(ctx context.Context) error {
n.mu.Lock()
defer n.mu.Unlock()
var errs error
for addr, p := range n.unconfiguredInsts {
err := p.Close(ctx)
if err != nil {
errs = errors.Join(errs, fmt.Errorf("closing provider %q: %w", addr, err))
}
}
n.unconfiguredInsts = nil // discard all of the memoized instances
return errs
}

View File

@@ -26,6 +26,9 @@ import (
// each other, and so implementations must use suitable synchronization to
// avoid data races between calls.
type PlanGlue interface {
// I'm not sure that this belongs here
ValidateProviderConfig(ctx context.Context, provider addrs.Provider, configVal cty.Value) tfdiags.Diagnostics
// Creates planned action(s) for the given resource instance and return
// the planned new state that would result from those actions.
//
@@ -215,6 +218,11 @@ type planningEvalGlue struct {
var _ evalglue.Glue = (*planningEvalGlue)(nil)
// ValidateProviderConfig implements evalglue.Glue.
func (p *planningEvalGlue) ValidateProviderConfig(ctx context.Context, provider addrs.Provider, configVal cty.Value) tfdiags.Diagnostics {
return p.planEngineGlue.ValidateProviderConfig(ctx, provider, configVal)
}
// ResourceInstanceValue implements evalglue.Glue.
func (p *planningEvalGlue) ResourceInstanceValue(ctx context.Context, ri *configgraph.ResourceInstance, configVal cty.Value, providerInst configgraph.Maybe[*configgraph.ProviderInstance]) (cty.Value, tfdiags.Diagnostics) {
desired := &DesiredResourceInstance{

View File

@@ -318,13 +318,18 @@ func TestPlan_managedResourceUnknownCount(t *testing.T) {
type planGlueCallLog struct {
oracle *eval.PlanningOracle
providers eval.Providers
providers eval.ProvidersSchema
resourceInstanceRequests addrs.Map[addrs.AbsResourceInstance, *eval.DesiredResourceInstance]
providerInstanceConfigs addrs.Map[addrs.AbsProviderInstanceCorrect, cty.Value]
mu sync.Mutex
}
// ValidateProviderConfig implements eval.PlanGlue
func (p *planGlueCallLog) ValidateProviderConfig(ctx context.Context, provider addrs.Provider, configVal cty.Value) tfdiags.Diagnostics {
return nil
}
// PlanDesiredResourceInstance implements eval.PlanGlue.
func (p *planGlueCallLog) PlanDesiredResourceInstance(ctx context.Context, inst *eval.DesiredResourceInstance) (cty.Value, tfdiags.Diagnostics) {
p.mu.Lock()

View File

@@ -165,7 +165,14 @@ type preparationGlue struct {
// preparationGlue uses provider schema information to prepare placeholder
// "final state" values for resource instances because validation does
// not use information from the state.
providers Providers
providers ProvidersSchema
}
// ValidateProviderConfig implements evalglue.Glue.
func (v *preparationGlue) ValidateProviderConfig(ctx context.Context, provider addrs.Provider, configVal cty.Value) tfdiags.Diagnostics {
// TODO maybe this is why we create validation glue?
//return v.providers.ValidateProviderConfig(ctx, provider, configVal)
return nil
}
// ResourceInstanceValue implements evaluationGlue.
@@ -181,6 +188,14 @@ func (v *preparationGlue) ResourceInstanceValue(ctx context.Context, ri *configg
return cty.DynamicVal, diags
}
/* TODO maybe this is why we create validation glue?
validateDiags := v.providers.ValidateResourceConfig(ctx, ri.Provider, ri.Addr.Resource.Resource.Mode, ri.Addr.Resource.Resource.Type, configVal)
diags = diags.Append(validateDiags)
if diags.HasErrors() {
// Provider indicated an invalid resource configuration
return cty.DynamicVal, diags
}*/
// FIXME: If we have a managed or data resource instance, as opposed to
// an ephemeral resource instance, then we should check to make sure
// that ephemeral-marked values only appear in parts of the configVal

View File

@@ -20,8 +20,8 @@ import (
// here so that other parts of OpenTofu can interact with the evaluator.
type EvalContext = evalglue.EvalContext
type Providers = evalglue.Providers
type Provisioners = evalglue.Provisioners
type ProvidersSchema = evalglue.ProvidersSchema
type ProvisionersSchema = evalglue.ProvisionersSchema
type ExternalModules = evalglue.ExternalModules
type UncompiledModule = evalglue.UncompiledModule
@@ -33,10 +33,10 @@ func ModulesForTesting(modules map[addrs.ModuleSourceLocal]*configs.Module) Exte
return tofu2024.ModulesForTesting(modules)
}
func ProvidersForTesting(schemas map[addrs.Provider]*providers.GetProviderSchemaResponse) Providers {
func ProvidersForTesting(schemas map[addrs.Provider]*providers.GetProviderSchemaResponse) ProvidersSchema {
return evalglue.ProvidersForTesting(schemas)
}
func ProvisionersForTesting(schemas map[string]*configschema.Block) Provisioners {
func ProvisionersForTesting(schemas map[string]*configschema.Block) ProvisionersSchema {
return evalglue.ProvisionersForTesting(schemas)
}

View File

@@ -92,15 +92,6 @@ func (ri *ResourceInstance) Value(ctx context.Context) (cty.Value, tfdiags.Diagn
return exprs.AsEvalError(cty.DynamicVal), diags
}
// We need some help from our caller to validate the configuration,
// which typically involves asking whatever provider this type of
// resource belongs to.
moreDiags := ri.Glue.ValidateConfig(ctx, configVal)
diags = diags.Append(moreDiags)
if diags.HasErrors() {
return exprs.AsEvalError(cty.DynamicVal), diags
}
providerInst, providerInstMarks, moreDiags := ri.ProviderInstance(ctx)
diags = diags.Append(moreDiags)
if diags.HasErrors() {

View File

@@ -27,11 +27,6 @@ import (
// with the self-dependency detection used by this package to prevent
// deadlocks.
type ResourceInstanceGlue interface {
// ValidateConfig determines whether the given value is a valid
// configuration for a resource instance of the expected type, presumably
// by asking the provider that the resource type belongs to.
ValidateConfig(ctx context.Context, configVal cty.Value) tfdiags.Diagnostics
// ResultValue returns the results of whatever side-effects are happening
// for this resource in the current phase, such as getting the "planned new
// state" of the resource instance during the plan phase, while keeping this

View File

@@ -32,11 +32,11 @@ type EvalContext struct {
// Providers gives access to all of the providers available for use
// in this context.
Providers Providers
Providers ProvidersSchema
// Provisioners gives access to all of the provisioners available for
// use in this context.
Provisioners Provisioners
Provisioners ProvisionersSchema
// RootModuleDir and OriginalWorkingDir both represent local filesystem
// directories whose paths are exposed in various ways to expressions

View File

@@ -9,7 +9,6 @@ import (
"context"
"github.com/apparentlymart/go-versions/versions"
"github.com/zclconf/go-cty/cty"
"github.com/opentofu/opentofu/internal/addrs"
"github.com/opentofu/opentofu/internal/configs/configschema"
@@ -34,10 +33,10 @@ type ExternalModules interface {
}
// Providers is implemented by callers of this package to provide access
// to the providers needed by a configuration without this package needing
// to the provider schemas needed by a configuration without this package needing
// to know anything about how provider plugins work, or whether plugins are
// even being used.
type Providers interface {
type ProvidersSchema interface {
// ProviderConfigSchema returns the schema that should be used to evaluate
// a "provider" block associated with the given provider.
//
@@ -46,16 +45,6 @@ type Providers interface {
// configuration is needed.
ProviderConfigSchema(ctx context.Context, provider addrs.Provider) (*providers.Schema, tfdiags.Diagnostics)
// ValidateProviderConfig runs provider-specific logic to check whether
// the given configuration is valid. Returns at least one error diagnostic
// if the configuration is not valid, and may also return warning
// diagnostics regardless of whether the configuration is valid.
//
// The given config value is guaranteed to be an object conforming to
// the schema returned by a previous call to ProviderConfigSchema for
// the same provider.
ValidateProviderConfig(ctx context.Context, provider addrs.Provider, configVal cty.Value) tfdiags.Diagnostics
// ResourceTypeSchema returns the schema for configuration and state of
// a resource of the given type, or nil if the given provider does not
// offer any such resource type.
@@ -63,41 +52,11 @@ type Providers interface {
// Returns error diagnostics if the given provider isn't available for use
// at all, regardless of the resource type.
ResourceTypeSchema(ctx context.Context, provider addrs.Provider, mode addrs.ResourceMode, typeName string) (*providers.Schema, tfdiags.Diagnostics)
// ValidateResourceConfig runs provider-specific logic to check whether
// the given configuration is valid. Returns at least one error diagnostic
// if the configuration is not valid, and may also return warning
// diagnostics regardless of whether the configuration is valid.
//
// The given config value is guaranteed to be an object conforming to
// the schema returned by a previous call to ResourceTypeSchema for
// the same resource type.
ValidateResourceConfig(ctx context.Context, provider addrs.Provider, mode addrs.ResourceMode, typeName string, configVal cty.Value) tfdiags.Diagnostics
// NewConfiguredProvider starts a _configured_ instance of the given
// provider using the given configuration value.
//
// The evaluation system itself makes no use of configured providers, but
// higher-level processes wrapping it (e.g. the plan and apply engines)
// need to use configured providers for actions related to resources, etc,
// and so this is for their benefit to help ensure that they are definitely
// creating a configured instance of the same provider that other methods
// would be using to return schema information and validation results.
//
// It's the caller's responsibility to ensure that the given configuration
// value is valid according to the provider's schema and validation rules.
// That's usually achieved by taking a value provided by the evaluation
// system, which would then have already been processed using the results
// from [Providers.ProviderConfigSchema] and
// [Providers.ValidateProviderConfig]. If the returned diagnostics contains
// errors then the [providers.Configured] result is invalid and must not be
// used.
NewConfiguredProvider(ctx context.Context, provider addrs.Provider, configVal cty.Value) (providers.Configured, tfdiags.Diagnostics)
}
// Providers is implemented by callers of this package to provide access
// to the provisioners needed by a configuration.
type Provisioners interface {
type ProvisionersSchema interface {
// ProvisionerConfigSchema returns the schema that should be used to
// evaluate a "provisioner" block associated with the given provisioner
// type, or nil if there is no known provisioner of the given name.
@@ -124,14 +83,14 @@ func ensureExternalModules(given ExternalModules) ExternalModules {
return given
}
func ensureProviders(given Providers) Providers {
func ensureProviders(given ProvidersSchema) ProvidersSchema {
if given == nil {
return emptyDependencies{}
}
return given
}
func ensureProvisioners(given Provisioners) Provisioners {
func ensureProvisioners(given ProvisionersSchema) ProvisionersSchema {
if given == nil {
return emptyDependencies{}
}
@@ -160,14 +119,6 @@ func (e emptyDependencies) ProviderConfigSchema(ctx context.Context, provider ad
return nil, diags
}
// ValidateProviderConfig implements Providers.
func (e emptyDependencies) ValidateProviderConfig(ctx context.Context, provider addrs.Provider, configVal cty.Value) tfdiags.Diagnostics {
// Because our ResourceTypeSchema implementation never succeeds, there
// can never be a call to this function in practice and so we'll just
// do nothing here.
return nil
}
// ResourceTypeSchema implements Providers.
func (e emptyDependencies) ResourceTypeSchema(ctx context.Context, provider addrs.Provider, mode addrs.ResourceMode, typeName string) (*providers.Schema, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
@@ -179,25 +130,6 @@ func (e emptyDependencies) ResourceTypeSchema(ctx context.Context, provider addr
return nil, diags
}
// ValidateResourceConfig implements Providers.
func (e emptyDependencies) ValidateResourceConfig(ctx context.Context, provider addrs.Provider, mode addrs.ResourceMode, typeName string, configVal cty.Value) tfdiags.Diagnostics {
// Because our ResourceTypeSchema implementation never succeeds, there
// can never be a call to this function in practice and so we'll just
// do nothing here.
return nil
}
// NewConfiguredProvider implements Providers.
func (e emptyDependencies) NewConfiguredProvider(ctx context.Context, provider addrs.Provider, configVal cty.Value) (providers.Configured, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"No providers are available",
"There are no providers available for use in this context.",
))
return nil, diags
}
// ProvisionerConfigSchema implements Provisioners.
func (e emptyDependencies) ProvisionerConfigSchema(ctx context.Context, typeName string) (*configschema.Block, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics

View File

@@ -21,15 +21,15 @@ import (
// information directly from the given map.
//
// This is intended for unit testing only.
func ProvidersForTesting(schemas map[addrs.Provider]*providers.GetProviderSchemaResponse) Providers {
func ProvidersForTesting(schemas map[addrs.Provider]*providers.GetProviderSchemaResponse) ProvidersSchema {
return providersStatic{schemas}
}
// ProvisionersForTesting returns a [Provisioners] implementation that just
// ProvisionersForTesting returns a [ProvisionersSchema] implementation that just
// returns information directly from the given map.
//
// This is intended for unit testing only.
func ProvisionersForTesting(schemas map[string]*configschema.Block) Provisioners {
func ProvisionersForTesting(schemas map[string]*configschema.Block) ProvisionersSchema {
return provisionersStatic{schemas}
}

View File

@@ -10,6 +10,7 @@ import (
"github.com/zclconf/go-cty/cty"
"github.com/opentofu/opentofu/internal/addrs"
"github.com/opentofu/opentofu/internal/lang/eval/internal/configgraph"
"github.com/opentofu/opentofu/internal/tfdiags"
)
@@ -24,6 +25,9 @@ import (
// of [Glue] to adapt that into the minimal set of operations
// that are needed regardless of what overall operation we're currently driving.
type Glue interface {
// I'm not sure that this belongs here
ValidateProviderConfig(ctx context.Context, provider addrs.Provider, configVal cty.Value) tfdiags.Diagnostics
// ResourceInstanceValue returns the result value for the given resource
// instance.
//

View File

@@ -98,6 +98,7 @@ func CompileModuleInstance(
module.ProviderRequirements.RequiredProviders,
call.CalleeAddr,
call.EvalContext.Providers,
call.EvaluationGlue.ValidateProviderConfig,
)
providersSidechannel := compileModuleProvidersSidechannel(ctx, call.ProvidersFromParent, ret.providerConfigNodes)

View File

@@ -28,7 +28,8 @@ func compileModuleInstanceProviderConfigs(
declScope exprs.Scope,
reqdProviders map[string]*configs.RequiredProvider,
moduleInstanceAddr addrs.ModuleInstance,
providers evalglue.Providers,
providers evalglue.ProvidersSchema,
validateProviderConfig func(context.Context, addrs.Provider, cty.Value) tfdiags.Diagnostics,
) map[addrs.LocalProviderConfig]*configgraph.ProviderConfig {
// FIXME: The following is just enough to make simple examples work, but
// doesn't closely match the rather complicated way that OpenTofu has
@@ -89,7 +90,7 @@ func compileModuleInstanceProviderConfigs(
exprs.NewClosure(configEvalable, instanceScope),
),
ValidateConfig: func(ctx context.Context, v cty.Value) tfdiags.Diagnostics {
return providers.ValidateProviderConfig(ctx, providerAddr, v)
return validateProviderConfig(ctx, providerAddr, v)
},
}
},
@@ -156,7 +157,7 @@ func compileModuleInstanceProviderConfigs(
exprs.NewClosure(configEvalable, instanceScope),
),
ValidateConfig: func(ctx context.Context, v cty.Value) tfdiags.Diagnostics {
return providers.ValidateProviderConfig(ctx, providerAddr, v)
return validateProviderConfig(ctx, providerAddr, v)
},
}
},

View File

@@ -27,7 +27,7 @@ func compileModuleInstanceResources(
declScope exprs.Scope,
providersSideChannel *moduleProvidersSideChannel,
moduleInstanceAddr addrs.ModuleInstance,
providers evalglue.Providers,
providers evalglue.ProvidersSchema,
getResultValue func(context.Context, *configgraph.ResourceInstance, cty.Value, configgraph.Maybe[*configgraph.ProviderInstance]) (cty.Value, tfdiags.Diagnostics),
) map[addrs.Resource]*configgraph.Resource {
ret := make(map[addrs.Resource]*configgraph.Resource, len(managedConfigs)+len(dataConfigs)+len(ephemeralConfigs))
@@ -52,7 +52,7 @@ func compileModuleInstanceResource(
declScope exprs.Scope,
providersSideChannel *moduleProvidersSideChannel,
moduleInstanceAddr addrs.ModuleInstance,
providers evalglue.Providers,
providers evalglue.ProvidersSchema,
getResultValue func(context.Context, *configgraph.ResourceInstance, cty.Value, configgraph.Maybe[*configgraph.ProviderInstance]) (cty.Value, tfdiags.Diagnostics),
) (addrs.Resource, *configgraph.Resource) {
resourceAddr := config.Addr()
@@ -105,9 +105,6 @@ func compileModuleInstanceResource(
// in the current phase. (The planned new state during the plan
// phase, for example.)
inst.Glue = &resourceInstanceGlue{
validateConfig: func(ctx context.Context, configVal cty.Value) tfdiags.Diagnostics {
return providers.ValidateResourceConfig(ctx, config.Provider, resourceAddr.Mode, resourceAddr.Type, configVal)
},
getResultValue: func(ctx context.Context, configVal cty.Value, providerInst configgraph.Maybe[*configgraph.ProviderInstance]) (cty.Value, tfdiags.Diagnostics) {
return getResultValue(ctx, inst, configVal, providerInst)
},
@@ -123,15 +120,9 @@ func compileModuleInstanceResource(
// to us for needs that require interacting with outside concerns like
// provider plugins, an active plan or apply process, etc.
type resourceInstanceGlue struct {
validateConfig func(context.Context, cty.Value) tfdiags.Diagnostics
getResultValue func(context.Context, cty.Value, configgraph.Maybe[*configgraph.ProviderInstance]) (cty.Value, tfdiags.Diagnostics)
}
// ValidateConfig implements configgraph.ResourceInstanceGlue.
func (r *resourceInstanceGlue) ValidateConfig(ctx context.Context, configVal cty.Value) tfdiags.Diagnostics {
return r.validateConfig(ctx, configVal)
}
// ResultValue implements configgraph.ResourceInstanceGlue.
func (r *resourceInstanceGlue) ResultValue(ctx context.Context, configVal cty.Value, providerInst configgraph.Maybe[*configgraph.ProviderInstance]) (cty.Value, tfdiags.Diagnostics) {
return r.getResultValue(ctx, configVal, providerInst)