Skip to content

Commit

Permalink
Recursively diff Kustomizations
Browse files Browse the repository at this point in the history
Signed-off-by: Boris Kreitchman <bkreitch@gmail.com>
  • Loading branch information
bkreitch committed Aug 17, 2024
1 parent cc87ffd commit 77707d0
Show file tree
Hide file tree
Showing 3 changed files with 155 additions and 24 deletions.
15 changes: 15 additions & 0 deletions cmd/flux/diff_kustomization.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ type diffKsFlags struct {
ignorePaths []string
progressBar bool
strictSubst bool
recursive bool
localSources map[string]string
}

var diffKsArgs diffKsFlags
Expand All @@ -66,6 +68,8 @@ func init() {
diffKsCmd.Flags().StringVar(&diffKsArgs.kustomizationFile, "kustomization-file", "", "Path to the Flux Kustomization YAML file.")
diffKsCmd.Flags().BoolVar(&diffKsArgs.strictSubst, "strict-substitute", false,
"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.")
diffKsCmd.Flags().BoolVarP(&diffKsArgs.recursive, "recursive", "r", false, "Recursively diff Kustomizations")
diffKsCmd.Flags().StringToStringVar(&diffKsArgs.localSources, "local-sources", nil, "Local sources")
diffCmd.AddCommand(diffKsCmd)
}

Expand Down Expand Up @@ -93,6 +97,7 @@ func diffKsCmdRun(cmd *cobra.Command, args []string) error {
builder *build.Builder
err error
)

