From 5f463deae3643ca1c476bfc1494d4700ca5764b8 Mon Sep 17 00:00:00 2001 From: rmvangun <85766511+rmvangun@users.noreply.github.com> Date: Thu, 13 Feb 2025 18:50:09 -0500 Subject: [PATCH] Complete terraform backends (#609) * Fixed terraform backends * Update dependency helm/helm to v3.17.1 (#600) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * Update dependency aquaproj/aqua-renovate-config to v2.7.4 (#603) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * Update dependency lima-vm/lima to v1.0.6 (#601) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * Update dependency securego/gosec to v2.22.1 (#604) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * Update kubernetes packages to v0.32.2 (#605) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * Make windows friendly * Windows fix * Windows fix * Windows fix * tidy * Update dependency aquaproj/aqua-renovate-config to v2.7.4 (#603) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * Update dependency lima-vm/lima to v1.0.6 (#601) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * Update dependency securego/gosec to v2.22.1 (#604) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * Update dependency aquaproj/aqua-registry to v4.312.0 (#606) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * Update dependency kubernetes/kubectl to v1.32.2 (#610) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * Windows fix * Update google.golang.org/genproto digest to 5a70512 (#597) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * Update google.golang.org/genproto/googleapis/api digest to 5a70512 (#598) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * Update google.golang.org/genproto/googleapis/rpc digest to 5a70512 (#599) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * Update dependency aquaproj/aqua-renovate-config to v2.7.4 (#603) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * Update dependency lima-vm/lima to v1.0.6 (#601) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * Update dependency securego/gosec to v2.22.1 (#604) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * Update dependency aquaproj/aqua-registry to v4.312.0 (#606) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * Update dependency kubernetes/kubectl to v1.32.2 (#610) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * Update dependency siderolabs/talos to v1.9.4 (#611) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * Update module k8s.io/apiextensions-apiserver to v0.32.2 (#612) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- api/v1alpha1/terraform/terraform_config.go | 89 +++- .../terraform/terraform_config_test.go | 24 +- go.mod | 2 +- go.sum | 36 +- pkg/config/defaults.go | 8 +- pkg/env/shims.go | 8 + pkg/env/terraform_env.go | 259 +++++++--- pkg/env/terraform_env_test.go | 472 ++++++++++++++---- 8 files changed, 685 insertions(+), 213 deletions(-) diff --git a/api/v1alpha1/terraform/terraform_config.go b/api/v1alpha1/terraform/terraform_config.go index 930b2e74..56e017a5 100644 --- a/api/v1alpha1/terraform/terraform_config.go +++ b/api/v1alpha1/terraform/terraform_config.go @@ -2,11 +2,94 @@ package terraform // TerraformConfig represents the Terraform configuration type TerraformConfig struct { - Enabled *bool `yaml:"enabled,omitempty"` - Backend *string `yaml:"backend,omitempty"` + Enabled *bool `yaml:"enabled,omitempty"` + Backend *BackendConfig `yaml:"backend,omitempty"` } -// Merge performs a deep merge of the current TerraformConfig with another TerraformConfig. +type BackendConfig struct { + Type string `yaml:"type"` + S3 *S3Backend `yaml:"s3,omitempty"` + Kubernetes *KubernetesBackend `yaml:"kubernetes,omitempty"` + Local *LocalBackend `yaml:"local,omitempty"` +} + +// https://developer.hashicorp.com/terraform/language/backend/s3#configuration +type S3Backend struct { + Bucket *string `yaml:"bucket,omitempty"` + Key *string `yaml:"key,omitempty"` + Region *string `yaml:"region,omitempty"` + AccessKey *string `yaml:"access_key,omitempty"` + SecretKey *string `yaml:"secret_key,omitempty"` + SessionToken *string `yaml:"session_token,omitempty"` + RoleArn *string `yaml:"role_arn,omitempty"` + ExternalId *string `yaml:"external_id,omitempty"` + Profile *string `yaml:"profile,omitempty"` + SharedCredentialsFiles *[]string `yaml:"shared_credentials_files,omitempty"` + Token *string `yaml:"token,omitempty"` + SkipCredentialsValidation *bool `yaml:"skip_credentials_validation,omitempty"` + SkipRegionValidation *bool `yaml:"skip_region_validation,omitempty"` + SkipRequestingAccountId *bool `yaml:"skip_requesting_account_id,omitempty"` + SkipMetadataApiCheck *bool `yaml:"skip_metadata_api_check,omitempty"` + SkipS3Checksum *bool `yaml:"skip_s3_checksum,omitempty"` + UseDualstackEndpoint *bool `yaml:"use_dualstack_endpoint,omitempty"` + UseFipsEndpoint *bool `yaml:"use_fips_endpoint,omitempty"` + DynamoDBTable *string `yaml:"dynamodb_table,omitempty"` + UseLockfile *bool `yaml:"use_lockfile,omitempty"` + AllowedAccountIds *[]string `yaml:"allowed_account_ids,omitempty"` + CustomCaBundle *string `yaml:"custom_ca_bundle,omitempty"` + Ec2MetadataServiceEndpoint *string `yaml:"ec2_metadata_service_endpoint,omitempty"` + Ec2MetadataServiceEndpointMode *string `yaml:"ec2_metadata_service_endpoint_mode,omitempty"` + ForbiddenAccountIds *[]string `yaml:"forbidden_account_ids,omitempty"` + HttpProxy *string `yaml:"http_proxy,omitempty"` + HttpsProxy *string `yaml:"https_proxy,omitempty"` + Insecure *bool `yaml:"insecure,omitempty"` + NoProxy *string `yaml:"no_proxy,omitempty"` + MaxRetries *int `yaml:"max_retries,omitempty"` + RetryMode *string `yaml:"retry_mode,omitempty"` + SharedConfigFiles *[]string `yaml:"shared_config_files,omitempty"` + StsRegion *string `yaml:"sts_region,omitempty"` + UsePathStyle *bool `yaml:"use_path_style,omitempty"` + WorkspaceKeyPrefix *string `yaml:"workspace_key_prefix,omitempty"` + KmsKeyId *string `yaml:"kms_key_id,omitempty"` + SseCustomerKey *string `yaml:"sse_customer_key,omitempty"` +} + +// KubernetesBackend represents the configuration for the Kubernetes backend +type KubernetesBackend struct { + SecretSuffix *string `yaml:"secret_suffix,omitempty"` + Labels *map[string]string `yaml:"labels,omitempty"` + Namespace *string `yaml:"namespace,omitempty"` + InClusterConfig *bool `yaml:"in_cluster_config,omitempty"` + Host *string `yaml:"host,omitempty"` + Username *string `yaml:"username,omitempty"` + Password *string `yaml:"password,omitempty"` + Insecure *bool `yaml:"insecure,omitempty"` + ClientCertificate *string `yaml:"client_certificate,omitempty"` + ClientKey *string `yaml:"client_key,omitempty"` + ClusterCACertificate *string `yaml:"cluster_ca_certificate,omitempty"` + ConfigPath *string `yaml:"config_path,omitempty"` + ConfigPaths *[]string `yaml:"config_paths,omitempty"` + ConfigContext *string `yaml:"config_context,omitempty"` + ConfigContextAuthInfo *string `yaml:"config_context_auth_info,omitempty"` + ConfigContextCluster *string `yaml:"config_context_cluster,omitempty"` + Token *string `yaml:"token,omitempty"` + Exec *ExecConfig `yaml:"exec,omitempty"` +} + +// https://developer.hashicorp.com/terraform/language/backend/local#configuration-variables +type LocalBackend struct { + Path *string `yaml:"path,omitempty"` +} + +// ExecConfig represents the exec-based credential plugin configuration +type ExecConfig struct { + APIVersion *string `yaml:"api_version,omitempty"` + Command *string `yaml:"command,omitempty"` + Args *[]string `yaml:"args,omitempty"` + Env *map[string]string `yaml:"env,omitempty"` +} + +// Merge performs a simple merge of the current TerraformConfig with another TerraformConfig. func (base *TerraformConfig) Merge(overlay *TerraformConfig) { if overlay.Enabled != nil { base.Enabled = overlay.Enabled diff --git a/api/v1alpha1/terraform/terraform_config_test.go b/api/v1alpha1/terraform/terraform_config_test.go index bb16730d..3cbb47de 100644 --- a/api/v1alpha1/terraform/terraform_config_test.go +++ b/api/v1alpha1/terraform/terraform_config_test.go @@ -13,21 +13,21 @@ func TestTerraformConfig_Merge(t *testing.T) { } overlay := &TerraformConfig{ Enabled: ptrBool(true), - Backend: ptrString("backend"), + Backend: &BackendConfig{Type: "s3"}, } base.Merge(overlay) if base.Enabled == nil || *base.Enabled != true { t.Errorf("Enabled mismatch: expected %v, got %v", true, *base.Enabled) } - if base.Backend == nil || *base.Backend != "backend" { - t.Errorf("Backend mismatch: expected %v, got %v", "backend", *base.Backend) + if base.Backend == nil || base.Backend.Type != "s3" { + t.Errorf("Backend mismatch: expected %v, got %v", "s3", base.Backend.Type) } }) t.Run("MergeWithNilValues", func(t *testing.T) { base := &TerraformConfig{ Enabled: ptrBool(false), - Backend: ptrString("old-backend"), + Backend: &BackendConfig{Type: "s3"}, } overlay := &TerraformConfig{ Enabled: nil, @@ -37,8 +37,8 @@ func TestTerraformConfig_Merge(t *testing.T) { if base.Enabled == nil || *base.Enabled != false { t.Errorf("Enabled mismatch: expected %v, got %v", false, *base.Enabled) } - if base.Backend == nil || *base.Backend != "old-backend" { - t.Errorf("Backend mismatch: expected %v, got %v", "old-backend", *base.Backend) + if base.Backend == nil || base.Backend.Type != "s3" { + t.Errorf("Backend mismatch: expected %v, got %v", "s3", base.Backend.Type) } }) } @@ -47,7 +47,7 @@ func TestTerraformConfig_Copy(t *testing.T) { t.Run("CopyWithNonNilValues", func(t *testing.T) { original := &TerraformConfig{ Enabled: ptrBool(true), - Backend: ptrString("backend"), + Backend: &BackendConfig{Type: "s3"}, } copy := original.Copy() @@ -61,9 +61,9 @@ func TestTerraformConfig_Copy(t *testing.T) { if original.Enabled == nil || *original.Enabled == *copy.Enabled { t.Errorf("Original Enabled was modified: expected %v, got %v", true, *copy.Enabled) } - copy.Backend = ptrString("modified-backend") - if original.Backend == nil || *original.Backend == *copy.Backend { - t.Errorf("Original Backend was modified: expected %v, got %v", "backend", *copy.Backend) + copy.Backend = &BackendConfig{Type: "local"} + if original.Backend == nil || original.Backend.Type == copy.Backend.Type { + t.Errorf("Original Backend was modified: expected %v, got %v", "s3", copy.Backend.Type) } }) @@ -90,10 +90,6 @@ func TestTerraformConfig_Copy(t *testing.T) { } // Helper functions to create pointers for basic types -func ptrString(s string) *string { - return &s -} - func ptrBool(b bool) *bool { return &b } diff --git a/go.mod b/go.mod index 67c6aa7e..585ef60f 100644 --- a/go.mod +++ b/go.mod @@ -32,7 +32,7 @@ require ( cloud.google.com/go/auth v0.14.1 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.7 // indirect cloud.google.com/go/compute/metadata v0.6.0 // indirect - cloud.google.com/go/iam v1.3.1 // indirect + cloud.google.com/go/iam v1.4.0 // indirect cloud.google.com/go/kms v1.20.5 // indirect cloud.google.com/go/longrunning v0.6.4 // indirect cloud.google.com/go/monitoring v1.24.0 // indirect diff --git a/go.sum b/go.sum index 6e7e48a7..11318293 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,5 @@ c2sp.org/CCTV/age v0.0.0-20240306222714-3ec4d716e805 h1:u2qwJeEvnypw+OCPUHmoZE3IqwfuN5kgDfo5MLzpNM0= c2sp.org/CCTV/age v0.0.0-20240306222714-3ec4d716e805/go.mod h1:FomMrUJ2Lxt5jCLmZkG3FHa72zUprnhd3v/Z18Snm4w= -cel.dev/expr v0.19.2 h1:V354PbqIXr9IQdwy4SYA4xa0HXaWq1BUPAGzugBY5V4= -cel.dev/expr v0.19.2/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw= cel.dev/expr v0.20.0 h1:OunBvVCfvpWlt4dN7zg3FM6TDkzOePe1+foGJ9AXeeI= cel.dev/expr v0.20.0/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw= cloud.google.com/go v0.118.2 h1:bKXO7RXMFDkniAAvvuMrAPtQ/VHrs9e7J5UT3yrGdTY= @@ -12,8 +10,8 @@ cloud.google.com/go/auth/oauth2adapt v0.2.7 h1:/Lc7xODdqcEw8IrZ9SvwnlLX6j9FHQM74 cloud.google.com/go/auth/oauth2adapt v0.2.7/go.mod h1:NTbTTzfvPl1Y3V1nPpOgl2w6d/FjO7NNUQaWSox6ZMc= cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I= cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg= -cloud.google.com/go/iam v1.3.1 h1:KFf8SaT71yYq+sQtRISn90Gyhyf4X8RGgeAVC8XGf3E= -cloud.google.com/go/iam v1.3.1/go.mod h1:3wMtuyT4NcbnYNPLMBzYRFiEfjKfJlLVLrisE7bwm34= +cloud.google.com/go/iam v1.4.0 h1:ZNfy/TYfn2uh/ukvhp783WhnbVluqf/tzOaqVUPlIPA= +cloud.google.com/go/iam v1.4.0/go.mod h1:gMBgqPaERlriaOV0CUl//XUzDhSfXevn4OEUbg6VRs4= cloud.google.com/go/kms v1.20.5 h1:aQQ8esAIVZ1atdJRxihhdxGQ64/zEbJoJnCz/ydSmKg= cloud.google.com/go/kms v1.20.5/go.mod h1:C5A8M1sv2YWYy1AE6iSrnddSG9lRGdJq5XEdBy28Lmw= cloud.google.com/go/logging v1.13.0 h1:7j0HgAp0B94o1YRDqiqm26w4q1rDMH7XNRU34lJXHYc= @@ -32,12 +30,10 @@ filippo.io/age v1.2.1 h1:X0TZjehAZylOIj4DubWYU1vWQxv9bJpo+Uu2/LGhi1o= filippo.io/age v1.2.1/go.mod h1:JL9ew2lTN+Pyft4RiNGguFfOpewKwSHm5ayKD/A4004= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0 h1:g0EZJwz7xkXQiZAI5xi9f3WWFYBlX1CPTrR+NDToRkQ= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0/go.mod h1:XCW7KnZet0Opnr7HccfUw1PLc4CjHqpcaxW8DHklNkQ= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.1 h1:1mvYtZfWQAnwNah/C+Z+Jb9rQH95LPE2vlmMuWAHJk8= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.1/go.mod h1:75I/mXtme1JyWFtz8GocPHVFyH421IBoZErnO16dd0k= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.2 h1:F0gBpfdPLGsw+nsgk6aqqkZS1jiixa5WwFe3fk/T3Ys= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.2/go.mod h1:SqINnQ9lVVdRlyC8cd1lCI0SdX4n2paeABd2K8ggfnE= -github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.1 h1:Bk5uOhSAenHyR5P61D/NzeQCv+4fEVV8mOkJ82NqpWw= -github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.1/go.mod h1:QZ4pw3or1WPmRBxf0cHd1tknzrT54WPBOQoGutCPvSU= +github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY= +github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2/go.mod h1:Pa9ZNPuoNu/GztvBSKk9J1cDJW6vk/n0zLtV4mgd8N8= github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 h1:ywEEhmNahHBihViHepv3xPBn1663uRv2t2q/ESv9seY= github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0/go.mod h1:iZDifYGJTIgIIkYRNWPENUnqx6bJ2xnSDFI2tjwZNuY= github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.3.0 h1:7rKG7UmnrxX4N53TFhkYqjc+kVUZuw0fL8I3Fh+Ld9E= @@ -81,8 +77,6 @@ github.com/aws/aws-sdk-go-v2/credentials v1.17.59 h1:9btwmrt//Q6JcSdgJOLI98sdr5p github.com/aws/aws-sdk-go-v2/credentials v1.17.59/go.mod h1:NM8fM6ovI3zak23UISdWidyZuI1ghNe2xjzUZAyT+08= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.28 h1:KwsodFKVQTlI5EyhRSugALzsV6mG/SGrdjlMXSZSdso= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.28/go.mod h1:EY3APf9MzygVhKuPXAc5H+MkGb8k/DOSQjWS0LgkKqI= -github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.60 h1:ssZzp6JAGAbOYUTppPfKLa3Cbmx0PtnPsjh4RSy06Ao= -github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.60/go.mod h1:0fi8BNjII7rWunx2Cvezfnu1iZDCw7EWEiSQyC+Kgww= github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.61 h1:BBIPjlEWLxX1huGTkBu/eeqyaXC0pVwDCYbQuE/JPfU= github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.61/go.mod h1:6dkLZQM1D/wKKFJEvyB1OCXJ0f68wcIPDOiXm0KyT8A= github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.32 h1:BjUcr3X3K0wZPGFg2bxOWW3VPN8rkE3/61zhP+IHviA= @@ -173,8 +167,6 @@ github.com/fluxcd/pkg/apis/kustomize v1.9.0 h1:SJpT1CK58AnTvCpDKeGfMNA0Xud/4VReZ github.com/fluxcd/pkg/apis/kustomize v1.9.0/go.mod h1:AZl2GU03oPVue6SUivdiIYd/3mvF94j7t1G2JO26d4s= github.com/fluxcd/pkg/apis/meta v1.10.0 h1:rqbAuyl5ug7A5jjRf/rNwBXmNl6tJ9wG2iIsriwnQUk= github.com/fluxcd/pkg/apis/meta v1.10.0/go.mod h1:n7NstXHDaleAUMajcXTVkhz0MYkvEXy1C/eLI/t1xoI= -github.com/fluxcd/source-controller/api v1.4.1 h1:zV01D7xzHOXWbYXr36lXHWWYS7POARsjLt61Nbh3kVY= -github.com/fluxcd/source-controller/api v1.4.1/go.mod h1:gSjg57T+IG66SsBR0aquv+DFrm4YyBNpKIJVDnu3Ya8= github.com/fluxcd/source-controller/api v1.5.0 h1:caSR+u/r2Vh0jq/0pNR0r1zLxyvgatWuGSV2mxgTB/I= github.com/fluxcd/source-controller/api v1.5.0/go.mod h1:OZPuHMlLH2E2mnj6Q5DLkWfUOmJ20zA1LIvUVfNsYl8= github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= @@ -205,10 +197,6 @@ github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68= github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= -github.com/goccy/go-yaml v1.15.19 h1:ivDxLiW6SbmqPZwSAM9Yq+Yr68C9FLbTNyuH3ITizxQ= -github.com/goccy/go-yaml v1.15.19/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= -github.com/goccy/go-yaml v1.15.20 h1:eQHFLrr1lpLYAxupPD9ThZbGtncPl9nyu3nkAayEZgY= -github.com/goccy/go-yaml v1.15.20/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/goccy/go-yaml v1.15.22 h1:iQI1hvCoiYYiVFq76P4AI8ImgDOfgiyKnl/AWjK8/gA= github.com/goccy/go-yaml v1.15.22/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= @@ -266,8 +254,6 @@ github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/hcl/v2 v2.23.0 h1:Fphj1/gCylPxHutVSEOf2fBOh1VE4AuLV7+kbJf3qos= github.com/hashicorp/hcl/v2 v2.23.0/go.mod h1:62ZYHrXgPoX8xBnzl8QzbWq4dyDsDtfCRgIq1rbJEvA= -github.com/hashicorp/vault/api v1.15.0 h1:O24FYQCWwhwKnF7CuSqP30S51rTV7vz1iACXE/pj5DA= -github.com/hashicorp/vault/api v1.15.0/go.mod h1:+5YTO09JGn0u+b6ySD/LLVf8WkJCPLAL2Vkmrn2+CM8= github.com/hashicorp/vault/api v1.16.0 h1:nbEYGJiAPGzT9U4oWgaaB0g+Rj8E59QuHKyA5LhwQN4= github.com/hashicorp/vault/api v1.16.0/go.mod h1:KhuUhzOD8lDSk29AtzNjgAu2kxRA9jL9NAbkFlqvkBA= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= @@ -458,16 +444,10 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.220.0 h1:3oMI4gdBgB72WFVwE1nerDD8W3HUOS4kypK6rRLbGns= google.golang.org/api v0.220.0/go.mod h1:26ZAlY6aN/8WgpCzjPNy18QpYaz7Zgg1h0qe1GkZEmY= -google.golang.org/genproto v0.0.0-20250207221924-e9438ea467c6 h1:SSk8oMbcHFbMwftDvX4PHbkqss3RkEZUF+k1h9d/sns= -google.golang.org/genproto v0.0.0-20250207221924-e9438ea467c6/go.mod h1:wkQ2Aj/xvshAUDtO/JHvu9y+AaN9cqs28QuSVSHtZSY= google.golang.org/genproto v0.0.0-20250212204824-5a70512c5d8b h1:TdBaFxGAABTI8sz9jYHPtjje677pS4XXup9vJMlj8hQ= google.golang.org/genproto v0.0.0-20250212204824-5a70512c5d8b/go.mod h1:0TrvLFkilZy+XULmuoWfiTbTRXLWXJ1S44jQTW3lWwE= -google.golang.org/genproto/googleapis/api v0.0.0-20250207221924-e9438ea467c6 h1:L9JNMl/plZH9wmzQUHleO/ZZDSN+9Gh41wPczNy+5Fk= -google.golang.org/genproto/googleapis/api v0.0.0-20250207221924-e9438ea467c6/go.mod h1:iYONQfRdizDB8JJBybql13nArx91jcUk7zCXEsOofM4= google.golang.org/genproto/googleapis/api v0.0.0-20250212204824-5a70512c5d8b h1:i+d0RZa8Hs2L/MuaOQYI+krthcxdEbEM2N+Tf3kJ4zk= google.golang.org/genproto/googleapis/api v0.0.0-20250212204824-5a70512c5d8b/go.mod h1:iYONQfRdizDB8JJBybql13nArx91jcUk7zCXEsOofM4= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250207221924-e9438ea467c6 h1:2duwAxN2+k0xLNpjnHTXoMUgnv6VPSp5fiqTuwSxjmI= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250207221924-e9438ea467c6/go.mod h1:8BS3B93F/U1juMFq9+EDk+qOT5CO1R9IzXxG3PTqiRk= google.golang.org/genproto/googleapis/rpc v0.0.0-20250212204824-5a70512c5d8b h1:FQtJ1MxbXoIIrZHZ33M+w5+dAP9o86rgpjoKr/ZmT7k= google.golang.org/genproto/googleapis/rpc v0.0.0-20250212204824-5a70512c5d8b/go.mod h1:8BS3B93F/U1juMFq9+EDk+qOT5CO1R9IzXxG3PTqiRk= google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ= @@ -490,20 +470,12 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o= gotest.tools/v3 v3.4.0/go.mod h1:CtbdzLSsqVhDgMtKsx03ird5YTGB3ar27v0u/yKBW5g= -k8s.io/api v0.32.1 h1:f562zw9cy+GvXzXf0CKlVQ7yHJVYzLfL6JAS4kOAaOc= -k8s.io/api v0.32.1/go.mod h1:/Yi/BqkuueW1BgpoePYBRdDYfjPF5sgTr5+YqDZra5k= k8s.io/api v0.32.2 h1:bZrMLEkgizC24G9eViHGOPbW+aRo9duEISRIJKfdJuw= k8s.io/api v0.32.2/go.mod h1:hKlhk4x1sJyYnHENsrdCWw31FEmCijNGPJO5WzHiJ6Y= -k8s.io/apiextensions-apiserver v0.32.1 h1:hjkALhRUeCariC8DiVmb5jj0VjIc1N0DREP32+6UXZw= -k8s.io/apiextensions-apiserver v0.32.1/go.mod h1:sxWIGuGiYov7Io1fAS2X06NjMIk5CbRHc2StSmbaQto= k8s.io/apiextensions-apiserver v0.32.2 h1:2YMk285jWMk2188V2AERy5yDwBYrjgWYggscghPCvV4= k8s.io/apiextensions-apiserver v0.32.2/go.mod h1:GPwf8sph7YlJT3H6aKUWtd0E+oyShk/YHWQHf/OOgCA= -k8s.io/apimachinery v0.32.1 h1:683ENpaCBjma4CYqsmZyhEzrGz6cjn1MY/X2jB2hkZs= -k8s.io/apimachinery v0.32.1/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= k8s.io/apimachinery v0.32.2 h1:yoQBR9ZGkA6Rgmhbp/yuT9/g+4lxtsGYwW6dR6BDPLQ= k8s.io/apimachinery v0.32.2/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= -k8s.io/client-go v0.32.1 h1:otM0AxdhdBIaQh7l1Q0jQpmo7WOFIk5FFa4bg6YMdUU= -k8s.io/client-go v0.32.1/go.mod h1:aTTKZY7MdxUaJ/KiUs8D+GssR9zJZi77ZqtzcGXIiDg= k8s.io/client-go v0.32.2 h1:4dYCD4Nz+9RApM2b/3BtVvBHw54QjMFUl1OLcJG5yOA= k8s.io/client-go v0.32.2/go.mod h1:fpZ4oJXclZ3r2nDOv+Ux3XcJutfrwjKTCHz2H3sww94= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= diff --git a/pkg/config/defaults.go b/pkg/config/defaults.go index b8b7e686..9003f2ad 100644 --- a/pkg/config/defaults.go +++ b/pkg/config/defaults.go @@ -23,7 +23,9 @@ import ( var DefaultConfig = v1alpha1.Context{ Terraform: &terraform.TerraformConfig{ Enabled: ptrBool(true), - Backend: ptrString("local"), + Backend: &terraform.BackendConfig{ + Type: "local", + }, }, } @@ -65,7 +67,9 @@ var commonGitConfig = git.GitConfig{ var commonTerraformConfig = terraform.TerraformConfig{ Enabled: ptrBool(true), - Backend: ptrString("local"), + Backend: &terraform.BackendConfig{ + Type: "local", + }, } var commonClusterConfig = cluster.ClusterConfig{ diff --git a/pkg/env/shims.go b/pkg/env/shims.go index ce2b310d..79ad71c1 100644 --- a/pkg/env/shims.go +++ b/pkg/env/shims.go @@ -27,6 +27,14 @@ var readDir = os.ReadDir // Wrapper function for yaml.Unmarshal var yamlUnmarshal = yaml.Unmarshal +// Wrapper function for yaml.Marshal +var yamlMarshal = yaml.Marshal + +// intPtr returns a pointer to an int value +func intPtr(i int) *int { + return &i +} + // stringPtr returns a pointer to a string value func stringPtr(s string) *string { return &s diff --git a/pkg/env/terraform_env.go b/pkg/env/terraform_env.go index cd19caa1..0364f696 100644 --- a/pkg/env/terraform_env.go +++ b/pkg/env/terraform_env.go @@ -5,6 +5,7 @@ import ( "os" "path/filepath" "regexp" + "sort" "strings" "github.com/windsorcli/cli/pkg/di" @@ -65,8 +66,18 @@ func (e *TerraformEnvPrinter) GetEnvVars() (map[string]string, error) { tfDataDir := filepath.ToSlash(filepath.Join(configRoot, ".terraform", projectPath)) tfPlanPath := filepath.ToSlash(filepath.Join(tfDataDir, "terraform.tfplan")) + backendConfigArgs, err := e.generateBackendConfigArgs(projectPath, configRoot) + if err != nil { + return nil, fmt.Errorf("error generating backend config args: %w", err) + } + + initArgs := []string{ + "-backend=true", + strings.Join(backendConfigArgs, " "), + } + envVars["TF_DATA_DIR"] = strings.TrimSpace(tfDataDir) - envVars["TF_CLI_ARGS_init"] = strings.TrimSpace("-backend=true") + envVars["TF_CLI_ARGS_init"] = strings.TrimSpace(strings.Join(initArgs, " ")) envVars["TF_CLI_ARGS_plan"] = strings.TrimSpace(fmt.Sprintf("-out=\"%s\" %s", tfPlanPath, strings.Join(varFileArgs, " "))) envVars["TF_CLI_ARGS_apply"] = strings.TrimSpace(fmt.Sprintf("\"%s\"", tfPlanPath)) envVars["TF_CLI_ARGS_import"] = strings.TrimSpace(strings.Join(varFileArgs, " ")) @@ -96,9 +107,6 @@ func (e *TerraformEnvPrinter) Print() error { return e.BaseEnvPrinter.Print(envVars) } -// Ensure TerraformEnvPrinter implements the EnvPrinter interface. -var _ EnvPrinter = (*TerraformEnvPrinter)(nil) - // getAlias returns command aliases based on Localstack configuration. func (e *TerraformEnvPrinter) getAlias() (map[string]string, error) { enableLocalstack := e.configHandler.GetBool("aws.localstack.create", false) @@ -110,39 +118,180 @@ func (e *TerraformEnvPrinter) getAlias() (map[string]string, error) { return map[string]string{"terraform": ""}, nil } -// findRelativeTerraformProjectPath locates the Terraform project path by checking the current -// directory and its ancestors for Terraform files, returning the relative path if found. -func findRelativeTerraformProjectPath() (string, error) { +// generateBackendOverrideTf creates the backend_override.tf file for the project by determining +// the backend type and writing the appropriate configuration to the file. +func (e *TerraformEnvPrinter) generateBackendOverrideTf() error { currentPath, err := getwd() if err != nil { - return "", fmt.Errorf("error getting current directory: %w", err) + return fmt.Errorf("error getting current directory: %w", err) } - currentPath = filepath.Clean(currentPath) + projectPath, err := findRelativeTerraformProjectPath() + if err != nil { + return fmt.Errorf("error finding project path: %w", err) + } - globPattern := filepath.Join(currentPath, "*.tf") - matches, err := glob(globPattern) + if projectPath == "" { + return nil + } + + contextConfig := e.configHandler.GetConfig() + backend := contextConfig.Terraform.Backend + + backendOverridePath := filepath.Join(currentPath, "backend_override.tf") + var backendConfig string + + switch backend.Type { + case "local": + backendConfig = fmt.Sprintf(`terraform { + backend "local" {} +}`) + case "s3": + backendConfig = fmt.Sprintf(`terraform { + backend "s3" {} +}`) + case "kubernetes": + backendConfig = fmt.Sprintf(`terraform { + backend "kubernetes" {} +}`) + default: + return fmt.Errorf("unsupported backend: %s", backend.Type) + } + + err = writeFile(backendOverridePath, []byte(backendConfig), os.ModePerm) if err != nil { - return "", fmt.Errorf("error finding project path: %w", err) + return fmt.Errorf("error writing backend_override.tf: %w", err) } - if len(matches) == 0 { - return "", nil + + return nil +} + +// generateBackendConfigArgs constructs backend config args for terraform init. +// It reads the backend type from the config and adds relevant key-value pairs. +// The function supports local, s3, and kubernetes backends. +// It also includes backend.tfvars if present in the context directory. +func (e *TerraformEnvPrinter) generateBackendConfigArgs(projectPath, configRoot string) ([]string, error) { + backend := e.configHandler.GetConfig().Terraform.Backend + backendType := e.configHandler.GetString("terraform.backend.type", "") + if backendType == "" { + switch { + case backend.S3 != nil: + backendType = "s3" + case backend.Kubernetes != nil: + backendType = "kubernetes" + case backend.Local != nil: + backendType = "local" + default: + backendType = "local" + } } - pathParts := strings.Split(currentPath, string(os.PathSeparator)) - for i := len(pathParts) - 1; i >= 0; i-- { - if strings.EqualFold(pathParts[i], "terraform") || strings.EqualFold(pathParts[i], ".tf_modules") { - relativePath := filepath.Join(pathParts[i+1:]...) - return relativePath, nil + var backendConfigArgs []string + + addBackendConfigArg := func(key, value string) { + if value != "" { + backendConfigArgs = append(backendConfigArgs, fmt.Sprintf("-backend-config=\"%s=%s\"", key, filepath.ToSlash(value))) } } - return "", nil + if context := e.configHandler.GetContext(); context != "" { + backendTfvarsPath := filepath.Join(configRoot, "terraform", "backend.tfvars") + if _, err := stat(backendTfvarsPath); err == nil { + backendConfigArgs = append(backendConfigArgs, fmt.Sprintf("-backend-config=\"%s\"", filepath.ToSlash(backendTfvarsPath))) + } + } + + switch backendType { + case "local": + addBackendConfigArg("path", filepath.ToSlash(filepath.Join(configRoot, ".tfstate", projectPath, "terraform.tfstate"))) + case "s3": + addBackendConfigArg("key", filepath.ToSlash(filepath.Join(projectPath, "terraform.tfstate"))) + if backend.S3 != nil { + if err := processBackendConfig(backend.S3, addBackendConfigArg); err != nil { + return nil, fmt.Errorf("error processing S3 backend config: %w", err) + } + } + case "kubernetes": + addBackendConfigArg("secret_suffix", sanitizeForK8s(projectPath)) + if backend.Kubernetes != nil { + if err := processBackendConfig(backend.Kubernetes, addBackendConfigArg); err != nil { + return nil, fmt.Errorf("error processing Kubernetes backend config: %w", err) + } + } + default: + return nil, fmt.Errorf("unsupported backend: %s", backend.Type) + } + + return backendConfigArgs, nil +} + +// Ensure TerraformEnvPrinter implements the EnvPrinter interface. +var _ EnvPrinter = (*TerraformEnvPrinter)(nil) + +// processBackendConfig processes the backend config and adds the key-value pairs to the backend config args. +var processBackendConfig = func(backendConfig interface{}, addArg func(key, value string)) error { + yamlData, err := yamlMarshal(backendConfig) + if err != nil { + return fmt.Errorf("error marshalling backend to YAML: %w", err) + } + + var configMap map[string]interface{} + if err := yamlUnmarshal(yamlData, &configMap); err != nil { + return fmt.Errorf("error unmarshalling backend YAML: %w", err) + } + + var args []string + processMap("", configMap, func(key, value string) { + args = append(args, fmt.Sprintf("%s=%s", key, value)) + }) + + sort.Strings(args) + for _, arg := range args { + parts := strings.SplitN(arg, "=", 2) + if len(parts) == 2 { + addArg(parts[0], parts[1]) + } + } + + return nil +} + +// processMap processes a map and adds the key-value pairs to the backend config args. +func processMap(prefix string, configMap map[string]interface{}, addArg func(key, value string)) { + keys := make([]string, 0, len(configMap)) + for key := range configMap { + keys = append(keys, key) + } + sort.Strings(keys) + + for _, key := range keys { + fullKey := key + if prefix != "" { + fullKey = fmt.Sprintf("%s.%s", prefix, key) + } + + switch v := configMap[key].(type) { + case string: + addArg(fullKey, v) + case bool: + addArg(fullKey, fmt.Sprintf("%t", v)) + case int, uint64: + addArg(fullKey, fmt.Sprintf("%d", v)) + case []interface{}: + for _, item := range v { + if strItem, ok := item.(string); ok { + addArg(fullKey, strItem) + } + } + case map[string]interface{}: + processMap(fullKey, v, addArg) + } + } } // sanitizeForK8s ensures a string is compatible with Kubernetes naming conventions by converting // to lowercase, replacing invalid characters, and trimming to a maximum length of 63 characters. -func sanitizeForK8s(input string) string { +var sanitizeForK8s = func(input string) string { sanitized := strings.ToLower(input) sanitized = regexp.MustCompile(`[_]+`).ReplaceAllString(sanitized, "-") sanitized = regexp.MustCompile(`[^a-z0-9-]`).ReplaceAllString(sanitized, "-") @@ -154,66 +303,32 @@ func sanitizeForK8s(input string) string { return sanitized } -// generateBackendOverrideTf creates the backend_override.tf file for the project by determining -// the backend type and writing the appropriate configuration to the file. -func (e *TerraformEnvPrinter) generateBackendOverrideTf() error { +// findRelativeTerraformProjectPath locates the Terraform project path by checking the current +// directory and its ancestors for Terraform files, returning the relative path if found. +var findRelativeTerraformProjectPath = func() (string, error) { currentPath, err := getwd() if err != nil { - return fmt.Errorf("error getting current directory: %w", err) - } - - projectPath, err := findRelativeTerraformProjectPath() - if err != nil { - return fmt.Errorf("error finding project path: %w", err) + return "", fmt.Errorf("error getting current directory: %w", err) } - if projectPath == "" { - return nil - } + currentPath = filepath.Clean(currentPath) - configRoot, err := e.configHandler.GetConfigRoot() + globPattern := filepath.Join(currentPath, "*.tf") + matches, err := glob(globPattern) if err != nil { - return fmt.Errorf("error getting config root: %w", err) + return "", fmt.Errorf("error finding project path: %w", err) } - - contextConfig := e.configHandler.GetConfig() - backend := contextConfig.Terraform.Backend - - backendOverridePath := filepath.Join(currentPath, "backend_override.tf") - var backendConfig string - - switch *backend { - case "local": - backendConfig = fmt.Sprintf(` -terraform { - backend "local" { - path = "%s" - } -}`, filepath.ToSlash(filepath.Join(configRoot, ".tfstate", projectPath, "terraform.tfstate"))) - case "s3": - key := filepath.ToSlash(filepath.Join(projectPath, "terraform.tfstate")) - backendConfig = fmt.Sprintf(` -terraform { - backend "s3" { - key = "%s" - } -}`, key) - case "kubernetes": - projectNameSanitized := sanitizeForK8s(projectPath) - backendConfig = fmt.Sprintf(` -terraform { - backend "kubernetes" { - secret_suffix = "%s" - } -}`, projectNameSanitized) - default: - return fmt.Errorf("unsupported backend: %s", *backend) + if len(matches) == 0 { + return "", nil } - err = writeFile(backendOverridePath, []byte(backendConfig), os.ModePerm) - if err != nil { - return fmt.Errorf("error writing backend_override.tf: %w", err) + pathParts := strings.Split(currentPath, string(os.PathSeparator)) + for i := len(pathParts) - 1; i >= 0; i-- { + if strings.EqualFold(pathParts[i], "terraform") || strings.EqualFold(pathParts[i], ".tf_modules") { + relativePath := filepath.Join(pathParts[i+1:]...) + return relativePath, nil + } } - return nil + return "", nil } diff --git a/pkg/env/terraform_env_test.go b/pkg/env/terraform_env_test.go index a1afb993..ed9984ed 100644 --- a/pkg/env/terraform_env_test.go +++ b/pkg/env/terraform_env_test.go @@ -5,6 +5,7 @@ import ( "os" "path/filepath" "reflect" + "sort" "strings" "testing" @@ -39,7 +40,9 @@ func setupSafeTerraformEnvMocks(injector ...di.Injector) *TerraformEnvMocks { mockConfigHandler.GetConfigFunc = func() *v1alpha1.Context { return &v1alpha1.Context{ Terraform: &terraform.TerraformConfig{ - Backend: stringPtr("local"), + Backend: &terraform.BackendConfig{ + Type: "local", + }, }, } } @@ -50,6 +53,10 @@ func setupSafeTerraformEnvMocks(injector ...di.Injector) *TerraformEnvMocks { mockInjector.Register("shell", mockShell) mockInjector.Register("configHandler", mockConfigHandler) + stat = func(name string) (os.FileInfo, error) { + return nil, nil + } + return &TerraformEnvMocks{ Injector: mockInjector, Shell: mockShell, @@ -63,7 +70,7 @@ func TestTerraformEnv_GetEnvVars(t *testing.T) { expectedEnvVars := map[string]string{ "TF_DATA_DIR": `/mock/config/root/.terraform/project/path`, - "TF_CLI_ARGS_init": `-backend=true`, + "TF_CLI_ARGS_init": `-backend=true -backend-config="path=/mock/config/root/.tfstate/project/path/terraform.tfstate"`, "TF_CLI_ARGS_plan": `-out="/mock/config/root/.terraform/project/path/terraform.tfplan" -var-file="/mock/config/root/terraform/project/path.tfvars" -var-file="/mock/config/root/terraform/project/path.tfvars.json"`, "TF_CLI_ARGS_apply": `"/mock/config/root/.terraform/project/path/terraform.tfplan"`, "TF_CLI_ARGS_import": `-var-file="/mock/config/root/terraform/project/path.tfvars" -var-file="/mock/config/root/terraform/project/path.tfvars.json"`, @@ -312,7 +319,9 @@ func TestTerraformEnv_PostEnvHook(t *testing.T) { mocks.ConfigHandler.GetConfigFunc = func() *v1alpha1.Context { return &v1alpha1.Context{ Terraform: &terraform.TerraformConfig{ - Backend: stringPtr("local"), + Backend: &terraform.BackendConfig{ + Type: "local", + }, }, } } @@ -398,44 +407,14 @@ func TestTerraformEnv_PostEnvHook(t *testing.T) { } }) - t.Run("ErrorGettingConfigRoot", func(t *testing.T) { - mocks := setupSafeTerraformEnvMocks() - mocks.ConfigHandler.GetConfigRootFunc = func() (string, error) { - return "", fmt.Errorf("mock error getting config root") - } - - // Given a mocked getwd function simulating being in a terraform project root - originalGetwd := getwd - defer func() { getwd = originalGetwd }() - getwd = func() (string, error) { - return filepath.FromSlash("mock/project/root/terraform/project/path"), nil - } - originalGlob := glob - defer func() { glob = originalGlob }() - glob = func(pattern string) ([]string, error) { - return []string{filepath.FromSlash("mock/project/root/terraform/project/path/main.tf")}, nil - } - - // When the PostEnvHook function is called - terraformEnvPrinter := NewTerraformEnvPrinter(mocks.Injector) - terraformEnvPrinter.Initialize() - err := terraformEnvPrinter.PostEnvHook() - - // Then the error should contain the expected message - if err == nil { - t.Errorf("Expected error, got nil") - } - if !strings.Contains(err.Error(), "error getting config root") { - t.Errorf("Expected error message to contain 'error getting config root', got %v", err) - } - }) - t.Run("UnsupportedBackend", func(t *testing.T) { mocks := setupSafeTerraformEnvMocks() mocks.ConfigHandler.GetConfigFunc = func() *v1alpha1.Context { return &v1alpha1.Context{ Terraform: &terraform.TerraformConfig{ - Backend: stringPtr("unsupported"), + Backend: &terraform.BackendConfig{ + Type: "unsupported", + }, }, } } @@ -511,6 +490,8 @@ func TestTerraformEnv_Print(t *testing.T) { terraformEnvPrinter.Initialize() // Mock the stat function to simulate the existence of the terraform config file + originalStat := stat + defer func() { stat = originalStat }() stat = func(name string) (os.FileInfo, error) { if name == filepath.FromSlash("/mock/config/root/.terraform/config") { return nil, nil // Simulate that the file exists @@ -557,7 +538,7 @@ func TestTerraformEnv_Print(t *testing.T) { // Verify that PrintEnvVarsFunc was called with the correct envVars expectedEnvVars := map[string]string{ "TF_DATA_DIR": "/mock/config/root/.terraform/project/path", - "TF_CLI_ARGS_init": "-backend=true", + "TF_CLI_ARGS_init": "-backend=true -backend-config=\"path=/mock/config/root/.tfstate/project/path/terraform.tfstate\"", "TF_CLI_ARGS_plan": `-out="/mock/config/root/.terraform/project/path/terraform.tfplan"`, "TF_CLI_ARGS_apply": `"/mock/config/root/.terraform/project/path/terraform.tfplan"`, "TF_CLI_ARGS_import": "", @@ -828,12 +809,14 @@ func TestTerraformEnv_generateBackendOverrideTf(t *testing.T) { t.Run("Success", func(t *testing.T) { mocks := setupSafeTerraformEnvMocks() mocks.ConfigHandler.GetConfigRootFunc = func() (string, error) { - return "/mock/config/root", nil + return filepath.FromSlash("/mock/config/root"), nil } mocks.ConfigHandler.GetConfigFunc = func() *v1alpha1.Context { return &v1alpha1.Context{ Terraform: &terraform.TerraformConfig{ - Backend: stringPtr("local"), + Backend: &terraform.BackendConfig{ + Type: "local", + }, }, } } @@ -842,7 +825,7 @@ func TestTerraformEnv_generateBackendOverrideTf(t *testing.T) { originalGetwd := getwd defer func() { getwd = originalGetwd }() getwd = func() (string, error) { - return "/mock/project/root/terraform/project/path", nil + return filepath.FromSlash("/mock/project/root/terraform/project/path"), nil } // And a mocked glob function simulating finding Terraform files originalGlob := glob @@ -874,11 +857,8 @@ func TestTerraformEnv_generateBackendOverrideTf(t *testing.T) { t.Errorf("Expected no error, got %v", err) } - expectedContent := ` -terraform { - backend "local" { - path = "/mock/config/root/.tfstate/project/path/terraform.tfstate" - } + expectedContent := `terraform { + backend "local" {} }` if string(writtenData) != expectedContent { t.Errorf("Expected backend config %q, got %q", expectedContent, string(writtenData)) @@ -890,7 +870,9 @@ terraform { mocks.ConfigHandler.GetConfigFunc = func() *v1alpha1.Context { return &v1alpha1.Context{ Terraform: &terraform.TerraformConfig{ - Backend: stringPtr("s3"), + Backend: &terraform.BackendConfig{ + Type: "s3", + }, }, } } @@ -930,12 +912,9 @@ terraform { t.Errorf("Expected no error, got %v", err) } - expectedContent := fmt.Sprintf(` -terraform { - backend "s3" { - key = "%s" - } -}`, filepath.ToSlash("project/path/terraform.tfstate")) + expectedContent := `terraform { + backend "s3" {} +}` if string(writtenData) != expectedContent { t.Errorf("Expected backend config %q, got %q", expectedContent, string(writtenData)) } @@ -946,7 +925,9 @@ terraform { mocks.ConfigHandler.GetConfigFunc = func() *v1alpha1.Context { return &v1alpha1.Context{ Terraform: &terraform.TerraformConfig{ - Backend: stringPtr("kubernetes"), + Backend: &terraform.BackendConfig{ + Type: "kubernetes", + }, }, } } @@ -986,21 +967,24 @@ terraform { t.Errorf("Expected no error, got %v", err) } - expectedContent := ` -terraform { - backend "kubernetes" { - secret_suffix = "project-path" - } + expectedContent := `terraform { + backend "kubernetes" {} }` if string(writtenData) != expectedContent { t.Errorf("Expected backend config %q, got %q", expectedContent, string(writtenData)) } }) - t.Run("ErrorGettingConfigRoot", func(t *testing.T) { + t.Run("UnsupportedBackend", func(t *testing.T) { mocks := setupSafeTerraformEnvMocks() - mocks.ConfigHandler.GetConfigRootFunc = func() (string, error) { - return "", fmt.Errorf("mock error getting config root") + mocks.ConfigHandler.GetConfigFunc = func() *v1alpha1.Context { + return &v1alpha1.Context{ + Terraform: &terraform.TerraformConfig{ + Backend: &terraform.BackendConfig{ + Type: "unsupported", + }, + }, + } } // Given a mocked getwd function simulating being in a terraform project root @@ -1028,17 +1012,19 @@ terraform { if err == nil { t.Errorf("Expected error, got nil") } - if !strings.Contains(err.Error(), "mock error getting config root") { - t.Errorf("Expected error message to contain 'mock error getting config root', got %v", err) + if !strings.Contains(err.Error(), "unsupported backend: unsupported") { + t.Errorf("Expected error message to contain 'unsupported backend: unsupported', got %v", err) } }) - t.Run("UnsupportedBackend", func(t *testing.T) { + t.Run("NoTerraformFiles", func(t *testing.T) { mocks := setupSafeTerraformEnvMocks() mocks.ConfigHandler.GetConfigFunc = func() *v1alpha1.Context { return &v1alpha1.Context{ Terraform: &terraform.TerraformConfig{ - Backend: stringPtr("unsupported"), + Backend: &terraform.BackendConfig{ + Type: "local", + }, }, } } @@ -1049,13 +1035,10 @@ terraform { getwd = func() (string, error) { return filepath.FromSlash("/mock/project/root/terraform/project/path"), nil } - // And a mocked glob function simulating finding Terraform files + // And a mocked glob function simulating no Terraform files found originalGlob := glob defer func() { glob = originalGlob }() glob = func(pattern string) ([]string, error) { - if pattern == filepath.FromSlash("/mock/project/root/terraform/project/path/*.tf") { - return []string{filepath.FromSlash("/mock/project/root/terraform/project/path/main.tf")}, nil - } return nil, nil } @@ -1064,46 +1047,357 @@ terraform { terraformEnvPrinter.Initialize() err := terraformEnvPrinter.generateBackendOverrideTf() - // Then the error should contain the expected message + // Then no error should occur + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + }) +} + +func TestTerraformEnv_generateBackendConfigArgs(t *testing.T) { + t.Run("Success", func(t *testing.T) { + mocks := setupSafeTerraformEnvMocks() + terraformEnvPrinter := NewTerraformEnvPrinter(mocks.Injector) + terraformEnvPrinter.Initialize() + + projectPath := "project/path" + configRoot := filepath.FromSlash("/mock/config/root") + + backendConfigArgs, err := terraformEnvPrinter.generateBackendConfigArgs(projectPath, configRoot) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + expectedArgs := []string{ + fmt.Sprintf(`-backend-config="%s"`, filepath.ToSlash(filepath.Join(configRoot, "terraform", "backend.tfvars"))), + fmt.Sprintf(`-backend-config="path=%s"`, filepath.ToSlash(filepath.Join(configRoot, ".tfstate", projectPath, "terraform.tfstate"))), + } + + if !reflect.DeepEqual(backendConfigArgs, expectedArgs) { + t.Errorf("expected %v, got %v", expectedArgs, backendConfigArgs) + } + }) + + t.Run("LocalBackend", func(t *testing.T) { + mocks := setupSafeTerraformEnvMocks() + mocks.ConfigHandler.GetConfigFunc = func() *v1alpha1.Context { + return &v1alpha1.Context{ + Terraform: &terraform.TerraformConfig{ + Backend: &terraform.BackendConfig{ + Local: &terraform.LocalBackend{ + Path: stringPtr(filepath.FromSlash("/mock/config/root/.tfstate/project/path/terraform.tfstate")), + }, + }, + }, + } + } + terraformEnvPrinter := NewTerraformEnvPrinter(mocks.Injector) + terraformEnvPrinter.Initialize() + + projectPath := "project/path" + configRoot := filepath.FromSlash("/mock/config/root") + + backendConfigArgs, err := terraformEnvPrinter.generateBackendConfigArgs(projectPath, configRoot) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + expectedArgs := []string{ + fmt.Sprintf(`-backend-config="%s"`, filepath.ToSlash(filepath.Join(configRoot, "terraform", "backend.tfvars"))), + fmt.Sprintf(`-backend-config="path=%s"`, filepath.ToSlash(filepath.Join(configRoot, ".tfstate", projectPath, "terraform.tfstate"))), + } + + if !reflect.DeepEqual(backendConfigArgs, expectedArgs) { + t.Errorf("expected %v, got %v", expectedArgs, backendConfigArgs) + } + }) + + t.Run("S3Backend", func(t *testing.T) { + mocks := setupSafeTerraformEnvMocks() + mocks.ConfigHandler.GetConfigFunc = func() *v1alpha1.Context { + return &v1alpha1.Context{ + Terraform: &terraform.TerraformConfig{ + Backend: &terraform.BackendConfig{ + S3: &terraform.S3Backend{ + Bucket: stringPtr("mock-bucket"), + Region: stringPtr("mock-region"), + AccessKey: stringPtr("mock-access-key"), + SecretKey: stringPtr("mock-secret-key"), + MaxRetries: intPtr(5), + SkipCredentialsValidation: boolPtr(true), + }, + }, + }, + } + } + terraformEnvPrinter := NewTerraformEnvPrinter(mocks.Injector) + terraformEnvPrinter.Initialize() + + projectPath := filepath.FromSlash("project/path") + configRoot := filepath.FromSlash("/mock/config/root") + + backendConfigArgs, err := terraformEnvPrinter.generateBackendConfigArgs(projectPath, configRoot) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + expectedArgs := []string{ + fmt.Sprintf(`-backend-config="%s"`, filepath.ToSlash(filepath.Join(configRoot, "terraform", "backend.tfvars"))), + `-backend-config="key=project/path/terraform.tfstate"`, + `-backend-config="access_key=mock-access-key"`, + `-backend-config="bucket=mock-bucket"`, + `-backend-config="max_retries=5"`, + `-backend-config="region=mock-region"`, + `-backend-config="secret_key=mock-secret-key"`, + `-backend-config="skip_credentials_validation=true"`, + } + + if !reflect.DeepEqual(backendConfigArgs, expectedArgs) { + t.Errorf("expected %v, got %v", expectedArgs, backendConfigArgs) + } + }) + + t.Run("KubernetesBackend", func(t *testing.T) { + mocks := setupSafeTerraformEnvMocks() + mocks.ConfigHandler.GetConfigFunc = func() *v1alpha1.Context { + return &v1alpha1.Context{ + Terraform: &terraform.TerraformConfig{ + Backend: &terraform.BackendConfig{ + Kubernetes: &terraform.KubernetesBackend{ + SecretSuffix: stringPtr("mock-secret-suffix"), + Namespace: stringPtr("mock-namespace"), + }, + }, + }, + } + } + terraformEnvPrinter := NewTerraformEnvPrinter(mocks.Injector) + terraformEnvPrinter.Initialize() + + projectPath := filepath.FromSlash("project/path") + configRoot := filepath.FromSlash("/mock/config/root") + + backendConfigArgs, err := terraformEnvPrinter.generateBackendConfigArgs(projectPath, configRoot) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + expectedArgs := []string{ + fmt.Sprintf(`-backend-config="%s"`, filepath.ToSlash(filepath.Join(configRoot, "terraform", "backend.tfvars"))), + `-backend-config="secret_suffix=project-path"`, + `-backend-config="namespace=mock-namespace"`, + `-backend-config="secret_suffix=mock-secret-suffix"`, + } + + if !reflect.DeepEqual(backendConfigArgs, expectedArgs) { + t.Errorf("expected %v, got %v", expectedArgs, backendConfigArgs) + } + }) + + t.Run("BackendTfvarsFileExists", func(t *testing.T) { + mocks := setupSafeTerraformEnvMocks() + mocks.ConfigHandler.GetContextFunc = func() string { + return "mock-context" + } + terraformEnvPrinter := NewTerraformEnvPrinter(mocks.Injector) + terraformEnvPrinter.Initialize() + + projectPath := filepath.FromSlash("project/path") + configRoot := filepath.FromSlash("/mock/config/root") + + backendConfigArgs, err := terraformEnvPrinter.generateBackendConfigArgs(projectPath, configRoot) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + expectedArgs := []string{ + fmt.Sprintf(`-backend-config="%s"`, filepath.ToSlash(filepath.Join(configRoot, "terraform", "backend.tfvars"))), + fmt.Sprintf(`-backend-config="path=%s"`, filepath.ToSlash(filepath.Join(configRoot, ".tfstate", projectPath, "terraform.tfstate"))), + } + + if !reflect.DeepEqual(backendConfigArgs, expectedArgs) { + t.Errorf("expected %v, got %v", expectedArgs, backendConfigArgs) + } + }) + + t.Run("ErrorMarshallingBackendConfig", func(t *testing.T) { + mocks := setupSafeTerraformEnvMocks() + mocks.ConfigHandler.GetConfigFunc = func() *v1alpha1.Context { + return &v1alpha1.Context{ + Terraform: &terraform.TerraformConfig{ + Backend: &terraform.BackendConfig{ + Type: "s3", + S3: &terraform.S3Backend{}, + }, + }, + } + } + + // Mock yamlMarshal to return an error + originalYamlMarshal := yamlMarshal + defer func() { yamlMarshal = originalYamlMarshal }() + yamlMarshal = func(v interface{}) ([]byte, error) { + return nil, fmt.Errorf("mock marshalling error") + } + + terraformEnvPrinter := NewTerraformEnvPrinter(mocks.Injector) + terraformEnvPrinter.Initialize() + + projectPath := "project/path" + configRoot := filepath.FromSlash("/mock/config/root") + + _, err := terraformEnvPrinter.generateBackendConfigArgs(projectPath, configRoot) if err == nil { - t.Errorf("Expected error, got nil") + t.Errorf("expected error, got nil") } - if !strings.Contains(err.Error(), "unsupported backend: unsupported") { - t.Errorf("Expected error message to contain 'unsupported backend: unsupported', got %v", err) + + if !strings.Contains(err.Error(), "error marshalling backend to YAML: mock marshalling error") { + t.Errorf("expected error to contain %v, got %v", "error marshalling backend to YAML: mock marshalling error", err.Error()) } }) - t.Run("NoTerraformFiles", func(t *testing.T) { + t.Run("ErrorProcessingKubernetesBackendConfig", func(t *testing.T) { mocks := setupSafeTerraformEnvMocks() mocks.ConfigHandler.GetConfigFunc = func() *v1alpha1.Context { return &v1alpha1.Context{ Terraform: &terraform.TerraformConfig{ - Backend: stringPtr("local"), + Backend: &terraform.BackendConfig{ + Type: "kubernetes", + Kubernetes: &terraform.KubernetesBackend{}, + }, }, } } - // Given a mocked getwd function simulating being in a terraform project root - originalGetwd := getwd - defer func() { getwd = originalGetwd }() - getwd = func() (string, error) { - return filepath.FromSlash("/mock/project/root/terraform/project/path"), nil + // Mock processBackendConfig to return an error + originalProcessBackendConfig := processBackendConfig + defer func() { processBackendConfig = originalProcessBackendConfig }() + processBackendConfig = func(backendConfig interface{}, addArg func(key, value string)) error { + return fmt.Errorf("mock processing error") } - // And a mocked glob function simulating no Terraform files found - originalGlob := glob - defer func() { glob = originalGlob }() - glob = func(pattern string) ([]string, error) { - return nil, nil + + terraformEnvPrinter := NewTerraformEnvPrinter(mocks.Injector) + terraformEnvPrinter.Initialize() + + projectPath := "project/path" + configRoot := filepath.FromSlash("/mock/config/root") + + _, err := terraformEnvPrinter.generateBackendConfigArgs(projectPath, configRoot) + if err == nil { + t.Errorf("expected error, got nil") + } + + if !strings.Contains(err.Error(), "error processing Kubernetes backend config: mock processing error") { + t.Errorf("expected error to contain %v, got %v", "error processing Kubernetes backend config: mock processing error", err.Error()) + } + }) + + t.Run("UnsupportedBackendType", func(t *testing.T) { + mocks := setupSafeTerraformEnvMocks() + mocks.ConfigHandler.GetConfigFunc = func() *v1alpha1.Context { + return &v1alpha1.Context{ + Terraform: &terraform.TerraformConfig{ + Backend: &terraform.BackendConfig{ + Type: "unsupported", + }, + }, + } + } + + // Mock GetString to return "unsupported" for the backend type + mocks.ConfigHandler.GetStringFunc = func(key string, defaultValue ...string) string { + if key == "terraform.backend.type" { + return "unsupported" + } + if len(defaultValue) > 0 { + return defaultValue[0] + } + return "" } - // When generateBackendOverrideTf is called terraformEnvPrinter := NewTerraformEnvPrinter(mocks.Injector) terraformEnvPrinter.Initialize() - err := terraformEnvPrinter.generateBackendOverrideTf() - // Then no error should occur + projectPath := "project/path" + configRoot := filepath.FromSlash("/mock/config/root") + + _, err := terraformEnvPrinter.generateBackendConfigArgs(projectPath, configRoot) + if err == nil { + t.Errorf("expected error, got nil") + } + + if !strings.Contains(err.Error(), "unsupported backend: unsupported") { + t.Errorf("expected error to contain %v, got %v", "unsupported backend: unsupported", err.Error()) + } + }) +} + +func TestTerraformEnv_processBackendConfig(t *testing.T) { + t.Run("Success", func(t *testing.T) { + backendConfig := map[string]interface{}{ + "key1": "value1", + "key2": true, + "key3": 123, + "key4": []interface{}{"item1", "item2"}, + "key5": map[string]interface{}{ + "nestedKey1": "nestedValue1", + "nestedKey2": "nestedValue2", + }, + } + + var args []string + addArg := func(key, value string) { + args = append(args, fmt.Sprintf("%s=%s", key, value)) + } + + err := processBackendConfig(backendConfig, addArg) if err != nil { - t.Errorf("Expected no error, got %v", err) + t.Errorf("unexpected error: %v", err) + } + + expectedArgs := []string{ + "key1=value1", + "key2=true", + "key3=123", + "key4=item1", + "key4=item2", + "key5.nestedKey1=nestedValue1", + "key5.nestedKey2=nestedValue2", + } + + sort.Strings(args) + sort.Strings(expectedArgs) + + if !reflect.DeepEqual(args, expectedArgs) { + t.Errorf("expected args %v, got %v", expectedArgs, args) + } + }) + + t.Run("ErrorUnmarshallingBackendConfig", func(t *testing.T) { + originalYamlUnmarshal := yamlUnmarshal + defer func() { yamlUnmarshal = originalYamlUnmarshal }() + + yamlUnmarshal = func(data []byte, v interface{}) error { + return fmt.Errorf("mocked error") + } + + backendConfig := map[string]interface{}{ + "key1": "value1", + } + + var args []string + addArg := func(key, value string) { + args = append(args, fmt.Sprintf("%s=%s", key, value)) + } + + err := processBackendConfig(backendConfig, addArg) + if err == nil { + t.Errorf("expected error, got nil") + } + + expectedError := "mocked error" + if !strings.Contains(err.Error(), expectedError) { + t.Errorf("expected error to contain %v, got %v", expectedError, err.Error()) } }) }