mirror of
https://github.com/opentffoundation/opentf.git
synced 2025-12-19 17:59:05 -05:00
339 lines
12 KiB
Go
339 lines
12 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"
|
|
"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
|
|
}
|