Add POC for preconfigured providers

The idea is quite simple but the commited implementation is definitely
not in the final shape.
Right now, OpenTofu is handling the providers, from the installation,
sub-process mantainance, configuration and cleanup of it.
By doing so, this adds a lot of run time.
This commit is exploring on how to instruct OpenTofu to work with
providers pre-configured and hosted by a different process.
Therefore, a providers server has been created and can be found in
https://github.com/opentofu/hackathon-providers-server.

With this commit, was introduced the support of this new experimental
block:
terraform {
  // ...
  preconfigured_providers {
    playground = {
      source = "opentofu/playground"
      addr = "localhost:50051"
      protocol_version = 6
    }
  }
}

Having this configured, OpenTofu will just create a gRPC client to
communicate with the indicated addr.
The server is running the same protobuf schema and it's supposed to
function exactly the same as a provider mantained by OpenTofu itself.

One important thing to note, is that for every provider that is registered
in OpenTofu as pre-configured, the following calls should not be performed:
* ValidateProviderConfig
* ConfigureProvider

Signed-off-by: Andrei Ciobanu <andrei.ciobanu@opentofu.org>
This commit is contained in:
Andrei Ciobanu
2025-06-05 15:05:46 +03:00
parent 3bdd0073a5
commit 4fc494ef8a
8 changed files with 440 additions and 19 deletions

View File

@@ -587,7 +587,7 @@ func (m *Meta) contextOpts(ctx context.Context) (*tofu.ContextOpts, error) {
opts.Provisioners = m.testingOverrides.Provisioners
} else {
var providerFactories map[addrs.Provider]providers.Factory
providerFactories, err = m.providerFactories()
providerFactories, err = m.providerFactories(m.loadPreconfiguredConfigs(ctx))
opts.Providers = providerFactories
opts.Provisioners = m.provisionerFactories()
}
@@ -965,3 +965,15 @@ func (c *Meta) MaybeGetSchemas(ctx context.Context, state *states.State, config
}
return nil, diags
}
func (c *Meta) loadPreconfiguredConfigs(ctx context.Context) map[addrs.Provider]*configs.PreconfiguredProvider {
var res map[addrs.Provider]*configs.PreconfiguredProvider
cfg, _ := c.configLoader.LoadConfig(ctx, ".", configs.StaticModuleCall{}) // TODO load selective
if cfg == nil {
return res
}
if cfg.Module.PreconfiguredProviders == nil {
return res
}
return cfg.Module.PreconfiguredProviders.PreconfiguredProviders
}

View File

