diff --git a/.travis.yml b/.travis.yml
index 95349f58110c4bdeb3bcb76d05722da70bf3279a..43283d79edb76ee743bef0d20b4aa248421adc18 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -2,7 +2,6 @@ language: go
 
 go:
   - 1.8.x
-  - 1.7.x
 
 os:
   - linux
diff --git a/Makefile b/Makefile
index 8612b0fa7b5c34f9937b6700ff4e3b8b0f33675f..e9d46b037bacc2def2bb760a69cab2a0925f59f1 100644
--- a/Makefile
+++ b/Makefile
@@ -20,7 +20,9 @@ EXTRA_GO_FLAGS =
 GO_FLAGS = -ldflags="-X main.version=$(VERSION) $(GO_LDFLAGS)" $(EXTRA_GO_FLAGS)
 GOFMT = gofmt
 
-JSONNET_FILES = lib/kubecfg_test.jsonnet examples/guestbook.jsonnet
+KCFG_TEST_FILE = lib/kubecfg_test.jsonnet
+GUESTBOOK_FILE = examples/guestbook.jsonnet
+JSONNET_FILES = $(KCFG_TEST_FILE) $(GUESTBOOK_FILE)
 # TODO: Simplify this once ./... ignores ./vendor
 GO_PACKAGES = ./cmd/... ./utils/... ./pkg/... ./metadata/...
 
@@ -36,7 +38,7 @@ gotest:
 
 jsonnettest: kubecfg $(JSONNET_FILES)
 #	TODO: use `kubecfg check` once implemented
-	./kubecfg -J lib show $(JSONNET_FILES) >/dev/null
+	./kubecfg -J lib show -f $(KCFG_TEST_FILE) -f $(GUESTBOOK_FILE) >/dev/null
 
 vet:
 	$(GO) vet $(GO_FLAGS) $(GO_PACKAGES)
diff --git a/cmd/delete.go b/cmd/delete.go
index 487d9b3d0ce4ff91d2453d23a838785347a0ddcb..4f31ef3a3852695ff815f395af62666a538d8a25 100644
--- a/cmd/delete.go
+++ b/cmd/delete.go
@@ -33,6 +33,7 @@ const (
 
 func init() {
 	RootCmd.AddCommand(deleteCmd)
+	addEnvCmdFlags(deleteCmd)
 	deleteCmd.PersistentFlags().Int64(flagGracePeriod, -1, "Number of seconds given to resources to terminate gracefully. A negative value is ignored")
 }
 
@@ -47,7 +48,17 @@ var deleteCmd = &cobra.Command{
 			return err
 		}
 
-		objs, err := readObjs(cmd, args)
+		files, err := getFiles(cmd, args)
+		if err != nil {
+			return err
+		}
+
+		vm, err := newExpander(cmd)
+		if err != nil {
+			return err
+		}
+
+		objs, err := vm.Expand(files)
 		if err != nil {
 			return err
 		}
diff --git a/cmd/diff.go b/cmd/diff.go
index e1cf8dc6b978b64767020a1c97c79868f4e53e68..eab76ed727eb57bce9bef6b22858b88c28019588 100644
--- a/cmd/diff.go
+++ b/cmd/diff.go
@@ -36,23 +36,33 @@ const flagDiffStrategy = "diff-strategy"
 var ErrDiffFound = fmt.Errorf("Differences found.")
 
 func init() {
+	addEnvCmdFlags(diffCmd)
 	diffCmd.PersistentFlags().String(flagDiffStrategy, "all", "Diff strategy, all or subset.")
 	RootCmd.AddCommand(diffCmd)
 }
 
 var diffCmd = &cobra.Command{
-	Use:   "diff",
+	Use:   "diff [<env>|-f <file-or-dir>]",
 	Short: "Display differences between server and local config",
 	RunE: func(cmd *cobra.Command, args []string) error {
 		out := cmd.OutOrStdout()
-
 		flags := cmd.Flags()
 		diffStrategy, err := flags.GetString(flagDiffStrategy)
 		if err != nil {
 			return err
 		}
 
-		objs, err := readObjs(cmd, args)
+		files, err := getFiles(cmd, args)
+		if err != nil {
+			return err
+		}
+
+		vm, err := newExpander(cmd)
+		if err != nil {
+			return err
+		}
+
+		objs, err := vm.Expand(files)
 		if err != nil {
 			return err
 		}
@@ -122,6 +132,22 @@ var diffCmd = &cobra.Command{
 		}
 		return nil
 	},
+	Long: `Display differences between server and local configuration.
+
+ksonnet applications are accepted, as well as normal JSON, YAML, and Jsonnet
+files.`,
+	Example: `  # Show diff between resources described in a local ksonnet application and
+  # the cluster referenced by the 'dev' environment. Can be used in any
+  # subdirectory of the application.
+  ksonnet diff -e=dev
+
+  # Show diff between resources described in a YAML file and the cluster
+  # referenced in '$KUBECONFIG'.
+  ksonnet diff -f ./pod.yaml
+
+  # Show diff between resources described in a YAML file and the cluster
+  # referred to by './kubeconfig'.
+  ksonnet diff --kubeconfig=./kubeconfig -f ./pod.yaml`,
 }
 
 func removeFields(config, live interface{}) interface{} {
diff --git a/cmd/init.go b/cmd/init.go
new file mode 100644
index 0000000000000000000000000000000000000000..ad4a7358801fb28ac28bd221d677788277321059
--- /dev/null
+++ b/cmd/init.go
@@ -0,0 +1,102 @@
+// Copyright 2017 The kubecfg 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 cmd
+
+import (
+	"fmt"
+	"os"
+	"path"
+	"path/filepath"
+
+	"github.com/ksonnet/kubecfg/metadata"
+	"github.com/ksonnet/kubecfg/pkg/kubecfg"
+	"github.com/spf13/cobra"
+)
+
+const (
+	flagAPISpec = "api-spec"
+)
+
+func init() {
+	RootCmd.AddCommand(initCmd)
+	// TODO: We need to make this default to checking the `kubeconfig` file.
+	initCmd.PersistentFlags().String(flagAPISpec, "version:v1.7.0", "Manually specify API version from OpenAPI schema, cluster, or Kubernetes version")
+}
+
+var initCmd = &cobra.Command{
+	Use:   "init <app-name>",
+	Short: "Initialize a ksonnet project",
+	RunE: func(cmd *cobra.Command, args []string) error {
+		flags := cmd.Flags()
+		if len(args) != 1 {
+			return fmt.Errorf("'init' takes a single argument that names the application we're initializing")
+		}
+
+		appName := args[0]
+		appDir, err := filepath.Abs(filepath.Dir(os.Args[0]))
+		if err != nil {
+			return err
+		}
+		appRoot := metadata.AbsPath(path.Join(appDir, appName))
+
+		specFlag, err := flags.GetString(flagAPISpec)
+		if err != nil {
+			return err
+		}
+
+		c, err := kubecfg.NewInitCmd(appRoot, specFlag)
+		if err != nil {
+			return err
+		}
+
+		return c.Run()
+	},
+	Long: `Initialize a ksonnet project in a new directory, 'app-name'. This process
+consists of two steps:
+
+1. Generating ksonnet-lib. Users can set flags to generate the library based on
+   a variety of data, including server configuration and an OpenAPI
+   specification of a Kubernetes build. By default, this is generated from the
+   capabilities of the cluster specified in the cluster of the current context
+   specified in $KUBECONFIG.
+2. Generating the following tree in the current directory.
+
+   app-name/
+     .gitignore     Default .gitignore; can customize VCS
+     .ksonnet/      Metadata for ksonnet
+     envs/          Env specs (defaults: dev, test, prod)
+       params.yaml  Specifies the schema of the environments
+       dev.yaml
+       test.yaml
+       prod.yaml
+       us-east.yaml [Example of user-generated env]
+     components/    Top-level Kubernetes objects defining application
+     lib/           user-written .libsonnet files
+     vendor/        mixin libraries, prototypes
+`,
+	Example: `  # Initialize ksonnet application, using the capabilities of live cluster
+  # specified in the $KUBECONFIG environment variable (specifically: the
+  # current context) to generate 'ksonnet-lib'.
+  ksonnet init app-name
+
+  # Initialize ksonnet application, using the OpenAPI specification generated
+  # in the Kubenetes v1.7.1 build to generate 'ksonnet-lib'.
+  ksonnet init app-name --api-spec=version:v1.7.1
+
+  # Initialize ksonnet application, using an OpenAPI specification file
+  # generated by a build of Kubernetes to generate 'ksonnet-lib'.
+  ksonnet init app-name --api-spec=file:swagger.json`,
+}
diff --git a/cmd/root.go b/cmd/root.go
index 9d188b6fef8ca987ac4d47bb6cc187f9dd115eac..b0f9f5c5bcc2acca4f0a7715a8c041ecf5c821fa 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -21,21 +21,20 @@ 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/metadata"
+	"github.com/ksonnet/kubecfg/pkg/kubecfg"
+	"github.com/ksonnet/kubecfg/template"
 	"github.com/ksonnet/kubecfg/utils"
 
 	// Register auth plugins
@@ -51,6 +50,11 @@ const (
 	flagTlaVarFile = "tla-str-file"
 	flagResolver   = "resolve-images"
 	flagResolvFail = "resolve-images-error"
+
+	// For use in the commands (e.g., diff, update, delete) that require either an
+	// environment or the -f flag.
+	flagFile      = "file"
+	flagFileShort = "f"
 )
 
 var clientConfig clientcmd.ClientConfig
@@ -155,176 +159,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)
+	spec.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.TlaVars, err = flags.GetStringSlice(flagTlaVar)
 	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)
-	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)
+	spec.Resolver, err = flags.GetString(flagResolver)
 	if err != nil {
 		return nil, err
 	}
-	failAction, err := flags.GetString(flagResolvFail)
+	spec.FailAction, err = flags.GetString(flagResolvFail)
 	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)
-	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
@@ -356,3 +233,39 @@ func restClientPool(cmd *cobra.Command) (dynamic.ClientPool, discovery.Discovery
 	pool := dynamic.NewClientPool(conf, mapper, pathresolver)
 	return pool, discoCache, nil
 }
