Recursively build and diff Kustomizations

Signed-off-by: Boris Kreitchman <bkreitch@gmail.com>
pull/4939/head
Boris Kreitchman 5 months ago
parent 1b4de026dd
commit 2d37544b06

@ -53,7 +53,12 @@ flux build kustomization my-app --path ./path/to/local/manifests \
# Exclude files by providing a comma separated list of entries that follow the .gitignore pattern fromat. # Exclude files by providing a comma separated list of entries that follow the .gitignore pattern fromat.
flux build kustomization my-app --path ./path/to/local/manifests \ flux build kustomization my-app --path ./path/to/local/manifests \
--kustomization-file ./path/to/local/my-app.yaml \ --kustomization-file ./path/to/local/my-app.yaml \
--ignore-paths "/to_ignore/**/*.yaml,ignore.yaml"`, --ignore-paths "/to_ignore/**/*.yaml,ignore.yaml
# Run recursively on all encountered Kustomizations
flux build kustomization my-app --path ./path/to/local/manifests \
--recursive \
--local-sources GitRepository/flux-system/my-repo=./path/to/local/git"`,
ValidArgsFunction: resourceNamesCompletionFunc(kustomizev1.GroupVersion.WithKind(kustomizev1.KustomizationKind)), ValidArgsFunction: resourceNamesCompletionFunc(kustomizev1.GroupVersion.WithKind(kustomizev1.KustomizationKind)),
RunE: buildKsCmdRun, RunE: buildKsCmdRun,
} }
@ -64,6 +69,8 @@ type buildKsFlags struct {
ignorePaths []string ignorePaths []string
dryRun bool dryRun bool
strictSubst bool strictSubst bool
recursive bool
localSources map[string]string
} }
var buildKsArgs buildKsFlags var buildKsArgs buildKsFlags
@ -75,6 +82,8 @@ func init() {
buildKsCmd.Flags().BoolVar(&buildKsArgs.dryRun, "dry-run", false, "Dry run mode.") buildKsCmd.Flags().BoolVar(&buildKsArgs.dryRun, "dry-run", false, "Dry run mode.")
buildKsCmd.Flags().BoolVar(&buildKsArgs.strictSubst, "strict-substitute", false, buildKsCmd.Flags().BoolVar(&buildKsArgs.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.") "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.")
buildKsCmd.Flags().BoolVarP(&buildKsArgs.recursive, "recursive", "r", false, "Recursively build Kustomizations")
buildKsCmd.Flags().StringToStringVar(&buildKsArgs.localSources, "local-sources", nil, "Comma-separated list of repositories in format: Kind/namespace/name=path")
buildCmd.AddCommand(buildKsCmd) buildCmd.AddCommand(buildKsCmd)
} }
@ -111,6 +120,8 @@ func buildKsCmdRun(cmd *cobra.Command, args []string) (err error) {
build.WithNamespace(*kubeconfigArgs.Namespace), build.WithNamespace(*kubeconfigArgs.Namespace),
build.WithIgnore(buildKsArgs.ignorePaths), build.WithIgnore(buildKsArgs.ignorePaths),
build.WithStrictSubstitute(buildKsArgs.strictSubst), build.WithStrictSubstitute(buildKsArgs.strictSubst),
build.WithRecursive(buildKsArgs.recursive),
build.WithLocalSources(buildKsArgs.localSources),
) )
} else { } else {
builder, err = build.NewBuilder(name, buildKsArgs.path, builder, err = build.NewBuilder(name, buildKsArgs.path,
@ -119,6 +130,8 @@ func buildKsCmdRun(cmd *cobra.Command, args []string) (err error) {
build.WithKustomizationFile(buildKsArgs.kustomizationFile), build.WithKustomizationFile(buildKsArgs.kustomizationFile),
build.WithIgnore(buildKsArgs.ignorePaths), build.WithIgnore(buildKsArgs.ignorePaths),
build.WithStrictSubstitute(buildKsArgs.strictSubst), build.WithStrictSubstitute(buildKsArgs.strictSubst),
build.WithRecursive(buildKsArgs.recursive),
build.WithLocalSources(buildKsArgs.localSources),
) )
} }

