Skip to content

Commit

Permalink
remote/backend/swift: Add support for locking
Browse files Browse the repository at this point in the history
  • Loading branch information
yanndegat committed Jul 17, 2018
1 parent 8cee67e commit 5428051
Show file tree
Hide file tree
Showing 4 changed files with 406 additions and 32 deletions.
11 changes: 11 additions & 0 deletions backend/remote-state/swift/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,13 @@ func New() backend.Backend {
Optional: true,
Description: descriptions["expire_after"],
},

"lock": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
Description: "Lock state access",
Default: true,
},
},
}

Expand Down Expand Up @@ -238,6 +245,7 @@ type Backend struct {
archiveContainer string
expireSecs int
container string
lock bool
}

func (b *Backend) configure(ctx context.Context) error {
Expand Down Expand Up @@ -276,6 +284,9 @@ func (b *Backend) configure(ctx context.Context) error {
b.container = data.Get("path").(string)
}

// Store the lock information
b.lock = data.Get("lock").(bool)

// Enable object archiving?
if archiveContainer, ok := data.GetOk("archive_container"); ok {
log.Printf("[DEBUG] Archive_container set, enabling object versioning")
Expand Down
94 changes: 88 additions & 6 deletions backend/remote-state/swift/backend_state.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ func (b *Backend) States() ([]string, error) {
archive: b.archive,
archiveContainer: b.archiveContainer,
expireSecs: b.expireSecs,
lockState: b.lock,
}

// List our container objects
Expand All @@ -34,7 +35,6 @@ func (b *Backend) States() ([]string, error) {
// Find the envs, we use a map since we can get duplicates with
// path suffixes.
envs := map[string]struct{}{}

for _, object := range objectNames {
object = strings.TrimPrefix(object, objectEnvPrefix)
object = strings.TrimSuffix(object, delimiter)
Expand All @@ -45,6 +45,18 @@ func (b *Backend) States() ([]string, error) {
continue
}

// swift is eventually consistent, thus a deleted object may
// be listed in objectList. To ensure consistency, we query
// each object with a "newest" arg set to true
payload, err := client.get(client.container, b.objectName(object))
if err != nil {
return nil, err
}
if payload == nil {
// object doesn't exist anymore. skipping.
continue
}

envs[object] = struct{}{}
}

Expand All @@ -71,10 +83,12 @@ func (b *Backend) DeleteState(name string) error {
archiveContainer: b.archiveContainer,
expireSecs: b.expireSecs,
objectName: b.objectName(name),
lockState: b.lock,
}

// List our container objects
// Delete our object
err := client.Delete()

return err

}
Expand All @@ -91,27 +105,85 @@ func (b *Backend) State(name string) (state.State, error) {
archiveContainer: b.archiveContainer,
expireSecs: b.expireSecs,
objectName: b.objectName(name),
lockState: b.lock,
}

stateMgr := &remote.State{Client: client}
var stateMgr state.State = &remote.State{Client: client}

// If we're not locking, disable it
if !b.lock {
stateMgr = &state.LockDisabled{Inner: stateMgr}
}

// Check to see if this state already exists.
// If we're trying to force-unlock a state, we can't take the lock before
// fetching the state. If the state doesn't exist, we have to assume this
// is a normal create operation, and take the lock at that point.
//
// If we need to force-unlock, but for some reason the state no longer
// exists, the user will have to use aws tools to manually fix the
// situation.
existing, err := b.States()
if err != nil {
return nil, err
}

exists := false
for _, s := range existing {
if s == name {
exists = true
break
}
}

// We need to create the object so it's listed by States.
if !exists {
// the default state always exists
if name == backend.DefaultStateName {
return stateMgr, nil
}

// Grab a lock, we use this to write an empty state if one doesn't
// exist already. We have to write an empty state as a sentinel value
// so States() knows it exists.
lockInfo := state.NewLockInfo()
lockInfo.Operation = "init"
lockId, err := stateMgr.Lock(lockInfo)
if err != nil {
return nil, fmt.Errorf("failed to lock state in Swift: %s", err)
}

// Local helper function so we can call it multiple places
lockUnlock := func(parent error) error {
if err := stateMgr.Unlock(lockId); err != nil {
return fmt.Errorf(strings.TrimSpace(errStateUnlock), lockId, err)
}

return parent
}

//if this isn't the default state name, we need to create the object so
//it's listed by States.
if name != backend.DefaultStateName {
// Grab the value
if err := stateMgr.RefreshState(); err != nil {
err = lockUnlock(err)
return nil, err
}

// If we have no state, we have to create an empty state
if v := stateMgr.State(); v == nil {
if err := stateMgr.WriteState(terraform.NewState()); err != nil {
err = lockUnlock(err)
return nil, err
}
if err := stateMgr.PersistState(); err != nil {
err = lockUnlock(err)
return nil, err
}
}

// Unlock, the state should now be initialized
if err := lockUnlock(nil); err != nil {
return nil, err
}
}

return stateMgr, nil
Expand All @@ -126,3 +198,13 @@ func (b *Backend) objectName(name string) string {

return name
}

const errStateUnlock = `
Error unlocking Swift state. Lock ID: %s
Error: %s
You may have to force-unlock this state in order to use it again.
The Swift backend acquires a lock during initialization to ensure
the minimum required keys are prepared.
`
12 changes: 9 additions & 3 deletions backend/remote-state/swift/backend_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,13 +61,19 @@ func TestBackend(t *testing.T) {

container := fmt.Sprintf("terraform-state-swift-testbackend-%x", time.Now().Unix())

b := backend.TestBackendConfig(t, New(), map[string]interface{}{
b1 := backend.TestBackendConfig(t, New(), map[string]interface{}{
"container": container,
}).(*Backend)

b2 := backend.TestBackendConfig(t, New(), map[string]interface{}{
"container": container,
}).(*Backend)

defer deleteSwiftContainer(t, b.client, container)
defer deleteSwiftContainer(t, b1.client, container)

backend.TestBackendStates(t, b)
backend.TestBackendStates(t, b1)
backend.TestBackendStateLocks(t, b1, b2)
backend.TestBackendStateForceUnlock(t, b1, b2)
}

func TestBackendArchive(t *testing.T) {
Expand Down
Loading

0 comments on commit 5428051

Please sign in to comment.