Implement localizer skeleton for kustomize localize (#4859)

* Add localizer outline

* Add localizer unit tests

* include previous change

* Add proper NewLocalizer, Localize unit tests

mock processKust

* Address code review feedback

* Make processKustFn public

* Remove mock testing
This commit is contained in:
Anna Song
2022-11-10 09:24:54 -08:00
committed by GitHub
parent beb2825f82
commit 50e731a1d9
5 changed files with 245 additions and 61 deletions

View File

@@ -0,0 +1,83 @@
// Copyright 2022 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package localizer
import (
"log"
"path/filepath"
"sigs.k8s.io/kustomize/api/ifc"
pLdr "sigs.k8s.io/kustomize/api/internal/plugins/loader"
"sigs.k8s.io/kustomize/api/internal/target"
"sigs.k8s.io/kustomize/api/konfig"
"sigs.k8s.io/kustomize/api/resmap"
"sigs.k8s.io/kustomize/api/types"
"sigs.k8s.io/kustomize/kyaml/errors"
"sigs.k8s.io/kustomize/kyaml/filesys"
"sigs.k8s.io/yaml"
)
// Localizer encapsulates all state needed to localize the root at ldr.
type Localizer struct {
fSys filesys.FileSystem
// kusttarget fields
validator ifc.Validator
rFactory *resmap.Factory
pLdr *pLdr.Loader
// underlying type is LocLoader
ldr ifc.Loader
// destination directory in newDir that mirrors ldr's current root.
dst filesys.ConfirmedDir
}
// NewLocalizer is the factory method for Localizer
func NewLocalizer(ldr *LocLoader, validator ifc.Validator, rFactory *resmap.Factory, pLdr *pLdr.Loader) (*Localizer, error) {
toDst, err := filepath.Rel(ldr.args.Scope.String(), ldr.Root())
if err != nil {
log.Fatalf("cannot find path from directory %q to %q inside directory: %s", ldr.args.Scope.String(),
ldr.Root(), err.Error())
}
dst := ldr.args.NewDir.Join(toDst)
if err = ldr.fSys.MkdirAll(dst); err != nil {
return nil, errors.WrapPrefixf(err, "unable to create directory in localize destination")
}
return &Localizer{
fSys: ldr.fSys,
validator: validator,
rFactory: rFactory,
pLdr: pLdr,
ldr: ldr,
dst: filesys.ConfirmedDir(dst),
}, nil
}
// Localize localizes the root that lc is at
func (lc *Localizer) Localize() error {
kt := target.NewKustTarget(lc.ldr, lc.validator, lc.rFactory, lc.pLdr)
err := kt.Load()
if err != nil {
return errors.Wrap(err)
}
kust := lc.processKust(kt)
content, err := yaml.Marshal(kust)
if err != nil {
return errors.WrapPrefixf(err, "unable to serialize localized kustomization file")
}
if err = lc.fSys.WriteFile(lc.dst.Join(konfig.DefaultKustomizationFileName()), content); err != nil {
return errors.WrapPrefixf(err, "unable to write localized kustomization file")
}
return nil
}
// TODO(annasong): implement
// processKust returns a copy of the kustomization at kt with paths localized.
func (lc *Localizer) processKust(kt *target.KustTarget) *types.Kustomization {
kust := kt.Kustomization()
return &kust
}

View File

@@ -0,0 +1,117 @@
// Copyright 2022 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package localizer_test
import (
"path/filepath"
"testing"
"github.com/stretchr/testify/require"
"sigs.k8s.io/kustomize/api/hasher"
. "sigs.k8s.io/kustomize/api/internal/localizer"
"sigs.k8s.io/kustomize/api/internal/plugins/loader"
"sigs.k8s.io/kustomize/api/internal/validate"
"sigs.k8s.io/kustomize/api/resmap"
"sigs.k8s.io/kustomize/api/resource"
"sigs.k8s.io/kustomize/api/types"
"sigs.k8s.io/kustomize/kyaml/filesys"
)
const podConfiguration = `apiVersion: v1
kind: Pod
metadata:
name: pod
spec:
containers:
- name: nginx
image: nginx:1.14.2
ports:
-containerPort: 80
`
func makeMemoryFs(t *testing.T) filesys.FileSystem {
t.Helper()
req := require.New(t)
fSys := filesys.MakeFsInMemory()
req.NoError(fSys.MkdirAll("/a/b"))
req.NoError(fSys.WriteFile("/a/pod.yaml", []byte(podConfiguration)))
dirChain := "/alpha/beta/gamma/delta"
req.NoError(fSys.MkdirAll(dirChain))
req.NoError(fSys.WriteFile(filepath.Join(dirChain, "deployment.yaml"), []byte("deployment configuration")))
req.NoError(fSys.Mkdir("/alpha/beta/say"))
return fSys
}
func addFiles(t *testing.T, fSys filesys.FileSystem, parentDir string, files map[string]string) {
t.Helper()
// in-memory file system makes all necessary dirs when writing files
for file, content := range files {
require.NoError(t, fSys.WriteFile(filepath.Join(parentDir, file), []byte(content)))
}
}
func createLocalizer(t *testing.T, fSys filesys.FileSystem, target string, scope string, newDir string) *Localizer {
t.Helper()
// no need to re-test LocLoader
ldr, _, err := NewLocLoader(target, scope, newDir, fSys)
require.NoError(t, err)
rmFactory := resmap.NewFactory(resource.NewFactory(&hasher.Hasher{}))
lc, err := NewLocalizer(
ldr,
validate.NewFieldValidator(),
rmFactory,
// file system can be in memory, as plugin configuration will prevent the use of file system anyway
loader.NewLoader(types.DisabledPluginConfig(), rmFactory, fSys))
require.NoError(t, err)
return lc
}
func TestNewLocalizerTargetIsScope(t *testing.T) {
fSys := makeMemoryFs(t)
_ = createLocalizer(t, fSys, "/a", "", "/a/b/dst")
fSysExpected := makeMemoryFs(t)
require.NoError(t, fSysExpected.MkdirAll("/a/b/dst"))
require.Equal(t, fSysExpected, fSys)
}
func TestNewLocalizerTargetNestedInScope(t *testing.T) {
fSys := makeMemoryFs(t)
_ = createLocalizer(t, fSys, "/a/b", "/", "/a/b/dst")
fSysExpected := makeMemoryFs(t)
require.NoError(t, fSysExpected.MkdirAll("/a/b/dst/a/b"))
require.Equal(t, fSysExpected, fSys)
}
func TestLocalizeKustomizationName(t *testing.T) {
fSys := makeMemoryFs(t)
kustomization := map[string]string{
"Kustomization": `apiVersion: kustomize.config.k8s.io/v1beta1
configMapGenerator:
- behavior: create
literals:
- APPLE=orange
name: map
kind: Kustomization
resources:
- pod.yaml
`,
}
addFiles(t, fSys, "/a", kustomization)
lclzr := createLocalizer(t, fSys, "/a", "/", "/dst")
require.NoError(t, lclzr.Localize())
fSysExpected := makeMemoryFs(t)
addFiles(t, fSysExpected, "/a", kustomization)
addFiles(t, fSysExpected, "/dst/a", map[string]string{
"kustomization.yaml": kustomization["Kustomization"],
})
require.Equal(t, fSysExpected, fSys)
}

View File

@@ -14,7 +14,7 @@ import (
"sigs.k8s.io/kustomize/kyaml/filesys"
)
const dstPrefix = "localized"
const DstPrefix = "localized"
// LocArgs holds localize arguments
type LocArgs struct {
@@ -28,8 +28,8 @@ type LocArgs struct {
NewDir filesys.ConfirmedDir
}
// locLoader is the Loader for kustomize localize. It is an ifc.Loader that enforces localize constraints.
type locLoader struct {
// LocLoader is the Loader for kustomize localize. It is an ifc.Loader that enforces localize constraints.
type LocLoader struct {
fSys filesys.FileSystem
args *LocArgs
@@ -41,11 +41,11 @@ type locLoader struct {
local bool
}
var _ ifc.Loader = &locLoader{}
var _ ifc.Loader = &LocLoader{}
// NewLocLoader is the factory method for Loader, under localize constraints, at targetArg. For invalid localize arguments,
// NewLocLoader is the factory method for LocLoader, under localize constraints, at targetArg. For invalid localize arguments,
// NewLocLoader returns an error.
func NewLocLoader(targetArg string, scopeArg string, newDirArg string, fSys filesys.FileSystem) (ifc.Loader, LocArgs, error) {
func NewLocLoader(targetArg string, scopeArg string, newDirArg string, fSys filesys.FileSystem) (*LocLoader, LocArgs, error) {
// check earlier to avoid cleanup
repoSpec, err := git.NewRepoSpecFromURL(targetArg)
if err == nil && repoSpec.Ref == "" {
@@ -76,7 +76,7 @@ func NewLocLoader(targetArg string, scopeArg string, newDirArg string, fSys file
Scope: scope,
NewDir: newDir,
}
return &locLoader{
return &LocLoader{
fSys: fSys,
args: &args,
Loader: ldr,
@@ -86,7 +86,7 @@ func NewLocLoader(targetArg string, scopeArg string, newDirArg string, fSys file
// Load returns the contents of path if path is a valid localize file.
// Otherwise, Load returns an error.
func (ll *locLoader) Load(path string) ([]byte, error) {
func (ll *LocLoader) Load(path string) ([]byte, error) {
// checks in root, and thus in scope
content, err := ll.Loader.Load(path)
if err != nil {
@@ -115,7 +115,7 @@ func (ll *locLoader) Load(path string) ([]byte, error) {
// New returns a Loader at path if path is a valid localize root.
// Otherwise, New returns an error.
func (ll *locLoader) New(path string) (ifc.Loader, error) {
func (ll *LocLoader) New(path string) (ifc.Loader, error) {
ldr, err := ll.Loader.New(path)
if err != nil {
return nil, errors.WrapPrefixf(err, "invalid root reference")
@@ -133,7 +133,7 @@ func (ll *locLoader) New(path string) (ifc.Loader, error) {
return nil, errors.Errorf("localize remote root %q missing ref query string parameter", path)
}
return &locLoader{
return &LocLoader{
fSys: ll.fSys,
args: ll.args,
Loader: ldr,

View File

@@ -10,28 +10,11 @@ import (
"github.com/stretchr/testify/require"
"sigs.k8s.io/kustomize/api/ifc"
lclzr "sigs.k8s.io/kustomize/api/internal/localizer"
. "sigs.k8s.io/kustomize/api/internal/localizer"
"sigs.k8s.io/kustomize/kyaml/filesys"
)
const dstPrefix = "localized"
func makeMemoryFs(t *testing.T) filesys.FileSystem {
t.Helper()
req := require.New(t)
fSys := filesys.MakeFsInMemory()
req.NoError(fSys.MkdirAll("/a/b"))
req.NoError(fSys.WriteFile("/a/kustomization.yaml", []byte("/a")))
dirChain := "/alpha/beta/gamma/delta"
req.NoError(fSys.MkdirAll(dirChain))
req.NoError(fSys.WriteFile(dirChain+"/kustomization.yaml", []byte(dirChain)))
req.NoError(fSys.Mkdir("/alpha/beta/c"))
return fSys
}
func checkNewLocLoader(req *require.Assertions, ldr ifc.Loader, args *lclzr.LocArgs, target string, scope string, newDir string, fSys filesys.FileSystem) {
func checkNewLocLoader(req *require.Assertions, ldr *LocLoader, args *LocArgs, target string, scope string, newDir string, fSys filesys.FileSystem) {
checkLoader(req, ldr, target)
checkLocArgs(req, args, target, scope, newDir, fSys)
}
@@ -41,7 +24,7 @@ func checkLoader(req *require.Assertions, ldr ifc.Loader, root string) {
req.Empty(ldr.Repo())
}
func checkLocArgs(req *require.Assertions, args *lclzr.LocArgs, target string, scope string, newDir string, fSys filesys.FileSystem) {
func checkLocArgs(req *require.Assertions, args *LocArgs, target string, scope string, newDir string, fSys filesys.FileSystem) {
req.Equal(target, args.Target.String())
req.Equal(scope, args.Scope.String())
req.Equal(newDir, args.NewDir.String())
@@ -55,7 +38,7 @@ func TestLocalLoadNewAndCleanup(t *testing.T) {
var buf bytes.Buffer
log.SetOutput(&buf)
// typical setup
ldr, args, err := lclzr.NewLocLoader("a", "/", "/newDir", fSys)
ldr, args, err := NewLocLoader("a", "/", "/newDir", fSys)
req.NoError(err)
checkNewLocLoader(req, ldr, &args, "/a", "/", "/newDir", fSys)
@@ -64,9 +47,9 @@ func TestLocalLoadNewAndCleanup(t *testing.T) {
req.Equal(fSysCopy, fSys)
// easy load directly in root
content, err := ldr.Load("kustomization.yaml")
content, err := ldr.Load("pod.yaml")
req.NoError(err)
req.Equal([]byte("/a"), content)
req.Equal([]byte(podConfiguration), content)
// typical sibling root reference
sibLdr, err := ldr.New("../alpha")
@@ -102,23 +85,23 @@ func TestNewLocLoaderDefaultForRootTarget(t *testing.T) {
req := require.New(t)
fSys := makeMemoryFs(t)
ldr, args, err := lclzr.NewLocLoader(params.target, params.scope, "", fSys)
ldr, args, err := NewLocLoader(params.target, params.scope, "", fSys)
req.NoError(err)
checkNewLocLoader(req, ldr, &args, "/", "/", "/"+dstPrefix, fSys)
checkNewLocLoader(req, ldr, &args, "/", "/", "/"+DstPrefix, fSys)
// file in root, but nested
content, err := ldr.Load("a/kustomization.yaml")
content, err := ldr.Load("a/pod.yaml")
req.NoError(err)
req.Equal([]byte("/a"), content)
req.Equal([]byte(podConfiguration), content)
childLdr, err := ldr.New("a")
req.NoError(err)
checkLoader(req, childLdr, "/a")
// messy, uncleaned path
content, err = childLdr.Load("./../a/kustomization.yaml")
content, err = childLdr.Load("./../a/pod.yaml")
req.NoError(err)
req.Equal([]byte("/a"), content)
req.Equal([]byte(podConfiguration), content)
})
}
}
@@ -129,9 +112,9 @@ func TestNewMultiple(t *testing.T) {
// default destination for non-file system root target
// destination outside of scope
ldr, args, err := lclzr.NewLocLoader("/alpha/beta", "/alpha", "", fSys)
ldr, args, err := NewLocLoader("/alpha/beta", "/alpha", "", fSys)
req.NoError(err)
checkNewLocLoader(req, ldr, &args, "/alpha/beta", "/alpha", "/"+dstPrefix+"-beta", fSys)
checkNewLocLoader(req, ldr, &args, "/alpha/beta", "/alpha", "/"+DstPrefix+"-beta", fSys)
// nested child root that isn't cleaned
descLdr, err := ldr.New("../beta/gamma/delta")
@@ -139,9 +122,9 @@ func TestNewMultiple(t *testing.T) {
checkLoader(req, descLdr, "/alpha/beta/gamma/delta")
// upwards traversal
higherLdr, err := descLdr.New("../../c")
higherLdr, err := descLdr.New("../../say")
req.NoError(err)
checkLoader(req, higherLdr, "/alpha/beta/c")
checkLoader(req, higherLdr, "/alpha/beta/say")
}
func makeWdFs(t *testing.T) map[string]filesys.FileSystem {
@@ -190,7 +173,7 @@ func TestNewLocLoaderCwdNotRoot(t *testing.T) {
req := require.New(t)
fSys := makeWdFs(t)[test.wd]
ldr, args, err := lclzr.NewLocLoader(test.target, test.scope, test.newDir, fSys)
ldr, args, err := NewLocLoader(test.target, test.scope, test.newDir, fSys)
req.NoError(err)
checkLoader(req, ldr, "a/b/c/d/e")
@@ -215,7 +198,7 @@ func TestNewLocLoaderFails(t *testing.T) {
"/newDir",
},
"file target": {
"/a/kustomization.yaml",
"/a/pod.yaml",
"/",
"/newDir",
},
@@ -240,7 +223,7 @@ func TestNewLocLoaderFails(t *testing.T) {
t.Run(name, func(t *testing.T) {
var buf bytes.Buffer
log.SetOutput(&buf)
_, _, err := lclzr.NewLocLoader(params.target, params.scope, params.dest, makeMemoryFs(t))
_, _, err := NewLocLoader(params.target, params.scope, params.dest, makeMemoryFs(t))
require.Error(t, err)
require.Empty(t, buf.String())
})
@@ -251,7 +234,7 @@ func TestNewFails(t *testing.T) {
req := require.New(t)
fSys := makeMemoryFs(t)
ldr, args, err := lclzr.NewLocLoader("/alpha/beta/gamma", "alpha", "alpha/beta/gamma/newDir", fSys)
ldr, args, err := NewLocLoader("/alpha/beta/gamma", "alpha", "alpha/beta/gamma/newDir", fSys)
req.NoError(err)
checkNewLocLoader(req, ldr, &args, "/alpha/beta/gamma", "/alpha", "/alpha/beta/gamma/newDir", fSys)
@@ -260,14 +243,14 @@ func TestNewFails(t *testing.T) {
"at dst": "newDir",
"ancestor": "../../beta",
"non-existent root": "delt",
"file": "delta/kustomization.yaml",
"file": "delta/deployment.yaml",
}
for name, root := range cases {
root := root
t.Run(name, func(t *testing.T) {
fSys := makeMemoryFs(t)
ldr, _, err := lclzr.NewLocLoader("/alpha/beta/gamma", "alpha", "alpha/beta/gamma/newDir", fSys)
ldr, _, err := NewLocLoader("/alpha/beta/gamma", "alpha", "alpha/beta/gamma/newDir", fSys)
require.NoError(t, err)
_, err = ldr.New(root)
@@ -280,16 +263,16 @@ func TestLoadFails(t *testing.T) {
req := require.New(t)
fSys := makeMemoryFs(t)
ldr, args, err := lclzr.NewLocLoader("./a/../a", "/a/../a", "/a/newDir", fSys)
ldr, args, err := NewLocLoader("./a/../a", "/a/../a", "/a/newDir", fSys)
req.NoError(err)
checkNewLocLoader(req, ldr, &args, "/a", "/a", "/a/newDir", fSys)
cases := map[string]string{
"absolute path": "/a/kustomization.yaml",
"absolute path": "/a/pod.yaml",
"directory": "b",
"non-existent file": "kubectl.yaml",
"file outside root": "../alpha/beta/gamma/delta/kustomization.yaml",
"inside dst": "newDir/kustomization.yaml",
"file outside root": "../alpha/beta/gamma/delta/deployment.yaml",
"inside dst": "newDir/pod.yaml",
}
for name, file := range cases {
file := file
@@ -297,10 +280,10 @@ func TestLoadFails(t *testing.T) {
req := require.New(t)
fSys := makeMemoryFs(t)
ldr, _, err := lclzr.NewLocLoader("./a/../a", "/a/../a", "/a/newDir", fSys)
ldr, _, err := NewLocLoader("./a/../a", "/a/../a", "/a/newDir", fSys)
req.NoError(err)
req.NoError(fSys.WriteFile("/a/newDir/kustomization.yaml", []byte("/a/newDir")))
req.NoError(fSys.WriteFile("/a/newDir/pod.yaml", []byte(podConfiguration)))
_, err = ldr.Load(file)
req.Error(err)

View File

@@ -14,13 +14,14 @@ import (
"sigs.k8s.io/kustomize/kyaml/filesys"
)
// establishScope returns the scope given localize arguments and targetLdr at targetArg
// establishScope returns the effective scope given localize arguments and targetLdr at targetArg. For remote targetArg,
// the effective scope is the downloaded repo.
func establishScope(scopeArg string, targetArg string, targetLdr ifc.Loader, fSys filesys.FileSystem) (filesys.ConfirmedDir, error) {
if targetLdr.Repo() != "" {
if repo := targetLdr.Repo(); repo != "" {
if scopeArg != "" {
return "", errors.Errorf("scope '%s' specified for remote localize target '%s'", scopeArg, targetArg)
}
return "", nil
return filesys.ConfirmedDir(repo), nil
}
// default scope
if scopeArg == "" {
@@ -71,13 +72,13 @@ func defaultNewDir(targetLdr ifc.Loader, spec *git.RepoSpec) string {
if repo == targetLdr.Root() {
targetDir = urlBase(spec.OrgRepo)
}
return strings.Join([]string{dstPrefix, targetDir, strings.ReplaceAll(spec.Ref, "/", "-")}, "-")
return strings.Join([]string{DstPrefix, targetDir, strings.ReplaceAll(spec.Ref, "/", "-")}, "-")
}
// special case for local target directory since destination directory cannot have "/" in name
if targetDir == string(filepath.Separator) {
return dstPrefix
return DstPrefix
}
return strings.Join([]string{dstPrefix, targetDir}, "-")
return strings.Join([]string{DstPrefix, targetDir}, "-")
}
// urlBase is the url equivalent of filepath.Base