if diffKsArgs.progressBar {
builder, err = build.NewBuilder(name, diffKsArgs.path,
build.WithClientConfig(kubeconfigArgs, kubeclientOptions),
Expand All @@ -101,6 +106,8 @@ func diffKsCmdRun(cmd *cobra.Command, args []string) error {
build.WithProgressBar(),
build.WithIgnore(diffKsArgs.ignorePaths),
build.WithStrictSubstitute(diffKsArgs.strictSubst),
build.WithRecursive(diffKsArgs.recursive),
build.WithLocalSources(diffKsArgs.localSources),
)
} else {
builder, err = build.NewBuilder(name, diffKsArgs.path,
Expand All @@ -109,6 +116,8 @@ func diffKsCmdRun(cmd *cobra.Command, args []string) error {
build.WithKustomizationFile(diffKsArgs.kustomizationFile),
build.WithIgnore(diffKsArgs.ignorePaths),
build.WithStrictSubstitute(diffKsArgs.strictSubst),
build.WithRecursive(diffKsArgs.recursive),
build.WithLocalSources(diffKsArgs.localSources),
)
}

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

select {
case <-sigc:
if diffKsArgs.progressBar {
err := builder.StopSpinner()
if err != nil {
return err
}
}
fmt.Println("Build cancelled... exiting.")
return builder.Cancel()
case err := <-errChan:
Expand Down
61 changes: 45 additions & 16 deletions internal/build/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,14 @@ import (
)

const (
controllerName = "kustomize-controller"
controllerGroup = "kustomize.toolkit.fluxcd.io"
mask = "**SOPS**"
dockercfgSecretType = "kubernetes.io/dockerconfigjson"
typeField = "type"
dataField = "data"
stringDataField = "stringData"
controllerName = "kustomize-controller"
controllerGroup = "kustomize.toolkit.fluxcd.io"
mask = "**SOPS**"
dockercfgSecretType = "kubernetes.io/dockerconfigjson"

Check failure

Code scanning / CodeQL

Hard-coded credentials Critical

Hard-coded
secret
.
typeField = "type"
dataField = "data"
stringDataField = "stringData"
spinnerDryRunMessage = "running dry-run"
)

var defaultTimeout = 80 * time.Second
Expand All @@ -81,6 +82,8 @@ type Builder struct {
spinner *yacspin.Spinner
dryRun bool
strictSubst bool
recursive bool
localSources map[string]string
}

// BuilderOptionFunc is a function that configures a Builder
Expand Down Expand Up @@ -110,7 +113,7 @@ func WithProgressBar() BuilderOptionFunc {
CharSet: yacspin.CharSets[59],
Suffix: "Kustomization diffing...",
SuffixAutoColon: true,
Message: "running dry-run",
Message: spinnerDryRunMessage,
StopCharacter: "✓",
StopColors: []string{"fgGreen"},
}
Expand Down Expand Up @@ -175,6 +178,37 @@ func WithIgnore(ignore []string) BuilderOptionFunc {
}
}

// WithRecursive sets the recurvice flag
func WithRecursive(recursive bool) BuilderOptionFunc {
return func(b *Builder) error {
b.recursive = recursive
return nil
}
}

// WithLocalSources sets the localSources field
func WithLocalSources(localSources map[string]string) BuilderOptionFunc {
return func(b *Builder) error {
b.localSources = localSources
return nil
}
}

func withClientConfigFrom(in *Builder) BuilderOptionFunc {
return func(b *Builder) error {
b.client = in.client
b.restMapper = in.restMapper
return nil
}
}

func withSpinnerFrom(in *Builder) BuilderOptionFunc {
return func(b *Builder) error {
b.spinner = in.spinner
return nil
}
}

// NewBuilder returns a new Builder
// It takes a kustomization name and a path to the resources
// It also takes a list of BuilderOptionFunc to configure the builder
Expand Down Expand Up @@ -583,20 +617,15 @@ func (b *Builder) Cancel() error {
b.mu.Lock()
defer b.mu.Unlock()

err := b.stopSpinner()
if err != nil {
return err
}

err = kustomize.CleanDirectory(b.resourcesPath, b.action)
err := kustomize.CleanDirectory(b.resourcesPath, b.action)
if err != nil {
return err
}

return nil
}

func (b *Builder) startSpinner() error {
func (b *Builder) StartSpinner() error {
if b.spinner == nil {
return nil
}
Expand All @@ -609,7 +638,7 @@ func (b *Builder) startSpinner() error {
return nil
}

func (b *Builder) stopSpinner() error {
func (b *Builder) StopSpinner() error {
if b.spinner == nil {
return nil
}
Expand Down
103 changes: 95 additions & 8 deletions internal/build/diff.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import (
"github.com/homeport/dyff/pkg/dyff"
"github.com/lucasb-eyer/go-colorful"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/errors"
"sigs.k8s.io/yaml"
Expand All @@ -57,6 +58,22 @@ func (b *Builder) Manager() (*ssa.ResourceManager, error) {
}

func (b *Builder) Diff() (string, bool, error) {
err := b.StartSpinner()
if err != nil {
return "", false, err
}

output, createdOrDrifted, diffErr := b.diff()

err = b.StopSpinner()
if err != nil {
return "", false, err
}

return output, createdOrDrifted, diffErr
}

func (b *Builder) diff() (string, bool, error) {
output := strings.Builder{}
createdOrDrifted := false
objects, err := b.Build()
Expand All @@ -77,11 +94,6 @@ func (b *Builder) Diff() (string, bool, error) {
ctx, cancel := context.WithTimeout(context.Background(), b.timeout)
defer cancel()

err = b.startSpinner()
if err != nil {
return "", false, err
}

var diffErrs []error
// create an inventory of objects to be reconciled
newInventory := newInventory()
Expand Down Expand Up @@ -127,6 +139,30 @@ func (b *Builder) Diff() (string, bool, error) {
}

addObjectsToInventory(newInventory, change)

if b.recursive && isKustomization(obj) && change.Action != ssa.CreatedAction {
kustomization, err := toKustomization(obj)
if err != nil {
return "", createdOrDrifted, err
}

if !kustomizationsEqual(kustomization, b.kustomization) {
subOutput, subCreatedOrDrifted, err := b.kustomizationDiff(kustomization)
if err != nil {
diffErrs = append(diffErrs, err)
}
if subCreatedOrDrifted {
createdOrDrifted = true
output.WriteString(bunt.Sprint(fmt.Sprintf("📁 %s changed\n", ssautil.FmtUnstructured(obj))))
output.WriteString(subOutput)
}

// finished with Kustomization diff
if b.spinner != nil {
b.spinner.Message(spinnerDryRunMessage)
}
}
}
}

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

err = b.stopSpinner()
return output.String(), createdOrDrifted, errors.Reduce(errors.Flatten(errors.NewAggregate(diffErrs)))
}

func isKustomization(object *unstructured.Unstructured) bool {
return object.GetKind() == "Kustomization" && strings.HasPrefix(object.GetAPIVersion(), controllerGroup)
}

func toKustomization(object *unstructured.Unstructured) (*kustomizev1.Kustomization, error) {
kustomization := &kustomizev1.Kustomization{}
obj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(object)
if err != nil {
return "", createdOrDrifted, err
return nil, fmt.Errorf("failed to convert to unstructured: %w", err)
}
err = runtime.DefaultUnstructuredConverter.FromUnstructured(obj, kustomization)
if err != nil {
return nil, fmt.Errorf("failed to convert to kustomization: %w", err)
}
return kustomization, nil
}

return output.String(), createdOrDrifted, errors.Reduce(errors.Flatten(errors.NewAggregate(diffErrs)))
func kustomizationsEqual(k1 *kustomizev1.Kustomization, k2 *kustomizev1.Kustomization) bool {
return k1.Name == k2.Name && k1.Namespace == k2.Namespace
}

func (b *Builder) kustomizationDiff(kustomization *kustomizev1.Kustomization) (string, bool, error) {
if b.spinner != nil {
b.spinner.Message(fmt.Sprintf("%s in %s", spinnerDryRunMessage, kustomization.Name))
}

sourceRef := kustomization.Spec.SourceRef.DeepCopy()
if sourceRef.Namespace == "" {
sourceRef.Namespace = kustomization.Namespace
}

sourceKey := sourceRef.String()
localPath, ok := b.localSources[sourceKey]
if !ok {
return "", false, fmt.Errorf("cannot get local path for %s of kustomization %s", sourceKey, kustomization.Name)
}

resourcesPath := filepath.Join(localPath, kustomization.Spec.Path)
subBuilder, err := NewBuilder(kustomization.Name, resourcesPath,
// use same client and spinner
withClientConfigFrom(b),
withSpinnerFrom(b),
WithTimeout(b.timeout),
WithIgnore(b.ignore),
WithStrictSubstitute(b.strictSubst),
WithRecursive(b.recursive),
WithLocalSources(b.localSources),
WithNamespace(kustomization.Namespace),
)
if err != nil {
return "", false, err
}

return subBuilder.diff()
}

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

0 comments on commit 77707d0

Please sign in to comment.