Files
opentf/internal/resources/managed.go
Martin Atkins 2585a63df0 resources: A new package for resource-related operations
The original motivation here was to implement the special case of making
a synthetic plan without calling into the provider when the desired state
is absent and the provider hasn't opted in to planning to destroy objects.
This case needs to be opted in because older providers will crash if asked
to plan with the config value or proposed value set to null.

However, there was already far too much code duplicated across both the
planning engine and the apply engine's "final plan" operation, and so this
seemed like a good prompt to finally resolve those TODO comments and factor
out the shared planning logic into a separate place that both can depend
on.

The new package "resources" is intended to grow into the home for all of
these resource-related operations that each wrap a lower-level provider
call but also perform preparation or followup actions such as making sure
the provider's response is valid according to the requirements of the
plugin protocol.

For now it only includes config validation and planning for managed
resources, but is designed to grow to include other similar operations in
future. The most likely next step is to add a method for applying a
previously-created plan, which would replace a bunch of the code in the
apply engine's implementation of the "ManagedApply" operation.

Currently the way this integrates with the rest of the provider-related
infrastructure is a little awkward. We can hopefully improve on that in
future, but for now the priority was to avoid this becoming yet another
sprawling architecture change that would be hard to review. Once we feel
like we've achieved the "walking skeleton" milestone for the new runtime
we can think about how we want to tidy up the rough edges between the
different components of this new design.

As has become tradition for these new codepaths, this is designed to be
independently testable but not yet actually tested because we want to wait
and see how all of this code settles before we start writing tests that
would then make it harder to refactor as we learn more.

Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
2026-03-04 10:30:11 -08:00

101 lines
3.7 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 resources
import (
"context"
"fmt"
"github.com/opentofu/opentofu/internal/addrs"
"github.com/opentofu/opentofu/internal/providers"
"github.com/opentofu/opentofu/internal/tfdiags"
)
// ManagedResourceType represents a named resource type in a specific provider,
// and also carries a client for interacting with that provider.
//
// Most methods of this type relate to managed-resource-related operations in
// the underlying provider protocol, but also include additional OpenTofu-level
// logic such as verifying that the provider is correctly implementing the
// protocol's constraints on how objects are allowed to change.
type ManagedResourceType struct {
// providerAddr is the provider that this resource type belongs to.
providerAddr addrs.Provider
// typeName is the resource type name as expected by the associated provider.
typeName string
// client is the client to use to interact with the provider that this
// resource type belongs to.
client providers.Interface
}
var _ ResourceType = (*ManagedResourceType)(nil)
// NewManagedResourceType constructs a new [ManagedResourceType] for the
// given resource type name in the provider whose client is provided.
//
// It's the caller's responsibility to make sure that the given client is
// actually for the provider indicated.
func NewManagedResourceType(providerAddr addrs.Provider, typeName string, client providers.Interface) *ManagedResourceType {
return &ManagedResourceType{
providerAddr: providerAddr,
typeName: typeName,
client: client,
}
}
// ResourceMode implements [ResourceType].
func (rt *ManagedResourceType) ResourceMode() addrs.ResourceMode {
return addrs.ManagedResourceMode
}
// ResourceTypeName implements [ResourceType].
func (rt *ManagedResourceType) ResourceTypeName() string {
return rt.typeName
}
// LoadSchema loads the schema for this resource type from its provider.
//
// This method performs no direct caching of the result, so the underlying
// provider client (originally passed to [NewManagedResourceType]) should
// provide its own caching.
func (rt *ManagedResourceType) LoadSchema(ctx context.Context) (providers.Schema, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
// This is awkward because we already have higher-level objects that
// can answer this question given an entire provider manager, but by the
// time we get here we've already used the provider manager to instantiate
// the client and no longer have access to the manager.
//
// TODO: Find a different way to structure this so that this concern
// can be centralized in one place while still accessing it indirectly
// through a fully-encapsulated "resource type" object that we can pass
// around independently of the plugin library it came from. The overall
// idea here is to move away from the pattern of passing around
// the provider manager, provider address, and resource type name as
// three separate arguments to functions and have it all encapsulated
// in a single object we can pass around, similar to how exprs.Valuer
// encapsulates everything needed to evaluate something.
resp := rt.client.GetProviderSchema(ctx)
diags = diags.Append(resp.Diagnostics)
if resp.Diagnostics.HasErrors() {
return providers.Schema{}, diags
}
ret, ok := resp.ResourceTypes[rt.typeName]
if !ok {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Unsupported resource type",
fmt.Sprintf("Provider %s does not support a managed resource type named %q.", rt.providerAddr.String(), rt.typeName),
))
return providers.Schema{}, diags
}
return ret, diags
}