@ -70,6 +70,12 @@ func TestBuildKustomization(t *testing.T) {
resultFile: "./testdata/build-kustomization/podinfo-with-ignore-result.yaml", resultFile: "./testdata/build-kustomization/podinfo-with-ignore-result.yaml",
assertFunc: "assertGoldenTemplateFile", assertFunc: "assertGoldenTemplateFile",
}, },
{
name: "build with recursive",
args: "build kustomization podinfo --path ./testdata/build-kustomization/podinfo-with-my-app --recursive --local-sources GitRepository/default/podinfo=./testdata/build-kustomization",
resultFile: "./testdata/build-kustomization/podinfo-with-my-app-result.yaml",
assertFunc: "assertGoldenTemplateFile",
},
} }
tmpl := map[string]string{ tmpl := map[string]string{
@ -157,6 +163,12 @@ spec:
resultFile: "./testdata/build-kustomization/podinfo-with-var-substitution-result.yaml", resultFile: "./testdata/build-kustomization/podinfo-with-var-substitution-result.yaml",
assertFunc: "assertGoldenTemplateFile", assertFunc: "assertGoldenTemplateFile",
}, },
{
name: "build with recursive",
args: "build kustomization podinfo --kustomization-file " + tmpFile + " --path ./testdata/build-kustomization/podinfo-with-my-app --recursive --local-sources GitRepository/default/podinfo=./testdata/build-kustomization",
resultFile: "./testdata/build-kustomization/podinfo-with-my-app-result.yaml",
assertFunc: "assertGoldenTemplateFile",
},
} }
tmpl := map[string]string{ tmpl := map[string]string{

@ -44,7 +44,12 @@ flux diff kustomization my-app --path ./path/to/local/manifests \
# Exclude files by providing a comma separated list of entries that follow the .gitignore pattern fromat. # Exclude files by providing a comma separated list of entries that follow the .gitignore pattern fromat.
flux diff kustomization my-app --path ./path/to/local/manifests \ flux diff kustomization my-app --path ./path/to/local/manifests \
--kustomization-file ./path/to/local/my-app.yaml \ --kustomization-file ./path/to/local/my-app.yaml \
--ignore-paths "/to_ignore/**/*.yaml,ignore.yaml"`, --ignore-paths "/to_ignore/**/*.yaml,ignore.yaml
# Run recursively on all encountered Kustomizations
flux diff kustomization my-app --path ./path/to/local/manifests \
--recursive \
--local-sources GitRepository/flux-system/my-repo=./path/to/local/git"`,
ValidArgsFunction: resourceNamesCompletionFunc(kustomizev1.GroupVersion.WithKind(kustomizev1.KustomizationKind)), ValidArgsFunction: resourceNamesCompletionFunc(kustomizev1.GroupVersion.WithKind(kustomizev1.KustomizationKind)),
RunE: diffKsCmdRun, RunE: diffKsCmdRun,
} }
@ -55,6 +60,8 @@ type diffKsFlags struct {
ignorePaths []string ignorePaths []string
progressBar bool progressBar bool
strictSubst bool strictSubst bool
recursive bool
localSources map[string]string
} }
var diffKsArgs diffKsFlags var diffKsArgs diffKsFlags
@ -66,6 +73,8 @@ func init() {
diffKsCmd.Flags().StringVar(&diffKsArgs.kustomizationFile, "kustomization-file", "", "Path to the Flux Kustomization YAML file.") diffKsCmd.Flags().StringVar(&diffKsArgs.kustomizationFile, "kustomization-file", "", "Path to the Flux Kustomization YAML file.")
diffKsCmd.Flags().BoolVar(&diffKsArgs.strictSubst, "strict-substitute", false, 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.") "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, "Comma-separated list of repositories in format: Kind/namespace/name=path")
diffCmd.AddCommand(diffKsCmd) diffCmd.AddCommand(diffKsCmd)
} }
@ -101,6 +110,9 @@ func diffKsCmdRun(cmd *cobra.Command, args []string) error {
build.WithProgressBar(), build.WithProgressBar(),
build.WithIgnore(diffKsArgs.ignorePaths), build.WithIgnore(diffKsArgs.ignorePaths),
build.WithStrictSubstitute(diffKsArgs.strictSubst), build.WithStrictSubstitute(diffKsArgs.strictSubst),
build.WithRecursive(diffKsArgs.recursive),
build.WithLocalSources(diffKsArgs.localSources),
build.WithSingleKustomization(),
) )
} else { } else {
builder, err = build.NewBuilder(name, diffKsArgs.path, builder, err = build.NewBuilder(name, diffKsArgs.path,
@ -109,6 +121,9 @@ func diffKsCmdRun(cmd *cobra.Command, args []string) error {
build.WithKustomizationFile(diffKsArgs.kustomizationFile), build.WithKustomizationFile(diffKsArgs.kustomizationFile),
build.WithIgnore(diffKsArgs.ignorePaths), build.WithIgnore(diffKsArgs.ignorePaths),
build.WithStrictSubstitute(diffKsArgs.strictSubst), build.WithStrictSubstitute(diffKsArgs.strictSubst),
build.WithRecursive(diffKsArgs.recursive),
build.WithLocalSources(diffKsArgs.localSources),
build.WithSingleKustomization(),
) )
} }
@ -138,6 +153,12 @@ func diffKsCmdRun(cmd *cobra.Command, args []string) error {
select { select {
case <-sigc: case <-sigc:
if diffKsArgs.progressBar {
err := builder.StopSpinner()
if err != nil {
return err
}
}
fmt.Println("Build cancelled... exiting.") fmt.Println("Build cancelled... exiting.")
return builder.Cancel() return builder.Cancel()
case err := <-errChan: case err := <-errChan:

@ -97,6 +97,12 @@ func TestDiffKustomization(t *testing.T) {
objectFile: "", objectFile: "",
assert: assertGoldenFile("./testdata/diff-kustomization/nothing-is-deployed.golden"), assert: assertGoldenFile("./testdata/diff-kustomization/nothing-is-deployed.golden"),
}, },
{
name: "diff with recursive",
args: "diff kustomization podinfo --path ./testdata/build-kustomization/podinfo-with-my-app --progress-bar=false --recursive --local-sources GitRepository/default/podinfo=./testdata/build-kustomization",
objectFile: "./testdata/diff-kustomization/my-app.yaml",
assert: assertGoldenFile("./testdata/diff-kustomization/diff-with-recursive.golden"),
},
} }
tmpl := map[string]string{ tmpl := map[string]string{

@ -429,7 +429,9 @@ func resetCmdArgs() {
tail: -1, tail: -1,
fluxNamespace: rootArgs.defaults.Namespace, fluxNamespace: rootArgs.defaults.Namespace,
} }
buildKsArgs = buildKsFlags{} buildKsArgs = buildKsFlags{
localSources: map[string]string{},
}
checkArgs = checkFlags{} checkArgs = checkFlags{}
createArgs = createFlags{} createArgs = createFlags{}
deleteArgs = deleteFlags{} deleteArgs = deleteFlags{}

@ -0,0 +1,6 @@
apiVersion: v1
data:
var: test
kind: ConfigMap
metadata:
name: my-app

@ -0,0 +1,29 @@
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
labels:
kustomize.toolkit.fluxcd.io/name: podinfo
kustomize.toolkit.fluxcd.io/namespace: {{ .fluxns }}
name: my-app
namespace: default
spec:
force: true
interval: 5m0s
path: ./my-app
prune: true
sourceRef:
kind: GitRepository
name: podinfo
targetNamespace: default
---
apiVersion: v1
data:
var: test
kind: ConfigMap
metadata:
labels:
kustomize.toolkit.fluxcd.io/name: my-app
kustomize.toolkit.fluxcd.io/namespace: default
name: my-app
namespace: default
---

@ -0,0 +1,4 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ./my-app.yaml

@ -0,0 +1,14 @@
---
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: my-app
spec:
interval: 5m0s
path: ./my-app
force: true
prune: true
sourceRef:
kind: GitRepository
name: podinfo
targetNamespace: default

@ -0,0 +1,2 @@
📁 Kustomization/default/my-app changed
► ConfigMap/default/my-app created

@ -0,0 +1,18 @@
---
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
labels:
kustomize.toolkit.fluxcd.io/name: podinfo
kustomize.toolkit.fluxcd.io/namespace: {{ .fluxns }}
name: my-app
namespace: default
spec:
interval: 5m0s
path: ./my-app
force: true
prune: true
sourceRef:
kind: GitRepository
name: podinfo
targetNamespace: default

@ -25,11 +25,13 @@ import (
"fmt" "fmt"
"io" "io"
"os" "os"
"path/filepath"
"strings" "strings"
"sync" "sync"
"time" "time"
"github.com/theckman/yacspin" "github.com/theckman/yacspin"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
@ -58,6 +60,7 @@ const (
typeField = "type" typeField = "type"
dataField = "data" dataField = "data"
stringDataField = "stringData" stringDataField = "stringData"
spinnerDryRunMessage = "running dry-run"
) )
var defaultTimeout = 80 * time.Second var defaultTimeout = 80 * time.Second
@ -81,6 +84,10 @@ type Builder struct {
spinner *yacspin.Spinner spinner *yacspin.Spinner
dryRun bool dryRun bool
strictSubst bool strictSubst bool
recursive bool
localSources map[string]string
// diff needs to handle kustomizations one by one
singleKustomization bool
} }
// BuilderOptionFunc is a function that configures a Builder // BuilderOptionFunc is a function that configures a Builder
@ -110,7 +117,7 @@ func WithProgressBar() BuilderOptionFunc {
CharSet: yacspin.CharSets[59], CharSet: yacspin.CharSets[59],
Suffix: "Kustomization diffing...", Suffix: "Kustomization diffing...",
SuffixAutoColon: true, SuffixAutoColon: true,
Message: "running dry-run", Message: spinnerDryRunMessage,
StopCharacter: "✓", StopCharacter: "✓",
StopColors: []string{"fgGreen"}, StopColors: []string{"fgGreen"},
} }
@ -175,6 +182,55 @@ func WithIgnore(ignore []string) BuilderOptionFunc {
} }
} }
// WithRecursive sets the recursive field
func WithRecursive(recursive bool) BuilderOptionFunc {
return func(b *Builder) error {
b.recursive = recursive
return nil
}
}
// WithLocalSources sets the local sources field
func WithLocalSources(localSources map[string]string) BuilderOptionFunc {
return func(b *Builder) error {
b.localSources = localSources
return nil
}
}
// WithSingleKustomization sets the single kustomization field to true
func WithSingleKustomization() BuilderOptionFunc {
return func(b *Builder) error {
b.singleKustomization = true
return nil
}
}
// withClientConfigFrom copies client and restMapper fields
func withClientConfigFrom(in *Builder) BuilderOptionFunc {
return func(b *Builder) error {
b.client = in.client
b.restMapper = in.restMapper
return nil
}
}
// withClientConfigFrom copies spinner field
func withSpinnerFrom(in *Builder) BuilderOptionFunc {
return func(b *Builder) error {
b.spinner = in.spinner
return nil
}
}
// withKustomization sets the kustomization field
func withKustomization(k *kustomizev1.Kustomization) BuilderOptionFunc {
return func(b *Builder) error {
b.kustomization = k
return nil
}
}
// NewBuilder returns a new Builder // NewBuilder returns a new Builder
// It takes a kustomization name and a path to the resources // It takes a kustomization name and a path to the resources
// It also takes a list of BuilderOptionFunc to configure the builder // It also takes a list of BuilderOptionFunc to configure the builder
@ -269,6 +325,27 @@ func (b *Builder) Build() ([]*unstructured.Unstructured, error) {
ssautil.SetCommonMetadata(objects, m.Labels, m.Annotations) ssautil.SetCommonMetadata(objects, m.Labels, m.Annotations)
} }
if b.recursive && !b.singleKustomization {
var objectsToAdd []*unstructured.Unstructured
for _, obj := range objects {
if isKustomization(obj) {
k, err := toKustomization(obj)
if err != nil {
return nil, err
}
if !kustomizationsEqual(k, b.kustomization) {
subObjects, err := b.kustomizationBuild(k)
if err != nil {
return nil, err
}
objectsToAdd = append(objectsToAdd, subObjects...)
}
}
}
objects = append(objects, objectsToAdd...)
}
return objects, nil return objects, nil
} }
@ -281,8 +358,12 @@ func (b *Builder) build() (m resmap.ResMap, err error) {
if !b.dryRun { if !b.dryRun {
liveKus, err = b.getKustomization(ctx) liveKus, err = b.getKustomization(ctx)
if err != nil { if err != nil {
if !apierrors.IsNotFound(err) || b.kustomization == nil {
return nil, fmt.Errorf("failed to get kustomization object: %w", err) return nil, fmt.Errorf("failed to get kustomization object: %w", err)
} }
// use provided Kustomization
liveKus = b.kustomization
}
} }
k, err := b.resolveKustomization(liveKus) k, err := b.resolveKustomization(liveKus)
if err != nil { if err != nil {
@ -334,6 +415,46 @@ func (b *Builder) build() (m resmap.ResMap, err error) {
} }
func (b *Builder) kustomizationBuild(k *kustomizev1.Kustomization) ([]*unstructured.Unstructured, error) {
resourcesPath, err := b.kustomizationPath(k)
if err != nil {
return nil, err
}
subBuilder, err := NewBuilder(k.Name, resourcesPath,
// use same client
withClientConfigFrom(b),
// kustomization will be used if there is no live kustomization
withKustomization(k),
WithTimeout(b.timeout),
WithNamespace(k.Namespace),
WithIgnore(b.ignore),
WithStrictSubstitute(b.strictSubst),
WithRecursive(b.recursive),
WithLocalSources(b.localSources),
)
if err != nil {
return nil, err
}
return subBuilder.Build()
}
func (b *Builder) kustomizationPath(k *kustomizev1.Kustomization) (string, error) {
sourceRef := k.Spec.SourceRef.DeepCopy()
if sourceRef.Namespace == "" {
sourceRef.Namespace = k.Namespace
}
sourceKey := sourceRef.String()
localPath, ok := b.localSources[sourceKey]
if !ok {
return "", fmt.Errorf("cannot get local path for %s of kustomization %s", sourceKey, k.Name)
}
return filepath.Join(localPath, k.Spec.Path), nil
}
func (b *Builder) unMarshallKustomization() (*kustomizev1.Kustomization, error) { func (b *Builder) unMarshallKustomization() (*kustomizev1.Kustomization, error) {
data, err := os.ReadFile(b.kustomizationFile) data, err := os.ReadFile(b.kustomizationFile)
if err != nil { if err != nil {
@ -423,6 +544,28 @@ func (b *Builder) do(ctx context.Context, kustomization kustomizev1.Kustomizatio
return m, nil return m, nil
} }
func isKustomization(object *unstructured.Unstructured) bool {
return strings.HasPrefix(object.GetAPIVersion(), kustomizev1.GroupVersion.Group+"/") &&
object.GetKind() == kustomizev1.KustomizationKind
}
func toKustomization(object *unstructured.Unstructured) (*kustomizev1.Kustomization, error) {
obj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(object)
if err != nil {
return nil, fmt.Errorf("failed to convert to unstructured: %w", err)
}
k := &kustomizev1.Kustomization{}
err = runtime.DefaultUnstructuredConverter.FromUnstructured(obj, k)
if err != nil {
return nil, fmt.Errorf("failed to convert to kustomization: %w", err)
}
return k, nil
}
func kustomizationsEqual(k1 *kustomizev1.Kustomization, k2 *kustomizev1.Kustomization) bool {
return k1.Name == k2.Name && k1.Namespace == k2.Namespace
}
func (b *Builder) setOwnerLabels(res *resource.Resource) error { func (b *Builder) setOwnerLabels(res *resource.Resource) error {
labels := res.GetLabels() labels := res.GetLabels()
@ -583,12 +726,7 @@ func (b *Builder) Cancel() error {
b.mu.Lock() b.mu.Lock()
defer b.mu.Unlock() defer b.mu.Unlock()
err := b.stopSpinner() err := kustomize.CleanDirectory(b.resourcesPath, b.action)
if err != nil {
return err
}
err = kustomize.CleanDirectory(b.resourcesPath, b.action)
if err != nil { if err != nil {
return err return err
} }
@ -596,7 +734,7 @@ func (b *Builder) Cancel() error {
return nil return nil
} }
func (b *Builder) startSpinner() error { func (b *Builder) StartSpinner() error {
if b.spinner == nil { if b.spinner == nil {
return nil return nil
} }
@ -609,7 +747,7 @@ func (b *Builder) startSpinner() error {
return nil return nil
} }
func (b *Builder) stopSpinner() error { func (b *Builder) StopSpinner() error {
if b.spinner == nil { if b.spinner == nil {
return nil return nil
} }

@ -26,6 +26,7 @@ import (
"github.com/fluxcd/pkg/apis/meta" "github.com/fluxcd/pkg/apis/meta"
"github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"sigs.k8s.io/kustomize/api/resource" "sigs.k8s.io/kustomize/api/resource"
"sigs.k8s.io/kustomize/kyaml/yaml" "sigs.k8s.io/kustomize/kyaml/yaml"
) )
@ -361,3 +362,242 @@ func Test_ResolveKustomization(t *testing.T) {
}) })
} }
} }
func Test_isKustomization(t *testing.T) {
tests := []struct {
name string
expected bool
object *unstructured.Unstructured
}{
{
name: "flux kustomization",
object: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "kustomize.toolkit.fluxcd.io/v1",
"kind": "Kustomization",
},
},
expected: true,
},
{
name: "other kustomization",
object: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "kustomize.config.k8s.io/v1beta1",
"kind": "Kustomization",
},
},
expected: false,
},
{
name: "wrong kind",
object: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "kustomize.toolkit.fluxcd.io/v1",
"kind": "ConfigMap",
},
},
expected: false,
},
{
name: "wrong object",
object: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
},
},
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
actual := isKustomization(tt.object)
if actual != tt.expected {
t.Fatalf("got '%v', want '%v'", actual, tt.expected)
}
})
}
}
func Test_kustomizationsEqual(t *testing.T) {
tests := []struct {
name string
kustomization1 *kustomizev1.Kustomization
kustomization2 *kustomizev1.Kustomization
expected bool
}{
{
name: "equal",
kustomization1: &kustomizev1.Kustomization{
ObjectMeta: metav1.ObjectMeta{
Name: "podinfo",
Namespace: "flux-system",
},
},
kustomization2: &kustomizev1.Kustomization{
ObjectMeta: metav1.ObjectMeta{
Name: "podinfo",
Namespace: "flux-system",
},
},
expected: true,
},
{
name: "wrong name",
kustomization1: &kustomizev1.Kustomization{
ObjectMeta: metav1.ObjectMeta{
Name: "podinfo",
Namespace: "flux-system",
},
},
kustomization2: &kustomizev1.Kustomization{
ObjectMeta: metav1.ObjectMeta{
Name: "my-app",
Namespace: "flux-system",
},
},
expected: false,
},
{
name: "wrong namespace",
kustomization1: &kustomizev1.Kustomization{
ObjectMeta: metav1.ObjectMeta{
Name: "podinfo",
Namespace: "flux-system",
},
},
kustomization2: &kustomizev1.Kustomization{
ObjectMeta: metav1.ObjectMeta{
Name: "podinfo",
Namespace: "my-ns",
},
},
expected: false,
},
{
name: "wrong name and namespace",
kustomization1: &kustomizev1.Kustomization{
ObjectMeta: metav1.ObjectMeta{
Name: "podinfo",
Namespace: "flux-system",
},
},
kustomization2: &kustomizev1.Kustomization{
ObjectMeta: metav1.ObjectMeta{
Name: "my-app",
Namespace: "my-ns",
},
},
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
actual := kustomizationsEqual(tt.kustomization1, tt.kustomization2)
if actual != tt.expected {
t.Fatalf("got '%v', want '%v'", actual, tt.expected)
}
})
}
}
func Test_kustomizationPath(t *testing.T) {
tests := []struct {
name string
kustomization *kustomizev1.Kustomization
expected string
wantErr bool
errString string
}{
{
name: "full repo",
kustomization: &kustomizev1.Kustomization{
ObjectMeta: metav1.ObjectMeta{
Name: "my-app",
Namespace: "flux-system",
},
Spec: kustomizev1.KustomizationSpec{
Path: "my-path",
SourceRef: kustomizev1.CrossNamespaceSourceReference{
Kind: "GitRepository",
Name: "my-repo",
Namespace: "flux-system",
},
},
},
expected: "path/to/local/git/my-path",
},
{
name: "repo without namespace",
kustomization: &kustomizev1.Kustomization{
ObjectMeta: metav1.ObjectMeta{
Name: "my-app",
Namespace: "flux-system",
},
Spec: kustomizev1.KustomizationSpec{
Path: "my-path",
SourceRef: kustomizev1.CrossNamespaceSourceReference{
Kind: "GitRepository",
Name: "my-repo",
Namespace: "",
},
},
},
expected: "path/to/local/git/my-path",
},
{
name: "repo not found",
kustomization: &kustomizev1.Kustomization{
ObjectMeta: metav1.ObjectMeta{
Name: "my-app",
Namespace: "flux-system",
},
Spec: kustomizev1.KustomizationSpec{
Path: "my-path",
SourceRef: kustomizev1.CrossNamespaceSourceReference{
Kind: "GitRepository",
Name: "my-repo",
Namespace: "my-ns",
},
},
},
wantErr: true,
errString: "cannot get local path",
},
}
b := &Builder{
name: "podinfo",
namespace: "flux-system",
localSources: map[string]string{
"GitRepository/flux-system/my-repo": "./path/to/local/git",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
actual, err := b.kustomizationPath(tt.kustomization)
if !tt.wantErr {
if err != nil {
t.Fatalf("unexpected err '%s'", err)
}
if actual != tt.expected {
t.Errorf("got '%v', want '%v'", actual, tt.expected)
}
} else {
if err == nil {
t.Fatal("expected error but got nil")
}
if !strings.Contains(err.Error(), tt.errString) {
t.Errorf("expected error '%s' to contain string '%s'", err.Error(), tt.errString)
}
}
})
}
}

