Skip to content

Commit 4437695

Browse files
intel352claude
andcommitted
feat: deploy-time multi-store secret fetching
Update injectSecrets to accept *WorkflowConfig and envName, routing each secret entry through ResolveSecretStore + getProviderForStore for correct per-store fetching. Update runDeployPhaseWithConfig and runMultiServiceDeploy to pass the full WorkflowConfig instead of SecretsConfig. Retain injectSecretsLegacy for callers that only have SecretsConfig. Add deploy_secrets_test.go for multi-store routing, env-override, legacy provider, and unknown-store error cases. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 0c8fe79 commit 4437695

5 files changed

Lines changed: 161 additions & 16 deletions

File tree

cmd/wfctl/ci_multiservice.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,9 +102,9 @@ func buildServiceBinary(name string, svc *config.ServiceConfig, verbose bool) er
102102
func runMultiServiceDeploy(
103103
deploy *config.CIDeployConfig,
104104
envName string,
105-
secretsCfg *config.SecretsConfig,
105+
wfCfg *config.WorkflowConfig,
106106
services map[string]*config.ServiceConfig,
107107
verbose bool,
108108
) error {
109-
return runDeployPhaseWithConfig(deploy, envName, secretsCfg, services, verbose)
109+
return runDeployPhaseWithConfig(deploy, envName, wfCfg, services, verbose)
110110
}

