diff --git a/cmd/delete.go b/cmd/delete.go
index 487d9b3d0ce4ff91d2453d23a838785347a0ddcb..ff7d60e68212be0b6b40042b5d09a6b6c982629c 100644
--- a/cmd/delete.go
+++ b/cmd/delete.go
@@ -47,7 +47,12 @@ var deleteCmd = &cobra.Command{
 			return err
 		}
 
-		objs, err := readObjs(cmd, args)
+		vm, err := newExpander(cmd)
+		if err != nil {
+			return err
+		}
+
+		objs, err := vm.Expand(args)
 		if err != nil {
 			return err
 		}
diff --git a/cmd/diff.go b/cmd/diff.go
index e1cf8dc6b978b64767020a1c97c79868f4e53e68..14a057407a762ba637835482d71ce54f6a00c856 100644
--- a/cmd/diff.go
+++ b/cmd/diff.go
@@ -52,7 +52,12 @@ var diffCmd = &cobra.Command{
 			return err
 		}
 
-		objs, err := readObjs(cmd, args)
+		vm, err := newExpander(cmd)
+		if err != nil {
+			return err
+		}
+
+		objs, err := vm.Expand(args)
 		if err != nil {
 			return err
 		}
diff --git a/cmd/root.go b/cmd/root.go
index 9d188b6fef8ca987ac4d47bb6cc187f9dd115eac..33b920fe4d62f06aa6ec650115ed4357a34c1f50 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -21,21 +21,18 @@ import (
 	goflag "flag"
 	"fmt"
 	"io"
-	"io/ioutil"
-	"net/http"
 	"os"
 	"path/filepath"
 	"strings"
 
 	log "github.com/sirupsen/logrus"
 	"github.com/spf13/cobra"
-	jsonnet "github.com/strickyak/jsonnet_cgo"
 	"golang.org/x/crypto/ssh/terminal"
-	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
 	"k8s.io/client-go/discovery"
 	"k8s.io/client-go/dynamic"
 	"k8s.io/client-go/tools/clientcmd"
 
+	"github.com/ksonnet/kubecfg/template"
 	"github.com/ksonnet/kubecfg/utils"
 
 	// Register auth plugins
@@ -155,176 +152,49 @@ func (f *logFormatter) Format(e *log.Entry) ([]byte, error) {
 	return buf.Bytes(), nil
 }
 
-// JsonnetVM constructs a new jsonnet.VM, according to command line
-// flags
-func JsonnetVM(cmd *cobra.Command) (*jsonnet.VM, error) {
-	vm := jsonnet.Make()
+func newExpander(cmd *cobra.Command) (*template.Expander, error) {
 	flags := cmd.Flags()
+	spec := template.Expander{}
+	var err error
 
-	jpath := os.Getenv("KUBECFG_JPATH")
-	for _, p := range filepath.SplitList(jpath) {
-		log.Debugln("Adding jsonnet search path", p)
-		vm.JpathAdd(p)
-	}
+	spec.EnvJPath = filepath.SplitList(os.Getenv("KUBECFG_JPATH"))
 
 	jpath, err := flags.GetString(flagJpath)
 	if err != nil {
 		return nil, err
 	}
-	for _, p := range filepath.SplitList(jpath) {
-		log.Debugln("Adding jsonnet search path", p)
-		vm.JpathAdd(p)
-	}
+	spec.FlagJpath = filepath.SplitList(jpath)
 
-	extvars, err := flags.GetStringSlice(flagExtVar)
+	spec.ExtVars, err = flags.GetStringSlice(flagExtVar)
 	if err != nil {
 		return nil, err
 	}
-	for _, extvar := range extvars {
-		kv := strings.SplitN(extvar, "=", 2)
-		switch len(kv) {
-		case 1:
-			v, present := os.LookupEnv(kv[0])
-			if present {
-				vm.ExtVar(kv[0], v)
-			} else {
-				return nil, fmt.Errorf("Missing environment variable: %s", kv[0])
-			}
-		case 2:
-			vm.ExtVar(kv[0], kv[1])
-		}
-	}
-
-	extvarfiles, err := flags.GetStringSlice(flagExtVarFile)
-	if err != nil {
-		return nil, err
-	}
-	for _, extvar := range extvarfiles {
-		kv := strings.SplitN(extvar, "=", 2)
-		if len(kv) != 2 {
-			return nil, fmt.Errorf("Failed to parse %s: missing '=' in %s", flagExtVarFile, extvar)
-		}
-		v, err := ioutil.ReadFile(kv[1])
-		if err != nil {
-			return nil, err
-		}
-		vm.ExtVar(kv[0], string(v))
-	}
 
-	tlavars, err := flags.GetStringSlice(flagTlaVar)
+	spec.ExtVarFiles, err = flags.GetStringSlice(flagExtVarFile)
 	if err != nil {
 		return nil, err
 	}
-	for _, tlavar := range tlavars {
-		kv := strings.SplitN(tlavar, "=", 2)
-		switch len(kv) {
-		case 1:
-			v, present := os.LookupEnv(kv[0])
-			if present {
-				vm.TlaVar(kv[0], v)
-			} else {
-				return nil, fmt.Errorf("Missing environment variable: %s", kv[0])
-			}
-		case 2:
-			vm.TlaVar(kv[0], kv[1])
-		}
-	}
 
-	tlavarfiles, err := flags.GetStringSlice(flagTlaVarFile)
+	spec.TlaVars, err = flags.GetStringSlice(flagTlaVar)
 	if err != nil {
 		return nil, err
 	}
-	for _, tlavar := range tlavarfiles {
-		kv := strings.SplitN(tlavar, "=", 2)
-		if len(kv) != 2 {
-			return nil, fmt.Errorf("Failed to parse %s: missing '=' in %s", flagTlaVarFile, tlavar)
-		}
-		v, err := ioutil.ReadFile(kv[1])
-		if err != nil {
-			return nil, err
-		}
-		vm.TlaVar(kv[0], string(v))
-	}
 
-	resolver, err := buildResolver(cmd)
+	spec.TlaVarFiles, err = flags.GetStringSlice(flagTlaVarFile)
 	if err != nil {
 		return nil, err
 	}
-	utils.RegisterNativeFuncs(vm, resolver)
-
-	return vm, nil
-}
 
-func buildResolver(cmd *cobra.Command) (utils.Resolver, error) {
-	flags := cmd.Flags()
-	resolver, err := flags.GetString(flagResolver)
-	if err != nil {
-		return nil, err
-	}
-	failAction, err := flags.GetString(flagResolvFail)
+	spec.Resolver, err = flags.GetString(flagResolver)
 	if err != nil {
 		return nil, err
 	}
-
-	ret := resolverErrorWrapper{}
-
-	switch failAction {
-	case "ignore":
-		ret.OnErr = func(error) error { return nil }
-	case "warn":
-		ret.OnErr = func(err error) error {
-			log.Warning(err.Error())
-			return nil
-		}
-	case "error":
-		ret.OnErr = func(err error) error { return err }
-	default:
-		return nil, fmt.Errorf("Bad value for --%s: %s", flagResolvFail, failAction)
-	}
-
-	switch resolver {
-	case "noop":
-		ret.Inner = utils.NewIdentityResolver()
-	case "registry":
-		ret.Inner = utils.NewRegistryResolver(&http.Client{
-			Transport: utils.NewAuthTransport(http.DefaultTransport),
-		})
-	default:
-		return nil, fmt.Errorf("Bad value for --%s: %s", flagResolver, resolver)
-	}
-
-	return &ret, nil
-}
-
-type resolverErrorWrapper struct {
-	Inner utils.Resolver
-	OnErr func(error) error
-}
-
-func (r *resolverErrorWrapper) Resolve(image *utils.ImageName) error {
-	err := r.Inner.Resolve(image)
-	if err != nil {
-		err = r.OnErr(err)
-	}
-	return err
-}
-
-func readObjs(cmd *cobra.Command, paths []string) ([]*unstructured.Unstructured, error) {
-	vm, err := JsonnetVM(cmd)
+	spec.FailAction, err = flags.GetString(flagResolvFail)
 	if err != nil {
 		return nil, err
 	}
-	defer vm.Destroy()
 
-	res := []*unstructured.Unstructured{}
-	for _, path := range paths {
-		objs, err := utils.Read(vm, path)
-		if err != nil {
-			return nil, fmt.Errorf("Error reading %s: %v", path, err)
-		}
-		res = append(res, utils.FlattenToV1(objs)...)
-	}
-	return res, nil
+	return &spec, nil
 }
 
 // For debugging
diff --git a/cmd/show.go b/cmd/show.go
index 6e98a35e67135b2489afc8e200e6aca3155a8c70..acc0ae0f36bbe7fc852e3fede5091f42267b0721 100644
--- a/cmd/show.go
+++ b/cmd/show.go
@@ -39,7 +39,12 @@ var showCmd = &cobra.Command{
 		flags := cmd.Flags()
 		out := cmd.OutOrStdout()
 
-		objs, err := readObjs(cmd, args)
+		vm, err := newExpander(cmd)
+		if err != nil {
+			return err
+		}
+
+		objs, err := vm.Expand(args)
 		if err != nil {
 			return err
 		}
diff --git a/cmd/update.go b/cmd/update.go
index c48696fe45bf2c3e7253d86c89dbc8500a7b855e..0efc1238befbcec3d525046c2faf2dc925078cdc 100644
--- a/cmd/update.go
+++ b/cmd/update.go
@@ -89,7 +89,12 @@ var updateCmd = &cobra.Command{
 			return err
 		}
 
-		objs, err := readObjs(cmd, args)
+		vm, err := newExpander(cmd)
+		if err != nil {
+			return err
+		}
+
+		objs, err := vm.Expand(args)
 		if err != nil {
 			return err
 		}
diff --git a/cmd/validate.go b/cmd/validate.go
index f44e9b4c0bf003dcd6070a21c6619e6da48c987b..dec8f6c6f8fa0cc27fa2a790615a7f7180afa991 100644
--- a/cmd/validate.go
+++ b/cmd/validate.go
@@ -32,10 +32,16 @@ var validateCmd = &cobra.Command{
 	Use:   "validate",
 	Short: "Compare generated manifest against server OpenAPI spec",
 	RunE: func(cmd *cobra.Command, args []string) error {
-		objs, err := readObjs(cmd, args)
+		vm, err := newExpander(cmd)
 		if err != nil {
 			return err
 		}
+
+		objs, err := vm.Expand(args)
+		if err != nil {
+			return err
+		}
+
 		_, disco, err := restClientPool(cmd)
 		if err != nil {
 			return err
diff --git a/template/expander.go b/template/expander.go
new file mode 100644
index 0000000000000000000000000000000000000000..d414db8c6ab33db2e46396210ab3c9c13401c381
--- /dev/null
+++ b/template/expander.go
@@ -0,0 +1,122 @@
+package template
+
+import (
+	"fmt"
+	"io/ioutil"
+	"os"
+	"strings"
+
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+
+	"github.com/ksonnet/kubecfg/utils"
+	log "github.com/sirupsen/logrus"
+	jsonnet "github.com/strickyak/jsonnet_cgo"
+)
+
+type Expander struct {
+	EnvJPath    []string
+	FlagJpath   []string
+	ExtVars     []string
+	ExtVarFiles []string
+	TlaVars     []string
+	TlaVarFiles []string
+
+	Resolver   string
+	FailAction string
+}
+
+func (spec *Expander) Expand(paths []string) ([]*unstructured.Unstructured, error) {
+	vm, err := spec.jsonnetVM()
+	if err != nil {
+		return nil, err
+	}
+	defer vm.Destroy()
+
+	res := []*unstructured.Unstructured{}
+	for _, path := range paths {
+		objs, err := utils.Read(vm, path)
+		if err != nil {
+			return nil, fmt.Errorf("Error reading %s: %v", path, err)
+		}
+		res = append(res, utils.FlattenToV1(objs)...)
+	}
+	return res, nil
+}
+
+// JsonnetVM constructs a new jsonnet.VM, according to command line
+// flags
+func (spec *Expander) jsonnetVM() (*jsonnet.VM, error) {
+	vm := jsonnet.Make()
+
+	for _, p := range spec.EnvJPath {
+		log.Debugln("Adding jsonnet search path", p)
+		vm.JpathAdd(p)
+	}
+
+	for _, p := range spec.FlagJpath {
+		log.Debugln("Adding jsonnet search path", p)
+		vm.JpathAdd(p)
+	}
+
+	for _, extvar := range spec.ExtVars {
+		kv := strings.SplitN(extvar, "=", 2)
+		switch len(kv) {
+		case 1:
+			v, present := os.LookupEnv(kv[0])
+			if present {
+				vm.ExtVar(kv[0], v)
+			} else {
+				return nil, fmt.Errorf("Missing environment variable: %s", kv[0])
+			}
+		case 2:
+			vm.ExtVar(kv[0], kv[1])
+		}
+	}
+
+	for _, extvar := range spec.ExtVarFiles {
+		kv := strings.SplitN(extvar, "=", 2)
+		if len(kv) != 2 {
+			return nil, fmt.Errorf("Failed to parse ext var files: missing '=' in %s", extvar)
+		}
+		v, err := ioutil.ReadFile(kv[1])
+		if err != nil {
+			return nil, err
+		}
+		vm.ExtVar(kv[0], string(v))
+	}
+
+	for _, tlavar := range spec.TlaVars {
+		kv := strings.SplitN(tlavar, "=", 2)
+		switch len(kv) {
+		case 1:
+			v, present := os.LookupEnv(kv[0])
+			if present {
+				vm.TlaVar(kv[0], v)
+			} else {
+				return nil, fmt.Errorf("Missing environment variable: %s", kv[0])
+			}
+		case 2:
+			vm.TlaVar(kv[0], kv[1])
+		}
+	}
+
+	for _, tlavar := range spec.TlaVarFiles {
+		kv := strings.SplitN(tlavar, "=", 2)
+		if len(kv) != 2 {
+			return nil, fmt.Errorf("Failed to parse tla var files: missing '=' in %s", tlavar)
+		}
+		v, err := ioutil.ReadFile(kv[1])
+		if err != nil {
+			return nil, err
+		}
+		vm.TlaVar(kv[0], string(v))
+	}
+
+	resolver, err := spec.buildResolver()
+	if err != nil {
+		return nil, err
+	}
+	utils.RegisterNativeFuncs(vm, resolver)
+
+	return vm, nil
+}
diff --git a/template/resolver.go b/template/resolver.go
new file mode 100644
index 0000000000000000000000000000000000000000..6c18c57474d16764d7e648d57d0912eefa528b8b
--- /dev/null
+++ b/template/resolver.go
@@ -0,0 +1,53 @@
+package template
+
+import (
+	"fmt"
+	"net/http"
+
+	"github.com/ksonnet/kubecfg/utils"
+	log "github.com/sirupsen/logrus"
+)
+
+func (spec *Expander) buildResolver() (utils.Resolver, error) {
+	ret := resolverErrorWrapper{}
+
+	switch spec.FailAction {
+	case "ignore":
+		ret.OnErr = func(error) error { return nil }
+	case "warn":
+		ret.OnErr = func(err error) error {
+			log.Warning(err.Error())
+			return nil
+		}
+	case "error":
+		ret.OnErr = func(err error) error { return err }
+	default:
+		return nil, fmt.Errorf("Unknown resolve failure type: %s", spec.FailAction)
+	}
+
+	switch spec.Resolver {
+	case "noop":
+		ret.Inner = utils.NewIdentityResolver()
+	case "registry":
+		ret.Inner = utils.NewRegistryResolver(&http.Client{
+			Transport: utils.NewAuthTransport(http.DefaultTransport),
+		})
+	default:
+		return nil, fmt.Errorf("Unknown resolver type: %s", spec.Resolver)
+	}
+
+	return &ret, nil
+}
+
+type resolverErrorWrapper struct {
+	Inner utils.Resolver
+	OnErr func(error) error
+}
+
+func (r *resolverErrorWrapper) Resolve(image *utils.ImageName) error {
+	err := r.Inner.Resolve(image)
+	if err != nil {
+		err = r.OnErr(err)
+	}
+	return err
+}