From fb1c81212aa1f0ee235bf89494c8ab04304047d0 Mon Sep 17 00:00:00 2001 From: mkultraWasHere Date: Fri, 15 May 2026 16:31:09 -0400 Subject: [PATCH 1/2] feat: add instance_profile config field for EC2 Image Builder Wire InstanceProfile through Config struct, viper defaults, and dreadgoad.yaml so `ami build` falls back to the configured IAM instance profile when no --instance-profile flag is passed. Add tests for the Config field (including mapstructure tag validation) and getFlagString precedence logic in a new cmd/ami_test.go. --- cli/cmd/ami.go | 2 +- cli/cmd/ami_test.go | 67 ++++++++++++++++++++++++++++++ cli/internal/config/config.go | 9 ++-- cli/internal/config/config_test.go | 31 ++++++++++++++ cli/internal/config/defaults.go | 1 + dreadgoad.yaml | 1 + 6 files changed, 106 insertions(+), 5 deletions(-) create mode 100644 cli/cmd/ami_test.go diff --git a/cli/cmd/ami.go b/cli/cmd/ami.go index 25f6f793..2603487e 100644 --- a/cli/cmd/ami.go +++ b/cli/cmd/ami.go @@ -174,7 +174,7 @@ func runAMIBuild(cmd *cobra.Command, args []string) error { region: getFlagString(cmd, "region", cfg.Region, ""), instanceType: getFlagStringOpt(cmd, "instance-type"), profile: getFlagStringOpt(cmd, "profile"), - instanceProfile: getFlagStringOpt(cmd, "instance-profile"), + instanceProfile: getFlagString(cmd, "instance-profile", cfg.InstanceProfile, ""), reuseResources: getFlagBool(cmd, "reuse-resources"), } diff --git a/cli/cmd/ami_test.go b/cli/cmd/ami_test.go new file mode 100644 index 00000000..678cf121 --- /dev/null +++ b/cli/cmd/ami_test.go @@ -0,0 +1,67 @@ +package cmd + +import ( + "testing" + + "github.com/spf13/cobra" +) + +func TestGetFlagString(t *testing.T) { + newCmd := func(flagVal string) *cobra.Command { + cmd := &cobra.Command{Use: "test"} + cmd.Flags().String("instance-profile", "", "") + if flagVal != "" { + if err := cmd.Flags().Set("instance-profile", flagVal); err != nil { + t.Fatalf("set flag: %v", err) + } + } + return cmd + } + + tests := []struct { + name string + flagVal string + fallback1 string + fallback2 string + want string + }{ + { + name: "flag wins over config fallback", + flagVal: "FlagProfile", + fallback1: "ConfigProfile", + fallback2: "default", + want: "FlagProfile", + }, + { + name: "config fallback used when flag empty", + flagVal: "", + fallback1: "ConfigProfile", + fallback2: "default", + want: "ConfigProfile", + }, + { + name: "second fallback when both flag and config empty", + flagVal: "", + fallback1: "", + fallback2: "default", + want: "default", + }, + { + name: "empty when all sources empty", + flagVal: "", + fallback1: "", + fallback2: "", + want: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmd := newCmd(tt.flagVal) + got := getFlagString(cmd, "instance-profile", tt.fallback1, tt.fallback2) + if got != tt.want { + t.Errorf("getFlagString() = %q, want %q", got, tt.want) + } + }) + } +} diff --git a/cli/internal/config/config.go b/cli/internal/config/config.go index 88c84419..112a805c 100644 --- a/cli/internal/config/config.go +++ b/cli/internal/config/config.go @@ -86,10 +86,11 @@ func (l LudusConfig) SSHTarget() string { // Config holds all CLI configuration. type Config struct { - Env string `mapstructure:"env"` - Provider string `mapstructure:"provider"` - Region string `mapstructure:"region"` - Debug bool `mapstructure:"debug"` + Env string `mapstructure:"env"` + Provider string `mapstructure:"provider"` + Region string `mapstructure:"region"` + InstanceProfile string `mapstructure:"instance_profile"` + Debug bool `mapstructure:"debug"` MaxRetries int `mapstructure:"max_retries"` RetryDelay int `mapstructure:"retry_delay"` IdleTimeout int `mapstructure:"idle_timeout"` diff --git a/cli/internal/config/config_test.go b/cli/internal/config/config_test.go index aad564f7..801c86d2 100644 --- a/cli/internal/config/config_test.go +++ b/cli/internal/config/config_test.go @@ -3,6 +3,7 @@ package config import ( "os" "path/filepath" + "reflect" "strings" "testing" @@ -185,6 +186,36 @@ func TestLabConfigPath(t *testing.T) { }) } +func TestConfigInstanceProfile(t *testing.T) { + t.Run("field accessible on struct", func(t *testing.T) { + c := &Config{InstanceProfile: "WarpgateImageBuilderInstanceProfile"} + if c.InstanceProfile != "WarpgateImageBuilderInstanceProfile" { + t.Errorf("InstanceProfile = %q, want %q", c.InstanceProfile, "WarpgateImageBuilderInstanceProfile") + } + }) + + t.Run("zero value is empty string", func(t *testing.T) { + c := &Config{} + if c.InstanceProfile != "" { + t.Errorf("InstanceProfile = %q, want empty", c.InstanceProfile) + } + }) + + t.Run("mapstructure tag is instance_profile", func(t *testing.T) { + // Verify the struct tag so viper unmarshals the YAML key correctly. + var c Config + typ := reflect.TypeOf(c) + f, ok := typ.FieldByName("InstanceProfile") + if !ok { + t.Fatal("Config struct missing InstanceProfile field") + } + tag := f.Tag.Get("mapstructure") + if tag != "instance_profile" { + t.Errorf("mapstructure tag = %q, want %q", tag, "instance_profile") + } + }) +} + func TestConfigInventoryPathDifferentEnvs(t *testing.T) { tests := []struct { env string diff --git a/cli/internal/config/defaults.go b/cli/internal/config/defaults.go index 17371f42..2135c2e5 100644 --- a/cli/internal/config/defaults.go +++ b/cli/internal/config/defaults.go @@ -33,6 +33,7 @@ var RebootPlaybooks = []string{ func setDefaults() { viper.SetDefault("env", "staging") viper.SetDefault("region", "") + viper.SetDefault("instance_profile", "") viper.SetDefault("debug", false) viper.SetDefault("max_retries", 3) viper.SetDefault("retry_delay", 30) diff --git a/dreadgoad.yaml b/dreadgoad.yaml index 89082e19..8fac57eb 100644 --- a/dreadgoad.yaml +++ b/dreadgoad.yaml @@ -2,6 +2,7 @@ env: staging provider: aws # Provider: aws, proxmox, ludus # region: us-west-2 # Override AWS region (default: from inventory) +instance_profile: WarpgateImageBuilderInstanceProfile # IAM instance profile for EC2 Image Builder debug: false max_retries: 3 retry_delay: 30 From 0181c63ae6df2c67b40ae405978b64fb63f0f146 Mon Sep 17 00:00:00 2001 From: mkultraWasHere Date: Fri, 15 May 2026 16:33:33 -0400 Subject: [PATCH 2/2] style: fix struct field alignment in Config after InstanceProfile addition gofmt requires all fields in a struct to be aligned when any field name widens the column. Re-align MaxRetries through Ludus fields. --- cli/internal/config/config.go | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/cli/internal/config/config.go b/cli/internal/config/config.go index 112a805c..19fb5f05 100644 --- a/cli/internal/config/config.go +++ b/cli/internal/config/config.go @@ -91,17 +91,17 @@ type Config struct { Region string `mapstructure:"region"` InstanceProfile string `mapstructure:"instance_profile"` Debug bool `mapstructure:"debug"` - MaxRetries int `mapstructure:"max_retries"` - RetryDelay int `mapstructure:"retry_delay"` - IdleTimeout int `mapstructure:"idle_timeout"` - LogDir string `mapstructure:"log_dir"` - Playbooks []string `mapstructure:"playbooks"` - ProjectRoot string `mapstructure:"project_root"` - Environments map[string]EnvironmentConfig `mapstructure:"environments"` - Extensions map[string]ExtensionConfig `mapstructure:"extensions"` - Infra InfraConfig `mapstructure:"infra"` - Proxmox ProxmoxConfig `mapstructure:"proxmox"` - Ludus LudusConfig `mapstructure:"ludus"` + MaxRetries int `mapstructure:"max_retries"` + RetryDelay int `mapstructure:"retry_delay"` + IdleTimeout int `mapstructure:"idle_timeout"` + LogDir string `mapstructure:"log_dir"` + Playbooks []string `mapstructure:"playbooks"` + ProjectRoot string `mapstructure:"project_root"` + Environments map[string]EnvironmentConfig `mapstructure:"environments"` + Extensions map[string]ExtensionConfig `mapstructure:"extensions"` + Infra InfraConfig `mapstructure:"infra"` + Proxmox ProxmoxConfig `mapstructure:"proxmox"` + Ludus LudusConfig `mapstructure:"ludus"` } var (