Skip to content

Commit 77707d0

Browse files
committed
Recursively diff Kustomizations
Signed-off-by: Boris Kreitchman <[email protected]>
1 parent cc87ffd commit 77707d0

File tree

3 files changed

+155
-24
lines changed

3 files changed

+155
-24
lines changed

cmd/flux/diff_kustomization.go

+15
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ type diffKsFlags struct {
5555
ignorePaths []string
5656
progressBar bool
5757
strictSubst bool
58+
recursive bool
59+
localSources map[string]string
5860
}
5961

6062
var diffKsArgs diffKsFlags
@@ -66,6 +68,8 @@ func init() {
6668
diffKsCmd.Flags().StringVar(&diffKsArgs.kustomizationFile, "kustomization-file", "", "Path to the Flux Kustomization YAML file.")
6769
diffKsCmd.Flags().BoolVar(&diffKsArgs.strictSubst, "strict-substitute", false,
6870
"When enabled, the post build substitutions will fail if a var without a default value is declared in files but is missing from the input vars.")
71+
diffKsCmd.Flags().BoolVarP(&diffKsArgs.recursive, "recursive", "r", false, "Recursively diff Kustomizations")
72+
diffKsCmd.Flags().StringToStringVar(&diffKsArgs.localSources, "local-sources", nil, "Local sources")
6973
diffCmd.AddCommand(diffKsCmd)
7074
}
7175

