diff --git a/cli/cmd/ami.go b/cli/cmd/ami.go new file mode 100644 index 00000000..b9fc051b --- /dev/null +++ b/cli/cmd/ami.go @@ -0,0 +1,495 @@ +package cmd + +import ( + "context" + "fmt" + "os" + "os/signal" + "path/filepath" + "regexp" + "strings" + "sync" + "time" + + "github.com/cowdogmoo/warpgate/v3/builder" + "github.com/cowdogmoo/warpgate/v3/builder/ami" + warplog "github.com/cowdogmoo/warpgate/v3/logging" + "github.com/cowdogmoo/warpgate/v3/progress" + "github.com/dreadnode/dreadgoad/internal/config" + "github.com/fatih/color" + "github.com/spf13/cobra" + "github.com/spf13/viper" + "gopkg.in/yaml.v3" +) + +var amiCmd = &cobra.Command{ + Use: "ami", + Short: "AMI image management", +} + +var amiPurgeCmd = &cobra.Command{ + Use: "purge [template]", + Short: "Remove Image Builder pipeline resources (not AMIs)", + Long: `Delete EC2 Image Builder pipeline resources (components, recipes, pipelines, +infrastructure configs, distribution configs) left behind by warpgate builds. +Does NOT delete the built AMIs themselves. + +Without arguments, removes all warpgate pipeline resources. +With a template name, only removes resources for that specific build.`, + RunE: runAMIPurge, +} + +var amiListResourcesCmd = &cobra.Command{ + Use: "list-resources", + Short: "List Image Builder pipeline resources created by warpgate", + Long: `Lists all EC2 Image Builder pipeline resources tagged with warpgate metadata. +These are the intermediate build resources (components, recipes, pipelines), +not the resulting AMIs.`, + RunE: runAMIListResources, +} + +var amiBuildCmd = &cobra.Command{ + Use: "build [template]", + Short: "Build an AMI from a warpgate template", + Long: `Build an AMI using EC2 Image Builder from a warpgate template. + +Template can be: + - A template name (e.g. "goad-dc-base") from warpgate-templates/ + - A path to a warpgate.yaml file or directory containing one + - Omitted with --all to build all templates in warpgate-templates/ + +With --all, builds run in parallel. Shows a progress bar per build +by default. Use --debug for detailed build output.`, + RunE: runAMIBuild, +} + +func init() { + rootCmd.AddCommand(amiCmd) + amiCmd.AddCommand(amiBuildCmd) + amiCmd.AddCommand(amiPurgeCmd) + amiCmd.AddCommand(amiListResourcesCmd) + + amiBuildCmd.Flags().String("region", "", "AWS region (overrides template)") + amiBuildCmd.Flags().String("instance-type", "", "EC2 instance type (overrides template)") + amiBuildCmd.Flags().String("profile", "", "AWS profile") + amiBuildCmd.Flags().String("instance-profile", "", "IAM instance profile for EC2 Image Builder") + amiBuildCmd.Flags().Bool("reuse-resources", false, "Reuse existing Image Builder resources instead of recreating") + amiBuildCmd.Flags().Bool("all", false, "Build all templates in warpgate-templates/") + + amiPurgeCmd.Flags().String("region", "", "AWS region") + amiPurgeCmd.Flags().String("profile", "", "AWS profile") + amiPurgeCmd.Flags().Bool("yes", false, "Skip confirmation prompt") + + amiListResourcesCmd.Flags().String("region", "", "AWS region") + amiListResourcesCmd.Flags().String("profile", "", "AWS profile") +} + +func resolveTemplates(cfg *config.Config, args []string, buildAll bool) ([]string, error) { + if !buildAll && len(args) == 0 { + return nil, fmt.Errorf("requires a template argument or --all flag") + } + if buildAll && len(args) > 0 { + return nil, fmt.Errorf("--all flag cannot be used with a template argument") + } + if buildAll { + templates, err := discoverWarpgateTemplates(cfg.ProjectRoot) + if err != nil { + return nil, err + } + if len(templates) == 0 { + return nil, fmt.Errorf("no templates found in warpgate-templates/") + } + return templates, nil + } + p, err := resolveTemplatePath(cfg, args[0]) + if err != nil { + return nil, err + } + return []string{p}, nil +} + +func runAMIBuild(cmd *cobra.Command, args []string) error { + ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt) + defer stop() + + cfg, err := config.Get() + if err != nil { + return err + } + + buildAll, _ := cmd.Flags().GetBool("all") + templates, err := resolveTemplates(cfg, args, buildAll) + if err != nil { + return err + } + + verbose := viper.GetBool("debug") + + bf := buildFlags{ + region: getFlagString(cmd, "region", cfg.Region, "us-west-1"), + instanceType: getFlagStringOpt(cmd, "instance-type"), + profile: getFlagStringOpt(cmd, "profile"), + instanceProfile: getFlagStringOpt(cmd, "instance-profile"), + reuseResources: getFlagBool(cmd, "reuse-resources"), + } + + // Set up progress display — add all bars before starting the render loop + // to avoid partial renders with mismatched ANSI cursor-up counts. + display := progress.NewDisplay(os.Stderr) + bars := make([]*progress.Bar, len(templates)) + for i, tmplPath := range templates { + bars[i] = display.AddBar(templateName(tmplPath), i+1, len(templates)) + } + if !verbose { + display.Start(500 * time.Millisecond) + } + + results := make([]amiBuildResult, len(templates)) + var wg sync.WaitGroup + + for i, tmplPath := range templates { + wg.Add(1) + + go func(idx int, path string, bar *progress.Bar) { + defer wg.Done() + result, buildErr := buildSingleAMI(ctx, cfg, path, bf, bar, verbose) + if buildErr != nil { + results[idx] = amiBuildResult{template: path, err: buildErr} + } else { + results[idx] = *result + } + }(i, tmplPath, bars[i]) + } + + wg.Wait() + + if !verbose { + display.Stop() + } + + fmt.Fprintln(os.Stderr) + printBuildSummary(results) + + for _, r := range results { + if r.err != nil { + return fmt.Errorf("one or more builds failed") + } + } + return nil +} + +type buildFlags struct { + region string + instanceType string + profile string + instanceProfile string + reuseResources bool +} + +type amiBuildResult struct { + template string + amiID string + region string + duration string + err error +} + +func buildSingleAMI(ctx context.Context, cfg *config.Config, templatePath string, bf buildFlags, bar *progress.Bar, verbose bool) (*amiBuildResult, error) { + tmplName := templateName(templatePath) + buildCfg, err := loadWarpgateTemplate(templatePath, cfg.ProjectRoot) + if err != nil { + bar.Fail() + return nil, fmt.Errorf("load template %s: %w", tmplName, err) + } + + for i := range buildCfg.Targets { + if buildCfg.Targets[i].Type != "ami" { + continue + } + if bf.region != "" { + buildCfg.Targets[i].Region = bf.region + } + if bf.instanceType != "" { + buildCfg.Targets[i].InstanceType = bf.instanceType + } + if bf.instanceProfile != "" { + buildCfg.Targets[i].InstanceProfileName = bf.instanceProfile + } + } + + clientCfg := ami.ClientConfig{ + Region: bf.region, + Profile: bf.profile, + } + + forceRecreate := !bf.reuseResources + + // Set up warpgate logger — quiet mode suppresses info logs that break progress bars + var warpLogger *warplog.CustomLogger + if verbose { + warpLogger = warplog.NewCustomLoggerWithOptions("debug", "color", false, true) + warpLogger.ConsoleWriter = os.Stderr + } else { + warpLogger = warplog.NewCustomLoggerWithOptions("error", "plain", true, false) + warpLogger.ConsoleWriter = os.Stderr + bar.Update("Initializing", 0.01, 0, 0) + } + + ctx = warplog.WithLogger(ctx, warpLogger) + + monitorCfg := ami.MonitorConfig{ + StreamLogs: verbose, + ShowEC2Status: verbose, + StatusCallback: func(update ami.StatusUpdate) { + if !verbose { + bar.Update(update.Stage, update.Progress, update.Elapsed, update.EstimatedRemaining) + } + }, + } + + imgBuilder, err := ami.NewImageBuilderWithAllOptions(ctx, clientCfg, forceRecreate, monitorCfg) + if err != nil { + bar.Fail() + return nil, fmt.Errorf("create AMI builder for %s: %w", tmplName, err) + } + defer func() { _ = imgBuilder.Close() }() + + result, err := imgBuilder.Build(ctx, *buildCfg) + if err != nil { + bar.Fail() + return nil, fmt.Errorf("%s failed: %w", tmplName, err) + } + + bar.CompleteWithMessage(result.AMIID) + + return &amiBuildResult{ + template: templatePath, + amiID: result.AMIID, + region: result.Region, + duration: result.Duration, + }, nil +} + +func templateName(path string) string { + return filepath.Base(filepath.Dir(path)) +} + +func getFlagString(cmd *cobra.Command, name, fallback1, fallback2 string) string { + if v, _ := cmd.Flags().GetString(name); v != "" { + return v + } + if fallback1 != "" { + return fallback1 + } + return fallback2 +} + +func getFlagStringOpt(cmd *cobra.Command, name string) string { + v, _ := cmd.Flags().GetString(name) + return v +} + +func getFlagBool(cmd *cobra.Command, name string) bool { + v, _ := cmd.Flags().GetBool(name) + return v +} + +func newAWSClients(cmd *cobra.Command, cfg *config.Config) (*ami.AWSClients, error) { + region := getFlagString(cmd, "region", cfg.Region, "us-west-1") + profile := getFlagStringOpt(cmd, "profile") + return ami.NewAWSClients(context.Background(), ami.ClientConfig{ + Region: region, + Profile: profile, + }) +} + +func runAMIListResources(cmd *cobra.Command, args []string) error { + cfg, err := config.Get() + if err != nil { + return err + } + + clients, err := newAWSClients(cmd, cfg) + if err != nil { + return fmt.Errorf("create AWS clients: %w", err) + } + + cleaner := ami.NewResourceCleaner(clients) + ctx := context.Background() + + resources, err := cleaner.ListWarpgateResources(ctx) + if err != nil { + return fmt.Errorf("list resources: %w", err) + } + + if len(resources) == 0 { + color.Green("No warpgate pipeline resources found.") + return nil + } + + fmt.Printf("\nFound %d pipeline resources:\n\n", len(resources)) + fmt.Printf(" %-30s %-25s %-10s %s\n", "TYPE", "NAME", "VERSION", "BUILD") + fmt.Printf(" %-30s %-25s %-10s %s\n", "----", "----", "-------", "-----") + for _, r := range resources { + version := r.Version + if version == "" { + version = "-" + } + buildName := r.BuildName + if buildName == "" { + buildName = "-" + } + fmt.Printf(" %-30s %-25s %-10s %s\n", r.Type, r.Name, version, buildName) + } + fmt.Println() + return nil +} + +func runAMIPurge(cmd *cobra.Command, args []string) error { + cfg, err := config.Get() + if err != nil { + return err + } + + clients, err := newAWSClients(cmd, cfg) + if err != nil { + return fmt.Errorf("create AWS clients: %w", err) + } + + cleaner := ami.NewResourceCleaner(clients) + ctx := context.Background() + + var resources []ami.ResourceInfo + if len(args) > 0 { + resources, err = cleaner.ListResourcesForBuild(ctx, args[0]) + } else { + resources, err = cleaner.ListWarpgateResources(ctx) + } + if err != nil { + return fmt.Errorf("list resources: %w", err) + } + + if len(resources) == 0 { + color.Green("No warpgate pipeline resources found.") + return nil + } + + fmt.Printf("\nPipeline resources to delete (%d):\n\n", len(resources)) + for _, r := range resources { + fmt.Printf(" %-30s %s\n", r.Type, r.Name) + } + fmt.Println() + color.Yellow("NOTE: This deletes pipeline resources only, NOT the built AMIs.") + + skipConfirm, _ := cmd.Flags().GetBool("yes") + if !skipConfirm { + fmt.Print("\nProceed? [y/N] ") + var answer string + if _, err := fmt.Scanln(&answer); err != nil { + return fmt.Errorf("read input: %w", err) + } + if strings.ToLower(strings.TrimSpace(answer)) != "y" { + fmt.Println("Aborted.") + return nil + } + } + + fmt.Println() + if err := cleaner.DeleteResources(ctx, resources); err != nil { + return fmt.Errorf("purge failed: %w", err) + } + + color.Green("Purge complete.") + return nil +} + +func resolveTemplatePath(cfg *config.Config, arg string) (string, error) { + if info, err := os.Stat(arg); err == nil { + if info.IsDir() { + p := filepath.Join(arg, "warpgate.yaml") + if _, err := os.Stat(p); err == nil { + return p, nil + } + return "", fmt.Errorf("no warpgate.yaml in directory: %s", arg) + } + return arg, nil + } + + p := filepath.Join(cfg.ProjectRoot, "warpgate-templates", arg, "warpgate.yaml") + if _, err := os.Stat(p); err == nil { + return p, nil + } + + return "", fmt.Errorf("template not found: %s (tried as path and in warpgate-templates/)", arg) +} + +func discoverWarpgateTemplates(projectRoot string) ([]string, error) { + dir := filepath.Join(projectRoot, "warpgate-templates") + entries, err := os.ReadDir(dir) + if err != nil { + return nil, fmt.Errorf("read warpgate-templates: %w", err) + } + + var templates []string + for _, entry := range entries { + if !entry.IsDir() { + continue + } + p := filepath.Join(dir, entry.Name(), "warpgate.yaml") + if _, err := os.Stat(p); err == nil { + templates = append(templates, p) + } + } + return templates, nil +} + +type templateWithVars struct { + Variables map[string]string `yaml:"variables"` +} + +func loadWarpgateTemplate(path, projectRoot string) (*builder.Config, error) { + data, err := os.ReadFile(path) + if err != nil { + return nil, err + } + + var tmpl templateWithVars + _ = yaml.Unmarshal(data, &tmpl) + + content := string(data) + + for k, v := range tmpl.Variables { + content = strings.ReplaceAll(content, "${"+k+"}", v) + } + + if _, ok := os.LookupEnv("PROVISION_REPO_PATH"); !ok && projectRoot != "" { + _ = os.Setenv("PROVISION_REPO_PATH", projectRoot) + } + + varPattern := regexp.MustCompile(`\$\{([^}]+)\}`) + content = varPattern.ReplaceAllStringFunc(content, func(match string) string { + varName := match[2 : len(match)-1] + if val, ok := os.LookupEnv(varName); ok { + return val + } + return match + }) + + var cfg builder.Config + if err := yaml.Unmarshal([]byte(content), &cfg); err != nil { + return nil, fmt.Errorf("parse template: %w", err) + } + + return &cfg, nil +} + +func printBuildSummary(results []amiBuildResult) { + for _, r := range results { + name := filepath.Base(filepath.Dir(r.template)) + if r.err != nil { + _, _ = color.New(color.FgRed).Fprintf(os.Stderr, " x %-25s FAILED: %s\n", name, r.err) + } else { + _, _ = color.New(color.FgGreen).Fprintf(os.Stderr, " + %-25s %s (%s)\n", name, r.amiID, r.duration) + } + } +} diff --git a/cli/cmd/env_cmd.go b/cli/cmd/env_cmd.go new file mode 100644 index 00000000..1fdff768 --- /dev/null +++ b/cli/cmd/env_cmd.go @@ -0,0 +1,336 @@ +package cmd + +import ( + "encoding/json" + "fmt" + "io/fs" + "os" + "path/filepath" + "strings" + + "github.com/dreadnode/dreadgoad/internal/config" + "github.com/dreadnode/dreadgoad/internal/variant" + "github.com/fatih/color" + "github.com/spf13/cobra" +) + +var envCmd = &cobra.Command{ + Use: "env", + Short: "Manage deployment environments", +} + +var envCreateCmd = &cobra.Command{ + Use: "create ", + Short: "Create a new deployment environment", + Long: `Scaffold a new deployment environment with all required infrastructure +and configuration files. + +Creates: + - infra/goad-deployment/{env}/env.hcl + - infra/goad-deployment/{env}/{region}/region.hcl + - infra/goad-deployment/{env}/{region}/network/terragrunt.hcl + - infra/goad-deployment/{env}/{region}/goad/{host}/terragrunt.hcl + templates + - ad/GOAD/data/{env}-config.json + +Use --variant to generate randomized entity names for the environment config. +Without --variant, the base config (dev-config.json) is copied as-is.`, + Args: cobra.ExactArgs(1), + RunE: runEnvCreate, +} + +var envListCmd = &cobra.Command{ + Use: "list", + Short: "List available environments", + RunE: runEnvList, +} + +func init() { + rootCmd.AddCommand(envCmd) + envCmd.AddCommand(envCreateCmd) + envCmd.AddCommand(envListCmd) + + envCreateCmd.Flags().String("region", "us-east-1", "AWS region for the environment") + envCreateCmd.Flags().String("vpc-cidr", "", "VPC CIDR block (default: auto-assigned)") + envCreateCmd.Flags().String("reference", "staging", "Reference environment to copy infrastructure from") + envCreateCmd.Flags().Bool("variant", false, "Generate randomized variant config") + envCreateCmd.Flags().Bool("force", false, "Overwrite existing environment") +} + +func vpcCIDRForEnv(envName string) string { + knownCIDRs := map[string]string{ + "dev": "10.0.0.0/16", + "staging": "10.1.0.0/16", + "prod": "10.2.0.0/16", + "test": "10.8.0.0/16", + } + if cidr, ok := knownCIDRs[envName]; ok { + return cidr + } + // Generate a deterministic second octet from env name (range 10-250) + var hash byte + for _, c := range envName { + hash = hash*31 + byte(c) + } + octet := int(hash)%240 + 10 + return fmt.Sprintf("10.%d.0.0/16", octet) +} + +func runEnvCreate(cmd *cobra.Command, args []string) error { + envName := strings.TrimSpace(args[0]) + if envName == "" { + return fmt.Errorf("environment name cannot be empty") + } + + cfg, err := config.Get() + if err != nil { + return err + } + + region, _ := cmd.Flags().GetString("region") + vpcCIDR, _ := cmd.Flags().GetString("vpc-cidr") + reference, _ := cmd.Flags().GetString("reference") + useVariant, _ := cmd.Flags().GetBool("variant") + force, _ := cmd.Flags().GetBool("force") + + if vpcCIDR == "" { + vpcCIDR = vpcCIDRForEnv(envName) + } + + deployment := cfg.Infra.Deployment + infraBase := filepath.Join(cfg.ProjectRoot, "infra", deployment) + envDir := filepath.Join(infraBase, envName) + regionDir := filepath.Join(envDir, region) + + if _, err := os.Stat(envDir); err == nil && !force { + return fmt.Errorf("environment %q already exists at %s\nUse --force to overwrite", envName, envDir) + } + + refRegionDir := findReferenceRegion(infraBase, reference) + if refRegionDir == "" { + return fmt.Errorf("reference environment %q not found in %s", reference, infraBase) + } + + color.Cyan("Creating environment: %s", envName) + fmt.Printf(" %-14s %s\n", "Region:", region) + fmt.Printf(" %-14s %s\n", "VPC CIDR:", vpcCIDR) + fmt.Printf(" %-14s %s\n", "Reference:", reference) + fmt.Printf(" %-14s %v\n", "Variant:", useVariant) + fmt.Println() + + if err := createEnvHCL(envDir, envName, vpcCIDR); err != nil { + return fmt.Errorf("create env.hcl: %w", err) + } + color.Green(" Created env.hcl") + + if err := createRegionHCL(regionDir, region); err != nil { + return fmt.Errorf("create region.hcl: %w", err) + } + color.Green(" Created %s/region.hcl", region) + + if err := copyInfrastructure(refRegionDir, regionDir); err != nil { + return fmt.Errorf("copy infrastructure: %w", err) + } + color.Green(" Copied infrastructure from %s", reference) + + configPath := filepath.Join(cfg.ProjectRoot, "ad", "GOAD", "data", envName+"-config.json") + if useVariant { + if err := generateVariantConfig(cfg.ProjectRoot, envName); err != nil { + return fmt.Errorf("generate variant config: %w", err) + } + color.Green(" Generated variant config: %s-config.json", envName) + } else { + if err := copyBaseConfig(cfg.ProjectRoot, envName); err != nil { + return fmt.Errorf("copy base config: %w", err) + } + color.Green(" Created config: %s-config.json", envName) + } + + fmt.Println() + color.Green("Environment %q created successfully!", envName) + fmt.Println() + fmt.Println("Next steps:") + fmt.Printf(" 1. Review: %s\n", envDir) + fmt.Printf(" 2. Review: %s\n", configPath) + fmt.Printf(" 3. Initialize: dreadgoad --env %s --region %s infra init\n", envName, region) + fmt.Printf(" 4. Plan: dreadgoad --env %s --region %s infra plan\n", envName, region) + fmt.Printf(" 5. Apply: dreadgoad --env %s --region %s infra apply --auto-approve\n", envName, region) + + return nil +} + +func runEnvList(cmd *cobra.Command, args []string) error { + cfg, err := config.Get() + if err != nil { + return err + } + + deployment := cfg.Infra.Deployment + infraBase := filepath.Join(cfg.ProjectRoot, "infra", deployment) + + entries, err := os.ReadDir(infraBase) + if err != nil { + return fmt.Errorf("read deployment directory: %w", err) + } + + color.Cyan("Available environments:") + for _, entry := range entries { + if !entry.IsDir() { + continue + } + name := entry.Name() + envHCL := filepath.Join(infraBase, name, "env.hcl") + if _, err := os.Stat(envHCL); err != nil { + continue + } + + var regions []string + regionEntries, _ := os.ReadDir(filepath.Join(infraBase, name)) + for _, re := range regionEntries { + if !re.IsDir() { + continue + } + regionHCL := filepath.Join(infraBase, name, re.Name(), "region.hcl") + if _, err := os.Stat(regionHCL); err == nil { + regions = append(regions, re.Name()) + } + } + + configFile := filepath.Join(cfg.ProjectRoot, "ad", "GOAD", "data", name+"-config.json") + hasConfig := false + if _, err := os.Stat(configFile); err == nil { + hasConfig = true + } + + marker := " " + if name == cfg.Env { + marker = "*" + } + + configStatus := color.RedString("no config") + if hasConfig { + configStatus = color.GreenString("config OK") + } + + fmt.Printf(" %s %-12s regions: %-20s %s\n", + marker, name, strings.Join(regions, ", "), configStatus) + } + + return nil +} + +func findReferenceRegion(infraBase, reference string) string { + refDir := filepath.Join(infraBase, reference) + entries, err := os.ReadDir(refDir) + if err != nil { + return "" + } + for _, entry := range entries { + if !entry.IsDir() { + continue + } + regionHCL := filepath.Join(refDir, entry.Name(), "region.hcl") + if _, err := os.Stat(regionHCL); err == nil { + return filepath.Join(refDir, entry.Name()) + } + } + return "" +} + +func createEnvHCL(envDir, envName, vpcCIDR string) error { + if err := os.MkdirAll(envDir, 0o755); err != nil { + return err + } + content := fmt.Sprintf(`# Set common variables for the environment. +# This is automatically pulled in by the root terragrunt.hcl configuration. +locals { + deployment_name = "goad" # Change to your deployment name + aws_account_id = get_aws_account_id() + env = %q + vpc_cidr = %q +} +`, envName, vpcCIDR) + return os.WriteFile(filepath.Join(envDir, "env.hcl"), []byte(content), 0o644) +} + +func createRegionHCL(regionDir, region string) error { + if err := os.MkdirAll(regionDir, 0o755); err != nil { + return err + } + content := fmt.Sprintf(`locals { + aws_region = %q +} +`, region) + return os.WriteFile(filepath.Join(regionDir, "region.hcl"), []byte(content), 0o644) +} + +func copyInfrastructure(srcRegionDir, dstRegionDir string) error { + return filepath.WalkDir(srcRegionDir, func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + + relPath, err := filepath.Rel(srcRegionDir, path) + if err != nil { + return err + } + + if strings.Contains(relPath, ".terragrunt-cache") || + strings.Contains(relPath, ".terraform") || + strings.HasSuffix(relPath, ".terraform.lock.hcl") || + relPath == "region.hcl" { + if d.IsDir() { + return filepath.SkipDir + } + return nil + } + + dstPath := filepath.Join(dstRegionDir, relPath) + + if d.IsDir() { + return os.MkdirAll(dstPath, 0o755) + } + + data, err := os.ReadFile(path) + if err != nil { + return err + } + return os.WriteFile(dstPath, data, 0o644) + }) +} + +func copyBaseConfig(projectRoot, envName string) error { + srcPath := filepath.Join(projectRoot, "ad", "GOAD", "data", "dev-config.json") + dstPath := filepath.Join(projectRoot, "ad", "GOAD", "data", envName+"-config.json") + + data, err := os.ReadFile(srcPath) + if err != nil { + return fmt.Errorf("read base config: %w", err) + } + + var parsed interface{} + if err := json.Unmarshal(data, &parsed); err != nil { + return fmt.Errorf("invalid base config JSON: %w", err) + } + + return os.WriteFile(dstPath, data, 0o644) +} + +func generateVariantConfig(projectRoot, envName string) error { + source := filepath.Join(projectRoot, "ad", "GOAD") + target := filepath.Join(projectRoot, "ad", "GOAD-"+envName) + + gen := variant.NewGenerator(source, target, envName) + if err := gen.Run(); err != nil { + return fmt.Errorf("variant generation: %w", err) + } + + srcConfig := filepath.Join(target, "data", "config.json") + dstConfig := filepath.Join(projectRoot, "ad", "GOAD", "data", envName+"-config.json") + + data, err := os.ReadFile(srcConfig) + if err != nil { + return fmt.Errorf("read generated variant config: %w", err) + } + + return os.WriteFile(dstConfig, data, 0o644) +} diff --git a/cli/go.mod b/cli/go.mod index dc71b91c..808f877c 100644 --- a/cli/go.mod +++ b/cli/go.mod @@ -8,36 +8,56 @@ require ( github.com/aws/aws-sdk-go-v2/service/ec2 v1.296.2 github.com/aws/aws-sdk-go-v2/service/ssm v1.68.4 github.com/aws/aws-sdk-go-v2/service/sts v1.41.10 + github.com/cowdogmoo/warpgate/v3 v3.2.1-0.20260407013053-5aed3c62eb90 github.com/fatih/color v1.19.0 github.com/spf13/cobra v1.10.2 github.com/spf13/viper v1.21.0 go.yaml.in/yaml/v3 v3.0.4 + gopkg.in/yaml.v3 v3.0.1 ) require ( + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 // indirect github.com/aws/aws-sdk-go-v2/credentials v1.19.14 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.21 // indirect github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 // indirect github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 // indirect github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 // indirect + github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs v1.68.0 // indirect + github.com/aws/aws-sdk-go-v2/service/iam v1.53.7 // indirect + github.com/aws/aws-sdk-go-v2/service/imagebuilder v1.51.4 // indirect github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 // indirect github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21 // indirect github.com/aws/aws-sdk-go-v2/service/signin v1.0.9 // indirect github.com/aws/aws-sdk-go-v2/service/sso v1.30.15 // indirect github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.19 // indirect - github.com/aws/smithy-go v1.24.2 // indirect + github.com/aws/smithy-go v1.24.3 // indirect + github.com/containerd/stargz-snapshotter/estargz v0.18.2 // indirect + github.com/docker/cli v29.3.1+incompatible // indirect + github.com/docker/distribution v2.8.3+incompatible // indirect + github.com/docker/docker-credential-helpers v0.9.5 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect - github.com/go-viper/mapstructure/v2 v2.4.0 // indirect + github.com/go-viper/mapstructure/v2 v2.5.0 // indirect + github.com/google/go-containerregistry v0.21.3 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/klauspost/compress v1.18.5 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/pelletier/go-toml/v2 v2.2.4 // indirect - github.com/sagikazarmark/locafero v0.11.0 // indirect - github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.1 // indirect + github.com/pelletier/go-toml/v2 v2.3.0 // indirect + github.com/sagikazarmark/locafero v0.12.0 // indirect + github.com/sirupsen/logrus v1.9.4 // indirect github.com/spf13/afero v1.15.0 // indirect github.com/spf13/cast v1.10.0 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/subosito/gotenv v1.6.0 // indirect + github.com/vbatts/tar-split v0.12.2 // indirect + golang.org/x/sync v0.20.0 // indirect golang.org/x/sys v0.42.0 // indirect - golang.org/x/text v0.28.0 // indirect + golang.org/x/term v0.41.0 // indirect + golang.org/x/text v0.35.0 // indirect + gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect + gotest.tools/v3 v3.5.2 // indirect ) diff --git a/cli/go.sum b/cli/go.sum index 935c135f..db103e91 100644 --- a/cli/go.sum +++ b/cli/go.sum @@ -1,5 +1,7 @@ github.com/aws/aws-sdk-go-v2 v1.41.5 h1:dj5kopbwUsVUVFgO4Fi5BIT3t4WyqIDjGKCangnV/yY= github.com/aws/aws-sdk-go-v2 v1.41.5/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 h1:eBMB84YGghSocM7PsjmmPffTa+1FBUeNvGvFou6V/4o= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8/go.mod h1:lyw7GFp3qENLh7kwzf7iMzAxDn+NzjXEAGjKS2UOKqI= github.com/aws/aws-sdk-go-v2/config v1.32.14 h1:opVIRo/ZbbI8OIqSOKmpFaY7IwfFUOCCXBsUpJOwDdI= github.com/aws/aws-sdk-go-v2/config v1.32.14/go.mod h1:U4/V0uKxh0Tl5sxmCBZ3AecYny4UNlVmObYjKuuaiOo= github.com/aws/aws-sdk-go-v2/credentials v1.19.14 h1:n+UcGWAIZHkXzYt87uMFBv/l8THYELoX6gVcUvgl6fI= @@ -12,8 +14,14 @@ github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 h1:PEgGVtPoB6NTpPrBgq github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21/go.mod h1:p+hz+PRAYlY3zcpJhPwXlLC4C+kqn70WIHwnzAfs6ps= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 h1:qYQ4pzQ2Oz6WpQ8T3HvGHnZydA72MnLuFK9tJwmrbHw= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6/go.mod h1:O3h0IK87yXci+kg6flUKzJnWeziQUKciKrLjcatSNcY= +github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs v1.68.0 h1:+/lmB/+i2oqkzbmlQxsW0kr/+wmJgmyiEF9VDJicX34= +github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs v1.68.0/go.mod h1:PobeppEnIjw4pcgjFryNDZCTH7AiqZw0yb5r98Gvf9c= github.com/aws/aws-sdk-go-v2/service/ec2 v1.296.2 h1:Ytu50ChAxCiDsOlBcBq8jbczXy6+QLb07T65DBJASRs= github.com/aws/aws-sdk-go-v2/service/ec2 v1.296.2/go.mod h1:R+2BNtUfTfhPY0RH18oL02q116bakeBWjanrbnVBqkM= +github.com/aws/aws-sdk-go-v2/service/iam v1.53.7 h1:n9YLiWtX3+6pTLZWvRJmtq5JIB9NA/KFelyCg5fOlTU= +github.com/aws/aws-sdk-go-v2/service/iam v1.53.7/go.mod h1:sP46Vo6MeJcM4s0ZXcG2PFmfiSyixhIuC/74W52yKuk= +github.com/aws/aws-sdk-go-v2/service/imagebuilder v1.51.4 h1:61NE9tKMXuMSWOENligQ6jchSYfh8wYjBxaWDZaJn+o= +github.com/aws/aws-sdk-go-v2/service/imagebuilder v1.51.4/go.mod h1:MmJet6SEjFAY+iKome2cLNYrK4yNbdclhBDkehrpNkE= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 h1:5EniKhLZe4xzL7a+fU3C2tfUN4nWIqlLesfrjkuPFTY= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21 h1:c31//R3xgIJMSC8S6hEVq+38DcvUlgFY0FM6mSI5oto= @@ -28,23 +36,37 @@ github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.19 h1:dzztQ1YmfPrxdrOiuZRMF6f github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.19/go.mod h1:YO8TrYtFdl5w/4vmjL8zaBSsiNp3w0L1FfKVKenZT7w= github.com/aws/aws-sdk-go-v2/service/sts v1.41.10 h1:p8ogvvLugcR/zLBXTXrTkj0RYBUdErbMnAFFp12Lm/U= github.com/aws/aws-sdk-go-v2/service/sts v1.41.10/go.mod h1:60dv0eZJfeVXfbT1tFJinbHrDfSJ2GZl4Q//OSSNAVw= -github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng= -github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= +github.com/aws/smithy-go v1.24.3 h1:XgOAaUgx+HhVBoP4v8n6HCQoTRDhoMghKqw4LNHsDNg= +github.com/aws/smithy-go v1.24.3/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= +github.com/containerd/stargz-snapshotter/estargz v0.18.2 h1:yXkZFYIzz3eoLwlTUZKz2iQ4MrckBxJjkmD16ynUTrw= +github.com/containerd/stargz-snapshotter/estargz v0.18.2/go.mod h1:XyVU5tcJ3PRpkA9XS2T5us6Eg35yM0214Y+wvrZTBrY= +github.com/cowdogmoo/warpgate/v3 v3.2.1-0.20260407013053-5aed3c62eb90 h1:Ei9lwtqln9ftktbxx0b2Bj66DHCtmDaew320TpJIyoE= +github.com/cowdogmoo/warpgate/v3 v3.2.1-0.20260407013053-5aed3c62eb90/go.mod h1:a6SUrGZAU4RWqVttY0ZZkD8E8GFwUco4JlFv/HM9Vl0= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/docker/cli v29.3.1+incompatible h1:M04FDj2TRehDacrosh7Vlkgc7AuQoWloQkf1PA5hmoI= +github.com/docker/cli v29.3.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= +github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/docker-credential-helpers v0.9.5 h1:EFNN8DHvaiK8zVqFA2DT6BjXE0GzfLOZ38ggPTKePkY= +github.com/docker/docker-credential-helpers v0.9.5/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c= github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w= github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= -github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= -github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= +github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/go-containerregistry v0.21.3 h1:Xr+yt3VvwOOn/5nJzd7UoOhwPGiPkYW0zWDLLUXqAi4= +github.com/google/go-containerregistry v0.21.3/go.mod h1:D5ZrJF1e6dMzvInpBPuMCX0FxURz7GLq2rV3Us9aPkc= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= +github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -53,17 +75,23 @@ github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHP github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= -github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= +github.com/pelletier/go-toml/v2 v2.3.0 h1:k59bC/lIZREW0/iVaQR8nDHxVq8OVlIzYCOJf421CaM= +github.com/pelletier/go-toml/v2 v2.3.0/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc= -github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik= -github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw= -github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U= +github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4= +github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI= +github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= +github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= @@ -75,19 +103,29 @@ github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= +github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4= +github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/vbatts/tar-split v0.12.2 h1:w/Y6tjxpeiFMR47yzZPlPj/FcPLpXbTUi/9H7d3CPa4= +github.com/vbatts/tar-split v0.12.2/go.mod h1:eF6B6i6ftWQcDqEn3/iGFRFRo8cBIMSJVOpnNdfTMFA= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= -golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= +golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU= +golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A= +golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= +golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= +gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= diff --git a/cli/internal/terragrunt/runner.go b/cli/internal/terragrunt/runner.go index bee305c0..fb91bd8a 100644 --- a/cli/internal/terragrunt/runner.go +++ b/cli/internal/terragrunt/runner.go @@ -12,36 +12,24 @@ import ( "strings" ) -// Options configures a terragrunt execution. type Options struct { - // Action is the terraform action: init, plan, apply, destroy, output. - Action string - // WorkDir is the directory to run terragrunt in. - WorkDir string - // TerragruntBinary is the path to the terragrunt binary. + Action string + WorkDir string TerragruntBinary string - // TerraformBinary is the path to the terraform/tofu binary. - TerraformBinary string - // AutoApprove skips confirmation prompts (apply/destroy). - AutoApprove bool - // NonInteractive disables interactive prompts. - NonInteractive bool - // ExcludeDirs is a comma-separated list of dirs to exclude from run-all. - ExcludeDirs string - // LogFile is an optional path to write output to. - LogFile string - // Debug enables verbose output. - Debug bool + TerraformBinary string + AutoApprove bool + NonInteractive bool + ExcludeDirs string + LogFile string + Debug bool } -// Result holds the outcome of a terragrunt execution. type Result struct { Module string Success bool Error error } -// Run executes a single terragrunt command in the given working directory. func Run(ctx context.Context, opts Options) error { args := buildArgs(opts) @@ -70,9 +58,8 @@ func Run(ctx context.Context, opts Options) error { return nil } -// RunAll executes `terragrunt run-all ` across all modules in the working directory. func RunAll(ctx context.Context, opts Options) error { - args := []string{"run-all", opts.Action} + args := []string{"run", "--all", opts.Action} if opts.AutoApprove && (opts.Action == "apply" || opts.Action == "destroy") { args = append(args, "-auto-approve") } @@ -80,7 +67,7 @@ func RunAll(ctx context.Context, opts Options) error { args = append(args, "--non-interactive") } - slog.Info("running terragrunt run-all", + slog.Info("running terragrunt run --all", "action", opts.Action, "dir", opts.WorkDir, ) @@ -103,13 +90,11 @@ func RunAll(ctx context.Context, opts Options) error { cmd.Stderr = writer if err := cmd.Run(); err != nil { - return fmt.Errorf("terragrunt run-all %s failed: %w", opts.Action, err) + return fmt.Errorf("terragrunt run --all %s failed: %w", opts.Action, err) } return nil } -// RunIndividual iterates subdirectories of modulePath and runs terragrunt -// in each one individually. Returns results for each subdirectory. func RunIndividual(ctx context.Context, opts Options, modulePath string, exclude []string) ([]Result, error) { entries, err := os.ReadDir(modulePath) if err != nil { @@ -172,7 +157,6 @@ func RunIndividual(ctx context.Context, opts Options, modulePath string, exclude return results, nil } -// Output runs `terragrunt output -json` and returns the raw JSON bytes. func Output(ctx context.Context, opts Options) ([]byte, error) { args := []string{"output", "-json"} @@ -201,7 +185,7 @@ func buildArgs(opts Options) []string { func buildEnv(opts Options) []string { env := os.Environ() if opts.TerraformBinary != "" { - env = append(env, "TERRAGRUNT_TFPATH="+opts.TerraformBinary) + env = append(env, "TG_TF_PATH="+opts.TerraformBinary) } return env }