diff --git a/internal/backend/remote-state/kubernetes/backend.go b/internal/backend/remote-state/kubernetes/backend.go index 8ba390dfb1..cd2f61ee44 100644 --- a/internal/backend/remote-state/kubernetes/backend.go +++ b/internal/backend/remote-state/kubernetes/backend.go @@ -9,6 +9,8 @@ import ( "log" "os" "path/filepath" + "strconv" + "strings" "github.com/mitchellh/go-homedir" "github.com/zclconf/go-cty/cty" @@ -54,7 +56,7 @@ func New() backend.Backend { "secret_suffix": { Type: cty.String, Required: true, - Description: "Suffix used when creating the secret. The secret will be named in the format: `tfstate-{workspace}-{secret_suffix}`.", + Description: "Suffix used when creating the secret. The secret will be named in the format: `tfstate-{workspace}-{secret_suffix}`. Note that the backend may append its own numeric index to the secret name when chunking large state files into multiple secrets. In this case, there will be multiple secrets named in the format: `tfstate-{workspace}-{secret_suffix}-{index}`.", }, "labels": { Type: cty.Map(cty.String), @@ -322,7 +324,17 @@ func (b *Backend) Configure(configVal cty.Value) tfdiags.Diagnostics { ns := data.String("namespace") b.namespace = ns + b.nameSuffix = data.String("secret_suffix") + if hasNumericSuffix(b.nameSuffix, "-") { + // If the last segment is a number, it's considered invalid. + // The backend automatically appends its own numeric suffix when chunking large state files into multiple secrets. + // Allowing a user-defined numeric suffix could cause conflicts with this mechanism. + return backendbase.ErrorAsDiagnostics( + fmt.Errorf("secret_suffix must not end with '-', got %q", b.nameSuffix), + ) + } + b.config = cfg return nil @@ -464,3 +476,16 @@ func decodeListOfString(v cty.Value) []string { } return ret } + +func hasNumericSuffix(value, substr string) bool { + // Find the last occurrence of '-' and get the part after it + if idx := strings.LastIndex(value, substr); idx != -1 { + lastPart := value[idx+1:] + // Try to convert the last part to an integer. + if _, err := strconv.Atoi(lastPart); err == nil { + return true + } + } + // Return false if no '-' is found or if the last part isn't numeric + return false +} diff --git a/internal/backend/remote-state/kubernetes/backend_test.go b/internal/backend/remote-state/kubernetes/backend_test.go index b86125aa53..699dc16098 100644 --- a/internal/backend/remote-state/kubernetes/backend_test.go +++ b/internal/backend/remote-state/kubernetes/backend_test.go @@ -197,3 +197,24 @@ func cleanupK8sResources(t *testing.T) { t.Fatal(errs) } } + +func Test_hasNumericSuffix(t *testing.T) { + tests := []struct { + input string + expected bool + }{ + {"my-secret-123", true}, + {"my-secret-abcd", false}, + {"nodashhere", false}, + {"another-secret-45abc", false}, + {"some-thing-1", true}, + {"some-thing-1-23", true}, + } + + for _, tt := range tests { + isNumeric := hasNumericSuffix(tt.input, "-") + if isNumeric != tt.expected { + t.Errorf("expected %v, got %v for input %s", tt.expected, isNumeric, tt.input) + } + } +} diff --git a/internal/backend/remote-state/kubernetes/client.go b/internal/backend/remote-state/kubernetes/client.go index 9e290dd1f9..5c44cb6627 100644 --- a/internal/backend/remote-state/kubernetes/client.go +++ b/internal/backend/remote-state/kubernetes/client.go @@ -102,6 +102,8 @@ func (c *RemoteClient) getSecrets() ([]unstructured.Unstructured, error) { for _, item := range res.Items { name := item.GetName() nameParts := strings.Split(name, "-") + // Because large Terraform state files are split into multiple secrets, + // we parse the index from the secret name. index, err := strconv.Atoi(nameParts[len(nameParts)-1]) if err != nil { index = 0 diff --git a/website/docs/language/backend/kubernetes.mdx b/website/docs/language/backend/kubernetes.mdx index 62a135f513..937c461a75 100644 --- a/website/docs/language/backend/kubernetes.mdx +++ b/website/docs/language/backend/kubernetes.mdx @@ -48,7 +48,7 @@ data "terraform_remote_state" "foo" { The following configuration options are supported: -* `secret_suffix` - (Required) Suffix used when creating secrets. Secrets will be named in the format: `tfstate-{workspace}-{secret_suffix}`. +* `secret_suffix` - (Required) Suffix used when creating the secret. The secret will be named in the format: `tfstate-{workspace}-{secret_suffix}`. Note that the backend may append its own numeric index to the secret name when chunking large state files into multiple secrets. In this case, there will be multiple secrets named in the format: `tfstate-{workspace}-{secret_suffix}-{index}`. * `labels` - (Optional) Map of additional labels to be applied to the secret and lease. * `namespace` - (Optional) Namespace to store the secret and lease in. Can be sourced from `KUBE_NAMESPACE`. * `in_cluster_config` - (Optional) Used to authenticate to the cluster from inside a pod. Can be sourced from `KUBE_IN_CLUSTER_CONFIG`.