Skip to content

Commit 73945d8

Browse files
authored
Merge pull request #76 from utilitywarehouse/dl-unique-dirs
Avoid building manifests in nested directories
2 parents 91aad74 + 265a362 commit 73945d8

File tree

3 files changed

+370
-24
lines changed

3 files changed

+370
-24
lines changed

README.md

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,16 @@ Usage:
2121

2222
NAME:
2323
kustomize-build-dirs - Given a list of input files, run `kustomize build` somewhere
24-
24+
2525
USAGE:
26-
kustomize-build-dirs [global options] command [command options]
27-
26+
kustomize-build-dirs [global options] command [command options]
27+
2828
COMMANDS:
2929
help, h Shows a list of commands or help for one command
30-
30+
3131
GLOBAL OPTIONS:
3232
--out-dir value Directory to output build manifests
33+
--depth value Minimum directory depth to work with (e.g., 2 means paths will be at least two levels deep like 'aaa/bbb/') (default: 0)
3334
--truncate-secrets Whether or not to truncate secrets. This can make life easier when you don't have strongbox credentials for some secrets (default: false)
3435
--help, -h show help
3536

@@ -47,6 +48,45 @@ in 'manifests.yaml' there. For example, if there is a kustomize directory at
4748
Will result in the built manifests being placed at
4849
'build/project-manifests/manifests.yaml'
4950

