Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 8 additions & 18 deletions cmd/wfctl/infra_secrets.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,9 @@ package main

import (
"fmt"
"os"

"github.com/GoCodeAlone/workflow/config"
"github.com/GoCodeAlone/workflow/secrets"
"gopkg.in/yaml.v3"
)

// SecretsConfig, SecretGen and InfraConfig are type aliases for the canonical
Expand All @@ -20,35 +18,27 @@ type SecretGen = config.SecretGen
type InfraConfig = config.InfraConfig

// parseSecretsConfig reads the "secrets:" top-level key from a YAML file.
// Imports declared in the file are resolved via config.LoadFromFile so that
// secrets.generate / secrets.entries blocks in imported files are visible.
// Returns nil, nil if the section is absent.
func parseSecretsConfig(cfgFile string) (*SecretsConfig, error) {
data, err := os.ReadFile(cfgFile)
cfg, err := config.LoadFromFile(cfgFile)
if err != nil {
return nil, fmt.Errorf("read %s: %w", cfgFile, err)
}
var parsed struct {
Secrets *SecretsConfig `yaml:"secrets"`
}
if err := yaml.Unmarshal(data, &parsed); err != nil {
return nil, fmt.Errorf("parse secrets config %s: %w", cfgFile, err)
}
return parsed.Secrets, nil
return cfg.Secrets, nil
}

// parseInfraConfig reads the "infra:" top-level section from a YAML file.
// Imports declared in the file are resolved via config.LoadFromFile so that
// infra: blocks in imported files are visible.
// Returns nil, nil if the section is absent.
func parseInfraConfig(cfgFile string) (*InfraConfig, error) {
data, err := os.ReadFile(cfgFile)
cfg, err := config.LoadFromFile(cfgFile)
if err != nil {
return nil, fmt.Errorf("read %s: %w", cfgFile, err)
}
var parsed struct {
Infra *InfraConfig `yaml:"infra"`
}
if err := yaml.Unmarshal(data, &parsed); err != nil {
return nil, fmt.Errorf("parse infra config %s: %w", cfgFile, err)
}
return parsed.Infra, nil
return cfg.Infra, nil
}

// resolveSecretsProvider constructs the appropriate secrets.Provider from cfg.
Expand Down
86 changes: 86 additions & 0 deletions cmd/wfctl/infra_secrets_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main

import (
"os"
"path/filepath"
"testing"

Expand Down Expand Up @@ -182,3 +183,88 @@ func TestResolveSecretsProvider_KeychainMissingService(t *testing.T) {

// Ensure GitHubSecretsProvider satisfies secrets.Provider interface.
var _ secrets.Provider = (*secrets.GitHubSecretsProvider)(nil)

// TestParseSecretsConfig_HonorsImports verifies that parseSecretsConfig
// resolves imports: a secrets.generate block declared only in an imported file
// is visible in the returned SecretsConfig.
func TestParseSecretsConfig_HonorsImports(t *testing.T) {
dir := t.TempDir()

importedYAML := `
secrets:
provider: env
config:
prefix: TEST_
generate:
- key: STAGING_PG_PASSWORD
type: random_hex
length: 32
`
mainYAML := `
imports:
- shared.yaml

modules:
- name: dummy
type: noop
`
if err := os.WriteFile(filepath.Join(dir, "shared.yaml"), []byte(importedYAML), 0o600); err != nil {
t.Fatal(err)
}
mainPath := filepath.Join(dir, "main.yaml")
if err := os.WriteFile(mainPath, []byte(mainYAML), 0o600); err != nil {
t.Fatal(err)
}

cfg, err := parseSecretsConfig(mainPath)
if err != nil {
t.Fatalf("parseSecretsConfig: %v", err)
}
if cfg == nil {
t.Fatal("expected non-nil SecretsConfig from imported file, got nil")
}
if len(cfg.Generate) != 1 {
t.Fatalf("expected 1 generate entry from import, got %d", len(cfg.Generate))
}
if cfg.Generate[0].Key != "STAGING_PG_PASSWORD" {
t.Errorf("expected key STAGING_PG_PASSWORD, got %q", cfg.Generate[0].Key)
}
}

// TestParseInfraConfig_HonorsImports verifies that parseInfraConfig resolves
// imports: an infra: block declared only in an imported file is visible in the
// returned InfraConfig.
func TestParseInfraConfig_HonorsImports(t *testing.T) {
dir := t.TempDir()

importedYAML := `
infra:
auto_bootstrap: false
`
mainYAML := `
imports:
- shared.yaml

modules:
- name: dummy
type: noop
`
if err := os.WriteFile(filepath.Join(dir, "shared.yaml"), []byte(importedYAML), 0o600); err != nil {
t.Fatal(err)
}
mainPath := filepath.Join(dir, "main.yaml")
if err := os.WriteFile(mainPath, []byte(mainYAML), 0o600); err != nil {
t.Fatal(err)
}

cfg, err := parseInfraConfig(mainPath)
if err != nil {
t.Fatalf("parseInfraConfig: %v", err)
}
if cfg == nil {
t.Fatal("expected non-nil InfraConfig from imported file, got nil")
}
if cfg.AutoBootstrap == nil || *cfg.AutoBootstrap {
t.Error("expected auto_bootstrap=false from import")
}
}
4 changes: 4 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,10 @@ func (cfg *WorkflowConfig) processImports(seen map[string]bool) error {
existingEntries[e.Name] = struct{}{}
}
}
// Merge Infra config — parent wins; import fills in if parent has no Infra block.
if impCfg.Infra != nil && cfg.Infra == nil {
cfg.Infra = impCfg.Infra
}
}

cfg.Imports = nil // clear after processing
Expand Down
40 changes: 40 additions & 0 deletions config/config_import_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -970,3 +970,43 @@ environments:
t.Error("expected Environments[local] from main")
}
}

// TestProcessImports_MergesInfraFromImport pins that WorkflowConfig.Infra is
// merged from imported files when the main config has no infra: block.
// parseInfraConfig (cmd/wfctl) uses config.LoadFromFile, so this exercises
// the same code path as wfctl infra apply auto-bootstrap detection.
func TestProcessImports_MergesInfraFromImport(t *testing.T) {
dir := t.TempDir()

importedYAML := `
infra:
auto_bootstrap: false
`
if err := os.WriteFile(filepath.Join(dir, "shared.yaml"), []byte(importedYAML), 0644); err != nil {
t.Fatal(err)
}

mainYAML := `
imports:
- shared.yaml

modules:
- name: dummy
type: noop
`
mainPath := filepath.Join(dir, "main.yaml")
if err := os.WriteFile(mainPath, []byte(mainYAML), 0644); err != nil {
t.Fatal(err)
}

cfg, err := LoadFromFile(mainPath)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if cfg.Infra == nil {
t.Fatal("expected cfg.Infra to be populated from import, got nil")
}
if cfg.Infra.AutoBootstrap == nil || *cfg.Infra.AutoBootstrap {
t.Error("expected AutoBootstrap=false from import")
}
}
Loading