diff --git a/.ansible-lint b/.ansible-lint deleted file mode 100644 index 1fc8ac67..00000000 --- a/.ansible-lint +++ /dev/null @@ -1,6 +0,0 @@ ---- -exclude_paths: - - .terragrunt-cache - - infra/ - - packer/ - - cli/ diff --git a/.hooks/linters/ansible-lint.yaml b/.hooks/linters/ansible-lint.yaml index f4a2ab0e..39671957 100644 --- a/.hooks/linters/ansible-lint.yaml +++ b/.hooks/linters/ansible-lint.yaml @@ -5,6 +5,7 @@ exclude_paths: - .claude/ - .github/ + - infra/ # Files with unskippable load-failure (unicode/runtime) or syntax-check issues - ad/GOAD-Light/files/dc01/templates/ - ad/GOAD-Mini/files/dc01/templates/ diff --git a/cli/cmd/ami.go b/cli/cmd/ami.go index b9fc051b..ef2ab7bf 100644 --- a/cli/cmd/ami.go +++ b/cli/cmd/ami.go @@ -22,6 +22,8 @@ import ( "gopkg.in/yaml.v3" ) +var envVarPattern = regexp.MustCompile(`\$\{([^}]+)\}`) + var amiCmd = &cobra.Command{ Use: "ami", Short: "AMI image management", @@ -463,11 +465,12 @@ func loadWarpgateTemplate(path, projectRoot string) (*builder.Config, error) { } if _, ok := os.LookupEnv("PROVISION_REPO_PATH"); !ok && projectRoot != "" { - _ = os.Setenv("PROVISION_REPO_PATH", projectRoot) + if err := os.Setenv("PROVISION_REPO_PATH", projectRoot); err != nil { + return nil, fmt.Errorf("set PROVISION_REPO_PATH: %w", err) + } } - varPattern := regexp.MustCompile(`\$\{([^}]+)\}`) - content = varPattern.ReplaceAllStringFunc(content, func(match string) string { + content = envVarPattern.ReplaceAllStringFunc(content, func(match string) string { varName := match[2 : len(match)-1] if val, ok := os.LookupEnv(varName); ok { return val diff --git a/cli/cmd/env_cmd.go b/cli/cmd/env_cmd.go index 1fdff768..26c13aa1 100644 --- a/cli/cmd/env_cmd.go +++ b/cli/cmd/env_cmd.go @@ -6,6 +6,7 @@ import ( "io/fs" "os" "path/filepath" + "regexp" "strings" "github.com/dreadnode/dreadgoad/internal/config" @@ -31,6 +32,7 @@ Creates: - infra/goad-deployment/{env}/{region}/network/terragrunt.hcl - infra/goad-deployment/{env}/{region}/goad/{host}/terragrunt.hcl + templates - ad/GOAD/data/{env}-config.json + - {env}-inventory (Ansible inventory with PENDING instance IDs) Use --variant to generate randomized entity names for the environment config. Without --variant, the base config (dev-config.json) is copied as-is.`, @@ -145,15 +147,23 @@ func runEnvCreate(cmd *cobra.Command, args []string) error { color.Green(" Created config: %s-config.json", envName) } + invPath := filepath.Join(cfg.ProjectRoot, envName+"-inventory") + if err := generateInventory(cfg.ProjectRoot, envName, region, reference); err != nil { + return fmt.Errorf("generate inventory: %w", err) + } + color.Green(" Created inventory: %s", filepath.Base(invPath)) + 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) + fmt.Printf(" 3. Review: %s\n", invPath) + fmt.Printf(" 4. Initialize: dreadgoad --env %s --region %s infra init\n", envName, region) + fmt.Printf(" 5. Plan: dreadgoad --env %s --region %s infra plan\n", envName, region) + fmt.Printf(" 6. Apply: dreadgoad --env %s --region %s infra apply --auto-approve\n", envName, region) + fmt.Printf(" 7. Sync IDs: dreadgoad --env %s --region %s inventory sync\n", envName, region) return nil } @@ -315,6 +325,56 @@ func copyBaseConfig(projectRoot, envName string) error { return os.WriteFile(dstPath, data, 0o644) } +func generateInventory(projectRoot, envName, region, reference string) error { + refInvPath := filepath.Join(projectRoot, reference+"-inventory") + dstInvPath := filepath.Join(projectRoot, envName+"-inventory") + + data, err := os.ReadFile(refInvPath) + if err != nil { + return fmt.Errorf("read reference inventory %s: %w", filepath.Base(refInvPath), err) + } + content := string(data) + + // Extract reference env and region from the inventory vars + envRe := regexp.MustCompile(`(?m)^(\s*env=)(.+)$`) + regionRe := regexp.MustCompile(`(?m)^(\s*ansible_aws_ssm_region=)(.+)$`) + bucketRe := regexp.MustCompile(`(?m)^(\s*ansible_aws_ssm_bucket_name=)(.+)$`) + instanceRe := regexp.MustCompile(`(ansible_host=)i-[0-9a-f]+`) + ipFieldRe := regexp.MustCompile(`\s+(?:dc_ipv4|host_ipv4)=\S+`) + + refEnv := reference + if m := envRe.FindStringSubmatch(content); len(m) > 2 { + refEnv = strings.TrimSpace(m[2]) + } + refRegion := "" + if m := regionRe.FindStringSubmatch(content); len(m) > 2 { + refRegion = strings.TrimSpace(m[2]) + } + + // Replace env + content = envRe.ReplaceAllString(content, "${1}"+envName) + + // Replace region + content = regionRe.ReplaceAllString(content, "${1}"+region) + + // Replace bucket name: swap ref env/region for new env/region + if refRegion != "" { + if m := bucketRe.FindStringSubmatch(content); len(m) > 2 { + oldBucket := strings.TrimSpace(m[2]) + newBucket := strings.Replace(oldBucket, refEnv+"-"+refRegion, envName+"-"+region, 1) + content = bucketRe.ReplaceAllString(content, "${1}"+newBucket) + } + } + + // Replace instance IDs with PENDING placeholder + content = instanceRe.ReplaceAllString(content, "${1}PENDING") + + // Strip dc_ipv4/host_ipv4 fields (will be populated after infra apply) + content = ipFieldRe.ReplaceAllString(content, "") + + return os.WriteFile(dstInvPath, []byte(content), 0o644) +} + func generateVariantConfig(projectRoot, envName string) error { source := filepath.Join(projectRoot, "ad", "GOAD") target := filepath.Join(projectRoot, "ad", "GOAD-"+envName) diff --git a/cli/cmd/infra_cmd.go b/cli/cmd/infra_cmd.go index 2901d22c..0d73225b 100644 --- a/cli/cmd/infra_cmd.go +++ b/cli/cmd/infra_cmd.go @@ -16,11 +16,11 @@ import ( var infraCmd = &cobra.Command{ Use: "infra", - Short: "Manage GOAD infrastructure via Terragrunt", - Long: `Manage the GOAD lab infrastructure lifecycle using Terragrunt. + Short: "Manage DreadGOAD infrastructure via Terragrunt", + Long: `Manage the DreadGOAD lab infrastructure lifecycle using Terragrunt. Operates on the infra/ directory which contains Terragrunt configurations -for deploying the GOAD lab (VPC, EC2 instances, security groups, etc.). +for deploying the DreadGOAD lab (VPC, EC2 instances, security groups, etc.). By default, commands operate on all modules (run-all). Use --module to target a specific module (e.g. network, goad/dc01).`, diff --git a/cli/cmd/lab.go b/cli/cmd/lab.go index 4252f938..f7b64a60 100644 --- a/cli/cmd/lab.go +++ b/cli/cmd/lab.go @@ -12,7 +12,7 @@ import ( var labCmd = &cobra.Command{ Use: "lab", - Short: "Manage GOAD lab lifecycle", + Short: "Manage DreadGOAD lab lifecycle", } var labStatusCmd = &cobra.Command{ diff --git a/cli/cmd/lab_list.go b/cli/cmd/lab_list.go index 1342aa90..30b928be 100644 --- a/cli/cmd/lab_list.go +++ b/cli/cmd/lab_list.go @@ -12,7 +12,7 @@ import ( var labListCmd = &cobra.Command{ Use: "list", - Short: "List available GOAD labs and their providers", + Short: "List available DreadGOAD labs and their providers", RunE: runLabList, } diff --git a/cli/internal/ansible/logparser.go b/cli/internal/ansible/logparser.go index 4e4b433f..b4e7a1a2 100644 --- a/cli/internal/ansible/logparser.go +++ b/cli/internal/ansible/logparser.go @@ -26,8 +26,8 @@ func CheckAnsibleSuccess(output string) bool { lines := strings.Split(output, "\n") for i, line := range lines { if strings.HasPrefix(line, "fatal:") { - // Check next 10 lines for "...ignoring" - end := i + 11 + // Check next 20 lines for "...ignoring" (multi-line YAML output can be long) + end := i + 21 if end > len(lines) { end = len(lines) } diff --git a/cli/internal/config/defaults.go b/cli/internal/config/defaults.go index 587a4bda..a0cf34d5 100644 --- a/cli/internal/config/defaults.go +++ b/cli/internal/config/defaults.go @@ -46,7 +46,7 @@ func setDefaults() { viper.SetDefault("extensions.elk.impact", "add a linux machine and add a logbeat agent on all windows machines") viper.SetDefault("extensions.elk.playbook", "ext-elk.yml") - viper.SetDefault("extensions.exchange.description", "Add an Exchange server to the GOAD lab") + viper.SetDefault("extensions.exchange.description", "Add an Exchange server to the DreadGOAD lab") viper.SetDefault("extensions.exchange.machines", []string{"srv01"}) viper.SetDefault("extensions.exchange.compatibility", []string{"GOAD", "GOAD-Light", "GOAD-Mini"}) viper.SetDefault("extensions.exchange.impact", "modifies AD schema and adds a server (heavy)") diff --git a/cli/internal/lab/discovery.go b/cli/internal/lab/discovery.go index f0273f10..86ba7198 100644 --- a/cli/internal/lab/discovery.go +++ b/cli/internal/lab/discovery.go @@ -11,7 +11,7 @@ import ( "go.yaml.in/yaml/v3" ) -// Lab represents a discovered GOAD lab definition. +// Lab represents a discovered DreadGOAD lab definition. type Lab struct { Name string `json:"name"` Path string `json:"path"` diff --git a/infra/goad-deployment/staging/us-west-1/goad/dc01/terragrunt.hcl b/infra/goad-deployment/staging/us-west-1/goad/dc01/terragrunt.hcl index 0441a2d8..bc33657d 100644 --- a/infra/goad-deployment/staging/us-west-1/goad/dc01/terragrunt.hcl +++ b/infra/goad-deployment/staging/us-west-1/goad/dc01/terragrunt.hcl @@ -70,8 +70,8 @@ inputs = { additional_windows_ami_filters = [ { - name = "name" - values = ["goad-dc-base-*"] # warpgate-templates/goad-dc-base + name = "tag:Name" + values = ["goad-dc-base"] } ] diff --git a/infra/goad-deployment/staging/us-west-1/goad/dc02/terragrunt.hcl b/infra/goad-deployment/staging/us-west-1/goad/dc02/terragrunt.hcl index 5c073bbe..ddb7da88 100644 --- a/infra/goad-deployment/staging/us-west-1/goad/dc02/terragrunt.hcl +++ b/infra/goad-deployment/staging/us-west-1/goad/dc02/terragrunt.hcl @@ -69,8 +69,8 @@ inputs = { additional_windows_ami_filters = [ { - name = "name" - values = ["goad-dc-base-*"] # warpgate-templates/goad-dc-base + name = "tag:Name" + values = ["goad-dc-base"] } ] diff --git a/infra/goad-deployment/staging/us-west-1/goad/dc03/terragrunt.hcl b/infra/goad-deployment/staging/us-west-1/goad/dc03/terragrunt.hcl index 668f058a..d79f443e 100644 --- a/infra/goad-deployment/staging/us-west-1/goad/dc03/terragrunt.hcl +++ b/infra/goad-deployment/staging/us-west-1/goad/dc03/terragrunt.hcl @@ -69,8 +69,8 @@ inputs = { additional_windows_ami_filters = [ { - name = "name" - values = ["goad-dc-base-2016-*"] # warpgate-templates/goad-dc-base-2016 + name = "tag:Name" + values = ["goad-dc-base-2016"] } ] diff --git a/infra/goad-deployment/staging/us-west-1/goad/srv02/terragrunt.hcl b/infra/goad-deployment/staging/us-west-1/goad/srv02/terragrunt.hcl index 11385662..2909b3f7 100644 --- a/infra/goad-deployment/staging/us-west-1/goad/srv02/terragrunt.hcl +++ b/infra/goad-deployment/staging/us-west-1/goad/srv02/terragrunt.hcl @@ -69,8 +69,8 @@ inputs = { additional_windows_ami_filters = [ { - name = "name" - values = ["goad-mssql-base-*"] # warpgate-templates/goad-mssql-base + name = "tag:Name" + values = ["goad-mssql-base"] } ] diff --git a/infra/goad-deployment/staging/us-west-1/goad/srv03/terragrunt.hcl b/infra/goad-deployment/staging/us-west-1/goad/srv03/terragrunt.hcl index 18a6421a..a29025e1 100644 --- a/infra/goad-deployment/staging/us-west-1/goad/srv03/terragrunt.hcl +++ b/infra/goad-deployment/staging/us-west-1/goad/srv03/terragrunt.hcl @@ -69,8 +69,8 @@ inputs = { additional_windows_ami_filters = [ { - name = "name" - values = ["goad-member-base-2016-*"] # warpgate-templates/goad-member-base-2016 + name = "tag:Name" + values = ["goad-member-base-2016"] } ] diff --git a/infra/goad-deployment/test/us-east-2/goad/dc01/terragrunt.hcl b/infra/goad-deployment/test/us-east-2/goad/dc01/terragrunt.hcl index 0441a2d8..bc33657d 100644 --- a/infra/goad-deployment/test/us-east-2/goad/dc01/terragrunt.hcl +++ b/infra/goad-deployment/test/us-east-2/goad/dc01/terragrunt.hcl @@ -70,8 +70,8 @@ inputs = { additional_windows_ami_filters = [ { - name = "name" - values = ["goad-dc-base-*"] # warpgate-templates/goad-dc-base + name = "tag:Name" + values = ["goad-dc-base"] } ] diff --git a/infra/goad-deployment/test/us-east-2/goad/dc02/terragrunt.hcl b/infra/goad-deployment/test/us-east-2/goad/dc02/terragrunt.hcl index 5c073bbe..ddb7da88 100644 --- a/infra/goad-deployment/test/us-east-2/goad/dc02/terragrunt.hcl +++ b/infra/goad-deployment/test/us-east-2/goad/dc02/terragrunt.hcl @@ -69,8 +69,8 @@ inputs = { additional_windows_ami_filters = [ { - name = "name" - values = ["goad-dc-base-*"] # warpgate-templates/goad-dc-base + name = "tag:Name" + values = ["goad-dc-base"] } ] diff --git a/infra/goad-deployment/test/us-east-2/goad/dc03/terragrunt.hcl b/infra/goad-deployment/test/us-east-2/goad/dc03/terragrunt.hcl index 668f058a..d79f443e 100644 --- a/infra/goad-deployment/test/us-east-2/goad/dc03/terragrunt.hcl +++ b/infra/goad-deployment/test/us-east-2/goad/dc03/terragrunt.hcl @@ -69,8 +69,8 @@ inputs = { additional_windows_ami_filters = [ { - name = "name" - values = ["goad-dc-base-2016-*"] # warpgate-templates/goad-dc-base-2016 + name = "tag:Name" + values = ["goad-dc-base-2016"] } ] diff --git a/infra/goad-deployment/test/us-east-2/goad/srv02/terragrunt.hcl b/infra/goad-deployment/test/us-east-2/goad/srv02/terragrunt.hcl index 11385662..2909b3f7 100644 --- a/infra/goad-deployment/test/us-east-2/goad/srv02/terragrunt.hcl +++ b/infra/goad-deployment/test/us-east-2/goad/srv02/terragrunt.hcl @@ -69,8 +69,8 @@ inputs = { additional_windows_ami_filters = [ { - name = "name" - values = ["goad-mssql-base-*"] # warpgate-templates/goad-mssql-base + name = "tag:Name" + values = ["goad-mssql-base"] } ] diff --git a/infra/goad-deployment/test/us-east-2/goad/srv03/terragrunt.hcl b/infra/goad-deployment/test/us-east-2/goad/srv03/terragrunt.hcl index 18a6421a..a29025e1 100644 --- a/infra/goad-deployment/test/us-east-2/goad/srv03/terragrunt.hcl +++ b/infra/goad-deployment/test/us-east-2/goad/srv03/terragrunt.hcl @@ -69,8 +69,8 @@ inputs = { additional_windows_ami_filters = [ { - name = "name" - values = ["goad-member-base-2016-*"] # warpgate-templates/goad-member-base-2016 + name = "tag:Name" + values = ["goad-member-base-2016"] } ] diff --git a/modules/terraform-aws-instance-factory/data.tf b/modules/terraform-aws-instance-factory/data.tf index d30d726a..2abe3e71 100644 --- a/modules/terraform-aws-instance-factory/data.tf +++ b/modules/terraform-aws-instance-factory/data.tf @@ -28,9 +28,9 @@ data "aws_ami" "linux" { } } - # Only apply default name filter if no name filter exists in additional_linux_ami_filters + # Only apply default name filter if no name/tag:Name/image-id filter exists in additional_linux_ami_filters dynamic "filter" { - for_each = length([for f in var.additional_linux_ami_filters : f if f.name == "name" || f.name == "image-id"]) > 0 ? [] : [1] + for_each = length([for f in var.additional_linux_ami_filters : f if f.name == "name" || f.name == "image-id" || f.name == "tag:Name"]) > 0 ? [] : [1] content { name = "name" values = ["${var.linux_os}*${var.linux_os_version}*"] @@ -77,9 +77,9 @@ data "aws_ami" "windows" { } } - # Only apply default name filter if no name filter exists in additional_windows_ami_filters + # Only apply default name filter if no name/tag:Name/image-id filter exists in additional_windows_ami_filters dynamic "filter" { - for_each = length([for f in var.additional_windows_ami_filters : f if f.name == "name" || f.name == "image-id"]) > 0 ? [] : [1] + for_each = length([for f in var.additional_windows_ami_filters : f if f.name == "name" || f.name == "image-id" || f.name == "tag:Name"]) > 0 ? [] : [1] content { name = "name" values = ["${var.windows_os}-${var.windows_os_version}*"] @@ -128,7 +128,7 @@ data "aws_ami" "macos" { # Only apply default name filter if no name filter exists in additional_macos_ami_filters dynamic "filter" { - for_each = length([for f in var.additional_macos_ami_filters : f if f.name == "name" || f.name == "image-id"]) > 0 ? [] : [1] + for_each = length([for f in var.additional_macos_ami_filters : f if f.name == "name" || f.name == "image-id" || f.name == "tag:Name"]) > 0 ? [] : [1] content { name = "name" values = ["${var.macos_os}*${var.macos_os_version}*"]