diff --git a/internal/backend/init/init.go b/internal/backend/init/init.go index 800c86ddd7..15dc3cbfd0 100644 --- a/internal/backend/init/init.go +++ b/internal/backend/init/init.go @@ -31,18 +31,31 @@ import ( "github.com/opentofu/opentofu/internal/tfdiags" ) +// Backends are hardcoded into OpenTofu because the API for backends uses +// complex structures and supporting that over the plugin system is currently +// prohibitively difficult. For those wanting to implement a custom backend, +// they can do so with recompilation. + // backends is the list of available backends. This is a global variable // because backends are currently hardcoded into OpenTofu and can't be // modified without recompilation. // // To read an available backend, use the Backend function. This ensures -// safe concurrent read access to the list of built-in backends. -// -// Backends are hardcoded into OpenTofu because the API for backends uses -// complex structures and supporting that over the plugin system is currently -// prohibitively difficult. For those wanting to implement a custom backend, -// they can do so with recompilation. +// safe concurrent read access to the list of built-in backends by holding +// [backendsLock]. var backends map[string]backend.InitFn + +// backendAliases complements [backends] by allowing alternative names for some +// backends. The keys are the alias names and the values are the canonical +// names. OpenTofu always normalizes any use of an alias into its canonical +// name so that the two are effectively interchangable. +// +// [backendsLock] also covers access to backendAliases. Use the Backend function +// to safely access both of these maps. +var backendAliases map[string]string + +// backendsLock is a mutex that must be held when accessing either [backends] +// or [backendAliases]. var backendsLock sync.Mutex // RemovedBackends is a record of previously supported backends which have @@ -78,6 +91,9 @@ func Init(services *disco.Disco) { // This is an implementation detail only, used for the cloud package "cloud": func(enc encryption.StateEncryption) backend.Backend { return backendCloud.New(services, enc) }, } + backendAliases = map[string]string{ + // There are currently no backend aliases + } RemovedBackends = map[string]string{ "artifactory": `The "artifactory" backend is not supported in OpenTofu v1.3 or later.`, @@ -91,10 +107,17 @@ func Init(services *disco.Disco) { // Backend returns the initialization factory for the given backend, or // nil if none exists. -func Backend(name string) backend.InitFn { +// +// The second return value is the canonical name for the selected backend, +// if any, which should be used in the UI and in OpenTofu's records of which +// backend is active in a particular working directory. +func Backend(name string) (backend.InitFn, string) { backendsLock.Lock() defer backendsLock.Unlock() - return backends[name] + if alias, ok := backendAliases[name]; ok { + name = alias + } + return backends[name], name } // Set sets a new backend in the list of backends. If f is nil then the diff --git a/internal/backend/init/init_test.go b/internal/backend/init/init_test.go index 61a5977009..3d487c8416 100644 --- a/internal/backend/init/init_test.go +++ b/internal/backend/init/init_test.go @@ -13,35 +13,70 @@ import ( ) func TestInit_backend(t *testing.T) { - // Initialize the backends map + // Initialize the backends and backendAliases maps Init(nil) backends := []struct { - Name string - Type string + RequestedName string + Type string + CanonicalName string }{ - {"local", "*local.Local"}, - {"remote", "*remote.Remote"}, - {"azurerm", "*azure.Backend"}, - {"consul", "*consul.Backend"}, - {"cos", "*cos.Backend"}, - {"gcs", "*gcs.Backend"}, - {"inmem", "*inmem.Backend"}, - {"pg", "*pg.Backend"}, - {"s3", "*s3.Backend"}, + {"local", "*local.Local", "local"}, + {"remote", "*remote.Remote", "remote"}, + {"azurerm", "*azure.Backend", "azurerm"}, + {"consul", "*consul.Backend", "consul"}, + {"cos", "*cos.Backend", "cos"}, + {"gcs", "*gcs.Backend", "gcs"}, + {"inmem", "*inmem.Backend", "inmem"}, + {"pg", "*pg.Backend", "pg"}, + {"s3", "*s3.Backend", "s3"}, } // Make sure we get the requested backend for _, b := range backends { - t.Run(b.Name, func(t *testing.T) { - f := Backend(b.Name) + t.Run(b.RequestedName, func(t *testing.T) { + f, canonName := Backend(b.RequestedName) if f == nil { - t.Fatalf("backend %q is not present; should be", b.Name) + t.Fatalf("backend %q is not present; should be", b.RequestedName) } bType := reflect.TypeOf(f(encryption.StateEncryptionDisabled())).String() if bType != b.Type { - t.Fatalf("expected backend %q to be %q, got: %q", b.Name, b.Type, bType) + t.Errorf("expected backend %q to be %q, got: %q", b.RequestedName, b.Type, bType) + } + if b.CanonicalName != canonName { + t.Errorf("expected canonical name to be %q, but got %q", b.CanonicalName, canonName) } }) } } + +// TestInit_backendConsistency ensures that the "backends" and "backendAliases" +// maps are kept consistent with one another, so that: +// - Every alias maps to a canonical backend name that is actually defined. +// - No single type name is both an alias _and_ a canonical name. +// - There must be a backend whose canonical name is "local" and no alias +// of that name because package command relies on this in various special +// cases. +func TestInit_backendConsistency(t *testing.T) { + // Initialize the backends and backendAliases maps + Init(nil) + + backendsLock.Lock() + defer backendsLock.Unlock() + + for aliasType, canonType := range backendAliases { + if _, ok := backends[canonType]; !ok { + t.Errorf("alias %q maps to canonical name %q, but the canonical name is not in the backends map", aliasType, canonType) + } + if _, ok := backends[aliasType]; ok { + t.Errorf("alias map has key %q, which is also a canonical name in the backends map", aliasType) + } + } + + if _, ok := backends["local"]; !ok { + t.Error(`"local" must be defined as a an available backend type because lots of code in package command treats it as a special case`) + } + if _, ok := backendAliases["local"]; ok { + t.Error(`"local" must not be defined as an alias because lots of code in package command treats it as a special case`) + } +} diff --git a/internal/builtin/providers/tf/data_source_state.go b/internal/builtin/providers/tf/data_source_state.go index 51a1d32a32..8231c2d392 100644 --- a/internal/builtin/providers/tf/data_source_state.go +++ b/internal/builtin/providers/tf/data_source_state.go @@ -278,5 +278,9 @@ func getBackendFactory(backendType string) backend.InitFn { return overrideBackendFactories[backendType] } - return backendInit.Backend(backendType) + // For the sake of this data source we don't care about the canonical + // name of the backend: canonical names or alias names are both accepted + // and are treated as interchangeable. + factory, _ := backendInit.Backend(backendType) + return factory } diff --git a/internal/command/command_test.go b/internal/command/command_test.go index 29778c8b0f..eb482dea23 100644 --- a/internal/command/command_test.go +++ b/internal/command/command_test.go @@ -752,7 +752,8 @@ func testBackendState(t *testing.T, s *states.State, c int) (*legacy.State, *htt Config: configs.SynthBody("", map[string]cty.Value{}), Eval: configs.NewStaticEvaluator(nil, configs.RootModuleCallForTesting()), } - b := backendInit.Backend("http")(encryption.StateEncryptionDisabled()) + httpBackendInit, _ := backendInit.Backend("http") + b := httpBackendInit(encryption.StateEncryptionDisabled()) configSchema := b.ConfigSchema() hash, _ := backendConfig.Hash(t.Context(), configSchema) diff --git a/internal/command/init.go b/internal/command/init.go index ca98ae8e2c..9512c4329f 100644 --- a/internal/command/init.go +++ b/internal/command/init.go @@ -495,7 +495,7 @@ func (c *InitCommand) initBackend(ctx context.Context, root *configs.Module, ext return nil, true, diags } - bf := backendInit.Backend(backendType) + bf, canonType := backendInit.Backend(backendType) if bf == nil { detail := fmt.Sprintf("There is no backend type named %q.", backendType) if msg, removed := backendInit.RemovedBackends[backendType]; removed { @@ -510,6 +510,9 @@ func (c *InitCommand) initBackend(ctx context.Context, root *configs.Module, ext }) return nil, true, diags } + if backendType != canonType { + c.Ui.Output(fmt.Sprintf("- %q is an alias for backend type %q", backendType, canonType)) + } b := bf(nil) // This is only used to get the schema, encryption should panic if attempted backendSchema := b.ConfigSchema() diff --git a/internal/command/meta_backend.go b/internal/command/meta_backend.go index 31ad141b0a..8c2b576060 100644 --- a/internal/command/meta_backend.go +++ b/internal/command/meta_backend.go @@ -311,11 +311,18 @@ func (m *Meta) selectWorkspace(ctx context.Context, b backend.Backend) error { func (m *Meta) BackendForLocalPlan(ctx context.Context, settings plans.Backend, enc encryption.StateEncryption) (backend.Enhanced, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics - f := backendInit.Backend(settings.Type) + f, canonType := backendInit.Backend(settings.Type) if f == nil { diags = diags.Append(fmt.Errorf(strings.TrimSpace(errBackendSavedUnknown), settings.Type)) return nil, diags } + if canonType != settings.Type { + // We should always save the canonical name in a plan -- never an alias + // name -- so getting here suggests a bug in the code that generated + // this plan. + diags = diags.Append(fmt.Errorf("saved plan should use canonical backend type %q, not alias %q; this is a bug in OpenTofu", canonType, settings.Type)) + return nil, diags + } b := f(enc) log.Printf("[TRACE] Meta.BackendForLocalPlan: instantiated backend of type %T", b) @@ -484,7 +491,7 @@ func (m *Meta) backendConfig(ctx context.Context, opts *BackendOpts) (*configs.B return nil, 0, nil } - bf := backendInit.Backend(c.Type) + bf, canonType := backendInit.Backend(c.Type) if bf == nil { detail := fmt.Sprintf("There is no backend type named %q.", c.Type) if msg, removed := backendInit.RemovedBackends[c.Type]; removed { @@ -521,6 +528,10 @@ func (m *Meta) backendConfig(ctx context.Context, opts *BackendOpts) (*configs.B // body without affecting others that hold this reference. configCopy := *c configCopy.Config = configBody + if c.Type != canonType { + log.Printf("[DEBUG] Meta.Backend: using canonical backend type %q instead of alias %q", canonType, c.Type) + configCopy.Type = canonType + } return &configCopy, configHash, diags } @@ -538,6 +549,11 @@ func (m *Meta) backendConfig(ctx context.Context, opts *BackendOpts) (*configs.B // which case this function will error. func (m *Meta) backendFromConfig(ctx context.Context, opts *BackendOpts, enc encryption.StateEncryption) (backend.Backend, tfdiags.Diagnostics) { // Get the local backend configuration. + // Note that [Meta.backendConfig] returns a possibly-modified copy of + // the original configuration, and in particular has the backend type + // already translated from an alias to the canonical name so everything + // using "c" after this can assume that c.Type is definitely the canonical + // name for a backend that actually exists and is not an alias. c, cHash, diags := m.backendConfig(ctx, opts) if diags.HasErrors() { return nil, diags @@ -814,11 +830,19 @@ func (m *Meta) backendFromState(ctx context.Context, enc encryption.StateEncrypt if s.Backend.Type == "" { return backendLocal.New(enc), diags } - f := backendInit.Backend(s.Backend.Type) + f, canonType := backendInit.Backend(s.Backend.Type) if f == nil { diags = diags.Append(fmt.Errorf(strings.TrimSpace(errBackendSavedUnknown), s.Backend.Type)) return nil, diags } + if canonType != s.Backend.Type { + // The "state" (really: the working directory state managed by + // the clistate package) should always record the canonical backend + // type, not an alias for it. If we get here then there's a bug in + // the code that generated the s.Backend values. + diags = diags.Append(fmt.Errorf("working directory is configured for backend alias %q instead of the canonical name %q; this is a bug in OpenTofu", s.Backend.Type, canonType)) + return nil, diags + } b := f(enc) // The configuration saved in the working directory state file is used @@ -1272,11 +1296,17 @@ func (m *Meta) savedBackend(ctx context.Context, sMgr *clistate.LocalState, enc s := sMgr.State() // Get the backend - f := backendInit.Backend(s.Backend.Type) + f, canonName := backendInit.Backend(s.Backend.Type) if f == nil { diags = diags.Append(fmt.Errorf(strings.TrimSpace(errBackendSavedUnknown), s.Backend.Type)) return nil, diags } + if s.Backend.Type != canonName { + // We should always save the canonical name in the clistate, so if we + // get here then it's a bug in whatever generated the clistate. + diags = diags.Append(fmt.Errorf("working directory state uses alias %q instead of canonical backend type %q; this is a bug in OpenTofu", s.Backend.Type, canonName)) + return nil, diags + } b := f(enc) // The configuration saved in the working directory state file is used @@ -1354,17 +1384,20 @@ func (m *Meta) backendConfigNeedsMigration(ctx context.Context, c *configs.Backe log.Print("[TRACE] backendConfigNeedsMigration: no cached config, so migration is required") return true } - if c.Type != s.Type { - log.Printf("[TRACE] backendConfigNeedsMigration: type changed from %q to %q, so migration is required", s.Type, c.Type) - return true - } // We need the backend's schema to do our comparison here. - f := backendInit.Backend(c.Type) + f, canonType := backendInit.Backend(c.Type) if f == nil { log.Printf("[TRACE] backendConfigNeedsMigration: no backend of type %q, which migration codepath must handle", c.Type) return true // let the migration codepath deal with the missing backend } + if canonType != c.Type { + log.Printf("[TRACE] backendConfigNeedsMigration: using canonical backend type %q instead of configured alias %q", canonType, c.Type) + } + if canonType != s.Type { + log.Printf("[TRACE] backendConfigNeedsMigration: type changed from %q to %q, so migration is required", s.Type, canonType) + return true + } b := f(nil) // We don't need encryption here as it's only used for config/schema // We use "NoneRequired" here because we're only evaluating the body written directly @@ -1401,11 +1434,18 @@ func (m *Meta) backendInitFromConfig(ctx context.Context, c *configs.Backend, en var diags tfdiags.Diagnostics // Get the backend - f := backendInit.Backend(c.Type) + // Note that Meta.backendConfig should already have rewritten c.Type to be + // canonical before we were called, so we are expecting canonType to + // match c.Type now. + f, canonType := backendInit.Backend(c.Type) if f == nil { diags = diags.Append(fmt.Errorf(strings.TrimSpace(errBackendNewUnknown), c.Type)) return nil, cty.NilVal, diags } + if c.Type != canonType { + diags = diags.Append(fmt.Errorf("backend configuration still contains alias type %q instead of canonical %q; this is a bug in OpenTofu", c.Type, canonType)) + return nil, cty.NilVal, diags + } b := f(enc) schema := b.ConfigSchema() @@ -1428,7 +1468,7 @@ func (m *Meta) backendInitFromConfig(ctx context.Context, c *configs.Backend, en var err error configVal, err = m.inputForSchema(configVal, schema) if err != nil { - diags = diags.Append(fmt.Errorf("Error asking for input to configure backend %q: %w", c.Type, err)) + diags = diags.Append(fmt.Errorf("Error asking for input to configure backend %q: %w", canonType, err)) } // We get an unknown here if the if the user aborted input, but we can't diff --git a/internal/command/plan_test.go b/internal/command/plan_test.go index ffe7b6e2d0..f5bcc50b7c 100644 --- a/internal/command/plan_test.go +++ b/internal/command/plan_test.go @@ -518,7 +518,8 @@ func TestPlan_outBackend(t *testing.T) { t.Errorf("wrong backend workspace %q; want %q", got, want) } { - httpBackend := backendinit.Backend("http")(encryption.StateEncryptionDisabled()) + httpBackendInit, _ := backendinit.Backend("http") + httpBackend := httpBackendInit(encryption.StateEncryptionDisabled()) schema := httpBackend.ConfigSchema() got, err := plan.Backend.Config.Decode(schema.ImpliedType()) if err != nil {