refactor: use new filesys/fs_memory for in-memory layer

Signed-off-by: rycli <cyril@ryc.li>
This commit is contained in:
rycli 2026-03-29 21:24:39 +02:00 committed by Cyril M.
parent cd3bb2d612
commit 3e0eb31dcc
5 changed files with 124 additions and 101 deletions

2
go.mod
View file

@ -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

4
go.sum
View file

@ -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=

View file

@ -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 {

View file

@ -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")
}
}

View file

@ -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