From 3e0eb31dcc974b1d08fa3cff57533d666acfc669 Mon Sep 17 00:00:00 2001 From: rycli Date: Sun, 29 Mar 2026 21:24:39 +0200 Subject: [PATCH] refactor: use new filesys/fs_memory for in-memory layer Signed-off-by: rycli --- go.mod | 2 +- go.sum | 4 +- internal/build/build.go | 94 ++++++++++++-------------- internal/build/build_test.go | 123 ++++++++++++++++++++++------------- internal/build/diff.go | 2 +- 5 files changed, 124 insertions(+), 101 deletions(-) diff --git a/go.mod b/go.mod index ee440ed0..fbd8d80b 100644 --- a/go.mod +++ b/go.mod @@ -268,4 +268,4 @@ require ( 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-20260329153243-0671cee33351 +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 4d52455b..5a7d37bd 100644 --- a/go.sum +++ b/go.sum @@ -502,8 +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-20260329153243-0671cee33351 h1:HqutfZNQ2KtDfrgryyjBOmVTRW8c4T2LXNxpDYOL2q8= -github.com/rycli/fluxcd-pkg/kustomize v0.0.0-20260329153243-0671cee33351/go.mod h1:cW08mnngSP8MJYb6mDmMvxH8YjNATdiML0udb37dk+M= +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 db9a8f8e..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,17 +66,17 @@ const ( var defaultTimeout = 80 * time.Second -// buildBackend controls how the kustomization manifest is generated +// fsBackend controls how the kustomization manifest is generated // and which filesystem is used for the kustomize build. -type buildBackend interface { +type fsBackend interface { Generate(gen *kustomize.Generator, dirPath string) (filesys.FileSystem, string, kustomize.Action, error) Cleanup(dirPath string, action kustomize.Action) error } -// onDiskBackend writes to the source directory, matching upstream behaviour. -type onDiskBackend struct{} +// onDiskFsBackend writes to the source directory. +type onDiskFsBackend struct{} -func (onDiskBackend) Generate(gen *kustomize.Generator, dirPath string) (filesys.FileSystem, string, kustomize.Action, error) { +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 @@ -83,33 +84,46 @@ func (onDiskBackend) Generate(gen *kustomize.Generator, dirPath string) (filesys return filesys.MakeFsOnDisk(), dirPath, action, nil } -func (onDiskBackend) Cleanup(dirPath string, action kustomize.Action) error { +func (onDiskFsBackend) Cleanup(dirPath string, action kustomize.Action) error { return kustomize.CleanDirectory(dirPath, action) } -const memFSRoot = "/work" +// inMemoryFsBackend builds in an in-memory filesystem without modifying the source directory. +type inMemoryFsBackend struct{} -// inMemoryBackend builds in an in-memory filesystem without modifying the source directory. -type inMemoryBackend struct{} - -func (inMemoryBackend) Generate(gen *kustomize.Generator, dirPath string) (filesys.FileSystem, string, kustomize.Action, error) { +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 } - memFS := filesys.MakeFsInMemory() - if err := loadDirToMemFS(dirPath, memFSRoot, memFS); err != nil { - return nil, "", action, fmt.Errorf("failed to load source dir: %w", 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) } - if err := memFS.WriteFile(filepath.Join(memFSRoot, filepath.Base(kfilePath)), manifest); err != nil { + 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 memFS, memFSRoot, action, nil + return fs, absDirPath, action, nil } -func (inMemoryBackend) Cleanup(string, kustomize.Action) error { return nil } +func (inMemoryFsBackend) Cleanup(string, kustomize.Action) error { return nil } // Builder builds yaml manifests // It retrieves the kustomization object from the k8s cluster @@ -134,7 +148,7 @@ type Builder struct { localSources map[string]string // diff needs to handle kustomizations one by one singleKustomization bool - backend buildBackend + fsBackend fsBackend } // BuilderOptionFunc is a function that configures a Builder @@ -249,7 +263,7 @@ func WithLocalSources(localSources map[string]string) BuilderOptionFunc { func WithInMemoryBuild(inMemoryBuild bool) BuilderOptionFunc { return func(b *Builder) error { if inMemoryBuild { - b.backend = inMemoryBackend{} + b.fsBackend = inMemoryFsBackend{} } return nil } @@ -280,10 +294,10 @@ func withSpinnerFrom(in *Builder) BuilderOptionFunc { } } -// withBackend sets the build backend -func withBackend(s buildBackend) BuilderOptionFunc { +// withFsBackend sets the build backend +func withFsBackend(s fsBackend) BuilderOptionFunc { return func(b *Builder) error { - b.backend = s + b.fsBackend = s return nil } } @@ -323,8 +337,8 @@ func NewBuilder(name, resources string, opts ...BuilderOptionFunc) (*Builder, er b.timeout = defaultTimeout } - if b.backend == nil { - b.backend = onDiskBackend{} + if b.fsBackend == nil { + b.fsBackend = onDiskFsBackend{} } if b.dryRun && b.kustomizationFile == "" && b.kustomization == nil { @@ -449,7 +463,7 @@ func (b *Builder) build() (m resmap.ResMap, err error) { // generate kustomization.yaml if needed buildFS, buildDir, action, er := b.generate(*k, b.resourcesPath) if er != nil { - errf := b.backend.Cleanup(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 } @@ -457,7 +471,7 @@ func (b *Builder) build() (m resmap.ResMap, err error) { b.action = action defer func() { - errf := b.backend.Cleanup(b.resourcesPath, b.action) + errf := b.fsBackend.Cleanup(b.resourcesPath, b.action) if err == nil { err = errf } @@ -505,7 +519,7 @@ func (b *Builder) kustomizationBuild(k *kustomizev1.Kustomization) ([]*unstructu WithRecursive(b.recursive), WithLocalSources(b.localSources), WithDryRun(b.dryRun), - withBackend(b.backend), + withFsBackend(b.fsBackend), ) if err != nil { return nil, err @@ -575,29 +589,7 @@ func (b *Builder) generate(kustomization kustomizev1.Kustomization, dirPath stri b.mu.Lock() defer b.mu.Unlock() - return b.backend.Generate(gen, dirPath) -} - -// loadDirToMemFS copies srcDir into dstDir on the given filesystem. -func loadDirToMemFS(srcDir, dstDir string, fs filesys.FileSystem) error { - return filepath.Walk(srcDir, func(p string, info os.FileInfo, err error) error { - if err != nil { - return err - } - rel, err := filepath.Rel(srcDir, p) - if err != nil { - return err - } - target := filepath.Join(dstDir, rel) - if info.IsDir() { - return fs.MkdirAll(target) - } - data, err := os.ReadFile(p) - if err != nil { - return err - } - return fs.WriteFile(target, data) - }) + return b.fsBackend.Generate(gen, dirPath) } func (b *Builder) do(ctx context.Context, kustomization kustomizev1.Kustomization, fs filesys.FileSystem, dirPath string) (resmap.ResMap, error) { @@ -824,7 +816,7 @@ func (b *Builder) Cancel() error { b.mu.Lock() defer b.mu.Unlock() - return b.backend.Cleanup(b.resourcesPath, b.action) + 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 bbd0aa6d..fa7ffff3 100644 --- a/internal/build/build_test.go +++ b/internal/build/build_test.go @@ -31,7 +31,6 @@ import ( 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/kyaml/filesys" "sigs.k8s.io/kustomize/kyaml/yaml" ) @@ -616,46 +615,22 @@ func Test_kustomizationPath(t *testing.T) { } } -func Test_loadDirToMemFS(t *testing.T) { - srcDir := t.TempDir() - - nested := filepath.Join(srcDir, "nested") - if err := os.MkdirAll(nested, 0o755); err != nil { - t.Fatalf("mkdir: %v", err) +// 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.WriteFile(filepath.Join(nested, "file.yaml"), []byte("nested-content"), 0o644); err != nil { - t.Fatalf("write: %v", err) - } - if err := os.WriteFile(filepath.Join(srcDir, "root.yaml"), []byte("root-content"), 0o644); err != nil { - t.Fatalf("write: %v", err) - } - - memFS := filesys.MakeFsInMemory() - dst := "/target" - if err := loadDirToMemFS(srcDir, dst, memFS); err != nil { - t.Fatalf("loadDirToMemFS: %v", err) - } - - tests := []struct { - path string - content string - }{ - {filepath.Join(dst, "root.yaml"), "root-content"}, - {filepath.Join(dst, "nested", "file.yaml"), "nested-content"}, - } - for _, tt := range tests { - data, err := memFS.ReadFile(tt.path) - if err != nil { - t.Fatalf("ReadFile(%s): %v", tt.path, err) - } - if diff := cmp.Diff(string(data), tt.content); diff != "" { - t.Errorf("content mismatch for %s: (-got +want)%s", tt.path, diff) - } + if err := os.Chdir(dir); err != nil { + t.Fatal(err) } + t.Cleanup(func() { os.Chdir(orig) }) } -func Test_inMemoryBackend_Generate(t *testing.T) { +func Test_inMemoryFsBackend_Generate(t *testing.T) { srcDir := t.TempDir() + chdirTemp(t, srcDir) kusYAML := `apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization @@ -698,21 +673,18 @@ data: }} gen := kustomize.NewGenerator(srcDir, ks) - backend := inMemoryBackend{} + backend := inMemoryFsBackend{} fs, dir, action, err := backend.Generate(gen, srcDir) if err != nil { t.Fatalf("Generate: %v", err) } - if dir != memFSRoot { - t.Errorf("expected dir %q, got %q", memFSRoot, dir) - } if action != kustomize.UnchangedAction { t.Errorf("expected UnchangedAction, got %q", action) } // kustomization.yaml should contain the merged targetNamespace - data, err := fs.ReadFile(filepath.Join(memFSRoot, "kustomization.yaml")) + data, err := fs.ReadFile(filepath.Join(dir, "kustomization.yaml")) if err != nil { t.Fatalf("ReadFile kustomization.yaml: %v", err) } @@ -720,8 +692,8 @@ data: t.Errorf("expected kustomization to contain targetNamespace, got:\n%s", data) } - // resource file should be copied verbatim - data, err = fs.ReadFile(filepath.Join(memFSRoot, "configmap.yaml")) + // 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) } @@ -745,11 +717,12 @@ data: } } -func Test_inMemoryBackend_Generate_parentRef(t *testing.T) { +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 @@ -786,13 +759,13 @@ resources: }} gen := kustomize.NewGenerator(overlayDir, ks) - backend := inMemoryBackend{} + backend := inMemoryFsBackend{} fs, dir, _, err := backend.Generate(gen, overlayDir) if err != nil { t.Fatalf("Generate: %v", err) } - // ../../configmap.yaml must resolve through the overlay + // ../../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) @@ -809,3 +782,61 @@ resources: 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 4f32bddf..8884e57f 100644 --- a/internal/build/diff.go +++ b/internal/build/diff.go @@ -230,7 +230,7 @@ func (b *Builder) kustomizationDiff(kustomization *kustomizev1.Kustomization) (s WithRecursive(b.recursive), WithLocalSources(b.localSources), WithSingleKustomization(), - withBackend(b.backend), + withFsBackend(b.fsBackend), ) if err != nil { return "", false, err