opentofu/internal/engine/plugins/plugins.go
Christian Mesh 326ae112f2 Introduce plugin managers
Provider and Provisioner Managers encapsulate all of
the logic nessesary to start, manage, and stop plugins.

They also have the advantage of sharing a schema cache between
managers started in different parts of the application.

Signed-off-by: Christian Mesh <christianmesh1@gmail.com>
2026-01-15 10:50:29 -05:00

204 lines
7.9 KiB
Go

// 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"
"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/plugins"
"github.com/opentofu/opentofu/internal/providers"
"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 plugins.ProviderManager
provisioners plugins.ProvisionerManager
}
var _ Providers = (*newRuntimePlugins)(nil)
var _ Provisioners = (*newRuntimePlugins)(nil)
func NewRuntimePlugins(plugins plugins.Library) Plugins {
return &newRuntimePlugins{
providers: plugins.NewProviderManager(),
provisioners: plugins.NewProvisionerManager(),
}
}
// NewConfiguredProvider implements evalglue.Providers.
func (n *newRuntimePlugins) NewConfiguredProvider(ctx context.Context, provider addrs.Provider, configVal cty.Value) (providers.Configured, tfdiags.Diagnostics) {
// TODO addr
return n.providers.NewConfiguredProvider(ctx, addrs.AbsProviderInstanceCorrect{
Config: addrs.AbsProviderConfigCorrect{
Config: addrs.ProviderConfigCorrect{
Provider: provider,
},
},
}, configVal)
}
// ProviderConfigSchema implements evalglue.Providers.
func (n *newRuntimePlugins) ProviderConfigSchema(ctx context.Context, provider addrs.Provider) (*providers.Schema, tfdiags.Diagnostics) {
schema, diags := n.providers.GetProviderSchema(ctx, provider)
if diags.HasErrors() {
return nil, diags
}
return &schema.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) {
schema, diags := n.providers.GetProviderSchema(ctx, provider)
if diags.HasErrors() {
return nil, diags
}
var types map[string]providers.Schema
switch mode {
case addrs.ManagedResourceMode:
types = schema.ResourceTypes
case addrs.DataResourceMode:
types = schema.DataSources
case addrs.EphemeralResourceMode:
types = schema.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, diags
}
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 {
return n.providers.Unconfigured(provider, func(inst providers.Unconfigured) tfdiags.Diagnostics {
configVal, _ = configVal.UnmarkDeep()
// NOTE: we ignore resp.PreparedConfig in this codepath, but not below in ConfigureProvider
// This is to handle some oddities in tfplugin5, documented in providers.ValidateProviderConfigResponse
return inst.ValidateProviderConfig(ctx, providers.ValidateProviderConfigRequest{Config: configVal}).Diagnostics
})
}
// ValidateResourceConfig implements evalglue.Providers.
func (n *newRuntimePlugins) ValidateResourceConfig(ctx context.Context, provider addrs.Provider, mode addrs.ResourceMode, typeName string, configVal cty.Value) tfdiags.Diagnostics {
return n.providers.Unconfigured(provider, func(inst providers.Unconfigured) tfdiags.Diagnostics {
configVal, _ = configVal.UnmarkDeep()
switch mode {
case addrs.ManagedResourceMode:
return inst.ValidateResourceConfig(ctx, providers.ValidateResourceConfigRequest{
TypeName: typeName,
Config: configVal,
}).Diagnostics
case addrs.DataResourceMode:
return inst.ValidateDataResourceConfig(ctx, providers.ValidateDataResourceConfigRequest{
TypeName: typeName,
Config: configVal,
}).Diagnostics
case addrs.EphemeralResourceMode:
return inst.ValidateEphemeralConfig(ctx, providers.ValidateEphemeralConfigRequest{
TypeName: typeName,
Config: configVal,
}).Diagnostics
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
}
})
}
// 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 {
return errors.Join(n.providers.Close(ctx), n.provisioners.Close())
}