Skip to content

Commit 265a362

Browse files
committed
Avoid building manifests in nested directories
... when changes were made to parent directory as well. This will prevent building same manifests multiple times as only the manifest of the parent directory will be built.
1 parent 91aad74 commit 265a362

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)