From ab7873e9b56bf0fa70088c2470a5b8ba9f8935d2 Mon Sep 17 00:00:00 2001 From: Russel Van Tuyl Date: Wed, 1 Jul 2026 17:51:21 -0500 Subject: [PATCH 1/4] feat(variant): enhance name component replacement logic and add tests --- cli/cmd/provision.go | 24 +++-- cli/internal/variant/generator.go | 89 +++++++++++++++++-- .../variant/replace_name_component_test.go | 37 ++++++++ 3 files changed, 138 insertions(+), 12 deletions(-) create mode 100644 cli/internal/variant/replace_name_component_test.go diff --git a/cli/cmd/provision.go b/cli/cmd/provision.go index fae9aa71..c99c561d 100644 --- a/cli/cmd/provision.go +++ b/cli/cmd/provision.go @@ -191,12 +191,26 @@ func bootstrapInventory(invPath string) error { } func bootstrapFromProviderTemplate(invPath string, cfg *config.Config) error { - labName := "GOAD" - if cfg.ResolvedProvider() == "proxmox" { - labName = cfg.ProxmoxLab() - } providerName := cfg.ResolvedProvider() - templatePath := filepath.Join(cfg.ProjectRoot, "ad", labName, "providers", providerName, "inventory") + + // Resolve the lab tree that holds the provider inventory template. For a + // variant environment, read from the variant target tree so the + // bootstrapped inventory (which carries domain_name and the asset layout) + // points at the variant's ad// assets rather than the stock + // ad/GOAD/ tree. Falls back to the stock/proxmox path for non-variants. + var templatePath string + if ec := cfg.ActiveEnvironment(); ec.Variant { + if _, target := cfg.ResolvedVariantPaths(); target != "" { + templatePath = filepath.Join(target, "providers", providerName, "inventory") + } + } + if templatePath == "" { + labName := "GOAD" + if providerName == "proxmox" { + labName = cfg.ProxmoxLab() + } + templatePath = filepath.Join(cfg.ProjectRoot, "ad", labName, "providers", providerName, "inventory") + } data, err := os.ReadFile(templatePath) if err != nil { diff --git a/cli/internal/variant/generator.go b/cli/internal/variant/generator.go index 629ccd5d..08531f6e 100644 --- a/cli/internal/variant/generator.go +++ b/cli/internal/variant/generator.go @@ -715,11 +715,7 @@ func (g *Generator) applyReplacements(content string) string { } if g.isNameComponent(r.Old) { - pattern := `\b` + regexp.QuoteMeta(r.Old) + `\b` - re, err := regexp.Compile(pattern) - if err == nil { - content = re.ReplaceAllString(content, r.New) - } + content = replaceNameComponent(content, r.Old, r.New) } else { content = strings.ReplaceAll(content, r.Old, r.New) } @@ -727,6 +723,56 @@ func (g *Generator) applyReplacements(content string) string { return content } +// replaceNameComponent replaces every occurrence of a name-component token +// (a firstname/surname fragment) with its replacement, honoring both normal +// word boundaries AND CamelCase boundaries. Go's regexp (RE2) has no +// lookahead, so we match a leading word boundary plus the token, then accept a +// match only when the character immediately after it is: +// +// (a) absent (end of string), +// (b) a non-word character (normal word boundary), or +// (c) an uppercase ASCII letter (CamelCase boundary, e.g. "StarkWallpaper"). +// +// A trailing lowercase letter, digit, or underscore is rejected so that +// "starky"/"starkey" are left intact. +func replaceNameComponent(content, old, replacement string) string { + re, err := regexp.Compile(`\b` + regexp.QuoteMeta(old)) + if err != nil { + return content + } + + var b strings.Builder + last := 0 + for _, loc := range re.FindAllStringIndex(content, -1) { + start, end := loc[0], loc[1] + + accept := true + if end < len(content) { + c := content[end] + isWord := c == '_' || + ('0' <= c && c <= '9') || + ('a' <= c && c <= 'z') || + ('A' <= c && c <= 'Z') + isUpper := 'A' <= c && c <= 'Z' + // Reject only when the following char is a word char that is NOT + // an uppercase letter (i.e. lowercase letter, digit, or underscore). + if isWord && !isUpper { + accept = false + } + } + + b.WriteString(content[last:start]) + if accept { + b.WriteString(replacement) + } else { + b.WriteString(content[start:end]) + } + last = end + } + b.WriteString(content[last:]) + return b.String() +} + // isNameComponent returns true if old is a firstname/surname component needing word-boundary protection. func (g *Generator) isNameComponent(old string) bool { if _, ok := g.mappings.Misc[old]; !ok { @@ -845,14 +891,23 @@ var textFilenames = map[string]bool{ func (g *Generator) transformFile(srcPath, relPath string) (transformed bool) { ext := filepath.Ext(srcPath) base := filepath.Base(srcPath) - targetFile := filepath.Join(g.TargetPath, relPath) + + // Rename the output basename so a file named after an identity + // (e.g. files/srv02/all/arya.txt) is written under the same rewritten + // name that config.json now references (kathleen.txt). Only the final + // path element is transformed; the directory portion of relPath is left + // untouched. Uses the same replacement machinery as file content. + relDir := filepath.Dir(relPath) + newBase := g.applyReplacements(base) + targetFile := filepath.Join(g.TargetPath, relDir, newBase) if err := os.MkdirAll(filepath.Dir(targetFile), 0o755); err != nil { fmt.Printf("Warning: mkdir failed for %s: %v\n", relPath, err) return false } - if textExtensions[ext] || textFilenames[base] { + isInventory := strings.HasPrefix(base, "inventory") + if textExtensions[ext] || textFilenames[base] || isInventory { content, err := os.ReadFile(srcPath) if err != nil { fmt.Printf("Warning: Could not read %s: %v\n", relPath, err) @@ -876,6 +931,10 @@ func (g *Generator) transformFile(srcPath, relPath string) (transformed bool) { } } + if isInventory { + newContent = g.repointDomainName(newContent) + } + if err := os.WriteFile(targetFile, []byte(newContent), 0o644); err != nil { fmt.Printf("Warning: Could not write %s: %v\n", relPath, err) return false @@ -887,6 +946,22 @@ func (g *Generator) transformFile(srcPath, relPath string) (transformed bool) { return false } +// repointDomainName rewrites the Ansible `domain_name` inventory variable to +// the variant target folder basename. Playbooks resolve vuln scripts/files as +// ad/{{ domain_name }}/..., so for a variant this must point at the variant's +// own ad// tree rather than the stock ad/GOAD/ tree. Only the value is +// changed; the key, indentation, and surrounding lines are preserved. +func (g *Generator) repointDomainName(content string) string { + target := filepath.Base(g.TargetPath) + re := regexp.MustCompile(`(?m)^(\s*domain_name\s*=).*$`) + return re.ReplaceAllStringFunc(content, func(line string) string { + if idx := strings.Index(line, "="); idx >= 0 { + return line[:idx+1] + target + } + return line + }) +} + // copyAndTransform copies the source directory, transforming text files. func (g *Generator) copyAndTransform() error { fmt.Println("\n=== Copying and Transforming Files ===") diff --git a/cli/internal/variant/replace_name_component_test.go b/cli/internal/variant/replace_name_component_test.go new file mode 100644 index 00000000..b1ca8a20 --- /dev/null +++ b/cli/internal/variant/replace_name_component_test.go @@ -0,0 +1,37 @@ +package variant + +import "testing" + +// TestReplaceNameComponent locks in the CamelCase-aware boundary matching that +// closes the compound-token leak (e.g. the GPO name "StarkWallpaper"). A name +// component is replaced when it is preceded by a word boundary and followed by +// end-of-string, a non-word character, or an UPPERCASE letter (CamelCase); it is +// left intact when followed by a lowercase letter, digit, or underscore. +func TestReplaceNameComponent(t *testing.T) { + cases := []struct { + name string + in string + old string + repl string + want string + }{ + {"camelCase boundary (the StarkWallpaper bug)", `New-GPO -Name "StarkWallpaper"`, "Stark", "Research", `New-GPO -Name "ResearchWallpaper"`}, + {"trailing backslash is a boundary", `north\Stark`, "Stark", "Research", `north\Research`}, + {"trailing dot is a boundary", "Stark.txt", "Stark", "Research", "Research.txt"}, + {"end of string", "Stark", "Stark", "Research", "Research"}, + {"standalone word", "the Stark house", "Stark", "Research", "the Research house"}, + {"multiple occurrences, mixed boundaries", "Stark and StarkWallpaper", "Stark", "Research", "Research and ResearchWallpaper"}, + {"lowercase continuation is NOT replaced", "Starkey", "Stark", "Research", "Starkey"}, + {"digit continuation is NOT replaced", "Stark1", "Stark", "Research", "Stark1"}, + {"underscore continuation is NOT replaced", "Stark_svc", "Stark", "Research", "Stark_svc"}, + {"no left boundary is NOT replaced", "aStark", "Stark", "Research", "aStark"}, + {"no occurrence is unchanged", "nothing here", "Stark", "Research", "nothing here"}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + if got := replaceNameComponent(tc.in, tc.old, tc.repl); got != tc.want { + t.Errorf("replaceNameComponent(%q, %q, %q) = %q, want %q", tc.in, tc.old, tc.repl, got, tc.want) + } + }) + } +} From 67607d3387106c65100e22eca48f53a829c48227 Mon Sep 17 00:00:00 2001 From: Russel Van Tuyl Date: Thu, 2 Jul 2026 16:59:36 -0500 Subject: [PATCH 2/4] fix(variant): preserve .gitkeep files while skipping git metadata --- cli/internal/variant/generator.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/cli/internal/variant/generator.go b/cli/internal/variant/generator.go index 08531f6e..1c573a1e 100644 --- a/cli/internal/variant/generator.go +++ b/cli/internal/variant/generator.go @@ -983,9 +983,12 @@ func (g *Generator) copyAndTransform() error { return nil } - // Skip .git files + // Skip git metadata (the .git directory itself is skipped above), but KEEP + // .gitkeep placeholders: they preserve otherwise-empty directories the lab + // relies on (e.g. files/srv02/wwwroot/upload/), which would silently vanish + // from the variant if dropped. rel, _ := filepath.Rel(g.SourcePath, path) - if strings.Contains(rel, ".git") { + if strings.Contains(rel, ".git") && d.Name() != ".gitkeep" { return nil } From f3c4f493d24519be1558afcb152a9a870ab1ece2 Mon Sep 17 00:00:00 2001 From: mkultraWasHere Date: Wed, 12 Aug 2026 11:47:25 -0400 Subject: [PATCH 3/4] fix(variant): add missing inventory domain names --- ad/GOAD-variant-1/data/inventory | 2 +- .../data/inventory_disable_vagrant | 2 +- ad/GOAD-variant-1/providers/aws/inventory | 1 + ad/GOAD-variant-1/providers/azure/inventory | 1 + ad/GOAD-variant-1/providers/ludus/inventory | 1 + ad/GOAD-variant-1/providers/proxmox/inventory | 1 + .../providers/virtualbox/inventory | 3 + ad/GOAD-variant-1/providers/vmware/inventory | 3 + .../providers/vmware_esxi/inventory | 3 + cli/cmd/bootstrap_test.go | 4 +- cli/internal/variant/generator.go | 54 ++++++++++++--- cli/internal/variant/generator_test.go | 66 +++++++++++++------ 12 files changed, 106 insertions(+), 35 deletions(-) diff --git a/ad/GOAD-variant-1/data/inventory b/ad/GOAD-variant-1/data/inventory index dcbe0a83..e6a7fc57 100644 --- a/ad/GOAD-variant-1/data/inventory +++ b/ad/GOAD-variant-1/data/inventory @@ -1,7 +1,7 @@ ; GLOBAL CONFIG [all:vars] ; domain_name : folder inside ad/ -domain_name=GOAD +domain_name=GOAD-variant-1 ; administrator user admin_user=administrator diff --git a/ad/GOAD-variant-1/data/inventory_disable_vagrant b/ad/GOAD-variant-1/data/inventory_disable_vagrant index fe857f3e..2ef09553 100644 --- a/ad/GOAD-variant-1/data/inventory_disable_vagrant +++ b/ad/GOAD-variant-1/data/inventory_disable_vagrant @@ -18,7 +18,7 @@ srv03 ansible_host={{ip_range}}.23 dns_domain=dc03 dict_key=srv03 ansible_user=a [all:vars] ; domain_name : folder inside ad/ -domain_name=GOAD +domain_name=GOAD-variant-1 ; winrm connection (windows) ansible_winrm_transport=ntlm diff --git a/ad/GOAD-variant-1/providers/aws/inventory b/ad/GOAD-variant-1/providers/aws/inventory index b3488171..a3c14a1a 100644 --- a/ad/GOAD-variant-1/providers/aws/inventory +++ b/ad/GOAD-variant-1/providers/aws/inventory @@ -17,4 +17,5 @@ dc03 ansible_host={{ip_range}}.12 dns_domain=dc03 dict_key=dc03 ansible_user=ans srv03 ansible_host={{ip_range}}.23 dns_domain=dc03 dict_key=srv03 ansible_user=ansible ansible_password=978i2pF43UJ- [all:vars] +domain_name=GOAD-variant-1 admin_user=goadmin diff --git a/ad/GOAD-variant-1/providers/azure/inventory b/ad/GOAD-variant-1/providers/azure/inventory index b3488171..a3c14a1a 100644 --- a/ad/GOAD-variant-1/providers/azure/inventory +++ b/ad/GOAD-variant-1/providers/azure/inventory @@ -17,4 +17,5 @@ dc03 ansible_host={{ip_range}}.12 dns_domain=dc03 dict_key=dc03 ansible_user=ans srv03 ansible_host={{ip_range}}.23 dns_domain=dc03 dict_key=srv03 ansible_user=ansible ansible_password=978i2pF43UJ- [all:vars] +domain_name=GOAD-variant-1 admin_user=goadmin diff --git a/ad/GOAD-variant-1/providers/ludus/inventory b/ad/GOAD-variant-1/providers/ludus/inventory index f7a05213..b4d232f7 100644 --- a/ad/GOAD-variant-1/providers/ludus/inventory +++ b/ad/GOAD-variant-1/providers/ludus/inventory @@ -17,6 +17,7 @@ dc03 ansible_host={{ip_range}}.12 dns_domain=dc03 dict_key=dc03 srv03 ansible_host={{ip_range}}.23 dns_domain=dc03 dict_key=srv03 [all:vars] +domain_name=GOAD-variant-1 force_dns_server=no dns_server={{ip_range}}.254 diff --git a/ad/GOAD-variant-1/providers/proxmox/inventory b/ad/GOAD-variant-1/providers/proxmox/inventory index ac06df41..7113be37 100644 --- a/ad/GOAD-variant-1/providers/proxmox/inventory +++ b/ad/GOAD-variant-1/providers/proxmox/inventory @@ -17,5 +17,6 @@ dc03 ansible_host={{ip_range}}.12 dns_domain=dc03 dict_key=dc03 srv03 ansible_host={{ip_range}}.23 dns_domain=dc03 dict_key=srv03 [all:vars] +domain_name=GOAD-variant-1 force_dns_server=yes dns_server={{ip_range}}.1 diff --git a/ad/GOAD-variant-1/providers/virtualbox/inventory b/ad/GOAD-variant-1/providers/virtualbox/inventory index 50cbae91..ae4c5910 100644 --- a/ad/GOAD-variant-1/providers/virtualbox/inventory +++ b/ad/GOAD-variant-1/providers/virtualbox/inventory @@ -15,3 +15,6 @@ srv02 ansible_host={{ip_range}}.22 dns_domain=dc02 dict_key=srv02 ; ------------------------------------------------ dc03 ansible_host={{ip_range}}.12 dns_domain=dc03 dict_key=dc03 srv03 ansible_host={{ip_range}}.23 dns_domain=dc03 dict_key=srv03 + +[all:vars] +domain_name=GOAD-variant-1 diff --git a/ad/GOAD-variant-1/providers/vmware/inventory b/ad/GOAD-variant-1/providers/vmware/inventory index 50cbae91..ae4c5910 100644 --- a/ad/GOAD-variant-1/providers/vmware/inventory +++ b/ad/GOAD-variant-1/providers/vmware/inventory @@ -15,3 +15,6 @@ srv02 ansible_host={{ip_range}}.22 dns_domain=dc02 dict_key=srv02 ; ------------------------------------------------ dc03 ansible_host={{ip_range}}.12 dns_domain=dc03 dict_key=dc03 srv03 ansible_host={{ip_range}}.23 dns_domain=dc03 dict_key=srv03 + +[all:vars] +domain_name=GOAD-variant-1 diff --git a/ad/GOAD-variant-1/providers/vmware_esxi/inventory b/ad/GOAD-variant-1/providers/vmware_esxi/inventory index 50cbae91..ae4c5910 100644 --- a/ad/GOAD-variant-1/providers/vmware_esxi/inventory +++ b/ad/GOAD-variant-1/providers/vmware_esxi/inventory @@ -15,3 +15,6 @@ srv02 ansible_host={{ip_range}}.22 dns_domain=dc02 dict_key=srv02 ; ------------------------------------------------ dc03 ansible_host={{ip_range}}.12 dns_domain=dc03 dict_key=dc03 srv03 ansible_host={{ip_range}}.23 dns_domain=dc03 dict_key=srv03 + +[all:vars] +domain_name=GOAD-variant-1 diff --git a/cli/cmd/bootstrap_test.go b/cli/cmd/bootstrap_test.go index 1d498f33..8e91a80f 100644 --- a/cli/cmd/bootstrap_test.go +++ b/cli/cmd/bootstrap_test.go @@ -158,7 +158,7 @@ func TestBootstrapFromProviderTemplateUsesDefaultVariantTarget(t *testing.T) { if err := os.MkdirAll(filepath.Dir(templatePath), 0o755); err != nil { t.Fatal(err) } - template := "[default]\ndc01 ansible_host={{ip_range}}.10\n" + template := "[default]\ndc01 ansible_host={{ip_range}}.10\n\n[all:vars]\ndomain_name=GOAD-variant-1\n" if err := os.WriteFile(templatePath, []byte(template), 0o644); err != nil { t.Fatal(err) } @@ -181,7 +181,7 @@ func TestBootstrapFromProviderTemplateUsesDefaultVariantTarget(t *testing.T) { if err != nil { t.Fatal(err) } - want := "[default]\ndc01 ansible_host=10.20.30.10\n" + want := "[default]\ndc01 ansible_host=10.20.30.10\n\n[all:vars]\ndomain_name=GOAD-variant-1\n" if string(got) != want { t.Fatalf("bootstrapped inventory = %q, want %q", got, want) } diff --git a/cli/internal/variant/generator.go b/cli/internal/variant/generator.go index 426c968d..457b5c53 100644 --- a/cli/internal/variant/generator.go +++ b/cli/internal/variant/generator.go @@ -1058,21 +1058,55 @@ func (g *Generator) transformFile(srcPath, relPath string) (transformed bool, er // repointDomainName rewrites the Ansible `domain_name` inventory variable to // the variant target folder basename. Playbooks resolve vuln scripts/files as // ad/{{ domain_name }}/..., so for a variant this must point at the variant's -// own ad// tree rather than the stock ad/GOAD/ tree. Only the value is -// changed; the key, indentation, and surrounding lines are preserved. +// own ad// tree rather than the stock ad/GOAD/ tree. Existing values +// are replaced in place; missing values are inserted under [all:vars], with +// the section appended when the provider template does not define it. func (g *Generator) repointDomainName(content string) string { target := filepath.Base(g.TargetPath) re := regexp.MustCompile(`(?m)^(\s*domain_name\s*=\s*).*$`) - return re.ReplaceAllStringFunc(content, func(line string) string { - if idx := strings.Index(line, "="); idx >= 0 { - valueStart := idx + 1 - for valueStart < len(line) && (line[valueStart] == ' ' || line[valueStart] == '\t') { - valueStart++ + if re.MatchString(content) { + return re.ReplaceAllStringFunc(content, func(line string) string { + if idx := strings.Index(line, "="); idx >= 0 { + valueStart := idx + 1 + for valueStart < len(line) && (line[valueStart] == ' ' || line[valueStart] == '\t') { + valueStart++ + } + return line[:valueStart] + target + } + return line + }) + } + + header := regexp.MustCompile(`(?m)^\[all:vars\][ \t]*`) + loc := header.FindStringIndex(content) + lineEnding := "\n" + if strings.Contains(content, "\r\n") { + lineEnding = "\r\n" + } + if loc == nil { + separator := "" + if content != "" { + separator = lineEnding + lineEnding + if strings.HasSuffix(content, lineEnding) { + separator = lineEnding + } + if strings.HasSuffix(content, lineEnding+lineEnding) { + separator = "" } - return line[:valueStart] + target } - return line - }) + return content + separator + "[all:vars]" + lineEnding + "domain_name=" + target + lineEnding + } + + insertAt := loc[1] + if strings.HasPrefix(content[insertAt:], "\r\n") { + insertAt += 2 + } else if strings.HasPrefix(content[insertAt:], "\n") { + insertAt++ + } else { + return content[:insertAt] + lineEnding + "domain_name=" + target + content[insertAt:] + } + + return content[:insertAt] + "domain_name=" + target + lineEnding + content[insertAt:] } // copyAndTransform copies the source directory, transforming text files. diff --git a/cli/internal/variant/generator_test.go b/cli/internal/variant/generator_test.go index 428711e6..24150bf0 100644 --- a/cli/internal/variant/generator_test.go +++ b/cli/internal/variant/generator_test.go @@ -76,30 +76,54 @@ func setupTestSource(t *testing.T) (sourceDir, targetDir string) { } func TestTransformFileRepointsInventoryDomainName(t *testing.T) { - sourceDir := t.TempDir() - targetDir := filepath.Join(t.TempDir(), "review-variant") - sourcePath := filepath.Join(sourceDir, "inventory_disable_vagrant") - content := "[all:vars]\n domain_name = GOAD\nadmin_user=administrator\n" - if err := os.WriteFile(sourcePath, []byte(content), 0o644); err != nil { - t.Fatal(err) + tests := []struct { + name string + content string + want string + }{ + { + name: "replaces existing value", + content: "[all:vars]\n domain_name = GOAD\nadmin_user=administrator\n", + want: "[all:vars]\n domain_name = review-variant\nadmin_user=administrator\n", + }, + { + name: "inserts missing value", + content: "[all:vars]\nadmin_user=administrator\n", + want: "[all:vars]\ndomain_name=review-variant\nadmin_user=administrator\n", + }, + { + name: "adds missing all vars section", + content: "[default]\ndc01 ansible_host=10.0.0.10\n", + want: "[default]\ndc01 ansible_host=10.0.0.10\n\n[all:vars]\ndomain_name=review-variant\n", + }, } - gen := NewGenerator(sourceDir, targetDir, "test") - transformed, err := gen.transformFile(sourcePath, filepath.Join("data", "inventory_disable_vagrant")) - if err != nil { - t.Fatalf("transformFile() error: %v", err) - } - if !transformed { - t.Fatal("transformFile() reported inventory was copied without transformation") - } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + sourceDir := t.TempDir() + targetDir := filepath.Join(t.TempDir(), "review-variant") + sourcePath := filepath.Join(sourceDir, "inventory_disable_vagrant") + if err := os.WriteFile(sourcePath, []byte(tt.content), 0o644); err != nil { + t.Fatal(err) + } - got, err := os.ReadFile(filepath.Join(targetDir, "data", "inventory_disable_vagrant")) - if err != nil { - t.Fatal(err) - } - want := "[all:vars]\n domain_name = review-variant\nadmin_user=administrator\n" - if string(got) != want { - t.Fatalf("transformed inventory = %q, want %q", got, want) + gen := NewGenerator(sourceDir, targetDir, "test") + transformed, err := gen.transformFile(sourcePath, filepath.Join("data", "inventory_disable_vagrant")) + if err != nil { + t.Fatalf("transformFile() error: %v", err) + } + if !transformed { + t.Fatal("transformFile() reported inventory was copied without transformation") + } + + got, err := os.ReadFile(filepath.Join(targetDir, "data", "inventory_disable_vagrant")) + if err != nil { + t.Fatal(err) + } + if string(got) != tt.want { + t.Fatalf("transformed inventory = %q, want %q", got, tt.want) + } + }) } } From 8b6cb3437e97e475e6a6c1d3aa77e75e731acab7 Mon Sep 17 00:00:00 2001 From: mkultraWasHere Date: Wed, 12 Aug 2026 12:01:18 -0400 Subject: [PATCH 4/4] style(variant): satisfy go-critic --- cli/internal/variant/generator.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/cli/internal/variant/generator.go b/cli/internal/variant/generator.go index 457b5c53..33f264c2 100644 --- a/cli/internal/variant/generator.go +++ b/cli/internal/variant/generator.go @@ -1098,11 +1098,12 @@ func (g *Generator) repointDomainName(content string) string { } insertAt := loc[1] - if strings.HasPrefix(content[insertAt:], "\r\n") { + switch { + case strings.HasPrefix(content[insertAt:], "\r\n"): insertAt += 2 - } else if strings.HasPrefix(content[insertAt:], "\n") { + case strings.HasPrefix(content[insertAt:], "\n"): insertAt++ - } else { + default: return content[:insertAt] + lineEnding + "domain_name=" + target + content[insertAt:] }