mirror of
https://github.com/opentofu/opentofu.git
synced 2025-02-25 18:45:20 -06:00
update local.Local to match the latest Backend
Update the methods, remove the handling of "current", and make tests pass.
This commit is contained in:
parent
96194fbc0d
commit
65527f35a4
@ -123,6 +123,9 @@ type Operation struct {
|
||||
// If LockState is true, the Operation must Lock any
|
||||
// state.Lockers for its duration, and Unlock when complete.
|
||||
LockState bool
|
||||
|
||||
// Environment is the named state that should be loaded from the Backend.
|
||||
Environment string
|
||||
}
|
||||
|
||||
// RunningOperation is the result of starting an operation.
|
||||
|
@ -53,8 +53,10 @@ type Local struct {
|
||||
StateOutPath string
|
||||
StateBackupPath string
|
||||
|
||||
// we only want to create a single instance of the local state
|
||||
state state.State
|
||||
// We only want to create a single instance of a local state, so store them
|
||||
// here as they're loaded.
|
||||
states map[string]state.State
|
||||
|
||||
// Terraform context. Many of these will be overridden or merged by
|
||||
// Operation. See Operation for more details.
|
||||
ContextOpts *terraform.ContextOpts
|
||||
@ -78,10 +80,6 @@ type Local struct {
|
||||
schema *schema.Backend
|
||||
opLock sync.Mutex
|
||||
once sync.Once
|
||||
|
||||
// workingDir is where the State* paths should be relative to.
|
||||
// This is currently only used for tests.
|
||||
workingDir string
|
||||
}
|
||||
|
||||
func (b *Local) Input(
|
||||
@ -118,54 +116,35 @@ func (b *Local) Configure(c *terraform.ResourceConfig) error {
|
||||
return f(c)
|
||||
}
|
||||
|
||||
func (b *Local) States() ([]string, string, error) {
|
||||
func (b *Local) States() ([]string, error) {
|
||||
// If we have a backend handling state, defer to that.
|
||||
if b.Backend != nil {
|
||||
if b, ok := b.Backend.(backend.MultiState); ok {
|
||||
return b.States()
|
||||
} else {
|
||||
return nil, "", ErrEnvNotSupported
|
||||
}
|
||||
return b.Backend.States()
|
||||
}
|
||||
|
||||
// the listing always start with "default"
|
||||
envs := []string{backend.DefaultStateName}
|
||||
|
||||
current, err := b.currentStateName()
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
entries, err := ioutil.ReadDir(filepath.Join(b.workingDir, DefaultEnvDir))
|
||||
entries, err := ioutil.ReadDir(DefaultEnvDir)
|
||||
// no error if there's no envs configured
|
||||
if os.IsNotExist(err) {
|
||||
return envs, backend.DefaultStateName, nil
|
||||
return envs, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
currentExists := false
|
||||
var listed []string
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
name := filepath.Base(entry.Name())
|
||||
if name == current {
|
||||
currentExists = true
|
||||
}
|
||||
listed = append(listed, name)
|
||||
listed = append(listed, filepath.Base(entry.Name()))
|
||||
}
|
||||
}
|
||||
|
||||
// current was out of sync for some reason, so return defualt
|
||||
if !currentExists {
|
||||
current = backend.DefaultStateName
|
||||
}
|
||||
|
||||
sort.Strings(listed)
|
||||
envs = append(envs, listed...)
|
||||
|
||||
return envs, current, nil
|
||||
return envs, nil
|
||||
}
|
||||
|
||||
// DeleteState removes a named state.
|
||||
@ -173,11 +152,7 @@ func (b *Local) States() ([]string, string, error) {
|
||||
func (b *Local) DeleteState(name string) error {
|
||||
// If we have a backend handling state, defer to that.
|
||||
if b.Backend != nil {
|
||||
if b, ok := b.Backend.(backend.MultiState); ok {
|
||||
return b.DeleteState(name)
|
||||
} else {
|
||||
return ErrEnvNotSupported
|
||||
}
|
||||
return b.Backend.DeleteState(name)
|
||||
}
|
||||
|
||||
if name == "" {
|
||||
@ -188,91 +163,25 @@ func (b *Local) DeleteState(name string) error {
|
||||
return errors.New("cannot delete default state")
|
||||
}
|
||||
|
||||
_, current, err := b.States()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// if we're deleting the current state, we change back to the default
|
||||
if name == current {
|
||||
if err := b.ChangeState(backend.DefaultStateName); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return os.RemoveAll(filepath.Join(b.workingDir, DefaultEnvDir, name))
|
||||
delete(b.states, name)
|
||||
return os.RemoveAll(filepath.Join(DefaultEnvDir, name))
|
||||
}
|
||||
|
||||
// Change to the named state, creating it if it doesn't exist.
|
||||
func (b *Local) ChangeState(name string) error {
|
||||
func (b *Local) State(name string) (state.State, error) {
|
||||
// If we have a backend handling state, defer to that.
|
||||
if b.Backend != nil {
|
||||
if b, ok := b.Backend.(backend.MultiState); ok {
|
||||
return b.ChangeState(name)
|
||||
} else {
|
||||
return ErrEnvNotSupported
|
||||
}
|
||||
return b.Backend.State(name)
|
||||
}
|
||||
|
||||
name = strings.TrimSpace(name)
|
||||
if name == "" {
|
||||
return errors.New("state name cannot be empty")
|
||||
if s, ok := b.states[name]; ok {
|
||||
return s, nil
|
||||
}
|
||||
|
||||
envs, current, err := b.States()
|
||||
if err != nil {
|
||||
return err
|
||||
if err := b.createState(name); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if name == current {
|
||||
return nil
|
||||
}
|
||||
|
||||
exists := false
|
||||
for _, env := range envs {
|
||||
if env == name {
|
||||
exists = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !exists {
|
||||
if err := b.createState(name); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
err = os.MkdirAll(filepath.Join(b.workingDir, DefaultDataDir), 0755)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = ioutil.WriteFile(
|
||||
filepath.Join(b.workingDir, DefaultDataDir, DefaultEnvFile),
|
||||
[]byte(name),
|
||||
0644,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// remove the current state so it's reloaded on the next call to State
|
||||
b.state = nil
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *Local) State() (state.State, error) {
|
||||
// If we have a backend handling state, defer to that.
|
||||
if b.Backend != nil {
|
||||
return b.Backend.State()
|
||||
}
|
||||
|
||||
if b.state != nil {
|
||||
return b.state, nil
|
||||
}
|
||||
|
||||
statePath, stateOutPath, backupPath, err := b.StatePaths()
|
||||
statePath, stateOutPath, backupPath, err := b.StatePaths(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -291,7 +200,10 @@ func (b *Local) State() (state.State, error) {
|
||||
}
|
||||
}
|
||||
|
||||
b.state = s
|
||||
if b.states == nil {
|
||||
b.states = map[string]state.State{}
|
||||
}
|
||||
b.states[name] = s
|
||||
return s, nil
|
||||
}
|
||||
|
||||
@ -385,20 +297,24 @@ func (b *Local) schemaConfigure(ctx context.Context) error {
|
||||
}
|
||||
|
||||
// StatePaths returns the StatePath, StateOutPath, and StateBackupPath as
|
||||
// configured by the current environment. If backups are disabled,
|
||||
// StateBackupPath will be an empty string.
|
||||
func (b *Local) StatePaths() (string, string, string, error) {
|
||||
// configured from the CLI.
|
||||
func (b *Local) StatePaths(name string) (string, string, string, error) {
|
||||
statePath := b.StatePath
|
||||
stateOutPath := b.StateOutPath
|
||||
backupPath := b.StateBackupPath
|
||||
|
||||
if statePath == "" {
|
||||
path, err := b.statePath()
|
||||
if err != nil {
|
||||
return "", "", "", err
|
||||
}
|
||||
statePath = path
|
||||
if name == "" {
|
||||
name = backend.DefaultStateName
|
||||
}
|
||||
|
||||
if name == backend.DefaultStateName {
|
||||
if statePath == "" {
|
||||
statePath = name
|
||||
}
|
||||
} else {
|
||||
statePath = filepath.Join(DefaultEnvDir, name, DefaultStateFilename)
|
||||
}
|
||||
|
||||
if stateOutPath == "" {
|
||||
stateOutPath = statePath
|
||||
}
|
||||
@ -413,33 +329,21 @@ func (b *Local) StatePaths() (string, string, string, error) {
|
||||
return statePath, stateOutPath, backupPath, nil
|
||||
}
|
||||
|
||||
func (b *Local) statePath() (string, error) {
|
||||
_, current, err := b.States()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
path := DefaultStateFilename
|
||||
|
||||
if current != backend.DefaultStateName && current != "" {
|
||||
path = filepath.Join(b.workingDir, DefaultEnvDir, current, DefaultStateFilename)
|
||||
}
|
||||
return path, nil
|
||||
}
|
||||
|
||||
// this only ensures that the named directory exists
|
||||
func (b *Local) createState(name string) error {
|
||||
stateNames, _, err := b.States()
|
||||
if err != nil {
|
||||
return err
|
||||
if name == backend.DefaultStateName {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, n := range stateNames {
|
||||
if name == n {
|
||||
// state exists, nothing to do
|
||||
return nil
|
||||
}
|
||||
stateDir := filepath.Join(DefaultEnvDir, name)
|
||||
s, err := os.Stat(stateDir)
|
||||
if err == nil && s.IsDir() {
|
||||
// no need to check for os.IsNotExist, since that is covered by os.MkdirAll
|
||||
// which will catch the other possible errors as well.
|
||||
return nil
|
||||
}
|
||||
|
||||
err = os.MkdirAll(filepath.Join(b.workingDir, DefaultEnvDir, name), 0755)
|
||||
err = os.MkdirAll(stateDir, 0755)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -451,7 +355,7 @@ func (b *Local) createState(name string) error {
|
||||
// configuration files.
|
||||
// If there are no configured environments, currentStateName returns "default"
|
||||
func (b *Local) currentStateName() (string, error) {
|
||||
contents, err := ioutil.ReadFile(filepath.Join(b.workingDir, DefaultDataDir, DefaultEnvFile))
|
||||
contents, err := ioutil.ReadFile(filepath.Join(DefaultDataDir, DefaultEnvFile))
|
||||
if os.IsNotExist(err) {
|
||||
return backend.DefaultStateName, nil
|
||||
}
|
||||
|
@ -23,7 +23,7 @@ func (b *Local) Context(op *backend.Operation) (*terraform.Context, state.State,
|
||||
|
||||
func (b *Local) context(op *backend.Operation) (*terraform.Context, state.State, error) {
|
||||
// Get the state.
|
||||
s, err := b.State()
|
||||
s, err := b.State(op.Environment)
|
||||
if err != nil {
|
||||
return nil, nil, errwrap.Wrapf("Error loading state: {{err}}", err)
|
||||
}
|
||||
|
@ -1,15 +1,15 @@
|
||||
package local
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/terraform/backend"
|
||||
"github.com/hashicorp/terraform/state"
|
||||
"github.com/hashicorp/terraform/terraform"
|
||||
)
|
||||
|
||||
@ -17,7 +17,6 @@ func TestLocal_impl(t *testing.T) {
|
||||
var _ backend.Enhanced = new(Local)
|
||||
var _ backend.Local = new(Local)
|
||||
var _ backend.CLI = new(Local)
|
||||
var _ backend.MultiState = new(Local)
|
||||
}
|
||||
|
||||
func checkState(t *testing.T, path, expected string) {
|
||||
@ -46,31 +45,24 @@ func TestLocal_addAndRemoveStates(t *testing.T) {
|
||||
expectedStates := []string{dflt}
|
||||
|
||||
b := &Local{}
|
||||
states, current, err := b.States()
|
||||
states, err := b.States()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if current != dflt {
|
||||
t.Fatalf("expected %q, got %q", dflt, current)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(states, expectedStates) {
|
||||
t.Fatalf("expected []string{%q}, got %q", dflt, states)
|
||||
}
|
||||
|
||||
expectedA := "test_A"
|
||||
if err := b.ChangeState(expectedA); err != nil {
|
||||
if _, err := b.State(expectedA); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
states, current, err = b.States()
|
||||
states, err = b.States()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if current != expectedA {
|
||||
t.Fatalf("expected %q, got %q", expectedA, current)
|
||||
}
|
||||
|
||||
expectedStates = append(expectedStates, expectedA)
|
||||
if !reflect.DeepEqual(states, expectedStates) {
|
||||
@ -78,17 +70,14 @@ func TestLocal_addAndRemoveStates(t *testing.T) {
|
||||
}
|
||||
|
||||
expectedB := "test_B"
|
||||
if err := b.ChangeState(expectedB); err != nil {
|
||||
if _, err := b.State(expectedB); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
states, current, err = b.States()
|
||||
states, err = b.States()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if current != expectedB {
|
||||
t.Fatalf("expected %q, got %q", expectedB, current)
|
||||
}
|
||||
|
||||
expectedStates = append(expectedStates, expectedB)
|
||||
if !reflect.DeepEqual(states, expectedStates) {
|
||||
@ -99,13 +88,10 @@ func TestLocal_addAndRemoveStates(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
states, current, err = b.States()
|
||||
states, err = b.States()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if current != expectedB {
|
||||
t.Fatalf("expected %q, got %q", dflt, current)
|
||||
}
|
||||
|
||||
expectedStates = []string{dflt, expectedB}
|
||||
if !reflect.DeepEqual(states, expectedStates) {
|
||||
@ -116,13 +102,10 @@ func TestLocal_addAndRemoveStates(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
states, current, err = b.States()
|
||||
states, err = b.States()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if current != dflt {
|
||||
t.Fatalf("expected %q, got %q", dflt, current)
|
||||
}
|
||||
|
||||
expectedStates = []string{dflt}
|
||||
if !reflect.DeepEqual(states, expectedStates) {
|
||||
@ -134,97 +117,45 @@ func TestLocal_addAndRemoveStates(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// verify the behavior with a backend that doesn't support multiple states
|
||||
func TestLocal_noMultiStateBackend(t *testing.T) {
|
||||
type noMultiState struct {
|
||||
backend.Backend
|
||||
}
|
||||
// a local backend which return sentinel errors for NamedState methods to
|
||||
// verify it's being called.
|
||||
type testDelegateBackend struct {
|
||||
*Local
|
||||
}
|
||||
|
||||
b := &Local{
|
||||
Backend: &noMultiState{},
|
||||
}
|
||||
var errTestDelegateState = errors.New("State called")
|
||||
var errTestDelegateStates = errors.New("States called")
|
||||
var errTestDelegateDeleteState = errors.New("Delete called")
|
||||
|
||||
_, _, err := b.States()
|
||||
if err != ErrEnvNotSupported {
|
||||
t.Fatal("backend does not support environments.", err)
|
||||
}
|
||||
func (b *testDelegateBackend) State(name string) (state.State, error) {
|
||||
return nil, errTestDelegateState
|
||||
}
|
||||
|
||||
err = b.ChangeState("test")
|
||||
if err != ErrEnvNotSupported {
|
||||
t.Fatal("backend does not support environments.", err)
|
||||
}
|
||||
func (b *testDelegateBackend) States() ([]string, error) {
|
||||
return nil, errTestDelegateStates
|
||||
}
|
||||
|
||||
err = b.ChangeState("test")
|
||||
if err != ErrEnvNotSupported {
|
||||
t.Fatal("backend does not support environments.", err)
|
||||
}
|
||||
func (b *testDelegateBackend) DeleteState(name string) error {
|
||||
return errTestDelegateDeleteState
|
||||
}
|
||||
|
||||
// verify that the MultiState methods are dispatched to the correct Backend.
|
||||
func TestLocal_multiStateBackend(t *testing.T) {
|
||||
defer testTmpDir(t)()
|
||||
|
||||
dflt := backend.DefaultStateName
|
||||
expectedStates := []string{dflt}
|
||||
|
||||
// make a second tmp dir for the sub-Backend.
|
||||
// we verify the corret backend was called by checking the paths.
|
||||
tmp, err := ioutil.TempDir("", "tf")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tmp)
|
||||
|
||||
fmt.Println("second tmp:", tmp)
|
||||
|
||||
// assign a separate backend where we can read the state
|
||||
b := &Local{
|
||||
Backend: &Local{
|
||||
workingDir: tmp,
|
||||
},
|
||||
Backend: &testDelegateBackend{},
|
||||
}
|
||||
|
||||
testA := "test_A"
|
||||
if err := b.ChangeState(testA); err != nil {
|
||||
t.Fatal(err)
|
||||
if _, err := b.State("test"); err != errTestDelegateState {
|
||||
t.Fatal("expected errTestDelegateState, got:", err)
|
||||
}
|
||||
|
||||
states, current, err := b.States()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if current != testA {
|
||||
t.Fatalf("expected %q, got %q", testA, current)
|
||||
if _, err := b.States(); err != errTestDelegateStates {
|
||||
t.Fatal("expected errTestDelegateStates, got:", err)
|
||||
}
|
||||
|
||||
expectedStates = append(expectedStates, testA)
|
||||
if !reflect.DeepEqual(states, expectedStates) {
|
||||
t.Fatalf("expected %q, got %q", expectedStates, states)
|
||||
}
|
||||
|
||||
// verify that no environment paths were created for the top-level Backend
|
||||
if _, err := os.Stat(DefaultDataDir); !os.IsNotExist(err) {
|
||||
t.Fatal("remote state operations should not have written local files")
|
||||
}
|
||||
|
||||
if _, err := os.Stat(filepath.Join(DefaultEnvDir, testA)); !os.IsNotExist(err) {
|
||||
t.Fatal("remote state operations should not have written local files")
|
||||
}
|
||||
|
||||
// remove the new state
|
||||
if err := b.DeleteState(testA); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
states, current, err = b.States()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if current != dflt {
|
||||
t.Fatalf("expected %q, got %q", dflt, current)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(states, expectedStates[:1]) {
|
||||
t.Fatalf("expected %q, got %q", expectedStates, states)
|
||||
if err := b.DeleteState("test"); err != errTestDelegateDeleteState {
|
||||
t.Fatal("expected errTestDelegateDeleteState, got:", err)
|
||||
}
|
||||
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user