51+
Passing the `--depth` flag will set a minimum directory depth for kustomize
52+
directories to be processed. If a directory’s depth is less than the specified
53+
depth value, it will be ignored and no manifests will be built for it.
54+
55+
For example, you made changes to the following files:
56+
57+
```
58+
cluster-a/
59+
└── file.yaml
60+
cluster-b/
61+
├── namespace-a/
62+
| ├── file.yaml
63+
| └── app-a/
64+
| └── file.yaml
65+
└── namespace-b/
66+
└── file.yaml
67+
cluster-c/
68+
└── namespace-a/
69+
└── app-a/
70+
└── file.yaml
71+
```
72+
73+
and run
74+
75+
```
76+
kustomize-build-dirs --out-dir build --depth 2 project-manifests
77+
```
78+
79+
the result manifests built will only include
80+
81+
```
82+
build/
83+
├── cluster-b/
84+
| ├── namespace-a/
85+
| └── namespace-b/
86+
└── cluster-c/
87+
└── namesapce-a/
88+
```
89+
5090
Passing the `--truncate-secrets` flag will cause the application to empty any
5191
files that look to be [`strongbox`](https://github.com/uw-labs/strongbox)
5292
encrypted before running `kustomize build`, so the contents of any secrets will

cmd/kustomize-build-dirs/main.go

Lines changed: 115 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"os"
88
"os/exec"
99
"path/filepath"
10+
"sort"
1011
"strings"
1112
"sync"
1213

@@ -29,6 +30,7 @@ var getwdFunc = os.Getwd
2930
func main() { //go-cov:skip
3031
var opts struct {
3132
outDir string
33+
dirDepth int
3234
doTruncateSecrets bool
3335
}
3436
app := &cli.App{
@@ -41,6 +43,12 @@ func main() { //go-cov:skip
4143
Usage: "Directory to output build manifests",
4244
Destination: &opts.outDir,
4345
},
46+
&cli.IntFlag{
47+
Name: "depth",
48+
Value: 0,
49+
Usage: "Minimum directory depth to work with (e.g., 2 means paths will be at least two levels deep like 'aaa/bbb/')",
50+
Destination: &opts.dirDepth,
51+
},
4452
&cli.BoolFlag{
4553
Name: "truncate-secrets",
4654
Value: false,
@@ -49,7 +57,12 @@ func main() { //go-cov:skip
4957
},
5058
},
5159
Action: func(c *cli.Context) error {
52-
return kustomizeBuildDirs(opts.outDir, opts.doTruncateSecrets, c.Args().Slice())
60+
return kustomizeBuildDirs(
61+
opts.outDir,
62+
opts.dirDepth,
63+
opts.doTruncateSecrets,
64+
c.Args().Slice(),
65+
)
5366
},
5467
}
5568

@@ -59,7 +72,12 @@ func main() { //go-cov:skip
5972
}
6073
}
6174

62-
func kustomizeBuildDirs(outDir string, doTruncateSecrets bool, filepaths []string) error {
75+
func kustomizeBuildDirs(
76+
outDir string,
77+
dirDepth int,
78+
doTruncateSecrets bool,
79+
filepaths []string,
80+
) error {
6381
rootDir, err := getwdFunc()
6482
if err != nil {
6583
return fmt.Errorf("error reading working directory: %v", err)
@@ -69,7 +87,7 @@ func kustomizeBuildDirs(outDir string, doTruncateSecrets bool, filepaths []strin
6987
return err
7088
}
7189

72-
kustomizationRoots, err := findKustomizationRoots(rootDir, filepaths)
90+
kustomizationRoots, err := findKustomizationRoots(rootDir, filepaths, dirDepth)
7391
if err != nil {
7492
return err
7593
}
@@ -109,10 +127,103 @@ func checkKustomizeInstalled() error {
109127
return nil
110128
}
111129

130+
// splitPath takes a full file path string and returns a slice of its directory components.
131+
// It extracts the directory portion of the path and splits it by forward slashes (/).
132+
// Example: "aaa/bbb/ccc/file.yaml" => ["aaa", "bbb", "ccc"]
133+
func splitPath(path string) []string {
134+
cleaned := filepath.ToSlash(filepath.Dir(path))
135+
if cleaned == "." {
136+
return []string{}
137+
}
138+
return strings.Split(cleaned, "/")
139+
}
140+
141+
// commonPrefix compares two string slices (representing directory segments)
142+
// and returns their longest shared prefix.
143+
// Example: ["aaa", "bbb", "ccc"], ["aaa", "bbb", "ddd"] => ["aaa", "bbb"]
144+
func commonPrefix(a, b []string) []string {
145+
minLen := len(a)
146+
if len(b) < minLen {
147+
minLen = len(b)
148+
}
149+
out := make([]string, 0, minLen)
150+
for i := 0; i < minLen; i++ {
151+
if a[i] != b[i] {
152+
break
153+
}
154+
out = append(out, a[i])
155+
}
156+
return out
157+
}
158+
159+
// deepestCommonDirs takes a list of file paths and returns a list of directory paths
160+
// that represent the deepest common directory for each group of similarly prefixed files
161+
// with a minimum directory depth enforced.
162+
// Example (with minDepth = 2):
163+
// Input: ["aaa/file.yaml", "aaa/bbb/file.yaml", "bbb/ccc/file1.yaml", "bbb/ccc/file2.yaml"]
164+
// Output: ["aaa/bbb/", "bbb/ccc/"]
165+
func deepestCommonDirs(paths []string, minDepth int) []string {
166+
if len(paths) == 0 {
167+
return nil
168+
}
169+
170+
dirSet := make(map[string]struct{})
171+
172+
for _, path := range paths {
173+
dir := filepath.ToSlash(filepath.Dir(path))
174+
if dir == "." {
175+
// File in root directory is represented as empty string
176+
dir = ""
177+
}
178+
dirSet[dir] = struct{}{}
179+
}
180+
181+
// Filter out directories that are shallower than minDepth
182+
var dirs []string
183+
for dir := range dirSet {
184+
segments := strings.Split(dir, "/")
185+
if dir == "" {
186+
segments = []string{}
187+
}
188+
if len(segments) >= minDepth {
189+
dirs = append(dirs, dir)
190+
}
191+
}
192+
sort.Strings(dirs) // Sort for consistent and hierarchical comparison
193+
194+
var result []string
195+
skipPrefixes := make(map[string]struct{})
196+
197+
for _, dir := range dirs {
198+
skip := false
199+
200+
for parent := range skipPrefixes {
201+
if strings.HasPrefix(dir+"/", parent+"/") {
202+
skip = true
203+
break
204+
}
205+
}
206+
207+
if !skip {
208+
skipPrefixes[dir] = struct{}{}
209+
if dir == "" {
210+
result = append(result, "")
211+
} else {
212+
result = append(result, dir+"/")
213+
}
214+
}
215+
}
216+
217+
return result
218+
}
219+
112220
// findKustomizationRoots finds, for each given path, the first parent
113221
// directory containing a 'kustomization.yaml'. It returns a list of such paths
114222
// relative to the root
115-
func findKustomizationRoots(root string, paths []string) ([]string, error) {
223+
func findKustomizationRoots(root string, paths []string, dirDepth int) ([]string, error) {
224+
// Group paths by shared prefixes and return their deepest common directories
225+
paths = deepestCommonDirs(paths, dirDepth)
226+
116227
// there may be multiple changes under the same path
117228
// so use a map to track unique ones
118229
rootsMap := map[string]struct{}{}

0 commit comments

Comments
 (0)