diff --git a/backend/remote-state/s3/backend.go b/backend/remote-state/s3/backend.go index 1a1e10bab..41cf037a9 100644 --- a/backend/remote-state/s3/backend.go +++ b/backend/remote-state/s3/backend.go @@ -2,6 +2,8 @@ package s3 import ( "context" + "fmt" + "strings" "github.com/aws/aws-sdk-go/service/dynamodb" "github.com/aws/aws-sdk-go/service/s3" @@ -25,6 +27,14 @@ func New() backend.Backend { Type: schema.TypeString, Required: true, Description: "The path to the state file inside the bucket", + ValidateFunc: func(v interface{}, s string) ([]string, []error) { + // s3 will strip leading slashes from an object, so while this will + // technically be accepted by s3, it will break our workspace hierarchy. + if strings.HasPrefix(v.(string), "/") { + return nil, []error{fmt.Errorf("key must not start with '/'")} + } + return nil, nil + }, }, "region": { diff --git a/backend/remote-state/s3/backend_test.go b/backend/remote-state/s3/backend_test.go index c5a1f5009..83af43e45 100644 --- a/backend/remote-state/s3/backend_test.go +++ b/backend/remote-state/s3/backend_test.go @@ -11,6 +11,7 @@ import ( "github.com/aws/aws-sdk-go/service/dynamodb" "github.com/aws/aws-sdk-go/service/s3" "github.com/hashicorp/terraform/backend" + "github.com/hashicorp/terraform/config" "github.com/hashicorp/terraform/state/remote" "github.com/hashicorp/terraform/terraform" ) @@ -65,6 +66,28 @@ func TestBackendConfig(t *testing.T) { } } +func TestBackendConfig_invalidKey(t *testing.T) { + testACC(t) + cfg := map[string]interface{}{ + "region": "us-west-1", + "bucket": "tf-test", + "key": "/leading-slash", + "encrypt": true, + "dynamodb_table": "dynamoTable", + } + + rawCfg, err := config.NewRawConfig(cfg) + if err != nil { + t.Fatal(err) + } + resCfg := terraform.NewResourceConfig(rawCfg) + + _, errs := New().Validate(resCfg) + if len(errs) != 1 { + t.Fatal("expected config validation error") + } +} + func TestBackend(t *testing.T) { testACC(t)