@@ -93,6 +97,7 @@ func diffKsCmdRun(cmd *cobra.Command, args []string) error {
9397
builder *build.Builder
9498
err error
9599
)
100+
96101
if diffKsArgs.progressBar {
97102
builder, err = build.NewBuilder(name, diffKsArgs.path,
98103
build.WithClientConfig(kubeconfigArgs, kubeclientOptions),
@@ -101,6 +106,8 @@ func diffKsCmdRun(cmd *cobra.Command, args []string) error {
101106
build.WithProgressBar(),
102107
build.WithIgnore(diffKsArgs.ignorePaths),
103108
build.WithStrictSubstitute(diffKsArgs.strictSubst),
109+
build.WithRecursive(diffKsArgs.recursive),
110+
build.WithLocalSources(diffKsArgs.localSources),
104111
)
105112
} else {
106113
builder, err = build.NewBuilder(name, diffKsArgs.path,
@@ -109,6 +116,8 @@ func diffKsCmdRun(cmd *cobra.Command, args []string) error {
109116
build.WithKustomizationFile(diffKsArgs.kustomizationFile),
110117
build.WithIgnore(diffKsArgs.ignorePaths),
111118
build.WithStrictSubstitute(diffKsArgs.strictSubst),
119+
build.WithRecursive(diffKsArgs.recursive),
120+
build.WithLocalSources(diffKsArgs.localSources),
112121
)
113122
}
114123

@@ -138,6 +147,12 @@ func diffKsCmdRun(cmd *cobra.Command, args []string) error {
138147

139148
select {
140149
case <-sigc:
150+
if diffKsArgs.progressBar {
151+
err := builder.StopSpinner()
152+
if err != nil {
153+
return err
154+
}
155+
}
141156
fmt.Println("Build cancelled... exiting.")
142157
return builder.Cancel()
143158
case err := <-errChan:

internal/build/build.go

+45-16
Original file line numberDiff line numberDiff line change
@@ -51,13 +51,14 @@ import (
5151
)
5252

5353
const (
54-
controllerName = "kustomize-controller"
55-
controllerGroup = "kustomize.toolkit.fluxcd.io"
56-
mask = "**SOPS**"
57-
dockercfgSecretType = "kubernetes.io/dockerconfigjson"
58-
typeField = "type"
59-
dataField = "data"
60-
stringDataField = "stringData"
54+
controllerName = "kustomize-controller"
55+
controllerGroup = "kustomize.toolkit.fluxcd.io"
56+
mask = "**SOPS**"
57+
dockercfgSecretType = "kubernetes.io/dockerconfigjson"
58+
typeField = "type"
59+
dataField = "data"
60+
stringDataField = "stringData"
61+
spinnerDryRunMessage = "running dry-run"
6162
)
6263

6364
var defaultTimeout = 80 * time.Second
@@ -81,6 +82,8 @@ type Builder struct {
8182
spinner *yacspin.Spinner
8283
dryRun bool
8384
strictSubst bool
85+
recursive bool
86+
localSources map[string]string
8487
}
8588

8689
// BuilderOptionFunc is a function that configures a Builder
@@ -110,7 +113,7 @@ func WithProgressBar() BuilderOptionFunc {
110113
CharSet: yacspin.CharSets[59],
111114
Suffix: "Kustomization diffing...",
112115
SuffixAutoColon: true,
113-
Message: "running dry-run",
116+
Message: spinnerDryRunMessage,
114117
StopCharacter: "✓",
115118
StopColors: []string{"fgGreen"},
116119
}
@@ -175,6 +178,37 @@ func WithIgnore(ignore []string) BuilderOptionFunc {
175178
}
176179
}
177180

181+
// WithRecursive sets the recurvice flag
182+
func WithRecursive(recursive bool) BuilderOptionFunc {
183+
return func(b *Builder) error {
184+
b.recursive = recursive
185+
return nil
186+
}
187+
}
188+
189+
// WithLocalSources sets the localSources field
190+
func WithLocalSources(localSources map[string]string) BuilderOptionFunc {
191+
return func(b *Builder) error {
192+
b.localSources = localSources
193+
return nil
194+
}
195+
}
196+
197+
func withClientConfigFrom(in *Builder) BuilderOptionFunc {
198+
return func(b *Builder) error {
199+
b.client = in.client
200+
b.restMapper = in.restMapper
201+
return nil
202+
}
203+
}
204+
205+
func withSpinnerFrom(in *Builder) BuilderOptionFunc {
206+
return func(b *Builder) error {
207+
b.spinner = in.spinner
208+
return nil
209+
}
210+
}
211+
178212
// NewBuilder returns a new Builder
179213
// It takes a kustomization name and a path to the resources
180214
// It also takes a list of BuilderOptionFunc to configure the builder
@@ -583,20 +617,15 @@ func (b *Builder) Cancel() error {
583617
b.mu.Lock()
584618
defer b.mu.Unlock()
585619

586-
err := b.stopSpinner()
587-
if err != nil {
588-
return err
589-
}
590-
591-
err = kustomize.CleanDirectory(b.resourcesPath, b.action)
620+
err := kustomize.CleanDirectory(b.resourcesPath, b.action)
592621
if err != nil {
593622
return err
594623
}
595624

596625
return nil
597626
}
598627

599-
func (b *Builder) startSpinner() error {
628+
func (b *Builder) StartSpinner() error {
600629
if b.spinner == nil {
601630
return nil
602631
}
@@ -609,7 +638,7 @@ func (b *Builder) startSpinner() error {
609638
return nil
610639
}
611640

612-
func (b *Builder) stopSpinner() error {
641+
func (b *Builder) StopSpinner() error {
613642
if b.spinner == nil {
614643
return nil
615644
}

internal/build/diff.go

+95-8
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import (
3333
"github.com/homeport/dyff/pkg/dyff"
3434
"github.com/lucasb-eyer/go-colorful"
3535
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
36+
"k8s.io/apimachinery/pkg/runtime"
3637
"k8s.io/apimachinery/pkg/runtime/schema"
3738
"k8s.io/apimachinery/pkg/util/errors"
3839
"sigs.k8s.io/yaml"
@@ -57,6 +58,22 @@ func (b *Builder) Manager() (*ssa.ResourceManager, error) {
5758
}
5859

5960
func (b *Builder) Diff() (string, bool, error) {
61+
err := b.StartSpinner()
62+
if err != nil {
63+
return "", false, err
64+
}
65+
66+
output, createdOrDrifted, diffErr := b.diff()
67+
68+
err = b.StopSpinner()
69+
if err != nil {
70+
return "", false, err
71+
}
72+
73+
return output, createdOrDrifted, diffErr
74+
}
75+
76+
func (b *Builder) diff() (string, bool, error) {
6077
output := strings.Builder{}
6178
createdOrDrifted := false
6279
objects, err := b.Build()
@@ -77,11 +94,6 @@ func (b *Builder) Diff() (string, bool, error) {
7794
ctx, cancel := context.WithTimeout(context.Background(), b.timeout)
7895
defer cancel()
7996

80-
err = b.startSpinner()
81-
if err != nil {
82-
return "", false, err
83-
}
84-
8597
var diffErrs []error
8698
// create an inventory of objects to be reconciled
8799
newInventory := newInventory()
@@ -127,6 +139,30 @@ func (b *Builder) Diff() (string, bool, error) {
127139
}
128140

129141
addObjectsToInventory(newInventory, change)
142+
143+
if b.recursive && isKustomization(obj) && change.Action != ssa.CreatedAction {
144+
kustomization, err := toKustomization(obj)
145+
if err != nil {
146+
return "", createdOrDrifted, err
147+
}
148+
149+
if !kustomizationsEqual(kustomization, b.kustomization) {
150+
subOutput, subCreatedOrDrifted, err := b.kustomizationDiff(kustomization)
151+
if err != nil {
152+
diffErrs = append(diffErrs, err)
153+
}
154+
if subCreatedOrDrifted {
155+
createdOrDrifted = true
156+
output.WriteString(bunt.Sprint(fmt.Sprintf("📁 %s changed\n", ssautil.FmtUnstructured(obj))))
157+
output.WriteString(subOutput)
158+
}
159+
160+
// finished with Kustomization diff
161+
if b.spinner != nil {
162+
b.spinner.Message(spinnerDryRunMessage)
163+
}
164+
}
165+
}
130166
}
131167

132168
if b.spinner != nil {
@@ -149,12 +185,63 @@ func (b *Builder) Diff() (string, bool, error) {
149185
}
150186
}
151187

152-
err = b.stopSpinner()
188+
return output.String(), createdOrDrifted, errors.Reduce(errors.Flatten(errors.NewAggregate(diffErrs)))
189+
}
190+
191+
func isKustomization(object *unstructured.Unstructured) bool {
192+
return object.GetKind() == "Kustomization" && strings.HasPrefix(object.GetAPIVersion(), controllerGroup)
193+
}
194+
195+
func toKustomization(object *unstructured.Unstructured) (*kustomizev1.Kustomization, error) {
196+
kustomization := &kustomizev1.Kustomization{}
197+
obj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(object)
153198
if err != nil {
154-
return "", createdOrDrifted, err
199+
return nil, fmt.Errorf("failed to convert to unstructured: %w", err)
155200
}
201+
err = runtime.DefaultUnstructuredConverter.FromUnstructured(obj, kustomization)
202+
if err != nil {
203+
return nil, fmt.Errorf("failed to convert to kustomization: %w", err)
204+
}
205+
return kustomization, nil
206+
}
156207

157-
return output.String(), createdOrDrifted, errors.Reduce(errors.Flatten(errors.NewAggregate(diffErrs)))
208+
func kustomizationsEqual(k1 *kustomizev1.Kustomization, k2 *kustomizev1.Kustomization) bool {
209+
return k1.Name == k2.Name && k1.Namespace == k2.Namespace
210+
}
211+
212+
func (b *Builder) kustomizationDiff(kustomization *kustomizev1.Kustomization) (string, bool, error) {
213+
if b.spinner != nil {
214+
b.spinner.Message(fmt.Sprintf("%s in %s", spinnerDryRunMessage, kustomization.Name))
215+
}
216+
217+
sourceRef := kustomization.Spec.SourceRef.DeepCopy()
218+
if sourceRef.Namespace == "" {
219+
sourceRef.Namespace = kustomization.Namespace
220+
}
221+
222+
sourceKey := sourceRef.String()
223+
localPath, ok := b.localSources[sourceKey]
224+
if !ok {
225+
return "", false, fmt.Errorf("cannot get local path for %s of kustomization %s", sourceKey, kustomization.Name)
226+
}
227+
228+
resourcesPath := filepath.Join(localPath, kustomization.Spec.Path)
229+
subBuilder, err := NewBuilder(kustomization.Name, resourcesPath,
230+
// use same client and spinner
231+
withClientConfigFrom(b),
232+
withSpinnerFrom(b),
233+
WithTimeout(b.timeout),
234+
WithIgnore(b.ignore),
235+
WithStrictSubstitute(b.strictSubst),
236+
WithRecursive(b.recursive),
237+
WithLocalSources(b.localSources),
238+
WithNamespace(kustomization.Namespace),
239+
)
240+
if err != nil {
241+
return "", false, err
242+
}
243+
244+
return subBuilder.diff()
158245
}
159246

160247
func writeYamls(liveObject, mergedObject *unstructured.Unstructured) (string, string, string, error) {

0 commit comments

Comments
 (0)