@ -57,6 +57,22 @@ func (b *Builder) Manager() (*ssa.ResourceManager, error) {
} }
func (b *Builder) Diff() (string, bool, 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{} output := strings.Builder{}
createdOrDrifted := false createdOrDrifted := false
objects, err := b.Build() objects, err := b.Build()
@ -77,11 +93,6 @@ func (b *Builder) Diff() (string, bool, error) {
ctx, cancel := context.WithTimeout(context.Background(), b.timeout) ctx, cancel := context.WithTimeout(context.Background(), b.timeout)
defer cancel() defer cancel()
err = b.startSpinner()
if err != nil {
return "", false, err
}
var diffErrs []error var diffErrs []error
// create an inventory of objects to be reconciled // create an inventory of objects to be reconciled
newInventory := newInventory() newInventory := newInventory()
@ -127,6 +138,30 @@ func (b *Builder) Diff() (string, bool, error) {
} }
addObjectsToInventory(newInventory, change) addObjectsToInventory(newInventory, change)
if b.recursive && isKustomization(obj) && change.Action != ssa.CreatedAction {
k, err := toKustomization(obj)
if err != nil {
return "", createdOrDrifted, err
}
if !kustomizationsEqual(k, b.kustomization) {
subOutput, subCreatedOrDrifted, err := b.kustomizationDiff(k)
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 { if b.spinner != nil {
@ -149,12 +184,43 @@ func (b *Builder) Diff() (string, bool, error) {
} }
} }
err = b.stopSpinner() return output.String(), createdOrDrifted, errors.Reduce(errors.Flatten(errors.NewAggregate(diffErrs)))
}
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),
WithNamespace(kustomization.Namespace),
WithIgnore(b.ignore),
WithStrictSubstitute(b.strictSubst),
WithRecursive(b.recursive),
WithLocalSources(b.localSources),
WithSingleKustomization(),
)
if err != nil { if err != nil {
return "", createdOrDrifted, err return "", false, err
} }
return output.String(), createdOrDrifted, errors.Reduce(errors.Flatten(errors.NewAggregate(diffErrs))) return subBuilder.diff()
} }
func writeYamls(liveObject, mergedObject *unstructured.Unstructured) (string, string, string, error) { func writeYamls(liveObject, mergedObject *unstructured.Unstructured) (string, string, string, error) {

Loading…
Cancel
Save