Skip to content

Commit 40c4c0f

Browse files
ostermanclaudeautofix-ci[bot]aknysh
authored
fix: prevent invalid empty backend.tf.json generation (#1833)
* fix: prevent invalid empty backend.tf.json from being generated - Add validation in generateComponentBackendConfig() to reject empty backend_type - Add ErrBackendTypeRequired sentinel error for proper error handling - Log warnings when skipping backend generation due to missing backend or backend_type - Include auto_generate_backend_file setting hint in warning messages - Add comprehensive tests to verify no backend files are generated when config is incomplete - Tests validate both warning messages and file non-existence This prevents the issue where an empty backend type would result in invalid Terraform config like `{"terraform": {"backend": {"": {}}}}`, which causes "Duplicate backend configuration" errors. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * test: improve test comment accuracy for backend_type warning path Clarify that the YAML processor sets empty backend_type strings, so tests hit the "empty after template processing" path rather than the "not configured" path. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * [autofix.ci] apply automated fixes * refactor: extract backend validation into pure testable functions - Add validateBackendConfig() pure function with 100% test coverage - Add checkBackendTypeAfterProcessing() pure function with 100% test coverage - Add comprehensive unit tests for all validation scenarios - Refactor ExecuteTerraformGenerateBackends to use extracted functions This improves testability by separating validation logic from side effects, allowing direct unit testing of all validation paths without requiring complex integration test setups. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * refactor: rename backendConfigValidationResult to backendValidation Simplify the type name and remove verbose field comments. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * refactor: rename to backendConfig and extractBackendConfig The struct holds the backend configuration, so backendConfig is clearer. The function extracts config from the component section, so extractBackendConfig better describes what it does. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * refactor: use error type instead of string for skip reasons Replace SkipReason string with Err error field in backendConfig. Add sentinel errors: ErrBackendSectionMissing, ErrBackendTypeMissing, ErrBackendTypeEmptyAfterRender. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * fix: reject empty backend config for remote backends When backend_type is set to s3, gcs, azurerm, or cloud but the backend section is empty, skip backend generation with a warning instead of generating invalid Terraform configuration like {"terraform":{"backend":{"s3":{}}}}. The local backend is still allowed to have empty configuration since Terraform supports it without any required fields. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * docs: add blog post for empty backend config validation fix 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Andriy Knysh <aknysh@users.noreply.github.com>
1 parent ed4c02b commit 40c4c0f

File tree

6 files changed

+520
-15
lines changed

6 files changed

+520
-15
lines changed

errors/errors.go

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -111,14 +111,19 @@ var (
111111
ErrAzurePermissionDenied = errors.New("permission denied accessing Azure blob")
112112

113113
// Azure authentication errors.
114-
ErrAzureOIDClaimNotFound = errors.New("oid claim not found in token")
115-
ErrAzureUsernameClaimNotFound = errors.New("no username claim found in token (tried upn, unique_name, email)")
116-
ErrAzureInvalidJWTFormat = errors.New("invalid JWT format")
117-
ErrAzureExpirationTimeEmpty = errors.New("expiration time is empty")
118-
ErrAzureTimeParseFailure = errors.New("unable to parse time: tried RFC3339, local time formats, and Unix timestamp")
119-
ErrAzureNoAccountsInCache = errors.New("no accounts found in cache")
120-
ErrAzureNoAccountForTenant = errors.New("no account found for tenant")
121-
ErrBackendConfigRequired = errors.New("backend configuration is required")
114+
ErrAzureOIDClaimNotFound = errors.New("oid claim not found in token")
115+
ErrAzureUsernameClaimNotFound = errors.New("no username claim found in token (tried upn, unique_name, email)")
116+
ErrAzureInvalidJWTFormat = errors.New("invalid JWT format")
117+
ErrAzureExpirationTimeEmpty = errors.New("expiration time is empty")
118+
ErrAzureTimeParseFailure = errors.New("unable to parse time: tried RFC3339, local time formats, and Unix timestamp")
119+
ErrAzureNoAccountsInCache = errors.New("no accounts found in cache")
120+
ErrAzureNoAccountForTenant = errors.New("no account found for tenant")
121+
ErrBackendConfigRequired = errors.New("backend configuration is required")
122+
ErrBackendTypeRequired = errors.New("backend_type is required")
123+
ErrBackendSectionMissing = errors.New("no 'backend' section configured")
124+
ErrBackendTypeMissing = errors.New("no 'backend_type' configured")
125+
ErrBackendTypeEmptyAfterRender = errors.New("'backend_type' is empty after template processing")
126+
ErrBackendConfigEmpty = errors.New("'backend' section is empty but 'backend_type' requires configuration")
122127

123128
// Git-related errors.
124129
ErrGitNotAvailable = errors.New("git must be available and on the PATH")

internal/exec/terraform_generate_backends.go

Lines changed: 56 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,46 @@ import (
1717
u "github.com/cloudposse/atmos/pkg/utils"
1818
)
1919

20+
// backendConfig holds extracted backend configuration from a component section.
21+
type backendConfig struct {
22+
BackendSection map[string]any
23+
BackendType string
24+
Err error
25+
}
26+
27+
// extractBackendConfig extracts and validates backend configuration from a component section.
28+
func extractBackendConfig(componentSection map[string]any) backendConfig {
29+
backendSection, ok := componentSection[cfg.BackendSectionName].(map[string]any)
30+
if !ok {
31+
return backendConfig{Err: errUtils.ErrBackendSectionMissing}
32+
}
33+
34+
backendType, ok := componentSection[cfg.BackendTypeSectionName].(string)
35+
if !ok {
36+
return backendConfig{Err: errUtils.ErrBackendTypeMissing}
37+
}
38+
39+
// Check if backend section is empty for backends that require configuration.
40+
// The "local" backend can work without any configuration, but remote backends
41+
// like s3, gcs, azurerm, and cloud require at least some configuration.
42+
if len(backendSection) == 0 && backendType != cfg.BackendTypeLocal {
43+
return backendConfig{Err: errUtils.ErrBackendConfigEmpty}
44+
}
45+
46+
return backendConfig{
47+
BackendSection: backendSection,
48+
BackendType: backendType,
49+
}
50+
}
51+
52+
// checkBackendTypeAfterProcessing validates that backend_type is not empty after template processing.
53+
func checkBackendTypeAfterProcessing(backendType string) error {
54+
if backendType == "" {
55+
return errUtils.ErrBackendTypeEmptyAfterRender
56+
}
57+
return nil
58+
}
59+
2060
// ExecuteTerraformGenerateBackendsCmd executes `terraform generate backends` command.
2161
func ExecuteTerraformGenerateBackendsCmd(cmd *cobra.Command, args []string) error {
2262
defer perf.Track(nil, "exec.ExecuteTerraformGenerateBackendsCmd")()
@@ -132,15 +172,16 @@ func ExecuteTerraformGenerateBackends(
132172
}
133173
}
134174

135-
// Component backend
136-
if backendSection, ok = componentSection[cfg.BackendSectionName].(map[string]any); !ok {
137-
continue
138-
}
139-
140-
// Backend type
141-
if backendTypeSection, ok = componentSection[cfg.BackendTypeSectionName].(string); !ok {
175+
// Extract backend configuration.
176+
backend := extractBackendConfig(componentSection)
177+
if backend.Err != nil {
178+
log.Warn("Skipping backend generation: "+backend.Err.Error()+". "+
179+
"Set 'components.terraform.auto_generate_backend_file: false' in atmos.yaml to disable.",
180+
"component", componentName, "stack", stackFileName)
142181
continue
143182
}
183+
backendSection = backend.BackendSection
184+
backendTypeSection = backend.BackendType
144185

145186
if varsSection, ok = componentSection[cfg.VarsSectionName].(map[string]any); !ok {
146187
varsSection = map[string]any{}
@@ -295,6 +336,14 @@ func ExecuteTerraformGenerateBackends(
295336
backendTypeSection = i
296337
}
297338

339+
// Skip if backend_type is empty after template processing.
340+
if err := checkBackendTypeAfterProcessing(backendTypeSection); err != nil {
341+
log.Warn("Skipping backend generation: "+err.Error()+". "+
342+
"Set 'components.terraform.auto_generate_backend_file: false' in atmos.yaml to disable.",
343+
"component", componentName, "stack", stackName)
344+
continue
345+
}
346+
298347
var backendFilePath string
299348
var backendFileAbsolutePath string
300349

0 commit comments

Comments
 (0)