diff --git a/internal/backend/backend.go b/internal/backend/backend.go index d07c38df07..ca439a4bc5 100644 --- a/internal/backend/backend.go +++ b/internal/backend/backend.go @@ -238,6 +238,9 @@ type Operation struct { // Type is the operation to perform. Type OperationType + // Encryption is used by enhanced backends for planning and tofu.Context initialization + Encryption encryption.Encryption + // PlanId is an opaque value that backends can use to execute a specific // plan for an apply operation. // diff --git a/internal/backend/local/backend.go b/internal/backend/local/backend.go index 24f667fd4d..d7a662a013 100644 --- a/internal/backend/local/backend.go +++ b/internal/backend/local/backend.go @@ -106,9 +106,6 @@ func New(enc encryption.StateEncryption) *Local { // NewWithBackend returns a new local backend initialized with a // dedicated backend for non-enhanced behavior. func NewWithBackend(backend backend.Backend, enc encryption.StateEncryption) *Local { - if backend == nil && enc == nil { - panic("either backend or encryption required for backend.Local initialization") - } return &Local{ Backend: backend, encryption: enc, diff --git a/internal/backend/local/backend_apply_test.go b/internal/backend/local/backend_apply_test.go index 6dc2c9000e..8878e08c4a 100644 --- a/internal/backend/local/backend_apply_test.go +++ b/internal/backend/local/backend_apply_test.go @@ -379,6 +379,7 @@ func testOperationApply(t *testing.T, configDir string) (*backend.Operation, fun return &backend.Operation{ Type: backend.OperationTypeApply, + Encryption: encryption.Disabled(), ConfigDir: configDir, ConfigLoader: configLoader, StateLocker: clistate.NewNoopLocker(), diff --git a/internal/backend/local/backend_local.go b/internal/backend/local/backend_local.go index 370846c0df..6e31503c58 100644 --- a/internal/backend/local/backend_local.go +++ b/internal/backend/local/backend_local.go @@ -78,6 +78,7 @@ func (b *Local) localRun(op *backend.Operation) (*backend.LocalRun, *configload. } coreOpts.UIInput = op.UIIn coreOpts.Hooks = op.Hooks + coreOpts.Encryption = op.Encryption var ctxDiags tfdiags.Diagnostics var configSnap *configload.Snapshot diff --git a/internal/backend/local/backend_plan.go b/internal/backend/local/backend_plan.go index 07e0c10ac2..d491ba1eda 100644 --- a/internal/backend/local/backend_plan.go +++ b/internal/backend/local/backend_plan.go @@ -12,7 +12,6 @@ import ( "log" "github.com/opentofu/opentofu/internal/backend" - "github.com/opentofu/opentofu/internal/encryption" "github.com/opentofu/opentofu/internal/genconfig" "github.com/opentofu/opentofu/internal/logging" "github.com/opentofu/opentofu/internal/plans" @@ -173,7 +172,7 @@ func (b *Local) opPlan( StateFile: plannedStateFile, Plan: plan, DependencyLocks: op.DependencyLocks, - }, encryption.PlanEncryptionTODO()) + }, op.Encryption.PlanFile()) if err != nil { diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, diff --git a/internal/backend/local/backend_plan_test.go b/internal/backend/local/backend_plan_test.go index e290a3a0dd..d2d908cc6f 100644 --- a/internal/backend/local/backend_plan_test.go +++ b/internal/backend/local/backend_plan_test.go @@ -732,6 +732,7 @@ func testOperationPlan(t *testing.T, configDir string) (*backend.Operation, func return &backend.Operation{ Type: backend.OperationTypePlan, + Encryption: encryption.Disabled(), ConfigDir: configDir, ConfigLoader: configLoader, StateLocker: clistate.NewNoopLocker(), diff --git a/internal/backend/remote/backend_context.go b/internal/backend/remote/backend_context.go index 2a2976b87f..54bd07e149 100644 --- a/internal/backend/remote/backend_context.go +++ b/internal/backend/remote/backend_context.go @@ -72,6 +72,7 @@ func (b *Remote) LocalRun(op *backend.Operation) (*backend.LocalRun, statemgr.Fu // Copy set options from the operation opts.UIInput = op.UIIn + opts.Encryption = op.Encryption // Load the latest state. If we enter contextFromPlanFile below then the // state snapshot in the plan file must match this, or else it'll return diff --git a/internal/builtin/providers/tf/data_source_state.go b/internal/builtin/providers/tf/data_source_state.go index acbbaf8b4f..7d5b0ac7f6 100644 --- a/internal/builtin/providers/tf/data_source_state.go +++ b/internal/builtin/providers/tf/data_source_state.go @@ -73,7 +73,7 @@ func dataSourceRemoteStateValidate(cfg cty.Value) tfdiags.Diagnostics { // Getting the backend implicitly validates the configuration for it, // but we can only do that if it's all known already. if cfg.GetAttr("config").IsWhollyKnown() && cfg.GetAttr("backend").IsKnown() { - _, _, moreDiags := getBackend(cfg) + _, _, moreDiags := getBackend(cfg, nil) // Don't need the encryption for validation here diags = diags.Append(moreDiags) } else { // Otherwise we'll just type-check the config object itself. @@ -103,10 +103,10 @@ func dataSourceRemoteStateValidate(cfg cty.Value) tfdiags.Diagnostics { return diags } -func dataSourceRemoteStateRead(d cty.Value) (cty.Value, tfdiags.Diagnostics) { +func dataSourceRemoteStateRead(d cty.Value, enc encryption.StateEncryption) (cty.Value, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics - b, cfg, moreDiags := getBackend(d) + b, cfg, moreDiags := getBackend(d, enc) diags = diags.Append(moreDiags) if moreDiags.HasErrors() { return cty.NilVal, diags @@ -184,7 +184,7 @@ func dataSourceRemoteStateRead(d cty.Value) (cty.Value, tfdiags.Diagnostics) { return cty.ObjectVal(newState), diags } -func getBackend(cfg cty.Value) (backend.Backend, cty.Value, tfdiags.Diagnostics) { +func getBackend(cfg cty.Value, enc encryption.StateEncryption) (backend.Backend, cty.Value, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics backendType := cfg.GetAttr("backend").AsString() @@ -212,7 +212,7 @@ func getBackend(cfg cty.Value) (backend.Backend, cty.Value, tfdiags.Diagnostics) )) return nil, cty.NilVal, diags } - b := f(encryption.StateEncryptionTODO()) + b := f(enc) config := cfg.GetAttr("config") if config.IsNull() { diff --git a/internal/builtin/providers/tf/data_source_state_test.go b/internal/builtin/providers/tf/data_source_state_test.go index d72e49be49..a5f0d64780 100644 --- a/internal/builtin/providers/tf/data_source_state_test.go +++ b/internal/builtin/providers/tf/data_source_state_test.go @@ -296,7 +296,7 @@ func TestState_basic(t *testing.T) { var got cty.Value if !diags.HasErrors() && config.IsWhollyKnown() { var moreDiags tfdiags.Diagnostics - got, moreDiags = dataSourceRemoteStateRead(config) + got, moreDiags = dataSourceRemoteStateRead(config, encryption.StateEncryptionDisabled()) diags = diags.Append(moreDiags) } diff --git a/internal/builtin/providers/tf/provider.go b/internal/builtin/providers/tf/provider.go index 6d6c2822d8..038730835f 100644 --- a/internal/builtin/providers/tf/provider.go +++ b/internal/builtin/providers/tf/provider.go @@ -8,7 +8,10 @@ package tf import ( "fmt" "log" + "strings" + "github.com/opentofu/opentofu/internal/addrs" + "github.com/opentofu/opentofu/internal/encryption" "github.com/opentofu/opentofu/internal/providers" ) @@ -70,6 +73,10 @@ func (p *Provider) ConfigureProvider(providers.ConfigureProviderRequest) provide // ReadDataSource returns the data source's current state. func (p *Provider) ReadDataSource(req providers.ReadDataSourceRequest) providers.ReadDataSourceResponse { + panic("Should not be called directly, special case for terraform_remote_state") +} + +func (p *Provider) ReadDataSourceEncrypted(req providers.ReadDataSourceRequest, path addrs.AbsResourceInstance, enc encryption.Encryption) providers.ReadDataSourceResponse { // call function var res providers.ReadDataSourceResponse @@ -79,7 +86,23 @@ func (p *Provider) ReadDataSource(req providers.ReadDataSourceRequest) providers return res } - newState, diags := dataSourceRemoteStateRead(req.Config) + // These string manipulations are kind of funky + key := path.String() + + // data.terraform_remote_state.foo[4] -> foo[4] + // module.submod[1].data.terraform_remote_state.bar -> module.submod[1].bar + key = strings.Replace(key, "data.terraform_remote_state.", "", 1) + + // module.submod[1].bar -> submod[1].bar + key = strings.TrimPrefix(key, "module.") + + log.Printf("[DEBUG] accessing remote state at %s", key) + + newState, diags := dataSourceRemoteStateRead(req.Config, enc.RemoteState(key)) + + if diags.HasErrors() { + diags = diags.Append(fmt.Errorf("%s: Unable to read remote state", path.String())) + } res.State = newState res.Diagnostics = diags diff --git a/internal/cloud/backend_context.go b/internal/cloud/backend_context.go index 4a49bff74a..5010ed69a2 100644 --- a/internal/cloud/backend_context.go +++ b/internal/cloud/backend_context.go @@ -72,6 +72,7 @@ func (b *Cloud) LocalRun(op *backend.Operation) (*backend.LocalRun, statemgr.Ful // Copy set options from the operation opts.UIInput = op.UIIn + opts.Encryption = op.Encryption // Load the latest state. If we enter contextFromPlanFile below then the // state snapshot in the plan file must match this, or else it'll return diff --git a/internal/command/apply.go b/internal/command/apply.go index 118f7608c3..2197ae6146 100644 --- a/internal/command/apply.go +++ b/internal/command/apply.go @@ -12,6 +12,7 @@ import ( "github.com/opentofu/opentofu/internal/backend" "github.com/opentofu/opentofu/internal/command/arguments" "github.com/opentofu/opentofu/internal/command/views" + "github.com/opentofu/opentofu/internal/encryption" "github.com/opentofu/opentofu/internal/plans/planfile" "github.com/opentofu/opentofu/internal/tfdiags" ) @@ -66,8 +67,16 @@ func (c *ApplyCommand) Run(rawArgs []string) int { return 1 } + // Load the encryption configuration + enc, encDiags := c.Encryption() + diags = diags.Append(encDiags) + if encDiags.HasErrors() { + view.Diagnostics(diags) + return 1 + } + // Attempt to load the plan file, if specified - planFile, diags := c.LoadPlanFile(args.PlanPath) + planFile, diags := c.LoadPlanFile(args.PlanPath, enc) if diags.HasErrors() { view.Diagnostics(diags) return 1 @@ -99,7 +108,7 @@ func (c *ApplyCommand) Run(rawArgs []string) int { // Prepare the backend, passing the plan file if present, and the // backend-specific arguments - be, beDiags := c.PrepareBackend(planFile, args.State, args.ViewType) + be, beDiags := c.PrepareBackend(planFile, args.State, args.ViewType, enc.Backend()) diags = diags.Append(beDiags) if diags.HasErrors() { view.Diagnostics(diags) @@ -107,7 +116,7 @@ func (c *ApplyCommand) Run(rawArgs []string) int { } // Build the operation request - opReq, opDiags := c.OperationRequest(be, view, args.ViewType, planFile, args.Operation, args.AutoApprove) + opReq, opDiags := c.OperationRequest(be, view, args.ViewType, planFile, args.Operation, args.AutoApprove, enc) diags = diags.Append(opDiags) // Collect variable value and add them to the operation request @@ -152,14 +161,14 @@ func (c *ApplyCommand) Run(rawArgs []string) int { return 0 } -func (c *ApplyCommand) LoadPlanFile(path string) (*planfile.WrappedPlanFile, tfdiags.Diagnostics) { +func (c *ApplyCommand) LoadPlanFile(path string, enc encryption.Encryption) (*planfile.WrappedPlanFile, tfdiags.Diagnostics) { var planFile *planfile.WrappedPlanFile var diags tfdiags.Diagnostics // Try to load plan if path is specified if path != "" { var err error - planFile, err = c.PlanFile(path) + planFile, err = c.PlanFile(path, enc.PlanFile()) if err != nil { diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, @@ -196,7 +205,7 @@ func (c *ApplyCommand) LoadPlanFile(path string) (*planfile.WrappedPlanFile, tfd return planFile, diags } -func (c *ApplyCommand) PrepareBackend(planFile *planfile.WrappedPlanFile, args *arguments.State, viewType arguments.ViewType) (backend.Enhanced, tfdiags.Diagnostics) { +func (c *ApplyCommand) PrepareBackend(planFile *planfile.WrappedPlanFile, args *arguments.State, viewType arguments.ViewType, enc encryption.StateEncryption) (backend.Enhanced, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics // FIXME: we need to apply the state arguments to the meta object here @@ -227,7 +236,7 @@ func (c *ApplyCommand) PrepareBackend(planFile *planfile.WrappedPlanFile, args * )) return nil, diags } - be, beDiags = c.BackendForLocalPlan(plan.Backend) + be, beDiags = c.BackendForLocalPlan(plan.Backend, enc) } else { // Both new plans and saved cloud plans load their backend from config. backendConfig, configDiags := c.loadBackendConfig(".") @@ -239,7 +248,7 @@ func (c *ApplyCommand) PrepareBackend(planFile *planfile.WrappedPlanFile, args * be, beDiags = c.Backend(&BackendOpts{ Config: backendConfig, ViewType: viewType, - }) + }, enc) } diags = diags.Append(beDiags) @@ -256,6 +265,7 @@ func (c *ApplyCommand) OperationRequest( planFile *planfile.WrappedPlanFile, args *arguments.Operation, autoApprove bool, + enc encryption.Encryption, ) (*backend.Operation, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics @@ -265,7 +275,7 @@ func (c *ApplyCommand) OperationRequest( diags = diags.Append(c.providerDevOverrideRuntimeWarnings()) // Build the operation - opReq := c.Operation(be, viewType) + opReq := c.Operation(be, viewType, enc) opReq.AutoApprove = autoApprove opReq.ConfigDir = "." opReq.PlanMode = args.PlanMode diff --git a/internal/command/autocomplete.go b/internal/command/autocomplete.go index e2f7e46de9..abd3fa4c56 100644 --- a/internal/command/autocomplete.go +++ b/internal/command/autocomplete.go @@ -57,7 +57,7 @@ func (m *Meta) completePredictWorkspaceName() complete.Predictor { b, diags := m.Backend(&BackendOpts{ Config: backendConfig, - }) + }, nil) // Don't need state encryption here. if diags.HasErrors() { return nil } diff --git a/internal/command/console.go b/internal/command/console.go index 2153fd3341..e5812e56d7 100644 --- a/internal/command/console.go +++ b/internal/command/console.go @@ -52,6 +52,14 @@ func (c *ConsoleCommand) Run(args []string) int { var diags tfdiags.Diagnostics + // Load the encryption configuration + enc, encDiags := c.EncryptionFromPath(configPath) + diags = diags.Append(encDiags) + if encDiags.HasErrors() { + c.showDiagnostics(diags) + return 1 + } + backendConfig, backendDiags := c.loadBackendConfig(configPath) diags = diags.Append(backendDiags) if diags.HasErrors() { @@ -62,7 +70,7 @@ func (c *ConsoleCommand) Run(args []string) int { // Load the backend b, backendDiags := c.Backend(&BackendOpts{ Config: backendConfig, - }) + }, enc.Backend()) diags = diags.Append(backendDiags) if backendDiags.HasErrors() { c.showDiagnostics(diags) @@ -81,7 +89,7 @@ func (c *ConsoleCommand) Run(args []string) int { c.ignoreRemoteVersionConflict(b) // Build the operation - opReq := c.Operation(b, arguments.ViewHuman) + opReq := c.Operation(b, arguments.ViewHuman, enc) opReq.ConfigDir = configPath opReq.ConfigLoader, err = c.initConfigLoader() opReq.AllowUnsetVariables = true // we'll just evaluate them as unknown diff --git a/internal/command/graph.go b/internal/command/graph.go index eb9617a927..648e121b1e 100644 --- a/internal/command/graph.go +++ b/internal/command/graph.go @@ -25,6 +25,8 @@ type GraphCommand struct { } func (c *GraphCommand) Run(args []string) int { + var diags tfdiags.Diagnostics + var drawCycles bool var graphTypeStr string var moduleDepth int @@ -56,18 +58,24 @@ func (c *GraphCommand) Run(args []string) int { return 1 } + // Load the encryption configuration + enc, encDiags := c.EncryptionFromPath(configPath) + diags = diags.Append(encDiags) + if encDiags.HasErrors() { + c.showDiagnostics(diags) + return 1 + } + // Try to load plan if path is specified var planFile *planfile.WrappedPlanFile if planPath != "" { - planFile, err = c.PlanFile(planPath) + planFile, err = c.PlanFile(planPath, enc.PlanFile()) if err != nil { c.Ui.Error(err.Error()) return 1 } } - var diags tfdiags.Diagnostics - backendConfig, backendDiags := c.loadBackendConfig(configPath) diags = diags.Append(backendDiags) if diags.HasErrors() { @@ -78,7 +86,7 @@ func (c *GraphCommand) Run(args []string) int { // Load the backend b, backendDiags := c.Backend(&BackendOpts{ Config: backendConfig, - }) + }, enc.Backend()) diags = diags.Append(backendDiags) if backendDiags.HasErrors() { c.showDiagnostics(diags) @@ -97,7 +105,7 @@ func (c *GraphCommand) Run(args []string) int { c.ignoreRemoteVersionConflict(b) // Build the operation - opReq := c.Operation(b, arguments.ViewHuman) + opReq := c.Operation(b, arguments.ViewHuman, enc) opReq.ConfigDir = configPath opReq.ConfigLoader, err = c.initConfigLoader() opReq.PlanFile = planFile diff --git a/internal/command/import.go b/internal/command/import.go index 42177d9aa8..513d8ee75b 100644 --- a/internal/command/import.go +++ b/internal/command/import.go @@ -110,6 +110,14 @@ func (c *ImportCommand) Run(args []string) int { return 1 } + // Load the encryption configuration + enc, encDiags := c.EncryptionFromPath(configPath) + diags = diags.Append(encDiags) + if encDiags.HasErrors() { + c.showDiagnostics(diags) + return 1 + } + // Verify that the given address points to something that exists in config. // This is to reduce the risk that a typo in the resource address will // import something that OpenTofu will want to immediately destroy on @@ -167,7 +175,7 @@ func (c *ImportCommand) Run(args []string) int { // Load the backend b, backendDiags := c.Backend(&BackendOpts{ Config: config.Module.Backend, - }) + }, enc.Backend()) diags = diags.Append(backendDiags) if backendDiags.HasErrors() { c.showDiagnostics(diags) @@ -186,7 +194,7 @@ func (c *ImportCommand) Run(args []string) int { } // Build the operation - opReq := c.Operation(b, arguments.ViewHuman) + opReq := c.Operation(b, arguments.ViewHuman, enc) opReq.ConfigDir = configPath opReq.ConfigLoader, err = c.initConfigLoader() if err != nil { diff --git a/internal/command/init.go b/internal/command/init.go index f236a37c24..cc0a4a968d 100644 --- a/internal/command/init.go +++ b/internal/command/init.go @@ -28,6 +28,7 @@ import ( "github.com/opentofu/opentofu/internal/command/arguments" "github.com/opentofu/opentofu/internal/configs" "github.com/opentofu/opentofu/internal/configs/configschema" + "github.com/opentofu/opentofu/internal/encryption" "github.com/opentofu/opentofu/internal/getproviders" "github.com/opentofu/opentofu/internal/providercache" "github.com/opentofu/opentofu/internal/states" @@ -190,6 +191,14 @@ func (c *InitCommand) Run(args []string) int { return 1 } + // Load the encryption configuration + enc, encDiags := c.EncryptionFromModule(rootModEarly) + diags = diags.Append(encDiags) + if encDiags.HasErrors() { + c.showDiagnostics(diags) + return 1 + } + var back backend.Backend // There may be config errors or backend init errors but these will be shown later _after_ @@ -199,12 +208,12 @@ func (c *InitCommand) Run(args []string) int { switch { case flagCloud && rootModEarly.CloudConfig != nil: - back, backendOutput, backDiags = c.initCloud(ctx, rootModEarly, flagConfigExtra) + back, backendOutput, backDiags = c.initCloud(ctx, rootModEarly, flagConfigExtra, enc) case flagBackend: - back, backendOutput, backDiags = c.initBackend(ctx, rootModEarly, flagConfigExtra) + back, backendOutput, backDiags = c.initBackend(ctx, rootModEarly, flagConfigExtra, enc) default: // load the previously-stored backend config - back, backDiags = c.Meta.backendFromState(ctx) + back, backDiags = c.Meta.backendFromState(ctx, enc.Backend()) } if backendOutput { header = true @@ -413,7 +422,7 @@ func (c *InitCommand) getModules(ctx context.Context, path, testsDir string, ear return true, installAbort, diags } -func (c *InitCommand) initCloud(ctx context.Context, root *configs.Module, extraConfig rawFlags) (be backend.Backend, output bool, diags tfdiags.Diagnostics) { +func (c *InitCommand) initCloud(ctx context.Context, root *configs.Module, extraConfig rawFlags, enc encryption.Encryption) (be backend.Backend, output bool, diags tfdiags.Diagnostics) { ctx, span := tracer.Start(ctx, "initialize cloud backend") _ = ctx // prevent staticcheck from complaining to avoid a maintenence hazard of having the wrong ctx in scope here defer span.End() @@ -436,12 +445,12 @@ func (c *InitCommand) initCloud(ctx context.Context, root *configs.Module, extra Init: true, } - back, backDiags := c.Backend(opts) + back, backDiags := c.Backend(opts, enc.Backend()) diags = diags.Append(backDiags) return back, true, diags } -func (c *InitCommand) initBackend(ctx context.Context, root *configs.Module, extraConfig rawFlags) (be backend.Backend, output bool, diags tfdiags.Diagnostics) { +func (c *InitCommand) initBackend(ctx context.Context, root *configs.Module, extraConfig rawFlags, enc encryption.Encryption) (be backend.Backend, output bool, diags tfdiags.Diagnostics) { ctx, span := tracer.Start(ctx, "initialize backend") _ = ctx // prevent staticcheck from complaining to avoid a maintenence hazard of having the wrong ctx in scope here defer span.End() @@ -519,7 +528,7 @@ the backend configuration is present and valid. Init: true, } - back, backDiags := c.Backend(opts) + back, backDiags := c.Backend(opts, enc.Backend()) diags = diags.Append(backDiags) return back, true, diags } diff --git a/internal/command/meta_backend.go b/internal/command/meta_backend.go index 9f8fa08829..9bd403eed5 100644 --- a/internal/command/meta_backend.go +++ b/internal/command/meta_backend.go @@ -92,7 +92,7 @@ type BackendWithRemoteTerraformVersion interface { // A side-effect of this method is the population of m.backendState, recording // the final resolved backend configuration after dealing with overrides from // the "tofu init" command line, etc. -func (m *Meta) Backend(opts *BackendOpts) (backend.Enhanced, tfdiags.Diagnostics) { +func (m *Meta) Backend(opts *BackendOpts, enc encryption.StateEncryption) (backend.Enhanced, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics // If no opts are set, then initialize @@ -105,7 +105,7 @@ func (m *Meta) Backend(opts *BackendOpts) (backend.Enhanced, tfdiags.Diagnostics var b backend.Backend if !opts.ForceLocal { var backendDiags tfdiags.Diagnostics - b, backendDiags = m.backendFromConfig(opts) + b, backendDiags = m.backendFromConfig(opts, enc) diags = diags.Append(backendDiags) if diags.HasErrors() { @@ -182,7 +182,7 @@ func (m *Meta) Backend(opts *BackendOpts) (backend.Enhanced, tfdiags.Diagnostics } // Build the local backend - local := backendLocal.NewWithBackend(b, encryption.StateEncryptionTODO()) + local := backendLocal.NewWithBackend(b, enc) if err := local.CLIInit(cliOpts); err != nil { // Local backend isn't allowed to fail. It would be a bug. panic(err) @@ -305,7 +305,7 @@ func (m *Meta) selectWorkspace(b backend.Backend) error { // The current workspace name is also stored as part of the plan, and so this // method will check that it matches the currently-selected workspace name // and produce error diagnostics if not. -func (m *Meta) BackendForLocalPlan(settings plans.Backend) (backend.Enhanced, tfdiags.Diagnostics) { +func (m *Meta) BackendForLocalPlan(settings plans.Backend, enc encryption.StateEncryption) (backend.Enhanced, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics f := backendInit.Backend(settings.Type) @@ -313,7 +313,7 @@ func (m *Meta) BackendForLocalPlan(settings plans.Backend) (backend.Enhanced, tf diags = diags.Append(fmt.Errorf(strings.TrimSpace(errBackendSavedUnknown), settings.Type)) return nil, diags } - b := f(encryption.StateEncryptionTODO()) + b := f(enc) log.Printf("[TRACE] Meta.BackendForLocalPlan: instantiated backend of type %T", b) schema := b.ConfigSchema() @@ -372,7 +372,7 @@ func (m *Meta) BackendForLocalPlan(settings plans.Backend) (backend.Enhanced, tf return nil, diags } cliOpts.Validation = false // don't validate here in case config contains file(...) calls where the file doesn't exist - local := backendLocal.NewWithBackend(b, encryption.StateEncryptionTODO()) + local := backendLocal.NewWithBackend(b, enc) if err := local.CLIInit(cliOpts); err != nil { // Local backend should never fail, so this is always a bug. panic(err) @@ -406,7 +406,7 @@ func (m *Meta) backendCLIOpts() (*backend.CLIOpts, error) { // This prepares the operation. After calling this, the caller is expected // to modify fields of the operation such as Sequence to specify what will // be called. -func (m *Meta) Operation(b backend.Backend, vt arguments.ViewType) *backend.Operation { +func (m *Meta) Operation(b backend.Backend, vt arguments.ViewType, enc encryption.Encryption) *backend.Operation { schema := b.ConfigSchema() workspace, err := m.Workspace() if err != nil { @@ -441,6 +441,7 @@ func (m *Meta) Operation(b backend.Backend, vt arguments.ViewType) *backend.Oper } return &backend.Operation{ + Encryption: enc, PlanOutBackend: planOutBackend, Targets: m.targets, UIIn: m.UIInput(), @@ -494,7 +495,7 @@ func (m *Meta) backendConfig(opts *BackendOpts) (*configs.Backend, int, tfdiags. }) return nil, 0, diags } - b := bf(encryption.StateEncryptionTODO()) + b := bf(nil) // Just using this for config/schema, don't need encryption here configSchema := b.ConfigSchema() configBody := c.Config @@ -527,7 +528,7 @@ func (m *Meta) backendConfig(opts *BackendOpts) (*configs.Backend, int, tfdiags. // // This function may query the user for input unless input is disabled, in // which case this function will error. -func (m *Meta) backendFromConfig(opts *BackendOpts) (backend.Backend, tfdiags.Diagnostics) { +func (m *Meta) backendFromConfig(opts *BackendOpts, enc encryption.StateEncryption) (backend.Backend, tfdiags.Diagnostics) { // Get the local backend configuration. c, cHash, diags := m.backendConfig(opts) if diags.HasErrors() { @@ -626,7 +627,7 @@ func (m *Meta) backendFromConfig(opts *BackendOpts) (backend.Backend, tfdiags.Di return nil, diags } - return m.backend_c_r_S(c, cHash, sMgr, true, opts) + return m.backend_c_r_S(c, cHash, sMgr, true, opts, enc) // Configuring a backend for the first time or -reconfigure flag was used case c != nil && s.Backend.Empty(): @@ -649,7 +650,7 @@ func (m *Meta) backendFromConfig(opts *BackendOpts) (backend.Backend, tfdiags.Di } return nil, diags } - return m.backend_C_r_s(c, cHash, sMgr, opts) + return m.backend_C_r_s(c, cHash, sMgr, opts, enc) // Potentially changing a backend configuration case c != nil && !s.Backend.Empty(): // We are not going to migrate if... @@ -659,7 +660,7 @@ func (m *Meta) backendFromConfig(opts *BackendOpts) (backend.Backend, tfdiags.Di // AND we're not providing any overrides. An override can mean a change overriding an unchanged backend block (indicated by the hash value). if (uint64(cHash) == s.Backend.Hash) && (!opts.Init || opts.ConfigOverride == nil) { log.Printf("[TRACE] Meta.Backend: using already-initialized, unchanged %q backend configuration", c.Type) - savedBackend, diags := m.savedBackend(sMgr) + savedBackend, diags := m.savedBackend(sMgr, enc) // Verify that selected workspace exist. Otherwise prompt user to create one if opts.Init && savedBackend != nil { if err := m.selectWorkspace(savedBackend); err != nil { @@ -676,7 +677,7 @@ func (m *Meta) backendFromConfig(opts *BackendOpts) (backend.Backend, tfdiags.Di // don't need to migrate, we update the backend cache hash value. if !m.backendConfigNeedsMigration(c, s.Backend) { log.Printf("[TRACE] Meta.Backend: using already-initialized %q backend configuration", c.Type) - savedBackend, moreDiags := m.savedBackend(sMgr) + savedBackend, moreDiags := m.savedBackend(sMgr, enc) diags = diags.Append(moreDiags) if moreDiags.HasErrors() { return nil, diags @@ -716,7 +717,7 @@ func (m *Meta) backendFromConfig(opts *BackendOpts) (backend.Backend, tfdiags.Di } log.Printf("[WARN] backend config has changed since last init") - return m.backend_C_r_S_changed(c, cHash, sMgr, true, opts) + return m.backend_C_r_S_changed(c, cHash, sMgr, true, opts, enc) default: diags = diags.Append(fmt.Errorf( @@ -777,7 +778,7 @@ func (m *Meta) determineInitReason(previousBackendType string, currentBackendTyp // from the backend state. This should be used only when a user runs // `tofu init -backend=false`. This function returns a local backend if // there is no backend state or no backend configured. -func (m *Meta) backendFromState(ctx context.Context) (backend.Backend, tfdiags.Diagnostics) { +func (m *Meta) backendFromState(ctx context.Context, enc encryption.StateEncryption) (backend.Backend, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics // Get the path to where we store a local cache of backend configuration // if we're using a remote backend. This may not yet exist which means @@ -792,25 +793,25 @@ func (m *Meta) backendFromState(ctx context.Context) (backend.Backend, tfdiags.D if s == nil { // no state, so return a local backend log.Printf("[TRACE] Meta.Backend: backend has not previously been initialized in this working directory") - return backendLocal.New(encryption.StateEncryptionTODO()), diags + return backendLocal.New(enc), diags } if s.Backend == nil { // s.Backend is nil, so return a local backend log.Printf("[TRACE] Meta.Backend: working directory was previously initialized but has no backend (is using legacy remote state?)") - return backendLocal.New(encryption.StateEncryptionTODO()), diags + return backendLocal.New(enc), diags } log.Printf("[TRACE] Meta.Backend: working directory was previously initialized for %q backend", s.Backend.Type) //backend init function if s.Backend.Type == "" { - return backendLocal.New(encryption.StateEncryptionTODO()), diags + return backendLocal.New(enc), diags } f := backendInit.Backend(s.Backend.Type) if f == nil { diags = diags.Append(fmt.Errorf(strings.TrimSpace(errBackendSavedUnknown), s.Backend.Type)) return nil, diags } - b := f(encryption.StateEncryptionTODO()) + b := f(enc) // The configuration saved in the working directory state file is used // in this case, since it will contain any additional values that @@ -872,7 +873,7 @@ func (m *Meta) backendFromState(ctx context.Context) (backend.Backend, tfdiags.D // Unconfiguring a backend (moving from backend => local). func (m *Meta) backend_c_r_S( - c *configs.Backend, cHash int, sMgr *clistate.LocalState, output bool, opts *BackendOpts) (backend.Backend, tfdiags.Diagnostics) { + c *configs.Backend, cHash int, sMgr *clistate.LocalState, output bool, opts *BackendOpts, enc encryption.StateEncryption) (backend.Backend, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics @@ -901,14 +902,14 @@ func (m *Meta) backend_c_r_S( } // Grab a purely local backend to get the local state if it exists - localB, moreDiags := m.Backend(&BackendOpts{ForceLocal: true, Init: true}) + localB, moreDiags := m.Backend(&BackendOpts{ForceLocal: true, Init: true}, enc) diags = diags.Append(moreDiags) if moreDiags.HasErrors() { return nil, diags } // Initialize the configured backend - b, moreDiags := m.savedBackend(sMgr) + b, moreDiags := m.savedBackend(sMgr, enc) diags = diags.Append(moreDiags) if moreDiags.HasErrors() { return nil, diags @@ -949,7 +950,7 @@ func (m *Meta) backend_c_r_S( } // Configuring a backend for the first time. -func (m *Meta) backend_C_r_s(c *configs.Backend, cHash int, sMgr *clistate.LocalState, opts *BackendOpts) (backend.Backend, tfdiags.Diagnostics) { +func (m *Meta) backend_C_r_s(c *configs.Backend, cHash int, sMgr *clistate.LocalState, opts *BackendOpts, enc encryption.StateEncryption) (backend.Backend, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics vt := arguments.ViewJSON @@ -960,7 +961,7 @@ func (m *Meta) backend_C_r_s(c *configs.Backend, cHash int, sMgr *clistate.Local } // Grab a purely local backend to get the local state if it exists - localB, localBDiags := m.Backend(&BackendOpts{ForceLocal: true, Init: true}) + localB, localBDiags := m.Backend(&BackendOpts{ForceLocal: true, Init: true}, enc) if localBDiags.HasErrors() { diags = diags.Append(localBDiags) return nil, diags @@ -1000,7 +1001,7 @@ func (m *Meta) backend_C_r_s(c *configs.Backend, cHash int, sMgr *clistate.Local } // Get the backend - b, configVal, moreDiags := m.backendInitFromConfig(c) + b, configVal, moreDiags := m.backendInitFromConfig(c, enc) diags = diags.Append(moreDiags) if diags.HasErrors() { return nil, diags @@ -1118,7 +1119,7 @@ func (m *Meta) backend_C_r_s(c *configs.Backend, cHash int, sMgr *clistate.Local } // Changing a previously saved backend. -func (m *Meta) backend_C_r_S_changed(c *configs.Backend, cHash int, sMgr *clistate.LocalState, output bool, opts *BackendOpts) (backend.Backend, tfdiags.Diagnostics) { +func (m *Meta) backend_C_r_S_changed(c *configs.Backend, cHash int, sMgr *clistate.LocalState, output bool, opts *BackendOpts, enc encryption.StateEncryption) (backend.Backend, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics vt := arguments.ViewJSON @@ -1161,7 +1162,7 @@ func (m *Meta) backend_C_r_S_changed(c *configs.Backend, cHash int, sMgr *clista } // Get the backend - b, configVal, moreDiags := m.backendInitFromConfig(c) + b, configVal, moreDiags := m.backendInitFromConfig(c, enc) diags = diags.Append(moreDiags) if moreDiags.HasErrors() { return nil, diags @@ -1175,7 +1176,7 @@ func (m *Meta) backend_C_r_S_changed(c *configs.Backend, cHash int, sMgr *clista // state lives. if cloudMode != cloud.ConfigChangeInPlace { // Grab the existing backend - oldB, oldBDiags := m.savedBackend(sMgr) + oldB, oldBDiags := m.savedBackend(sMgr, enc) diags = diags.Append(oldBDiags) if oldBDiags.HasErrors() { return nil, diags @@ -1256,7 +1257,7 @@ func (m *Meta) backend_C_r_S_changed(c *configs.Backend, cHash int, sMgr *clista // TODO: This is extremely similar to Meta.backendFromState() but for legacy reasons this is the // function used by the migration APIs within this file. The other handles 'init -backend=false', // specifically. -func (m *Meta) savedBackend(sMgr *clistate.LocalState) (backend.Backend, tfdiags.Diagnostics) { +func (m *Meta) savedBackend(sMgr *clistate.LocalState, enc encryption.StateEncryption) (backend.Backend, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics s := sMgr.State() @@ -1267,7 +1268,7 @@ func (m *Meta) savedBackend(sMgr *clistate.LocalState) (backend.Backend, tfdiags diags = diags.Append(fmt.Errorf(strings.TrimSpace(errBackendSavedUnknown), s.Backend.Type)) return nil, diags } - b := f(encryption.StateEncryptionTODO()) + b := f(enc) // The configuration saved in the working directory state file is used // in this case, since it will contain any additional values that @@ -1355,7 +1356,7 @@ func (m *Meta) backendConfigNeedsMigration(c *configs.Backend, s *legacy.Backend 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 } - b := f(encryption.StateEncryptionTODO()) + b := f(nil) // We don't need encryption here as it's only used for config/schema schema := b.ConfigSchema() decSpec := schema.NoneRequired().DecoderSpec() @@ -1383,7 +1384,7 @@ func (m *Meta) backendConfigNeedsMigration(c *configs.Backend, s *legacy.Backend return true } -func (m *Meta) backendInitFromConfig(c *configs.Backend) (backend.Backend, cty.Value, tfdiags.Diagnostics) { +func (m *Meta) backendInitFromConfig(c *configs.Backend, enc encryption.StateEncryption) (backend.Backend, cty.Value, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics // Get the backend @@ -1392,7 +1393,7 @@ func (m *Meta) backendInitFromConfig(c *configs.Backend) (backend.Backend, cty.V diags = diags.Append(fmt.Errorf(strings.TrimSpace(errBackendNewUnknown), c.Type)) return nil, cty.NilVal, diags } - b := f(encryption.StateEncryptionTODO()) + b := f(enc) schema := b.ConfigSchema() decSpec := schema.NoneRequired().DecoderSpec() diff --git a/internal/command/meta_backend_test.go b/internal/command/meta_backend_test.go index 5277b7c256..df0125e7ed 100644 --- a/internal/command/meta_backend_test.go +++ b/internal/command/meta_backend_test.go @@ -41,7 +41,7 @@ func TestMetaBackend_emptyDir(t *testing.T) { // Get the backend m := testMetaBackend(t, nil) - b, diags := m.Backend(&BackendOpts{Init: true}) + b, diags := m.Backend(&BackendOpts{Init: true}, encryption.StateEncryptionDisabled()) if diags.HasErrors() { t.Fatal(diags.Err()) } @@ -110,7 +110,7 @@ func TestMetaBackend_emptyWithDefaultState(t *testing.T) { // Get the backend m := testMetaBackend(t, nil) - b, diags := m.Backend(&BackendOpts{Init: true}) + b, diags := m.Backend(&BackendOpts{Init: true}, encryption.StateEncryptionDisabled()) if diags.HasErrors() { t.Fatal(diags.Err()) } @@ -181,7 +181,7 @@ func TestMetaBackend_emptyWithExplicitState(t *testing.T) { m.statePath = statePath // Get the backend - b, diags := m.Backend(&BackendOpts{Init: true}) + b, diags := m.Backend(&BackendOpts{Init: true}, encryption.StateEncryptionDisabled()) if diags.HasErrors() { t.Fatal(diags.Err()) } @@ -233,7 +233,7 @@ func TestMetaBackend_configureInterpolation(t *testing.T) { m := testMetaBackend(t, nil) // Get the backend - _, err := m.Backend(&BackendOpts{Init: true}) + _, err := m.Backend(&BackendOpts{Init: true}, encryption.StateEncryptionDisabled()) if err == nil { t.Fatal("should error") } @@ -249,7 +249,7 @@ func TestMetaBackend_configureNew(t *testing.T) { m := testMetaBackend(t, nil) // Get the backend - b, diags := m.Backend(&BackendOpts{Init: true}) + b, diags := m.Backend(&BackendOpts{Init: true}, encryption.StateEncryptionDisabled()) if diags.HasErrors() { t.Fatal(diags.Err()) } @@ -320,7 +320,7 @@ func TestMetaBackend_configureNewWithState(t *testing.T) { m.migrateState = false // Get the backend - b, diags := m.Backend(&BackendOpts{Init: true}) + b, diags := m.Backend(&BackendOpts{Init: true}, encryption.StateEncryptionDisabled()) if diags.HasErrors() { t.Fatal(diags.Err()) } @@ -395,7 +395,7 @@ func TestMetaBackend_configureNewWithoutCopy(t *testing.T) { m.input = false // init the backend - _, diags := m.Backend(&BackendOpts{Init: true}) + _, diags := m.Backend(&BackendOpts{Init: true}, encryption.StateEncryptionDisabled()) if diags.HasErrors() { t.Fatal(diags.Err()) } @@ -443,7 +443,7 @@ func TestMetaBackend_configureNewWithStateNoMigrate(t *testing.T) { m := testMetaBackend(t, nil) // Get the backend - b, diags := m.Backend(&BackendOpts{Init: true}) + b, diags := m.Backend(&BackendOpts{Init: true}, encryption.StateEncryptionDisabled()) if diags.HasErrors() { t.Fatal(diags.Err()) } @@ -486,7 +486,7 @@ func TestMetaBackend_configureNewWithStateExisting(t *testing.T) { m.forceInitCopy = true // Get the backend - b, diags := m.Backend(&BackendOpts{Init: true}) + b, diags := m.Backend(&BackendOpts{Init: true}, encryption.StateEncryptionDisabled()) if diags.HasErrors() { t.Fatal(diags.Err()) } @@ -558,7 +558,7 @@ func TestMetaBackend_configureNewWithStateExistingNoMigrate(t *testing.T) { m := testMetaBackend(t, nil) // Get the backend - b, diags := m.Backend(&BackendOpts{Init: true}) + b, diags := m.Backend(&BackendOpts{Init: true}, encryption.StateEncryptionDisabled()) if diags.HasErrors() { t.Fatal(diags.Err()) } @@ -623,7 +623,7 @@ func TestMetaBackend_configuredUnchanged(t *testing.T) { m := testMetaBackend(t, nil) // Get the backend - b, diags := m.Backend(&BackendOpts{Init: true}) + b, diags := m.Backend(&BackendOpts{Init: true}, encryption.StateEncryptionDisabled()) if diags.HasErrors() { t.Fatal(diags.Err()) } @@ -669,7 +669,7 @@ func TestMetaBackend_configuredChange(t *testing.T) { m := testMetaBackend(t, nil) // Get the backend - b, diags := m.Backend(&BackendOpts{Init: true}) + b, diags := m.Backend(&BackendOpts{Init: true}, encryption.StateEncryptionDisabled()) if diags.HasErrors() { t.Fatal(diags.Err()) } @@ -755,7 +755,7 @@ func TestMetaBackend_reconfigureChange(t *testing.T) { m.reconfigure = true // Get the backend - b, diags := m.Backend(&BackendOpts{Init: true}) + b, diags := m.Backend(&BackendOpts{Init: true}, encryption.StateEncryptionDisabled()) if diags.HasErrors() { t.Fatal(diags.Err()) } @@ -802,7 +802,7 @@ func TestMetaBackend_initSelectedWorkspaceDoesNotExist(t *testing.T) { })() // Get the backend - _, diags := m.Backend(&BackendOpts{Init: true}) + _, diags := m.Backend(&BackendOpts{Init: true}, encryption.StateEncryptionDisabled()) if diags.HasErrors() { t.Fatal(diags.Err()) } @@ -844,7 +844,7 @@ func TestMetaBackend_initSelectedWorkspaceDoesNotExistAutoSelect(t *testing.T) { } // Get the backend - _, diags := m.Backend(&BackendOpts{Init: true}) + _, diags := m.Backend(&BackendOpts{Init: true}, encryption.StateEncryptionDisabled()) if diags.HasErrors() { t.Fatal(diags.Err()) } @@ -873,7 +873,7 @@ func TestMetaBackend_initSelectedWorkspaceDoesNotExistInputFalse(t *testing.T) { m.input = false // Get the backend - _, diags := m.Backend(&BackendOpts{Init: true}) + _, diags := m.Backend(&BackendOpts{Init: true}, encryption.StateEncryptionDisabled()) // Should fail immediately if got, want := diags.ErrWithWarnings().Error(), `Currently selected workspace "bar" does not exist`; !strings.Contains(got, want) { @@ -895,7 +895,7 @@ func TestMetaBackend_configuredChangeCopy(t *testing.T) { m := testMetaBackend(t, nil) // Get the backend - b, diags := m.Backend(&BackendOpts{Init: true}) + b, diags := m.Backend(&BackendOpts{Init: true}, encryption.StateEncryptionDisabled()) if diags.HasErrors() { t.Fatal(diags.Err()) } @@ -948,7 +948,7 @@ func TestMetaBackend_configuredChangeCopy_singleState(t *testing.T) { m := testMetaBackend(t, nil) // Get the backend - b, diags := m.Backend(&BackendOpts{Init: true}) + b, diags := m.Backend(&BackendOpts{Init: true}, encryption.StateEncryptionDisabled()) if diags.HasErrors() { t.Fatal(diags.Err()) } @@ -1002,7 +1002,7 @@ func TestMetaBackend_configuredChangeCopy_multiToSingleDefault(t *testing.T) { m := testMetaBackend(t, nil) // Get the backend - b, diags := m.Backend(&BackendOpts{Init: true}) + b, diags := m.Backend(&BackendOpts{Init: true}, encryption.StateEncryptionDisabled()) if diags.HasErrors() { t.Fatal(diags.Err()) } @@ -1056,7 +1056,7 @@ func TestMetaBackend_configuredChangeCopy_multiToSingle(t *testing.T) { m := testMetaBackend(t, nil) // Get the backend - b, diags := m.Backend(&BackendOpts{Init: true}) + b, diags := m.Backend(&BackendOpts{Init: true}, encryption.StateEncryptionDisabled()) if diags.HasErrors() { t.Fatal(diags.Err()) } @@ -1130,7 +1130,7 @@ func TestMetaBackend_configuredChangeCopy_multiToSingleCurrentEnv(t *testing.T) } // Get the backend - b, diags := m.Backend(&BackendOpts{Init: true}) + b, diags := m.Backend(&BackendOpts{Init: true}, encryption.StateEncryptionDisabled()) if diags.HasErrors() { t.Fatal(diags.Err()) } @@ -1185,7 +1185,7 @@ func TestMetaBackend_configuredChangeCopy_multiToMulti(t *testing.T) { m := testMetaBackend(t, nil) // Get the backend - b, diags := m.Backend(&BackendOpts{Init: true}) + b, diags := m.Backend(&BackendOpts{Init: true}, encryption.StateEncryptionDisabled()) if diags.HasErrors() { t.Fatal(diags.Err()) } @@ -1283,7 +1283,7 @@ func TestMetaBackend_configuredChangeCopy_multiToNoDefaultWithDefault(t *testing m := testMetaBackend(t, nil) // Get the backend - b, diags := m.Backend(&BackendOpts{Init: true}) + b, diags := m.Backend(&BackendOpts{Init: true}, encryption.StateEncryptionDisabled()) if diags.HasErrors() { t.Fatal(diags.Err()) } @@ -1357,7 +1357,7 @@ func TestMetaBackend_configuredChangeCopy_multiToNoDefaultWithoutDefault(t *test m := testMetaBackend(t, nil) // Get the backend - b, diags := m.Backend(&BackendOpts{Init: true}) + b, diags := m.Backend(&BackendOpts{Init: true}, encryption.StateEncryptionDisabled()) if diags.HasErrors() { t.Fatal(diags.Err()) } @@ -1423,7 +1423,7 @@ func TestMetaBackend_configuredUnset(t *testing.T) { m := testMetaBackend(t, nil) // Get the backend - b, diags := m.Backend(&BackendOpts{Init: true}) + b, diags := m.Backend(&BackendOpts{Init: true}, encryption.StateEncryptionDisabled()) if diags.HasErrors() { t.Fatal(diags.Err()) } @@ -1485,7 +1485,7 @@ func TestMetaBackend_configuredUnsetCopy(t *testing.T) { m := testMetaBackend(t, nil) // Get the backend - b, diags := m.Backend(&BackendOpts{Init: true}) + b, diags := m.Backend(&BackendOpts{Init: true}, encryption.StateEncryptionDisabled()) if diags.HasErrors() { t.Fatal(diags.Err()) } @@ -1553,7 +1553,7 @@ func TestMetaBackend_planLocal(t *testing.T) { m := testMetaBackend(t, nil) // Get the backend - b, diags := m.BackendForLocalPlan(backendConfig) + b, diags := m.BackendForLocalPlan(backendConfig, encryption.StateEncryptionDisabled()) if diags.HasErrors() { t.Fatal(diags.Err()) } @@ -1654,7 +1654,7 @@ func TestMetaBackend_planLocalStatePath(t *testing.T) { m.stateOutPath = statePath // Get the backend - b, diags := m.BackendForLocalPlan(plannedBackend) + b, diags := m.BackendForLocalPlan(plannedBackend, encryption.StateEncryptionDisabled()) if diags.HasErrors() { t.Fatal(diags.Err()) } @@ -1743,7 +1743,7 @@ func TestMetaBackend_planLocalMatch(t *testing.T) { m := testMetaBackend(t, nil) // Get the backend - b, diags := m.BackendForLocalPlan(backendConfig) + b, diags := m.BackendForLocalPlan(backendConfig, encryption.StateEncryptionDisabled()) if diags.HasErrors() { t.Fatal(diags.Err()) } @@ -1828,7 +1828,7 @@ func TestMetaBackend_configureWithExtra(t *testing.T) { _, diags := m.Backend(&BackendOpts{ ConfigOverride: configs.SynthBody("synth", extras), Init: true, - }) + }, encryption.StateEncryptionDisabled()) if diags.HasErrors() { t.Fatal(diags.Err()) } @@ -1844,7 +1844,7 @@ func TestMetaBackend_configureWithExtra(t *testing.T) { _, err = m.Backend(&BackendOpts{ ConfigOverride: configs.SynthBody("synth", extras), Init: true, - }) + }, encryption.StateEncryptionDisabled()) if err != nil { t.Fatalf("unexpected error: %s", err) } @@ -1871,7 +1871,7 @@ func TestMetaBackend_localDoesNotDeleteLocal(t *testing.T) { m := testMetaBackend(t, nil) m.forceInitCopy = true // init the backend - _, diags := m.Backend(&BackendOpts{Init: true}) + _, diags := m.Backend(&BackendOpts{Init: true}, encryption.StateEncryptionDisabled()) if diags.HasErrors() { t.Fatal(diags.Err()) } @@ -1894,7 +1894,7 @@ func TestMetaBackend_configToExtra(t *testing.T) { m := testMetaBackend(t, nil) _, err := m.Backend(&BackendOpts{ Init: true, - }) + }, encryption.StateEncryptionDisabled()) if err != nil { t.Fatalf("unexpected error: %s", err) } @@ -1916,7 +1916,7 @@ func TestMetaBackend_configToExtra(t *testing.T) { _, diags := m.Backend(&BackendOpts{ ConfigOverride: configs.SynthBody("synth", extras), Init: true, - }) + }, encryption.StateEncryptionDisabled()) if diags.HasErrors() { t.Fatal(diags.Err()) } @@ -1943,7 +1943,7 @@ func TestBackendFromState(t *testing.T) { // them to match just for this test. wd.OverrideDataDir(".") - stateBackend, diags := m.backendFromState(context.Background()) + stateBackend, diags := m.backendFromState(context.Background(), encryption.StateEncryptionDisabled()) if diags.HasErrors() { t.Fatal(diags.Err()) } diff --git a/internal/command/meta_encryption.go b/internal/command/meta_encryption.go new file mode 100644 index 0000000000..0e08375c63 --- /dev/null +++ b/internal/command/meta_encryption.go @@ -0,0 +1,65 @@ +package command + +import ( + "fmt" + "os" + + "github.com/opentofu/opentofu/internal/configs" + "github.com/opentofu/opentofu/internal/encryption" + "github.com/opentofu/opentofu/internal/encryption/config" + "github.com/opentofu/opentofu/internal/encryption/keyprovider/static" + "github.com/opentofu/opentofu/internal/encryption/method/aesgcm" + "github.com/opentofu/opentofu/internal/encryption/registry/lockingencryptionregistry" + "github.com/opentofu/opentofu/internal/tfdiags" +) + +const encryptionConfigEnvName = "TF_ENCRYPTION" + +func (m *Meta) Encryption() (encryption.Encryption, tfdiags.Diagnostics) { + path, err := os.Getwd() + if err != nil { + return nil, tfdiags.Diagnostics{}.Append(fmt.Errorf("Error getting pwd: %w", err)) + } + + return m.EncryptionFromPath(path) +} + +func (m *Meta) EncryptionFromPath(path string) (encryption.Encryption, tfdiags.Diagnostics) { + // This is not ideal, but given how fragmented the command package is, loading the root module here is our best option + // See other meta commands like version check which do that same. + module, diags := m.loadSingleModule(path) + if diags.HasErrors() { + return nil, diags + } + enc, encDiags := m.EncryptionFromModule(module) + diags = diags.Append(encDiags) + return enc, diags +} + +func (m *Meta) EncryptionFromModule(module *configs.Module) (encryption.Encryption, tfdiags.Diagnostics) { + reg := lockingencryptionregistry.New() + if err := reg.RegisterKeyProvider(static.New()); err != nil { + panic(err) + } + if err := reg.RegisterMethod(aesgcm.New()); err != nil { + panic(err) + } + + cfg := module.Encryption + var diags tfdiags.Diagnostics + + env := os.Getenv(encryptionConfigEnvName) + if len(env) != 0 { + envCfg, envDiags := config.LoadConfigFromString(encryptionConfigEnvName, env) + diags = diags.Append(envDiags) + if envDiags.HasErrors() { + return nil, diags + } + cfg = cfg.Merge(envCfg) + } + + enc, encDiags := encryption.New(reg, cfg) + diags = diags.Append(encDiags) + + return enc, diags +} diff --git a/internal/command/meta_new.go b/internal/command/meta_new.go index 7c14e6611c..ea9a877c7a 100644 --- a/internal/command/meta_new.go +++ b/internal/command/meta_new.go @@ -38,7 +38,7 @@ func (m *Meta) Input() bool { // // Error will be non-nil if path refers to something which looks like a plan // file and loading the file fails. -func (m *Meta) PlanFile(path string) (*planfile.WrappedPlanFile, error) { +func (m *Meta) PlanFile(path string, enc encryption.PlanEncryption) (*planfile.WrappedPlanFile, error) { fi, err := os.Stat(path) if err != nil { return nil, err @@ -49,5 +49,5 @@ func (m *Meta) PlanFile(path string) (*planfile.WrappedPlanFile, error) { return nil, nil } - return planfile.OpenWrapped(path, encryption.PlanEncryptionTODO()) + return planfile.OpenWrapped(path, enc) } diff --git a/internal/command/output.go b/internal/command/output.go index b4c51bdac7..77addecf56 100644 --- a/internal/command/output.go +++ b/internal/command/output.go @@ -11,6 +11,7 @@ import ( "github.com/opentofu/opentofu/internal/command/arguments" "github.com/opentofu/opentofu/internal/command/views" + "github.com/opentofu/opentofu/internal/encryption" "github.com/opentofu/opentofu/internal/states" "github.com/opentofu/opentofu/internal/tfdiags" ) @@ -36,8 +37,16 @@ func (c *OutputCommand) Run(rawArgs []string) int { view := views.NewOutput(args.ViewType, c.View) + // Load the encryption configuration + enc, encDiags := c.Encryption() + diags = diags.Append(encDiags) + if encDiags.HasErrors() { + c.View.Diagnostics(diags) + return 1 + } + // Fetch data from state - outputs, diags := c.Outputs(args.StatePath) + outputs, diags := c.Outputs(args.StatePath, enc) if diags.HasErrors() { view.Diagnostics(diags) return 1 @@ -56,7 +65,7 @@ func (c *OutputCommand) Run(rawArgs []string) int { return 0 } -func (c *OutputCommand) Outputs(statePath string) (map[string]*states.OutputValue, tfdiags.Diagnostics) { +func (c *OutputCommand) Outputs(statePath string, enc encryption.Encryption) (map[string]*states.OutputValue, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics // Allow state path override @@ -65,7 +74,7 @@ func (c *OutputCommand) Outputs(statePath string) (map[string]*states.OutputValu } // Load the backend - b, backendDiags := c.Backend(nil) + b, backendDiags := c.Backend(nil, enc.Backend()) diags = diags.Append(backendDiags) if diags.HasErrors() { return nil, diags diff --git a/internal/command/plan.go b/internal/command/plan.go index 43d9df1dfc..6e830f3e97 100644 --- a/internal/command/plan.go +++ b/internal/command/plan.go @@ -12,6 +12,7 @@ import ( "github.com/opentofu/opentofu/internal/backend" "github.com/opentofu/opentofu/internal/command/arguments" "github.com/opentofu/opentofu/internal/command/views" + "github.com/opentofu/opentofu/internal/encryption" "github.com/opentofu/opentofu/internal/tfdiags" ) @@ -68,8 +69,16 @@ func (c *PlanCommand) Run(rawArgs []string) int { diags = diags.Append(c.providerDevOverrideRuntimeWarnings()) + // Load the encryption configuration + enc, encDiags := c.Encryption() + diags = diags.Append(encDiags) + if encDiags.HasErrors() { + view.Diagnostics(diags) + return 1 + } + // Prepare the backend with the backend-specific arguments - be, beDiags := c.PrepareBackend(args.State, args.ViewType) + be, beDiags := c.PrepareBackend(args.State, args.ViewType, enc) diags = diags.Append(beDiags) if diags.HasErrors() { view.Diagnostics(diags) @@ -77,7 +86,7 @@ func (c *PlanCommand) Run(rawArgs []string) int { } // Build the operation request - opReq, opDiags := c.OperationRequest(be, view, args.ViewType, args.Operation, args.OutPath, args.GenerateConfigPath) + opReq, opDiags := c.OperationRequest(be, view, args.ViewType, args.Operation, args.OutPath, args.GenerateConfigPath, enc) diags = diags.Append(opDiags) if diags.HasErrors() { view.Diagnostics(diags) @@ -115,7 +124,7 @@ func (c *PlanCommand) Run(rawArgs []string) int { return op.Result.ExitStatus() } -func (c *PlanCommand) PrepareBackend(args *arguments.State, viewType arguments.ViewType) (backend.Enhanced, tfdiags.Diagnostics) { +func (c *PlanCommand) PrepareBackend(args *arguments.State, viewType arguments.ViewType, enc encryption.Encryption) (backend.Enhanced, tfdiags.Diagnostics) { // FIXME: we need to apply the state arguments to the meta object here // because they are later used when initializing the backend. Carving a // path to pass these arguments to the functions that need them is @@ -131,7 +140,7 @@ func (c *PlanCommand) PrepareBackend(args *arguments.State, viewType arguments.V be, beDiags := c.Backend(&BackendOpts{ Config: backendConfig, ViewType: viewType, - }) + }, enc.Backend()) diags = diags.Append(beDiags) if beDiags.HasErrors() { return nil, diags @@ -147,11 +156,12 @@ func (c *PlanCommand) OperationRequest( args *arguments.Operation, planOutPath string, generateConfigOut string, + enc encryption.Encryption, ) (*backend.Operation, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics // Build the operation - opReq := c.Operation(be, viewType) + opReq := c.Operation(be, viewType, enc) opReq.ConfigDir = "." opReq.PlanMode = args.PlanMode opReq.Hooks = view.Hooks() diff --git a/internal/command/providers.go b/internal/command/providers.go index da1e3f9306..e3372b4005 100644 --- a/internal/command/providers.go +++ b/internal/command/providers.go @@ -82,10 +82,18 @@ func (c *ProvidersCommand) Run(args []string) int { return 1 } + // Load the encryption configuration + enc, encDiags := c.EncryptionFromPath(configPath) + diags = diags.Append(encDiags) + if encDiags.HasErrors() { + c.showDiagnostics(diags) + return 1 + } + // Load the backend b, backendDiags := c.Backend(&BackendOpts{ Config: config.Module.Backend, - }) + }, enc.Backend()) diags = diags.Append(backendDiags) if backendDiags.HasErrors() { c.showDiagnostics(diags) diff --git a/internal/command/providers_schema.go b/internal/command/providers_schema.go index 99a60a847e..9811f8d2e7 100644 --- a/internal/command/providers_schema.go +++ b/internal/command/providers_schema.go @@ -58,7 +58,7 @@ func (c *ProvidersSchemaCommand) Run(args []string) int { var diags tfdiags.Diagnostics // Load the backend - b, backendDiags := c.Backend(nil) + b, backendDiags := c.Backend(nil, nil) // Encryption not needed here diags = diags.Append(backendDiags) if backendDiags.HasErrors() { c.showDiagnostics(diags) @@ -84,7 +84,7 @@ func (c *ProvidersSchemaCommand) Run(args []string) int { } // Build the operation - opReq := c.Operation(b, arguments.ViewJSON) + opReq := c.Operation(b, arguments.ViewJSON, nil) // Encryption not needed here opReq.ConfigDir = cwd opReq.ConfigLoader, err = c.initConfigLoader() opReq.AllowUnsetVariables = true diff --git a/internal/command/refresh.go b/internal/command/refresh.go index 287579133c..f67b0f493f 100644 --- a/internal/command/refresh.go +++ b/internal/command/refresh.go @@ -12,6 +12,7 @@ import ( "github.com/opentofu/opentofu/internal/backend" "github.com/opentofu/opentofu/internal/command/arguments" "github.com/opentofu/opentofu/internal/command/views" + "github.com/opentofu/opentofu/internal/encryption" "github.com/opentofu/opentofu/internal/tfdiags" ) @@ -68,8 +69,16 @@ func (c *RefreshCommand) Run(rawArgs []string) int { // object state for now. c.Meta.parallelism = args.Operation.Parallelism + // Load the encryption configuration + enc, encDiags := c.Encryption() + diags = diags.Append(encDiags) + if encDiags.HasErrors() { + c.showDiagnostics(diags) + return 1 + } + // Prepare the backend with the backend-specific arguments - be, beDiags := c.PrepareBackend(args.State, args.ViewType) + be, beDiags := c.PrepareBackend(args.State, args.ViewType, enc) diags = diags.Append(beDiags) if diags.HasErrors() { view.Diagnostics(diags) @@ -77,7 +86,7 @@ func (c *RefreshCommand) Run(rawArgs []string) int { } // Build the operation request - opReq, opDiags := c.OperationRequest(be, view, args.ViewType, args.Operation) + opReq, opDiags := c.OperationRequest(be, view, args.ViewType, args.Operation, enc) diags = diags.Append(opDiags) if diags.HasErrors() { view.Diagnostics(diags) @@ -112,7 +121,7 @@ func (c *RefreshCommand) Run(rawArgs []string) int { return op.Result.ExitStatus() } -func (c *RefreshCommand) PrepareBackend(args *arguments.State, viewType arguments.ViewType) (backend.Enhanced, tfdiags.Diagnostics) { +func (c *RefreshCommand) PrepareBackend(args *arguments.State, viewType arguments.ViewType, enc encryption.Encryption) (backend.Enhanced, tfdiags.Diagnostics) { // FIXME: we need to apply the state arguments to the meta object here // because they are later used when initializing the backend. Carving a // path to pass these arguments to the functions that need them is @@ -128,7 +137,7 @@ func (c *RefreshCommand) PrepareBackend(args *arguments.State, viewType argument be, beDiags := c.Backend(&BackendOpts{ Config: backendConfig, ViewType: viewType, - }) + }, enc.Backend()) diags = diags.Append(beDiags) if beDiags.HasErrors() { return nil, diags @@ -137,12 +146,12 @@ func (c *RefreshCommand) PrepareBackend(args *arguments.State, viewType argument return be, diags } -func (c *RefreshCommand) OperationRequest(be backend.Enhanced, view views.Refresh, viewType arguments.ViewType, args *arguments.Operation, +func (c *RefreshCommand) OperationRequest(be backend.Enhanced, view views.Refresh, viewType arguments.ViewType, args *arguments.Operation, enc encryption.Encryption, ) (*backend.Operation, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics // Build the operation - opReq := c.Operation(be, viewType) + opReq := c.Operation(be, viewType, enc) opReq.ConfigDir = "." opReq.Hooks = view.Hooks() opReq.Targets = args.Targets diff --git a/internal/command/show.go b/internal/command/show.go index 7e981a3dd2..553c412602 100644 --- a/internal/command/show.go +++ b/internal/command/show.go @@ -78,8 +78,16 @@ func (c *ShowCommand) Run(rawArgs []string) int { return 1 } + // Load the encryption configuration + enc, encDiags := c.Encryption() + diags = diags.Append(encDiags) + if encDiags.HasErrors() { + c.showDiagnostics(diags) + return 1 + } + // Get the data we need to display - plan, jsonPlan, stateFile, config, schemas, showDiags := c.show(args.Path) + plan, jsonPlan, stateFile, config, schemas, showDiags := c.show(args.Path, enc) diags = diags.Append(showDiags) if showDiags.HasErrors() { view.Diagnostics(diags) @@ -111,7 +119,7 @@ func (c *ShowCommand) Synopsis() string { return "Show the current state or a saved plan" } -func (c *ShowCommand) show(path string) (*plans.Plan, *cloudplan.RemotePlanJSON, *statefile.File, *configs.Config, *tofu.Schemas, tfdiags.Diagnostics) { +func (c *ShowCommand) show(path string, enc encryption.Encryption) (*plans.Plan, *cloudplan.RemotePlanJSON, *statefile.File, *configs.Config, *tofu.Schemas, tfdiags.Diagnostics) { var diags, showDiags, migrateDiags tfdiags.Diagnostics var plan *plans.Plan var jsonPlan *cloudplan.RemotePlanJSON @@ -122,7 +130,7 @@ func (c *ShowCommand) show(path string) (*plans.Plan, *cloudplan.RemotePlanJSON, // No plan file or state file argument provided, // so get the latest state snapshot if path == "" { - stateFile, showDiags = c.showFromLatestStateSnapshot() + stateFile, showDiags = c.showFromLatestStateSnapshot(enc) diags = diags.Append(showDiags) if showDiags.HasErrors() { return plan, jsonPlan, stateFile, config, schemas, diags @@ -133,7 +141,7 @@ func (c *ShowCommand) show(path string) (*plans.Plan, *cloudplan.RemotePlanJSON, // so try to load the argument as a plan file first. // If that fails, try to load it as a statefile. if path != "" { - plan, jsonPlan, stateFile, config, showDiags = c.showFromPath(path) + plan, jsonPlan, stateFile, config, showDiags = c.showFromPath(path, enc) diags = diags.Append(showDiags) if showDiags.HasErrors() { return plan, jsonPlan, stateFile, config, schemas, diags @@ -158,11 +166,11 @@ func (c *ShowCommand) show(path string) (*plans.Plan, *cloudplan.RemotePlanJSON, return plan, jsonPlan, stateFile, config, schemas, diags } -func (c *ShowCommand) showFromLatestStateSnapshot() (*statefile.File, tfdiags.Diagnostics) { +func (c *ShowCommand) showFromLatestStateSnapshot(enc encryption.Encryption) (*statefile.File, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics // Load the backend - b, backendDiags := c.Backend(nil) + b, backendDiags := c.Backend(nil, enc.Backend()) diags = diags.Append(backendDiags) if backendDiags.HasErrors() { return nil, diags @@ -186,7 +194,7 @@ func (c *ShowCommand) showFromLatestStateSnapshot() (*statefile.File, tfdiags.Di return stateFile, diags } -func (c *ShowCommand) showFromPath(path string) (*plans.Plan, *cloudplan.RemotePlanJSON, *statefile.File, *configs.Config, tfdiags.Diagnostics) { +func (c *ShowCommand) showFromPath(path string, enc encryption.Encryption) (*plans.Plan, *cloudplan.RemotePlanJSON, *statefile.File, *configs.Config, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics var planErr, stateErr error var plan *plans.Plan @@ -198,9 +206,9 @@ func (c *ShowCommand) showFromPath(path string) (*plans.Plan, *cloudplan.RemoteP // state file. First, try to get a plan and associated data from a local // plan file. If that fails, try to get a json plan from the path argument. // If that fails, try to get the statefile from the path argument. - plan, jsonPlan, stateFile, config, planErr = c.getPlanFromPath(path) + plan, jsonPlan, stateFile, config, planErr = c.getPlanFromPath(path, enc) if planErr != nil { - stateFile, stateErr = getStateFromPath(path) + stateFile, stateErr = getStateFromPath(path, enc) if stateErr != nil { // To avoid spamming the user with irrelevant errors, first check to // see if one of our errors happens to know for a fact what file @@ -266,14 +274,14 @@ func (c *ShowCommand) showFromPath(path string) (*plans.Plan, *cloudplan.RemoteP // yield a json plan, and cloud plans do not yield real plan/state/config // structs. An error generally suggests that the given path is either a // directory or a statefile. -func (c *ShowCommand) getPlanFromPath(path string) (*plans.Plan, *cloudplan.RemotePlanJSON, *statefile.File, *configs.Config, error) { +func (c *ShowCommand) getPlanFromPath(path string, enc encryption.Encryption) (*plans.Plan, *cloudplan.RemotePlanJSON, *statefile.File, *configs.Config, error) { var err error var plan *plans.Plan var jsonPlan *cloudplan.RemotePlanJSON var stateFile *statefile.File var config *configs.Config - pf, err := planfile.OpenWrapped(path, encryption.PlanEncryptionTODO()) + pf, err := planfile.OpenWrapped(path, enc.PlanFile()) if err != nil { return nil, nil, nil, nil, err } @@ -282,15 +290,15 @@ func (c *ShowCommand) getPlanFromPath(path string) (*plans.Plan, *cloudplan.Remo plan, stateFile, config, err = getDataFromPlanfileReader(lp) } else if cp, ok := pf.Cloud(); ok { redacted := c.viewType != arguments.ViewJSON - jsonPlan, err = c.getDataFromCloudPlan(cp, redacted) + jsonPlan, err = c.getDataFromCloudPlan(cp, redacted, enc) } return plan, jsonPlan, stateFile, config, err } -func (c *ShowCommand) getDataFromCloudPlan(plan *cloudplan.SavedPlanBookmark, redacted bool) (*cloudplan.RemotePlanJSON, error) { +func (c *ShowCommand) getDataFromCloudPlan(plan *cloudplan.SavedPlanBookmark, redacted bool, enc encryption.Encryption) (*cloudplan.RemotePlanJSON, error) { // Set up the backend - b, backendDiags := c.Backend(nil) + b, backendDiags := c.Backend(nil, enc.Backend()) if backendDiags.HasErrors() { return nil, errUnusable(backendDiags.Err(), "cloud plan") } @@ -331,7 +339,7 @@ func getDataFromPlanfileReader(planReader *planfile.Reader) (*plans.Plan, *state } // getStateFromPath returns a statefile if the user-supplied path points to a statefile. -func getStateFromPath(path string) (*statefile.File, error) { +func getStateFromPath(path string, enc encryption.Encryption) (*statefile.File, error) { file, err := os.Open(path) if err != nil { return nil, fmt.Errorf("Error loading statefile: %w", err) @@ -339,7 +347,7 @@ func getStateFromPath(path string) (*statefile.File, error) { defer file.Close() var stateFile *statefile.File - stateFile, err = statefile.Read(file, encryption.StateEncryptionTODO()) // Should we use encryption -> statefile config here? + stateFile, err = statefile.Read(file, enc.StateFile()) if err != nil { return nil, fmt.Errorf("Error reading %s as a statefile: %w", path, err) } diff --git a/internal/command/state_list.go b/internal/command/state_list.go index 618f27d188..457e71f6fa 100644 --- a/internal/command/state_list.go +++ b/internal/command/state_list.go @@ -39,8 +39,15 @@ func (c *StateListCommand) Run(args []string) int { c.Meta.statePath = statePath } + // Load the encryption configuration + enc, encDiags := c.Encryption() + if encDiags.HasErrors() { + c.showDiagnostics(encDiags) + return 1 + } + // Load the backend - b, backendDiags := c.Backend(nil) + b, backendDiags := c.Backend(nil, enc.Backend()) if backendDiags.HasErrors() { c.showDiagnostics(backendDiags) return 1 diff --git a/internal/command/state_meta.go b/internal/command/state_meta.go index c2fcddbb79..b87ad7223e 100644 --- a/internal/command/state_meta.go +++ b/internal/command/state_meta.go @@ -28,17 +28,17 @@ type StateMeta struct { // the backend, but changes the way that backups are done. This configures // backups to be timestamped rather than just the original state path plus a // backup path. -func (c *StateMeta) State() (statemgr.Full, error) { +func (c *StateMeta) State(enc encryption.Encryption) (statemgr.Full, error) { var realState statemgr.Full backupPath := c.backupPath stateOutPath := c.statePath // use the specified state if c.statePath != "" { - realState = statemgr.NewFilesystem(c.statePath, encryption.StateEncryptionTODO()) + realState = statemgr.NewFilesystem(c.statePath, encryption.StateEncryptionDisabled()) // User specified state file should not be encrypted } else { // Load the backend - b, backendDiags := c.Backend(nil) + b, backendDiags := c.Backend(nil, enc.Backend()) if backendDiags.HasErrors() { return nil, backendDiags.Err() } @@ -62,7 +62,7 @@ func (c *StateMeta) State() (statemgr.Full, error) { } // Get a local backend - localRaw, backendDiags := c.Backend(&BackendOpts{ForceLocal: true}) + localRaw, backendDiags := c.Backend(&BackendOpts{ForceLocal: true}, enc.Backend()) if backendDiags.HasErrors() { // This should never fail panic(backendDiags.Err()) diff --git a/internal/command/state_mv.go b/internal/command/state_mv.go index b6c83de0e7..30f9611405 100644 --- a/internal/command/state_mv.go +++ b/internal/command/state_mv.go @@ -69,8 +69,15 @@ func (c *StateMvCommand) Run(args []string) int { setLegacyLocalBackendOptions = append(setLegacyLocalBackendOptions, "-backup-out") } + // Load the encryption configuration + enc, encDiags := c.Encryption() + if encDiags.HasErrors() { + c.showDiagnostics(encDiags) + return 1 + } + if len(setLegacyLocalBackendOptions) > 0 { - currentBackend, diags := c.backendFromConfig(&BackendOpts{}) + currentBackend, diags := c.backendFromConfig(&BackendOpts{}, enc.Backend()) if diags.HasErrors() { c.showDiagnostics(diags) return 1 @@ -93,7 +100,7 @@ func (c *StateMvCommand) Run(args []string) int { } // Read the from state - stateFromMgr, err := c.State() + stateFromMgr, err := c.State(enc) if err != nil { c.Ui.Error(fmt.Sprintf(errStateLoadingState, err)) return 1 @@ -131,7 +138,7 @@ func (c *StateMvCommand) Run(args []string) int { c.statePath = statePathOut c.backupPath = backupPathOut - stateToMgr, err = c.State() + stateToMgr, err = c.State(enc) if err != nil { c.Ui.Error(fmt.Sprintf(errStateLoadingState, err)) return 1 @@ -392,7 +399,7 @@ func (c *StateMvCommand) Run(args []string) int { return 0 // This is as far as we go in dry-run mode } - b, backendDiags := c.Backend(nil) + b, backendDiags := c.Backend(nil, enc.Backend()) diags = diags.Append(backendDiags) if backendDiags.HasErrors() { c.showDiagnostics(diags) diff --git a/internal/command/state_pull.go b/internal/command/state_pull.go index eece6e18e9..e132b4bc9c 100644 --- a/internal/command/state_pull.go +++ b/internal/command/state_pull.go @@ -34,8 +34,15 @@ func (c *StatePullCommand) Run(args []string) int { return 1 } + // Load the encryption configuration + enc, encDiags := c.Encryption() + if encDiags.HasErrors() { + c.showDiagnostics(encDiags) + return 1 + } + // Load the backend - b, backendDiags := c.Backend(nil) + b, backendDiags := c.Backend(nil, enc.Backend()) if backendDiags.HasErrors() { c.showDiagnostics(backendDiags) return 1 @@ -65,7 +72,7 @@ func (c *StatePullCommand) Run(args []string) int { if stateFile != nil { // we produce no output if the statefile is nil var buf bytes.Buffer - err = statefile.Write(stateFile, &buf, encryption.StateEncryptionTODO()) + err = statefile.Write(stateFile, &buf, encryption.StateEncryptionDisabled()) // Don't encrypt to stdout if err != nil { c.Ui.Error(fmt.Sprintf("Failed to write state: %s", err)) return 1 diff --git a/internal/command/state_push.go b/internal/command/state_push.go index 271683d6cb..b86f193f02 100644 --- a/internal/command/state_push.go +++ b/internal/command/state_push.go @@ -52,6 +52,13 @@ func (c *StatePushCommand) Run(args []string) int { return 1 } + // Load the encryption configuration + enc, encDiags := c.Encryption() + if encDiags.HasErrors() { + c.showDiagnostics(encDiags) + return 1 + } + // Determine our reader for the input state. This is the filepath // or stdin if "-" is given. var r io.Reader = os.Stdin @@ -69,7 +76,7 @@ func (c *StatePushCommand) Run(args []string) int { } // Read the state - srcStateFile, err := statefile.Read(r, encryption.StateEncryptionTODO()) // Should we use encryption -> statefile config here? + srcStateFile, err := statefile.Read(r, encryption.StateEncryptionDisabled()) // Assume the given statefile is not encrypted if c, ok := r.(io.Closer); ok { // Close the reader if possible right now since we're done with it. c.Close() @@ -80,7 +87,7 @@ func (c *StatePushCommand) Run(args []string) int { } // Load the backend - b, backendDiags := c.Backend(nil) + b, backendDiags := c.Backend(nil, enc.Backend()) if backendDiags.HasErrors() { c.showDiagnostics(backendDiags) return 1 diff --git a/internal/command/state_replace_provider.go b/internal/command/state_replace_provider.go index 777e240432..742336dfb3 100644 --- a/internal/command/state_replace_provider.go +++ b/internal/command/state_replace_provider.go @@ -78,8 +78,16 @@ func (c *StateReplaceProviderCommand) Run(args []string) int { return 1 } + // Load the encryption configuration + enc, encDiags := c.Encryption() + diags = diags.Append(encDiags) + if encDiags.HasErrors() { + c.showDiagnostics(diags) + return 1 + } + // Initialize the state manager as configured - stateMgr, err := c.State() + stateMgr, err := c.State(enc) if err != nil { c.Ui.Error(fmt.Sprintf(errStateLoadingState, err)) return 1 @@ -167,7 +175,7 @@ func (c *StateReplaceProviderCommand) Run(args []string) int { resource.ProviderConfig.Provider = to } - b, backendDiags := c.Backend(nil) + b, backendDiags := c.Backend(nil, enc.Backend()) diags = diags.Append(backendDiags) if backendDiags.HasErrors() { c.showDiagnostics(diags) diff --git a/internal/command/state_rm.go b/internal/command/state_rm.go index 10584792e7..796895d610 100644 --- a/internal/command/state_rm.go +++ b/internal/command/state_rm.go @@ -49,8 +49,15 @@ func (c *StateRmCommand) Run(args []string) int { return 1 } + // Load the encryption configuration + enc, encDiags := c.Encryption() + if encDiags.HasErrors() { + c.showDiagnostics(encDiags) + return 1 + } + // Get the state - stateMgr, err := c.State() + stateMgr, err := c.State(enc) if err != nil { c.Ui.Error(fmt.Sprintf(errStateLoadingState, err)) return 1 @@ -117,7 +124,7 @@ func (c *StateRmCommand) Run(args []string) int { return 0 // This is as far as we go in dry-run mode } - b, backendDiags := c.Backend(nil) + b, backendDiags := c.Backend(nil, enc.Backend()) diags = diags.Append(backendDiags) if backendDiags.HasErrors() { c.showDiagnostics(diags) diff --git a/internal/command/state_show.go b/internal/command/state_show.go index 90a3902ba7..188d390e0d 100644 --- a/internal/command/state_show.go +++ b/internal/command/state_show.go @@ -50,8 +50,15 @@ func (c *StateShowCommand) Run(args []string) int { return 1 } + // Load the encryption configuration + enc, encDiags := c.Encryption() + if encDiags.HasErrors() { + c.showDiagnostics(encDiags) + return 1 + } + // Load the backend - b, backendDiags := c.Backend(nil) + b, backendDiags := c.Backend(nil, enc.Backend()) if backendDiags.HasErrors() { c.showDiagnostics(backendDiags) return 1 @@ -82,7 +89,7 @@ func (c *StateShowCommand) Run(args []string) int { } // Build the operation (required to get the schemas) - opReq := c.Operation(b, arguments.ViewHuman) + opReq := c.Operation(b, arguments.ViewHuman, enc) opReq.AllowUnsetVariables = true opReq.ConfigDir = cwd diff --git a/internal/command/state_test.go b/internal/command/state_test.go index 429bed5f14..75528b88f4 100644 --- a/internal/command/state_test.go +++ b/internal/command/state_test.go @@ -11,6 +11,7 @@ import ( "sort" "testing" + "github.com/opentofu/opentofu/internal/encryption" "github.com/opentofu/opentofu/internal/states/statemgr" ) @@ -32,7 +33,7 @@ func testStateBackups(t *testing.T, dir string) []string { func TestStateDefaultBackupExtension(t *testing.T) { testCwd(t) - s, err := (&StateMeta{}).State() + s, err := (&StateMeta{}).State(encryption.Disabled()) if err != nil { t.Fatal(err) } diff --git a/internal/command/taint.go b/internal/command/taint.go index 2def91e290..5a5663d6cd 100644 --- a/internal/command/taint.go +++ b/internal/command/taint.go @@ -67,8 +67,15 @@ func (c *TaintCommand) Run(args []string) int { return 1 } + // Load the encryption configuration + enc, encDiags := c.Encryption() + if encDiags.HasErrors() { + c.showDiagnostics(encDiags) + return 1 + } + // Load the backend - b, backendDiags := c.Backend(nil) + b, backendDiags := c.Backend(nil, enc.Backend()) diags = diags.Append(backendDiags) if backendDiags.HasErrors() { c.showDiagnostics(diags) diff --git a/internal/command/test.go b/internal/command/test.go index 1123ae21ab..7c11bdac73 100644 --- a/internal/command/test.go +++ b/internal/command/test.go @@ -23,6 +23,7 @@ import ( "github.com/opentofu/opentofu/internal/command/arguments" "github.com/opentofu/opentofu/internal/command/views" "github.com/opentofu/opentofu/internal/configs" + "github.com/opentofu/opentofu/internal/encryption" "github.com/opentofu/opentofu/internal/logging" "github.com/opentofu/opentofu/internal/moduletest" "github.com/opentofu/opentofu/internal/plans" @@ -212,6 +213,9 @@ func (c *TestCommand) Run(rawArgs []string) int { return 1 } + // Don't use encryption during testing + opts.Encryption = encryption.Disabled() + // Print out all the diagnostics we have from the setup. These will just be // warnings, and we want them out of the way before we start the actual // testing. diff --git a/internal/command/unlock.go b/internal/command/unlock.go index 631d5ac8f3..c2bbbfa91d 100644 --- a/internal/command/unlock.go +++ b/internal/command/unlock.go @@ -64,7 +64,7 @@ func (c *UnlockCommand) Run(args []string) int { // Load the backend b, backendDiags := c.Backend(&BackendOpts{ Config: backendConfig, - }) + }, nil) // Should not be needed for an unlock diags = diags.Append(backendDiags) if backendDiags.HasErrors() { c.showDiagnostics(diags) diff --git a/internal/command/untaint.go b/internal/command/untaint.go index 77e0e30453..d74f91f75f 100644 --- a/internal/command/untaint.go +++ b/internal/command/untaint.go @@ -57,8 +57,16 @@ func (c *UntaintCommand) Run(args []string) int { return 1 } + // Load the encryption configuration + enc, encDiags := c.Encryption() + diags = diags.Append(encDiags) + if encDiags.HasErrors() { + c.showDiagnostics(diags) + return 1 + } + // Load the backend - b, backendDiags := c.Backend(nil) + b, backendDiags := c.Backend(nil, enc.Backend()) diags = diags.Append(backendDiags) if backendDiags.HasErrors() { c.showDiagnostics(diags) diff --git a/internal/command/workspace_delete.go b/internal/command/workspace_delete.go index 0b9fa2bc23..a6b0273336 100644 --- a/internal/command/workspace_delete.go +++ b/internal/command/workspace_delete.go @@ -63,10 +63,18 @@ func (c *WorkspaceDeleteCommand) Run(args []string) int { return 1 } + // Load the encryption configuration + enc, encDiags := c.EncryptionFromPath(configPath) + diags = diags.Append(encDiags) + if encDiags.HasErrors() { + c.showDiagnostics(diags) + return 1 + } + // Load the backend b, backendDiags := c.Backend(&BackendOpts{ Config: backendConfig, - }) + }, enc.Backend()) diags = diags.Append(backendDiags) if backendDiags.HasErrors() { c.showDiagnostics(diags) diff --git a/internal/command/workspace_list.go b/internal/command/workspace_list.go index 82e4f535dc..abc271413c 100644 --- a/internal/command/workspace_list.go +++ b/internal/command/workspace_list.go @@ -50,7 +50,7 @@ func (c *WorkspaceListCommand) Run(args []string) int { // Load the backend b, backendDiags := c.Backend(&BackendOpts{ Config: backendConfig, - }) + }, nil) diags = diags.Append(backendDiags) if backendDiags.HasErrors() { c.showDiagnostics(diags) diff --git a/internal/command/workspace_new.go b/internal/command/workspace_new.go index e155be9eea..fd6e41dcb0 100644 --- a/internal/command/workspace_new.go +++ b/internal/command/workspace_new.go @@ -79,10 +79,18 @@ func (c *WorkspaceNewCommand) Run(args []string) int { return 1 } + // Load the encryption configuration + enc, encDiags := c.EncryptionFromPath(configPath) + diags = diags.Append(encDiags) + if encDiags.HasErrors() { + c.showDiagnostics(diags) + return 1 + } + // Load the backend b, backendDiags := c.Backend(&BackendOpts{ Config: backendConfig, - }) + }, enc.Backend()) diags = diags.Append(backendDiags) if backendDiags.HasErrors() { c.showDiagnostics(diags) @@ -151,7 +159,7 @@ func (c *WorkspaceNewCommand) Run(args []string) int { return 1 } - stateFile, err := statefile.Read(f, encryption.StateEncryptionTODO()) // Should we use encryption -> statefile config here? + stateFile, err := statefile.Read(f, encryption.StateEncryptionDisabled()) // Assume given statefile is not encrypted if err != nil { c.Ui.Error(err.Error()) return 1 diff --git a/internal/command/workspace_select.go b/internal/command/workspace_select.go index 488718320b..41af5425df 100644 --- a/internal/command/workspace_select.go +++ b/internal/command/workspace_select.go @@ -60,10 +60,18 @@ func (c *WorkspaceSelectCommand) Run(args []string) int { return 1 } + // Load the encryption configuration + enc, encDiags := c.EncryptionFromPath(configPath) + diags = diags.Append(encDiags) + if encDiags.HasErrors() { + c.showDiagnostics(diags) + return 1 + } + // Load the backend b, backendDiags := c.Backend(&BackendOpts{ Config: backendConfig, - }) + }, enc.Backend()) diags = diags.Append(backendDiags) if backendDiags.HasErrors() { c.showDiagnostics(diags) diff --git a/internal/configs/parser_config.go b/internal/configs/parser_config.go index 9e723e6f11..4e4ae35640 100644 --- a/internal/configs/parser_config.go +++ b/internal/configs/parser_config.go @@ -337,6 +337,9 @@ var terraformBlockSchema = &hcl.BodySchema{ Type: "provider_meta", LabelNames: []string{"provider"}, }, + { + Type: "encryption", + }, }, } diff --git a/internal/encryption/base.go b/internal/encryption/base.go index fb37dc4fb8..bb859ddb2c 100644 --- a/internal/encryption/base.go +++ b/internal/encryption/base.go @@ -46,6 +46,17 @@ type basedata struct { Version string `json:"encryption_version"` // This is both a sigil for a valid encrypted payload and a future compatability field } +func IsEncryptionPayload(data []byte) (bool, error) { + es := basedata{} + err := json.Unmarshal(data, &es) + if err != nil { + return false, err + } + + // This could be extended with full version checking later on + return es.Version != "", nil +} + func (s *baseEncryption) encrypt(data []byte) ([]byte, error) { // No configuration provided, don't do anything if s.target == nil { @@ -100,19 +111,29 @@ func (s *baseEncryption) decrypt(data []byte, validator func([]byte) error) ([]b es := basedata{} err := json.Unmarshal(data, &es) - if err != nil { - return nil, fmt.Errorf("invalid data format for decryption: %w", err) - } - if len(es.Version) == 0 { + if len(es.Version) == 0 || err != nil { // Not a valid payload, might be already decrypted - err = validator(data) - if err != nil { + verr := validator(data) + if verr != nil { // Nope, just bad input - return nil, fmt.Errorf("unable to determine data structure during decryption: %w", err) + + // Return the outer json error if we have one + if err != nil { + return nil, fmt.Errorf("invalid data format for decryption: %w, %w", err, verr) + } + + // Must have been invalid json payload + return nil, fmt.Errorf("unable to determine data structure during decryption: %w", verr) } // Yep, it's already decrypted - return data, nil + for target := s.target; target != nil; target = target.Fallback { + if target.Fallback == nil { + // fallback allowed + return data, nil + } + } + return data, fmt.Errorf("decrypted payload provided without fallback specified") } if es.Version != encryptionVersion { diff --git a/internal/encryption/config/config.go b/internal/encryption/config/config.go index ddf1f1cde5..0eb0531816 100644 --- a/internal/encryption/config/config.go +++ b/internal/encryption/config/config.go @@ -20,7 +20,7 @@ type EncryptionConfig struct { Backend *EnforcableTargetConfig `hcl:"backend,block"` StateFile *EnforcableTargetConfig `hcl:"statefile,block"` PlanFile *EnforcableTargetConfig `hcl:"planfile,block"` - Remote *RemoteConfig `hcl:"remote_data_source,block"` + Remote *RemoteConfig `hcl:"remote,block"` // Not preserved through merge operations DeclRange hcl.Range @@ -61,7 +61,7 @@ func (m MethodConfig) Addr() (method.Addr, hcl.Diagnostics) { // sources. type RemoteConfig struct { Default *TargetConfig `hcl:"default,block"` - Targets []NamedTargetConfig `hcl:"remote_data_source,block"` + Targets []NamedTargetConfig `hcl:"remote_state,block"` } // TargetConfig describes the target.encryption.state, target.encryption.plan, etc blocks. diff --git a/internal/encryption/encryption.go b/internal/encryption/encryption.go index cf6aa0749e..302977ad56 100644 --- a/internal/encryption/encryption.go +++ b/internal/encryption/encryption.go @@ -15,54 +15,116 @@ import ( // purpose. If no encryption configuration is present, it should return a pass through method that doesn't do anything. type Encryption interface { // StateFile produces a StateEncryption overlay for encrypting and decrypting state files for local storage. - StateFile() (StateEncryption, hcl.Diagnostics) + StateFile() StateEncryption // PlanFile produces a PlanEncryption overlay for encrypting and decrypting plan files. - PlanFile() (PlanEncryption, hcl.Diagnostics) + PlanFile() PlanEncryption // Backend produces a StateEncryption overlay for storing state files on remote backends, such as an S3 bucket. - Backend() (StateEncryption, hcl.Diagnostics) + Backend() StateEncryption - // RemoteState produces a ReadOnlyStateEncryption for reading remote states using the terraform_remote_state data + // RemoteState produces a StateEncryption for reading remote states using the terraform_remote_state data // source. - RemoteState(string) (ReadOnlyStateEncryption, hcl.Diagnostics) + RemoteState(string) StateEncryption } type encryption struct { + statefile StateEncryption + planfile PlanEncryption + backend StateEncryption + remoteDefault StateEncryption + remotes map[string]StateEncryption + // Inputs cfg *config.EncryptionConfig reg registry.Registry } // New creates a new Encryption provider from the given configuration and registry. -func New(reg registry.Registry, cfg *config.EncryptionConfig) Encryption { - return &encryption{ +func New(reg registry.Registry, cfg *config.EncryptionConfig) (Encryption, hcl.Diagnostics) { + if cfg == nil { + return Disabled(), nil + } + + enc := &encryption{ cfg: cfg, reg: reg, + + remotes: make(map[string]StateEncryption), } -} + var diags hcl.Diagnostics + var encDiags hcl.Diagnostics -func (e *encryption) StateFile() (StateEncryption, hcl.Diagnostics) { - return newStateEncryption(e, e.cfg.StateFile.AsTargetConfig(), e.cfg.StateFile.Enforced, "statefile") -} + if cfg.StateFile != nil { + enc.statefile, encDiags = newStateEncryption(enc, cfg.StateFile.AsTargetConfig(), cfg.StateFile.Enforced, "statefile") + diags = append(diags, encDiags...) + } else { + enc.statefile = StateEncryptionDisabled() + } -func (e *encryption) PlanFile() (PlanEncryption, hcl.Diagnostics) { - return newPlanEncryption(e, e.cfg.PlanFile.AsTargetConfig(), e.cfg.PlanFile.Enforced, "planfile") -} + if cfg.PlanFile != nil { + enc.planfile, encDiags = newPlanEncryption(enc, cfg.PlanFile.AsTargetConfig(), cfg.PlanFile.Enforced, "planfile") + diags = append(diags, encDiags...) + } else { + enc.planfile = PlanEncryptionDisabled() + } -func (e *encryption) Backend() (StateEncryption, hcl.Diagnostics) { - return newStateEncryption(e, e.cfg.StateFile.AsTargetConfig(), e.cfg.StateFile.Enforced, "backend") -} + if cfg.Backend != nil { + enc.backend, encDiags = newStateEncryption(enc, cfg.Backend.AsTargetConfig(), cfg.Backend.Enforced, "backend") + diags = append(diags, encDiags...) + } else { + enc.backend = StateEncryptionDisabled() + } -func (e *encryption) RemoteState(name string) (ReadOnlyStateEncryption, hcl.Diagnostics) { - for _, remoteTarget := range e.cfg.Remote.Targets { - if remoteTarget.Name == name { + if cfg.Remote != nil && cfg.Remote.Default != nil { + enc.remoteDefault, encDiags = newStateEncryption(enc, cfg.Remote.Default, false, "remote.default") + diags = append(diags, encDiags...) + } else { + enc.remoteDefault = StateEncryptionDisabled() + } + + if cfg.Remote != nil { + for _, remoteTarget := range cfg.Remote.Targets { // TODO the addr here should be generated in one place. addr := "remote.remote_state_datasource." + remoteTarget.Name - return newStateEncryption( - e, remoteTarget.AsTargetConfig(), false, addr, - ) + enc.remotes[remoteTarget.Name], encDiags = newStateEncryption(enc, remoteTarget.AsTargetConfig(), false, addr) + diags = append(diags, encDiags...) } } - return newStateEncryption(e, e.cfg.Remote.Default, false, "remote.default") + if diags.HasErrors() { + return nil, diags + } + return enc, diags +} + +func (e *encryption) StateFile() StateEncryption { + return e.statefile +} + +func (e *encryption) PlanFile() PlanEncryption { + return e.planfile +} + +func (e *encryption) Backend() StateEncryption { + return e.backend +} + +func (e *encryption) RemoteState(name string) StateEncryption { + if enc, ok := e.remotes[name]; ok { + return enc + } + return e.remoteDefault +} + +// Mostly used in tests +type encryptionDisabled struct{} + +func Disabled() Encryption { + return &encryptionDisabled{} +} +func (e *encryptionDisabled) StateFile() StateEncryption { return StateEncryptionDisabled() } +func (e *encryptionDisabled) PlanFile() PlanEncryption { return PlanEncryptionDisabled() } +func (e *encryptionDisabled) Backend() StateEncryption { return StateEncryptionDisabled() } +func (e *encryptionDisabled) RemoteState(name string) StateEncryption { + return StateEncryptionDisabled() } diff --git a/internal/encryption/enctest/setup.go b/internal/encryption/enctest/setup.go new file mode 100644 index 0000000000..6951bd746e --- /dev/null +++ b/internal/encryption/enctest/setup.go @@ -0,0 +1,101 @@ +// Copyright (c) The OpenTofu Authors +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) 2023 HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package enctest + +// This package is used for supplying a fully configured encryption instance for use in unit and integration tests + +import ( + "github.com/hashicorp/hcl/v2" + "github.com/opentofu/opentofu/internal/encryption" + "github.com/opentofu/opentofu/internal/encryption/config" + "github.com/opentofu/opentofu/internal/encryption/keyprovider/static" + "github.com/opentofu/opentofu/internal/encryption/method/aesgcm" + "github.com/opentofu/opentofu/internal/encryption/registry/lockingencryptionregistry" +) + +// TODO docstrings once this stabilizes + +func EncryptionDirect(configData string) encryption.Encryption { + reg := lockingencryptionregistry.New() + if err := reg.RegisterKeyProvider(static.New()); err != nil { + panic(err) + } + if err := reg.RegisterMethod(aesgcm.New()); err != nil { + panic(err) + } + + cfg, diags := config.LoadConfigFromString("Test Config Source", configData) + + handleDiags(diags) + + enc, diags := encryption.New(reg, cfg) + handleDiags(diags) + + return enc +} + +func EncryptionRequired() encryption.Encryption { + return EncryptionDirect(` + key_provider "static" "basic" { + key = "6f6f706830656f67686f6834616872756f3751756165686565796f6f72653169" + } + method "aes_gcm" "example" { + keys = key_provider.static.basic + } + statefile { + method = method.aes_gcm.example + } + planfile { + method = method.aes_gcm.example + } + backend { + method = method.aes_gcm.example + } + remote { + default { + method = method.aes_gcm.example + } + } + `) +} + +func EncryptionWithFallback() encryption.Encryption { + return EncryptionDirect(` + key_provider "static" "basic" { + key = "6f6f706830656f67686f6834616872756f3751756165686565796f6f72653169" + } + method "aes_gcm" "example" { + keys = key_provider.static.basic + } + statefile { + method = method.aes_gcm.example + fallback {} + } + planfile { + method = method.aes_gcm.example + fallback {} + } + backend { + method = method.aes_gcm.example + fallback {} + } + remote { + default { + method = method.aes_gcm.example + fallback {} + } + } + `) +} + +func handleDiags(diags hcl.Diagnostics) { + for _, d := range diags { + println(d.Error()) + } + if diags.HasErrors() { + panic(diags.Error()) + } +} diff --git a/internal/encryption/example_test.go b/internal/encryption/example_test.go index 23606ba441..3eecd2b7a4 100644 --- a/internal/encryption/example_test.go +++ b/internal/encryption/example_test.go @@ -62,11 +62,11 @@ func Example() { cfg := config.MergeConfigs(cfgA, cfgB) // Construct the encryption object - enc := encryption.New(reg, cfg) - - sfe, diags := enc.StateFile() + enc, diags := encryption.New(reg, cfg) handleDiags(diags) + sfe := enc.StateFile() + // Encrypt the data, for this example we will be using the string "test", // but in a real world scenario this would be the plan file. sourceData := []byte("test") diff --git a/internal/encryption/plan.go b/internal/encryption/plan.go index 72d52c1f18..1088a3f5b5 100644 --- a/internal/encryption/plan.go +++ b/internal/encryption/plan.go @@ -61,8 +61,8 @@ func (p planEncryption) EncryptPlan(data []byte) ([]byte, error) { func (p planEncryption) DecryptPlan(data []byte) ([]byte, error) { return p.base.decrypt(data, func(data []byte) error { // Check magic bytes - if len(data) < 4 || string(data[:4]) != "PK" { - return fmt.Errorf("Invalid plan file") + if len(data) < 2 || string(data[:2]) != "PK" { + return fmt.Errorf("Invalid plan file %v", string(data[:2])) } return nil }) @@ -80,8 +80,3 @@ func (s *planDisabled) EncryptPlan(plainPlan []byte) ([]byte, error) { func (s *planDisabled) DecryptPlan(encryptedPlan []byte) ([]byte, error) { return encryptedPlan, nil } - -// TODO REMOVEME once plan encryption is fully integrated into the codebase -func PlanEncryptionTODO() PlanEncryption { - return &planDisabled{} -} diff --git a/internal/encryption/state.go b/internal/encryption/state.go index 78e5bb38a9..ea026f258c 100644 --- a/internal/encryption/state.go +++ b/internal/encryption/state.go @@ -13,10 +13,8 @@ import ( "github.com/opentofu/opentofu/internal/encryption/config" ) -const StateEncryptionMarkerField = "encryption" - -// ReadOnlyStateEncryption is an encryption layer for reading encrypted state files. -type ReadOnlyStateEncryption interface { +// StateEncryption describes the interface for encrypting state files. +type StateEncryption interface { // DecryptState decrypts a potentially encrypted state file and returns a valid JSON-serialized state file. // // When implementing this function: @@ -34,11 +32,6 @@ type ReadOnlyStateEncryption interface { // and all encryption-related matters. After the function returns, use the returned byte array as a normal state // file. DecryptState([]byte) ([]byte, error) -} - -// StateEncryption describes the interface for encrypting state files. -type StateEncryption interface { - ReadOnlyStateEncryption // EncryptState encrypts a state file and returns the encrypted form. // @@ -76,7 +69,7 @@ func (s *stateEncryption) EncryptState(plainState []byte) ([]byte, error) { func (s *stateEncryption) DecryptState(encryptedState []byte) ([]byte, error) { return s.base.decrypt(encryptedState, func(data []byte) error { tmp := struct { - FormatVersion string `json:"format_version"` + FormatVersion string `json:"terraform_version"` }{} err := json.Unmarshal(data, &tmp) if err != nil { @@ -103,8 +96,3 @@ func (s *stateDisabled) EncryptState(plainState []byte) ([]byte, error) { func (s *stateDisabled) DecryptState(encryptedState []byte) ([]byte, error) { return encryptedState, nil } - -// TODO REMOVEME once state encryption is fully integrated into the codebase -func StateEncryptionTODO() StateEncryption { - return &stateDisabled{} -} diff --git a/internal/states/statefile/read.go b/internal/states/statefile/read.go index 1b02e9f2dc..e168205a44 100644 --- a/internal/states/statefile/read.go +++ b/internal/states/statefile/read.go @@ -76,8 +76,10 @@ func Read(r io.Reader, enc encryption.StateEncryption) (*File, error) { return nil, ErrNoState } - decrypted, decDiags := enc.DecryptState(src) - diags = diags.Append(decDiags) + decrypted, err := enc.DecryptState(src) + if err != nil { + return nil, err + } state, err := readState(decrypted) if err != nil { @@ -191,6 +193,23 @@ func sniffJSONStateVersion(src []byte) (uint64, tfdiags.Diagnostics) { } if sniff.Version == nil { + encrypted, err := encryption.IsEncryptionPayload(src) + if err != nil { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + unsupportedFormat, + fmt.Sprintf("The state file can not be checked for presense of encryption: %s", err.Error()), + )) + return 0, diags + } + if encrypted { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + unsupportedFormat, + "This state file is encrypted and can not be read without an encryption configuration", + )) + return 0, diags + } diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, unsupportedFormat, diff --git a/internal/states/statefile/read_test.go b/internal/states/statefile/read_test.go index 74fd76485c..8343fc788e 100644 --- a/internal/states/statefile/read_test.go +++ b/internal/states/statefile/read_test.go @@ -3,11 +3,13 @@ package statefile import ( + "bytes" "errors" "os" "testing" "github.com/opentofu/opentofu/internal/encryption" + "github.com/opentofu/opentofu/internal/encryption/enctest" ) func TestReadErrNoState_emptyFile(t *testing.T) { @@ -34,3 +36,20 @@ func TestReadErrNoState_nilFile(t *testing.T) { t.Fatalf("expected ErrNoState, got %T", err) } } +func TestReadEmptyWithEncryption(t *testing.T) { + payload := bytes.NewBufferString("") + + _, err := Read(payload, enctest.EncryptionRequired().Backend()) + if !errors.Is(err, ErrNoState) { + t.Fatalf("expected ErrNoState, got %T", err) + } +} +func TestReadEmptyJsonWithEncryption(t *testing.T) { + payload := bytes.NewBufferString("{}") + + _, err := Read(payload, enctest.EncryptionRequired().Backend()) + + if err == nil || err.Error() != "unable to determine data structure during decryption: Given payload is not a state file" { + t.Fatalf("expected encryption error, got %v", err) + } +} diff --git a/internal/states/statefile/roundtrip_test.go b/internal/states/statefile/roundtrip_test.go index 3ea29651c4..355135c8ba 100644 --- a/internal/states/statefile/roundtrip_test.go +++ b/internal/states/statefile/roundtrip_test.go @@ -15,6 +15,7 @@ import ( "github.com/go-test/deep" "github.com/opentofu/opentofu/internal/encryption" + "github.com/opentofu/opentofu/internal/encryption/enctest" ) func TestRoundtrip(t *testing.T) { @@ -79,3 +80,48 @@ func TestRoundtrip(t *testing.T) { }) } } + +func TestRoundtripEncryption(t *testing.T) { + const path = "testdata/roundtrip/v4-modules.out.tfstate" + + enc := enctest.EncryptionWithFallback().Backend() + + unencryptedInput, err := os.Open(path) + if err != nil { + t.Fatal(err) + } + defer unencryptedInput.Close() + + // Read unencrypted using fallback + originalState, err := Read(unencryptedInput, enc) + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + // Write encrypted + var encrypted bytes.Buffer + err = Write(originalState, &encrypted, enc) + if err != nil { + t.Fatal(err) + } + + // Make sure it is encrypted / not readable + encryptedCopy := encrypted + _, err = Read(&encryptedCopy, encryption.StateEncryptionDisabled()) + if err == nil || err.Error() != "Unsupported state file format: This state file is encrypted and can not be read without an encryption configuration" { + t.Fatalf("expected written state file to be encrypted!") + } + + // Read encrypted + newState, err := Read(&encrypted, enc) + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + // Compare before/after encryption workflow + problems := deep.Equal(newState, originalState) + sort.Strings(problems) + for _, problem := range problems { + t.Error(problem) + } +} diff --git a/internal/tofu/context.go b/internal/tofu/context.go index 68b41f2145..ad1a58167d 100644 --- a/internal/tofu/context.go +++ b/internal/tofu/context.go @@ -14,6 +14,7 @@ import ( "github.com/opentofu/opentofu/internal/addrs" "github.com/opentofu/opentofu/internal/configs" + "github.com/opentofu/opentofu/internal/encryption" "github.com/opentofu/opentofu/internal/logging" "github.com/opentofu/opentofu/internal/providers" "github.com/opentofu/opentofu/internal/provisioners" @@ -43,6 +44,7 @@ type ContextOpts struct { Parallelism int Providers map[addrs.Provider]providers.Factory Provisioners map[string]provisioners.Factory + Encryption encryption.Encryption UIInput UIInput } @@ -89,6 +91,8 @@ type Context struct { runCond *sync.Cond runContext context.Context runContextCancel context.CancelFunc + + encryption encryption.Encryption } // (additional methods on Context can be found in context_*.go files.) @@ -144,6 +148,8 @@ func NewContext(opts *ContextOpts) (*Context, tfdiags.Diagnostics) { parallelSem: NewSemaphore(par), providerInputConfig: make(map[string]map[string]cty.Value), sh: sh, + + encryption: opts.Encryption, }, diags } diff --git a/internal/tofu/context_test.go b/internal/tofu/context_test.go index 1aeca2839c..1e942fb2a5 100644 --- a/internal/tofu/context_test.go +++ b/internal/tofu/context_test.go @@ -261,6 +261,8 @@ func testContext2(t *testing.T, opts *ContextOpts) *Context { t.Fatalf("failed to create test context\n\n%s\n", diags.Err()) } + ctx.encryption = encryption.Disabled() + return ctx } diff --git a/internal/tofu/context_walk.go b/internal/tofu/context_walk.go index 2c5500fd40..a1744cefdf 100644 --- a/internal/tofu/context_walk.go +++ b/internal/tofu/context_walk.go @@ -153,5 +153,6 @@ func (c *Context) graphWalker(operation walkOperation, opts *graphWalkOpts) *Con Operation: operation, StopContext: c.runContext, PlanTimestamp: opts.PlanTimeTimestamp, + Encryption: c.encryption, } } diff --git a/internal/tofu/eval_context.go b/internal/tofu/eval_context.go index 1ef1096277..50ccf1f503 100644 --- a/internal/tofu/eval_context.go +++ b/internal/tofu/eval_context.go @@ -10,6 +10,7 @@ import ( "github.com/opentofu/opentofu/internal/addrs" "github.com/opentofu/opentofu/internal/checks" "github.com/opentofu/opentofu/internal/configs/configschema" + "github.com/opentofu/opentofu/internal/encryption" "github.com/opentofu/opentofu/internal/instances" "github.com/opentofu/opentofu/internal/lang" "github.com/opentofu/opentofu/internal/plans" @@ -214,4 +215,7 @@ type EvalContext interface { // WithPath returns a copy of the context with the internal path set to the // path argument. WithPath(path addrs.ModuleInstance) EvalContext + + // Returns the currently configured encryption setup + GetEncryption() encryption.Encryption } diff --git a/internal/tofu/eval_context_builtin.go b/internal/tofu/eval_context_builtin.go index 376fab4a90..5c333bc21e 100644 --- a/internal/tofu/eval_context_builtin.go +++ b/internal/tofu/eval_context_builtin.go @@ -17,6 +17,7 @@ import ( "github.com/opentofu/opentofu/internal/addrs" "github.com/opentofu/opentofu/internal/checks" "github.com/opentofu/opentofu/internal/configs/configschema" + "github.com/opentofu/opentofu/internal/encryption" "github.com/opentofu/opentofu/internal/instances" "github.com/opentofu/opentofu/internal/lang" "github.com/opentofu/opentofu/internal/plans" @@ -76,6 +77,7 @@ type BuiltinEvalContext struct { InstanceExpanderValue *instances.Expander MoveResultsValue refactoring.MoveResults ImportResolverValue *ImportResolver + Encryption encryption.Encryption } // BuiltinEvalContext implements EvalContext @@ -515,3 +517,7 @@ func (ctx *BuiltinEvalContext) MoveResults() refactoring.MoveResults { func (ctx *BuiltinEvalContext) ImportResolver() *ImportResolver { return ctx.ImportResolverValue } + +func (ctx *BuiltinEvalContext) GetEncryption() encryption.Encryption { + return ctx.Encryption +} diff --git a/internal/tofu/eval_context_mock.go b/internal/tofu/eval_context_mock.go index 59f6490e70..522d00ba49 100644 --- a/internal/tofu/eval_context_mock.go +++ b/internal/tofu/eval_context_mock.go @@ -11,6 +11,7 @@ import ( "github.com/opentofu/opentofu/internal/addrs" "github.com/opentofu/opentofu/internal/checks" "github.com/opentofu/opentofu/internal/configs/configschema" + "github.com/opentofu/opentofu/internal/encryption" "github.com/opentofu/opentofu/internal/instances" "github.com/opentofu/opentofu/internal/lang" "github.com/opentofu/opentofu/internal/plans" @@ -412,3 +413,7 @@ func (c *MockEvalContext) InstanceExpander() *instances.Expander { c.InstanceExpanderCalled = true return c.InstanceExpanderExpander } + +func (c *MockEvalContext) GetEncryption() encryption.Encryption { + return encryption.Disabled() +} diff --git a/internal/tofu/graph_walk_context.go b/internal/tofu/graph_walk_context.go index a984d20708..80d4e795ba 100644 --- a/internal/tofu/graph_walk_context.go +++ b/internal/tofu/graph_walk_context.go @@ -16,6 +16,7 @@ import ( "github.com/opentofu/opentofu/internal/checks" "github.com/opentofu/opentofu/internal/configs" "github.com/opentofu/opentofu/internal/configs/configschema" + "github.com/opentofu/opentofu/internal/encryption" "github.com/opentofu/opentofu/internal/instances" "github.com/opentofu/opentofu/internal/plans" "github.com/opentofu/opentofu/internal/providers" @@ -45,6 +46,7 @@ type ContextGraphWalker struct { RootVariableValues InputValues Config *configs.Config PlanTimestamp time.Time + Encryption encryption.Encryption // This is an output. Do not set this, nor read it while a graph walk // is in progress. @@ -117,6 +119,7 @@ func (w *ContextGraphWalker) EvalContext() EvalContext { Evaluator: evaluator, VariableValues: w.variableValues, VariableValuesLock: &w.variableValuesLock, + Encryption: w.Encryption, } return ctx diff --git a/internal/tofu/node_resource_abstract_instance.go b/internal/tofu/node_resource_abstract_instance.go index f8f87d4b96..e1a437cf17 100644 --- a/internal/tofu/node_resource_abstract_instance.go +++ b/internal/tofu/node_resource_abstract_instance.go @@ -17,6 +17,7 @@ import ( "github.com/opentofu/opentofu/internal/checks" "github.com/opentofu/opentofu/internal/configs" "github.com/opentofu/opentofu/internal/configs/configschema" + "github.com/opentofu/opentofu/internal/encryption" "github.com/opentofu/opentofu/internal/instances" "github.com/opentofu/opentofu/internal/plans" "github.com/opentofu/opentofu/internal/plans/objchange" @@ -1420,6 +1421,10 @@ func processIgnoreChangesIndividual(prior, config cty.Value, ignoreChangesPath [ return ret, nil } +type ProviderWithEncryption interface { + ReadDataSourceEncrypted(req providers.ReadDataSourceRequest, path addrs.AbsResourceInstance, enc encryption.Encryption) providers.ReadDataSourceResponse +} + // readDataSource handles everything needed to call ReadDataSource on the provider. // A previously evaluated configVal can be passed in, or a new one is generated // from the resource configuration. @@ -1474,11 +1479,18 @@ func (n *NodeAbstractResourceInstance) readDataSource(ctx EvalContext, configVal return newVal, diags } - resp := provider.ReadDataSource(providers.ReadDataSourceRequest{ + req := providers.ReadDataSourceRequest{ TypeName: n.Addr.ContainingResource().Resource.Type, Config: configVal, ProviderMeta: metaConfigVal, - }) + } + var resp providers.ReadDataSourceResponse + if tfp, ok := provider.(ProviderWithEncryption); ok { + // Special case for terraform_remote_state + resp = tfp.ReadDataSourceEncrypted(req, n.Addr, ctx.GetEncryption()) + } else { + resp = provider.ReadDataSource(req) + } diags = diags.Append(resp.Diagnostics.InConfigBody(config.Config, n.Addr.String())) if diags.HasErrors() { return newVal, diags diff --git a/website/docs/cli/config/environment-variables.mdx b/website/docs/cli/config/environment-variables.mdx index 8d06b53122..298c3c12ef 100644 --- a/website/docs/cli/config/environment-variables.mdx +++ b/website/docs/cli/config/environment-variables.mdx @@ -169,3 +169,12 @@ For more details on `.terraformignore`, please see [Excluding Files from Upload The CLI integration with cloud backends lets you use them on the command line. The integration requires including a `cloud` block in your OpenTofu configuration. You can define its arguments directly in your configuration file or supply them through environment variables, which can be useful for non-interactive workflows like Continuous Integration (CI). Refer to [Cloud Backend Settings](/docs/cli/cloud/settings#environment-variables) for a full list of `cloud` block environment variables. + +## TF_ENCRYPTION + +The `TF_ENCRYPTION` environment variable is an alternate method of specifying the contents of the `terraform { encryption {} }` block. If provided, it will be parsed as either HCL or JSON and override configuration present in the .tf config files. + +```shell +# Add/Override encryption key_provider.static.mykp +export TF_ENCRYPTION='key_provider "static" "mykp" { key = "6f6f706830656f67686f6834616872756f3751756165686565796f6f72653169" }' +```