+
+func addEnvCmdFlags(cmd *cobra.Command) {
+	cmd.PersistentFlags().StringArrayP(flagFile, flagFileShort, nil, "Filename or directory that contains the configuration to apply (accepts YAML, JSON, and Jsonnet)")
+}
+
+func parseEnvCmd(cmd *cobra.Command, args []string) (*string, []string, error) {
+	flags := cmd.Flags()
+
+	files, err := flags.GetStringArray(flagFile)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	var env *string
+	if len(args) == 1 {
+		env = &args[0]
+	}
+
+	return env, files, nil
+}
+
+// TODO: Remove this and use `kubecfg.GetFiles` when we move commands into
+// `pkg`.
+func getFiles(cmd *cobra.Command, args []string) ([]string, error) {
+	env, files, err := parseEnvCmd(cmd, args)
+	if err != nil {
+		return nil, err
+	}
+
+	cwd, err := os.Getwd()
+	if err != nil {
+		return nil, err
+	}
+
+	return kubecfg.GetFiles(metadata.AbsPath(cwd), env, files)
+}
diff --git a/cmd/show.go b/cmd/show.go
index 6e98a35e67135b2489afc8e200e6aca3155a8c70..e9c021a6c19a0da50c8655a5ae09456ec86c3055 100644
--- a/cmd/show.go
+++ b/cmd/show.go
@@ -29,17 +29,28 @@ const (
 
 func init() {
 	RootCmd.AddCommand(showCmd)
+	addEnvCmdFlags(showCmd)
 	showCmd.PersistentFlags().StringP(flagFormat, "o", "yaml", "Output format.  Supported values are: json, yaml")
 }
 
 var showCmd = &cobra.Command{
-	Use:   "show",
+	Use:   "show [<env>|-f <file-or-dir>]",
 	Short: "Show expanded resource definitions",
 	RunE: func(cmd *cobra.Command, args []string) error {
 		flags := cmd.Flags()
 		out := cmd.OutOrStdout()
 
-		objs, err := readObjs(cmd, args)
+		files, err := getFiles(cmd, args)
+		if err != nil {
+			return err
+		}
+
+		vm, err := newExpander(cmd)
+		if err != nil {
+			return err
+		}
+
+		objs, err := vm.Expand(files)
 		if err != nil {
 			return err
 		}
diff --git a/cmd/show_test.go b/cmd/show_test.go
index c404abe810b3429858133b04a343fdc4ef25ffc4..316dc6d000372a7397024fdb58560ab5466feb22 100644
--- a/cmd/show_test.go
+++ b/cmd/show_test.go
@@ -81,7 +81,7 @@ func TestShow(t *testing.T) {
 		output := cmdOutput(t, []string{"show",
 			"-J", filepath.FromSlash("../testdata/lib"),
 			"-o", format,
-			filepath.FromSlash("../testdata/test.jsonnet"),
+			"-f", filepath.FromSlash("../testdata/test.jsonnet"),
 			"-V", "aVar=aVal",
 			"-V", "anVar",
 			"--ext-str-file", "filevar=" + filepath.FromSlash("../testdata/extvar.file"),
diff --git a/cmd/update.go b/cmd/update.go
index c48696fe45bf2c3e7253d86c89dbc8500a7b855e..810f2b8af91984c8aa8d973eaebad9a5eb861dab 100644
--- a/cmd/update.go
+++ b/cmd/update.go
@@ -16,8 +16,11 @@
 package cmd
 
 import (
+	"os"
+
 	"github.com/spf13/cobra"
 
+	"github.com/ksonnet/kubecfg/metadata"
 	"github.com/ksonnet/kubecfg/pkg/kubecfg"
 )
 
@@ -45,6 +48,8 @@ const (
 
 func init() {
 	RootCmd.AddCommand(updateCmd)
+
+	addEnvCmdFlags(updateCmd)
 	updateCmd.PersistentFlags().Bool(flagCreate, true, "Create missing resources")
 	updateCmd.PersistentFlags().Bool(flagSkipGc, false, "Don't perform garbage collection, even with --"+flagGcTag)
 	updateCmd.PersistentFlags().String(flagGcTag, "", "Add this tag to updated objects, and garbage collect existing objects with this tag and not in config")
@@ -52,13 +57,20 @@ func init() {
 }
 
 var updateCmd = &cobra.Command{
-	Use:   "update",
-	Short: "Update Kubernetes resources with local config",
+	Use: "update [<env>|-f <file-or-dir>]",
+	Short: `Update (or optionally create) Kubernetes resources on the cluster using the
+local configuration. Accepts JSON, YAML, or Jsonnet.`,
 	RunE: func(cmd *cobra.Command, args []string) error {
 		flags := cmd.Flags()
 		var err error
+
 		c := kubecfg.UpdateCmd{}
 
+		c.Environment, c.Files, err = parseEnvCmd(cmd, args)
+		if err != nil {
+			return err
+		}
+
 		c.Create, err = flags.GetBool(flagCreate)
 		if err != nil {
 			return err
@@ -89,11 +101,37 @@ var updateCmd = &cobra.Command{
 			return err
 		}
 
-		objs, err := readObjs(cmd, args)
+		c.Expander, err = newExpander(cmd)
+		if err != nil {
+			return err
+		}
+
+		cwd, err := os.Getwd()
 		if err != nil {
 			return err
 		}
 
-		return c.Run(objs)
+		return c.Run(metadata.AbsPath(cwd))
 	},
+	Long: `Update (or optionally create) Kubernetes resources on the cluster using the
+local configuration. Use the '--create' flag to control whether we create them
+if they do not exist (default: true).
+
+ksonnet applications are accepted, as well as normal JSON, YAML, and Jsonnet
+files.`,
+	Example: `  # Create or update all resources described in a ksonnet application, and
+  # running in the 'dev' environment. Can be used in any subdirectory of the
+  # application.
+  ksonnet update dev
+
+  # Create or update resources described in a YAML file. Automatically picks up
+  # the cluster's location from '$KUBECONFIG'.
+  ksonnet appy -f ./pod.yaml
+
+  # Update resources described in a YAML file, and running in cluster referred
+  # to by './kubeconfig'.
+  ksonnet update --kubeconfig=./kubeconfig -f ./pod.yaml
+
+  # Display set of actions we will execute when we run 'update'.
+  ksonnet update dev --dry-run`,
 }
diff --git a/cmd/validate.go b/cmd/validate.go
index f44e9b4c0bf003dcd6070a21c6619e6da48c987b..abdeb39640e8b56053e58046ff76cb03fadb7c26 100644
--- a/cmd/validate.go
+++ b/cmd/validate.go
@@ -26,16 +26,28 @@ import (
 
 func init() {
 	RootCmd.AddCommand(validateCmd)
+	addEnvCmdFlags(validateCmd)
 }
 
 var validateCmd = &cobra.Command{
-	Use:   "validate",
+	Use:   "validate [<env>|-f <file-or-dir>]",
 	Short: "Compare generated manifest against server OpenAPI spec",
 	RunE: func(cmd *cobra.Command, args []string) error {
-		objs, err := readObjs(cmd, args)
+		files, err := getFiles(cmd, args)
 		if err != nil {
 			return err
 		}
+
+		vm, err := newExpander(cmd)
+		if err != nil {
+			return err
+		}
+
+		objs, err := vm.Expand(files)
+		if err != nil {
+			return err
+		}
+
 		_, disco, err := restClientPool(cmd)
 		if err != nil {
 			return err
@@ -69,4 +81,20 @@ var validateCmd = &cobra.Command{
 
 		return nil
 	},
+	Long: `Validate that an application or file is compliant with the Kubernetes
+specification.
+
+ksonnet applications are accepted, as well as normal JSON, YAML, and Jsonnet
+files.`,
+	Example: `  # Validate all resources described in a ksonnet application, expanding
+  # ksonnet code with 'dev' environment where necessary (i.e., not YAML, JSON,
+  # or non-ksonnet Jsonnet code).
+  ksonnet validate -e=dev
+
+  # Validate resources described in a YAML file.
+  ksonnet validate -f ./pod.yaml
+
+  # Validate resources described in a Jsonnet file. Does not expand using
+  # environment bindings.
+  ksonnet validate -f ./pod.jsonnet`,
 }
diff --git a/metadata/clusterspec.go b/metadata/clusterspec.go
index bbe44dd15bdc584da78dcd062f2cc11441ee948e..a3c3f02b56c595cc19d5b17aedc400072986f1f0 100644
--- a/metadata/clusterspec.go
+++ b/metadata/clusterspec.go
@@ -1,15 +1,49 @@
 package metadata
 
 import (
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"path/filepath"
+	"strings"
+
 	"github.com/spf13/afero"
 )
 
+const (
+	k8sVersionURLTemplate = "https://raw.githubusercontent.com/kubernetes/kubernetes/%s/api/openapi-spec/swagger.json"
+)
+
+func parseClusterSpec(specFlag string, fs afero.Fs) (ClusterSpec, error) {
+	split := strings.SplitN(specFlag, ":", 2)
+	if len(split) <= 1 || split[1] == "" {
+		return nil, fmt.Errorf("Invalid API specification '%s'", specFlag)
+	}
+
+	switch split[0] {
+	case "version":
+		return &clusterSpecVersion{k8sVersion: split[1]}, nil
+	case "file":
+		abs, err := filepath.Abs(split[1])
+		if err != nil {
+			return nil, err
+		}
+		absPath := AbsPath(abs)
+		return &clusterSpecFile{specPath: absPath, fs: fs}, nil
+	case "url":
+		return &clusterSpecLive{apiServerURL: split[1]}, nil
+	default:
+		return nil, fmt.Errorf("Could not parse cluster spec '%s'", specFlag)
+	}
+}
+
 type clusterSpecFile struct {
 	specPath AbsPath
+	fs       afero.Fs
 }
 
 func (cs *clusterSpecFile) data() ([]byte, error) {
-	return afero.ReadFile(appFS, string(cs.specPath))
+	return afero.ReadFile(cs.fs, string(cs.specPath))
 }
 
 func (cs *clusterSpecFile) resource() string {
@@ -21,8 +55,7 @@ type clusterSpecLive struct {
 }
 
 func (cs *clusterSpecLive) data() ([]byte, error) {
-	// TODO: Implement getting spec from path, k8sVersion, and URL.
-	panic("Not implemented")
+	return nil, fmt.Errorf("Initializing from OpenAPI spec in live cluster is not implemented")
 }
 
 func (cs *clusterSpecLive) resource() string {
@@ -34,8 +67,20 @@ type clusterSpecVersion struct {
 }
 
 func (cs *clusterSpecVersion) data() ([]byte, error) {
-	// TODO: Implement getting spec from path, k8sVersion, and URL.
-	panic("Not implemented")
+	versionURL := fmt.Sprintf(k8sVersionURLTemplate, cs.k8sVersion)
+	resp, err := http.Get(versionURL)
+	if err != nil {
+		return nil, err
+	}
+	defer resp.Body.Close()
+
+	if resp.StatusCode != 200 {
+		return nil, fmt.Errorf(
+			"Recieved status code '%d' when trying to retrieve OpenAPI schema for cluster version '%s' from URL '%s'",
+			resp.StatusCode, cs.k8sVersion, versionURL)
+	}
+
+	return ioutil.ReadAll(resp.Body)
 }
 
 func (cs *clusterSpecVersion) resource() string {
diff --git a/metadata/clusterspec_test.go b/metadata/clusterspec_test.go
index 4d13beda69ff703293c4cf29e4546c30a51aeccf..3a4f28f896a8e4e04d196ede6efe1bf14c65fad9 100644
--- a/metadata/clusterspec_test.go
+++ b/metadata/clusterspec_test.go
@@ -12,13 +12,13 @@ type parseSuccess struct {
 
 var successTests = []parseSuccess{
 	{"version:v1.7.1", &clusterSpecVersion{"v1.7.1"}},
-	{"file:swagger.json", &clusterSpecFile{"swagger.json"}},
+	{"file:swagger.json", &clusterSpecFile{"swagger.json", testFS}},
 	{"url:file:///some_file", &clusterSpecLive{"file:///some_file"}},
 }
 
 func TestClusterSpecParsingSuccess(t *testing.T) {
 	for _, test := range successTests {
-		parsed, err := ParseClusterSpec(test.input)
+		parsed, err := parseClusterSpec(test.input, testFS)
 		if err != nil {
 			t.Errorf("Failed to parse spec: %v", err)
 		}
@@ -66,7 +66,7 @@ var failureTests = []parseFailure{
 
 func TestClusterSpecParsingFailure(t *testing.T) {
 	for _, test := range failureTests {
-		_, err := ParseClusterSpec(test.input)
+		_, err := parseClusterSpec(test.input, testFS)
 		if err == nil {
 			t.Errorf("Cluster spec parse for '%s' should have failed, but succeeded", test.input)
 		} else if msg := err.Error(); msg != test.errorMsg {
diff --git a/metadata/interface.go b/metadata/interface.go
index 5d9ed9e39458da38ff544adde7b4e9c890df77ba..bc58d16ba114ffbfb78da45f94ba63046a38a5ed 100644
--- a/metadata/interface.go
+++ b/metadata/interface.go
@@ -1,23 +1,25 @@
 package metadata
 
 import (
-	"fmt"
-	"path/filepath"
-	"strings"
-
 	"github.com/spf13/afero"
 )
 
+var appFS afero.Fs
+
 // AbsPath is an advisory type that represents an absolute path. It is advisory
 // in that it is not forced to be absolute, but rather, meant to indicate
 // intent, and make code easier to read.
 type AbsPath string
 
+// AbsPaths is a slice of `AbsPath`.
+type AbsPaths []string
+
 // Manager abstracts over a ksonnet application's metadata, allowing users to do
 // things like: create and delete environments; search for prototypes; vendor
 // libraries; and other non-core-application tasks.
 type Manager interface {
 	Root() AbsPath
+	ComponentPaths() (AbsPaths, error)
 	//
 	// TODO: Fill in methods as we need them.
 	//
@@ -40,7 +42,7 @@ func Find(path AbsPath) (Manager, error) {
 // capabilities-compliant version of ksonnet-lib, and then generate the
 // directory tree for an application.
 func Init(rootPath AbsPath, spec ClusterSpec) (Manager, error) {
-	return initManager(rootPath, spec, afero.NewOsFs())
+	return initManager(rootPath, spec, appFS)
 }
 
 // ClusterSpec represents the API supported by some cluster. There are several
@@ -57,24 +59,9 @@ type ClusterSpec interface {
 // will output a ClusterSpec representing the cluster specification associated
 // with the `v1.7.1` build of Kubernetes.
 func ParseClusterSpec(specFlag string) (ClusterSpec, error) {
-	split := strings.SplitN(specFlag, ":", 2)
-	if len(split) == 0 || len(split) == 1 || split[1] == "" {
-		return nil, fmt.Errorf("Invalid API specification '%s'", specFlag)
-	}
+	return parseClusterSpec(specFlag, appFS)
+}
 
-	switch split[0] {
-	case "version":
-		return &clusterSpecVersion{k8sVersion: split[1]}, nil
-	case "file":
-		abs, err := filepath.Abs(split[1])
-		if err != nil {
-			return nil, err
-		}
-		absPath := AbsPath(abs)
-		return &clusterSpecFile{specPath: absPath}, nil
-	case "url":
-		return &clusterSpecLive{apiServerURL: split[1]}, nil
-	default:
-		return nil, fmt.Errorf("Could not parse cluster spec '%s'", specFlag)
-	}
+func init() {
+	appFS = afero.NewOsFs()
 }
diff --git a/metadata/manager.go b/metadata/manager.go
index 9898bfed3b3ffbcfec433cbc953632b745270570..2bfb04c78525d726e38df915eb27a0e391b0a903 100644
--- a/metadata/manager.go
+++ b/metadata/manager.go
@@ -1,11 +1,14 @@
 package metadata
 
 import (
+	"encoding/json"
 	"fmt"
 	"os"
 	"path"
 	"path/filepath"
 
+	"github.com/ksonnet/ksonnet-lib/ksonnet-gen/ksonnet"
+	"github.com/ksonnet/ksonnet-lib/ksonnet-gen/kubespec"
 	"github.com/spf13/afero"
 )
 
@@ -24,7 +27,8 @@ const (
 	schemaDir     = "vendor/schema"
 	vendorLibDir  = "vendor/lib"
 
-	schemaFilename = "swagger.json"
+	schemaFilename         = "swagger.json"
+	ksonnetLibCoreFilename = "k8s.libsonnet"
 )
 
 type manager struct {
@@ -62,18 +66,41 @@ func findManager(abs AbsPath, appFS afero.Fs) (*manager, error) {
 }
 
 func initManager(rootPath AbsPath, spec ClusterSpec, appFS afero.Fs) (*manager, error) {
-	data, err := spec.data()
+	//
+	// IMPLEMENTATION NOTE: We get the cluster specification and generate
+	// ksonnet-lib before initializing the directory structure so that failure of
+	// either (e.g., GET'ing the spec from a live cluster returns 404) does not
+	// result in a partially-initialized directory structure.
+	//
+
+	// Get cluster specification data, possibly from the network.
+	specData, err := spec.data()
 	if err != nil {
 		return nil, err
 	}
 
 	m := newManager(rootPath, appFS)
 
+	// Generate the program text for ksonnet-lib.
+	ksonnetLibDir := appendToAbsPath(m.schemaDir, defaultEnvName)
+	ksonnetLibData, err := generateKsonnetLibData(ksonnetLibDir, specData)
+	if err != nil {
+		return nil, err
+	}
+
+	// Initialize directory structure.
 	if err = m.createAppDirTree(); err != nil {
 		return nil, err
 	}
 
-	if err = m.cacheClusterSpecData(defaultEnvName, data); err != nil {
+	// Cache specification data.
+	if err = m.cacheClusterSpecData(defaultEnvName, specData); err != nil {
+		return nil, err
+	}
+
+	ksonnetLibPath := appendToAbsPath(ksonnetLibDir, ksonnetLibCoreFilename)
+	err = afero.WriteFile(appFS, string(ksonnetLibPath), ksonnetLibData, 0644)
+	if err != nil {
 		return nil, err
 	}
 
@@ -98,6 +125,25 @@ func (m *manager) Root() AbsPath {
 	return m.rootPath
 }
 
+func (m *manager) ComponentPaths() (AbsPaths, error) {
+	paths := []string{}
+	err := afero.Walk(m.appFS, string(m.componentsPath), func(path string, info os.FileInfo, err error) error {
+		if err != nil {
+			return err
+		}
+
+		if !info.IsDir() {
+			paths = append(paths, path)
+		}
+		return nil
+	})
+	if err != nil {
+		return nil, err
+	}
+
+	return paths, nil
+}
+
 func (m *manager) cacheClusterSpecData(name string, specData []byte) error {
 	envPath := string(appendToAbsPath(m.schemaDir, name))
 	err := m.appFS.MkdirAll(envPath, os.ModePerm)
@@ -136,3 +182,18 @@ func (m *manager) createAppDirTree() error {
 
 	return nil
 }
+
+func generateKsonnetLibData(ksonnetLibDir AbsPath, text []byte) ([]byte, error) {
+	// Deserialize the API object.
+	s := kubespec.APISpec{}
+	err := json.Unmarshal(text, &s)
+	if err != nil {
+		return nil, err
+	}
+
+	s.Text = text
+	s.FilePath = filepath.Dir(string(ksonnetLibDir))
+
+	// Emit Jsonnet code.
+	return ksonnet.Emit(&s, nil, nil)
+}
diff --git a/metadata/manager_test.go b/metadata/manager_test.go
index 1329bc8b9d5385d6f5f9d8227bba8a96c540304f..1e591ac85ce206ee7ad320c63868d20986e56c14 100644
--- a/metadata/manager_test.go
+++ b/metadata/manager_test.go
@@ -3,6 +3,7 @@ package metadata
 import (
 	"fmt"
 	"os"
+	"sort"
 	"testing"
 
 	"github.com/spf13/afero"
@@ -10,23 +11,41 @@ import (
 
 const (
 	blankSwagger     = "/blankSwagger.json"
-	blankSwaggerData = `{}`
+	blankSwaggerData = `{
+  "swagger": "2.0",
+  "info": {
+   "title": "Kubernetes",
+   "version": "v1.7.0"
+  },
+  "paths": {
+  },
+  "definitions": {
+  }
+}`
+	blankKsonnetLib = `// AUTOGENERATED from the Kubernetes OpenAPI specification. DO NOT MODIFY.
+// Kubernetes version: v1.7.0
+
+{
+  local hidden = {
+  },
+}
+`
 )
 
-var appFS = afero.NewMemMapFs()
+var testFS = afero.NewMemMapFs()
 
 func init() {
-	afero.WriteFile(appFS, blankSwagger, []byte(blankSwaggerData), os.ModePerm)
+	afero.WriteFile(testFS, blankSwagger, []byte(blankSwaggerData), os.ModePerm)
 }
 
 func TestInitSuccess(t *testing.T) {
-	spec, err := ParseClusterSpec(fmt.Sprintf("file:%s", blankSwagger))
+	spec, err := parseClusterSpec(fmt.Sprintf("file:%s", blankSwagger), testFS)
 	if err != nil {
 		t.Fatalf("Failed to parse cluster spec: %v", err)
 	}
 
 	appPath := AbsPath("/fromEmptySwagger")
-	_, err = initManager(appPath, spec, appFS)
+	_, err = initManager(appPath, spec, testFS)
 	if err != nil {
 		t.Fatalf("Failed to init cluster spec: %v", err)
 	}
@@ -44,7 +63,7 @@ func TestInitSuccess(t *testing.T) {
 
 	for _, p := range paths {
 		path := appendToAbsPath(appPath, string(p))
-		exists, err := afero.DirExists(appFS, string(path))
+		exists, err := afero.DirExists(testFS, string(path))
 		if err != nil {
 			t.Fatalf("Expected to create directory '%s', but failed:\n%v", p, err)
 		} else if !exists {
@@ -54,17 +73,25 @@ func TestInitSuccess(t *testing.T) {
 
 	envPath := appendToAbsPath(appPath, string(defaultEnvDir))
 	schemaPath := appendToAbsPath(envPath, schemaFilename)
-	bytes, err := afero.ReadFile(appFS, string(schemaPath))
+	bytes, err := afero.ReadFile(testFS, string(schemaPath))
 	if err != nil {
 		t.Fatalf("Failed to read swagger file at '%s':\n%v", schemaPath, err)
 	} else if actualSwagger := string(bytes); actualSwagger != blankSwaggerData {
 		t.Fatalf("Expected swagger file at '%s' to have value: '%s', got: '%s'", schemaPath, blankSwaggerData, actualSwagger)
 	}
+
+	ksonnetLibPath := appendToAbsPath(envPath, ksonnetLibCoreFilename)
+	ksonnetLibBytes, err := afero.ReadFile(testFS, string(ksonnetLibPath))
+	if err != nil {
+		t.Fatalf("Failed to read ksonnet-lib file at '%s':\n%v", ksonnetLibPath, err)
+	} else if actualKsonnetLib := string(ksonnetLibBytes); actualKsonnetLib != blankKsonnetLib {
+		t.Fatalf("Expected swagger file at '%s' to have value: '%s', got: '%s'", ksonnetLibPath, blankKsonnetLib, actualKsonnetLib)
+	}
 }
 
 func TestFindSuccess(t *testing.T) {
 	findSuccess := func(t *testing.T, appDir, currDir AbsPath) {
-		m, err := findManager(currDir, appFS)
+		m, err := findManager(currDir, testFS)
 		if err != nil {
 			t.Fatalf("Failed to find manager at path '%s':\n%v", currDir, err)
 		} else if m.rootPath != appDir {
@@ -72,13 +99,13 @@ func TestFindSuccess(t *testing.T) {
 		}
 	}
 
-	spec, err := ParseClusterSpec(fmt.Sprintf("file:%s", blankSwagger))
+	spec, err := parseClusterSpec(fmt.Sprintf("file:%s", blankSwagger), testFS)
 	if err != nil {
 		t.Fatalf("Failed to parse cluster spec: %v", err)
 	}
 
 	appPath := AbsPath("/findSuccess")
-	_, err = initManager(appPath, spec, appFS)
+	_, err = initManager(appPath, spec, testFS)
 	if err != nil {
 		t.Fatalf("Failed to init cluster spec: %v", err)
 	}
@@ -90,7 +117,7 @@ func TestFindSuccess(t *testing.T) {
 
 	// Create empty app file.
 	appFile := appendToAbsPath(components, "app.jsonnet")
-	f, err := appFS.OpenFile(string(appFile), os.O_RDONLY|os.O_CREATE, 0777)
+	f, err := testFS.OpenFile(string(appFile), os.O_RDONLY|os.O_CREATE, 0777)
 	if err != nil {
 		t.Fatalf("Failed to touch app file '%s'\n%v", appFile, err)
 	}
@@ -99,9 +126,62 @@ func TestFindSuccess(t *testing.T) {
 	findSuccess(t, appPath, appFile)
 }
 
+func TestComponentPaths(t *testing.T) {
+	spec, err := parseClusterSpec(fmt.Sprintf("file:%s", blankSwagger), testFS)
+	if err != nil {
+		t.Fatalf("Failed to parse cluster spec: %v", err)
+	}
+
+	appPath := AbsPath("/componentPaths")
+	m, err := initManager(appPath, spec, testFS)
+	if err != nil {
+		t.Fatalf("Failed to init cluster spec: %v", err)
+	}
+
+	// Create empty app file.
+	components := appendToAbsPath(appPath, componentsDir)
+	appFile1 := appendToAbsPath(components, "component1.jsonnet")
+	f1, err := testFS.OpenFile(string(appFile1), os.O_RDONLY|os.O_CREATE, 0777)
+	if err != nil {
+		t.Fatalf("Failed to touch app file '%s'\n%v", appFile1, err)
+	}
+	f1.Close()
+
+	// Create empty file in a nested directory.
+	appSubdir := appendToAbsPath(components, "appSubdir")
+	err = testFS.MkdirAll(string(appSubdir), os.ModePerm)
+	if err != nil {
+		t.Fatalf("Failed to create directory '%s'\n%v", appSubdir, err)
+	}
+	appFile2 := appendToAbsPath(appSubdir, "component2.jsonnet")
+	f2, err := testFS.OpenFile(string(appFile2), os.O_RDONLY|os.O_CREATE, 0777)
+	if err != nil {
+		t.Fatalf("Failed to touch app file '%s'\n%v", appFile1, err)
+	}
+	f2.Close()
+
+	// Create a directory that won't be listed in the call to `ComponentPaths`.
+	unlistedDir := string(appendToAbsPath(components, "doNotListMe"))
+	err = testFS.MkdirAll(unlistedDir, os.ModePerm)
+	if err != nil {
+		t.Fatalf("Failed to create directory '%s'\n%v", unlistedDir, err)
+	}
+
+	paths, err := m.ComponentPaths()
+	if err != nil {
+		t.Fatalf("Failed to find component paths: %v", err)
+	}
+
+	sort.Slice(paths, func(i, j int) bool { return paths[i] < paths[j] })
+
+	if len(paths) != 2 || paths[0] != string(appFile2) || paths[1] != string(appFile1) {
+		t.Fatalf("m.ComponentPaths failed; expected '%s', got '%s'", []string{string(appFile1), string(appFile2)}, paths)
+	}
+}
+
 func TestFindFailure(t *testing.T) {
 	findFailure := func(t *testing.T, currDir AbsPath) {
-		_, err := findManager(currDir, appFS)
+		_, err := findManager(currDir, testFS)
 		if err == nil {
 			t.Fatalf("Expected to fail to find ksonnet app in '%s', but succeeded", currDir)
 		}
@@ -113,20 +193,20 @@ func TestFindFailure(t *testing.T) {
 }
 
 func TestDoubleNewFailure(t *testing.T) {
-	spec, err := ParseClusterSpec(fmt.Sprintf("file:%s", blankSwagger))
+	spec, err := parseClusterSpec(fmt.Sprintf("file:%s", blankSwagger), testFS)
 	if err != nil {
 		t.Fatalf("Failed to parse cluster spec: %v", err)
 	}
 
 	appPath := AbsPath("/doubleNew")
 
-	_, err = initManager(appPath, spec, appFS)
+	_, err = initManager(appPath, spec, testFS)
 	if err != nil {
 		t.Fatalf("Failed to init cluster spec: %v", err)
 	}
 
 	targetErr := fmt.Sprintf("Could not create app; directory '%s' already exists", appPath)
-	_, err = initManager(appPath, spec, appFS)
+	_, err = initManager(appPath, spec, testFS)
 	if err == nil || err.Error() != targetErr {
 		t.Fatalf("Expected to fail to create app with message '%s', got '%s'", targetErr, err.Error())
 	}
diff --git a/pkg/kubecfg/common.go b/pkg/kubecfg/common.go
new file mode 100644
index 0000000000000000000000000000000000000000..542c3b2625b6692e3e8c68995e0258c16df6a46c
--- /dev/null
+++ b/pkg/kubecfg/common.go
@@ -0,0 +1,34 @@
+package kubecfg
+
+import (
+	"fmt"
+
+	"github.com/ksonnet/kubecfg/metadata"
+)
+
+// TODO: Make this private when we move more commands into `pkg`.
+func GetFiles(wd metadata.AbsPath, env *string, files []string) ([]string, error) {
+	envPresent := env != nil
+	filesPresent := len(files) > 0
+
+	// This is equivalent to: `if !xor(envPresent, filesPresent) {`
+	if envPresent && filesPresent {
+		return nil, fmt.Errorf("Either an environment name or a file list is required, but not both")
+	} else if !envPresent && !filesPresent {
+		return nil, fmt.Errorf("Must specify either an environment or a file list")
+	}
+
+	if envPresent {
+		manager, err := metadata.Find(wd)
+		if err != nil {
+			return nil, err
+		}
+
+		files, err = manager.ComponentPaths()
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	return files, nil
+}
diff --git a/pkg/kubecfg/init.go b/pkg/kubecfg/init.go
new file mode 100644
index 0000000000000000000000000000000000000000..454d7dc8c8d1789a3ad0696dbef75f6127463fab
--- /dev/null
+++ b/pkg/kubecfg/init.go
@@ -0,0 +1,25 @@
+package kubecfg
+
+import "github.com/ksonnet/kubecfg/metadata"
+
+type InitCmd struct {
+	rootPath metadata.AbsPath
+	spec     metadata.ClusterSpec
+}
+
+func NewInitCmd(rootPath metadata.AbsPath, specFlag string) (*InitCmd, error) {
+	// NOTE: We're taking `rootPath` here as an absolute path (rather than a partial path we expand to an absolute path)
+	// to make it more testable.
+
+	spec, err := metadata.ParseClusterSpec(specFlag)
+	if err != nil {
+		return nil, err
+	}
+
+	return &InitCmd{rootPath: rootPath, spec: spec}, nil
+}
+
+func (c *InitCmd) Run() error {
+	_, err := metadata.Init(c.rootPath, c.spec)
+	return err
+}
diff --git a/pkg/kubecfg/update.go b/pkg/kubecfg/update.go
index 9a95c84e17358a76b7232613b7d08ca04a794c15..dc6916072f4474a3ab5af6ea5295410c2d168d94 100644
--- a/pkg/kubecfg/update.go
+++ b/pkg/kubecfg/update.go
@@ -9,7 +9,6 @@ import (
 	"k8s.io/apimachinery/pkg/api/errors"
 	"k8s.io/apimachinery/pkg/api/meta"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
 	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/runtime/schema"
 	"k8s.io/apimachinery/pkg/types"
@@ -18,6 +17,8 @@ import (
 	"k8s.io/client-go/discovery"
 	"k8s.io/client-go/dynamic"
 
+	"github.com/ksonnet/kubecfg/metadata"
+	"github.com/ksonnet/kubecfg/template"
 	"github.com/ksonnet/kubecfg/utils"
 )
 
@@ -44,13 +45,27 @@ type UpdateCmd struct {
 	Discovery        discovery.DiscoveryInterface
 	DefaultNamespace string
 
+	Expander    *template.Expander
+	Environment *string
+	Files       []string
+
 	Create bool
 	GcTag  string
 	SkipGc bool
 	DryRun bool
 }
 
-func (c UpdateCmd) Run(objs []*unstructured.Unstructured) error {
+func (c UpdateCmd) Run(wd metadata.AbsPath) error {
+	files, err := GetFiles(wd, c.Environment, c.Files)
+	if err != nil {
+		return err
+	}
+
+	objs, err := c.Expander.Expand(files)
+	if err != nil {
+		return err
+	}
+
 	dryRunText := ""
 	if c.DryRun {
 		dryRunText = " (dry-run)"
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
+}
diff --git a/vendor/github.com/ksonnet/ksonnet-lib/LICENSE b/vendor/github.com/ksonnet/ksonnet-lib/LICENSE
new file mode 100644
index 0000000000000000000000000000000000000000..8dada3edaf50dbc082c9a125058f25def75e625a
--- /dev/null
+++ b/vendor/github.com/ksonnet/ksonnet-lib/LICENSE
@@ -0,0 +1,201 @@
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "{}"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright {yyyy} {name of copyright owner}
+
+   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.
diff --git a/vendor/github.com/ksonnet/ksonnet-lib/ksonnet-gen/jsonnet/rewrite.go b/vendor/github.com/ksonnet/ksonnet-lib/ksonnet-gen/jsonnet/rewrite.go
new file mode 100644
index 0000000000000000000000000000000000000000..65aa5e964bc8c28a91276ac15088cdb53951cdb7
--- /dev/null
+++ b/vendor/github.com/ksonnet/ksonnet-lib/ksonnet-gen/jsonnet/rewrite.go
@@ -0,0 +1,115 @@
+// Package jsonnet contains a collection of simple rewriting
+// facilities that allow us to easily map text from the OpenAPI spec
+// to things that are Jsonnet-friendly (e.g., renaming identifiers
+// that are Jsonnet keywords, lowerCamelCase'ing names, and so on).
+package jsonnet
+
+import (
+	"fmt"
+	"log"
+	"strings"
+
+	"github.com/ksonnet/ksonnet-lib/ksonnet-gen/kubespec"
+	"github.com/ksonnet/ksonnet-lib/ksonnet-gen/kubeversion"
+)
+
+// FieldKey represents the literal text of a key for some JSON object
+// field, after rewriting to avoid collisions with Jsonnet keywords.
+// For example, for `{foo: ...}`, the `FieldKey` would be `foo`, while
+// for `{error: ...}`, the `FieldKey` would be `"error"` (with
+// quotation marks, to avoid collisions).
+type FieldKey string
+
+// FuncParam represents the parameter to a Jsonnet function, after
+// being rewritten to avoid collisions with Jsonnet keywords and
+// normalized to fit the Jsonnet style (i.e., lowerCamelCase) using a
+// manual set of custom transformations that change per Kubernetes
+// version. For example, in `foo(BarAPI) {...}`, `FuncParam` would be
+// `barApi`, and in `foo(error) {...}`, `FuncParam` would be
+// `errorParam`.
+type FuncParam string
+
+// Identifier represents any identifier in a Jsonnet program, after
+// being normalized to fit the Jsonnet style (i.e., lowerCamelCase)
+// using a manual set of custom transformations that change per
+// Kubernetes version. For example, `fooAPI` becomes `fooApi`.
+type Identifier string
+
+// RewriteAsFieldKey takes a `PropertyName` and converts it to a valid
+// Jsonnet field name. For example, if the `PropertyName` has a value
+// of `"error"`, then this would generate an invalid object, `{error:
+// ...}`. Hence, this function will quote this string, so that it ends
+// up like: `{"error": ...}`.
+func RewriteAsFieldKey(text kubespec.PropertyName) FieldKey {
+	// NOTE: Because the field needs to have precisely the same text as
+	// the Kubernetes API spec, we do not compute a version-specific ID
+	// alias as we do for other rewrites.
+	if _, ok := jsonnetKeywordSet[text]; ok {
+		return FieldKey(fmt.Sprintf("\"%s\"", text))
+	}
+	return FieldKey(text)
+}
+
+// RewriteAsFuncParam takes a `PropertyName` and converts it to a
+// valid Jsonnet function parameter. For example, if the
+// `PropertyName` has a value of `"error"`, then this would generate
+// an invalid function parameter, `function(error) ...`. Hence, this
+// function will alter the identifier, so that it ends up like:
+// `function(errorParam) ...`.
+//
+// NOTE: This transformation involves a hand-curated style change to
+// lowerCamelCase (e.g., `fooAPI` -> `fooApi`). This list changes per
+// Kubernetes version, according to identifiers that don't conform to
+// this style.
+func RewriteAsFuncParam(
+	k8sVersion string, text kubespec.PropertyName,
+) FuncParam {
+	id := RewriteAsIdentifier(k8sVersion, text)
+	if _, ok := jsonnetKeywordSet[kubespec.PropertyName(id)]; ok {
+		return FuncParam(fmt.Sprintf("%sParam", id))
+	}
+	return FuncParam(id)
+}
+
+// RewriteAsIdentifier takes a `GroupName`, `ObjectKind`,
+// `PropertyName`, or `string`, and converts it to a Jsonnet-style
+// Identifier. Typically this includes lower-casing the first letter,
+// but also changing initialisms like fooAPI -> fooApi.
+//
+// NOTE: This transformation involves a hand-curated style change to
+// lowerCamelCase (e.g., `fooAPI` -> `fooApi`). This list changes per
+// Kubernetes version, according to identifiers that don't conform to
+// this style.
+func RewriteAsIdentifier(
+	k8sVersion string, rawID fmt.Stringer,
+) Identifier {
+	var id = rawID.String()
+
+	if len(id) == 0 {
+		log.Fatalf("Can't lowercase first letter of 0-rune string")
+	}
+	kindString := kubeversion.MapIdentifier(k8sVersion, id)
+
+	upper := strings.ToLower(kindString[:1])
+	return Identifier(upper + kindString[1:])
+}
+
+var jsonnetKeywordSet = map[kubespec.PropertyName]string{
+	"assert":     "assert",
+	"else":       "else",
+	"error":      "error",
+	"false":      "false",
+	"for":        "for",
+	"function":   "function",
+	"if":         "if",
+	"import":     "import",
+	"importstr":  "importstr",
+	"in":         "in",
+	"local":      "local",
+	"null":       "null",
+	"tailstrict": "tailstrict",
+	"then":       "then",
+	"self":       "self",
+	"super":      "super",
+	"true":       "true",
+}
diff --git a/vendor/github.com/ksonnet/ksonnet-lib/ksonnet-gen/ksonnet/buffer.go b/vendor/github.com/ksonnet/ksonnet-lib/ksonnet-gen/ksonnet/buffer.go
new file mode 100644
index 0000000000000000000000000000000000000000..15dc6cced7cee305c3722c5a7da18030fc9f1853
--- /dev/null
+++ b/vendor/github.com/ksonnet/ksonnet-lib/ksonnet-gen/ksonnet/buffer.go
@@ -0,0 +1,58 @@
+package ksonnet
+
+import (
+	"bytes"
+	"fmt"
+	"strings"
+)
+
+// indentWriter abstracts the task of writing out indented text to a
+// buffer. Different components can call `indent` and `dedent` as
+// appropriate to specify how indentation needs to change, rather than
+// to keep track of the current indentation.
+//
+// For example, if one component is responsible for writing an array,
+// and an element in that array is a function, the component
+// responsible for the array need only know to call `indent` after the
+// '[' character and `dedent` before the ']' character, while the
+// routine responsible for writing out the function can handle its own
+// indentation independently.
+type indentWriter struct {
+	depth  int
+	err    error
+	buffer bytes.Buffer
+}
+
+func newIndentWriter() *indentWriter {
+	var buffer bytes.Buffer
+	return &indentWriter{
+		depth:  0,
+		err:    nil,
+		buffer: buffer,
+	}
+}
+
+func (m *indentWriter) writeLine(text string) {
+	if m.err != nil {
+		return
+	}
+	prefix := strings.Repeat("  ", m.depth)
+	line := fmt.Sprintf("%s%s\n", prefix, text)
+	_, m.err = m.buffer.WriteString(line)
+}
+
+func (m *indentWriter) bytes() ([]byte, error) {
+	if m.err != nil {
+		return nil, m.err
+	}
+
+	return m.buffer.Bytes(), nil
+}
+
+func (m *indentWriter) indent() {
+	m.depth++
+}
+
+func (m *indentWriter) dedent() {
+	m.depth--
+}
diff --git a/vendor/github.com/ksonnet/ksonnet-lib/ksonnet-gen/ksonnet/emit.go b/vendor/github.com/ksonnet/ksonnet-lib/ksonnet-gen/ksonnet/emit.go
new file mode 100644
index 0000000000000000000000000000000000000000..908ff43c11742cb8993bfc854d50520b12b2bbe4
--- /dev/null
+++ b/vendor/github.com/ksonnet/ksonnet-lib/ksonnet-gen/ksonnet/emit.go
@@ -0,0 +1,888 @@
+package ksonnet
+
+import (
+	"fmt"
+	"log"
+	"sort"
+	"strings"
+
+	"github.com/ksonnet/ksonnet-lib/ksonnet-gen/jsonnet"
+	"github.com/ksonnet/ksonnet-lib/ksonnet-gen/kubespec"
+	"github.com/ksonnet/ksonnet-lib/ksonnet-gen/kubeversion"
+)
+
+// Emit takes a swagger API specification, and returns the text of
+// `ksonnet-lib`, written in Jsonnet.
+func Emit(spec *kubespec.APISpec, ksonnetLibSHA, k8sSHA *string) ([]byte, error) {
+	root := newRoot(spec, ksonnetLibSHA, k8sSHA)
+
+	m := newIndentWriter()
+	root.emit(m)
+	return m.bytes()
+}
+
+//-----------------------------------------------------------------------------
+// Root.
+//-----------------------------------------------------------------------------
+
+// `root` is an abstraction of the root of `k8s.libsonnet`, which can be
+// emitted as Jsonnet code using the `emit` method.
+//
+// `root` contains and manages a set of `groups`, which represent a
+// set of Kubernetes API groups (e.g., core, apps, extensions), and
+// holds all of the logic required to build the `groups` from an
+// `kubespec.APISpec`.
+type root struct {
+	spec         *kubespec.APISpec
+	groups       groupSet // set of groups, e.g., core, apps, extensions.
+	hiddenGroups groupSet
+
+	ksonnetLibSHA *string
+	k8sSHA        *string
+}
+
+func newRoot(spec *kubespec.APISpec, ksonnetLibSHA, k8sSHA *string) *root {
+	root := root{
+		spec:         spec,
+		groups:       make(groupSet),
+		hiddenGroups: make(groupSet),
+
+		ksonnetLibSHA: ksonnetLibSHA,
+		k8sSHA:        k8sSHA,
+	}
+
+	for defName, def := range spec.Definitions {
+		root.addDefinition(defName, def)
+	}
+
+	return &root
+}
+
+func (root *root) emit(m *indentWriter) {
+	m.writeLine("// AUTOGENERATED from the Kubernetes OpenAPI specification. DO NOT MODIFY.")
+	m.writeLine(fmt.Sprintf("// Kubernetes version: %s", root.spec.Info.Version))
+
+	if root.ksonnetLibSHA != nil {
+		m.writeLine(fmt.Sprintf(
+			"// SHA of ksonnet-lib HEAD: %s", *root.ksonnetLibSHA))
+	}
+
+	if root.ksonnetLibSHA != nil {
+		m.writeLine(fmt.Sprintf(
+			"// SHA of Kubernetes HEAD OpenAPI spec is generated from: %s",
+			*root.k8sSHA))
+	}
+	m.writeLine("")
+
+	m.writeLine("{")
+	m.indent()
+
+	// Emit in sorted order so that we can diff the output.
+	for _, group := range root.groups.toSortedSlice() {
+		group.emit(m)
+	}
+
+	m.writeLine("local hidden = {")
+	m.indent()
+
+	for _, hiddenGroup := range root.hiddenGroups.toSortedSlice() {
+		hiddenGroup.emit(m)
+	}
+
+	m.dedent()
+	m.writeLine("},")
+
+	m.dedent()
+	m.writeLine("}")
+}
+
+func (root *root) addDefinition(
+	path kubespec.DefinitionName, def *kubespec.SchemaDefinition,
+) {
+	parsedName := path.Parse()
+	if parsedName.Version == nil {
+		return
+	}
+	apiObject := root.createAPIObject(parsedName, def)
+
+	for propName, prop := range def.Properties {
+		pm := newPropertyMethod(propName, path, prop, apiObject)
+		apiObject.properties[propName] = pm
+
+		st := prop.Type
+		if isMixinRef(pm.ref) ||
+			(st != nil && *st == "array" && prop.Items.Ref != nil) {
+			typeAliasName := propName + "Type"
+			ta, ok := apiObject.properties[typeAliasName]
+			if ok && ta.kind != typeAlias {
+				log.Panicf(
+					"Can't create type alias '%s' because a property with that name already exists", typeAliasName)
+			}
+
+			ta = newPropertyTypeAlias(typeAliasName, path, prop, apiObject)
+			apiObject.properties[typeAliasName] = ta
+		}
+	}
+}
+
+func (root *root) createAPIObject(
+	parsedName *kubespec.ParsedDefinitionName, def *kubespec.SchemaDefinition,
+) *apiObject {
+	if parsedName.Version == nil {
+		log.Panicf(
+			"Can't make API object from name with nil version in path: '%s'",
+			parsedName.Unparse())
+	}
+
+	var groupName kubespec.GroupName
+	if parsedName.Group == nil {
+		groupName = "core"
+	} else {
+		groupName = *parsedName.Group
+	}
+
+	var qualifiedName kubespec.GroupName
+	if len(def.TopLevelSpecs) > 0 && def.TopLevelSpecs[0].Group != "" {
+		qualifiedName = def.TopLevelSpecs[0].Group
+	} else {
+		qualifiedName = groupName
+	}
+
+	// Separate out top-level definitions from everything else.
+	var groups groupSet
+	if len(def.TopLevelSpecs) > 0 {
+		groups = root.groups
+	} else {
+		groups = root.hiddenGroups
+	}
+
+	group, ok := groups[groupName]
+	if !ok {
+		group = newGroup(groupName, qualifiedName, root)
+		groups[groupName] = group
+	}
+
+	versionedAPI, ok := group.versionedAPIs[*parsedName.Version]
+	if !ok {
+		versionedAPI = newVersionedAPI(*parsedName.Version, group)
+		group.versionedAPIs[*parsedName.Version] = versionedAPI
+	}
+
+	apiObject, ok := versionedAPI.apiObjects[parsedName.Kind]
+	if ok {
+		log.Panicf("Duplicate object kinds with name '%s'", parsedName.Unparse())
+	}
+	apiObject = newAPIObject(parsedName, versionedAPI, def)
+	versionedAPI.apiObjects[parsedName.Kind] = apiObject
+	return apiObject
+}
+
+func (root *root) getAPIObject(
+	parsedName *kubespec.ParsedDefinitionName,
+) *apiObject {
+	ao, err := root.getAPIObjectHelper(parsedName, false)
+	if err == nil {
+		return ao
+	}
+
+	ao, err = root.getAPIObjectHelper(parsedName, true)
+	if err != nil {
+		log.Panic(err.Error())
+	}
+	return ao
+}
+
+func (root *root) getAPIObjectHelper(
+	parsedName *kubespec.ParsedDefinitionName, hidden bool,
+) (*apiObject, error) {
+	if parsedName.Version == nil {
+		log.Panicf(
+			"Can't get API object with nil version: '%s'", parsedName.Unparse())
+	}
+
+	var groupName kubespec.GroupName
+	if parsedName.Group == nil {
+		groupName = "core"
+	} else {
+		groupName = *parsedName.Group
+	}
+
+	var groups groupSet
+	if hidden {
+		groups = root.groups
+	} else {
+		groups = root.hiddenGroups
+	}
+
+	group, ok := groups[groupName]
+	if !ok {
+		return nil, fmt.Errorf(
+			"Could not retrieve object, group in path '%s' doesn't exist",
+			parsedName.Unparse())
+	}
+
+	versionedAPI, ok := group.versionedAPIs[*parsedName.Version]
+	if !ok {
+		return nil, fmt.Errorf(
+			"Could not retrieve object, versioned API in path '%s' doesn't exist",
+			parsedName.Unparse())
+	}
+
+	if apiObject, ok := versionedAPI.apiObjects[parsedName.Kind]; ok {
+		return apiObject, nil
+	}
+	return nil, fmt.Errorf(
+		"Could not retrieve object, kind in path '%s' doesn't exist",
+		parsedName.Unparse())
+}
+
+//-----------------------------------------------------------------------------
+// Group.
+//-----------------------------------------------------------------------------
+
+// `group` is an abstract representation of a Kubernetes API group
+// (e.g., apps, extensions, core), which can be emitted as Jsonnet
+// code using the `emit` method.
+//
+// `group` contains a set of versioned APIs (e.g., v1, v1beta1, etc.),
+// though the logic for creating them is handled largely by `root`.
+type group struct {
+	name          kubespec.GroupName // e.g., core, apps, extensions.
+	qualifiedName kubespec.GroupName // e.g., rbac.authorization.k8s.io.
+	versionedAPIs versionedAPISet    // e.g., v1, v1beta1.
+	parent        *root
+}
+type groupSet map[kubespec.GroupName]*group
+type groupSlice []*group
+
+func newGroup(
+	name kubespec.GroupName, qualifiedName kubespec.GroupName, parent *root,
+) *group {
+	return &group{
+		name:          name,
+		qualifiedName: qualifiedName,
+		versionedAPIs: make(versionedAPISet),
+		parent:        parent,
+	}
+}
+
+func (group *group) root() *root {
+	return group.parent
+}
+
+func (group *group) emit(m *indentWriter) {
+	k8sVersion := group.root().spec.Info.Version
+	mixinName := jsonnet.RewriteAsIdentifier(k8sVersion, group.name)
+	line := fmt.Sprintf("%s:: {", mixinName)
+	m.writeLine(line)
+	m.indent()
+
+	// Emit in sorted order so that we can diff the output.
+	for _, versioned := range group.versionedAPIs.toSortedSlice() {
+		versioned.emit(m)
+	}
+
+	m.dedent()
+	m.writeLine("},")
+}
+
+func (gs groupSet) toSortedSlice() groupSlice {
+	groups := groupSlice{}
+	for _, group := range gs {
+		groups = append(groups, group)
+	}
+	sort.Slice(groups, func(i, j int) bool {
+		return groups[i].name < groups[j].name
+	})
+	return groups
+}
+
+//-----------------------------------------------------------------------------
+// Versioned API.
+//-----------------------------------------------------------------------------
+
+// `versionedAPI` is an abstract representation of a version of a
+// Kubernetes API group (e.g., apps.v1beta1, extensions.v1beta1,
+// core.v1), which can be emitted as Jsonnet code using the `emit`
+// method.
+//
+// `versionedAPI` contains a set of API objects (e.g., v1.Container,
+// v1beta1.Deployment, etc.), though the logic for creating them is
+// handled largely by `root`.
+type versionedAPI struct {
+	version    kubespec.VersionString // version string, e.g., v1, v1beta1.
+	apiObjects apiObjectSet           // set of objects, e.g, v1.Container.
+	parent     *group
+}
+type versionedAPISet map[kubespec.VersionString]*versionedAPI
+type versionedAPISlice []*versionedAPI
+
+func newVersionedAPI(
+	version kubespec.VersionString, parent *group,
+) *versionedAPI {
+	return &versionedAPI{
+		version:    version,
+		apiObjects: make(apiObjectSet),
+		parent:     parent,
+	}
+}
+
+func (va *versionedAPI) root() *root {
+	return va.parent.parent
+}
+
+func (va *versionedAPI) emit(m *indentWriter) {
+	// NOTE: Do not need to call `jsonnet.RewriteAsIdentifier`.
+	line := fmt.Sprintf("%s:: {", va.version)
+	m.writeLine(line)
+	m.indent()
+
+	gn := va.parent.qualifiedName
+	if gn == "core" {
+		m.writeLine(fmt.Sprintf(
+			"local apiVersion = {apiVersion: \"%s\"},", va.version))
+	} else {
+		m.writeLine(fmt.Sprintf(
+			"local apiVersion = {apiVersion: \"%s/%s\"},", gn, va.version))
+	}
+
+	// Emit in sorted order so that we can diff the output.
+	for _, object := range va.apiObjects.toSortedSlice() {
+		object.emit(m)
+	}
+
+	m.dedent()
+	m.writeLine("},")
+}
+
+func (vas versionedAPISet) toSortedSlice() versionedAPISlice {
+	versionedAPIs := versionedAPISlice{}
+	for _, va := range vas {
+		versionedAPIs = append(versionedAPIs, va)
+	}
+	sort.Slice(versionedAPIs, func(i, j int) bool {
+		return versionedAPIs[i].version < versionedAPIs[j].version
+	})
+	return versionedAPIs
+}
+
+//-----------------------------------------------------------------------------
+// API object.
+//-----------------------------------------------------------------------------
+
+// `apiObject` is an abstract representation of a Kubernetes API
+// object (e.g., v1.Container, v1beta1.Deployment), which can be
+// emitted as Jsonnet code using the `emit` method.
+//
+// `apiObject` contains a set of property methods and mixins which
+// formulate the basis of much of ksonnet-lib's programming surface.
+// The logic for creating them is handled largely by `root`.
+type apiObject struct {
+	name       kubespec.ObjectKind // e.g., `Container` in `v1.Container`
+	properties propertySet         // e.g., container.image, container.env
+	parsedName *kubespec.ParsedDefinitionName
+	comments   comments
+	parent     *versionedAPI
+	isTopLevel bool
+}
+type apiObjectSet map[kubespec.ObjectKind]*apiObject
+type apiObjectSlice []*apiObject
+
+func newAPIObject(
+	name *kubespec.ParsedDefinitionName, parent *versionedAPI,
+	def *kubespec.SchemaDefinition,
+) *apiObject {
+	isTopLevel := len(def.TopLevelSpecs) > 0
+	comments := newComments(def.Description)
+	return &apiObject{
+		name:       name.Kind,
+		parsedName: name,
+		properties: make(propertySet),
+		comments:   comments,
+		parent:     parent,
+		isTopLevel: isTopLevel,
+	}
+}
+
+func (ao apiObject) toRefPropertyMethod(
+	name kubespec.PropertyName, path kubespec.DefinitionName, parent *apiObject,
+) *property {
+	return &property{
+		ref:        path.AsObjectRef(),
+		schemaType: nil,
+		itemTypes:  kubespec.Items{},
+		name:       name,
+		path:       path,
+		comments:   ao.comments,
+		parent:     parent,
+	}
+}
+
+func (ao *apiObject) root() *root {
+	return ao.parent.parent.parent
+}
+
+func (ao *apiObject) emit(m *indentWriter) {
+	k8sVersion := ao.root().spec.Info.Version
+	jsonnetName := kubespec.ObjectKind(
+		jsonnet.RewriteAsIdentifier(k8sVersion, ao.name))
+	if _, ok := ao.parent.apiObjects[jsonnetName]; ok {
+		log.Panicf(
+			"Tried to lowercase first character of object kind '%s', but lowercase name was already present in version '%s'",
+			jsonnetName,
+			ao.parent.version)
+	}
+
+	ao.comments.emit(m)
+
+	m.writeLine(fmt.Sprintf("%s:: {", jsonnetName))
+	m.indent()
+
+	if ao.isTopLevel {
+		// NOTE: It is important to NOT capitalize `ao.name` here.
+		m.writeLine(fmt.Sprintf("local kind = {kind: \"%s\"},", ao.name))
+	}
+	ao.emitConstructors(m)
+
+	for _, pm := range ao.properties.sortAndFilterBlacklisted() {
+		// Skip special properties and fields that `$ref` another API
+		// object type, since those will go in the `mixin` namespace.
+		if isSpecialProperty(pm.name) || isMixinRef(pm.ref) {
+			continue
+		}
+		pm.emit(m)
+	}
+
+	// Emit the properties that `$ref` another API object type in the
+	// `mixin:: {` namespace.
+	m.writeLine("mixin:: {")
+	m.indent()
+
+	for _, pm := range ao.properties.sortAndFilterBlacklisted() {
+		// TODO: Emit mixin code also for arrays whose elements are
+		// `$ref`.
+		if !isMixinRef(pm.ref) {
+			continue
+		}
+
+		pm.emit(m)
+	}
+
+	m.dedent()
+	m.writeLine("},")
+
+	m.dedent()
+	m.writeLine("},")
+}
+
+// `emitAsRefMixins` recursively emits an API object as a collection
+// of mixin methods, particularly when another API object has a
+// property that uses `$ref` to reference the current API object.
+//
+// For example, `v1beta1.Deployment` has a field, `spec`, which is of
+// type `v1beta1.DeploymentSpec`. In this case, we'd like to
+// recursively capture all the properties of `v1beta1.DeploymentSpec`
+// and create mixin methods, so that we can do something like
+// `someDeployment + deployment.mixin.spec.minReadySeconds(3)`.
+func (ao *apiObject) emitAsRefMixins(
+	m *indentWriter, p *property, parentMixinName *string,
+) {
+	k8sVersion := ao.root().spec.Info.Version
+	functionName := jsonnet.RewriteAsIdentifier(k8sVersion, p.name)
+	paramName := jsonnet.RewriteAsFuncParam(k8sVersion, p.name)
+	fieldName := jsonnet.RewriteAsFieldKey(p.name)
+	mixinName := fmt.Sprintf("__%sMixin", functionName)
+	var mixinText string
+	if parentMixinName == nil {
+		mixinText = fmt.Sprintf(
+			"local %s(%s) = {%s+: %s},", mixinName, paramName, fieldName, paramName)
+	} else {
+		mixinText = fmt.Sprintf(
+			"local %s(%s) = %s({%s+: %s}),",
+			mixinName, paramName, *parentMixinName, fieldName, paramName)
+	}
+
+	if _, ok := ao.parent.apiObjects[kubespec.ObjectKind(functionName)]; ok {
+		log.Panicf(
+			"Tried to lowercase first character of object kind '%s', but lowercase name was already present in version '%s'",
+			functionName,
+			ao.parent.version)
+	}
+
+	// NOTE: Comments are emitted by `property#emit`, before we
+	// call this method.
+
+	line := fmt.Sprintf("%s:: {", functionName)
+	m.writeLine(line)
+	m.indent()
+
+	m.writeLine(mixinText)
+	m.writeLine(
+		fmt.Sprintf("mixinInstance(%s):: %s(%s),", paramName, mixinName, paramName))
+
+	for _, pm := range ao.properties.sortAndFilterBlacklisted() {
+		if isSpecialProperty(pm.name) {
+			continue
+		}
+		pm.emitAsRefMixin(m, mixinName)
+	}
+
+	m.dedent()
+	m.writeLine("},")
+}
+
+func (ao *apiObject) emitConstructors(m *indentWriter) {
+	k8sVersion := ao.root().spec.Info.Version
+	path := ao.parsedName.Unparse()
+
+	specs, ok := kubeversion.ConstructorSpec(k8sVersion, path)
+	if !ok {
+		ao.emitConstructor(m, constructorName, []kubeversion.CustomConstructorParam{})
+		return
+	}
+
+	for _, spec := range specs {
+		ao.emitConstructor(m, spec.ID, spec.Params)
+	}
+}
+
+func (ao *apiObject) emitConstructor(
+	m *indentWriter, id string, params []kubeversion.CustomConstructorParam,
+) {
+	// Panic if a function with the constructor's name already exists.
+	specName := kubespec.PropertyName(id)
+	if dm, ok := ao.properties[specName]; ok {
+		log.Panicf(
+			"Attempted to create constructor, but '%s' property already existed at '%s'",
+			specName, dm.path)
+	}
+
+	// Default body of the constructor. Usually either `apiVersion +
+	// kind` or `{}`.
+	var defaultSetters []string
+	if ao.isTopLevel {
+		defaultSetters = specialPropertiesList
+	} else {
+		defaultSetters = []string{"{}"}
+	}
+
+	// Build parameters and body of constructor. Considering the example
+	// of the constructor of `v1.Container`:
+	//
+	//   new(name, image):: self.name(name) + self.image(image),
+	//
+	// Here we want to (1) assemble the parameter list (i.e., `name` and
+	// `image`), as well as the body (i.e., the calls to `self.name` and
+	// so on).
+	paramLiterals := []string{}
+	setters := defaultSetters
+	for _, param := range params {
+		// Add the param to the param list, including default value if
+		// applicable.
+		if param.DefaultValue != nil {
+			paramLiterals = append(
+				paramLiterals, fmt.Sprintf("%s=%s", param.ID, *param.DefaultValue))
+		} else {
+			paramLiterals = append(paramLiterals, param.ID)
+		}
+
+		// Add an element to the body (e.g., `self.name` above).
+		if param.RelativePath == nil {
+			prop, ok := ao.properties[kubespec.PropertyName(param.ID)]
+			if !ok {
+				log.Panicf(
+					"Attempted to create constructor, but property '%s' does not exist",
+					param.ID)
+			}
+			setters = append(
+				setters, fmt.Sprintf("self.%s(%s)", prop.name, param.ID))
+		} else {
+			// TODO(hausdorff): We may want to verify this relative path
+			// exists.
+			setters = append(
+				setters, fmt.Sprintf("self.%s(%s)", *param.RelativePath, param.ID))
+		}
+	}
+
+	// Write out constructor.
+	paramsText := strings.Join(paramLiterals, ", ")
+	bodyText := strings.Join(setters, " + ")
+	m.writeLine(fmt.Sprintf("%s(%s):: %s,", specName, paramsText, bodyText))
+}
+
+func (aos apiObjectSet) toSortedSlice() apiObjectSlice {
+	apiObjects := apiObjectSlice{}
+	for _, apiObject := range aos {
+		apiObjects = append(apiObjects, apiObject)
+	}
+	sort.Slice(apiObjects, func(i, j int) bool {
+		return apiObjects[i].name < apiObjects[j].name
+	})
+	return apiObjects
+}
+
+//-----------------------------------------------------------------------------
+// Property method.
+//-----------------------------------------------------------------------------
+
+type propertyKind int
+
+const (
+	method propertyKind = iota
+	typeAlias
+)
+
+// `property` is an abstract representation of a ksonnet-lib's
+// property methods, which can be emitted as Jsonnet code using the
+// `emit` method.
+//
+// For example, ksonnet-lib exposes many functions such as
+// `v1.container.image`, which can be added together with the `+`
+// operator to construct a complete image. `property` is an
+// abstract representation of these so-called "property methods".
+//
+// `property` contains the name of the property given in the
+// `apiObject` that is its parent (for example, `Deployment` has a
+// field called `containers`, which is an array of `v1.Container`), as
+// well as the `kubespec.PropertyName`, which contains information
+// required to generate the Jsonnet code.
+//
+// The logic for creating them is handled largely by `root`.
+type property struct {
+	kind       propertyKind
+	ref        *kubespec.ObjectRef
+	schemaType *kubespec.SchemaType
+	itemTypes  kubespec.Items
+	name       kubespec.PropertyName // e.g., image in container.image.
+	path       kubespec.DefinitionName
+	comments   comments
+	parent     *apiObject
+}
+type propertySet map[kubespec.PropertyName]*property
+type propertySlice []*property
+
+func newPropertyMethod(
+	name kubespec.PropertyName, path kubespec.DefinitionName,
+	prop *kubespec.Property, parent *apiObject,
+) *property {
+	comments := newComments(prop.Description)
+	return &property{
+		kind:       method,
+		ref:        prop.Ref,
+		schemaType: prop.Type,
+		itemTypes:  prop.Items,
+		name:       name,
+		path:       path,
+		comments:   comments,
+		parent:     parent,
+	}
+}
+
+func newPropertyTypeAlias(
+	name kubespec.PropertyName, path kubespec.DefinitionName,
+	prop *kubespec.Property, parent *apiObject,
+) *property {
+	comments := newComments(prop.Description)
+	return &property{
+		kind:       typeAlias,
+		ref:        prop.Ref,
+		schemaType: prop.Type,
+		itemTypes:  prop.Items,
+		name:       name,
+		path:       path,
+		comments:   comments,
+		parent:     parent,
+	}
+}
+
+func (p *property) root() *root {
+	return p.parent.parent.parent.parent
+}
+
+func (p *property) emit(m *indentWriter) {
+	p.emitHelper(m, nil)
+}
+
+// `emitAsRefMixin` will emit a property as a mixin method, so that it
+// can be "mixed in" to alter an existing object.
+//
+// For example if we have a fully-formed deployment object,
+// `someDeployment`, we'd like to be able to do something like
+// `someDeployment + deployment.mixin.spec.minReadySeconds(3)` to "mix
+// in" a change to the `spec.minReadySeconds` field.
+//
+// This method will take the `property`, which specifies a
+// property method, and use it to emit such a "mixin method".
+func (p *property) emitAsRefMixin(
+	m *indentWriter, parentMixinName string,
+) {
+	p.emitHelper(m, &parentMixinName)
+}
+
+func (p *property) emitAsTypeAlias(m *indentWriter) {
+	var path kubespec.DefinitionName
+	if p.ref != nil {
+		path = *p.ref.Name()
+	} else {
+		path = *p.itemTypes.Ref.Name()
+	}
+	parsedPath := path.Parse()
+	if parsedPath.Version == nil {
+		log.Printf("Could not emit type alias for '%s'\n", path)
+		return
+	}
+
+	// Chop the `Type` off the end of the type alias name, rewrite the
+	// "base" of the type alias, and then append `Type` to the end
+	// again.
+	//
+	// Why: the desired behavior is for a rewrite rule to apply to both
+	// a method and its type alias. For example, if we specify that
+	// `scaleIO` should be rewritten `scaleIo`, then we'd like the type
+	// alias to be emitted as `scaleIoType`, not `scaleIOType`,
+	// automatically, so that the user doesn't have to specify another,
+	// separate rule for the type alias itself.
+	k8sVersion := p.root().spec.Info.Version
+	trimmedName := kubespec.PropertyName(strings.TrimSuffix(string(p.name), "Type"))
+	typeName := jsonnet.RewriteAsIdentifier(k8sVersion, trimmedName) + "Type"
+
+	var group kubespec.GroupName
+	if parsedPath.Group == nil {
+		group = "core"
+	} else {
+		group = *parsedPath.Group
+	}
+
+	id := jsonnet.RewriteAsIdentifier(k8sVersion, parsedPath.Kind)
+	line := fmt.Sprintf(
+		"%s:: hidden.%s.%s.%s,",
+		typeName, group, parsedPath.Version, id)
+
+	m.writeLine(line)
+}
+
+// `emitHelper` emits the Jsonnet program text for a `property`,
+// handling both the case that it's a mixin (i.e., `parentMixinName !=
+// nil`), and the case that it's a "normal", non-mixin property method
+// (i.e., `parentMixinName == nil`).
+//
+// NOTE: To get `emitHelper` to emit this property as a mixin, it is
+// REQUIRED for `parentMixinName` to be non-nil; likewise, to get
+// `emitHelper` to emit this property as a normal, non-mixin property
+// method, it is necessary for `parentMixinName == nil`.
+func (p *property) emitHelper(
+	m *indentWriter, parentMixinName *string,
+) {
+	if p.kind == typeAlias {
+		p.emitAsTypeAlias(m)
+		return
+	}
+
+	p.comments.emit(m)
+
+	k8sVersion := p.root().spec.Info.Version
+	functionName := jsonnet.RewriteAsIdentifier(k8sVersion, p.name)
+	paramName := jsonnet.RewriteAsFuncParam(k8sVersion, p.name)
+	fieldName := jsonnet.RewriteAsFieldKey(p.name)
+	signature := fmt.Sprintf("%s(%s)::", functionName, paramName)
+
+	if isMixinRef(p.ref) {
+		parsedRefPath := p.ref.Name().Parse()
+		apiObject := p.root().getAPIObject(parsedRefPath)
+		apiObject.emitAsRefMixins(m, p, parentMixinName)
+	} else if p.ref != nil && !isMixinRef(p.ref) {
+		var body string
+		if parentMixinName == nil {
+			body = fmt.Sprintf("{%s: %s}", fieldName, paramName)
+		} else {
+			body = fmt.Sprintf("%s({%s: %s})", *parentMixinName, fieldName, paramName)
+		}
+		line := fmt.Sprintf("%s %s,", signature, body)
+		m.writeLine(line)
+	} else if p.schemaType != nil {
+		paramType := *p.schemaType
+
+		var body string
+		switch paramType {
+		case "array":
+			if parentMixinName == nil {
+				body = fmt.Sprintf(
+					"if std.type(%s) == \"array\" then {%s+: %s} else {%s+: [%s]}",
+					paramName, fieldName, paramName, fieldName, paramName,
+				)
+			} else {
+				body = fmt.Sprintf(
+					"if std.type(%s) == \"array\" then %s({%s+: %s}) else %s({%s+: [%s]})",
+					paramName, *parentMixinName, fieldName, paramName, *parentMixinName,
+					fieldName, paramName,
+				)
+			}
+		case "integer", "string", "boolean":
+			if parentMixinName == nil {
+				body = fmt.Sprintf("{%s: %s}", fieldName, paramName)
+			} else {
+				body = fmt.Sprintf("%s({%s: %s})", *parentMixinName, fieldName, paramName)
+			}
+		case "object":
+			if parentMixinName == nil {
+				body = fmt.Sprintf("{%s+: %s}", fieldName, paramName)
+			} else {
+				body = fmt.Sprintf("%s({%s+: %s})", *parentMixinName, fieldName, paramName)
+			}
+		default:
+			log.Panicf("Unrecognized type '%s'", paramType)
+		}
+
+		line := fmt.Sprintf("%s %s,", signature, body)
+		m.writeLine(line)
+	} else {
+		log.Panicf("Neither a type nor a ref")
+	}
+}
+
+func (aos propertySet) sortAndFilterBlacklisted() propertySlice {
+	properties := propertySlice{}
+	for _, pm := range aos {
+		k8sVersion := pm.root().spec.Info.Version
+		var name kubespec.PropertyName
+		if pm.kind == typeAlias {
+			name = kubespec.PropertyName(strings.TrimSuffix(string(pm.name), "Type"))
+		} else {
+			name = pm.name
+		}
+		if kubeversion.IsBlacklistedProperty(k8sVersion, pm.path, name) {
+			continue
+		} else if pm.ref != nil {
+			if parsed := pm.ref.Name().Parse(); parsed.Version == nil {
+				// TODO: Might want to error out here.
+				continue
+			}
+		}
+		properties = append(properties, pm)
+	}
+	sort.Slice(properties, func(i, j int) bool {
+		return properties[i].name < properties[j].name
+	})
+	return properties
+}
+
+//-----------------------------------------------------------------------------
+// Comments.
+//-----------------------------------------------------------------------------
+
+type comments []string
+
+func newComments(text string) comments {
+	return strings.Split(text, "\n")
+}
+
+func (cs *comments) emit(m *indentWriter) {
+	for _, comment := range *cs {
+		if comment == "" {
+			// Don't create trailing space if comment is empty.
+			m.writeLine("//")
+		} else {
+			m.writeLine(fmt.Sprintf("// %s", comment))
+		}
+	}
+}
diff --git a/vendor/github.com/ksonnet/ksonnet-lib/ksonnet-gen/ksonnet/util.go b/vendor/github.com/ksonnet/ksonnet-lib/ksonnet-gen/ksonnet/util.go
new file mode 100644
index 0000000000000000000000000000000000000000..b02a9bd4d31add0eb782990f1807599b28fde6b8
--- /dev/null
+++ b/vendor/github.com/ksonnet/ksonnet-lib/ksonnet-gen/ksonnet/util.go
@@ -0,0 +1,29 @@
+package ksonnet
+
+import (
+	"github.com/ksonnet/ksonnet-lib/ksonnet-gen/kubespec"
+)
+
+const constructorName = "new"
+
+// isMixinRef will check whether a `ObjectRef` refers to an API object
+// that can be turned into a mixin. This should be true of the vast
+// majority of non-nil `ObjectRef`s. The most common exception is
+// `IntOrString`, which should not be turned into a mixin, and should
+// instead by transformed into a property method that behaves
+// identically to one taking an int or a ref as argument.
+func isMixinRef(or *kubespec.ObjectRef) bool {
+	return or != nil && *or != "#/definitions/io.k8s.apimachinery.pkg.util.intstr.IntOrString"
+}
+
+var specialProperties = map[kubespec.PropertyName]kubespec.PropertyName{
+	"apiVersion": "apiVersion",
+	"kind":       "kind",
+}
+
+var specialPropertiesList = []string{"apiVersion", "kind"}
+
+func isSpecialProperty(pn kubespec.PropertyName) bool {
+	_, ok := specialProperties[pn]
+	return ok
+}
diff --git a/vendor/github.com/ksonnet/ksonnet-lib/ksonnet-gen/kubespec/parsing.go b/vendor/github.com/ksonnet/ksonnet-lib/ksonnet-gen/kubespec/parsing.go
new file mode 100644
index 0000000000000000000000000000000000000000..d154fd8b0d7f3f2919256e290682baa186507296
--- /dev/null
+++ b/vendor/github.com/ksonnet/ksonnet-lib/ksonnet-gen/kubespec/parsing.go
@@ -0,0 +1,235 @@
+package kubespec
+
+import (
+	"fmt"
+	"log"
+	"strings"
+)
+
+//-----------------------------------------------------------------------------
+// Utility methods for `DefinitionName` and `ObjectRef`.
+//-----------------------------------------------------------------------------
+
+// Parse will parse a `DefinitionName` into a structured
+// `ParsedDefinitionName`.
+func (dn *DefinitionName) Parse() *ParsedDefinitionName {
+	split := strings.Split(string(*dn), ".")
+	if len(split) < 6 {
+		log.Fatalf("Failed to parse definition name '%s'", string(*dn))
+	} else if split[0] != "io" || split[1] != "k8s" || split[3] != "pkg" {
+		log.Fatalf("Failed to parse definition name '%s'", string(*dn))
+	}
+
+	codebase := split[2]
+
+	if split[4] == "api" {
+		// Name is something like: `io.k8s.kubernetes.pkg.api.v1.LimitRangeSpec`.
+		if len(split) < 7 {
+			log.Fatalf(
+				"Expected >= 7 path components for package 'api' in path: '%s'",
+				string(*dn))
+		}
+		versionString := VersionString(split[5])
+		return &ParsedDefinitionName{
+			PackageType: Core,
+			Codebase:    codebase,
+			Group:       nil,
+			Version:     &versionString,
+			Kind:        ObjectKind(split[6]),
+		}
+	} else if split[4] == "apis" {
+		// Name is something like: `io.k8s.kubernetes.pkg.apis.batch.v1.JobList`.
+		if len(split) < 8 {
+			log.Fatalf(
+				"Expected >= 8 path components for package 'apis' in path: '%s'",
+				string(*dn))
+		}
+		groupName := GroupName(split[5])
+		versionString := VersionString(split[6])
+		return &ParsedDefinitionName{
+			PackageType: APIs,
+			Codebase:    codebase,
+			Group:       &groupName,
+			Version:     &versionString,
+			Kind:        ObjectKind(split[7]),
+		}
+	} else if split[4] == "util" {
+		if len(split) < 7 {
+			log.Fatalf(
+				"Expected >= 7 path components for package 'api' in path: '%s'",
+				string(*dn))
+		}
+		versionString := VersionString(split[5])
+		return &ParsedDefinitionName{
+			PackageType: Util,
+			Codebase:    codebase,
+			Group:       nil,
+			Version:     &versionString,
+			Kind:        ObjectKind(split[6]),
+		}
+	} else if split[4] == "runtime" {
+		// Name is something like: `io.k8s.apimachinery.pkg.runtime.RawExtension`.
+		return &ParsedDefinitionName{
+			PackageType: Runtime,
+			Codebase:    codebase,
+			Group:       nil,
+			Version:     nil,
+			Kind:        ObjectKind(split[5]),
+		}
+	} else if split[4] == "version" {
+		// Name is something like: `io.k8s.apimachinery.pkg.version.Info`.
+		return &ParsedDefinitionName{
+			PackageType: Version,
+			Codebase:    codebase,
+			Group:       nil,
+			Version:     nil,
+			Kind:        ObjectKind(split[5]),
+		}
+	}
+
+	log.Fatalf("Unknown package name '%s' in path: '%s'", split[4], string(*dn))
+	return nil
+}
+
+// Name parses a `DefinitionName` from an `ObjectRef`. `ObjectRef`s
+// that refer to a definition contain two parts: (1) a special prefix,
+// and (2) a `DefinitionName`, so this function simply strips the
+// prefix off.
+func (or *ObjectRef) Name() *DefinitionName {
+	defn := "#/definitions/"
+	ref := string(*or)
+	if !strings.HasPrefix(ref, defn) {
+		log.Fatalln(ref)
+	}
+	name := DefinitionName(strings.TrimPrefix(ref, defn))
+	return &name
+}
+
+func (dn DefinitionName) AsObjectRef() *ObjectRef {
+	or := ObjectRef("#/definitions/" + dn)
+	return &or
+}
+
+//-----------------------------------------------------------------------------
+// Parsed definition name.
+//-----------------------------------------------------------------------------
+
+// Package represents the type of the definition, either `APIs`, which
+// have API groups (e.g., extensions, apps, meta, and so on), or
+// `Core`, which does not.
+type Package int
+
+const (
+	// Core is a package that contains the Kubernetes Core objects.
+	Core Package = iota
+
+	// APIs is a set of non-core packages grouped loosely by semantic
+	// functionality (e.g., apps, extensions, and so on).
+	APIs
+
+	//
+	// Internal packages.
+	//
+
+	// Util is a package that contains utilities used for both testing
+	// and running Kubernetes.
+	Util
+
+	// Runtime is a package that contains various utilities used in the
+	// Kubernetes runtime.
+	Runtime
+
+	// Version is a package that supplies version information collected
+	// at build time.
+	Version
+)
+
+// ParsedDefinitionName is a parsed version of a fully-qualified
+// OpenAPI spec name. For example,
+// `io.k8s.kubernetes.pkg.api.v1.Container` would parse into an
+// instance of the struct below.
+type ParsedDefinitionName struct {
+	PackageType Package
+	Codebase    string
+	Group       *GroupName     // Pointer because it's optional.
+	Version     *VersionString // Pointer because it's optional.
+	Kind        ObjectKind
+}
+
+// GroupName represetents a Kubernetes group name (e.g., apps,
+// extensions, etc.)
+type GroupName string
+
+func (gn GroupName) String() string {
+	return string(gn)
+}
+
+// ObjectKind represents the `kind` of a Kubernetes API object (e.g.,
+// Service, Deployment, etc.)
+type ObjectKind string
+
+func (ok ObjectKind) String() string {
+	return string(ok)
+}
+
+// VersionString is the string representation of an API version (e.g.,
+// v1, v1beta1, etc.)
+type VersionString string
+
+func (vs VersionString) String() string {
+	return string(vs)
+}
+
+// Unparse transforms a `ParsedDefinitionName` back into its
+// corresponding string, e.g.,
+// `io.k8s.kubernetes.pkg.api.v1.Container`.
+func (p *ParsedDefinitionName) Unparse() DefinitionName {
+	switch p.PackageType {
+	case Core:
+		{
+			return DefinitionName(fmt.Sprintf(
+				"io.k8s.%s.pkg.api.%s.%s",
+				p.Codebase,
+				*p.Version,
+				p.Kind))
+		}
+	case Util:
+		{
+			return DefinitionName(fmt.Sprintf(
+				"io.k8s.%s.pkg.util.%s.%s",
+				p.Codebase,
+				*p.Version,
+				p.Kind))
+		}
+	case APIs:
+		{
+			return DefinitionName(fmt.Sprintf(
+				"io.k8s.%s.pkg.apis.%s.%s.%s",
+				p.Codebase,
+				*p.Group,
+				*p.Version,
+				p.Kind))
+		}
+	case Version:
+		{
+			return DefinitionName(fmt.Sprintf(
+				"io.k8s.%s.pkg.version.%s",
+				p.Codebase,
+				p.Kind))
+		}
+	case Runtime:
+		{
+			return DefinitionName(fmt.Sprintf(
+				"io.k8s.%s.pkg.runtime.%s",
+				p.Codebase,
+				p.Kind))
+		}
+	default:
+		{
+			log.Fatalf(
+				"Failed to unparse definition name, did not recognize kind '%d'",
+				p.PackageType)
+			return ""
+		}
+	}
+}
diff --git a/vendor/github.com/ksonnet/ksonnet-lib/ksonnet-gen/kubespec/swagger.go b/vendor/github.com/ksonnet/ksonnet-lib/ksonnet-gen/kubespec/swagger.go
new file mode 100644
index 0000000000000000000000000000000000000000..2310d1a9028260e355703e9f53667396f1472c5e
--- /dev/null
+++ b/vendor/github.com/ksonnet/ksonnet-lib/ksonnet-gen/kubespec/swagger.go
@@ -0,0 +1,108 @@
+package kubespec
+
+// APISpec represents an OpenAPI specification of an API.
+type APISpec struct {
+	SwaggerVersion string            `json:"swagger"`
+	Info           *SchemaInfo       `json:"info"`
+	Definitions    SchemaDefinitions `json:"definitions"`
+
+	// Fields we currently ignore:
+	//   - paths
+	//   - securityDefinitions
+	//   - security
+
+	// Not part of the OpenAPI spec. Filled in later.
+	FilePath string
+	Text     []byte
+}
+
+// SchemaInfo contains information about the the API represented with
+// `APISpec`. For example, `title` might be `"Kubernetes"`, and
+// `version` might be `"v1.7.0"`.
+type SchemaInfo struct {
+	Title   string `json:"title"`
+	Version string `json:"version"`
+}
+
+// SchemaDefinition is an API object definition. For example, this
+// might contain a name (e.g., `v1.APIGroup`), a set of properties
+// (e.g., `apiVersion`, `kind`, and so on), and the names of required
+// properties.
+type SchemaDefinition struct {
+	Type          *SchemaType   `json:"type"`
+	Description   string        `json:"description"` // nullable.
+	Required      []string      `json:"required"`    // nullable.
+	Properties    Properties    `json:"properties"`  // nullable.
+	TopLevelSpecs TopLevelSpecs `json:"x-kubernetes-group-version-kind"`
+}
+
+// TopLevelSpec is a property that exists on `SchemaDefinition`s for
+// top-level API objects.
+type TopLevelSpec struct {
+	Group   GroupName     `json:"Group"`
+	Version VersionString `json:"Version"`
+	Kind    ObjectKind    `json:"Kind"`
+}
+type TopLevelSpecs []*TopLevelSpec
+
+// SchemaDefinitions is a named collection of `SchemaDefinition`s,
+// represented as a collection mapping definition name ->
+// `SchemaDefinition`.
+type SchemaDefinitions map[DefinitionName]*SchemaDefinition
+
+// Property represents an object property for some API object. For
+// example, `v1.APIGroup` might contain a property called
+// `apiVersion`, which would be specifid by a `Property`.
+type Property struct {
+	Description string      `json:"description"`
+	Type        *SchemaType `json:"type"`
+	Ref         *ObjectRef  `json:"$ref"`
+	Items       Items       `json:"items"` // nil unless Type == "array".
+}
+
+// Properties is a named collection of `Properties`s, represented as a
+// collection mapping definition name -> `Properties`.
+type Properties map[PropertyName]*Property
+
+// Items represents the type of an element in an array. Usually this
+// is used to fully specify a `Property` object whose `type` field is
+// `"array"`.
+type Items struct {
+	Ref *ObjectRef `json:"$ref"`
+
+	// Ignored fields:
+	// - Type *SchemaType `json:"type"`
+	// - Format *string `json:"format"`
+}
+
+// SchemaType represents the type of some object in an API spec. For
+// example, a property might have type `string`.
+type SchemaType string
+
+func (st SchemaType) String() string {
+	return string(st)
+}
+
+// ObjectRef represents a reference to some API object. For example,
+// `#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta`
+type ObjectRef string
+
+func (or ObjectRef) String() string {
+	return string(or)
+}
+
+// PropertyName represents the name of a property. For example,
+// `apiVersion` or `kind`.
+type PropertyName string
+
+func (pn PropertyName) String() string {
+	return string(pn)
+}
+
+// DefinitionName represents the name of a definition. For example,
+// `v1.APIGroup`.
+type DefinitionName string
+
+func (dn DefinitionName) String() string {
+	return string(dn)
+}
diff --git a/vendor/github.com/ksonnet/ksonnet-lib/ksonnet-gen/kubeversion/blacklist.jq b/vendor/github.com/ksonnet/ksonnet-lib/ksonnet-gen/kubeversion/blacklist.jq
new file mode 100755
index 0000000000000000000000000000000000000000..99bcca7e25eed0a0340f098d3c6a9aaafdde5ec5
--- /dev/null
+++ b/vendor/github.com/ksonnet/ksonnet-lib/ksonnet-gen/kubeversion/blacklist.jq
@@ -0,0 +1,103 @@
+#!/usr/bin/env jq -S -f
+
+
+# -----------------------------------------------------------------------------
+# USAGE NOTES.
+#
+# This `jq` script will generate a list of top-level Kubernetes API
+# objects that contain either (or both of):
+#
+#   1. a property with the name `"status"`, or
+#   2. a property whose type is `meta.v1.ListMeta`.
+#
+# For example:
+#
+#  {
+#    "io.k8s.apimachinery.pkg.apis.meta.v1.Status": [
+#      "status", "metadata"
+#    ]
+#  }
+#
+# This would indicate that the fields `metadata` and `status` are to
+# be blacklisted in the object `meta.v1.Status`.
+#
+#
+# Usage:
+#   cat swagger.json | jq -S -f blacklist.jq
+#
+# Or, if you are on an OS with jq > v1.4
+#   cat swagger.json | ./blacklist.jq
+#
+# NOTE: It is very important to pass the -S flag here, because sorting
+# the object keys makes the output diffable.
+# -----------------------------------------------------------------------------
+
+
+# has_status_prop takes an Kubernetes API object definition from the
+# swagger spec, and outputs a boolean indicating whether that API
+# object has a property called `status`.
+#
+# For example, the input might be a
+# `io.k8s.kubernetes.pkg.apis.apps.v1beta1.Deployment` object, which
+# does indeed have a `status` field.
+def has_status_prop:
+  . as $definition
+  | if $definition.properties.status != null then true else false end;
+
+# property_has_listmeta_type takes the property of a Kubernetes API
+# object definition, and returns a bool indicating whether its type is
+# a `$ref` of `meta.v1.ListMeta`.
+#
+# For example, `io.k8s.kubernetes.pkg.apis.apps.v1beta1.Deployment`
+# does not have a property with a type that is a `$ref` to
+# `meta.v1.ListMeta`.
+def property_has_listmeta_type:
+  . as $property
+  | $property["$ref"] != null and
+    $property["$ref"] == "#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.ListMeta";
+
+# props_with_listmeta_type returns the names of all properties in some
+# Kubernetes API object definition whose type is `meta.v1.ListMeta`.
+#
+# For example, `io.k8s.kubernetes.pkg.apis.apps.v1beta1.Deployment`
+# does not contain any properties with this type, so we would return
+# an empty array, while another object might return a list of names.
+def props_with_listmeta_type: [
+  . as $definition
+  | select($definition.properties != null)
+  | $definition.properties
+  | to_entries[]
+  | select(.value | property_has_listmeta_type)
+  | .key
+];
+
+# entry_blacklist_props takes a key/value pair representing a
+# Kubernetes API object and its name, and returns a list of properties
+# that are blacklisted.
+#
+# For example, `.key` might be
+# `io.k8s.kubernetes.pkg.apis.apps.v1beta1.Deployment`, while `.value`
+# woudl be the actual swagger specification of the `Deployment`
+# object.
+def entry_blacklist_props:
+  .value as $definition
+  | ($definition | has_status_prop) as $has_status_prop
+  | ($definition | props_with_listmeta_type) as $props_with_listmeta_type
+  | ($props_with_listmeta_type | length > 0) as $has_listmeta_type_props
+  | if $has_status_prop and $has_listmeta_type_props
+    then {(.key): (["status"] | .+ $props_with_listmeta_type)}
+    elif $has_status_prop
+    then {(.key): ["status"]}
+    elif $has_listmeta_type_props
+    then {(.key): $props_with_listmeta_type}
+    else {(.key): []}
+    end;
+
+def create_blacklist:
+  [ .definitions | to_entries[] | entry_blacklist_props ]
+  | add
+  | with_entries(select(.value | length > 0));
+
+
+# Execute.
+create_blacklist
diff --git a/vendor/github.com/ksonnet/ksonnet-lib/ksonnet-gen/kubeversion/data.go b/vendor/github.com/ksonnet/ksonnet-lib/ksonnet-gen/kubeversion/data.go
new file mode 100644
index 0000000000000000000000000000000000000000..a455bebc3a833c880eb07f14e8b10d9a4bc29b2e
--- /dev/null
+++ b/vendor/github.com/ksonnet/ksonnet-lib/ksonnet-gen/kubeversion/data.go
@@ -0,0 +1,254 @@
+package kubeversion
+
+//-----------------------------------------------------------------------------
+// Kubernetes version-specific data for customizing code that's
+// emitted.
+//-----------------------------------------------------------------------------
+
+var versions = map[string]versionData{
+	"v1.7.0": versionData{
+		idAliases: map[string]string{
+			// Properties of objects. Stuff like `cinder.volumeId`.
+			"hostIPC":                         "hostIpc",
+			"hostPID":                         "hostPid",
+			"targetCPUUtilizationPercentage":  "targetCpuUtilizationPercentage",
+			"externalID":                      "externalId",
+			"podCIDR":                         "podCidr",
+			"providerID":                      "providerId",
+			"bootID":                          "bootId",
+			"machineID":                       "machineId",
+			"systemUUID":                      "systemUuid",
+			"volumeID":                        "volumeId",
+			"diskURI":                         "diskUri",
+			"targetWWNs":                      "targetWwns",
+			"datasetUUID":                     "datasetUuid",
+			"pdID":                            "pdId",
+			"scaleIO":                         "scaleIo",
+			"podIP":                           "podIp",
+			"hostIP":                          "hostIp",
+			"clusterIP":                       "clusterIp",
+			"externalIPs":                     "externalIps",
+			"loadBalancerIP":                  "loadBalancerIp",
+			"containerID":                     "containerId",
+			"imageID":                         "imageId",
+			"serverAddressByClientCIDRs":      "serverAddressByClientCidrs",
+			"clientCIDR":                      "clientCidr",
+			"nonResourceURLs":                 "nonResourceUrls",
+			"currentCPUUtilizationPercentage": "currentCpuUtilizationPercentage",
+			"downwardAPI":                     "downwardApi",
+
+			// Types. These have capitalized first letters, and exist in
+			// places like `core.v1.AWSElasticBlockStoreVolumeSource`.
+			"AWSElasticBlockStoreVolumeSource": "awsElasticBlockStoreVolumeSource",
+			"CephFSVolumeSource":               "cephFsVolumeSource",
+			"DownwardAPIProjection":            "downwardApiProjection",
+			"DownwardAPIVolumeFile":            "downwardApiVolumeFile",
+			"DownwardAPIVolumeSource":          "downwardApiVolumeSource",
+			"FCVolumeSource":                   "fcVolumeSource",
+			"GCEPersistentDiskVolumeSource":    "gcePersistentDiskVolumeSource",
+			"HTTPGetAction":                    "httpGetAction",
+			"HTTPHeader":                       "httpHeader",
+			"ISCSIVolumeSource":                "iscsiVolumeSource",
+			"NFSVolumeSource":                  "nfsVolumeSource",
+			"RBDVolumeSource":                  "rbdVolumeSource",
+			"SELinuxOptions":                   "seLinuxOptions",
+			"ScaleIOVolumeSource":              "scaleIoVolumeSource",
+			"TCPSocketAction":                  "tcpSocketAction",
+			"APIVersion":                       "apiVersion",
+			"FSGroupStrategyOptions":           "fsGroupStrategyOptions",
+			"HTTPIngressPath":                  "httpIngressPath",
+			"HTTPIngressRuleValue":             "httpIngressRuleValue",
+			"IDRange":                          "idRange",
+			"IngressTLS":                       "ingressTls",
+			"SELinuxStrategyOptions":           "seLinuxStrategyOptions",
+			"APIGroup":                         "apiGroup",
+			"APIGroupList":                     "apiGroupList",
+			"APIResource":                      "apiResource",
+			"APIResourceList":                  "apiResourceList",
+			"APIVersions":                      "apiVersions",
+			"ServerAddressByClientCIDR":        "serverAddressByClientCidr",
+		},
+		constructorSpecs: map[string][]CustomConstructorSpec{
+			"io.k8s.kubernetes.pkg.api.v1.Container": []CustomConstructorSpec{
+				newConstructor("new", newParam("name"), newParam("image")),
+			},
+			"io.k8s.kubernetes.pkg.api.v1.ContainerPort": []CustomConstructorSpec{
+				newConstructor("new", newParam("containerPort")),
+				newConstructor("newNamed", newParam("name"), newParam("containerPort")),
+			},
+			"io.k8s.kubernetes.pkg.api.v1.EnvVar": []CustomConstructorSpec{
+				newConstructor("new", newParam("name"), newParam("value")),
+				newConstructor(
+					"fromSecretRef",
+					newParam("name"),
+					newParamNestedRef("secretRefName", "mixin.valueFrom.secretKeyRef.name"),
+					newParamNestedRef("secretRefKey", "mixin.valueFrom.secretKeyRef.key")),
+				newConstructor(
+					"fromFieldPath",
+					newParam("name"),
+					newParamNestedRef("fieldPath", "mixin.valueFrom.fieldRef.fieldPath")),
+			},
+			"io.k8s.kubernetes.pkg.api.v1.KeyToPath": []CustomConstructorSpec{
+				newConstructor("new", newParam("key"), newParam("path")),
+			},
+			"io.k8s.kubernetes.pkg.api.v1.Service": []CustomConstructorSpec{
+				newConstructor(
+					"new",
+					newParamNestedRef("name", "mixin.metadata.name"),
+					newParamNestedRef("selector", "mixin.spec.selector"),
+					newParamNestedRef("ports", "mixin.spec.ports")),
+			},
+			"io.k8s.kubernetes.pkg.api.v1.ServicePort": []CustomConstructorSpec{
+				newConstructor("new", newParam("port"), newParam("targetPort")),
+				newConstructor("newNamed", newParam("name"), newParam("port"), newParam("targetPort")),
+			},
+			"io.k8s.kubernetes.pkg.api.v1.Volume": []CustomConstructorSpec{
+				newConstructor(
+					"fromConfigMap",
+					newParam("name"),
+					newParamNestedRef("configMapName", "mixin.configMap.name"),
+					newParamNestedRef("configMapItems", "mixin.configMap.items")),
+				newConstructor(
+					"fromEmptyDir",
+					newParam("name"),
+					newParamNestedRefDefault("emptyDir", "mixin.emptyDir.mixinInstance", "{}")),
+				newConstructor(
+					"fromPersistentVolumeClaim",
+					newParam("name"),
+					newParamNestedRef("claimName", "mixin.persistentVolumeClaim.claimName")),
+				newConstructor(
+					"fromHostPath",
+					newParam("name"),
+					newParamNestedRef("hostPath", "mixin.hostPath.path")),
+			},
+			"io.k8s.kubernetes.pkg.api.v1.VolumeMount": []CustomConstructorSpec{
+				newConstructor("new", newParam("name"), newParam("mountPath"), newParamWithDefault("readOnly", "false")),
+			},
+			"io.k8s.kubernetes.pkg.apis.apps.v1beta1.Deployment": []CustomConstructorSpec{
+				newConstructor(
+					"new",
+					newParamNestedRef("name", "mixin.metadata.name"),
+					newParamNestedRef("replicas", "mixin.spec.replicas"),
+					newParamNestedRef("containers", "mixin.spec.template.spec.containers"),
+					newParamNestedRefDefault("podLabels", "mixin.spec.template.metadata.labels", "{}")),
+			},
+			"io.k8s.kubernetes.pkg.apis.extensions.v1beta1.Deployment": []CustomConstructorSpec{
+				newConstructor(
+					"new",
+					newParamNestedRef("name", "mixin.metadata.name"),
+					newParamNestedRef("replicas", "mixin.spec.replicas"),
+					newParamNestedRef("containers", "mixin.spec.template.spec.containers"),
+					newParamNestedRefDefault("podLabels", "mixin.spec.template.metadata.labels", "{}")),
+			},
+		},
+		propertyBlacklist: map[string]propertySet{
+			// Metadata fields.
+			"io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta": newPropertySet(
+				"creationTimestamp", "deletionTimestamp", "generation",
+				"ownerReferences", "resourceVersion", "selfLink", "uid",
+			),
+
+			// Fields whose types are
+			// `io.k8s.apimachinery.pkg.apis.meta.v1.ListMeta`.
+			"io.k8s.kubernetes.pkg.api.v1.ComponentStatusList":                              newPropertySet("metadata"),
+			"io.k8s.kubernetes.pkg.api.v1.ConfigMapList":                                    newPropertySet("metadata"),
+			"io.k8s.kubernetes.pkg.api.v1.EndpointsList":                                    newPropertySet("metadata"),
+			"io.k8s.kubernetes.pkg.api.v1.EventList":                                        newPropertySet("metadata"),
+			"io.k8s.kubernetes.pkg.api.v1.LimitRangeList":                                   newPropertySet("metadata"),
+			"io.k8s.kubernetes.pkg.api.v1.NamespaceList":                                    newPropertySet("metadata"),
+			"io.k8s.kubernetes.pkg.api.v1.NodeList":                                         newPropertySet("metadata"),
+			"io.k8s.kubernetes.pkg.api.v1.PersistentVolumeClaimList":                        newPropertySet("metadata"),
+			"io.k8s.kubernetes.pkg.api.v1.PersistentVolumeList":                             newPropertySet("metadata"),
+			"io.k8s.kubernetes.pkg.api.v1.PodList":                                          newPropertySet("metadata"),
+			"io.k8s.kubernetes.pkg.api.v1.PodTemplateList":                                  newPropertySet("metadata"),
+			"io.k8s.kubernetes.pkg.api.v1.ReplicationControllerList":                        newPropertySet("metadata"),
+			"io.k8s.kubernetes.pkg.api.v1.ResourceQuotaList":                                newPropertySet("metadata"),
+			"io.k8s.kubernetes.pkg.api.v1.SecretList":                                       newPropertySet("metadata"),
+			"io.k8s.kubernetes.pkg.api.v1.ServiceAccountList":                               newPropertySet("metadata"),
+			"io.k8s.kubernetes.pkg.api.v1.ServiceList":                                      newPropertySet("metadata"),
+			"io.k8s.kubernetes.pkg.apis.apps.v1beta1.DeploymentList":                        newPropertySet("metadata"),
+			"io.k8s.kubernetes.pkg.apis.apps.v1beta1.StatefulSetList":                       newPropertySet("metadata"),
+			"io.k8s.kubernetes.pkg.apis.autoscaling.v1.HorizontalPodAutoscalerList":         newPropertySet("metadata"),
+			"io.k8s.kubernetes.pkg.apis.autoscaling.v2alpha1.HorizontalPodAutoscalerList":   newPropertySet("metadata"),
+			"io.k8s.kubernetes.pkg.apis.batch.v1.JobList":                                   newPropertySet("metadata"),
+			"io.k8s.kubernetes.pkg.apis.batch.v2alpha1.CronJobList":                         newPropertySet("metadata"),
+			"io.k8s.kubernetes.pkg.apis.certificates.v1beta1.CertificateSigningRequestList": newPropertySet("metadata"),
+			"io.k8s.kubernetes.pkg.apis.extensions.v1beta1.DaemonSetList":                   newPropertySet("metadata"),
+			"io.k8s.kubernetes.pkg.apis.extensions.v1beta1.DeploymentList":                  newPropertySet("metadata"),
+			"io.k8s.kubernetes.pkg.apis.extensions.v1beta1.IngressList":                     newPropertySet("metadata"),
+			"io.k8s.kubernetes.pkg.apis.extensions.v1beta1.NetworkPolicyList":               newPropertySet("metadata"),
+			"io.k8s.kubernetes.pkg.apis.extensions.v1beta1.PodSecurityPolicyList":           newPropertySet("metadata"),
+			"io.k8s.kubernetes.pkg.apis.extensions.v1beta1.ReplicaSetList":                  newPropertySet("metadata"),
+			"io.k8s.kubernetes.pkg.apis.extensions.v1beta1.ThirdPartyResourceList":          newPropertySet("metadata"),
+			"io.k8s.kubernetes.pkg.apis.policy.v1beta1.PodDisruptionBudgetList":             newPropertySet("metadata"),
+			"io.k8s.kubernetes.pkg.apis.rbac.v1alpha1.ClusterRoleBindingList":               newPropertySet("metadata"),
+			"io.k8s.kubernetes.pkg.apis.rbac.v1alpha1.ClusterRoleList":                      newPropertySet("metadata"),
+			"io.k8s.kubernetes.pkg.apis.rbac.v1alpha1.RoleBindingList":                      newPropertySet("metadata"),
+			"io.k8s.kubernetes.pkg.apis.rbac.v1alpha1.RoleList":                             newPropertySet("metadata"),
+			"io.k8s.kubernetes.pkg.apis.rbac.v1beta1.ClusterRoleBindingList":                newPropertySet("metadata"),
+			"io.k8s.kubernetes.pkg.apis.rbac.v1beta1.ClusterRoleList":                       newPropertySet("metadata"),
+			"io.k8s.kubernetes.pkg.apis.rbac.v1beta1.RoleBindingList":                       newPropertySet("metadata"),
+			"io.k8s.kubernetes.pkg.apis.rbac.v1beta1.RoleList":                              newPropertySet("metadata"),
+			"io.k8s.kubernetes.pkg.apis.settings.v1alpha1.PodPresetList":                    newPropertySet("metadata"),
+			"io.k8s.kubernetes.pkg.apis.storage.v1.StorageClassList":                        newPropertySet("metadata"),
+			"io.k8s.kubernetes.pkg.apis.storage.v1beta1.StorageClassList":                   newPropertySet("metadata"),
+
+			// Status fields.
+			"io.k8s.kubernetes.pkg.api.v1.Namespace":                                    newPropertySet("status"),
+			"io.k8s.kubernetes.pkg.api.v1.Node":                                         newPropertySet("status"),
+			"io.k8s.kubernetes.pkg.api.v1.NodeCondition":                                newPropertySet("status"),
+			"io.k8s.kubernetes.pkg.api.v1.PersistentVolume":                             newPropertySet("status"),
+			"io.k8s.kubernetes.pkg.api.v1.PersistentVolumeClaim":                        newPropertySet("status"),
+			"io.k8s.kubernetes.pkg.api.v1.Pod":                                          newPropertySet("status"),
+			"io.k8s.kubernetes.pkg.api.v1.PodCondition":                                 newPropertySet("status"),
+			"io.k8s.kubernetes.pkg.api.v1.ReplicationController":                        newPropertySet("status"),
+			"io.k8s.kubernetes.pkg.api.v1.ReplicationControllerCondition":               newPropertySet("status"),
+			"io.k8s.kubernetes.pkg.api.v1.ResourceQuota":                                newPropertySet("status"),
+			"io.k8s.kubernetes.pkg.api.v1.Service":                                      newPropertySet("status"),
+			"io.k8s.kubernetes.pkg.apis.apps.v1beta1.Deployment":                        newPropertySet("status"),
+			"io.k8s.kubernetes.pkg.apis.apps.v1beta1.DeploymentCondition":               newPropertySet("status"),
+			"io.k8s.kubernetes.pkg.apis.apps.v1beta1.Scale":                             newPropertySet("status"),
+			"io.k8s.kubernetes.pkg.apis.apps.v1beta1.StatefulSet":                       newPropertySet("status"),
+			"io.k8s.kubernetes.pkg.apis.authentication.v1.TokenReview":                  newPropertySet("status"),
+			"io.k8s.kubernetes.pkg.apis.authentication.v1beta1.TokenReview":             newPropertySet("status"),
+			"io.k8s.kubernetes.pkg.apis.authorization.v1.LocalSubjectAccessReview":      newPropertySet("status"),
+			"io.k8s.kubernetes.pkg.apis.authorization.v1.SelfSubjectAccessReview":       newPropertySet("status"),
+			"io.k8s.kubernetes.pkg.apis.authorization.v1.SubjectAccessReview":           newPropertySet("status"),
+			"io.k8s.kubernetes.pkg.apis.authorization.v1beta1.LocalSubjectAccessReview": newPropertySet("status"),
+			"io.k8s.kubernetes.pkg.apis.authorization.v1beta1.SelfSubjectAccessReview":  newPropertySet("status"),
+			"io.k8s.kubernetes.pkg.apis.authorization.v1beta1.SubjectAccessReview":      newPropertySet("status"),
+			"io.k8s.kubernetes.pkg.apis.autoscaling.v1.HorizontalPodAutoscaler":         newPropertySet("status"),
+			"io.k8s.kubernetes.pkg.apis.autoscaling.v1.Scale":                           newPropertySet("status"),
+			"io.k8s.kubernetes.pkg.apis.autoscaling.v2alpha1.HorizontalPodAutoscaler":   newPropertySet("status"),
+			"io.k8s.kubernetes.pkg.apis.batch.v1.Job":                                   newPropertySet("status"),
+			"io.k8s.kubernetes.pkg.apis.batch.v1.JobCondition":                          newPropertySet("status"),
+			"io.k8s.kubernetes.pkg.apis.batch.v2alpha1.CronJob":                         newPropertySet("status"),
+			"io.k8s.kubernetes.pkg.apis.certificates.v1beta1.CertificateSigningRequest": newPropertySet("status"),
+			"io.k8s.kubernetes.pkg.apis.extensions.v1beta1.DaemonSet":                   newPropertySet("status"),
+			"io.k8s.kubernetes.pkg.apis.extensions.v1beta1.Deployment":                  newPropertySet("status"),
+			"io.k8s.kubernetes.pkg.apis.extensions.v1beta1.DeploymentCondition":         newPropertySet("status"),
+			"io.k8s.kubernetes.pkg.apis.extensions.v1beta1.Ingress":                     newPropertySet("status"),
+			"io.k8s.kubernetes.pkg.apis.extensions.v1beta1.ReplicaSet":                  newPropertySet("status"),
+			"io.k8s.kubernetes.pkg.apis.extensions.v1beta1.ReplicaSetCondition":         newPropertySet("status"),
+			"io.k8s.kubernetes.pkg.apis.extensions.v1beta1.Scale":                       newPropertySet("status"),
+			"io.k8s.kubernetes.pkg.apis.policy.v1beta1.PodDisruptionBudget":             newPropertySet("status"),
+
+			// TODO: Find a more principled way to omit "status" types.
+			// Currently we emit these in the `local hidden` in the `root`,
+			// so that we can type aliases. To get around the fact that some
+			// of their function names collide with Jsonnet keywords, we
+			// simply choose not to emit them. Eventually we will approach
+			// this problem in a more principled manner.
+			"io.k8s.kubernetes.pkg.api.v1.ComponentCondition":                     newPropertySet("error", "status"),
+			"io.k8s.kubernetes.pkg.apis.authentication.v1.TokenReviewStatus":      newPropertySet("error"),
+			"io.k8s.kubernetes.pkg.apis.authentication.v1beta1.TokenReviewStatus": newPropertySet("error"),
+
+			// Has both status and a property with type
+			// `io.k8s.apimachinery.pkg.apis.meta.v1.ListMeta`.
+			"io.k8s.apimachinery.pkg.apis.meta.v1.Status": newPropertySet("status", "metadata"),
+
+			// Misc.
+			"io.k8s.kubernetes.pkg.apis.extensions.v1beta1.DaemonSetSpec": newPropertySet("templateGeneration"),
+		},
+	},
+}
diff --git a/vendor/github.com/ksonnet/ksonnet-lib/ksonnet-gen/kubeversion/version.go b/vendor/github.com/ksonnet/ksonnet-lib/ksonnet-gen/kubeversion/version.go
new file mode 100644
index 0000000000000000000000000000000000000000..f988fcfeaf43f6c33cf433c8aa2c75f05adda88a
--- /dev/null
+++ b/vendor/github.com/ksonnet/ksonnet-lib/ksonnet-gen/kubeversion/version.go
@@ -0,0 +1,183 @@
+// Package kubeversion contains a collection of helper methods that
+// help to customize the code generated for ksonnet-lib to suit
+// different Kubernetes versions.
+//
+// For example, we may choose not to emit certain properties for some
+// objects in Kubernetes v1.7.0; or, we might want to rename a
+// property method. This package contains both the helper methods that
+// perform such transformations, as well as the data for the
+// transformations we use for each version.
+package kubeversion
+
+import (
+	"log"
+
+	"github.com/ksonnet/ksonnet-lib/ksonnet-gen/kubespec"
+)
+
+// MapIdentifier takes a text identifier and maps it to a
+// Jsonnet-appropriate identifier, for some version of Kubernetes. For
+// example, in Kubernetes v1.7.0, we might map `clusterIP` ->
+// `clusterIp`.
+func MapIdentifier(k8sVersion, id string) string {
+	verData, ok := versions[k8sVersion]
+	if !ok {
+		log.Fatalf("Unrecognized Kubernetes version '%s'", k8sVersion)
+	}
+
+	if alias, ok := verData.idAliases[id]; ok {
+		return alias
+	}
+	return id
+}
+
+// IsBlacklistedProperty taks a definition name (e.g.,
+// `io.k8s.kubernetes.pkg.apis.apps.v1beta1.Deployment`), a property
+// name (e.g., `status`), and reports whether it is blacklisted for
+// some Kubernetes version. This is particularly useful when deciding
+// whether or not to generate mixins and property methods for a given
+// property (as we likely wouldn't in the case of, say, `status`).
+func IsBlacklistedProperty(
+	k8sVersion string, path kubespec.DefinitionName,
+	propertyName kubespec.PropertyName,
+) bool {
+	verData, ok := versions[k8sVersion]
+	if !ok {
+		return false
+	}
+
+	bl, ok := verData.propertyBlacklist[string(path)]
+	if !ok {
+		return false
+	}
+
+	_, ok = bl[string(propertyName)]
+	return ok
+}
+
+func ConstructorSpec(
+	k8sVersion string, path kubespec.DefinitionName,
+) ([]CustomConstructorSpec, bool) {
+	verData, ok := versions[k8sVersion]
+	if !ok {
+		log.Fatalf("Unrecognized Kubernetes version '%s'", k8sVersion)
+	}
+
+	spec, ok := verData.constructorSpecs[string(path)]
+	return spec, ok
+}
+
+//-----------------------------------------------------------------------------
+// Core data structures for specifying version information.
+//-----------------------------------------------------------------------------
+
+type versionData struct {
+	idAliases         map[string]string
+	constructorSpecs  map[string][]CustomConstructorSpec
+	propertyBlacklist map[string]propertySet
+}
+
+type propertySet map[string]bool
+
+func newPropertySet(strings ...string) propertySet {
+	ps := make(propertySet)
+	for _, s := range strings {
+		ps[s] = true
+	}
+
+	return ps
+}
+
+//-----------------------------------------------------------------------------
+// Public Data structures for specifying custom constructors for API
+// objects.
+//-----------------------------------------------------------------------------
+
+// CustomConstructorSpec specifies a custom constructor for
+// `ksonnet-gen` to emit as part of ksonnet-lib. In particular, this
+// specifies a constructor of the form:
+//
+//   foo(bar, baz):: self.bar(bar) + self.baz(baz)
+//
+// The parameter list and the body are all generated from the `Params`
+// field.
+//
+// DESIGN NOTES:
+//
+// * If the user specifies a custom constructor, we will not emit the
+//   default zero-argument constructor, `new()`. This is a purposeful
+//   decision which we make because we are typically customizing the
+//   constructors precisely because the zero-argument constructor is
+//   not meaninful for a given API object.
+// * We currently do not check that parameter names are unique.
+//   Duplicate identifiers in a parameter list results in a Jsonnet
+//   compiler error, though, so this should be caught by review and
+//   CI, and it is hence not important for this case to be covered by
+//   this code.
+type CustomConstructorSpec struct {
+	ID     string
+	Params []CustomConstructorParam
+}
+
+func newConstructor(
+	id string, params ...CustomConstructorParam,
+) CustomConstructorSpec {
+	return CustomConstructorSpec{
+		ID:     id,
+		Params: params,
+	}
+}
+
+// CustomConstructorParam specifies a parameter for a
+// `CustomConstructorSpec`. This class allows users to specify
+// constructors of various forms, including:
+//
+// * The "normal" form, e.g., `foo(bar):: self.bar(bar)`,
+// * Parameters with default values, e.g., `foo(bar="baz")::
+//   self.bar(bar)`, and
+// * Parameters that are nested inside the object, e.g., `foo(bar)::
+//   self.baz.bat.bar(bar)`
+//
+// DESIGN NOTES:
+//
+// * For constructors that use nested paths, we do not currently check
+//   that the path is valid. So for example, `self.baz.bat.bar` in the
+//   example above may not correspond to a real property. We make this
+//   decision because it complicates the code, and it doesn't seem
+//   worth it since this feature is used relatively rarely.
+type CustomConstructorParam struct {
+	ID           string
+	DefaultValue *string
+	RelativePath *string
+}
+
+func newParam(name string) CustomConstructorParam {
+	return CustomConstructorParam{
+		ID:           name,
+		DefaultValue: nil,
+	}
+}
+
+func newParamWithDefault(name, def string) CustomConstructorParam {
+	return CustomConstructorParam{
+		ID:           name,
+		DefaultValue: &def,
+	}
+}
+
+func newParamNestedRef(name, relativePath string) CustomConstructorParam {
+	return CustomConstructorParam{
+		ID:           name,
+		RelativePath: &relativePath,
+	}
+}
+
+func newParamNestedRefDefault(
+	name, relativePath, def string,
+) CustomConstructorParam {
+	return CustomConstructorParam{
+		ID:           name,
+		RelativePath: &relativePath,
+		DefaultValue: &def,
+	}
+}
diff --git a/vendor/vendor.json b/vendor/vendor.json
index 7e9f4892e7a5721a9ad2e57ad6c381a3cf5c20a5..35bf4820f25089705c1ba8539b5cabe94e820471 100644
--- a/vendor/vendor.json
+++ b/vendor/vendor.json
@@ -190,6 +190,30 @@
 			"revision": "acf38b000a03e4ab89e40f20f1e548f4e6ac7f72",
 			"revisionTime": "2017-03-14T01:17:55Z"
 		},
+		{
+			"checksumSHA1": "wX+GmcWpMzCIcxR9YtN1FCM9BEE=",
+			"path": "github.com/ksonnet/ksonnet-lib/ksonnet-gen/jsonnet",
+			"revision": "fcf4c31408afc7b7469a1722c4cc06ccc315a722",
+			"revisionTime": "2017-08-16T22:14:57Z"
+		},
+		{
+			"checksumSHA1": "LGk0N301rp2uQSOmy7kBQEksues=",
+			"path": "github.com/ksonnet/ksonnet-lib/ksonnet-gen/ksonnet",
+			"revision": "fcf4c31408afc7b7469a1722c4cc06ccc315a722",
+			"revisionTime": "2017-08-16T22:14:57Z"
+		},
+		{
+			"checksumSHA1": "BiiHRiYpSOb+vHiP6h/A9lRotEY=",
+			"path": "github.com/ksonnet/ksonnet-lib/ksonnet-gen/kubespec",
+			"revision": "fcf4c31408afc7b7469a1722c4cc06ccc315a722",
+			"revisionTime": "2017-08-16T22:14:57Z"
+		},
+		{
+			"checksumSHA1": "jpBOSqq1T9UyiANimAKtzyTt3qo=",
+			"path": "github.com/ksonnet/ksonnet-lib/ksonnet-gen/kubeversion",
+			"revision": "fcf4c31408afc7b7469a1722c4cc06ccc315a722",
+			"revisionTime": "2017-08-16T22:14:57Z"
+		},
 		{
 			"checksumSHA1": "T8soMJArSZrYnhmdpAnq1bVxQ6Q=",
 			"path": "github.com/mailru/easyjson/buffer",