@@ -126,6 +126,11 @@ func (m *Meta) annotateDependencyLocksWithOverrides(ret *depsfile.Locks) *depsfi
log.Printf("[DEBUG] Provider %s is overridden as an \"unmanaged provider\"", addr)
ret.SetProviderOverridden(addr)
}
for addr := range m.loadPreconfiguredConfigs(context.TODO()) {
log.Printf("[DEBUG] Provider %s is overridden as a \"preconfigured provider\"", addr)
ret.SetProviderOverridden(addr)
}
if m.testingOverrides != nil {
for addr := range m.testingOverrides.Providers {
log.Printf("[DEBUG] Provider %s is overridden in Meta.testingOverrides", addr)

View File

@@ -10,11 +10,13 @@ import (
"errors"
"fmt"
"log"
"net"
"os"
"os/exec"
"strings"
plugin "github.com/hashicorp/go-plugin"
"github.com/opentofu/opentofu/internal/configs"
"github.com/opentofu/opentofu/internal/addrs"
terraformProvider "github.com/opentofu/opentofu/internal/builtin/providers/tf"
@@ -232,7 +234,7 @@ func (m *Meta) providerDevOverrideRuntimeWarnings() tfdiags.Diagnostics {
// package have been modified outside of the installer. If it returns an error,
// the returned map may be incomplete or invalid, but will be as complete
// as possible given the cause of the error.
func (m *Meta) providerFactories() (map[addrs.Provider]providers.Factory, error) {
func (m *Meta) providerFactories(preconfiguredProviders map[addrs.Provider]*configs.PreconfiguredProvider) (map[addrs.Provider]providers.Factory, error) {
locks, diags := m.lockedDependencies()
if diags.HasErrors() {
return nil, fmt.Errorf("failed to read dependency lock file: %w", diags.Err())
@@ -275,7 +277,7 @@ func (m *Meta) providerFactories() (map[addrs.Provider]providers.Factory, error)
devOverrideProviders := m.ProviderDevOverrides
unmanagedProviders := m.UnmanagedProviders
factories := make(map[addrs.Provider]providers.Factory, len(providerLocks)+len(internalFactories)+len(unmanagedProviders))
factories := make(map[addrs.Provider]providers.Factory, len(providerLocks)+len(internalFactories)+len(unmanagedProviders)+len(preconfiguredProviders))
for name, factory := range internalFactories {
factories[addrs.NewBuiltInProvider(name)] = factory
}
@@ -328,6 +330,16 @@ func (m *Meta) providerFactories() (map[addrs.Provider]providers.Factory, error)
for provider, localDir := range devOverrideProviders {
factories[provider] = devOverrideProviderFactory(provider, localDir)
}
for provider, providerCfg := range preconfiguredProviders {
reattach, err := preconfiguredToReattach(providerCfg)
if err != nil {
return nil, err
}
if reattach != nil {
factories[provider] = preconfiguredProviderFactory(provider, providerCfg, reattach)
}
}
for provider, reattach := range unmanagedProviders {
factories[provider] = unmanagedProviderFactory(provider, reattach)
}
@@ -381,7 +393,7 @@ func providerFactory(meta *providercache.CachedProvider) providers.Factory {
}
protoVer := client.NegotiatedVersion()
p, err := initializeProviderInstance(raw, protoVer, client, meta.Provider)
p, err := initializeProviderInstance(raw, protoVer, client, meta.Provider, false)
if errors.Is(err, errUnsupportedProtocolVersion) {
panic(err)
}
@@ -392,18 +404,20 @@ func providerFactory(meta *providercache.CachedProvider) providers.Factory {
// initializeProviderInstance uses the plugin dispensed by the RPC client, and initializes a plugin instance
// per the protocol version
func initializeProviderInstance(plugin interface{}, protoVer int, pluginClient *plugin.Client, pluginAddr addrs.Provider) (providers.Interface, error) {
func initializeProviderInstance(plugin interface{}, protoVer int, pluginClient *plugin.Client, pluginAddr addrs.Provider, preconfigured bool) (providers.Interface, error) {
// store the client so that the plugin can kill the child process
switch protoVer {
case 5:
p := plugin.(*tfplugin.GRPCProvider)
p.PluginClient = pluginClient
p.Addr = pluginAddr
p.Preconfigured = preconfigured
return p, nil
case 6:
p := plugin.(*tfplugin6.GRPCProvider)
p.PluginClient = pluginClient
p.Addr = pluginAddr
p.Preconfigured = preconfigured
return p, nil
default:
return nil, errUnsupportedProtocolVersion
@@ -476,10 +490,92 @@ func unmanagedProviderFactory(provider addrs.Provider, reattach *plugin.Reattach
protoVer = 5
}
return initializeProviderInstance(raw, protoVer, client, provider)
return initializeProviderInstance(raw, protoVer, client, provider, false)
}
}
func preconfiguredProviderFactory(provider addrs.Provider, preconfiguredProvider *configs.PreconfiguredProvider, reattach *plugin.ReattachConfig) providers.Factory {
return func() (providers.Interface, error) {
config := &plugin.ClientConfig{
HandshakeConfig: tfplugin.Handshake,
Logger: logging.NewProviderLogger("unmanaged."),
AllowedProtocols: []plugin.Protocol{plugin.ProtocolGRPC},
Managed: false,
Reattach: reattach,
SyncStdout: logging.PluginOutputMonitor(fmt.Sprintf("%s:stdout", provider)),
SyncStderr: logging.PluginOutputMonitor(fmt.Sprintf("%s:stderr", provider)),
}
if preconfiguredProvider.ProtocolVersion == 0 {
// As of the 0.15 release, sdk.v2 doesn't include the protocol
// version in the ReattachConfig (only recently added to
// go-plugin), so client.NegotiatedVersion() always returns 0. We
// assume that an unmanaged provider reporting protocol version 0 is
// actually using proto v5 for backwards compatibility.
if defaultPlugins, ok := tfplugin.VersionedPlugins[5]; ok {
config.Plugins = defaultPlugins
} else {
return nil, errors.New("no supported plugins for protocol 0")
}
} else if plugins, ok := tfplugin.VersionedPlugins[preconfiguredProvider.ProtocolVersion]; !ok {
return nil, fmt.Errorf("no supported plugins for protocol %d", preconfiguredProvider.ProtocolVersion)
} else {
config.Plugins = plugins
}
client := plugin.NewClient(config)
rpcClient, err := client.Client()
if err != nil {
return nil, err
}
raw, err := rpcClient.Dispense(tfplugin.ProviderPluginName)
if err != nil {
return nil, err
}
protoVer := client.NegotiatedVersion()
if protoVer == 0 {
// As of the 0.15 release, sdk.v2 doesn't include the protocol
// version in the ReattachConfig (only recently added to
// go-plugin), so client.NegotiatedVersion() always returns 0. We
// assume that an unmanaged provider reporting protocol version 0 is
// actually using proto v5 for backwards compatibility.
protoVer = 5
}
return initializeProviderInstance(raw, protoVer, client, provider, true)
}
}
func preconfiguredToReattach(provider *configs.PreconfiguredProvider) (resp *plugin.ReattachConfig, err error) {
// TODO
//switch c.Addr.Network {
//case "unix":
// addr, err = net.ResolveUnixAddr("unix", c.Addr.String)
// if err != nil {
// return unmanagedProviders, fmt.Errorf("Invalid unix socket path %q for %q: %w", c.Addr.String, p, err)
// }
//case "tcp":
// addr, err = net.ResolveTCPAddr("tcp", c.Addr.String)
// if err != nil {
// return unmanagedProviders, fmt.Errorf("Invalid TCP address %q for %q: %w", c.Addr.String, p, err)
// }
//default:
// return unmanagedProviders, fmt.Errorf("Unknown address type %q for %q", c.Addr.Network, p)
//}
addr, err := net.ResolveTCPAddr("tcp", provider.Addr)
if err != nil {
return nil, err
}
return &plugin.ReattachConfig{
Protocol: plugin.ProtocolGRPC,
ProtocolVersion: provider.ProtocolVersion,
Addr: addr,
Test: true, // To block opentofu from calling close
}, nil
}
// providerFactoryError is a stub providers.Factory that returns an error
// when called. It's used to allow providerFactories to still produce a
// factory for each available provider in an error case, for situations

View File

@@ -35,13 +35,14 @@ type Module struct {
ActiveExperiments experiments.Set
Backend *Backend
CloudConfig *CloudConfig
ProviderConfigs map[string]*Provider
ProviderRequirements *RequiredProviders
ProviderLocalNames map[addrs.Provider]string
ProviderMetas map[addrs.Provider]*ProviderMeta
Encryption *config.EncryptionConfig
Backend *Backend
CloudConfig *CloudConfig
ProviderConfigs map[string]*Provider
ProviderRequirements *RequiredProviders
PreconfiguredProviders *PreconfiguredProviders
ProviderLocalNames map[addrs.Provider]string
ProviderMetas map[addrs.Provider]*ProviderMeta
Encryption *config.EncryptionConfig
Variables map[string]*Variable
Locals map[string]*Local
@@ -91,12 +92,13 @@ type File struct {
ActiveExperiments experiments.Set
Backends []*Backend
CloudConfigs []*CloudConfig
ProviderConfigs []*Provider
ProviderMetas []*ProviderMeta
RequiredProviders []*RequiredProviders
Encryptions []*config.EncryptionConfig
Backends []*Backend
CloudConfigs []*CloudConfig
ProviderConfigs []*Provider
ProviderMetas []*ProviderMeta
RequiredProviders []*RequiredProviders
PreconfiguredProviders []*PreconfiguredProviders
Encryptions []*config.EncryptionConfig
Variables []*Variable
Locals []*Local
@@ -203,6 +205,20 @@ func NewModule(primaryFiles, overrideFiles []*File, call StaticModuleCall, sourc
mod.ProviderRequirements = r
}
}
for _, file := range primaryFiles {
for _, r := range file.PreconfiguredProviders { // TODO andrei this should be allowed only in the root module
if mod.PreconfiguredProviders != nil {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Duplicate preconfigured providers configuration",
Detail: fmt.Sprintf("A module may have only one preconfigured providers configuration. The preconfigured providers were previously configured at %s.", mod.ProviderRequirements.DeclRange),
Subject: &r.DeclRange,
})
continue
}
mod.PreconfiguredProviders = r
}
}
// If no required_providers block is configured, create a useful empty
// state to reduce nil checks elsewhere
@@ -220,6 +236,11 @@ func NewModule(primaryFiles, overrideFiles []*File, call StaticModuleCall, sourc
mod.ProviderRequirements.RequiredProviders[name] = rp
}
}
for _, override := range file.PreconfiguredProviders {
for name, pp := range override.PreconfiguredProviders {
mod.PreconfiguredProviders.PreconfiguredProviders[name] = pp
}
}
}
for _, file := range primaryFiles {

View File

@@ -105,6 +105,11 @@ func (p *Parser) loadConfigFile(path string, override bool) (*File, hcl.Diagnost
diags = append(diags, reqsDiags...)
file.RequiredProviders = append(file.RequiredProviders, reqs)
case "preconfigured_providers":
reqs, reqsDiags := decodePreconfiguredProvider(innerBlock)
diags = append(diags, reqsDiags...)
file.PreconfiguredProviders = append(file.PreconfiguredProviders, reqs)
case "provider_meta":
providerCfg, cfgDiags := decodeProviderMetaBlock(innerBlock)
diags = append(diags, cfgDiags...)
@@ -333,6 +338,9 @@ var terraformBlockSchema = &hcl.BodySchema{
{
Type: "required_providers",
},
{
Type: "preconfigured_providers",
},
{
Type: "provider_meta",
LabelNames: []string{"provider"},

View File

@@ -0,0 +1,253 @@
// Copyright (c) The OpenTofu Authors
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package configs
import (
"fmt"
"github.com/hashicorp/hcl/v2"
"github.com/opentofu/opentofu/internal/addrs"
"github.com/zclconf/go-cty/cty"
)
type PreconfiguredProvider struct {
Name string
Addr string
ProtocolVersion int
Type addrs.Provider
DeclRange hcl.Range
Aliases []addrs.LocalProviderConfig
}
type PreconfiguredProviders struct {
PreconfiguredProviders map[addrs.Provider]*PreconfiguredProvider
DeclRange hcl.Range
}
func decodePreconfiguredProvider(block *hcl.Block) (*PreconfiguredProviders, hcl.Diagnostics) {
ret := &PreconfiguredProviders{
PreconfiguredProviders: make(map[addrs.Provider]*PreconfiguredProvider),
DeclRange: block.DefRange,
}
attrs, diags := block.Body.JustAttributes()
if diags.HasErrors() {
// Returns an empty RequiredProvider to allow further validations to work properly,
// allowing to return all the diagnostics correctly.
return ret, diags
}
for name, attr := range attrs {
rp := &PreconfiguredProvider{
Name: name,
DeclRange: attr.Expr.Range(),
}
provAddr, addrsDiags := addrs.ParseProviderSourceString(name)
if addrsDiags.HasErrors() {
for _, diagnostic := range addrsDiags.ToHCL() {
diags = diags.Append(diagnostic)
}
return nil, diags
}
// Look for a single static string, in case we have the legacy version-only
// format in the configuration.
if expr, err := attr.Expr.Value(nil); err == nil && expr.Type().IsPrimitiveType() {
pType, err := addrs.ParseProviderPart(rp.Name)
if err != nil {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid provider name",
Detail: err.Error(),
Subject: attr.Expr.Range().Ptr(),
})
continue
}
rp.Type = addrs.ImpliedProviderForUnqualifiedType(pType)
ret.PreconfiguredProviders[provAddr] = rp
continue
}
// verify that the local name is already localized or produce an error.
nameDiags := checkProviderNameNormalized(name, attr.Expr.Range())
if nameDiags.HasErrors() {
diags = append(diags, nameDiags...)
continue
}
kvs, mapDiags := hcl.ExprMap(attr.Expr)
if mapDiags.HasErrors() {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid required_providers object",
Detail: "required_providers entries must be strings or objects.",
Subject: attr.Expr.Range().Ptr(),
})
continue
}
LOOP:
for _, kv := range kvs {
key, keyDiags := kv.Key.Value(nil)
if keyDiags.HasErrors() {
diags = append(diags, keyDiags...)
continue
}
if key.Type() != cty.String {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid Attribute",
Detail: fmt.Sprintf("Invalid attribute value for provider requirement: %#v", key),
Subject: kv.Key.Range().Ptr(),
})
continue
}
switch key.AsString() {
case "source":
source, err := kv.Value.Value(nil)
if err != nil || !source.Type().Equals(cty.String) {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid source",
Detail: "Source must be specified as a string.",
Subject: kv.Value.Range().Ptr(),
})
continue
}
fqn, sourceDiags := addrs.ParseProviderSourceString(source.AsString())
if sourceDiags.HasErrors() {
hclDiags := sourceDiags.ToHCL()
// The diagnostics from ParseProviderSourceString don't contain
// source location information because it has no context to compute
// them from, and so we'll add those in quickly here before we
// return.
for _, diag := range hclDiags {
if diag.Subject == nil {
diag.Subject = kv.Value.Range().Ptr()
}
}
diags = append(diags, hclDiags...)
continue
}
provAddr = fqn
rp.Type = fqn
case "addr":
addr, err := kv.Value.Value(nil)
if err != nil || !addr.Type().Equals(cty.String) {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid addr",
Detail: "Addr must be specified as a string.",
Subject: kv.Value.Range().Ptr(),
})
continue
}
rp.Addr = addr.AsString()
case "protocol_version":
vers, err := kv.Value.Value(nil)
if err != nil || !vers.Type().Equals(cty.Number) {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid protocol_version",
Detail: "protocol_version must be specified as a number.",
Subject: kv.Value.Range().Ptr(),
})
continue
}
versF := vers.AsBigFloat()
if !versF.IsInt() {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid protocol_version",
Detail: "protocol_version must be specified as an integer.",
Subject: kv.Value.Range().Ptr(),
})
continue
}
versI, _ := versF.Int64()
rp.ProtocolVersion = int(versI)
case "configuration_aliases":
exprs, listDiags := hcl.ExprList(kv.Value)
if listDiags.HasErrors() {
diags = append(diags, listDiags...)
continue
}
for _, expr := range exprs {
traversal, travDiags := hcl.AbsTraversalForExpr(expr)
if travDiags.HasErrors() {
diags = append(diags, travDiags...)
continue
}
addr, cfgDiags := ParseProviderConfigCompact(traversal)
if cfgDiags.HasErrors() {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid configuration_aliases value",
Detail: `Configuration aliases can only contain references to local provider configuration names in the format of provider.alias`,
Subject: kv.Value.Range().Ptr(),
})
continue
}
if addr.LocalName != name {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid configuration_aliases value",
Detail: fmt.Sprintf(`Configuration aliases must be prefixed with the provider name. Expected %q, but found %q.`, name, addr.LocalName),
Subject: kv.Value.Range().Ptr(),
})
continue
}
rp.Aliases = append(rp.Aliases, addr)
}
default:
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid required_providers object",
Detail: `required_providers objects can only contain "version", "source" and "configuration_aliases" attributes. To configure a provider, use a "provider" block.`,
Subject: kv.Key.Range().Ptr(),
})
break LOOP
}
}
if diags.HasErrors() {
continue
}
// We can add the required provider when there are no errors.
// If a source was not given, create an implied type.
if rp.Type.IsZero() {
pType, err := addrs.ParseProviderPart(rp.Name)
if err != nil {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid provider name",
Detail: err.Error(),
Subject: attr.Expr.Range().Ptr(),
})
} else {
rp.Type = addrs.ImpliedProviderForUnqualifiedType(pType)
}
}
ret.PreconfiguredProviders[provAddr] = rp
}
return ret, diags
}