cmd/wfctl/ci_run.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -61,11 +61,11 @@ func runCIRun(args []string) error {
6161
return fmt.Errorf("--env is required for deploy phase")
6262
}
6363
if len(cfg.Services) > 0 {
64-
if err := runMultiServiceDeploy(cfg.CI.Deploy, *env, cfg.Secrets, cfg.Services, *verbose); err != nil {
64+
if err := runMultiServiceDeploy(cfg.CI.Deploy, *env, &cfg, cfg.Services, *verbose); err != nil {
6565
return fmt.Errorf("deploy phase failed: %w", err)
6666
}
6767
} else {
68-
if err := runDeployPhaseWithConfig(cfg.CI.Deploy, *env, cfg.Secrets, nil, *verbose); err != nil {
68+
if err := runDeployPhaseWithConfig(cfg.CI.Deploy, *env, &cfg, nil, *verbose); err != nil {
6969
return fmt.Errorf("deploy phase failed: %w", err)
7070
}
7171
}
@@ -287,7 +287,7 @@ func runDeployPhase(deploy *config.CIDeployConfig, envName string, verbose bool)
287287
func runDeployPhaseWithConfig(
288288
deploy *config.CIDeployConfig,
289289
envName string,
290-
secretsCfg *config.SecretsConfig,
290+
wfCfg *config.WorkflowConfig,
291291
services map[string]*config.ServiceConfig,
292292
verbose bool,
293293
) error {
@@ -319,8 +319,8 @@ func runDeployPhaseWithConfig(
319319
}
320320
}
321321

322-
// Step 2: secret injection.
323-
secrets, err := injectSecrets(ctx, secretsCfg)
322+
// Step 2: secret injection — route each secret to its correct store.
323+
secrets, err := injectSecrets(ctx, wfCfg, envName)
324324
if err != nil {
325325
return fmt.Errorf("secret injection: %w", err)
326326
}

cmd/wfctl/deploy_providers.go

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -445,9 +445,34 @@ func pollHealthCheck(ctx context.Context, cfg DeployConfig) error {
445445

446446
// ── secret injection ──────────────────────────────────────────────────────────
447447

448-
// injectSecrets fetches secrets from the configured provider and returns them
449-
// as a name→value map for use during deployment.
450-
func injectSecrets(ctx context.Context, secretsCfg *config.SecretsConfig) (map[string]string, error) {
448+
// injectSecrets fetches secrets from the configured provider(s) and returns them
449+
// as a name→value map for use during deployment. When cfg contains a SecretStores
450+
// map or per-secret Store fields, each secret is routed to its correct store.
451+
// The envName parameter is used to apply environment-level SecretsStoreOverride.
452+
func injectSecrets(ctx context.Context, cfg *config.WorkflowConfig, envName string) (map[string]string, error) {
453+
if cfg == nil || cfg.Secrets == nil || len(cfg.Secrets.Entries) == 0 {
454+
return nil, nil
455+
}
456+
457+
result := make(map[string]string, len(cfg.Secrets.Entries))
458+
for _, entry := range cfg.Secrets.Entries {
459+
storeName := ResolveSecretStore(entry.Name, envName, cfg)
460+
provider, err := getProviderForStore(storeName, cfg)
461+
if err != nil {
462+
return nil, fmt.Errorf("secret %q: store %q: %w", entry.Name, storeName, err)
463+
}
464+
val, err := provider.Get(ctx, entry.Name)
465+
if err != nil {
466+
return nil, fmt.Errorf("secret %q: fetch from %q: %w", entry.Name, storeName, err)
467+
}
468+
result[entry.Name] = val
469+
}
470+
return result, nil
471+
}
472+
473+
// injectSecretsLegacy is the pre-multi-store implementation kept for callers
474+
// that only have a SecretsConfig (not a full WorkflowConfig).
475+
func injectSecretsLegacy(ctx context.Context, secretsCfg *config.SecretsConfig) (map[string]string, error) {
451476
if secretsCfg == nil || len(secretsCfg.Entries) == 0 {
452477
return nil, nil
453478
}

cmd/wfctl/deploy_providers_test.go

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,7 @@ func TestK8sStrategy(t *testing.T) {
221221
// ── injectSecrets ─────────────────────────────────────────────────────────────
222222

223223
func TestInjectSecrets_Nil(t *testing.T) {
224-
secrets, err := injectSecrets(context.Background(), nil)
224+
secrets, err := injectSecrets(context.Background(), nil, "")
225225
if err != nil {
226226
t.Fatalf("injectSecrets(nil): unexpected error: %v", err)
227227
}
@@ -233,13 +233,15 @@ func TestInjectSecrets_Nil(t *testing.T) {
233233
func TestInjectSecrets_EnvProvider(t *testing.T) {
234234
t.Setenv("TEST_SECRET_KEY", "supersecret")
235235

236-
secretsCfg := &config.SecretsConfig{
237-
Provider: "env",
238-
Entries: []config.SecretEntry{
239-
{Name: "TEST_SECRET_KEY"},
236+
wfCfg := &config.WorkflowConfig{
237+
Secrets: &config.SecretsConfig{
238+
Provider: "env",
239+
Entries: []config.SecretEntry{
240+
{Name: "TEST_SECRET_KEY"},
241+
},
240242
},
241243
}
242-
secrets, err := injectSecrets(context.Background(), secretsCfg)
244+
secrets, err := injectSecrets(context.Background(), wfCfg, "")
243245
if err != nil {
244246
t.Fatalf("injectSecrets: %v", err)
245247
}

cmd/wfctl/deploy_secrets_test.go

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/GoCodeAlone/workflow/config"
8+
)
9+
10+
func TestInjectSecrets_MultiStoreRouting(t *testing.T) {
11+
// Set secrets in the environment
12+
t.Setenv("DB_PASS", "pg-password")
13+
t.Setenv("JWT_KEY", "jwt-signing-key")
14+
15+
wfCfg := &config.WorkflowConfig{
16+
SecretStores: map[string]*config.SecretStoreConfig{
17+
"local-env": {Provider: "env"},
18+
},
19+
Secrets: &config.SecretsConfig{
20+
DefaultStore: "local-env",
21+
Entries: []config.SecretEntry{
22+
{Name: "DB_PASS", Store: "local-env"},
23+
{Name: "JWT_KEY"}, // uses defaultStore
24+
},
25+
},
26+
}
27+
28+
secrets, err := injectSecrets(context.Background(), wfCfg, "local")
29+
if err != nil {
30+
t.Fatalf("injectSecrets: %v", err)
31+
}
32+
33+
if secrets["DB_PASS"] != "pg-password" {
34+
t.Errorf("DB_PASS: got %q, want pg-password", secrets["DB_PASS"])
35+
}
36+
if secrets["JWT_KEY"] != "jwt-signing-key" {
37+
t.Errorf("JWT_KEY: got %q, want jwt-signing-key", secrets["JWT_KEY"])
38+
}
39+
}
40+
41+
func TestInjectSecrets_EnvOverrideRouting(t *testing.T) {
42+
t.Setenv("MY_API_KEY", "api-key-value")
43+
44+
wfCfg := &config.WorkflowConfig{
45+
SecretStores: map[string]*config.SecretStoreConfig{
46+
"local": {Provider: "env"},
47+
},
48+
Secrets: &config.SecretsConfig{
49+
DefaultStore: "local",
50+
Entries: []config.SecretEntry{
51+
{Name: "MY_API_KEY"},
52+
},
53+
},
54+
Environments: map[string]*config.EnvironmentConfig{
55+
"staging": {SecretsStoreOverride: "local"}, // routes to env provider
56+
},
57+
}
58+
59+
secrets, err := injectSecrets(context.Background(), wfCfg, "staging")
60+
if err != nil {
61+
t.Fatalf("injectSecrets: %v", err)
62+
}
63+
if secrets["MY_API_KEY"] != "api-key-value" {
64+
t.Errorf("MY_API_KEY: got %q, want api-key-value", secrets["MY_API_KEY"])
65+
}
66+
}
67+
68+
func TestInjectSecrets_UnknownStore_Error(t *testing.T) {
69+
wfCfg := &config.WorkflowConfig{
70+
Secrets: &config.SecretsConfig{
71+
Entries: []config.SecretEntry{
72+
{Name: "MY_SECRET", Store: "nonexistent-provider"},
73+
},
74+
},
75+
}
76+
77+
_, err := injectSecrets(context.Background(), wfCfg, "")
78+
if err == nil {
79+
t.Error("expected error for unknown store provider")
80+
}
81+
}
82+
83+
func TestInjectSecrets_LegacyProvider(t *testing.T) {
84+
t.Setenv("LEGACY_SECRET", "legacy-value")
85+
86+
wfCfg := &config.WorkflowConfig{
87+
Secrets: &config.SecretsConfig{
88+
Provider: "env", // legacy field
89+
Entries: []config.SecretEntry{
90+
{Name: "LEGACY_SECRET"},
91+
},
92+
},
93+
}
94+
95+
secrets, err := injectSecrets(context.Background(), wfCfg, "")
96+
if err != nil {
97+
t.Fatalf("injectSecrets (legacy): %v", err)
98+
}
99+
if secrets["LEGACY_SECRET"] != "legacy-value" {
100+
t.Errorf("LEGACY_SECRET: got %q, want legacy-value", secrets["LEGACY_SECRET"])
101+
}
102+
}
103+
104+
func TestInjectSecrets_EmptyEntries(t *testing.T) {
105+
wfCfg := &config.WorkflowConfig{
106+
Secrets: &config.SecretsConfig{
107+
Provider: "env",
108+
Entries: nil,
109+
},
110+
}
111+
secrets, err := injectSecrets(context.Background(), wfCfg, "")
112+
if err != nil {
113+
t.Fatalf("unexpected error: %v", err)
114+
}
115+
if secrets != nil {
116+
t.Errorf("expected nil for empty entries, got %v", secrets)
117+
}
118+
}

0 commit comments

Comments
 (0)