diff --git a/main.go b/main.go index 03e65cb..56a25fa 100644 --- a/main.go +++ b/main.go @@ -236,8 +236,66 @@ func PostRender(command string) PostRenderer { } } +type environment struct { + region string + env string + rootPath string +} + +func discoverEnvironments(rootsDir string) ([]environment, error) { + regions, err := os.ReadDir(rootsDir) + if err != nil { + return nil, fmt.Errorf("error reading roots-dir %s: %w", rootsDir, err) + } + + var envs []environment + for _, region := range regions { + if !region.IsDir() { + continue + } + regionPath := filepath.Join(rootsDir, region.Name()) + entries, err := os.ReadDir(regionPath) + if err != nil { + return nil, fmt.Errorf("error reading region dir %s: %w", regionPath, err) + } + for _, entry := range entries { + if !entry.IsDir() { + continue + } + envs = append(envs, environment{ + region: region.Name(), + env: entry.Name(), + rootPath: filepath.Join(regionPath, entry.Name()), + }) + } + } + return envs, nil +} + +func computeOutputPath(renderDir, region, env string) string { + if env == "nonstable" { + return filepath.Join(renderDir, "root-nonstable-a004") + } + + regionNumber := "1" + for i := len(region) - 1; i >= 0; i-- { + if region[i] >= '0' && region[i] <= '9' { + // Find the start of the trailing number + j := i + for j > 0 && region[j-1] >= '0' && region[j-1] <= '9' { + j-- + } + regionNumber = region[j : i+1] + break + } + } + + return filepath.Join(renderDir, fmt.Sprintf("root-use%s-%s", regionNumber, env)) +} + func main() { root := flag.String("root", "bootstrap", "Directory to initially look for k8s manifests containing Argo applications. The root of the tree.") + rootsDir := flag.String("roots-dir", "", "Parent directory containing / subdirectories. When set, processes all environments in a single invocation. Mutually exclusive with -root.") workdir := flag.String("workdir", ".", "Directory to run the command in.") renderDir := flag.String("output", ".zz.auto-generated", "Path to store the compiled Argo applications.") maxDepth := flag.Int("max-depth", InfiniteDepth, "Maximum depth for the depth first walk.") @@ -256,14 +314,6 @@ func main() { } start := time.Now() - if err := helm.VerifyRenderDir(*renderDir); err != nil { - log.Fatal(err) - } - - h, err := getHashStore(*hashStore, *hashStrategy, *renderDir) - if err != nil { - log.Fatal(err) - } w := &Walker{ CopySource: CopySource, @@ -280,9 +330,37 @@ func main() { w.PostRender = PostRender(*postRenderer) } - if err := w.Walk(*root, *renderDir, *maxDepth, h); err != nil { - log.Fatal(err) + if *rootsDir != "" { + envs, err := discoverEnvironments(*rootsDir) + if err != nil { + log.Fatal(err) + } + for _, e := range envs { + outputPath := computeOutputPath(*renderDir, e.region, e.env) + if err := helm.VerifyRenderDir(outputPath); err != nil { + log.Fatal(err) + } + h, err := getHashStore(*hashStore, *hashStrategy, outputPath) + if err != nil { + log.Fatal(err) + } + if err := w.Walk(e.rootPath, outputPath, *maxDepth, h); err != nil { + log.Fatal(err) + } + } + } else { + if err := helm.VerifyRenderDir(*renderDir); err != nil { + log.Fatal(err) + } + h, err := getHashStore(*hashStore, *hashStrategy, *renderDir) + if err != nil { + log.Fatal(err) + } + if err := w.Walk(*root, *renderDir, *maxDepth, h); err != nil { + log.Fatal(err) + } } + log.Printf("mani-diffy took %v to run", time.Since(start)) } diff --git a/multi_root_test.go b/multi_root_test.go new file mode 100644 index 0000000..c7652e3 --- /dev/null +++ b/multi_root_test.go @@ -0,0 +1,120 @@ +package main + +import ( + "os" + "path/filepath" + "testing" +) + +func TestComputeOutputPath(t *testing.T) { + tests := []struct { + name string + renderDir string + region string + env string + want string + }{ + { + name: "standard us-east-1", + renderDir: "zz.auto-generated", + region: "us-east-1", + env: "chalk", + want: "zz.auto-generated/root-use1-chalk", + }, + { + name: "us-east-2", + renderDir: "zz.auto-generated", + region: "us-east-2", + env: "finplat-dev", + want: "zz.auto-generated/root-use2-finplat-dev", + }, + { + name: "nonstable special case", + renderDir: "zz.auto-generated", + region: "us-east-1", + env: "nonstable", + want: "zz.auto-generated/root-nonstable-a004", + }, + { + name: "nonstable3 is not special", + renderDir: "zz.auto-generated", + region: "us-east-1", + env: "nonstable3", + want: "zz.auto-generated/root-use1-nonstable3", + }, + { + name: "prod", + renderDir: "zz.auto-generated", + region: "us-east-1", + env: "prod", + want: "zz.auto-generated/root-use1-prod", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := computeOutputPath(tt.renderDir, tt.region, tt.env) + if got != tt.want { + t.Errorf("computeOutputPath(%q, %q, %q) = %q, want %q", + tt.renderDir, tt.region, tt.env, got, tt.want) + } + }) + } +} + +func TestDiscoverEnvironments(t *testing.T) { + // Create a temp directory structure: + // rootsDir/us-east-1/chalk/ + // rootsDir/us-east-1/prod/ + // rootsDir/us-east-2/finplat-dev/ + tmpDir := t.TempDir() + + dirs := []string{ + "us-east-1/chalk", + "us-east-1/prod", + "us-east-2/finplat-dev", + } + for _, d := range dirs { + if err := os.MkdirAll(filepath.Join(tmpDir, d), 0755); err != nil { + t.Fatal(err) + } + } + + // Also create a file at the top level that should be ignored + if err := os.WriteFile(filepath.Join(tmpDir, "README.md"), []byte("hi"), 0644); err != nil { + t.Fatal(err) + } + + envs, err := discoverEnvironments(tmpDir) + if err != nil { + t.Fatal(err) + } + + if len(envs) != 3 { + t.Fatalf("expected 3 environments, got %d", len(envs)) + } + + // Build a set for easier checking + found := make(map[string]string) + for _, e := range envs { + key := e.region + "/" + e.env + found[key] = e.rootPath + } + + for _, d := range dirs { + if _, ok := found[d]; !ok { + t.Errorf("expected to find environment %s", d) + } + expectedPath := filepath.Join(tmpDir, d) + if found[d] != expectedPath { + t.Errorf("environment %s: rootPath = %q, want %q", d, found[d], expectedPath) + } + } +} + +func TestDiscoverEnvironments_NonexistentDir(t *testing.T) { + _, err := discoverEnvironments("/nonexistent/path") + if err == nil { + t.Fatal("expected error for nonexistent directory") + } +}