diff --git a/internal/command/meta_backend.go b/internal/command/meta_backend.go index 21ee7e5fd506..40f758f795f3 100644 --- a/internal/command/meta_backend.go +++ b/internal/command/meta_backend.go @@ -276,7 +276,7 @@ func (m *Meta) selectWorkspace(b backend.Backend) error { log.Printf("[TRACE] Meta.selectWorkspace: selecting the new HCP Terraform workspace requested by the user (%s)", name) return m.SetWorkspace(name) } else { - return errors.New(strings.TrimSpace(errBackendNoExistingWorkspaces)) + return &errBackendNoExistingWorkspaces{} } } @@ -341,7 +341,7 @@ func (m *Meta) BackendForLocalPlan(settings plans.Backend) (backendrun.Operation f := backendInit.Backend(settings.Type) if f == nil { - diags = diags.Append(fmt.Errorf(strings.TrimSpace(errBackendSavedUnknown), settings.Type)) + diags = diags.Append(errBackendSavedUnknown{settings.Type}) return nil, diags } b := f() @@ -751,11 +751,7 @@ func (m *Meta) backendFromConfig(opts *BackendOpts) (backend.Backend, tfdiags.Di initReason := fmt.Sprintf("Unsetting the previously set backend %q", s.Backend.Type) if !opts.Init { - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Error, - "Backend initialization required, please run \"terraform init\"", - fmt.Sprintf(strings.TrimSpace(errBackendInit), initReason), - )) + diags = diags.Append(errBackendInitDiag(initReason)) return nil, diags } @@ -787,18 +783,10 @@ func (m *Meta) backendFromConfig(opts *BackendOpts) (backend.Backend, tfdiags.Di if !opts.Init { if backendConfig.Type == "cloud" { initReason := "Initial configuration of HCP Terraform or Terraform Enterprise" - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Error, - "HCP Terraform or Terraform Enterprise initialization required: please run \"terraform init\"", - fmt.Sprintf(strings.TrimSpace(errBackendInitCloud), initReason), - )) + diags = diags.Append(errBackendInitCloudDiag(initReason)) } else { initReason := fmt.Sprintf("Initial configuration of the requested backend %q", backendConfig.Type) - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Error, - "Backend initialization required, please run \"terraform init\"", - fmt.Sprintf(strings.TrimSpace(errBackendInit), initReason), - )) + diags = diags.Append(errBackendInitDiag(initReason)) } return nil, diags } @@ -979,23 +967,11 @@ func (m *Meta) determineInitReason(previousBackendType string, currentBackendTyp var diags tfdiags.Diagnostics switch cloudMode { case cloud.ConfigChangeInPlace: - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Error, - "HCP Terraform or Terraform Enterprise initialization required: please run \"terraform init\"", - fmt.Sprintf(strings.TrimSpace(errBackendInitCloud), initReason), - )) + diags = diags.Append(errBackendInitCloudDiag(initReason)) case cloud.ConfigMigrationIn: - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Error, - "HCP Terraform or Terraform Enterprise initialization required: please run \"terraform init\"", - fmt.Sprintf(strings.TrimSpace(errBackendInitCloud), initReason), - )) + diags = diags.Append(errBackendInitCloudDiag(initReason)) default: - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Error, - "Backend initialization required: please run \"terraform init\"", - fmt.Sprintf(strings.TrimSpace(errBackendInit), initReason), - )) + diags = diags.Append(errBackendInitDiag(initReason)) } return diags @@ -1035,7 +1011,7 @@ func (m *Meta) backendFromState(_ context.Context) (backend.Backend, tfdiags.Dia } f := backendInit.Backend(s.Backend.Type) if f == nil { - diags = diags.Append(fmt.Errorf(strings.TrimSpace(errBackendSavedUnknown), s.Backend.Type)) + diags = diags.Append(errBackendSavedUnknown{s.Backend.Type}) return nil, diags } b := f() @@ -1158,11 +1134,11 @@ func (m *Meta) backend_c_r_S( // Remove the stored metadata s.Backend = nil if err := sMgr.WriteState(s); err != nil { - diags = diags.Append(fmt.Errorf(strings.TrimSpace(errBackendClearSaved), err)) + diags = diags.Append(errBackendClearSaved{err}) return nil, diags } if err := sMgr.PersistState(); err != nil { - diags = diags.Append(fmt.Errorf(strings.TrimSpace(errBackendClearSaved), err)) + diags = diags.Append(errBackendClearSaved{err}) return nil, diags } @@ -1195,9 +1171,9 @@ func (m *Meta) backend_C_r_s(c *configs.Backend, cHash int, sMgr *clistate.Local } workspaces, wDiags := localB.Workspaces() - diags = diags.Append(wDiags) if wDiags.HasErrors() { - diags = diags.Append(fmt.Errorf(errBackendLocalRead, wDiags.Err())) + diags = diags.Append(wDiags.Warnings()) + diags = diags.Append(&errBackendLocalRead{wDiags.Err()}) return nil, diags } @@ -1205,11 +1181,12 @@ func (m *Meta) backend_C_r_s(c *configs.Backend, cHash int, sMgr *clistate.Local for _, workspace := range workspaces { localState, sDiags := localB.StateMgr(workspace) if sDiags.HasErrors() { - diags = diags.Append(fmt.Errorf(errBackendLocalRead, sDiags.Err())) + diags = diags.Append(sDiags.Warnings()) + diags = diags.Append(&errBackendLocalRead{sDiags.Err()}) return nil, diags } if err := localState.RefreshState(); err != nil { - diags = diags.Append(fmt.Errorf(errBackendLocalRead, err)) + diags = diags.Append(&errBackendLocalRead{err}) return nil, diags } @@ -1268,11 +1245,11 @@ func (m *Meta) backend_C_r_s(c *configs.Backend, cHash int, sMgr *clistate.Local for _, localState := range localStates { // We always delete the local state, unless that was our new state too. if err := localState.WriteState(nil); err != nil { - diags = diags.Append(fmt.Errorf(errBackendMigrateLocalDelete, err)) + diags = diags.Append(&errBackendMigrateLocalDelete{err}) return nil, diags } if err := localState.PersistState(nil); err != nil { - diags = diags.Append(fmt.Errorf(errBackendMigrateLocalDelete, err)) + diags = diags.Append(&errBackendMigrateLocalDelete{err}) return nil, diags } } @@ -1326,11 +1303,11 @@ func (m *Meta) backend_C_r_s(c *configs.Backend, cHash int, sMgr *clistate.Local } if err := sMgr.WriteState(s); err != nil { - diags = diags.Append(fmt.Errorf(errBackendWriteSaved, err)) + diags = diags.Append(errBackendWriteSavedDiag(err)) return nil, diags } if err := sMgr.PersistState(); err != nil { - diags = diags.Append(fmt.Errorf(errBackendWriteSaved, err)) + diags = diags.Append(errBackendWriteSavedDiag(err)) return nil, diags } @@ -1456,11 +1433,11 @@ func (m *Meta) backend_C_r_S_changed(c *configs.Backend, cHash int, sMgr *clista } if err := sMgr.WriteState(s); err != nil { - diags = diags.Append(fmt.Errorf(errBackendWriteSaved, err)) + diags = diags.Append(errBackendWriteSavedDiag(err)) return nil, diags } if err := sMgr.PersistState(); err != nil { - diags = diags.Append(fmt.Errorf(errBackendWriteSaved, err)) + diags = diags.Append(errBackendWriteSavedDiag(err)) return nil, diags } @@ -1489,7 +1466,7 @@ func (m *Meta) savedBackend(sMgr *clistate.LocalState) (backend.Backend, tfdiags // Get the backend f := backendInit.Backend(s.Backend.Type) if f == nil { - diags = diags.Append(fmt.Errorf(strings.TrimSpace(errBackendSavedUnknown), s.Backend.Type)) + diags = diags.Append(errBackendSavedUnknown{s.Backend.Type}) return nil, diags } b := f() @@ -1543,8 +1520,9 @@ func (m *Meta) updateSavedBackendHash(cHash int, sMgr *clistate.LocalState) tfdi if s.Backend.Hash != uint64(cHash) { s.Backend.Hash = uint64(cHash) if err := sMgr.WriteState(s); err != nil { - diags = diags.Append(err) + diags = diags.Append(errBackendWriteSavedDiag(err)) } + // No need to call PersistState as it's a no-op } return diags @@ -1788,7 +1766,7 @@ func (m *Meta) backendInitFromConfig(c *configs.Backend) (backend.Backend, cty.V // Get the backend f := backendInit.Backend(c.Type) if f == nil { - diags = diags.Append(fmt.Errorf(strings.TrimSpace(errBackendNewUnknown), c.Type)) + diags = diags.Append(errBackendNewUnknown{c.Type}) return nil, cty.NilVal, diags } b := f() @@ -2149,105 +2127,6 @@ func (m *Meta) GetStateStoreProviderFactory(config *configs.StateStore, locks *d // Output constants and initialization code //------------------------------------------------------------------- -const errBackendLocalRead = ` -Error reading local state: %s - -Terraform is trying to read your local state to determine if there is -state to migrate to your newly configured backend. Terraform can't continue -without this check because that would risk losing state. Please resolve the -error above and try again. -` - -const errBackendMigrateLocalDelete = ` -Error deleting local state after migration: %s - -Your local state is deleted after successfully migrating it to the newly -configured backend. As part of the deletion process, a backup is made at -the standard backup path unless explicitly asked not to. To cleanly operate -with a backend, we must delete the local state file. Please resolve the -issue above and retry the command. -` - -const errBackendNewUnknown = ` -The backend %q could not be found. - -This is the backend specified in your Terraform configuration file. -This error could be a simple typo in your configuration, but it can also -be caused by using a Terraform version that doesn't support the specified -backend type. Please check your configuration and your Terraform version. - -If you'd like to run Terraform and store state locally, you can fix this -error by removing the backend configuration from your configuration. -` - -const errBackendNoExistingWorkspaces = ` -No existing workspaces. - -Use the "terraform workspace" command to create and select a new workspace. -If the backend already contains existing workspaces, you may need to update -the backend configuration. -` - -const errBackendSavedUnknown = ` -The backend %q could not be found. - -This is the backend that this Terraform environment is configured to use -both in your configuration and saved locally as your last-used backend. -If it isn't found, it could mean an alternate version of Terraform was -used with this configuration. Please use the proper version of Terraform that -contains support for this backend. - -If you'd like to force remove this backend, you must update your configuration -to not use the backend and run "terraform init" (or any other command) again. -` - -const errBackendClearSaved = ` -Error clearing the backend configuration: %s - -Terraform removes the saved backend configuration when you're removing a -configured backend. This must be done so future Terraform runs know to not -use the backend configuration. Please look at the error above, resolve it, -and try again. -` - -const errBackendInit = ` -Reason: %s - -The "backend" is the interface that Terraform uses to store state, -perform operations, etc. If this message is showing up, it means that the -Terraform configuration you're using is using a custom configuration for -the Terraform backend. - -Changes to backend configurations require reinitialization. This allows -Terraform to set up the new configuration, copy existing state, etc. Please run -"terraform init" with either the "-reconfigure" or "-migrate-state" flags to -use the current configuration. - -If the change reason above is incorrect, please verify your configuration -hasn't changed and try again. At this point, no changes to your existing -configuration or state have been made. -` - -const errBackendInitCloud = ` -Reason: %s. - -Changes to the HCP Terraform configuration block require reinitialization, to discover any changes to the available workspaces. - -To re-initialize, run: - terraform init - -Terraform has not yet made changes to your existing configuration or state. -` - -const errBackendWriteSaved = ` -Error saving the backend configuration: %s - -Terraform saves the complete backend configuration in a local file for -configuring the backend on future operations. This cannot be disabled. Errors -are usually due to simple file permission errors. Please look at the error -above, resolve it, and try again. -` - const outputBackendMigrateChange = ` Terraform detected that the backend type changed from %q to %q. ` @@ -2280,10 +2159,3 @@ const successBackendSet = ` Successfully configured the backend %q! Terraform will automatically use this backend unless the backend configuration changes. ` - -var migrateOrReconfigDiag = tfdiags.Sourceless( - tfdiags.Error, - "Backend configuration changed", - "A change in the backend configuration has been detected, which may require migrating existing state.\n\n"+ - "If you wish to attempt automatic migration of the state, use \"terraform init -migrate-state\".\n"+ - `If you wish to store the current configuration with no changes to the state, use "terraform init -reconfigure".`) diff --git a/internal/command/meta_backend_errors.go b/internal/command/meta_backend_errors.go new file mode 100644 index 000000000000..a11b089dd337 --- /dev/null +++ b/internal/command/meta_backend_errors.go @@ -0,0 +1,188 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package command + +import ( + "fmt" + + "github.com/hashicorp/terraform/internal/tfdiags" +) + +// errBackendLocalRead is a custom error used to alert users that state +// files on their local filesystem were not erased successfully after +// migrating that state to a remote-state backend. +type errBackendLocalRead struct { + innerError error +} + +func (e *errBackendLocalRead) Error() string { + return fmt.Sprintf(`Error reading local state: %s + +Terraform is trying to read your local state to determine if there is +state to migrate to your newly configured backend. Terraform can't continue +without this check because that would risk losing state. Please resolve the +error above and try again.`, e.innerError) +} + +// errBackendMigrateLocalDelete is a custom error used to alert users that state +// files on their local filesystem were not erased successfully after migrating +// that state to a remote-state backend. +type errBackendMigrateLocalDelete struct { + innerError error +} + +func (e *errBackendMigrateLocalDelete) Error() string { + return fmt.Sprintf(`Error deleting local state after migration: %s + +Your local state is deleted after successfully migrating it to the newly +configured backend. As part of the deletion process, a backup is made at +the standard backup path unless explicitly asked not to. To cleanly operate +with a backend, we must delete the local state file. Please resolve the +issue above and retry the command.`, e.innerError) +} + +// errBackendSavedUnknown is a custom error used to alert users that their +// configuration describes a backend that's not implemented in Terraform. +type errBackendNewUnknown struct { + backendName string +} + +func (e *errBackendNewUnknown) Error() string { + return fmt.Sprintf(`The backend %q could not be found. + +This is the backend specified in your Terraform configuration file. +This error could be a simple typo in your configuration, but it can also +be caused by using a Terraform version that doesn't support the specified +backend type. Please check your configuration and your Terraform version. + +If you'd like to run Terraform and store state locally, you can fix this +error by removing the backend configuration from your configuration.`, e.backendName) +} + +// errBackendSavedUnknown is a custom error used to alert users that their +// plan file describes a backend that's not implemented in Terraform. +type errBackendSavedUnknown struct { + backendName string +} + +func (e *errBackendSavedUnknown) Error() string { + return fmt.Sprintf(`The backend %q could not be found. + +This is the backend that this Terraform environment is configured to use +both in your configuration and saved locally as your last-used backend. +If it isn't found, it could mean an alternate version of Terraform was +used with this configuration. Please use the proper version of Terraform that +contains support for this backend. + +If you'd like to force remove this backend, you must update your configuration +to not use the backend and run "terraform init" (or any other command) again.`, e.backendName) +} + +// errBackendClearSaved is a custom error used to alert users that +// Terraform failed to empty the backend state file's contents. +type errBackendClearSaved struct { + innerError error +} + +func (e *errBackendClearSaved) Error() string { + return fmt.Sprintf(`Error clearing the backend configuration: %s + +Terraform removes the saved backend configuration when you're removing a +configured backend. This must be done so future Terraform runs know to not +use the backend configuration. Please look at the error above, resolve it, +and try again.`, e.innerError) +} + +// errBackendInitDiag creates a diagnostic to present to users when +// users attempt to run a non-init command after making a change to their +// backend configuration. +// +// An init reason should be provided as an argument. +func errBackendInitDiag(initReason string) tfdiags.Diagnostic { + msg := fmt.Sprintf(`Reason: %s + +The "backend" is the interface that Terraform uses to store state, +perform operations, etc. If this message is showing up, it means that the +Terraform configuration you're using is using a custom configuration for +the Terraform backend. + +Changes to backend configurations require reinitialization. This allows +Terraform to set up the new configuration, copy existing state, etc. Please run +"terraform init" with either the "-reconfigure" or "-migrate-state" flags to +use the current configuration. + +If the change reason above is incorrect, please verify your configuration +hasn't changed and try again. At this point, no changes to your existing +configuration or state have been made.`, initReason) + + return tfdiags.Sourceless( + tfdiags.Error, + "Backend initialization required, please run \"terraform init\"", + msg, + ) +} + +// errBackendInitCloudDiag creates a diagnostic to present to users when +// an init command encounters config changes in a `cloud` block. +// +// An init reason should be provided as an argument. +func errBackendInitCloudDiag(initReason string) tfdiags.Diagnostic { + msg := fmt.Sprintf(`Reason: %s. + +Changes to the HCP Terraform configuration block require reinitialization, to discover any changes to the available workspaces. + +To re-initialize, run: + terraform init + +Terraform has not yet made changes to your existing configuration or state.`, initReason) + + return tfdiags.Sourceless( + tfdiags.Error, + "HCP Terraform or Terraform Enterprise initialization required: please run \"terraform init\"", + msg, + ) +} + +// errBackendWriteSavedDiag creates a diagnostic to present to users when +// an init command experiences an error while writing to the backend state file. +func errBackendWriteSavedDiag(innerError error) tfdiags.Diagnostic { + msg := fmt.Sprintf(`Error saving the backend configuration: %s + +Terraform saves the complete backend configuration in a local file for +configuring the backend on future operations. This cannot be disabled. Errors +are usually due to simple file permission errors. Please look at the error +above, resolve it, and try again.`, innerError) + + return tfdiags.Sourceless( + tfdiags.Error, + "HCP Terraform or Terraform Enterprise initialization required: please run \"terraform init\"", + msg, + ) +} + +// errBackendNoExistingWorkspaces is returned by calling code when it expects a backend.Backend +// to report one or more workspaces exist. +// +// The returned error may be used as a sentinel error and acted upon or just wrapped in a +// diagnostic and returned. +type errBackendNoExistingWorkspaces struct{} + +func (e *errBackendNoExistingWorkspaces) Error() string { + return `No existing workspaces. + +Use the "terraform workspace" command to create and select a new workspace. +If the backend already contains existing workspaces, you may need to update +the backend configuration.` +} + +// migrateOrReconfigDiag creates a diagnostic to present to users when +// an init command encounters a mismatch in backend state and the current config +// and Terraform needs users to provide additional instructions about how Terraform +// should proceed. +var migrateOrReconfigDiag = tfdiags.Sourceless( + tfdiags.Error, + "Backend configuration changed", + "A change in the backend configuration has been detected, which may require migrating existing state.\n\n"+ + "If you wish to attempt automatic migration of the state, use \"terraform init -migrate-state\".\n"+ + `If you wish to store the current configuration with no changes to the state, use "terraform init -reconfigure".`)