diff --git a/cmd/flux/build_kustomization.go b/cmd/flux/build_kustomization.go index 2000faac..7827f7c9 100644 --- a/cmd/flux/build_kustomization.go +++ b/cmd/flux/build_kustomization.go @@ -72,6 +72,7 @@ type buildKsFlags struct { strictSubst bool recursive bool localSources map[string]string + inMemoryBuild bool } var buildKsArgs buildKsFlags @@ -85,6 +86,8 @@ func init() { "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") + buildKsCmd.Flags().BoolVar(&buildKsArgs.inMemoryBuild, "in-memory-build", false, + "Use in-memory filesystem during build.") buildCmd.AddCommand(buildKsCmd) } @@ -130,6 +133,7 @@ func buildKsCmdRun(cmd *cobra.Command, args []string) (err error) { build.WithStrictSubstitute(buildKsArgs.strictSubst), build.WithRecursive(buildKsArgs.recursive), build.WithLocalSources(buildKsArgs.localSources), + build.WithInMemoryBuild(buildKsArgs.inMemoryBuild), ) } else { builder, err = build.NewBuilder(name, buildKsArgs.path, @@ -140,6 +144,7 @@ func buildKsCmdRun(cmd *cobra.Command, args []string) (err error) { build.WithStrictSubstitute(buildKsArgs.strictSubst), build.WithRecursive(buildKsArgs.recursive), build.WithLocalSources(buildKsArgs.localSources), + build.WithInMemoryBuild(buildKsArgs.inMemoryBuild), ) } diff --git a/cmd/flux/build_kustomization_test.go b/cmd/flux/build_kustomization_test.go index 7b49506e..b9fde3c0 100644 --- a/cmd/flux/build_kustomization_test.go +++ b/cmd/flux/build_kustomization_test.go @@ -52,6 +52,12 @@ func TestBuildKustomization(t *testing.T) { resultFile: "./testdata/build-kustomization/podinfo-result.yaml", assertFunc: "assertGoldenTemplateFile", }, + { + name: "build podinfo with in-memory-build", + args: "build kustomization podinfo --path ./testdata/build-kustomization/podinfo --in-memory-build", + resultFile: "./testdata/build-kustomization/podinfo-result.yaml", + assertFunc: "assertGoldenTemplateFile", + }, { name: "build podinfo without service", args: "build kustomization podinfo --path ./testdata/build-kustomization/delete-service", @@ -70,12 +76,24 @@ func TestBuildKustomization(t *testing.T) { resultFile: "./testdata/build-kustomization/podinfo-with-ignore-result.yaml", assertFunc: "assertGoldenTemplateFile", }, + { + name: "build ignore with in-memory-build", + args: "build kustomization podinfo --path ./testdata/build-kustomization/ignore --ignore-paths \"!configmap.yaml,!secret.yaml\" --in-memory-build", + resultFile: "./testdata/build-kustomization/podinfo-with-ignore-result.yaml", + 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", }, + { + name: "build with recursive and in-memory-build", + args: "build kustomization podinfo --path ./testdata/build-kustomization/podinfo-with-my-app --recursive --local-sources GitRepository/default/podinfo=./testdata/build-kustomization --in-memory-build", + resultFile: "./testdata/build-kustomization/podinfo-with-my-app-result.yaml", + assertFunc: "assertGoldenTemplateFile", + }, } tmpl := map[string]string{ @@ -145,6 +163,12 @@ spec: resultFile: "./testdata/build-kustomization/podinfo-result.yaml", assertFunc: "assertGoldenTemplateFile", }, + { + name: "build podinfo with in-memory-build", + args: "build kustomization podinfo --kustomization-file " + tmpFile + " --path ./testdata/build-kustomization/podinfo --in-memory-build", + resultFile: "./testdata/build-kustomization/podinfo-result.yaml", + assertFunc: "assertGoldenTemplateFile", + }, { name: "build podinfo without service", args: "build kustomization podinfo --kustomization-file " + tmpFile + " --path ./testdata/build-kustomization/delete-service", @@ -175,6 +199,18 @@ spec: resultFile: "./testdata/build-kustomization/podinfo-with-my-app-result.yaml", assertFunc: "assertGoldenTemplateFile", }, + { + name: "build with recursive and in-memory-build", + args: "build kustomization podinfo --kustomization-file " + tmpFile + " --path ./testdata/build-kustomization/podinfo-with-my-app --recursive --local-sources GitRepository/default/podinfo=./testdata/build-kustomization --in-memory-build", + resultFile: "./testdata/build-kustomization/podinfo-with-my-app-result.yaml", + assertFunc: "assertGoldenTemplateFile", + }, + { + name: "build with recursive and in-memory-build in dry-run mode", + args: "build kustomization podinfo --kustomization-file " + tmpFile + " --path ./testdata/build-kustomization/podinfo-with-my-app --recursive --local-sources GitRepository/default/podinfo=./testdata/build-kustomization --in-memory-build --dry-run", + resultFile: "./testdata/build-kustomization/podinfo-with-my-app-result.yaml", + assertFunc: "assertGoldenTemplateFile", + }, } tmpl := map[string]string{ @@ -241,6 +277,12 @@ func TestBuildKustomizationPathNormalization(t *testing.T) { resultFile: "./testdata/build-kustomization/podinfo-result.yaml", assertFunc: "assertGoldenTemplateFile", }, + { + name: "build with absolute path and in-memory-build", + args: "build kustomization podinfo --path " + absTestDataPath + " --in-memory-build", + resultFile: "./testdata/build-kustomization/podinfo-result.yaml", + assertFunc: "assertGoldenTemplateFile", + }, { name: "build with complex relative path (parent dir)", args: "build kustomization podinfo --path ./testdata/build-kustomization/../build-kustomization/podinfo", diff --git a/cmd/flux/diff_kustomization.go b/cmd/flux/diff_kustomization.go index 0480e293..6b9aa08a 100644 --- a/cmd/flux/diff_kustomization.go +++ b/cmd/flux/diff_kustomization.go @@ -62,6 +62,7 @@ type diffKsFlags struct { strictSubst bool recursive bool localSources map[string]string + inMemoryBuild bool } var diffKsArgs diffKsFlags @@ -75,6 +76,8 @@ func init() { "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") + diffKsCmd.Flags().BoolVar(&diffKsArgs.inMemoryBuild, "in-memory-build", false, + "Use in-memory filesystem during build.") diffCmd.AddCommand(diffKsCmd) } @@ -113,6 +116,7 @@ func diffKsCmdRun(cmd *cobra.Command, args []string) error { build.WithRecursive(diffKsArgs.recursive), build.WithLocalSources(diffKsArgs.localSources), build.WithSingleKustomization(), + build.WithInMemoryBuild(diffKsArgs.inMemoryBuild), ) } else { builder, err = build.NewBuilder(name, diffKsArgs.path, @@ -124,6 +128,7 @@ func diffKsCmdRun(cmd *cobra.Command, args []string) error { build.WithRecursive(diffKsArgs.recursive), build.WithLocalSources(diffKsArgs.localSources), build.WithSingleKustomization(), + build.WithInMemoryBuild(diffKsArgs.inMemoryBuild), ) } diff --git a/go.mod b/go.mod index 7554accd..fbd8d80b 100644 --- a/go.mod +++ b/go.mod @@ -267,3 +267,5 @@ require ( sigs.k8s.io/randfill v1.0.0 // indirect sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482 // indirect ) + +replace github.com/fluxcd/pkg/kustomize => github.com/rycli/fluxcd-pkg/kustomize v0.0.0-20260329192052-94b031e1aca6 diff --git a/go.sum b/go.sum index 3aed1cda..5a7d37bd 100644 --- a/go.sum +++ b/go.sum @@ -204,8 +204,6 @@ github.com/fluxcd/pkg/git v0.46.0 h1:QMh0+ZzQ2jO6rIGj4ffR5trZ8g/cxvt8cVajReJ8Iyw github.com/fluxcd/pkg/git v0.46.0/go.mod h1:iHcIjx9c8zye3PQiajTJYxgOMRiy7WCs+hfLKDswpfI= github.com/fluxcd/pkg/gittestserver v0.26.0 h1:+RZrCzFRsE+d5WaqAoqaPCEgcgv/jZp6+f7DS0+Ynb8= github.com/fluxcd/pkg/gittestserver v0.26.0/go.mod h1:7fybYb0yej1fFNiF1ohs0Jr0XzyaZQ/cRh3AFEoCtuc= -github.com/fluxcd/pkg/kustomize v1.28.0 h1:0RuFVczJRabbt8frHZ/ql8aqte6BOOKk274O09l6/hE= -github.com/fluxcd/pkg/kustomize v1.28.0/go.mod h1:cW08mnngSP8MJYb6mDmMvxH8YjNATdiML0udb37dk+M= github.com/fluxcd/pkg/oci v0.63.0 h1:ZPKTT2C+gWYjhP63xC76iTPdYE9w3ABcsDq77uhAgwo= github.com/fluxcd/pkg/oci v0.63.0/go.mod h1:qMPz4njvm6hJzdyGSb8ydSqrapXxTQwJonxHIsdeXSQ= github.com/fluxcd/pkg/runtime v0.103.0 h1:J5y5GPhWdkyqIUBlaI1FP2N02TtZmsjbWhhZubuTSFk= @@ -504,6 +502,8 @@ github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0t github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/rycli/fluxcd-pkg/kustomize v0.0.0-20260329192052-94b031e1aca6 h1:a231NZKoN+nuPkqivk8u0kKA6OaWdB30CCurQigx/Tg= +github.com/rycli/fluxcd-pkg/kustomize v0.0.0-20260329192052-94b031e1aca6/go.mod h1:cW08mnngSP8MJYb6mDmMvxH8YjNATdiML0udb37dk+M= github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= diff --git a/internal/build/build.go b/internal/build/build.go index 7deca47c..6f1b44c5 100644 --- a/internal/build/build.go +++ b/internal/build/build.go @@ -45,6 +45,7 @@ import ( kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" "github.com/fluxcd/pkg/kustomize" + buildfs "github.com/fluxcd/pkg/kustomize/filesys" runclient "github.com/fluxcd/pkg/runtime/client" ssautil "github.com/fluxcd/pkg/ssa/utils" "sigs.k8s.io/kustomize/kyaml/filesys" @@ -65,6 +66,65 @@ const ( var defaultTimeout = 80 * time.Second +// fsBackend controls how the kustomization manifest is generated +// and which filesystem is used for the kustomize build. +type fsBackend interface { + Generate(gen *kustomize.Generator, dirPath string) (filesys.FileSystem, string, kustomize.Action, error) + Cleanup(dirPath string, action kustomize.Action) error +} + +// onDiskFsBackend writes to the source directory. +type onDiskFsBackend struct{} + +func (onDiskFsBackend) Generate(gen *kustomize.Generator, dirPath string) (filesys.FileSystem, string, kustomize.Action, error) { + action, err := gen.WriteFile(dirPath, kustomize.WithSaveOriginalKustomization()) + if err != nil { + return nil, "", action, err + } + return filesys.MakeFsOnDisk(), dirPath, action, nil +} + +func (onDiskFsBackend) Cleanup(dirPath string, action kustomize.Action) error { + return kustomize.CleanDirectory(dirPath, action) +} + +// inMemoryFsBackend builds in an in-memory filesystem without modifying the source directory. +type inMemoryFsBackend struct{} + +func (inMemoryFsBackend) Generate(gen *kustomize.Generator, dirPath string) (filesys.FileSystem, string, kustomize.Action, error) { + manifest, kfilePath, action, err := gen.GenerateManifest(dirPath) + if err != nil { + return nil, "", action, err + } + + absDirPath, err := filepath.Abs(dirPath) + if err != nil { + return nil, "", action, fmt.Errorf("failed to resolve dirPath: %w", err) + } + absDirPath, err = filepath.EvalSymlinks(absDirPath) + if err != nil { + return nil, "", action, fmt.Errorf("failed to eval symlinks: %w", err) + } + + cwd, err := os.Getwd() + if err != nil { + return nil, "", action, fmt.Errorf("failed to get working directory: %w", err) + } + + diskFS, err := buildfs.MakeFsOnDiskSecure(cwd) + if err != nil { + return nil, "", action, fmt.Errorf("failed to create secure filesystem: %w", err) + } + fs := buildfs.MakeFsInMemory(diskFS) + + if err := fs.WriteFile(filepath.Join(absDirPath, filepath.Base(kfilePath)), manifest); err != nil { + return nil, "", action, err + } + return fs, absDirPath, action, nil +} + +func (inMemoryFsBackend) Cleanup(string, kustomize.Action) error { return nil } + // Builder builds yaml manifests // It retrieves the kustomization object from the k8s cluster // and overlays the manifests with the resources specified in the resourcesPath @@ -88,6 +148,7 @@ type Builder struct { localSources map[string]string // diff needs to handle kustomizations one by one singleKustomization bool + fsBackend fsBackend } // BuilderOptionFunc is a function that configures a Builder @@ -198,6 +259,16 @@ func WithLocalSources(localSources map[string]string) BuilderOptionFunc { } } +// WithInMemoryBuild sets the in-memory build backend +func WithInMemoryBuild(inMemoryBuild bool) BuilderOptionFunc { + return func(b *Builder) error { + if inMemoryBuild { + b.fsBackend = inMemoryFsBackend{} + } + return nil + } +} + // WithSingleKustomization sets the single kustomization field to true func WithSingleKustomization() BuilderOptionFunc { return func(b *Builder) error { @@ -223,6 +294,14 @@ func withSpinnerFrom(in *Builder) BuilderOptionFunc { } } +// withFsBackend sets the build backend +func withFsBackend(s fsBackend) BuilderOptionFunc { + return func(b *Builder) error { + b.fsBackend = s + return nil + } +} + // withKustomization sets the kustomization field func withKustomization(k *kustomizev1.Kustomization) BuilderOptionFunc { return func(b *Builder) error { @@ -258,6 +337,10 @@ func NewBuilder(name, resources string, opts ...BuilderOptionFunc) (*Builder, er b.timeout = defaultTimeout } + if b.fsBackend == nil { + b.fsBackend = onDiskFsBackend{} + } + if b.dryRun && b.kustomizationFile == "" && b.kustomization == nil { return nil, fmt.Errorf("kustomization file is required for dry-run") } @@ -378,9 +461,9 @@ func (b *Builder) build() (m resmap.ResMap, err error) { b.kustomization = k // generate kustomization.yaml if needed - action, er := b.generate(*k, b.resourcesPath) + buildFS, buildDir, action, er := b.generate(*k, b.resourcesPath) if er != nil { - errf := kustomize.CleanDirectory(b.resourcesPath, action) + errf := b.fsBackend.Cleanup(b.resourcesPath, action) err = fmt.Errorf("failed to generate kustomization.yaml: %w", fmt.Errorf("%v %v", er, errf)) return } @@ -388,14 +471,14 @@ func (b *Builder) build() (m resmap.ResMap, err error) { b.action = action defer func() { - errf := b.Cancel() + errf := b.fsBackend.Cleanup(b.resourcesPath, b.action) if err == nil { err = errf } }() // build the kustomization - m, err = b.do(ctx, *k, b.resourcesPath) + m, err = b.do(ctx, *k, buildFS, buildDir) if err != nil { return } @@ -436,6 +519,7 @@ func (b *Builder) kustomizationBuild(k *kustomizev1.Kustomization) ([]*unstructu WithRecursive(b.recursive), WithLocalSources(b.localSources), WithDryRun(b.dryRun), + withFsBackend(b.fsBackend), ) if err != nil { return nil, err @@ -490,10 +574,10 @@ func (b *Builder) unMarshallKustomization() (*kustomizev1.Kustomization, error) return k, nil } -func (b *Builder) generate(kustomization kustomizev1.Kustomization, dirPath string) (kustomize.Action, error) { +func (b *Builder) generate(kustomization kustomizev1.Kustomization, dirPath string) (filesys.FileSystem, string, kustomize.Action, error) { data, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&kustomization) if err != nil { - return "", err + return nil, "", kustomize.UnchangedAction, err } // a scanner will be used down the line to parse the list @@ -505,12 +589,10 @@ func (b *Builder) generate(kustomization kustomizev1.Kustomization, dirPath stri b.mu.Lock() defer b.mu.Unlock() - return gen.WriteFile(dirPath, kustomize.WithSaveOriginalKustomization()) + return b.fsBackend.Generate(gen, dirPath) } -func (b *Builder) do(ctx context.Context, kustomization kustomizev1.Kustomization, dirPath string) (resmap.ResMap, error) { - fs := filesys.MakeFsOnDisk() - +func (b *Builder) do(ctx context.Context, kustomization kustomizev1.Kustomization, fs filesys.FileSystem, dirPath string) (resmap.ResMap, error) { // acquire the lock b.mu.Lock() defer b.mu.Unlock() @@ -734,12 +816,7 @@ func (b *Builder) Cancel() error { b.mu.Lock() defer b.mu.Unlock() - err := kustomize.CleanDirectory(b.resourcesPath, b.action) - if err != nil { - return err - } - - return nil + return b.fsBackend.Cleanup(b.resourcesPath, b.action) } func (b *Builder) StartSpinner() error { diff --git a/internal/build/build_test.go b/internal/build/build_test.go index bf82513f..fa7ffff3 100644 --- a/internal/build/build_test.go +++ b/internal/build/build_test.go @@ -18,12 +18,15 @@ package build import ( "fmt" + "os" + "path/filepath" "strings" "testing" "time" kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" "github.com/fluxcd/pkg/apis/meta" + "github.com/fluxcd/pkg/kustomize" "github.com/google/go-cmp/cmp" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -611,3 +614,229 @@ func Test_kustomizationPath(t *testing.T) { }) } } + +// chdirTemp changes to the given directory and restores the original on cleanup. +func chdirTemp(t *testing.T, dir string) { + t.Helper() + orig, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + if err := os.Chdir(dir); err != nil { + t.Fatal(err) + } + t.Cleanup(func() { os.Chdir(orig) }) +} + +func Test_inMemoryFsBackend_Generate(t *testing.T) { + srcDir := t.TempDir() + chdirTemp(t, srcDir) + + kusYAML := `apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: +- configmap.yaml +` + cmYAML := `apiVersion: v1 +kind: ConfigMap +metadata: + name: test-cm +data: + key: value +` + if err := os.WriteFile(filepath.Join(srcDir, "kustomization.yaml"), []byte(kusYAML), 0o644); err != nil { + t.Fatalf("write: %v", err) + } + if err := os.WriteFile(filepath.Join(srcDir, "configmap.yaml"), []byte(cmYAML), 0o644); err != nil { + t.Fatalf("write: %v", err) + } + + // snapshot source dir + beforeFiles := map[string]string{} + filepath.Walk(srcDir, func(p string, info os.FileInfo, err error) error { + if err != nil || info.IsDir() { + return err + } + data, _ := os.ReadFile(p) + rel, _ := filepath.Rel(srcDir, p) + beforeFiles[rel] = string(data) + return nil + }) + + ks := unstructured.Unstructured{Object: map[string]interface{}{ + "apiVersion": "kustomize.toolkit.fluxcd.io/v1", + "kind": "Kustomization", + "metadata": map[string]interface{}{"name": "test", "namespace": "default"}, + "spec": map[string]interface{}{ + "targetNamespace": "my-ns", + }, + }} + gen := kustomize.NewGenerator(srcDir, ks) + + backend := inMemoryFsBackend{} + fs, dir, action, err := backend.Generate(gen, srcDir) + if err != nil { + t.Fatalf("Generate: %v", err) + } + + if action != kustomize.UnchangedAction { + t.Errorf("expected UnchangedAction, got %q", action) + } + + // kustomization.yaml should contain the merged targetNamespace + data, err := fs.ReadFile(filepath.Join(dir, "kustomization.yaml")) + if err != nil { + t.Fatalf("ReadFile kustomization.yaml: %v", err) + } + if !strings.Contains(string(data), "my-ns") { + t.Errorf("expected kustomization to contain targetNamespace, got:\n%s", data) + } + + // resource file should be readable from disk through the memory fs + data, err = fs.ReadFile(filepath.Join(dir, "configmap.yaml")) + if err != nil { + t.Fatalf("ReadFile configmap.yaml: %v", err) + } + if diff := cmp.Diff(string(data), cmYAML); diff != "" { + t.Errorf("configmap mismatch: (-got +want)%s", diff) + } + + // source directory must be unmodified + afterFiles := map[string]string{} + filepath.Walk(srcDir, func(p string, info os.FileInfo, err error) error { + if err != nil || info.IsDir() { + return err + } + data, _ := os.ReadFile(p) + rel, _ := filepath.Rel(srcDir, p) + afterFiles[rel] = string(data) + return nil + }) + if diff := cmp.Diff(afterFiles, beforeFiles); diff != "" { + t.Errorf("source directory was modified: (-got +want)%s", diff) + } +} + +func Test_inMemoryFsBackend_Generate_parentRef(t *testing.T) { + // tmpDir/ + // configmap.yaml (referenced as ../../configmap.yaml) + // overlay/sub/kustomization.yaml + tmpDir := t.TempDir() + chdirTemp(t, tmpDir) + + cmYAML := `apiVersion: v1 +kind: ConfigMap +metadata: + name: parent-cm +data: + key: value +` + if err := os.WriteFile(filepath.Join(tmpDir, "configmap.yaml"), []byte(cmYAML), 0o644); err != nil { + t.Fatalf("write: %v", err) + } + + overlayDir := filepath.Join(tmpDir, "overlay", "sub") + if err := os.MkdirAll(overlayDir, 0o755); err != nil { + t.Fatalf("mkdir: %v", err) + } + + kusYAML := `apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: +- ../../configmap.yaml +` + if err := os.WriteFile(filepath.Join(overlayDir, "kustomization.yaml"), []byte(kusYAML), 0o644); err != nil { + t.Fatalf("write: %v", err) + } + + ks := unstructured.Unstructured{Object: map[string]interface{}{ + "apiVersion": "kustomize.toolkit.fluxcd.io/v1", + "kind": "Kustomization", + "metadata": map[string]interface{}{"name": "test", "namespace": "default"}, + "spec": map[string]interface{}{ + "targetNamespace": "parent-ns", + }, + }} + gen := kustomize.NewGenerator(overlayDir, ks) + + backend := inMemoryFsBackend{} + fs, dir, _, err := backend.Generate(gen, overlayDir) + if err != nil { + t.Fatalf("Generate: %v", err) + } + + // ../../configmap.yaml must resolve through the disk layer + m, err := kustomize.Build(fs, dir) + if err != nil { + t.Fatalf("kustomize.Build failed (parent ref not resolved): %v", err) + } + + resources := m.Resources() + if len(resources) != 1 { + t.Fatalf("expected 1 resource, got %d", len(resources)) + } + if resources[0].GetName() != "parent-cm" { + t.Errorf("expected resource name parent-cm, got %s", resources[0].GetName()) + } + if resources[0].GetNamespace() != "parent-ns" { + t.Errorf("expected namespace parent-ns, got %s", resources[0].GetNamespace()) + } +} + +func Test_inMemoryFsBackend_Generate_outsideCwd(t *testing.T) { + // Two sibling temp dirs: one for the source tree, one as cwd. + // The kustomization references a file that exists on disk but is + // outside cwd, so the secure filesystem must reject it. + // + // parentDir/ + // outside/configmap.yaml (exists but outside cwd) + // cwd/overlay/kustomization.yaml (references ../../outside/configmap.yaml) + parentDir := t.TempDir() + + outsideDir := filepath.Join(parentDir, "outside") + if err := os.MkdirAll(outsideDir, 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(outsideDir, "configmap.yaml"), []byte(`apiVersion: v1 +kind: ConfigMap +metadata: + name: outside-cm +`), 0o644); err != nil { + t.Fatal(err) + } + + cwdDir := filepath.Join(parentDir, "cwd") + overlayDir := filepath.Join(cwdDir, "overlay") + if err := os.MkdirAll(overlayDir, 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(overlayDir, "kustomization.yaml"), []byte(`apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: +- ../../outside/configmap.yaml +`), 0o644); err != nil { + t.Fatal(err) + } + + // Set cwd to cwdDir so the secure root excludes outsideDir. + chdirTemp(t, cwdDir) + + ks := unstructured.Unstructured{Object: map[string]interface{}{ + "apiVersion": "kustomize.toolkit.fluxcd.io/v1", + "kind": "Kustomization", + "metadata": map[string]interface{}{"name": "test", "namespace": "default"}, + }} + gen := kustomize.NewGenerator(overlayDir, ks) + + backend := inMemoryFsBackend{} + fs, dir, _, err := backend.Generate(gen, overlayDir) + if err != nil { + t.Fatalf("Generate: %v", err) + } + + // Build must fail because the resource is outside the secure root. + _, err = kustomize.Build(fs, dir) + if err == nil { + t.Fatal("expected error when referencing resource outside cwd, got nil") + } +} diff --git a/internal/build/diff.go b/internal/build/diff.go index 4485dd0f..8884e57f 100644 --- a/internal/build/diff.go +++ b/internal/build/diff.go @@ -230,6 +230,7 @@ func (b *Builder) kustomizationDiff(kustomization *kustomizev1.Kustomization) (s WithRecursive(b.recursive), WithLocalSources(b.localSources), WithSingleKustomization(), + withFsBackend(b.fsBackend), ) if err != nil { return "", false, err