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
c := &cobra.Command{
Use: pgmconfig.PgmName,
Use: pgmconfig.ProgramName,
Short: "Manages declarative configuration of Kubernetes",
Long: `
Manages declarative configuration of Kubernetes.

View File

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

View File

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

View File

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

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

View File

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

View File

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