-
Notifications
You must be signed in to change notification settings - Fork 0
fix(wfctl): v0.18.9 — env-resolution consistency in ci run deploy + infra_output #476
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
d5d26f2
a20a405
036b871
2cd43c2
0bb71a3
5562126
cc22551
f0faf96
33f09f2
6674def
25602af
4bc15d2
b50e3f3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,73 @@ | ||
| package main | ||
|
|
||
| import ( | ||
| "testing" | ||
|
|
||
| "github.com/GoCodeAlone/workflow/config" | ||
| ) | ||
|
|
||
| func TestPluginDeployProvider_UsesEnvResolvedName(t *testing.T) { | ||
| wfCfg := &config.WorkflowConfig{ | ||
| Modules: []config.ModuleConfig{ | ||
| { | ||
| Name: "do-provider", | ||
| Type: "iac.provider", | ||
| Config: map[string]any{"provider": "digitalocean"}, | ||
| }, | ||
| { | ||
| Name: "bmw-app", | ||
| Type: "infra.container_service", | ||
| Config: map[string]any{"provider": "do-provider"}, | ||
| Environments: map[string]*config.InfraEnvironmentResolution{ | ||
| "staging": { | ||
| Config: map[string]any{"name": "bmw-staging"}, | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
| } | ||
|
|
||
| dp, err := newPluginDeployProvider("digitalocean", wfCfg, "staging") | ||
| if err != nil { | ||
| t.Fatalf("newPluginDeployProvider: %v", err) | ||
| } | ||
|
|
||
| pdp, ok := dp.(*pluginDeployProvider) | ||
| if !ok { | ||
| t.Fatalf("expected *pluginDeployProvider, got %T", dp) | ||
| } | ||
| if pdp.resourceName != "bmw-staging" { | ||
| t.Errorf("resourceName = %q, want %q (env-resolved name)", pdp.resourceName, "bmw-staging") | ||
| } | ||
| } | ||
|
|
||
| func TestPluginDeployProvider_FallsBackToModuleNameWhenNoEnv(t *testing.T) { | ||
| wfCfg := &config.WorkflowConfig{ | ||
| Modules: []config.ModuleConfig{ | ||
| { | ||
| Name: "do-provider", | ||
| Type: "iac.provider", | ||
| Config: map[string]any{"provider": "digitalocean"}, | ||
| }, | ||
| { | ||
| Name: "bmw-app", | ||
| Type: "infra.container_service", | ||
| Config: map[string]any{"provider": "do-provider"}, | ||
| // NOTE: no Environments block — base name should be used | ||
| }, | ||
| }, | ||
| } | ||
|
|
||
| dp, err := newPluginDeployProvider("digitalocean", wfCfg, "") | ||
| if err != nil { | ||
| t.Fatalf("newPluginDeployProvider: %v", err) | ||
| } | ||
|
|
||
| pdp, ok := dp.(*pluginDeployProvider) | ||
| if !ok { | ||
| t.Fatalf("expected *pluginDeployProvider, got %T", dp) | ||
| } | ||
| if pdp.resourceName != "bmw-app" { | ||
| t.Errorf("resourceName = %q, want %q (base module name when no env)", pdp.resourceName, "bmw-app") | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -825,7 +825,24 @@ func runInfraApply(args []string) error { | |
| return fmt.Errorf("resolve secrets provider for infra_output sync: %w", err) | ||
| } | ||
| states := loadCurrentState(cfgFile, envName) | ||
| return syncInfraOutputSecrets(ctx, secretsCfg, secretsProvider, states) | ||
| // Only reload the workflow config when env resolution is actually needed: | ||
| // it is needed only when --env is set AND at least one infra_output secret | ||
| // generator is configured (otherwise syncInfraOutputSecrets is a no-op for | ||
| // env resolution regardless). | ||
| var wfCfg *config.WorkflowConfig | ||
| if envName != "" { | ||
| for _, g := range secretsCfg.Generate { | ||
| if g.Type == "infra_output" { | ||
| var loadErr error | ||
| wfCfg, loadErr = config.LoadFromFile(cfgFile) | ||
| if loadErr != nil { | ||
| return fmt.Errorf("load config for infra_output env resolution: %w", loadErr) | ||
| } | ||
| break | ||
| } | ||
| } | ||
| } | ||
|
Comment on lines
+832
to
+844
|
||
| return syncInfraOutputSecrets(ctx, secretsCfg, secretsProvider, states, wfCfg, envName) | ||
| } | ||
|
|
||
| func runInfraStatus(args []string) error { | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -4,7 +4,10 @@ import ( | |||||||||||||||||||||||||||||||||||||||||||||||||||
| "context" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| "errors" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| "fmt" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| "sort" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| "strings" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| "github.com/GoCodeAlone/workflow/config" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| "github.com/GoCodeAlone/workflow/interfaces" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| "github.com/GoCodeAlone/workflow/secrets" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -23,10 +26,78 @@ func buildStateOutputsMap(states []interfaces.ResourceState) map[string]map[stri | |||||||||||||||||||||||||||||||||||||||||||||||||||
| return m | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| // resolveInfraOutput resolves a single "module.field" source string against the | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| // pre-loaded state outputs map, applying per-env module name resolution so that | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| // a source like "bmw-database.uri" finds the state keyed by the env-resolved | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| // name (e.g. "bmw-staging-db") when --env staging renames the module. | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| // | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| // wfCfg may be nil (e.g. tests that only care about base-name resolution). | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| // When envName is empty no resolution is performed and the source module name | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| // is used verbatim. | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| func resolveInfraOutput(wfCfg *config.WorkflowConfig, source, envName string, stateOutputs map[string]map[string]any) (string, error) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if source == "" { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| return "", fmt.Errorf("infra_output: source is required (format: \"module.field\")") | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| dot := strings.Index(source, ".") | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if dot < 1 || dot >= len(source)-1 { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| return "", fmt.Errorf("infra_output: invalid source %q: expected \"module.field\" format", source) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| moduleName := source[:dot] | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| field := source[dot+1:] | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Apply env resolution: the state was persisted under the env-resolved name. | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if envName != "" && wfCfg != nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| for i := range wfCfg.Modules { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| m := &wfCfg.Modules[i] | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if m.Name != moduleName { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| resolved, ok := m.ResolveForEnv(envName) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if !ok { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| return "", fmt.Errorf("infra_output: module %q is explicitly disabled for environment %q — cannot read infra_output from a disabled module", moduleName, envName) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if resolved.Name != "" { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| moduleName = resolved.Name | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| break | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| if stateOutputs == nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| return "", fmt.Errorf("infra_output: state outputs not available for source %q — did infra apply succeed?", source) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| outputs, ok := stateOutputs[moduleName] | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if !ok { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| return "", fmt.Errorf("infra_output: module %q not found in state (available: %s)", moduleName, strings.Join(stateKeys(stateOutputs), ", ")) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| val, ok := outputs[field] | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if !ok { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| return "", fmt.Errorf("infra_output: field %q not found in outputs of module %q", field, moduleName) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| s, ok := val.(string) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if !ok { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| return "", fmt.Errorf("infra_output: output field %q of module %q is %T, expected string", field, moduleName, val) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| return s, nil | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+66
to
+81
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| if stateOutputs == nil { | |
| return "", fmt.Errorf("infra_output: state outputs not available for source %q — did infra apply succeed?", source) | |
| } | |
| outputs, ok := stateOutputs[moduleName] | |
| if !ok { | |
| return "", fmt.Errorf("infra_output: module %q not found in state (available: %s)", moduleName, strings.Join(stateKeys(stateOutputs), ", ")) | |
| } | |
| val, ok := outputs[field] | |
| if !ok { | |
| return "", fmt.Errorf("infra_output: field %q not found in outputs of module %q", field, moduleName) | |
| } | |
| s, ok := val.(string) | |
| if !ok { | |
| return "", fmt.Errorf("infra_output: output field %q of module %q is %T, expected string", field, moduleName, val) | |
| } | |
| return s, nil | |
| return secrets.GenerateSecret( | |
| "infra_output", | |
| map[string]any{ | |
| "source": moduleName + "." + field, | |
| }, | |
| map[string]any{ | |
| "_state_outputs": stateOutputs, | |
| }, | |
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The v0.18.9 changelog test list omits
TestInfraOutput_ExplicitlyDisabledModuleErrors, which was added incmd/wfctl/infra_secrets_env_test.go. Please update this line to include the third test (or list the file without enumerating specific test names) so the changelog stays accurate.