View File

@@ -77,6 +77,9 @@ type GRPCProvider struct {
// to use as the parent context for gRPC API calls.
ctx context.Context
// This is a flag to know not to call methods that are not meant for the externally managed providers
Preconfigured bool
mu sync.Mutex
// schema stores the schema for this provider. This is used to properly
// serialize the requests for schemas.
@@ -186,6 +189,11 @@ func (p *GRPCProvider) GetProviderSchema(ctx context.Context) (resp providers.Ge
func (p *GRPCProvider) ValidateProviderConfig(ctx context.Context, r providers.ValidateProviderConfigRequest) (resp providers.ValidateProviderConfigResponse) {
logger.Trace("GRPCProvider: ValidateProviderConfig")
if p.Preconfigured {
logger.Info("GRPCProvider: ValidateProviderConfig call skipped since this is a preconfigured provider")
return resp
}
schema := p.GetProviderSchema(ctx)
if schema.Diagnostics.HasErrors() {
resp.Diagnostics = schema.Diagnostics
@@ -342,6 +350,11 @@ func (p *GRPCProvider) UpgradeResourceState(ctx context.Context, r providers.Upg
func (p *GRPCProvider) ConfigureProvider(ctx context.Context, r providers.ConfigureProviderRequest) (resp providers.ConfigureProviderResponse) {
logger.Trace("GRPCProvider: ConfigureProvider")
if p.Preconfigured {
logger.Info("GRPCProvider.v6: ConfigureProvider call skipped since this is a preconfigured provider")
return resp
}
schema := p.GetProviderSchema(ctx)
if schema.Diagnostics.HasErrors() {
resp.Diagnostics = schema.Diagnostics

View File

@@ -77,6 +77,9 @@ type GRPCProvider struct {
// to use as the parent context for gRPC API calls.
ctx context.Context
// This is a flag to know not to call methods that are not meant for the externally managed providers
Preconfigured bool
mu sync.Mutex
// schema stores the schema for this provider. This is used to properly
// serialize the requests for schemas.
@@ -186,6 +189,11 @@ func (p *GRPCProvider) GetProviderSchema(ctx context.Context) (resp providers.Ge
func (p *GRPCProvider) ValidateProviderConfig(ctx context.Context, r providers.ValidateProviderConfigRequest) (resp providers.ValidateProviderConfigResponse) {
logger.Trace("GRPCProvider.v6: ValidateProviderConfig")
if p.Preconfigured {
logger.Info("GRPCProvider.v6: ValidateProviderConfig call skipped since this is a preconfigured provider")
return resp
}
schema := p.GetProviderSchema(ctx)
if schema.Diagnostics.HasErrors() {
resp.Diagnostics = schema.Diagnostics
@@ -335,6 +343,11 @@ func (p *GRPCProvider) UpgradeResourceState(ctx context.Context, r providers.Upg
func (p *GRPCProvider) ConfigureProvider(ctx context.Context, r providers.ConfigureProviderRequest) (resp providers.ConfigureProviderResponse) {
logger.Trace("GRPCProvider.v6: ConfigureProvider")
if p.Preconfigured {
logger.Info("GRPCProvider.v6: ConfigureProvider call skipped since this is a preconfigured provider")
return resp
}
schema := p.GetProviderSchema(ctx)
var mp []byte