diff --git a/api/internal/localizer/localizer.go b/api/internal/localizer/localizer.go new file mode 100644 index 000000000..545c44753 --- /dev/null +++ b/api/internal/localizer/localizer.go @@ -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 +} diff --git a/api/internal/localizer/localizer_test.go b/api/internal/localizer/localizer_test.go new file mode 100644 index 000000000..d2a2171a1 --- /dev/null +++ b/api/internal/localizer/localizer_test.go @@ -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) +} diff --git a/api/internal/localizer/locloader.go b/api/internal/localizer/locloader.go index bc036a531..d754cb0c1 100644 --- a/api/internal/localizer/locloader.go +++ b/api/internal/localizer/locloader.go @@ -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, diff --git a/api/internal/localizer/locloader_test.go b/api/internal/localizer/locloader_test.go index e2e4a72bb..8c0a7c1ad 100644 --- a/api/internal/localizer/locloader_test.go +++ b/api/internal/localizer/locloader_test.go @@ -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) diff --git a/api/internal/localizer/util.go b/api/internal/localizer/util.go index bc14a9552..c1564d513 100644 --- a/api/internal/localizer/util.go +++ b/api/internal/localizer/util.go @@ -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