mirror of
https://github.com/kubernetes-sigs/kustomize.git
synced 2026-06-10 08:20:59 +00:00
Define a plugin compiler.
This commit is contained in:
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
146
pkg/plugins/compiler.go
Normal 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
|
||||||
|
}
|
||||||
60
pkg/plugins/compiler_test.go
Normal file
60
pkg/plugins/compiler_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -16,22 +16,22 @@ 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
|
||||||
oldXdg string
|
workDir string
|
||||||
wasSet bool
|
oldXdg string
|
||||||
|
wasSet bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTestEnvController(t *testing.T) *TestEnvController {
|
func NewTestEnvController(t *testing.T) *TestEnvController {
|
||||||
@@ -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.
|
||||||
// SrcRoot is a objRoot directory for plugin source code
|
objRoot := filepath.Join(
|
||||||
// used by tests.
|
x.workDir, pgmconfig.ProgramName, plugin.PluginRoot)
|
||||||
//
|
err := os.MkdirAll(objRoot, os.ModePerm)
|
||||||
// Plugin object code files have to be in a particular
|
if err != nil {
|
||||||
// location to be found and loaded for security reasons,
|
x.t.Error(err)
|
||||||
// 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
|
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
|
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() {
|
||||||
|
|||||||
Reference in New Issue
Block a user