diff --git a/backend/remote-state/s3/backend_state.go b/backend/remote-state/s3/backend_state.go index f38b199b0..e26db42f7 100644 --- a/backend/remote-state/s3/backend_state.go +++ b/backend/remote-state/s3/backend_state.go @@ -15,9 +15,15 @@ import ( ) func (b *Backend) States() ([]string, error) { + prefix := b.workspaceKeyPrefix + "/" + + // List bucket root if there is no workspaceKeyPrefix + if b.workspaceKeyPrefix == "" { + prefix = "" + } params := &s3.ListObjectsInput{ Bucket: &b.bucketName, - Prefix: aws.String(b.workspaceKeyPrefix + "/"), + Prefix: aws.String(prefix), } resp, err := b.s3Client.ListObjects(params) @@ -25,24 +31,31 @@ func (b *Backend) States() ([]string, error) { return nil, err } - envs := []string{backend.DefaultStateName} + wss := []string{backend.DefaultStateName} for _, obj := range resp.Contents { - env := b.keyEnv(*obj.Key) - if env != "" { - envs = append(envs, env) + ws := b.keyEnv(*obj.Key) + if ws != "" { + wss = append(wss, ws) } } - sort.Strings(envs[1:]) - return envs, nil + sort.Strings(wss[1:]) + return wss, nil } -// extract the env name from the S3 key func (b *Backend) keyEnv(key string) string { - // we have 3 parts, the prefix, the env name, and the key name - parts := strings.SplitN(key, "/", 3) - if len(parts) < 3 { - // no env here + if b.workspaceKeyPrefix == "" { + parts := strings.SplitN(key, "/", 2) + if len(parts) > 1 && parts[1] == b.keyName { + return parts[0] + } else { + return "" + } + } + + parts := strings.SplitAfter(key, b.workspaceKeyPrefix) + + if len(parts) < 2 { return "" } @@ -51,6 +64,12 @@ func (b *Backend) keyEnv(key string) string { return "" } + parts = strings.SplitN(parts[1], "/", 3) + + if len(parts) < 3 { + return "" + } + // not our key, so don't include it in our listing if parts[2] != b.keyName { return "" @@ -177,7 +196,12 @@ func (b *Backend) path(name string) string { return b.keyName } - return strings.Join([]string{b.workspaceKeyPrefix, name, b.keyName}, "/") + if b.workspaceKeyPrefix != "" { + return strings.Join([]string{b.workspaceKeyPrefix, name, b.keyName}, "/") + } else { + // Trim the leading / for no workspace prefix + return strings.Join([]string{b.workspaceKeyPrefix, name, b.keyName}, "/")[1:] + } } const errStateUnlock = ` diff --git a/backend/remote-state/s3/backend_test.go b/backend/remote-state/s3/backend_test.go index 83af43e45..a86b144b5 100644 --- a/backend/remote-state/s3/backend_test.go +++ b/backend/remote-state/s3/backend_test.go @@ -249,6 +249,74 @@ func TestBackendExtraPaths(t *testing.T) { } } +func TestKeyEnv(t *testing.T) { + testACC(t) + keyName := "some/paths/tfstate" + + bucket0Name := fmt.Sprintf("terraform-remote-s3-test-%x-0", time.Now().Unix()) + b0 := backend.TestBackendConfig(t, New(), map[string]interface{}{ + "bucket": bucket0Name, + "key": keyName, + "encrypt": true, + "workspace_key_prefix": "", + }).(*Backend) + + createS3Bucket(t, b0.s3Client, bucket0Name) + defer deleteS3Bucket(t, b0.s3Client, bucket0Name) + + bucket1Name := fmt.Sprintf("terraform-remote-s3-test-%x-1", time.Now().Unix()) + b1 := backend.TestBackendConfig(t, New(), map[string]interface{}{ + "bucket": bucket1Name, + "key": keyName, + "encrypt": true, + "workspace_key_prefix": "project/env:", + }).(*Backend) + + createS3Bucket(t, b1.s3Client, bucket1Name) + defer deleteS3Bucket(t, b1.s3Client, bucket1Name) + + bucket2Name := fmt.Sprintf("terraform-remote-s3-test-%x-2", time.Now().Unix()) + b2 := backend.TestBackendConfig(t, New(), map[string]interface{}{ + "bucket": bucket2Name, + "key": keyName, + "encrypt": true, + }).(*Backend) + + createS3Bucket(t, b2.s3Client, bucket2Name) + defer deleteS3Bucket(t, b2.s3Client, bucket2Name) + + if err := testGetWorkspaceForKey(b0, "some/paths/tfstate", ""); err != nil { + t.Fatal(err) + } + + if err := testGetWorkspaceForKey(b0, "ws1/some/paths/tfstate", "ws1"); err != nil { + t.Fatal(err) + } + + if err := testGetWorkspaceForKey(b1, "project/env:/ws1/some/paths/tfstate", "ws1"); err != nil { + t.Fatal(err) + } + + if err := testGetWorkspaceForKey(b1, "project/env:/ws2/some/paths/tfstate", "ws2"); err != nil { + t.Fatal(err) + } + + if err := testGetWorkspaceForKey(b2, "env:/ws3/some/paths/tfstate", "ws3"); err != nil { + t.Fatal(err) + } + + backend.TestBackend(t, b0, nil) + backend.TestBackend(t, b1, nil) + backend.TestBackend(t, b2, nil) +} + +func testGetWorkspaceForKey(b *Backend, key string, expected string) error { + if actual := b.keyEnv(key); actual != expected { + return fmt.Errorf("incorrect workspace for key[%q]. Expected[%q]: Actual[%q]", key, expected, actual) + } + return nil +} + func checkStateList(b backend.Backend, expected []string) error { states, err := b.States() if err != nil {