Read state refactor (#3706)

Signed-off-by: KrishnaSindhur <krishna.sindhur@harness.io>
Signed-off-by: krishna sindhur <krishna.sindhur@harness.io>
This commit is contained in:
krishna sindhur
2026-03-19 20:33:16 +05:30
committed by GitHub
parent 48af3ba77a
commit 6a385c3cbc
9 changed files with 579 additions and 101 deletions

View File

@@ -6,8 +6,8 @@
package cloud
import (
"github.com/opentofu/opentofu/internal/command/clistate"
"github.com/opentofu/opentofu/internal/configs"
legacy "github.com/opentofu/opentofu/internal/legacy/tofu"
)
// Most of the logic for migrating into and out of "cloud mode" actually lives
@@ -50,7 +50,7 @@ const (
// the way we currently model working directory settings and config, so its
// signature probably won't survive any non-trivial refactoring of how
// the CLI layer thinks about backends/state storage.
func DetectConfigChangeType(wdState *legacy.BackendState, config *configs.Backend, haveLocalStates bool) ConfigChangeMode {
func DetectConfigChangeType(wdState *clistate.BackendState, config *configs.Backend, haveLocalStates bool) ConfigChangeMode {
// Although externally the cloud integration isn't really a "backend",
// internally we treat it a bit like one just to preserve all of our
// existing interfaces that assume backends. "cloud" is the placeholder

View File

@@ -8,8 +8,8 @@ package cloud
import (
"testing"
"github.com/opentofu/opentofu/internal/command/clistate"
"github.com/opentofu/opentofu/internal/configs"
legacy "github.com/opentofu/opentofu/internal/legacy/tofu"
)
func TestDetectConfigChangeType(t *testing.T) {
@@ -103,10 +103,10 @@ func TestDetectConfigChangeType(t *testing.T) {
for name, test := range tests {
t.Run(name, func(t *testing.T) {
var state *legacy.BackendState
var state *clistate.BackendState
var config *configs.Backend
if test.stateType != "" {
state = &legacy.BackendState{
state = &clistate.BackendState{
Type: test.stateType,
// everything else is irrelevant for our purposes here
}

View File

@@ -0,0 +1,168 @@
// Copyright (c) The OpenTofu Authors
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package clistate
import (
"bufio"
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"github.com/mitchellh/copystructure"
"github.com/opentofu/opentofu/internal/configs/configschema"
"github.com/opentofu/opentofu/internal/plans"
tfversion "github.com/opentofu/opentofu/version"
"github.com/zclconf/go-cty/cty"
ctyjson "github.com/zclconf/go-cty/cty/json"
)
// StateVersion is the current supported version for CLI state files.
const StateVersion = 3
type CLIState struct {
// Version is the state file protocol version.
Version int `json:"version"`
// Backend tracks the configuration for the backend in use with
// this state. This is used to track any changes in the backend
// configuration.
Backend *BackendState `json:"backend,omitempty"`
}
func NewState() *CLIState {
return &CLIState{
Version: StateVersion,
}
}
// DeepCopy performs a deep copy of the CLI state structure and returns
// a new structure.
func (s *CLIState) DeepCopy() *CLIState {
if s == nil {
return nil
}
cpy, err := copystructure.Copy(s)
if err != nil {
panic(err)
}
return cpy.(*CLIState)
}
// BackendState stores the configuration to connect to a backend.
type BackendState struct {
Type string `json:"type"` // Backend type
ConfigRaw json.RawMessage `json:"config"` // Backend raw config
Hash uint64 `json:"hash"` // Hash of configuration from config files
}
func (b *BackendState) Empty() bool {
return b == nil || b.Type == ""
}
func (b *BackendState) Config(schema *configschema.Block) (cty.Value, error) {
ty := schema.ImpliedType()
if b == nil {
return cty.NullVal(ty), nil
}
return ctyjson.Unmarshal(b.ConfigRaw, ty)
}
func (b *BackendState) SetConfig(val cty.Value, schema *configschema.Block) error {
ty := schema.ImpliedType()
buf, err := ctyjson.Marshal(val, ty)
if err != nil {
return err
}
b.ConfigRaw = buf
return nil
}
func (b *BackendState) ForPlan(schema *configschema.Block, workspaceName string) (*plans.Backend, error) {
if b == nil {
return nil, nil
}
configVal, err := b.Config(schema)
if err != nil {
return nil, fmt.Errorf("failed to decode backend config: %w", err)
}
return plans.NewBackend(b.Type, configVal, schema, workspaceName)
}
var ErrNoState = errors.New("no state")
type jsonVersionOnly struct {
Version int `json:"version"`
}
// ReadState reads the CLI state file format written by WriteState.
func ReadState(src io.Reader) (*CLIState, error) {
if f, ok := src.(*os.File); ok && f == nil {
return nil, ErrNoState
}
buf := bufio.NewReader(src)
if _, err := buf.Peek(1); err != nil {
if errors.Is(err, io.EOF) {
return nil, ErrNoState
}
return nil, err
}
jsonBytes, err := io.ReadAll(buf)
if err != nil {
return nil, fmt.Errorf("reading CLI state file failed: %w", err)
}
ver := &jsonVersionOnly{}
if err := json.Unmarshal(jsonBytes, ver); err != nil {
return nil, fmt.Errorf("decoding CLI state file version failed: %w", err)
}
if ver.Version != StateVersion {
return nil, fmt.Errorf(
"opentofu %s does not support CLI state version %d, please update",
tfversion.SemVer.String(),
ver.Version,
)
}
st := &CLIState{}
if err := json.Unmarshal(jsonBytes, st); err != nil {
return nil, fmt.Errorf("decoding CLI state file failed: %w", err)
}
// Ensure the version is set consistently
st.Version = StateVersion
return st, nil
}
// WriteState writes CLIState in JSON form.
func WriteState(st *CLIState, dst io.Writer) error {
if st == nil {
return nil
}
st.Version = StateVersion
data, err := json.MarshalIndent(st, "", " ")
if err != nil {
return fmt.Errorf("failed to encode CLI state: %w", err)
}
data = append(data, '\n')
if _, err := io.Copy(dst, bytes.NewReader(data)); err != nil {
return fmt.Errorf("failed to write CLI state: %w", err)
}
return nil
}

View File

@@ -0,0 +1,339 @@
// Copyright (c) The OpenTofu Authors
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package clistate
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"os"
"strings"
"testing"
"github.com/opentofu/opentofu/internal/configs/configschema"
"github.com/zclconf/go-cty/cty"
)
func TestReadState_EmptyFile(t *testing.T) {
reader := bytes.NewReader([]byte{})
_, err := ReadState(reader)
if !errors.Is(err, ErrNoState) {
t.Fatalf("expected ErrNoState, got %T", err)
}
}
func TestReadState_NilFile(t *testing.T) {
var f *os.File
_, err := ReadState(f)
if !errors.Is(err, ErrNoState) {
t.Fatalf("expected ErrNoState, got %T", err)
}
}
func TestReadState_ValidState(t *testing.T) {
state := &CLIState{
Version: StateVersion,
}
buf := &bytes.Buffer{}
if err := WriteState(state, buf); err != nil {
t.Fatalf("failed to write state: %v", err)
}
result, err := ReadState(buf)
if err != nil {
t.Fatalf("ReadState failed: %v", err)
}
if result.Version != StateVersion {
t.Errorf("expected version %d, got %d", StateVersion, result.Version)
}
}
func TestReadState_InvalidJSON(t *testing.T) {
reader := strings.NewReader("{invalid json")
_, err := ReadState(reader)
if err == nil {
t.Fatal("expected error for invalid JSON, got nil")
}
if !strings.Contains(err.Error(), "decoding CLI state file version failed") {
t.Errorf("unexpected error message: %v", err)
}
}
func TestReadState_MissingVersion(t *testing.T) {
reader := strings.NewReader(`{}`)
_, err := ReadState(reader)
if err == nil {
t.Fatal("expected error for missing version, got nil")
}
if !strings.Contains(err.Error(), "does not support CLI state version 0") {
t.Errorf("unexpected error message: %v", err)
}
}
func TestReadState_UnsupportedVersion(t *testing.T) {
tests := []struct {
name string
version int
}{
{"state file v1", 1},
{"state file v2", 2},
{"state file v4", 4},
{"future version", 10},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
input := fmt.Sprintf(`{"version": %d}`, tt.version)
_, err := ReadState(strings.NewReader(input))
if err == nil {
t.Fatalf("expected error for version %d, got nil", tt.version)
}
want := fmt.Sprintf("does not support CLI state version %d", tt.version)
if !strings.Contains(err.Error(), want) {
t.Errorf("got error %q, want substring %q", err.Error(), want)
}
})
}
}
func TestWriteState_NilState(t *testing.T) {
buf := &bytes.Buffer{}
err := WriteState(nil, buf)
if err != nil {
t.Fatalf("WriteState with nil should not error, got: %v", err)
}
if buf.Len() != 0 {
t.Errorf("expected empty buffer for nil state, got %d bytes", buf.Len())
}
}
func TestWriteState_ValidState(t *testing.T) {
state := &CLIState{
Version: StateVersion,
}
buf := &bytes.Buffer{}
err := WriteState(state, buf)
if err != nil {
t.Fatalf("WriteState failed: %v", err)
}
var parsed CLIState
if err := json.Unmarshal(buf.Bytes(), &parsed); err != nil {
t.Fatalf("failed to parse written state: %v", err)
}
if parsed.Version != StateVersion {
t.Errorf("expected version %d, got %d", StateVersion, parsed.Version)
}
}
func TestWriteState_SetsVersion(t *testing.T) {
state := &CLIState{
Version: 99, // Invalid version
}
buf := &bytes.Buffer{}
err := WriteState(state, buf)
if err != nil {
t.Fatalf("WriteState failed: %v", err)
}
var parsed CLIState
if err := json.Unmarshal(buf.Bytes(), &parsed); err != nil {
t.Fatalf("failed to parse written state: %v", err)
}
if parsed.Version != StateVersion {
t.Errorf("expected version to be corrected to %d, got %d", StateVersion, parsed.Version)
}
}
func TestNewState(t *testing.T) {
state := NewState()
if state.Version != StateVersion {
t.Errorf("expected version %d, got %d", StateVersion, state.Version)
}
}
func TestCLIState_DeepCopy(t *testing.T) {
original := &CLIState{
Version: StateVersion,
}
copied := original.DeepCopy()
if copied.Version != original.Version {
t.Errorf("version mismatch: %d != %d", copied.Version, original.Version)
}
}
func TestCLIState_DeepCopy_Nil(t *testing.T) {
var state *CLIState
copied := state.DeepCopy()
if copied != nil {
t.Errorf("expected nil for DeepCopy of nil, got %T", copied)
}
}
func TestBackendState_Empty(t *testing.T) {
tests := []struct {
name string
backend *BackendState
expected bool
}{
{"nil backend", nil, true},
{"empty type", &BackendState{Type: ""}, true},
{"with type", &BackendState{Type: "local"}, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := tt.backend.Empty(); got != tt.expected {
t.Errorf("Empty() = %v, want %v", got, tt.expected)
}
})
}
}
func TestBackendState_ConfigSetAndGet(t *testing.T) {
schema := &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"path": {
Type: cty.String,
Optional: true,
},
},
}
backend := &BackendState{Type: "local"}
val := cty.ObjectVal(map[string]cty.Value{
"path": cty.StringVal("/tmp/state"),
})
err := backend.SetConfig(val, schema)
if err != nil {
t.Fatalf("SetConfig failed: %v", err)
}
retrieved, err := backend.Config(schema)
if err != nil {
t.Fatalf("Config failed: %v", err)
}
if !val.RawEquals(retrieved) {
t.Errorf("retrieved config doesn't match original:\nwant: %#v\ngot: %#v", val, retrieved)
}
}
func TestBackendState_ConfigNil(t *testing.T) {
schema := &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"path": {Type: cty.String, Optional: true},
},
}
var backend *BackendState
val, err := backend.Config(schema)
if err != nil {
t.Fatalf("Config on nil backend failed: %v", err)
}
expectedType := schema.ImpliedType()
if !val.Type().Equals(expectedType) {
t.Errorf("expected type %s for nil backend, got %s", expectedType.FriendlyName(), val.Type().FriendlyName())
}
if !val.IsNull() {
t.Error("expected null value for nil backend")
}
}
func TestBackendState_ForPlan(t *testing.T) {
schema := &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"path": {
Type: cty.String,
Optional: true,
},
},
}
backend := &BackendState{
Type: "local",
Hash: 12345,
}
val := cty.ObjectVal(map[string]cty.Value{
"path": cty.StringVal("/tmp/state"),
})
err := backend.SetConfig(val, schema)
if err != nil {
t.Fatalf("SetConfig failed: %v", err)
}
plan, err := backend.ForPlan(schema, "default")
if err != nil {
t.Fatalf("ForPlan failed: %v", err)
}
if plan == nil {
t.Fatal("expected non-nil plan backend")
}
}
func TestBackendState_ForPlanNil(t *testing.T) {
schema := &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"path": {Type: cty.String, Optional: true},
},
}
var backend *BackendState
plan, err := backend.ForPlan(schema, "default")
if err != nil {
t.Fatalf("ForPlan on nil backend failed: %v", err)
}
if plan != nil {
t.Errorf("expected nil plan for nil backend, got %v", plan)
}
}
func TestReadWriteRoundTrip(t *testing.T) {
original := &CLIState{
Version: StateVersion,
Backend: &BackendState{
Type: "s3",
Hash: 12345,
},
}
buf := &bytes.Buffer{}
if err := WriteState(original, buf); err != nil {
t.Fatalf("WriteState failed: %v", err)
}
result, err := ReadState(buf)
if err != nil {
t.Fatalf("ReadState failed: %v", err)
}
if result.Version != StateVersion {
t.Errorf("version mismatch: got %d, want %d", result.Version, StateVersion)
}
if result.Backend == nil {
t.Fatal("expected backend to be non-nil")
}
if result.Backend.Type != "s3" {
t.Errorf("backend type: got %q, want %q", result.Backend.Type, "s3")
}
}

