Merge pull request #11187 from hashicorp/jbardin/state-locking

State Locking initial implementations
This commit is contained in:
James Bardin 2017-02-01 14:35:55 -05:00 committed by GitHub
commit 9acb86a182
15 changed files with 973 additions and 197 deletions

View File

@ -1,6 +1,7 @@
package command
import (
"io/ioutil"
"os"
"path/filepath"
"strings"
@ -37,21 +38,35 @@ func TestMetaBackend_emptyDir(t *testing.T) {
}
// Verify it exists where we expect it to
if _, err := os.Stat(DefaultStateFilename); err != nil {
t.Fatalf("err: %s", err)
if isEmptyState(DefaultStateFilename) {
t.Fatalf("no state was written")
}
// Verify no backup since it was empty to start
if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err == nil {
t.Fatalf("err: %s", err)
if !isEmptyState(DefaultStateFilename + DefaultBackupExtension) {
t.Fatal("backup state should be empty")
}
// Verify no backend state was made
if _, err := os.Stat(filepath.Join(m.DataDir(), DefaultStateFilename)); err == nil {
t.Fatalf("err: %s", err)
if !isEmptyState(filepath.Join(m.DataDir(), DefaultStateFilename)) {
t.Fatal("backend state should be empty")
}
}
// check for no state. Either the file doesn't exist, or is empty
func isEmptyState(path string) bool {
fi, err := os.Stat(path)
if os.IsNotExist(err) {
return true
}
if fi.Size() == 0 {
return true
}
return false
}
// Test a directory with a legacy state and no config continues to
// use the legacy state.
func TestMetaBackend_emptyWithDefaultState(t *testing.T) {
@ -95,8 +110,10 @@ func TestMetaBackend_emptyWithDefaultState(t *testing.T) {
if _, err := os.Stat(DefaultStateFilename); err != nil {
t.Fatalf("err: %s", err)
}
if _, err := os.Stat(filepath.Join(m.DataDir(), DefaultStateFilename)); err == nil {
t.Fatalf("err: %s", err)
stateName := filepath.Join(m.DataDir(), DefaultStateFilename)
if !isEmptyState(stateName) {
t.Fatal("expected no state at", stateName)
}
// Write some state
@ -108,8 +125,8 @@ func TestMetaBackend_emptyWithDefaultState(t *testing.T) {
}
// Verify a backup was made since we're modifying a pre-existing state
if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err != nil {
t.Fatalf("err: %s", err)
if isEmptyState(DefaultStateFilename + DefaultBackupExtension) {
t.Fatal("backup state should not be empty")
}
}
@ -161,10 +178,12 @@ func TestMetaBackend_emptyWithExplicitState(t *testing.T) {
// Verify neither defaults exist
if _, err := os.Stat(DefaultStateFilename); err == nil {
t.Fatalf("err: %s", err)
t.Fatal("file should not exist")
}
if _, err := os.Stat(filepath.Join(m.DataDir(), DefaultStateFilename)); err == nil {
t.Fatalf("err: %s", err)
stateName := filepath.Join(m.DataDir(), DefaultStateFilename)
if !isEmptyState(stateName) {
t.Fatal("expected no state at", stateName)
}
// Write some state
@ -176,8 +195,8 @@ func TestMetaBackend_emptyWithExplicitState(t *testing.T) {
}
// Verify a backup was made since we're modifying a pre-existing state
if _, err := os.Stat(statePath + DefaultBackupExtension); err != nil {
t.Fatalf("err: %s", err)
if isEmptyState(statePath + DefaultBackupExtension) {
t.Fatal("backup state should not be empty")
}
}
@ -224,15 +243,15 @@ func TestMetaBackend_emptyLegacyRemote(t *testing.T) {
// Verify the default paths don't exist
if _, err := os.Stat(DefaultStateFilename); err == nil {
t.Fatalf("err: %s", err)
t.Fatal("file should not exist")
}
// Verify a backup doesn't exist
if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err == nil {
t.Fatalf("err: %s", err)
t.Fatal("file should not exist")
}
if _, err := os.Stat(statePath + DefaultBackupExtension); err == nil {
t.Fatalf("err: %s", err)
t.Fatal("file should not exist")
}
}
@ -293,12 +312,12 @@ func TestMetaBackend_configureNew(t *testing.T) {
// Verify the default paths don't exist
if _, err := os.Stat(DefaultStateFilename); err == nil {
t.Fatalf("err: %s", err)
t.Fatal("file should not exist")
}
// Verify a backup doesn't exist
if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err == nil {
t.Fatalf("err: %s", err)
t.Fatal("file should not exist")
}
}
@ -364,13 +383,15 @@ func TestMetaBackend_configureNewWithState(t *testing.T) {
}
// Verify the default paths don't exist
if _, err := os.Stat(DefaultStateFilename); err == nil {
t.Fatalf("err: %s", err)
if !isEmptyState(DefaultStateFilename) {
data, _ := ioutil.ReadFile(DefaultStateFilename)
t.Fatal("state should not exist, but contains:\n", string(data))
}
// Verify a backup does exist
if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err != nil {
t.Fatalf("err: %s", err)
if isEmptyState(DefaultStateFilename + DefaultBackupExtension) {
t.Fatal("backup state is empty or missing")
}
}
@ -408,13 +429,15 @@ func TestMetaBackend_configureNewWithStateNoMigrate(t *testing.T) {
}
// Verify the default paths don't exist
if _, err := os.Stat(DefaultStateFilename); err == nil {
t.Fatalf("err: %s", err)
if !isEmptyState(DefaultStateFilename) {
data, _ := ioutil.ReadFile(DefaultStateFilename)
t.Fatal("state should not exist, but contains:\n", string(data))
}
// Verify a backup does exist
if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err != nil {
t.Fatalf("err: %s", err)
if isEmptyState(DefaultStateFilename + DefaultBackupExtension) {
t.Fatal("backup state is empty or missing")
}
}
@ -480,13 +503,15 @@ func TestMetaBackend_configureNewWithStateExisting(t *testing.T) {
}
// Verify the default paths don't exist
if _, err := os.Stat(DefaultStateFilename); err == nil {
t.Fatalf("err: %s", err)
if !isEmptyState(DefaultStateFilename) {
data, _ := ioutil.ReadFile(DefaultStateFilename)
t.Fatal("state should not exist, but contains:\n", string(data))
}
// Verify a backup does exist
if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err != nil {
t.Fatalf("err: %s", err)
if isEmptyState(DefaultStateFilename + DefaultBackupExtension) {
t.Fatal("backup state is empty or missing")
}
}
@ -552,13 +577,15 @@ func TestMetaBackend_configureNewWithStateExistingNoMigrate(t *testing.T) {
}
// Verify the default paths don't exist
if _, err := os.Stat(DefaultStateFilename); err == nil {
t.Fatalf("err: %s", err)
if !isEmptyState(DefaultStateFilename) {
data, _ := ioutil.ReadFile(DefaultStateFilename)
t.Fatal("state should not exist, but contains:\n", string(data))
}
// Verify a backup does exist
if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err != nil {
t.Fatalf("err: %s", err)
if isEmptyState(DefaultStateFilename + DefaultBackupExtension) {
t.Fatal("backup state is empty or missing")
}
}
@ -642,13 +669,17 @@ func TestMetaBackend_configureNewLegacy(t *testing.T) {
}
// Verify the default paths don't exist
if _, err := os.Stat(DefaultStateFilename); err == nil {
t.Fatalf("err: %s", err)
if !isEmptyState(DefaultStateFilename) {
data, _ := ioutil.ReadFile(DefaultStateFilename)
t.Fatal("state should not exist, but contains:\n", string(data))
}
// Verify a backup doesn't exist
if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err == nil {
t.Fatalf("err: %s", err)
if !isEmptyState(DefaultStateFilename + DefaultBackupExtension) {
data, _ := ioutil.ReadFile(DefaultStateFilename)
t.Fatal("backup should be empty, but contains:\n", string(data))
}
}
@ -736,12 +767,12 @@ func TestMetaBackend_configureNewLegacyCopy(t *testing.T) {
// Verify the default paths don't exist
if _, err := os.Stat(DefaultStateFilename); err == nil {
t.Fatalf("err: %s", err)
t.Fatal("file should not exist")
}
// Verify a backup doesn't exist
if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err == nil {
t.Fatalf("err: %s", err)
t.Fatal("file should not exist")
}
}
@ -776,12 +807,12 @@ func TestMetaBackend_configuredUnchanged(t *testing.T) {
// Verify the default paths don't exist
if _, err := os.Stat(DefaultStateFilename); err == nil {
t.Fatalf("err: %s", err)
t.Fatal("file should not exist")
}
// Verify a backup doesn't exist
if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err == nil {
t.Fatalf("err: %s", err)
t.Fatal("file should not exist")
}
}
@ -820,12 +851,12 @@ func TestMetaBackend_configuredChange(t *testing.T) {
// Verify the default paths don't exist
if _, err := os.Stat(DefaultStateFilename); err == nil {
t.Fatalf("err: %s", err)
t.Fatal("file should not exist")
}
// Verify a backup doesn't exist
if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err == nil {
t.Fatalf("err: %s", err)
t.Fatal("file should not exist")
}
// Write some state
@ -855,12 +886,12 @@ func TestMetaBackend_configuredChange(t *testing.T) {
// Verify no local state
if _, err := os.Stat(DefaultStateFilename); err == nil {
t.Fatalf("err: %s", err)
t.Fatal("file should not exist")
}
// Verify no local backup
if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err == nil {
t.Fatalf("err: %s", err)
t.Fatal("file should not exist")
}
}
@ -902,12 +933,12 @@ func TestMetaBackend_configuredChangeCopy(t *testing.T) {
// Verify no local state
if _, err := os.Stat(DefaultStateFilename); err == nil {
t.Fatalf("err: %s", err)
t.Fatal("file should not exist")
}
// Verify no local backup
if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err == nil {
t.Fatalf("err: %s", err)
t.Fatal("file should not exist")
}
}
@ -945,13 +976,15 @@ func TestMetaBackend_configuredUnset(t *testing.T) {
}
// Verify the default paths don't exist
if _, err := os.Stat(DefaultStateFilename); err == nil {
t.Fatalf("err: %s", err)
if !isEmptyState(DefaultStateFilename) {
data, _ := ioutil.ReadFile(DefaultStateFilename)
t.Fatal("state should not exist, but contains:\n", string(data))
}
// Verify a backup doesn't exist
if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err == nil {
t.Fatalf("err: %s", err)
if !isEmptyState(DefaultStateFilename + DefaultBackupExtension) {
data, _ := ioutil.ReadFile(DefaultStateFilename + DefaultBackupExtension)
t.Fatal("backup should not exist, but contains:\n", string(data))
}
// Verify we have no configured backend/legacy
@ -982,13 +1015,14 @@ func TestMetaBackend_configuredUnset(t *testing.T) {
}
// Verify it exists where we expect it to
if _, err := os.Stat(DefaultStateFilename); err != nil {
t.Fatalf("err: %s", err)
if isEmptyState(DefaultStateFilename) {
t.Fatal(DefaultStateFilename, "is empty")
}
// Verify no backup since it was empty to start
if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err == nil {
t.Fatalf("err: %s", err)
if !isEmptyState(DefaultStateFilename + DefaultBackupExtension) {
data, _ := ioutil.ReadFile(DefaultStateFilename + DefaultBackupExtension)
t.Fatal("backup state should be empty, but contains:\n", string(data))
}
}
@ -1029,8 +1063,8 @@ func TestMetaBackend_configuredUnsetCopy(t *testing.T) {
}
// Verify a backup doesn't exist
if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err == nil {
t.Fatalf("err: %s", err)
if !isEmptyState(DefaultStateFilename + DefaultBackupExtension) {
t.Fatalf("backup state should be empty")
}
// Verify we have no configured backend/legacy
@ -1066,8 +1100,8 @@ func TestMetaBackend_configuredUnsetCopy(t *testing.T) {
}
// Verify a backup since it wasn't empty to start
if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err != nil {
t.Fatalf("err: %s", err)
if isEmptyState(DefaultStateFilename + DefaultBackupExtension) {
t.Fatal("backup is empty")
}
}
@ -1114,7 +1148,7 @@ func TestMetaBackend_configuredUnchangedLegacy(t *testing.T) {
// Verify a backup doesn't exist
if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err == nil {
t.Fatalf("err: %s", err)
t.Fatal("file should not exist")
}
// Verify we have no configured legacy
@ -1165,12 +1199,12 @@ func TestMetaBackend_configuredUnchangedLegacy(t *testing.T) {
// Verify no local state
if _, err := os.Stat(DefaultStateFilename); err == nil {
t.Fatalf("err: %s", err)
t.Fatal("file should not exist")
}
// Verify no local backup
if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err == nil {
t.Fatalf("err: %s", err)
t.Fatal("file should not exist")
}
}
@ -1212,12 +1246,12 @@ func TestMetaBackend_configuredUnchangedLegacyCopy(t *testing.T) {
// Verify the default paths don't exist
if _, err := os.Stat(DefaultStateFilename); err == nil {
t.Fatalf("err: %s", err)
t.Fatal("file should not exist")
}
// Verify a backup doesn't exist
if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err == nil {
t.Fatalf("err: %s", err)
t.Fatal("file should not exist")
}
// Verify we have no configured legacy
@ -1268,12 +1302,12 @@ func TestMetaBackend_configuredUnchangedLegacyCopy(t *testing.T) {
// Verify no local state
if _, err := os.Stat(DefaultStateFilename); err == nil {
t.Fatalf("err: %s", err)
t.Fatal("file should not exist")
}
// Verify no local backup
if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err == nil {
t.Fatalf("err: %s", err)
t.Fatal("file should not exist")
}
}
@ -1312,12 +1346,12 @@ func TestMetaBackend_configuredChangedLegacy(t *testing.T) {
// Verify the default paths don't exist
if _, err := os.Stat(DefaultStateFilename); err == nil {
t.Fatalf("err: %s", err)
t.Fatal("file should not exist")
}
// Verify a backup doesn't exist
if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err == nil {
t.Fatalf("err: %s", err)
t.Fatal("file should not exist")
}
// Verify we have no configured legacy
@ -1368,12 +1402,12 @@ func TestMetaBackend_configuredChangedLegacy(t *testing.T) {
// Verify no local state
if _, err := os.Stat(DefaultStateFilename); err == nil {
t.Fatalf("err: %s", err)
t.Fatal("file should not exist")
}
// Verify no local backup
if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err == nil {
t.Fatalf("err: %s", err)
t.Fatal("file should not exist")
}
}
@ -1415,12 +1449,12 @@ func TestMetaBackend_configuredChangedLegacyCopyBackend(t *testing.T) {
// Verify the default paths don't exist
if _, err := os.Stat(DefaultStateFilename); err == nil {
t.Fatalf("err: %s", err)
t.Fatal("file should not exist")
}
// Verify a backup doesn't exist
if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err == nil {
t.Fatalf("err: %s", err)
t.Fatal("file should not exist")
}
// Verify we have no configured legacy
@ -1471,12 +1505,12 @@ func TestMetaBackend_configuredChangedLegacyCopyBackend(t *testing.T) {
// Verify no local state
if _, err := os.Stat(DefaultStateFilename); err == nil {
t.Fatalf("err: %s", err)
t.Fatal("file should not exist")
}
// Verify no local backup
if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err == nil {
t.Fatalf("err: %s", err)
t.Fatal("file should not exist")
}
}
@ -1518,12 +1552,12 @@ func TestMetaBackend_configuredChangedLegacyCopyLegacy(t *testing.T) {
// Verify the default paths don't exist
if _, err := os.Stat(DefaultStateFilename); err == nil {
t.Fatalf("err: %s", err)
t.Fatal("file should not exist")
}
// Verify a backup doesn't exist
if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err == nil {
t.Fatalf("err: %s", err)
t.Fatal("file should not exist")
}
// Verify we have no configured legacy
@ -1574,12 +1608,12 @@ func TestMetaBackend_configuredChangedLegacyCopyLegacy(t *testing.T) {
// Verify no local state
if _, err := os.Stat(DefaultStateFilename); err == nil {
t.Fatalf("err: %s", err)
t.Fatal("file should not exist")
}
// Verify no local backup
if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err == nil {
t.Fatalf("err: %s", err)
t.Fatal("file should not exist")
}
}
@ -1621,12 +1655,12 @@ func TestMetaBackend_configuredChangedLegacyCopyBoth(t *testing.T) {
// Verify the default paths don't exist
if _, err := os.Stat(DefaultStateFilename); err == nil {
t.Fatalf("err: %s", err)
t.Fatal("file should not exist")
}
// Verify a backup doesn't exist
if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err == nil {
t.Fatalf("err: %s", err)
t.Fatal("file should not exist")
}
// Verify we have no configured legacy
@ -1677,12 +1711,12 @@ func TestMetaBackend_configuredChangedLegacyCopyBoth(t *testing.T) {
// Verify no local state
if _, err := os.Stat(DefaultStateFilename); err == nil {
t.Fatalf("err: %s", err)
t.Fatal("file should not exist")
}
// Verify no local backup
if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err == nil {
t.Fatalf("err: %s", err)
t.Fatal("file should not exist")
}
}
@ -1720,13 +1754,13 @@ func TestMetaBackend_configuredUnsetWithLegacyNoCopy(t *testing.T) {
}
// Verify the default paths dont exist since we had no state
if _, err := os.Stat(DefaultStateFilename); err == nil {
t.Fatalf("err: %s", err)
if !isEmptyState(DefaultStateFilename) {
t.Fatal("state should be empty")
}
// Verify a backup doesn't exist
if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err == nil {
t.Fatalf("err: %s", err)
if !isEmptyState(DefaultStateFilename + DefaultBackupExtension) {
t.Fatal("backup should be empty")
}
// Verify we have no configured backend/legacy
@ -1813,13 +1847,13 @@ func TestMetaBackend_configuredUnsetWithLegacyCopyBackend(t *testing.T) {
}
// Verify the default paths exist
if _, err := os.Stat(DefaultStateFilename); err != nil {
t.Fatalf("err: %s", err)
if isEmptyState(DefaultStateFilename) {
t.Fatalf("default state was empty")
}
// Verify a backup doesn't exist
if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err == nil {
t.Fatalf("err: %s", err)
if !isEmptyState(DefaultStateFilename + DefaultBackupExtension) {
t.Fatal("backupstate should be empty")
}
// Verify we have no configured backend/legacy
@ -1869,8 +1903,8 @@ func TestMetaBackend_configuredUnsetWithLegacyCopyBackend(t *testing.T) {
}
// Verify a local backup
if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err != nil {
t.Fatalf("err: %s", err)
if isEmptyState(DefaultStateFilename + DefaultBackupExtension) {
t.Fatal("backup is empty")
}
}
@ -1911,13 +1945,13 @@ func TestMetaBackend_configuredUnsetWithLegacyCopyLegacy(t *testing.T) {
}
// Verify the default paths exist
if _, err := os.Stat(DefaultStateFilename); err != nil {
t.Fatalf("err: %s", err)
if isEmptyState(DefaultStateFilename) {
t.Fatalf("default state was empty")
}
// Verify a backup doesn't exist
if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err == nil {
t.Fatalf("err: %s", err)
if !isEmptyState(DefaultStateFilename + DefaultBackupExtension) {
t.Fatal("backupstate should be empty")
}
// Verify we have no configured backend/legacy
@ -1967,8 +2001,8 @@ func TestMetaBackend_configuredUnsetWithLegacyCopyLegacy(t *testing.T) {
}
// Verify a local backup
if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err != nil {
t.Fatalf("err: %s", err)
if isEmptyState(DefaultStateFilename + DefaultBackupExtension) {
t.Fatal("backup is empty")
}
}
@ -2009,13 +2043,13 @@ func TestMetaBackend_configuredUnsetWithLegacyCopyBoth(t *testing.T) {
}
// Verify the default paths exist
if _, err := os.Stat(DefaultStateFilename); err != nil {
t.Fatalf("err: %s", err)
if isEmptyState(DefaultStateFilename) {
t.Fatal("state is empty")
}
// Verify a backup exists
if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err != nil {
t.Fatalf("err: %s", err)
if isEmptyState(DefaultStateFilename + DefaultBackupExtension) {
t.Fatal("backup is empty")
}
// Verify we have no configured backend/legacy
@ -2065,8 +2099,8 @@ func TestMetaBackend_configuredUnsetWithLegacyCopyBoth(t *testing.T) {
}
// Verify a local backup
if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err != nil {
t.Fatalf("err: %s", err)
if isEmptyState(DefaultStateFilename + DefaultBackupExtension) {
t.Fatal("backup is empty")
}
}
@ -2106,14 +2140,14 @@ func TestMetaBackend_planLocal(t *testing.T) {
t.Fatalf("state should be nil: %#v", state)
}
// Verify the default path
if _, err := os.Stat(DefaultStateFilename); err == nil {
t.Fatalf("err: %s", err)
// Verify the default path doens't exist
if !isEmptyState(DefaultStateFilename) {
t.Fatal("expected empty state")
}
// Verify a backup doesn't exists
if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err == nil {
t.Fatalf("err: %s", err)
if !isEmptyState(DefaultStateFilename + DefaultBackupExtension) {
t.Fatal("expected empty backup")
}
// Verify we have no configured backend/legacy
@ -2148,8 +2182,8 @@ func TestMetaBackend_planLocal(t *testing.T) {
}
// Verify no local backup
if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err == nil {
t.Fatalf("err: %s", err)
if !isEmptyState(DefaultStateFilename + DefaultBackupExtension) {
t.Fatalf("backup state should be empty")
}
}
@ -2207,7 +2241,7 @@ func TestMetaBackend_planLocalStatePath(t *testing.T) {
// Verify a backup doesn't exists
if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err == nil {
t.Fatalf("err: %s", err)
t.Fatal("file should not exist")
}
// Verify we have no configured backend/legacy
@ -2242,8 +2276,8 @@ func TestMetaBackend_planLocalStatePath(t *testing.T) {
}
// Verify we have a backup
if _, err := os.Stat(statePath + DefaultBackupExtension); err != nil {
t.Fatalf("err: %s", err)
if isEmptyState(statePath + DefaultBackupExtension) {
t.Fatal("backup is empty")
}
}
@ -2287,8 +2321,8 @@ func TestMetaBackend_planLocalMatch(t *testing.T) {
}
// Verify the default path
if _, err := os.Stat(DefaultStateFilename); err != nil {
t.Fatalf("err: %s", err)
if isEmptyState(DefaultStateFilename) {
t.Fatal("state is empty")
}
// Verify a backup exists
@ -2328,8 +2362,8 @@ func TestMetaBackend_planLocalMatch(t *testing.T) {
}
// Verify local backup
if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err != nil {
t.Fatalf("err: %s", err)
if isEmptyState(DefaultStateFilename + DefaultBackupExtension) {
t.Fatal("backup is empty")
}
}
@ -2374,7 +2408,7 @@ func TestMetaBackend_planLocalMismatchLineage(t *testing.T) {
// Verify a backup doesn't exists
if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err == nil {
t.Fatalf("err: %s", err)
t.Fatal("file should not exist")
}
// Verify we have no configured backend/legacy
@ -2426,7 +2460,7 @@ func TestMetaBackend_planLocalNewer(t *testing.T) {
// Verify a backup doesn't exists
if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err == nil {
t.Fatalf("err: %s", err)
t.Fatal("file should not exist")
}
// Verify we have no configured backend/legacy
@ -2486,14 +2520,14 @@ func TestMetaBackend_planBackendEmptyDir(t *testing.T) {
t.Fatalf("bad: %#v", state)
}
// Verify the default path
if _, err := os.Stat(DefaultStateFilename); err == nil {
t.Fatalf("err: %s", err)
// Verify the default path doesn't exist
if !isEmptyState(DefaultStateFilename) {
t.Fatal("state is not empty")
}
// Verify a backup exists
if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err == nil {
t.Fatalf("err: %s", err)
// Verify a backup doesn't exist
if !isEmptyState(DefaultStateFilename + DefaultBackupExtension) {
t.Fatal("backup is not empty")
}
// Verify we have no configured backend/legacy
@ -2529,12 +2563,12 @@ func TestMetaBackend_planBackendEmptyDir(t *testing.T) {
// Verify no default path
if _, err := os.Stat(DefaultStateFilename); err == nil {
t.Fatalf("err: %s", err)
t.Fatal("file should not exist")
}
// Verify no local backup
if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err == nil {
t.Fatalf("err: %s", err)
t.Fatal("file should not exist")
}
}
@ -2588,14 +2622,14 @@ func TestMetaBackend_planBackendMatch(t *testing.T) {
t.Fatalf("bad: %#v", state)
}
// Verify the default path
// Verify the default path exists
if _, err := os.Stat(DefaultStateFilename); err == nil {
t.Fatalf("err: %s", err)
t.Fatal("file should not exist")
}
// Verify a backup exists
// Verify a backup doesn't exist
if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err == nil {
t.Fatalf("err: %s", err)
t.Fatal("file should not exist")
}
// Verify we have no configured backend/legacy
@ -2631,12 +2665,12 @@ func TestMetaBackend_planBackendMatch(t *testing.T) {
// Verify no default path
if _, err := os.Stat(DefaultStateFilename); err == nil {
t.Fatalf("err: %s", err)
t.Fatal("file should not exist")
}
// Verify no local backup
if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err == nil {
t.Fatalf("err: %s", err)
t.Fatal("file should not exist")
}
}
@ -2686,9 +2720,9 @@ func TestMetaBackend_planBackendMismatchLineage(t *testing.T) {
t.Fatalf("bad: %#v", actual)
}
// Verify a backup doesn't exists
// Verify a backup doesn't exist
if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err == nil {
t.Fatalf("err: %s", err)
t.Fatal("file should not exist")
}
// Verify we have no configured backend/legacy
@ -2699,7 +2733,7 @@ func TestMetaBackend_planBackendMismatchLineage(t *testing.T) {
// Verify we have no default state
if _, err := os.Stat(DefaultStateFilename); err == nil {
t.Fatalf("err: %s", err)
t.Fatal("file should not exist")
}
}
@ -2753,12 +2787,12 @@ func TestMetaBackend_planLegacy(t *testing.T) {
// Verify the default path
if _, err := os.Stat(DefaultStateFilename); err == nil {
t.Fatalf("err: %s", err)
t.Fatal("file should not exist")
}
// Verify a backup doesn't exist
if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err == nil {
t.Fatalf("err: %s", err)
t.Fatal("file should not exist")
}
// Verify we have no configured backend/legacy
@ -2794,12 +2828,12 @@ func TestMetaBackend_planLegacy(t *testing.T) {
// Verify no default path
if _, err := os.Stat(DefaultStateFilename); err == nil {
t.Fatalf("err: %s", err)
t.Fatal("file should not exist")
}
// Verify no local backup
if _, err := os.Stat(DefaultStateFilename + DefaultBackupExtension); err == nil {
t.Fatalf("err: %s", err)
t.Fatal("file should not exist")
}
}

View File

@ -1,8 +1,6 @@
package state
import (
"github.com/hashicorp/terraform/terraform"
)
import "github.com/hashicorp/terraform/terraform"
// BackupState wraps a State that backs up the state on the first time that
// a WriteState or PersistState is called.
@ -43,6 +41,21 @@ func (s *BackupState) PersistState() error {
return s.Real.PersistState()
}
// all states get wrapped by BackupState, so it has to be a Locker
func (s *BackupState) Lock(reason string) error {
if s, ok := s.Real.(Locker); ok {
return s.Lock(reason)
}
return nil
}
func (s *BackupState) Unlock() error {
if s, ok := s.Real.(Locker); ok {
return s.Unlock()
}
return nil
}
func (s *BackupState) backup() error {
state := s.Real.State()
if state == nil {
@ -53,9 +66,14 @@ func (s *BackupState) backup() error {
state = s.Real.State()
}
ls := &LocalState{Path: s.Path}
if err := ls.WriteState(state); err != nil {
return err
// LocalState.WriteState ensures that a file always exists for locking
// purposes, but we don't need a backup or lock if the state is empty, so
// skip this with a nil state.
if state != nil {
ls := &LocalState{Path: s.Path}
if err := ls.WriteState(state); err != nil {
return err
}
}
s.done = true

View File

@ -3,6 +3,7 @@ package state
import (
"fmt"
multierror "github.com/hashicorp/go-multierror"
"github.com/hashicorp/terraform/terraform"
)
@ -16,6 +17,56 @@ type CacheState struct {
state *terraform.State
}
// Locker implementation.
// Since remote states are wrapped in a CacheState, we need to implement the
// Lock/Unlock methods here to delegate them to the remote client.
func (s *CacheState) Lock(reason string) error {
durable, durableIsLocker := s.Durable.(Locker)
cache, cacheIsLocker := s.Cache.(Locker)
if durableIsLocker {
if err := durable.Lock(reason); err != nil {
return err
}
}
// We try to lock the Cache too, which is usually a local file. This also
// protects against multiple local processes if the remote state doesn't
// support locking.
if cacheIsLocker {
if err := cache.Lock(reason); err != nil {
// try to unlock Durable if this failed
if unlockErr := durable.Unlock(); unlockErr != nil {
err = multierror.Append(err, unlockErr)
}
return err
}
}
return nil
}
// Unlock unlocks both the Durable and Cache states.
func (s *CacheState) Unlock() error {
durable, durableIsLocker := s.Durable.(Locker)
cache, cacheIsLocker := s.Cache.(Locker)
var err error
if durableIsLocker {
if unlockErr := durable.Unlock(); unlockErr != nil {
err = multierror.Append(err, unlockErr)
}
}
if cacheIsLocker {
if unlockErr := cache.Unlock(); unlockErr != nil {
err = multierror.Append(err, unlockErr)
}
}
return err
}
// StateReader impl.
func (s *CacheState) State() *terraform.State {
return s.state.DeepCopy()

View File

@ -1,12 +1,36 @@
package state
import (
"bytes"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"time"
"github.com/hashicorp/terraform/terraform"
)
// lock metadata structure for local locks
type lockInfo struct {
// Path to the state file
Path string
// The time the lock was taken
Created time.Time
// The time this lock expires
Expires time.Time
// The lock reason passed to State.Lock
Reason string
}
// return the lock info formatted in an error
func (l *lockInfo) Err() error {
return fmt.Errorf("state file %q locked. created:%s, expires:%s, reason:%s",
l.Path, l.Created, l.Expires, l.Reason)
}
// LocalState manages a state storage that is local to the filesystem.
type LocalState struct {
// Path is the path to read the state from. PathOut is the path to
@ -15,6 +39,9 @@ type LocalState struct {
Path string
PathOut string
// the file handle corresponding to PathOut
stateFileOut *os.File
state *terraform.State
readState *terraform.State
written bool
@ -31,42 +58,89 @@ func (s *LocalState) State() *terraform.State {
return s.state.DeepCopy()
}
// WriteState for LocalState always persists the state as well.
//
// StateWriter impl.
func (s *LocalState) WriteState(state *terraform.State) error {
s.state = state
path := s.PathOut
if path == "" {
path = s.Path
}
// If we don't have any state, we actually delete the file if it exists
if state == nil {
err := os.Remove(path)
if err != nil && os.IsNotExist(err) {
return nil
// Lock implements a local filesystem state.Locker.
func (s *LocalState) Lock(reason string) error {
if s.stateFileOut == nil {
if err := s.createStateFiles(); err != nil {
return err
}
return err
}
// Create all the directories
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
return err
if err := s.lock(); err != nil {
if info, err := s.lockInfo(); err == nil {
return info.Err()
}
return fmt.Errorf("state file %q locked: %s", s.Path, err)
}
f, err := os.Create(path)
return s.writeLockInfo(reason)
}
func (s *LocalState) Unlock() error {
os.Remove(s.lockInfoPath())
return s.unlock()
}
// Open the state file, creating the directories and file as needed.
func (s *LocalState) createStateFiles() error {
if s.PathOut == "" {
s.PathOut = s.Path
}
f, err := createFileAndDirs(s.PathOut)
if err != nil {
return err
}
defer f.Close()
s.stateFileOut = f
return nil
}
func createFileAndDirs(path string) (*os.File, error) {
// Create all the directories
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
return nil, err
}
f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE, 0666)
if err != nil {
return nil, err
}
return f, nil
}
// WriteState for LocalState always persists the state as well.
// TODO: this should use a more robust method of writing state, by first
// writing to a temp file on the same filesystem, and renaming the file over
// the original.
//
// StateWriter impl.
func (s *LocalState) WriteState(state *terraform.State) error {
if s.stateFileOut == nil {
if err := s.createStateFiles(); err != nil {
return nil
}
}
defer s.stateFileOut.Sync()
s.state = state
if _, err := s.stateFileOut.Seek(0, os.SEEK_SET); err != nil {
return err
}
if err := s.stateFileOut.Truncate(0); err != nil {
return err
}
if state == nil {
// if we have no state, don't write anything else.
return nil
}
s.state.IncrementSerialMaybe(s.readState)
s.readState = s.state
if err := terraform.WriteState(s.state, f); err != nil {
if err := terraform.WriteState(s.state, s.stateFileOut); err != nil {
return err
}
@ -83,33 +157,90 @@ func (s *LocalState) PersistState() error {
// StateRefresher impl.
func (s *LocalState) RefreshState() error {
// If we've never loaded before, read from Path, otherwise we
// read from PathOut.
path := s.Path
if s.written && s.PathOut != "" {
path = s.PathOut
}
f, err := os.Open(path)
if err != nil {
// It is okay if the file doesn't exist, we treat that as a nil state
if !os.IsNotExist(err) {
return err
}
f = nil
}
var state *terraform.State
if f != nil {
defer f.Close()
state, err = terraform.ReadState(f)
var reader io.Reader
if !s.written {
// we haven't written a state file yet, so load from Path
f, err := os.Open(s.Path)
if err != nil {
return err
// It is okay if the file doesn't exist, we treat that as a nil state
if !os.IsNotExist(err) {
return err
}
// we need a non-nil reader for ReadState and an empty buffer works
// to return EOF immediately
reader = bytes.NewBuffer(nil)
} else {
defer f.Close()
reader = f
}
} else {
// we have a state file, make sure we're at the start
s.stateFileOut.Seek(0, os.SEEK_SET)
reader = s.stateFileOut
}
state, err := terraform.ReadState(reader)
// if there's no state we just assign the nil return value
if err != nil && err != terraform.ErrNoState {
return err
}
s.state = state
s.readState = state
return nil
}
// return the path for the lockInfo metadata.
func (s *LocalState) lockInfoPath() string {
stateDir, stateName := filepath.Split(s.Path)
if stateName == "" {
panic("empty state file path")
}
if stateName[0] == '.' {
stateName = stateName[1:]
}
return filepath.Join(stateDir, fmt.Sprintf(".%s.lock.info", stateName))
}
// lockInfo returns the data in a lock info file
func (s *LocalState) lockInfo() (*lockInfo, error) {
path := s.lockInfoPath()
infoData, err := ioutil.ReadFile(path)
if err != nil {
return nil, err
}
info := lockInfo{}
err = json.Unmarshal(infoData, &info)
if err != nil {
return nil, fmt.Errorf("state file %q locked, but could not unmarshal lock info: %s", s.Path, err)
}
return &info, nil
}
// write a new lock info file
func (s *LocalState) writeLockInfo(reason string) error {
path := s.lockInfoPath()
lockInfo := &lockInfo{
Path: s.Path,
Created: time.Now().UTC(),
Expires: time.Now().Add(time.Hour).UTC(),
Reason: reason,
}
infoData, err := json.Marshal(lockInfo)
if err != nil {
panic(fmt.Sprintf("could not marshal lock info: %#v", lockInfo))
}
err = ioutil.WriteFile(path, infoData, 0600)
if err != nil {
return fmt.Errorf("could not write lock info for %q: %s", s.Path, err)
}
return nil
}

34
state/local_lock_unix.go Normal file
View File

@ -0,0 +1,34 @@
// +build !windows
package state
import (
"os"
"syscall"
)
// use fcntl POSIX locks for the most consistent behavior across platforms, and
// hopefully some campatibility over NFS and CIFS.
func (s *LocalState) lock() error {
flock := &syscall.Flock_t{
Type: syscall.F_RDLCK | syscall.F_WRLCK,
Whence: int16(os.SEEK_SET),
Start: 0,
Len: 0,
}
fd := s.stateFileOut.Fd()
return syscall.FcntlFlock(fd, syscall.F_SETLK, flock)
}
func (s *LocalState) unlock() error {
flock := &syscall.Flock_t{
Type: syscall.F_UNLCK,
Whence: int16(os.SEEK_SET),
Start: 0,
Len: 0,
}
fd := s.stateFileOut.Fd()
return syscall.FcntlFlock(fd, syscall.F_SETLK, flock)
}

150
state/local_lock_windows.go Normal file
View File

@ -0,0 +1,150 @@
// +build windows
package state
import (
"math"
"os"
"sync"
"syscall"
"unsafe"
)
type stateLock struct {
handle syscall.Handle
}
var (
modkernel32 = syscall.NewLazyDLL("kernel32.dll")
procLockFileEx = modkernel32.NewProc("LockFileEx")
procCreateEventW = modkernel32.NewProc("CreateEventW")
lockedFilesMu sync.Mutex
lockedFiles = map[*os.File]syscall.Handle{}
)
const (
// dwFlags defined for LockFileEx
// https://msdn.microsoft.com/en-us/library/windows/desktop/aa365203(v=vs.85).aspx
_LOCKFILE_FAIL_IMMEDIATELY = 1
_LOCKFILE_EXCLUSIVE_LOCK = 2
)
func (s *LocalState) lock() error {
lockedFilesMu.Lock()
defer lockedFilesMu.Unlock()
name, err := syscall.UTF16PtrFromString(s.PathOut)
if err != nil {
return err
}
handle, err := syscall.CreateFile(
name,
syscall.GENERIC_READ|syscall.GENERIC_WRITE,
// since this file is already open in out process, we need shared
// access here for this call.
syscall.FILE_SHARE_READ|syscall.FILE_SHARE_WRITE,
nil,
syscall.OPEN_EXISTING,
syscall.FILE_ATTRIBUTE_NORMAL,
0,
)
if err != nil {
return err
}
lockedFiles[s.stateFileOut] = handle
// even though we're failing immediately, an overlapped event structure is
// required
ol, err := newOverlapped()
if err != nil {
return err
}
defer syscall.CloseHandle(ol.HEvent)
return lockFileEx(
handle,
_LOCKFILE_EXCLUSIVE_LOCK|_LOCKFILE_FAIL_IMMEDIATELY,
0, // reserved
0, // bytes low
math.MaxUint32, // bytes high
ol,
)
}
func (s *LocalState) unlock() error {
lockedFilesMu.Lock()
defer lockedFilesMu.Unlock()
handle, ok := lockedFiles[s.stateFileOut]
if !ok {
// we allow multiple Unlock calls
return nil
}
delete(lockedFiles, s.stateFileOut)
return syscall.Close(handle)
}
func lockFileEx(h syscall.Handle, flags, reserved, locklow, lockhigh uint32, ol *syscall.Overlapped) (err error) {
r1, _, e1 := syscall.Syscall6(
procLockFileEx.Addr(),
6,
uintptr(h),
uintptr(flags),
uintptr(reserved),
uintptr(locklow),
uintptr(lockhigh),
uintptr(unsafe.Pointer(ol)),
)
if r1 == 0 {
if e1 != 0 {
err = error(e1)
} else {
err = syscall.EINVAL
}
}
return
}
// newOverlapped creates a structure used to track asynchronous
// I/O requests that have been issued.
func newOverlapped() (*syscall.Overlapped, error) {
event, err := createEvent(nil, true, false, nil)
if err != nil {
return nil, err
}
return &syscall.Overlapped{HEvent: event}, nil
}
func createEvent(sa *syscall.SecurityAttributes, manualReset bool, initialState bool, name *uint16) (handle syscall.Handle, err error) {
var _p0 uint32
if manualReset {
_p0 = 1
}
var _p1 uint32
if initialState {
_p1 = 1
}
r0, _, e1 := syscall.Syscall6(
procCreateEventW.Addr(),
4,
uintptr(unsafe.Pointer(sa)),
uintptr(_p0),
uintptr(_p1),
uintptr(unsafe.Pointer(name)),
0,
0,
)
handle = syscall.Handle(r0)
if handle == syscall.InvalidHandle {
if e1 != 0 {
err = error(e1)
} else {
err = syscall.EINVAL
}
}
return
}

View File

@ -3,6 +3,7 @@ package state
import (
"io/ioutil"
"os"
"os/exec"
"testing"
"github.com/hashicorp/terraform/terraform"
@ -14,6 +15,61 @@ func TestLocalState(t *testing.T) {
TestState(t, ls)
}
func TestLocalStateLocks(t *testing.T) {
s := testLocalState(t)
defer os.Remove(s.Path)
// lock first
if err := s.Lock("test"); err != nil {
t.Fatal(err)
}
out, err := exec.Command("go", "run", "testdata/lockstate.go", s.Path).CombinedOutput()
if err != nil {
t.Fatal("unexpected lock failure", err)
}
if string(out) != "lock failed" {
t.Fatal("expected 'locked failed', got", string(out))
}
// check our lock info
lockInfo, err := s.lockInfo()
if err != nil {
t.Fatal(err)
}
if lockInfo.Reason != "test" {
t.Fatalf("invalid lock info %#v\n", lockInfo)
}
// a noop, since we unlock on exit
if err := s.Unlock(); err != nil {
t.Fatal(err)
}
// local locks can re-lock
if err := s.Lock("test"); err != nil {
t.Fatal(err)
}
// Unlock should be repeatable
if err := s.Unlock(); err != nil {
t.Fatal(err)
}
if err := s.Unlock(); err != nil {
t.Fatal(err)
}
// make sure lock info is gone
lockInfoPath := s.lockInfoPath()
if _, err := os.Stat(lockInfoPath); !os.IsNotExist(err) {
t.Fatal("lock info not removed")
}
}
func TestLocalState_pathOut(t *testing.T) {
f, err := ioutil.TempFile("", "tf")
if err != nil {

View File

@ -44,6 +44,38 @@ func testClient(t *testing.T, c Client) {
}
}
func testClientLocks(t *testing.T, c Client) {
s3Client := c.(*S3Client)
// initial lock
if err := s3Client.Lock("test"); err != nil {
t.Fatal(err)
}
// second lock should fail
if err := s3Client.Lock("test"); err == nil {
t.Fatal("expected error, got nil")
}
// unlock should work
if err := s3Client.Unlock(); err != nil {
t.Fatal(err)
}
// now we should be able to lock again
if err := s3Client.Lock("test"); err != nil {
t.Fatal(err)
}
// unlock should be idempotent
if err := s3Client.Unlock(); err != nil {
t.Fatal(err)
}
if err := s3Client.Unlock(); err != nil {
t.Fatal(err)
}
}
func TestRemoteClient_noPayload(t *testing.T) {
s := &State{
Client: nilClient{},

View File

@ -7,10 +7,12 @@ import (
"log"
"os"
"strconv"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/dynamodb"
"github.com/aws/aws-sdk-go/service/s3"
"github.com/hashicorp/go-cleanhttp"
"github.com/hashicorp/go-multierror"
@ -89,6 +91,7 @@ providing credentials for the AWS S3 remote`))
}
sess := session.New(awsConfig)
nativeClient := s3.New(sess)
dynClient := dynamodb.New(sess)
return &S3Client{
nativeClient: nativeClient,
@ -97,6 +100,8 @@ providing credentials for the AWS S3 remote`))
serverSideEncryption: serverSideEncryption,
acl: acl,
kmsKeyID: kmsKeyID,
dynClient: dynClient,
lockTable: conf["lock_table"],
}, nil
}
@ -107,6 +112,8 @@ type S3Client struct {
serverSideEncryption bool
acl string
kmsKeyID string
dynClient *dynamodb.DynamoDB
lockTable string
}
func (c *S3Client) Get() (*Payload, error) {
@ -188,3 +195,73 @@ func (c *S3Client) Delete() error {
return err
}
func (c *S3Client) Lock(reason string) error {
if c.lockTable == "" {
return nil
}
stateName := fmt.Sprintf("%s/%s", c.bucketName, c.keyName)
putParams := &dynamodb.PutItemInput{
Item: map[string]*dynamodb.AttributeValue{
"LockID": {S: aws.String(stateName)},
"Created": {S: aws.String(time.Now().UTC().Format(time.RFC3339))},
"Expires": {S: aws.String(time.Now().Add(time.Hour).UTC().Format(time.RFC3339))},
"Info": {S: aws.String(reason)},
},
TableName: aws.String(c.lockTable),
ConditionExpression: aws.String("attribute_not_exists(LockID)"),
}
_, err := c.dynClient.PutItem(putParams)
if err != nil {
getParams := &dynamodb.GetItemInput{
Key: map[string]*dynamodb.AttributeValue{
"LockID": {S: aws.String(fmt.Sprintf("%s/%s", c.bucketName, c.keyName))},
},
ProjectionExpression: aws.String("LockID, Created, Expires, Info"),
TableName: aws.String(c.lockTable),
}
resp, err := c.dynClient.GetItem(getParams)
if err != nil {
return fmt.Errorf("s3 state file %q locked, cfailed to retrive info: %s", stateName, err)
}
var created, expires, info string
if v, ok := resp.Item["Created"]; ok && v.S != nil {
created = *v.S
}
if v, ok := resp.Item["Expires"]; ok && v.S != nil {
expires = *v.S
}
if v, ok := resp.Item["Info"]; ok && v.S != nil {
info = *v.S
}
return fmt.Errorf("state file %q locked. created:%s, expires:%s, reason:%s",
stateName, created, expires, info)
}
return nil
}
func (c *S3Client) Unlock() error {
if c.lockTable == "" {
return nil
}
params := &dynamodb.DeleteItemInput{
Key: map[string]*dynamodb.AttributeValue{
"LockID": {S: aws.String(fmt.Sprintf("%s/%s", c.bucketName, c.keyName))},
},
TableName: aws.String(c.lockTable),
}
_, err := c.dynClient.DeleteItem(params)
if err != nil {
return err
}
return nil
}

View File

@ -6,6 +6,8 @@ import (
"testing"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/dynamodb"
"github.com/aws/aws-sdk-go/service/s3"
)
@ -123,9 +125,113 @@ func TestS3Client(t *testing.T) {
_, err := nativeClient.DeleteBucket(deleteBucketReq)
if err != nil {
t.Logf("WARNING: Failed to delete the test S3 bucket. It has been left in your AWS account and may incur storage charges. (error was %s)", err)
t.Logf("WARNING: Failed to delete the test S3 bucket. It may have been left in your AWS account and may incur storage charges. (error was %s)", err)
}
}()
testClient(t, client)
}
func TestS3ClientLocks(t *testing.T) {
// This test creates a DynamoDB table.
// It may incur costs, so it will only run if AWS credential environment
// variables are present.
accessKeyId := os.Getenv("AWS_ACCESS_KEY_ID")
if accessKeyId == "" {
t.Skipf("skipping; AWS_ACCESS_KEY_ID must be set")
}
regionName := os.Getenv("AWS_DEFAULT_REGION")
if regionName == "" {
regionName = "us-west-2"
}
bucketName := fmt.Sprintf("terraform-remote-s3-lock-%x", time.Now().Unix())
keyName := "testState"
config := make(map[string]string)
config["region"] = regionName
config["bucket"] = bucketName
config["key"] = keyName
config["encrypt"] = "1"
config["lock_table"] = bucketName
client, err := s3Factory(config)
if err != nil {
t.Fatalf("Error for valid config")
}
s3Client := client.(*S3Client)
// set this up before we try to crate the table, in case we timeout creating it.
defer deleteDynaboDBTable(t, s3Client, bucketName)
createDynamoDBTable(t, s3Client, bucketName)
testClientLocks(t, client)
}
// create the dynamoDB table, and wait until we can query it.
func createDynamoDBTable(t *testing.T, c *S3Client, tableName string) {
createInput := &dynamodb.CreateTableInput{
AttributeDefinitions: []*dynamodb.AttributeDefinition{
{
AttributeName: aws.String("LockID"),
AttributeType: aws.String("S"),
},
},
KeySchema: []*dynamodb.KeySchemaElement{
{
AttributeName: aws.String("LockID"),
KeyType: aws.String("HASH"),
},
},
ProvisionedThroughput: &dynamodb.ProvisionedThroughput{
ReadCapacityUnits: aws.Int64(5),
WriteCapacityUnits: aws.Int64(5),
},
TableName: aws.String(tableName),
}
_, err := c.dynClient.CreateTable(createInput)
if err != nil {
t.Fatal(err)
}
// now wait until it's ACTIVE
start := time.Now()
time.Sleep(time.Second)
describeInput := &dynamodb.DescribeTableInput{
TableName: aws.String(tableName),
}
for {
resp, err := c.dynClient.DescribeTable(describeInput)
if err != nil {
t.Fatal(err)
}
if *resp.Table.TableStatus == "ACTIVE" {
return
}
if time.Since(start) > time.Minute {
t.Fatalf("timed out creating DynamoDB table %s", tableName)
}
time.Sleep(3 * time.Second)
}
}
func deleteDynaboDBTable(t *testing.T, c *S3Client, tableName string) {
params := &dynamodb.DeleteTableInput{
TableName: aws.String(tableName),
}
_, err := c.dynClient.DeleteTable(params)
if err != nil {
t.Logf("WARNING: Failed to delete the test DynamoDB table %q. It has been left in your AWS account and may incur charges. (error was %s)", tableName, err)
}
}

View File

@ -60,3 +60,26 @@ func (s *State) PersistState() error {
return s.Client.Put(buf.Bytes())
}
// Lock calls the Client's Lock method if it's implemented.
func (s *State) Lock(reason string) error {
if c, ok := s.Client.(stateLocker); ok {
return c.Lock(reason)
}
return nil
}
// Unlock calls the Client's Unlock method if it's implemented.
func (s *State) Unlock() error {
if c, ok := s.Client.(stateLocker); ok {
return c.Unlock()
}
return nil
}
// stateLocker mirrors the state.Locker interface. This can be implemented by
// Clients to provide methods for locking and unlocking remote state.
type stateLocker interface {
Lock(reason string) error
Unlock() error
}

View File

@ -40,3 +40,9 @@ type StateRefresher interface {
type StatePersister interface {
PersistState() error
}
// Locker is implemented to lock state during command execution.
type Locker interface {
Lock(reason string) error
Unlock() error
}

23
state/state_test.go Normal file
View File

@ -0,0 +1,23 @@
package state
import (
"flag"
"io/ioutil"
"log"
"os"
"testing"
"github.com/hashicorp/terraform/helper/logging"
)
func TestMain(m *testing.M) {
flag.Parse()
if testing.Verbose() {
// if we're verbose, use the logging requested by TF_LOG
logging.SetOutput()
} else {
// otherwise silence all logs
log.SetOutput(ioutil.Discard)
}
os.Exit(m.Run())
}

28
state/testdata/lockstate.go vendored Normal file
View File

@ -0,0 +1,28 @@
package main
import (
"io"
"log"
"os"
"github.com/hashicorp/terraform/state"
)
// Attempt to open and lock a terraform state file.
// Lock failure exits with 0 and writes "lock failed" to stderr.
func main() {
if len(os.Args) != 2 {
log.Fatal(os.Args[0], "statefile")
}
s := &state.LocalState{
Path: os.Args[1],
}
err := s.Lock("test")
if err != nil {
io.WriteString(os.Stderr, "lock failed")
}
return
}

View File

@ -4,6 +4,7 @@ import (
"bufio"
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
@ -1800,10 +1801,16 @@ func testForV0State(buf *bufio.Reader) error {
return nil
}
// ErrNoState is returned by ReadState when the io.Reader contains no data
var ErrNoState = errors.New("no state")
// ReadState reads a state structure out of a reader in the format that
// was written by WriteState.
func ReadState(src io.Reader) (*State, error) {
buf := bufio.NewReader(src)
if _, err := buf.Peek(1); err == io.EOF {
return nil, ErrNoState
}
if err := testForV0State(buf); err != nil {
return nil, err