diff --git a/pkg/commands/commands.go b/pkg/commands/commands.go index 33e18683b..dbba6bb0c 100644 --- a/pkg/commands/commands.go +++ b/pkg/commands/commands.go @@ -42,7 +42,7 @@ func NewDefaultCommand() *cobra.Command { stdOut := os.Stdout c := &cobra.Command{ - Use: pgmconfig.PgmName, + Use: pgmconfig.ProgramName, Short: "Manages declarative configuration of Kubernetes", Long: ` Manages declarative configuration of Kubernetes. diff --git a/pkg/pgmconfig/config.go b/pkg/pgmconfig/config.go index c0f8522b4..5185c50cd 100644 --- a/pkg/pgmconfig/config.go +++ b/pkg/pgmconfig/config.go @@ -34,12 +34,12 @@ func ConfigRoot() string { dir := os.Getenv(XDG_CONFIG_HOME) if len(dir) == 0 { dir = filepath.Join( - homeDir(), defaultConfigSubdir) + HomeDir(), defaultConfigSubdir) } - return filepath.Join(dir, PgmName) + return filepath.Join(dir, ProgramName) } -func homeDir() string { +func HomeDir() string { home := os.Getenv(homeEnv()) if len(home) > 0 { return home diff --git a/pkg/pgmconfig/config_test.go b/pkg/pgmconfig/config_test.go index 2833afea7..fc32e83a4 100644 --- a/pkg/pgmconfig/config_test.go +++ b/pkg/pgmconfig/config_test.go @@ -34,7 +34,7 @@ func TestConfigDirNoXdg(t *testing.T) { } if !strings.HasSuffix( s, - rootedPath(defaultConfigSubdir, PgmName)) { + rootedPath(defaultConfigSubdir, ProgramName)) { t.Fatalf("unexpected config dir: %s", s) } } @@ -50,7 +50,7 @@ func TestConfigDirWithXdg(t *testing.T) { if isSet { os.Setenv(XDG_CONFIG_HOME, xdg) } - if s != rootedPath("blah", PgmName) { + if s != rootedPath("blah", ProgramName) { t.Fatalf("unexpected config dir: %s", s) } } diff --git a/pkg/pgmconfig/constants.go b/pkg/pgmconfig/constants.go index 2aecd4fbf..92aaf5fba 100644 --- a/pkg/pgmconfig/constants.go +++ b/pkg/pgmconfig/constants.go @@ -28,6 +28,9 @@ var KustomizationFileNames = []string{ } const ( - PgmName = "kustomize" - Repo = "sigs.k8s.io" + // Program name, for help, finding the XDG_CONFIG_DIR, etc. + ProgramName = "kustomize" + // Domain from which kustomize code is imported, for locating + // plugin source code under $GOPATH. + DomainName = "sigs.k8s.io" ) diff --git a/pkg/plugins/compiler.go b/pkg/plugins/compiler.go new file mode 100644 index 000000000..cfcd57c99 --- /dev/null +++ b/pkg/plugins/compiler.go @@ -0,0 +1,146 @@ +/* +Copyright 2019 The Kubernetes Authors. + Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package plugins + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "time" + + "sigs.k8s.io/kustomize/k8sdeps/kv/plugin" + "sigs.k8s.io/kustomize/pkg/pgmconfig" +) + +// Compiler creates Go plugin object files. +// +// Source code is read from +// ${srcRoot}/${g}/${v}/${k}.go +// +// Object code is written to +// ${objRoot}/${g}/${v}/${k}.so +type Compiler struct { + srcRoot string + objRoot string +} + +// DefaultSrcRoot guesses where the user +// has her ${g}/${v}/${k}.go files. +func DefaultSrcRoot() (string, error) { + var nope []string + var root string + + root = filepath.Join( + os.Getenv("GOPATH"), "src", + pgmconfig.DomainName, + pgmconfig.ProgramName, plugin.PluginRoot) + if FileExists(root) { + return root, nil + } + nope = append(nope, root) + + root = filepath.Join( + pgmconfig.ConfigRoot(), plugin.PluginRoot) + if FileExists(root) { + return root, nil + } + nope = append(nope, root) + + root = filepath.Join( + pgmconfig.HomeDir(), + pgmconfig.ProgramName, plugin.PluginRoot) + if FileExists(root) { + return root, nil + } + nope = append(nope, root) + + return "", fmt.Errorf( + "no default src root; tried %v", nope) +} + +// NewCompiler returns a new compiler instance. +func NewCompiler(srcRoot, objRoot string) *Compiler { + return &Compiler{srcRoot: srcRoot, objRoot: objRoot} +} + +// ObjRoot is root of compilation target tree. +func (b *Compiler) ObjRoot() string { + return b.objRoot +} + +func goBin() string { + return filepath.Join(os.Getenv("GOROOT"), "bin", "go") +} + +// Compile reads ${srcRoot}/${g}/${v}/${k}.go +// and writes ${objRoot}/${g}/${v}/${k}.so +func (b *Compiler) Compile(g, v, k string) error { + objDir := filepath.Join(b.objRoot, g, v) + objFile := filepath.Join(objDir, k) + ".so" + if RecentFileExists(objFile) { + // Skip rebuilding it. + return nil + } + err := os.MkdirAll(objDir, os.ModePerm) + if err != nil { + return err + } + srcFile := filepath.Join(b.srcRoot, g, v, k) + ".go" + if !FileExists(srcFile) { + return fmt.Errorf( + "cannot find source %s", srcFile) + } + commands := []string{ + "build", + "-buildmode", + "plugin", + "-tags=plugin", + "-o", objFile, srcFile, + } + goBin := goBin() + if !FileExists(goBin) { + return fmt.Errorf( + "cannot find go compiler %s", goBin) + } + cmd := exec.Command(goBin, commands...) + cmd.Env = os.Environ() + if err := cmd.Run(); err != nil { + return fmt.Errorf( + "compiler error building %s: %v", srcFile, err) + } + return nil +} + +// True if file less than 3 minutes old, i.e. not +// accidentally left over from some earlier build. +func RecentFileExists(path string) bool { + fi, err := os.Stat(path) + if err != nil { + if os.IsNotExist(err) { + return false + } + } + age := time.Now().Sub(fi.ModTime()) + return age.Minutes() < 3 +} + +func FileExists(name string) bool { + if _, err := os.Stat(name); err != nil { + if os.IsNotExist(err) { + return false + } + } + return true +} diff --git a/pkg/plugins/compiler_test.go b/pkg/plugins/compiler_test.go new file mode 100644 index 000000000..b836f6481 --- /dev/null +++ b/pkg/plugins/compiler_test.go @@ -0,0 +1,60 @@ +/* +Copyright 2019 The Kubernetes Authors. + Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package plugins_test + +import ( + "io/ioutil" + "os" + "path/filepath" + "testing" + + . "sigs.k8s.io/kustomize/pkg/plugins" +) + +// Regression coverage over compiler behavior. +func TestCompiler(t *testing.T) { + configRoot, err := ioutil.TempDir("", "kustomize-compiler-test") + if err != nil { + t.Errorf("failed to make temp dir: %v", err) + } + srcRoot, err := DefaultSrcRoot() + if err != nil { + t.Error(err) + } + c := NewCompiler(srcRoot, configRoot) + if configRoot != c.ObjRoot() { + t.Errorf("unexpected objRoot %s", c.ObjRoot()) + } + expectObj := filepath.Join( + c.ObjRoot(), + "someteam.example.com", "v1", "DatePrefixer.so") + if FileExists(expectObj) { + t.Errorf("obj file should not exist yet: %s", expectObj) + } + err = c.Compile("someteam.example.com", "v1", "DatePrefixer") + if err != nil { + t.Error(err) + } + if !RecentFileExists(expectObj) { + t.Errorf("didn't find expected obj file %s", expectObj) + } + err = os.RemoveAll(c.ObjRoot()) + if err != nil { + t.Errorf( + "removing temp dir: %s %v", c.ObjRoot(), err) + } + if FileExists(expectObj) { + t.Errorf("cleanup failed; still see: %s", expectObj) + } +} diff --git a/pkg/plugins/transformers.go b/pkg/plugins/transformers.go index b6cf28864..a30717706 100644 --- a/pkg/plugins/transformers.go +++ b/pkg/plugins/transformers.go @@ -77,12 +77,13 @@ func loadAndConfigurePlugin( fileName string, res *resource.Resource) (Configurable, error) { goPlugin, err := plugin.Open(fileName) if err != nil { - return nil, fmt.Errorf("plugin %s file not opened", fileName) + return nil, errors.Wrapf(err, "plugin %s fails to load", fileName) } symbol, err := goPlugin.Lookup(kplugin.PluginSymbol) if err != nil { - return nil, fmt.Errorf( - "plugin %s doesn't have symbol %s", fileName, kplugin.PluginSymbol) + return nil, errors.Wrapf( + err, "plugin %s doesn't have symbol %s", + fileName, kplugin.PluginSymbol) } c, ok := symbol.(Configurable) if !ok { diff --git a/pkg/target/kusttarget.go b/pkg/target/kusttarget.go index ac5589927..33287917f 100644 --- a/pkg/target/kusttarget.go +++ b/pkg/target/kusttarget.go @@ -233,6 +233,7 @@ func (kt *KustTarget) generateFromPlugins( generators, err := kt.loadGeneratorPlugins() if err != nil { errs.Append(err) + return } for _, g := range generators { resMap, err := g.Generate() diff --git a/pkg/target/testenvcontroller_test.go b/pkg/target/testenvcontroller_test.go index bdf0bfad1..f68c182f1 100644 --- a/pkg/target/testenvcontroller_test.go +++ b/pkg/target/testenvcontroller_test.go @@ -16,22 +16,22 @@ package target_test import ( "io/ioutil" "os" - "os/exec" "path/filepath" - "testing" - "time" - "sigs.k8s.io/kustomize/k8sdeps/kv/plugin" + "testing" + "sigs.k8s.io/kustomize/pkg/pgmconfig" + "sigs.k8s.io/kustomize/pkg/plugins" ) // TestEnvController manages the KustTarget test environment. // It sets/resets XDG_CONFIG_HOME, makes/removes a temp objRoot. type TestEnvController struct { - t *testing.T - xdgConfigHome string - oldXdg string - wasSet bool + t *testing.T + compiler *plugins.Compiler + workDir string + oldXdg string + wasSet bool } func NewTestEnvController(t *testing.T) *TestEnvController { @@ -39,121 +39,62 @@ func NewTestEnvController(t *testing.T) *TestEnvController { } func (x *TestEnvController) Set() *TestEnvController { - x.makeTmpConfigHomeDir() - x.makeObjectRootDir() + x.createWorkDir() + x.compiler = x.makeCompiler() x.setEnv() return x } func (x *TestEnvController) Reset() { x.resetEnv() - x.removeTmpConfigHomeDir() + x.removeWorkDir() } -func (x *TestEnvController) fileExists(name string) bool { - if _, err := os.Stat(name); err != nil { - if os.IsNotExist(err) { - return false - } - } - return true -} - -func (x *TestEnvController) recentFileExists(path string) bool { - fi, err := os.Stat(path) +func (x *TestEnvController) BuildGoPlugin(g, v, k string) { + err := x.compiler.Compile(g, v, k) if err != nil { - if os.IsNotExist(err) { - return false - } - } - age := time.Now().Sub(fi.ModTime()) - return age.Minutes() < 1 -} - -func (x *TestEnvController) BuildGoPlugin(plugin ...string) { - obj := filepath.Join( - append([]string{x.ObjectRoot()}, plugin...)...) + ".so" - if x.recentFileExists(obj) { - // Skip rebuilding it. - return - } - src := filepath.Join( - append([]string{x.SrcRoot()}, plugin...)...) + ".go" - if !x.fileExists(src) { - x.t.Errorf("cannot find go plugin source %s", src) - } - commands := []string{ - "build", - "-buildmode", - "plugin", - "-tags=plugin", - "-o", obj, src, - } - goBin := filepath.Join(os.Getenv("GOROOT"), "bin", "go") - if !x.fileExists(src) { - x.t.Errorf("cannot find go compiler %s", goBin) - } - cmd := exec.Command(goBin, commands...) - cmd.Env = os.Environ() - // cmd.Dir = filepath.Join(dir, "kustomize", "plugins") - - if err := cmd.Run(); err != nil { - x.t.Errorf("compiler error building %s: %v", src, err) + x.t.Errorf("compile failed: %v", err) } } -// ObjectRoot is the objRoot dir for plugin object files. -func (x *TestEnvController) ObjectRoot() string { - return filepath.Join( - x.xdgConfigHome, pgmconfig.PgmName, plugin.PluginRoot) -} - -// SrcRoot is a objRoot directory for plugin source code -// used by tests. -// -// Plugin object code files have to be in a particular -// location to be found and loaded for security reasons, -// but placement of plugin source code is up to the user. -// -// This function returns a location for storing example -// plugins for tests. And maybe builtins at some point. -func (x *TestEnvController) SrcRoot() string { - dir := filepath.Join( - os.Getenv("GOPATH"), "src", - pgmconfig.Repo, pgmconfig.PgmName, plugin.PluginRoot) - if _, err := os.Stat(dir); err != nil { - x.t.Errorf("plugin source objRoot '%s' not found", dir) +func (x *TestEnvController) makeCompiler() *plugins.Compiler { + // The plugin loader wants to find object code under + // $XDG_CONFIG_HOME/kustomize/plugins + // and the compiler writes object code to + // $objRoot + // so set things up accordingly. + objRoot := filepath.Join( + x.workDir, pgmconfig.ProgramName, plugin.PluginRoot) + err := os.MkdirAll(objRoot, os.ModePerm) + if err != nil { + x.t.Error(err) } - return dir + srcRoot, err := plugins.DefaultSrcRoot() + if err != nil { + x.t.Error(err) + } + return plugins.NewCompiler(srcRoot, objRoot) } -func (x *TestEnvController) makeTmpConfigHomeDir() { +func (x *TestEnvController) createWorkDir() { var err error - x.xdgConfigHome, err = ioutil.TempDir("", "kustomizetests") + x.workDir, err = ioutil.TempDir("", "kustomize-plugin-tests") if err != nil { - x.t.Errorf("failed to make temp objRoot: %v", err) + x.t.Errorf("failed to make work dir: %v", err) } } -func (x *TestEnvController) makeObjectRootDir() { - err := os.MkdirAll(x.ObjectRoot(), os.ModePerm) +func (x *TestEnvController) removeWorkDir() { + err := os.RemoveAll(x.workDir) if err != nil { x.t.Errorf( - "making temp object objRoot %s: %v", x.ObjectRoot(), err) - } -} - -func (x *TestEnvController) removeTmpConfigHomeDir() { - err := os.RemoveAll(x.xdgConfigHome) - if err != nil { - x.t.Errorf( - "removing temp object objRoot: %s %v", x.xdgConfigHome, err) + "removing work dir: %s %v", x.workDir, err) } } func (x *TestEnvController) setEnv() { x.oldXdg, x.wasSet = os.LookupEnv(pgmconfig.XDG_CONFIG_HOME) - os.Setenv(pgmconfig.XDG_CONFIG_HOME, x.xdgConfigHome) + os.Setenv(pgmconfig.XDG_CONFIG_HOME, x.workDir) } func (x *TestEnvController) resetEnv() {