View File

@@ -18,7 +18,6 @@ import (
multierror "github.com/hashicorp/go-multierror"
"github.com/opentofu/opentofu/internal/flock"
"github.com/opentofu/opentofu/internal/legacy/tofu"
"github.com/opentofu/opentofu/internal/states/statemgr"
)
@@ -43,23 +42,21 @@ type LocalState struct {
// hurt to remove file we never wrote to.
created bool
mu sync.Mutex
state *tofu.State
readState *tofu.State
written bool
mu sync.Mutex
state *CLIState
written bool
}
// SetState will force a specific state in-memory for this local state.
func (s *LocalState) SetState(state *tofu.State) {
func (s *LocalState) SetState(state *CLIState) {
s.mu.Lock()
defer s.mu.Unlock()
s.state = state.DeepCopy()
s.readState = state.DeepCopy()
}
// StateReader impl.
func (s *LocalState) State() *tofu.State {
func (s *LocalState) State() *CLIState {
return s.state.DeepCopy()
}
@@ -69,7 +66,7 @@ func (s *LocalState) State() *tofu.State {
// the original.
//
// StateWriter impl.
func (s *LocalState) WriteState(state *tofu.State) error {
func (s *LocalState) WriteState(state *CLIState) error {
s.mu.Lock()
defer s.mu.Unlock()
@@ -81,21 +78,14 @@ func (s *LocalState) WriteState(state *tofu.State) error {
// Sync after write
return s.stateFileOut.Sync()
}
func (s *LocalState) writeState(state *tofu.State) error {
func (s *LocalState) writeState(state *CLIState) error {
if s.stateFileOut == nil {
if err := s.createStateFiles(); err != nil {
return err
}
}
s.state = state.DeepCopy() // don't want mutations before we actually get this written to disk
if s.readState != nil && s.state != nil {
// We don't trust callers to properly manage serials. Instead, we assume
// that a WriteState is always for the next version after what was
// most recently read.
s.state.Serial = s.readState.Serial
}
s.state = state.DeepCopy()
if _, err := s.stateFileOut.Seek(0, io.SeekStart); err != nil {
return err
@@ -105,15 +95,10 @@ func (s *LocalState) writeState(state *tofu.State) error {
}
if state == nil {
// if we have no state, don't write anything else.
return nil
}
if !s.state.MarshalEqual(s.readState) {
s.state.Serial++
}
if err := tofu.WriteState(s.state, s.stateFileOut); err != nil {
if err := WriteState(s.state, s.stateFileOut); err != nil {
return err
}
@@ -176,14 +161,13 @@ func (s *LocalState) RefreshState(_ context.Context) error {
reader = s.stateFileOut
}
state, err := tofu.ReadState(reader)
state, err := ReadState(reader)
// if there's no state we just assign the nil return value
if err != nil && err != tofu.ErrNoState {
if err != nil && err != ErrNoState {
return err
}
s.state = state
s.readState = s.state.DeepCopy()
return nil
}

View File

@@ -35,6 +35,7 @@ import (
"github.com/opentofu/opentofu/internal/addrs"
backendInit "github.com/opentofu/opentofu/internal/backend/init"
backendLocal "github.com/opentofu/opentofu/internal/backend/local"
"github.com/opentofu/opentofu/internal/command/clistate"
"github.com/opentofu/opentofu/internal/command/views"
"github.com/opentofu/opentofu/internal/command/workdir"
"github.com/opentofu/opentofu/internal/configs"
@@ -45,7 +46,6 @@ import (
"github.com/opentofu/opentofu/internal/encryption"
"github.com/opentofu/opentofu/internal/getproviders"
"github.com/opentofu/opentofu/internal/initwd"
legacy "github.com/opentofu/opentofu/internal/legacy/tofu"
_ "github.com/opentofu/opentofu/internal/logging"
"github.com/opentofu/opentofu/internal/plans"
"github.com/opentofu/opentofu/internal/plans/planfile"
@@ -451,7 +451,7 @@ func testStateFileWorkspaceDefault(t *testing.T, workspace string, s *states.Sta
// testStateFileRemote writes the state out to the remote statefile
// in the cwd. Use `testCwd` to change into a temp cwd.
func testStateFileRemote(t *testing.T, s *legacy.State) string {
func testStateFileRemote(t *testing.T, s *clistate.CLIState) string {
t.Helper()
path := filepath.Join(workdir.DefaultDataDir, arguments.DefaultStateFilename)
@@ -465,7 +465,7 @@ func testStateFileRemote(t *testing.T, s *legacy.State) string {
}
defer f.Close()
if err := legacy.WriteState(s, f); err != nil {
if err := clistate.WriteState(s, f); err != nil {
t.Fatalf("err: %s", err)
}
@@ -493,9 +493,9 @@ func testStateRead(t *testing.T, path string) *states.State {
// testDataStateRead reads a "data state", which is a file format resembling
// our state format v3 that is used only to track current backend settings.
//
// This old format still uses *legacy.State, but should be replaced with
// a more specialized type in a later release.
func testDataStateRead(t *testing.T, path string) *legacy.State {
// This uses *clistate.CLIState which is the specialized type for
// tracking backend configuration.
func testDataStateRead(t *testing.T, path string) *clistate.CLIState {
t.Helper()
f, err := os.Open(path)
@@ -504,7 +504,7 @@ func testDataStateRead(t *testing.T, path string) *legacy.State {
}
defer f.Close()
s, err := legacy.ReadState(f)
s, err := clistate.ReadState(f)
if err != nil {
t.Fatalf("err: %s", err)
}
@@ -671,7 +671,7 @@ func testInputMap(t *testing.T, answers map[string]string) func() {
// be returned about the backend configuration having changed and that
// "tofu init" must be run, since the test backend config cache created
// by this function contains the hash for an empty configuration.
func testBackendState(t *testing.T, s *states.State, c int) (*legacy.State, *httptest.Server) {
func testBackendState(t *testing.T, s *states.State, c int) (*clistate.CLIState, *httptest.Server) {
t.Helper()
var b64md5 string
@@ -715,8 +715,8 @@ func testBackendState(t *testing.T, s *states.State, c int) (*legacy.State, *htt
configSchema := b.ConfigSchema()
hash, _ := backendConfig.Hash(t.Context(), configSchema)
state := legacy.NewState()
state.Backend = &legacy.BackendState{
state := clistate.NewState()
state.Backend = &clistate.BackendState{
Type: "http",
ConfigRaw: json.RawMessage(fmt.Sprintf(`{"address":%q}`, srv.URL)),
Hash: uint64(hash),
@@ -726,12 +726,12 @@ func testBackendState(t *testing.T, s *states.State, c int) (*legacy.State, *htt
}
// testRemoteState is used to make a test HTTP server to return a given
// state file that can be used for testing legacy remote state.
// state file that can be used for testing remote backend state.
//
// The return values are a *legacy.State instance that should be written
// The return values are a *clistate.CLIState instance that should be written
// as the "data state" (really: backend state) and the server that the
// returned data state refers to.
func testRemoteState(t *testing.T, s *states.State, c int) (*legacy.State, *httptest.Server) {
func testRemoteState(t *testing.T, s *states.State, c int) (*clistate.CLIState, *httptest.Server) {
t.Helper()
var b64md5 string
@@ -753,10 +753,10 @@ func testRemoteState(t *testing.T, s *states.State, c int) (*legacy.State, *http
}
}
retState := legacy.NewState()
retState := clistate.NewState()
srv := httptest.NewServer(http.HandlerFunc(cb))
b := &legacy.BackendState{
b := &clistate.BackendState{
Type: "http",
}
if err := b.SetConfig(cty.ObjectVal(map[string]cty.Value{

View File

@@ -39,7 +39,7 @@ import (
"github.com/opentofu/opentofu/internal/configs/configload"
"github.com/opentofu/opentofu/internal/getmodules"
"github.com/opentofu/opentofu/internal/getproviders"
legacy "github.com/opentofu/opentofu/internal/legacy/tofu"
"github.com/opentofu/opentofu/internal/command/clistate"
"github.com/opentofu/opentofu/internal/plugins"
"github.com/opentofu/opentofu/internal/providers"
"github.com/opentofu/opentofu/internal/provisioners"
@@ -226,7 +226,7 @@ type Meta struct {
configLoader *configload.Loader
// backendState is the currently active backend state
backendState *legacy.BackendState
backendState *clistate.BackendState
// Variables for the context (private)
variableArgs flags.RawFlags

View File

@@ -38,7 +38,6 @@ import (
backendInit "github.com/opentofu/opentofu/internal/backend/init"
backendLocal "github.com/opentofu/opentofu/internal/backend/local"
legacy "github.com/opentofu/opentofu/internal/legacy/tofu"
)
// BackendOpts are the options used to initialize a backend.Backend.
@@ -218,7 +217,7 @@ func (m *Meta) Backend(ctx context.Context, opts *BackendOpts, enc encryption.St
// with inside backendFromConfig, because we still need that codepath
// to be able to recognize the lack of a config as distinct from
// explicitly setting local until we do some more refactoring here.
m.backendState = &legacy.BackendState{
m.backendState = &clistate.BackendState{
Type: "local",
ConfigRaw: json.RawMessage("{}"),
}
@@ -561,10 +560,7 @@ func (m *Meta) backendConfig(ctx context.Context, opts *BackendOpts) (*configs.B
// This function handles various edge cases around backend config loading. For
// example: new config changes, backend type changes, etc.
//
// As of the 0.12 release it can no longer migrate from legacy remote state
// to backends, and will instead instruct users to use 0.11 or earlier as
// a stepping-stone to do that migration.
//
// Legacy remote state is no longer supported.
// This function may query the user for input unless input is disabled, in
// which case this function will error.
func (m *Meta) backendFromConfig(ctx context.Context, opts *BackendOpts, enc encryption.StateEncryption) (backend.Backend, tfdiags.Diagnostics) {
@@ -581,19 +577,15 @@ func (m *Meta) backendFromConfig(ctx context.Context, opts *BackendOpts, enc enc
// ------------------------------------------------------------------------
// For historical reasons, current backend configuration for a working
// directory is kept in a *state-like* file, using the legacy state
// structures in the OpenTofu package. It is not actually a OpenTofu
// state, and so only the "backend" portion of it is actually used.
// directory is kept in a *state-like* file using clistate.CLIState.
// It is not actually an OpenTofu resource state, and so only the
// "backend" portion of it is actually used.
//
// The remainder of this code often confusingly refers to this as a "state",
// so it's unfortunately important to remember that this is not actually
// what we _usually_ think of as "state", and is instead a local working
// directory "backend configuration state" that is never persisted anywhere.
//
// Since the "real" state has since moved on to be represented by
// states.State, we can recognize the special meaning of state that applies
// to this function and its callees by their continued use of the
// otherwise-obsolete tofu.State.
// directory "backend configuration state" that is never persisted anywhere
// except the .terraform directory.
// ------------------------------------------------------------------------
// Get the path to where we store a local cache of backend configuration
@@ -610,11 +602,11 @@ func (m *Meta) backendFromConfig(ctx context.Context, opts *BackendOpts, enc enc
s := sMgr.State()
if s == nil {
log.Printf("[TRACE] Meta.Backend: backend has not previously been initialized in this working directory")
s = legacy.NewState()
s = clistate.NewState()
} else if s.Backend != nil {
log.Printf("[TRACE] Meta.Backend: working directory was previously initialized for %q backend", s.Backend.Type)
} else {
log.Printf("[TRACE] Meta.Backend: working directory was previously initialized but has no backend (is using legacy remote state?)")
log.Printf("[TRACE] Meta.Backend: working directory was previously initialized but has no backend configuration")
}
// if we want to force reconfiguration of the backend, we set the backend
@@ -633,17 +625,6 @@ func (m *Meta) backendFromConfig(ctx context.Context, opts *BackendOpts, enc enc
}
}()
if !s.Remote.Empty() {
// Legacy remote state is no longer supported. User must first
// migrate with Terraform 0.11 or earlier.
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Legacy remote state not supported",
"This working directory is configured for legacy remote state, which is no longer supported from Terraform v0.12 onwards, and thus not supported by OpenTofu, either. To migrate this environment, first run \"terraform init\" under a Terraform 0.11 release, and then upgrade to OpenTofu.",
))
return nil, diags
}
// This switch statement covers all the different combinations of
// configuring new backends, updating previously-configured backends, etc.
switch {
@@ -841,7 +822,7 @@ func (m *Meta) backendFromState(ctx context.Context, enc encryption.StateEncrypt
}
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?)")
log.Printf("[TRACE] Meta.Backend: working directory was previously initialized but has no backend configuration")
return backendLocal.New(enc), diags
}
log.Printf("[TRACE] Meta.Backend: working directory was previously initialized for %q backend", s.Backend.Type)
@@ -1129,9 +1110,9 @@ func (m *Meta) backend_C_r_s(ctx context.Context, c *configs.Backend, cHash int,
// Store the metadata in our saved state location
s := sMgr.State()
if s == nil {
s = legacy.NewState()
s = clistate.NewState()
}
s.Backend = &legacy.BackendState{
s.Backend = &clistate.BackendState{
Type: c.Type,
ConfigRaw: json.RawMessage(configJSON),
Hash: uint64(cHash),
@@ -1277,9 +1258,9 @@ func (m *Meta) backend_C_r_S_changed(ctx context.Context, c *configs.Backend, cH
// Update the backend state
s = sMgr.State()
if s == nil {
s = legacy.NewState()
s = clistate.NewState()
}
s.Backend = &legacy.BackendState{
s.Backend = &clistate.BackendState{
Type: c.Type,
ConfigRaw: json.RawMessage(configJSON),
Hash: uint64(cHash),
@@ -1408,7 +1389,7 @@ func (m *Meta) updateSavedBackendHash(cHash int, sMgr *clistate.LocalState) tfdi
// this function will conservatively assume that migration is required,
// expecting that the migration code will subsequently deal with the same
// errors.
func (m *Meta) backendConfigNeedsMigration(ctx context.Context, c *configs.Backend, s *legacy.BackendState) bool {
func (m *Meta) backendConfigNeedsMigration(ctx context.Context, c *configs.Backend, s *clistate.BackendState) bool {
if s == nil || s.Empty() {
log.Print("[TRACE] backendConfigNeedsMigration: no cached config, so migration is required")
return true

View File

@@ -20,9 +20,8 @@ import (
"github.com/opentofu/opentofu/internal/backend/remote-state/inmem"
"github.com/opentofu/opentofu/internal/encryption"
"github.com/opentofu/opentofu/internal/states"
"github.com/opentofu/opentofu/internal/states/statefile"
"github.com/opentofu/opentofu/internal/states/statemgr"
legacy "github.com/opentofu/opentofu/internal/legacy/tofu"
)
func TestWorkspace_createAndChange(t *testing.T) {
@@ -447,28 +446,35 @@ func TestWorkspace_deleteWithState(t *testing.T) {
t.Fatal(err)
}
// create a non-empty state
originalState := &legacy.State{
Modules: []*legacy.ModuleState{
{
Path: []string{"root"},
Resources: map[string]*legacy.ResourceState{
"test_instance.foo": {
Type: "test_instance",
Primary: &legacy.InstanceState{
ID: "bar",
},
},
},
originalState := states.BuildState(func(s *states.SyncState) {
s.SetResourceInstanceCurrent(
addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_instance",
Name: "foo",
}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
&states.ResourceInstanceObjectSrc{
AttrsJSON: []byte(`{"id":"bar"}`),
Status: states.ObjectReady,
},
},
}
addrs.AbsProviderConfig{
Provider: addrs.NewDefaultProvider("test"),
Module: addrs.RootModule,
},
addrs.NoKey,
)
})
f, err := os.Create(filepath.Join(local.DefaultWorkspaceDir, "test", "terraform.tfstate"))
if err != nil {
t.Fatal(err)
}
if err := legacy.WriteState(originalState, f); err != nil {
err = statefile.Write(&statefile.File{
Serial: 0,
Lineage: "test-lineage",
State: originalState,
}, f, encryption.StateEncryptionDisabled())
if err != nil {
t.Fatal(err)
}
f.Close()