Define a plugin compiler.

This commit is contained in:
jregan
2019-04-07 11:16:50 -07:00
committed by Jeffrey Regan
parent 1623f1e4c0
commit 175c754f61
9 changed files with 259 additions and 107 deletions

View File

@@ -42,7 +42,7 @@ func NewDefaultCommand() *cobra.Command {
stdOut := os.Stdout stdOut := os.Stdout
c := &cobra.Command{ c := &cobra.Command{
Use: pgmconfig.PgmName, Use: pgmconfig.ProgramName,
Short: "Manages declarative configuration of Kubernetes", Short: "Manages declarative configuration of Kubernetes",
Long: ` Long: `
Manages declarative configuration of Kubernetes. Manages declarative configuration of Kubernetes.

View File

@@ -34,12 +34,12 @@ func ConfigRoot() string {
dir := os.Getenv(XDG_CONFIG_HOME) dir := os.Getenv(XDG_CONFIG_HOME)
if len(dir) == 0 { if len(dir) == 0 {
dir = filepath.Join( 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()) home := os.Getenv(homeEnv())
if len(home) > 0 { if len(home) > 0 {
return home return home

View File

@@ -34,7 +34,7 @@ func TestConfigDirNoXdg(t *testing.T) {
} }
if !strings.HasSuffix( if !strings.HasSuffix(
s, s,
rootedPath(defaultConfigSubdir, PgmName)) { rootedPath(defaultConfigSubdir, ProgramName)) {
t.Fatalf("unexpected config dir: %s", s) t.Fatalf("unexpected config dir: %s", s)
} }
} }
@@ -50,7 +50,7 @@ func TestConfigDirWithXdg(t *testing.T) {
if isSet { if isSet {
os.Setenv(XDG_CONFIG_HOME, xdg) os.Setenv(XDG_CONFIG_HOME, xdg)
} }
if s != rootedPath("blah", PgmName) { if s != rootedPath("blah", ProgramName) {
t.Fatalf("unexpected config dir: %s", s) t.Fatalf("unexpected config dir: %s", s)
} }
} }

View File

@@ -28,6 +28,9 @@ var KustomizationFileNames = []string{
} }
const ( const (
PgmName = "kustomize" // Program name, for help, finding the XDG_CONFIG_DIR, etc.
Repo = "sigs.k8s.io" ProgramName = "kustomize"
// Domain from which kustomize code is imported, for locating
// plugin source code under $GOPATH.
DomainName = "sigs.k8s.io"
) )

146
pkg/plugins/compiler.go Normal file
View File

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

View File

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

View File

@@ -77,12 +77,13 @@ func loadAndConfigurePlugin(
fileName string, res *resource.Resource) (Configurable, error) { fileName string, res *resource.Resource) (Configurable, error) {
goPlugin, err := plugin.Open(fileName) goPlugin, err := plugin.Open(fileName)
if err != nil { 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) symbol, err := goPlugin.Lookup(kplugin.PluginSymbol)
if err != nil { if err != nil {
return nil, fmt.Errorf( return nil, errors.Wrapf(
"plugin %s doesn't have symbol %s", fileName, kplugin.PluginSymbol) err, "plugin %s doesn't have symbol %s",
fileName, kplugin.PluginSymbol)
} }
c, ok := symbol.(Configurable) c, ok := symbol.(Configurable)
if !ok { if !ok {

View File

@@ -233,6 +233,7 @@ func (kt *KustTarget) generateFromPlugins(
generators, err := kt.loadGeneratorPlugins() generators, err := kt.loadGeneratorPlugins()
if err != nil { if err != nil {
errs.Append(err) errs.Append(err)
return
} }
for _, g := range generators { for _, g := range generators {
resMap, err := g.Generate() resMap, err := g.Generate()

View File

@@ -16,20 +16,20 @@ package target_test
import ( import (
"io/ioutil" "io/ioutil"
"os" "os"
"os/exec"
"path/filepath" "path/filepath"
"testing"
"time"
"sigs.k8s.io/kustomize/k8sdeps/kv/plugin" "sigs.k8s.io/kustomize/k8sdeps/kv/plugin"
"testing"
"sigs.k8s.io/kustomize/pkg/pgmconfig" "sigs.k8s.io/kustomize/pkg/pgmconfig"
"sigs.k8s.io/kustomize/pkg/plugins"
) )
// TestEnvController manages the KustTarget test environment. // TestEnvController manages the KustTarget test environment.
// It sets/resets XDG_CONFIG_HOME, makes/removes a temp objRoot. // It sets/resets XDG_CONFIG_HOME, makes/removes a temp objRoot.
type TestEnvController struct { type TestEnvController struct {
t *testing.T t *testing.T
xdgConfigHome string compiler *plugins.Compiler
workDir string
oldXdg string oldXdg string
wasSet bool wasSet bool
} }
@@ -39,121 +39,62 @@ func NewTestEnvController(t *testing.T) *TestEnvController {
} }
func (x *TestEnvController) Set() *TestEnvController { func (x *TestEnvController) Set() *TestEnvController {
x.makeTmpConfigHomeDir() x.createWorkDir()
x.makeObjectRootDir() x.compiler = x.makeCompiler()
x.setEnv() x.setEnv()
return x return x
} }
func (x *TestEnvController) Reset() { func (x *TestEnvController) Reset() {
x.resetEnv() x.resetEnv()
x.removeTmpConfigHomeDir() x.removeWorkDir()
} }
func (x *TestEnvController) fileExists(name string) bool { func (x *TestEnvController) BuildGoPlugin(g, v, k string) {
if _, err := os.Stat(name); err != nil { err := x.compiler.Compile(g, v, k)
if os.IsNotExist(err) {
return false
}
}
return true
}
func (x *TestEnvController) recentFileExists(path string) bool {
fi, err := os.Stat(path)
if err != nil { if err != nil {
if os.IsNotExist(err) { x.t.Errorf("compile failed: %v", 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)
} }
} }
// ObjectRoot is the objRoot dir for plugin object files. func (x *TestEnvController) makeCompiler() *plugins.Compiler {
func (x *TestEnvController) ObjectRoot() string { // The plugin loader wants to find object code under
return filepath.Join( // $XDG_CONFIG_HOME/kustomize/plugins
x.xdgConfigHome, pgmconfig.PgmName, plugin.PluginRoot) // 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)
}
srcRoot, err := plugins.DefaultSrcRoot()
if err != nil {
x.t.Error(err)
}
return plugins.NewCompiler(srcRoot, objRoot)
} }
// SrcRoot is a objRoot directory for plugin source code func (x *TestEnvController) createWorkDir() {
// 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)
}
return dir
}
func (x *TestEnvController) makeTmpConfigHomeDir() {
var err error var err error
x.xdgConfigHome, err = ioutil.TempDir("", "kustomizetests") x.workDir, err = ioutil.TempDir("", "kustomize-plugin-tests")
if err != nil { 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() { func (x *TestEnvController) removeWorkDir() {
err := os.MkdirAll(x.ObjectRoot(), os.ModePerm) err := os.RemoveAll(x.workDir)
if err != nil { if err != nil {
x.t.Errorf( x.t.Errorf(
"making temp object objRoot %s: %v", x.ObjectRoot(), err) "removing work dir: %s %v", x.workDir, 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)
} }
} }
func (x *TestEnvController) setEnv() { func (x *TestEnvController) setEnv() {
x.oldXdg, x.wasSet = os.LookupEnv(pgmconfig.XDG_CONFIG_HOME) 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() { func (x *TestEnvController) resetEnv() {