diff --git a/cmd/apply.go b/cmd/apply.go index e5ad691ab4b9580753a09aa98ca0d6c5fae04207..e8ad6e09734a83e45d67ada13f173adef185b299 100644 --- a/cmd/apply.go +++ b/cmd/apply.go @@ -116,7 +116,13 @@ var applyCmd = &cobra.Command{ return err } - objs, err := expandEnvCmdObjs(cmd, env, componentNames, wd) + te := newCmdObjExpander(cmdObjExpanderConfig{ + cmd: cmd, + env: env, + components: componentNames, + cwd: wd, + }) + objs, err := te.Expand() if err != nil { return err } diff --git a/cmd/delete.go b/cmd/delete.go index a94827298a4429886a3db3758e5cbcb945d3ee08..9e39e57024da913e3025e5810666c60e93c58293 100644 --- a/cmd/delete.go +++ b/cmd/delete.go @@ -78,7 +78,13 @@ var deleteCmd = &cobra.Command{ return err } - objs, err := expandEnvCmdObjs(cmd, env, componentNames, wd) + te := newCmdObjExpander(cmdObjExpanderConfig{ + cmd: cmd, + env: env, + components: componentNames, + cwd: wd, + }) + objs, err := te.Expand() if err != nil { return err } diff --git a/cmd/diff.go b/cmd/diff.go index 93dcec1ddecc9998ecbf6e4cdacb7e0a98f48058..2893f164ca07141a5d1b50810a7f2f8be63811c6 100644 --- a/cmd/diff.go +++ b/cmd/diff.go @@ -23,6 +23,7 @@ import ( "k8s.io/client-go/discovery" "k8s.io/client-go/dynamic" + "github.com/spf13/afero" "github.com/spf13/cobra" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/client-go/tools/clientcmd" @@ -82,7 +83,7 @@ var diffCmd = &cobra.Command{ return err } - c, err := initDiffCmd(cmd, wd, env1, env2, componentNames, diffStrategy) + c, err := initDiffCmd(appFs, cmd, wd, env1, env2, componentNames, diffStrategy) if err != nil { return err } @@ -140,14 +141,14 @@ ks diff dev -c redis `, } -func initDiffCmd(cmd *cobra.Command, wd metadata.AbsPath, envFq1, envFq2 *string, files []string, diffStrategy string) (kubecfg.DiffCmd, error) { +func initDiffCmd(fs afero.Fs, cmd *cobra.Command, wd metadata.AbsPath, envFq1, envFq2 *string, files []string, diffStrategy string) (kubecfg.DiffCmd, error) { const ( remote = "remote" local = "local" ) if envFq2 == nil { - return initDiffSingleEnv(*envFq1, diffStrategy, files, cmd, wd) + return initDiffSingleEnv(fs, *envFq1, diffStrategy, files, cmd, wd) } // expect envs to be of the format local:myenv or remote:myenv @@ -168,11 +169,11 @@ func initDiffCmd(cmd *cobra.Command, wd metadata.AbsPath, envFq1, envFq2 *string } if env1[0] == local && env2[0] == local { - return initDiffLocalCmd(env1[1], env2[1], diffStrategy, cmd, manager) + return initDiffLocalCmd(fs, env1[1], env2[1], diffStrategy, cmd, manager) } if env1[0] == remote && env2[0] == remote { - return initDiffRemotesCmd(env1[1], env2[1], diffStrategy, cmd, manager) + return initDiffRemotesCmd(fs, env1[1], env2[1], diffStrategy, cmd, manager) } localEnv := env1[1] @@ -181,11 +182,11 @@ func initDiffCmd(cmd *cobra.Command, wd metadata.AbsPath, envFq1, envFq2 *string localEnv = env2[1] remoteEnv = env1[1] } - return initDiffRemoteCmd(localEnv, remoteEnv, diffStrategy, cmd, manager) + return initDiffRemoteCmd(fs, localEnv, remoteEnv, diffStrategy, cmd, manager) } // initDiffSingleEnv sets up configurations for diffing using one environment -func initDiffSingleEnv(env, diffStrategy string, files []string, cmd *cobra.Command, wd metadata.AbsPath) (kubecfg.DiffCmd, error) { +func initDiffSingleEnv(fs afero.Fs, env, diffStrategy string, files []string, cmd *cobra.Command, wd metadata.AbsPath) (kubecfg.DiffCmd, error) { c := kubecfg.DiffRemoteCmd{} c.DiffStrategy = diffStrategy c.Client = &kubecfg.Client{} @@ -195,7 +196,13 @@ func initDiffSingleEnv(env, diffStrategy string, files []string, cmd *cobra.Comm return nil, fmt.Errorf("single <env> argument with prefix 'local:' or 'remote:' not allowed") } - c.Client.APIObjects, err = expandEnvCmdObjs(cmd, env, files, wd) + te := newCmdObjExpander(cmdObjExpanderConfig{ + cmd: cmd, + env: env, + components: files, + cwd: wd, + }) + c.Client.APIObjects, err = te.Expand() if err != nil { return nil, err } @@ -214,21 +221,21 @@ func initDiffSingleEnv(env, diffStrategy string, files []string, cmd *cobra.Comm } // initDiffLocalCmd sets up configurations for diffing between two sets of expanded Kubernetes objects locally -func initDiffLocalCmd(env1, env2, diffStrategy string, cmd *cobra.Command, m metadata.Manager) (kubecfg.DiffCmd, error) { +func initDiffLocalCmd(fs afero.Fs, env1, env2, diffStrategy string, cmd *cobra.Command, m metadata.Manager) (kubecfg.DiffCmd, error) { c := kubecfg.DiffLocalCmd{} c.DiffStrategy = diffStrategy var err error c.Env1 = &kubecfg.LocalEnv{} c.Env1.Name = env1 - c.Env1.APIObjects, err = expandEnvObjs(cmd, c.Env1.Name, m) + c.Env1.APIObjects, err = expandEnvObjs(fs, cmd, c.Env1.Name, m) if err != nil { return nil, err } c.Env2 = &kubecfg.LocalEnv{} c.Env2.Name = env2 - c.Env2.APIObjects, err = expandEnvObjs(cmd, c.Env2.Name, m) + c.Env2.APIObjects, err = expandEnvObjs(fs, cmd, c.Env2.Name, m) if err != nil { return nil, err } @@ -237,7 +244,7 @@ func initDiffLocalCmd(env1, env2, diffStrategy string, cmd *cobra.Command, m met } // initDiffRemotesCmd sets up configurations for diffing between objects on two remote clusters -func initDiffRemotesCmd(env1, env2, diffStrategy string, cmd *cobra.Command, m metadata.Manager) (kubecfg.DiffCmd, error) { +func initDiffRemotesCmd(fs afero.Fs, env1, env2, diffStrategy string, cmd *cobra.Command, m metadata.Manager) (kubecfg.DiffCmd, error) { c := kubecfg.DiffRemotesCmd{} c.DiffStrategy = diffStrategy @@ -248,11 +255,11 @@ func initDiffRemotesCmd(env1, env2, diffStrategy string, cmd *cobra.Command, m m c.ClientB.Name = env2 var err error - c.ClientA.APIObjects, err = expandEnvObjs(cmd, c.ClientA.Name, m) + c.ClientA.APIObjects, err = expandEnvObjs(fs, cmd, c.ClientA.Name, m) if err != nil { return nil, err } - c.ClientB.APIObjects, err = expandEnvObjs(cmd, c.ClientB.Name, m) + c.ClientB.APIObjects, err = expandEnvObjs(fs, cmd, c.ClientB.Name, m) if err != nil { return nil, err } @@ -270,13 +277,13 @@ func initDiffRemotesCmd(env1, env2, diffStrategy string, cmd *cobra.Command, m m } // initDiffRemoteCmd sets up configurations for diffing between local objects and objects on a remote cluster -func initDiffRemoteCmd(localEnv, remoteEnv, diffStrategy string, cmd *cobra.Command, m metadata.Manager) (kubecfg.DiffCmd, error) { +func initDiffRemoteCmd(fs afero.Fs, localEnv, remoteEnv, diffStrategy string, cmd *cobra.Command, m metadata.Manager) (kubecfg.DiffCmd, error) { c := kubecfg.DiffRemoteCmd{} c.DiffStrategy = diffStrategy c.Client = &kubecfg.Client{} var err error - c.Client.APIObjects, err = expandEnvObjs(cmd, localEnv, m) + c.Client.APIObjects, err = expandEnvObjs(fs, cmd, localEnv, m) if err != nil { return nil, err } @@ -309,8 +316,8 @@ func setupClientConfig(env *string, cmd *cobra.Command) (dynamic.ClientPool, dis } // expandEnvObjs finds and expands templates for an environment -func expandEnvObjs(cmd *cobra.Command, env string, manager metadata.Manager) ([]*unstructured.Unstructured, error) { - expander, err := newExpander(cmd) +func expandEnvObjs(fs afero.Fs, cmd *cobra.Command, env string, manager metadata.Manager) ([]*unstructured.Unstructured, error) { + expander, err := newExpander(fs, cmd) if err != nil { return nil, err } diff --git a/cmd/root.go b/cmd/root.go index 947b07e4bc139fad3034a55541828828e27eab64..793dde9b50f9420a43f92891e89b137cd415eedb 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -29,7 +29,9 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "github.com/pkg/errors" log "github.com/sirupsen/logrus" + "github.com/spf13/afero" "github.com/spf13/cobra" "golang.org/x/crypto/ssh/terminal" "k8s.io/client-go/discovery" @@ -61,11 +63,16 @@ const ( flagComponentShort = "c" ) -var clientConfig clientcmd.ClientConfig -var overrides clientcmd.ConfigOverrides -var loadingRules clientcmd.ClientConfigLoadingRules +var ( + clientConfig clientcmd.ClientConfig + overrides clientcmd.ConfigOverrides + loadingRules clientcmd.ClientConfigLoadingRules + appFs afero.Fs +) func init() { + appFs = afero.NewOsFs() + RootCmd.PersistentFlags().CountP(flagVerbose, "v", "Increase verbosity. May be given multiple times.") // The "usual" clientcmd/kubectl flags @@ -222,9 +229,9 @@ func (f *logFormatter) Format(e *log.Entry) ([]byte, error) { return buf.Bytes(), nil } -func newExpander(cmd *cobra.Command) (*template.Expander, error) { +func newExpander(fs afero.Fs, cmd *cobra.Command) (*template.Expander, error) { flags := cmd.Flags() - spec := template.Expander{} + spec := template.NewExpander(fs) var err error spec.EnvJPath = filepath.SplitList(os.Getenv("KUBECFG_JPATH")) @@ -379,39 +386,64 @@ func overrideCluster(envName string, clientConfig clientcmd.ClientConfig, overri return fmt.Errorf("Attempting to deploy to environment '%s' at '%s', but cannot locate a server at that address", envName, env.Server) } -// expandEnvCmdObjs finds and expands templates for the family of commands of +type cmdObjExpanderConfig struct { + fs afero.Fs + cmd *cobra.Command + env string + components []string + cwd metadata.AbsPath +} + +// cmdObjExpander finds and expands templates for the family of commands of // the form `[<env>|-f <file-name>]`, e.g., `apply` and `delete`. That is, if // the user passes a list of files, we will expand all templates in those files, // while if a user passes an environment name, we will expand all component // files using that environment. -func expandEnvCmdObjs(cmd *cobra.Command, env string, components []string, cwd metadata.AbsPath) ([]*unstructured.Unstructured, error) { - expander, err := newExpander(cmd) +type cmdObjExpander struct { + config cmdObjExpanderConfig + templateExpanderFn func(afero.Fs, *cobra.Command) (*template.Expander, error) +} + +func newCmdObjExpander(c cmdObjExpanderConfig) *cmdObjExpander { + if c.fs == nil { + c.fs = appFs + } + + return &cmdObjExpander{ + config: c, + templateExpanderFn: newExpander, + } +} + +// Expands expands the templates. +func (te *cmdObjExpander) Expand() ([]*unstructured.Unstructured, error) { + expander, err := te.templateExpanderFn(te.config.fs, te.config.cmd) if err != nil { - return nil, err + return nil, errors.Wrap(err, "template expander") } // // Set up the template expander to be able to expand the ksonnet application. // - manager, err := metadata.Find(cwd) + manager, err := metadata.Find(te.config.cwd) if err != nil { - return nil, err + return nil, errors.Wrap(err, "find metadata") } libPath, vendorPath := manager.LibPaths() - metadataPath, mainPath, paramsPath, specPath := manager.EnvPaths(env) + metadataPath, mainPath, paramsPath, specPath := manager.EnvPaths(te.config.env) expander.FlagJpath = append([]string{string(libPath), string(vendorPath), string(metadataPath)}, expander.FlagJpath...) componentPaths, err := manager.ComponentPaths() if err != nil { - return nil, err + return nil, errors.Wrap(err, "component paths") } - baseObj, err := constructBaseObj(componentPaths, components) + baseObj, err := constructBaseObj(componentPaths, te.config.components) if err != nil { - return nil, err + return nil, errors.Wrap(err, "construct base object") } // diff --git a/cmd/show.go b/cmd/show.go index 02ba75970fa366575c5f2f0493f5d6fecffe4186..42196e59af845ffa95c20615ac7643778be10113 100644 --- a/cmd/show.go +++ b/cmd/show.go @@ -99,7 +99,13 @@ ks show dev -c redis -c nginx-server } wd := metadata.AbsPath(cwd) - objs, err := expandEnvCmdObjs(cmd, env, componentNames, wd) + te := newCmdObjExpander(cmdObjExpanderConfig{ + cmd: cmd, + env: env, + components: componentNames, + cwd: wd, + }) + objs, err := te.Expand() if err != nil { return err } diff --git a/cmd/validate.go b/cmd/validate.go index 3e629662928121ded981cb5589ac9c8691674b81..64afd822ba3238ac4785575db2b32ff6d9ad1619 100644 --- a/cmd/validate.go +++ b/cmd/validate.go @@ -66,7 +66,13 @@ var validateCmd = &cobra.Command{ return err } - objs, err := expandEnvCmdObjs(cmd, env, componentNames, wd) + te := newCmdObjExpander(cmdObjExpanderConfig{ + cmd: cmd, + env: env, + components: componentNames, + cwd: wd, + }) + objs, err := te.Expand() if err != nil { return err } diff --git a/template/expander.go b/template/expander.go index 93ea47b666b7683e1422df61da6293fbd30c1a44..1efe6d93784417dbd012d8990c64d2149db01c45 100644 --- a/template/expander.go +++ b/template/expander.go @@ -2,7 +2,6 @@ package template import ( "fmt" - "io/ioutil" "os" "strings" @@ -11,6 +10,7 @@ import ( jsonnet "github.com/google/go-jsonnet" "github.com/ksonnet/ksonnet/utils" log "github.com/sirupsen/logrus" + "github.com/spf13/afero" ) type Expander struct { @@ -24,8 +24,21 @@ type Expander struct { Resolver string FailAction string + + fs afero.Fs +} + +func NewExpander(fs afero.Fs) Expander { + if fs == nil { + fs = afero.NewOsFs() + } + + return Expander{ + fs: fs, + } } +// Expand expands paths to a slice of v1 Unstructured objects. func (spec *Expander) Expand(paths []string) ([]*unstructured.Unstructured, error) { vm, err := spec.jsonnetVM() if err != nil { @@ -34,7 +47,7 @@ func (spec *Expander) Expand(paths []string) ([]*unstructured.Unstructured, erro res := []*unstructured.Unstructured{} for _, path := range paths { - objs, err := utils.Read(vm, path) + objs, err := utils.Read(spec.fs, vm, path) if err != nil { return nil, fmt.Errorf("Error reading %s: %v", path, err) } @@ -83,7 +96,7 @@ func (spec *Expander) jsonnetVM() (*jsonnet.VM, error) { if len(kv) != 2 { return nil, fmt.Errorf("Failed to parse ext var files: missing '=' in %s", extvar) } - v, err := ioutil.ReadFile(kv[1]) + v, err := afero.ReadFile(spec.fs, kv[1]) if err != nil { return nil, err } @@ -110,7 +123,7 @@ func (spec *Expander) jsonnetVM() (*jsonnet.VM, error) { if len(kv) != 2 { return nil, fmt.Errorf("Failed to parse tla var files: missing '=' in %s", tlavar) } - v, err := ioutil.ReadFile(kv[1]) + v, err := afero.ReadFile(spec.fs, kv[1]) if err != nil { return nil, err } diff --git a/utils/acquire.go b/utils/acquire.go index 7b662ad7ebf2623c6068c0cace8b84413bd61ce5..004742ad8a8a628f483a1a0de520955bd18e2302 100644 --- a/utils/acquire.go +++ b/utils/acquire.go @@ -26,6 +26,7 @@ import ( jsonnet "github.com/google/go-jsonnet" log "github.com/sirupsen/logrus" + "github.com/spf13/afero" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/yaml" @@ -34,7 +35,7 @@ import ( // Read fetches and decodes K8s objects by path. // TODO: Replace this with something supporting more sophisticated // content negotiation. -func Read(vm *jsonnet.VM, path string) ([]runtime.Object, error) { +func Read(fs afero.Fs, vm *jsonnet.VM, path string) ([]runtime.Object, error) { ext := filepath.Ext(path) if ext == ".json" { f, err := os.Open(path) @@ -51,7 +52,7 @@ func Read(vm *jsonnet.VM, path string) ([]runtime.Object, error) { defer f.Close() return yamlReader(f) } else if ext == ".jsonnet" { - return jsonnetReader(vm, path) + return jsonnetReader(fs, vm, path) } return nil, fmt.Errorf("Unknown file extension: %s", path) @@ -125,8 +126,8 @@ func jsonWalk(obj interface{}) ([]interface{}, error) { } } -func jsonnetReader(vm *jsonnet.VM, path string) ([]runtime.Object, error) { - jsonnetBytes, err := ioutil.ReadFile(path) +func jsonnetReader(fs afero.Fs, vm *jsonnet.VM, path string) ([]runtime.Object, error) { + jsonnetBytes, err := afero.ReadFile(fs, path) if err != nil { return nil, err } diff --git a/utils/openapi_test.go b/utils/openapi_test.go index 17af0d5ce64aa6241774d63d18fcc462c19abab6..c71dd2c361a734601e95732aff30a81f8ec381da 100644 --- a/utils/openapi_test.go +++ b/utils/openapi_test.go @@ -18,13 +18,13 @@ package utils import ( "encoding/json" "fmt" - "io/ioutil" "path" "path/filepath" "strings" "testing" swagger "github.com/emicklei/go-restful-swagger12" + "github.com/spf13/afero" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" utilerrors "k8s.io/apimachinery/pkg/util/errors" @@ -32,11 +32,12 @@ import ( type schemaFromFile struct { dir string + fs afero.Fs } func (s schemaFromFile) SwaggerSchema(gv schema.GroupVersion) (*swagger.ApiDeclaration, error) { file := path.Join(s.dir, fmt.Sprintf("schema-%s.json", gv)) - data, err := ioutil.ReadFile(file) + data, err := afero.ReadFile(s.fs, file) if err != nil { return nil, err } @@ -50,7 +51,7 @@ func (s schemaFromFile) SwaggerSchema(gv schema.GroupVersion) (*swagger.ApiDecla } func TestValidate(t *testing.T) { - schemaReader := schemaFromFile{dir: filepath.FromSlash("../testdata")} + schemaReader := schemaFromFile{dir: filepath.FromSlash("../testdata"), fs: afero.NewOsFs()} s, err := NewSwaggerSchemaFor(schemaReader, schema.GroupVersion{Version: "v1"}) if err != nil { t.Fatalf("Error reading schema: %v", err)