diff --git a/cmd/apply.go b/cmd/apply.go
index e8ad6e09734a83e45d67ada13f173adef185b299..e04c08fe0d4dae4727c553006c8468e2e565ee94 100644
--- a/cmd/apply.go
+++ b/cmd/apply.go
@@ -21,7 +21,6 @@ import (
 
 	"github.com/spf13/cobra"
 
-	"github.com/ksonnet/ksonnet/metadata"
 	"github.com/ksonnet/ksonnet/pkg/kubecfg"
 )
 
@@ -104,7 +103,6 @@ var applyCmd = &cobra.Command{
 		if err != nil {
 			return err
 		}
-		wd := metadata.AbsPath(cwd)
 
 		c.ClientPool, c.Discovery, err = restClientPool(cmd, &env)
 		if err != nil {
@@ -120,14 +118,14 @@ var applyCmd = &cobra.Command{
 			cmd:        cmd,
 			env:        env,
 			components: componentNames,
-			cwd:        wd,
+			cwd:        cwd,
 		})
 		objs, err := te.Expand()
 		if err != nil {
 			return err
 		}
 
-		return c.Run(objs, wd)
+		return c.Run(objs, cwd)
 	},
 	Long: `
 The ` + "`apply`" + `command uses local manifest(s) to update (and optionally create)
diff --git a/cmd/delete.go b/cmd/delete.go
index 9e39e57024da913e3025e5810666c60e93c58293..1f91e4314fe225c8cf9337330ac7a9828e6e363f 100644
--- a/cmd/delete.go
+++ b/cmd/delete.go
@@ -21,7 +21,6 @@ import (
 
 	"github.com/spf13/cobra"
 
-	"github.com/ksonnet/ksonnet/metadata"
 	"github.com/ksonnet/ksonnet/pkg/kubecfg"
 )
 
@@ -66,7 +65,6 @@ var deleteCmd = &cobra.Command{
 		if err != nil {
 			return err
 		}
-		wd := metadata.AbsPath(cwd)
 
 		c.ClientPool, c.Discovery, err = restClientPool(cmd, &env)
 		if err != nil {
@@ -82,7 +80,7 @@ var deleteCmd = &cobra.Command{
 			cmd:        cmd,
 			env:        env,
 			components: componentNames,
-			cwd:        wd,
+			cwd:        cwd,
 		})
 		objs, err := te.Expand()
 		if err != nil {
diff --git a/cmd/diff.go b/cmd/diff.go
index 4cb362ac99359ff3b4035a2e67fd3c4a401d977f..13bf1b8128c4bd27faac1c9f44f953eb71bb8e7b 100644
--- a/cmd/diff.go
+++ b/cmd/diff.go
@@ -61,7 +61,6 @@ var diffCmd = &cobra.Command{
 		if err != nil {
 			return err
 		}
-		wd := metadata.AbsPath(cwd)
 
 		componentNames, err := flags.GetStringArray(flagComponent)
 		if err != nil {
@@ -83,7 +82,7 @@ var diffCmd = &cobra.Command{
 			return err
 		}
 
-		c, err := initDiffCmd(appFs, cmd, wd, env1, env2, componentNames, diffStrategy)
+		c, err := initDiffCmd(appFs, cmd, cwd, env1, env2, componentNames, diffStrategy)
 		if err != nil {
 			return err
 		}
@@ -141,7 +140,7 @@ ks diff dev -c redis
 `,
 }
 
-func initDiffCmd(fs afero.Fs, cmd *cobra.Command, wd metadata.AbsPath, envFq1, envFq2 *string, files []string, diffStrategy string) (kubecfg.DiffCmd, error) {
+func initDiffCmd(fs afero.Fs, cmd *cobra.Command, wd string, envFq1, envFq2 *string, files []string, diffStrategy string) (kubecfg.DiffCmd, error) {
 	const (
 		remote = "remote"
 		local  = "local"
@@ -186,7 +185,7 @@ func initDiffCmd(fs afero.Fs, cmd *cobra.Command, wd metadata.AbsPath, envFq1, e
 }
 
 // initDiffSingleEnv sets up configurations for diffing using one environment
-func initDiffSingleEnv(fs afero.Fs, env, diffStrategy string, files []string, cmd *cobra.Command, wd metadata.AbsPath) (kubecfg.DiffCmd, error) {
+func initDiffSingleEnv(fs afero.Fs, env, diffStrategy string, files []string, cmd *cobra.Command, wd string) (kubecfg.DiffCmd, error) {
 	c := kubecfg.DiffRemoteCmd{}
 	c.DiffStrategy = diffStrategy
 	c.Client = &kubecfg.Client{}
@@ -322,8 +321,11 @@ func expandEnvObjs(fs afero.Fs, cmd *cobra.Command, env string, manager metadata
 		return nil, err
 	}
 
-	libPath, vendorPath := manager.LibPaths()
-	metadataPath, mainPath, paramsPath := manager.EnvPaths(env)
+	_, vendorPath := manager.LibPaths()
+	libPath, mainPath, paramsPath, err := manager.EnvPaths(env)
+	if err != nil {
+		return nil, err
+	}
 	componentPaths, err := manager.ComponentPaths()
 	if err != nil {
 		return nil, err
@@ -339,7 +341,7 @@ func expandEnvObjs(fs afero.Fs, cmd *cobra.Command, env string, manager metadata
 		return nil, err
 	}
 
-	expander.FlagJpath = append([]string{string(libPath), string(vendorPath), string(metadataPath)}, expander.FlagJpath...)
+	expander.FlagJpath = append([]string{string(vendorPath), string(libPath)}, expander.FlagJpath...)
 	expander.ExtCodes = append([]string{baseObj, params, envSpec}, expander.ExtCodes...)
 
 	envFiles := []string{string(mainPath)}
diff --git a/cmd/env.go b/cmd/env.go
index 638b7edb8a41ea780681a141416b70d5f834a53c..ca74f4986315c9f57b855cbe44e6641974705e77 100644
--- a/cmd/env.go
+++ b/cmd/env.go
@@ -125,9 +125,8 @@ var envAddCmd = &cobra.Command{
 		if err != nil {
 			return err
 		}
-		appRoot := metadata.AbsPath(appDir)
 
-		manager, err := metadata.Find(appRoot)
+		manager, err := metadata.Find(appDir)
 		if err != nil {
 			return err
 		}
@@ -209,9 +208,8 @@ var envRmCmd = &cobra.Command{
 		if err != nil {
 			return err
 		}
-		appRoot := metadata.AbsPath(appDir)
 
-		manager, err := metadata.Find(appRoot)
+		manager, err := metadata.Find(appDir)
 		if err != nil {
 			return err
 		}
@@ -258,9 +256,8 @@ var envListCmd = &cobra.Command{
 		if err != nil {
 			return err
 		}
-		appRoot := metadata.AbsPath(appDir)
 
-		manager, err := metadata.Find(appRoot)
+		manager, err := metadata.Find(appDir)
 		if err != nil {
 			return err
 		}
@@ -301,9 +298,8 @@ var envSetCmd = &cobra.Command{
 		if err != nil {
 			return err
 		}
-		appRoot := metadata.AbsPath(appDir)
 
-		manager, err := metadata.Find(appRoot)
+		manager, err := metadata.Find(appDir)
 		if err != nil {
 			return err
 		}
diff --git a/cmd/init.go b/cmd/init.go
index 2b44b57ca2ba652d44aad865d5ba29b9b990e113..b8361112e8ae4f5df52b37754b01b78e34a444ba 100644
--- a/cmd/init.go
+++ b/cmd/init.go
@@ -21,7 +21,6 @@ import (
 	"os"
 	"path/filepath"
 
-	"github.com/ksonnet/ksonnet/metadata"
 	"github.com/ksonnet/ksonnet/pkg/kubecfg"
 	log "github.com/sirupsen/logrus"
 	"github.com/spf13/cobra"
@@ -62,13 +61,11 @@ var initCmd = &cobra.Command{
 			return err
 		}
 
-		path, err := genKsRoot(appName, wd, initDir)
+		appRoot, err := genKsRoot(appName, wd, initDir)
 		if err != nil {
 			return err
 		}
 
-		appRoot := metadata.AbsPath(path)
-
 		specFlag, err := flags.GetString(flagAPISpec)
 		if err != nil {
 			return err
@@ -80,7 +77,7 @@ var initCmd = &cobra.Command{
 		}
 
 		log.Infof("Creating a new app '%s' at path '%s'", appName, appRoot)
-		c, err := kubecfg.NewInitCmd(appName, appRoot, specFlag, &server, &namespace)
+		c, err := kubecfg.NewInitCmd(appName, appRoot, &specFlag, &server, &namespace)
 		if err != nil {
 			return err
 		}
diff --git a/cmd/pkg.go b/cmd/pkg.go
index 3a7baca813b935d8930105780f919a0263d5cfcc..68728cde2d42dd456c29ee3392f071f9660eb392 100644
--- a/cmd/pkg.go
+++ b/cmd/pkg.go
@@ -22,7 +22,7 @@ import (
 
 	"github.com/ksonnet/ksonnet/metadata"
 	"github.com/ksonnet/ksonnet/metadata/parts"
-	"github.com/ksonnet/ksonnet/utils"
+	str "github.com/ksonnet/ksonnet/strings"
 	"github.com/spf13/cobra"
 )
 
@@ -101,9 +101,8 @@ var pkgInstallCmd = &cobra.Command{
 		if err != nil {
 			return err
 		}
-		wd := metadata.AbsPath(cwd)
 
-		manager, err := metadata.Find(wd)
+		manager, err := metadata.Find(cwd)
 		if err != nil {
 			return err
 		}
@@ -165,9 +164,8 @@ var pkgDescribeCmd = &cobra.Command{
 		if err != nil {
 			return err
 		}
-		wd := metadata.AbsPath(cwd)
 
-		manager, err := metadata.Find(wd)
+		manager, err := metadata.Find(cwd)
 		if err != nil {
 			return err
 		}
@@ -238,9 +236,8 @@ var pkgListCmd = &cobra.Command{
 		if err != nil {
 			return err
 		}
-		wd := metadata.AbsPath(cwd)
 
-		manager, err := metadata.Find(wd)
+		manager, err := metadata.Find(cwd)
 		if err != nil {
 			return err
 		}
@@ -273,7 +270,7 @@ var pkgListCmd = &cobra.Command{
 			}
 		}
 
-		formatted, err := utils.PadRows(rows)
+		formatted, err := str.PadRows(rows)
 		if err != nil {
 			return err
 		}
diff --git a/cmd/prototype.go b/cmd/prototype.go
index 5df4520bb1d4e111d750fc0a525b5e8463c8656c..821c589286cd2bbebe96102ede1d73616eff60f5 100644
--- a/cmd/prototype.go
+++ b/cmd/prototype.go
@@ -26,7 +26,7 @@ import (
 	"github.com/ksonnet/ksonnet/prototype"
 	"github.com/ksonnet/ksonnet/prototype/snippet"
 	"github.com/ksonnet/ksonnet/prototype/snippet/jsonnet"
-	"github.com/ksonnet/ksonnet/utils"
+	str "github.com/ksonnet/ksonnet/strings"
 	"github.com/spf13/cobra"
 )
 
@@ -87,9 +87,8 @@ var prototypeListCmd = &cobra.Command{
 		if err != nil {
 			return err
 		}
-		wd := metadata.AbsPath(cwd)
 
-		manager, err := metadata.Find(wd)
+		manager, err := metadata.Find(cwd)
 		if err != nil {
 			return err
 		}
@@ -143,10 +142,9 @@ var prototypeDescribeCmd = &cobra.Command{
 		if err != nil {
 			return err
 		}
-		wd := metadata.AbsPath(cwd)
 
 		extProtos := prototype.SpecificationSchemas{}
-		manager, err := metadata.Find(wd)
+		manager, err := metadata.Find(cwd)
 		if err == nil {
 			extProtos, err = manager.GetAllPrototypes()
 			if err != nil {
@@ -255,10 +253,9 @@ var prototypePreviewCmd = &cobra.Command{
 		if err != nil {
 			return err
 		}
-		wd := metadata.AbsPath(cwd)
 
 		extProtos := prototype.SpecificationSchemas{}
-		manager, err := metadata.Find(wd)
+		manager, err := metadata.Find(cwd)
 		if err == nil {
 			extProtos, err = manager.GetAllPrototypes()
 			if err != nil {
@@ -356,7 +353,7 @@ var prototypeUseCmd = &cobra.Command{
 		if err != nil {
 			return err
 		}
-		manager, err := metadata.Find(metadata.AbsPath(cwd))
+		manager, err := metadata.Find(cwd)
 		if err != nil {
 			return fmt.Errorf("Command can only be run in a ksonnet application directory:\n\n%v", err)
 		}
@@ -500,7 +497,7 @@ func expandPrototype(proto *prototype.SpecificationSchema, templateType prototyp
 	}
 	if templateType == prototype.Jsonnet {
 		componentsText := "components." + componentName
-		if !utils.IsASCIIIdentifier(componentName) {
+		if !str.IsASCIIIdentifier(componentName) {
 			componentsText = fmt.Sprintf(`components["%s"]`, componentName)
 		}
 		template = append([]string{
diff --git a/cmd/registry.go b/cmd/registry.go
index 341032babcf94eaf40540d4df25f1fc45aac0703..7d385272843871ad61d37080428d217b85b508a7 100644
--- a/cmd/registry.go
+++ b/cmd/registry.go
@@ -7,7 +7,7 @@ import (
 
 	"github.com/ksonnet/ksonnet/metadata"
 	"github.com/ksonnet/ksonnet/pkg/kubecfg"
-	"github.com/ksonnet/ksonnet/utils"
+	str "github.com/ksonnet/ksonnet/strings"
 	"github.com/spf13/cobra"
 )
 
@@ -76,9 +76,8 @@ var registryListCmd = &cobra.Command{
 		if err != nil {
 			return err
 		}
-		wd := metadata.AbsPath(cwd)
 
-		manager, err := metadata.Find(wd)
+		manager, err := metadata.Find(cwd)
 		if err != nil {
 			return err
 		}
@@ -100,7 +99,7 @@ var registryListCmd = &cobra.Command{
 			rows = append(rows, []string{name, regRef.Protocol, regRef.URI})
 		}
 
-		formatted, err := utils.PadRows(rows)
+		formatted, err := str.PadRows(rows)
 		if err != nil {
 			return err
 		}
@@ -136,9 +135,8 @@ var registryDescribeCmd = &cobra.Command{
 		if err != nil {
 			return err
 		}
-		wd := metadata.AbsPath(cwd)
 
-		manager, err := metadata.Find(wd)
+		manager, err := metadata.Find(cwd)
 		if err != nil {
 			return err
 		}
diff --git a/cmd/root.go b/cmd/root.go
index bb5668f789fad5ddba58d172bef04e427bcceadf..9dd9f72245b08a3f736b2ba90bcdf20dcb6b0a60 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -39,6 +39,7 @@ import (
 	"k8s.io/client-go/tools/clientcmd"
 
 	"github.com/ksonnet/ksonnet/metadata"
+	str "github.com/ksonnet/ksonnet/strings"
 	"github.com/ksonnet/ksonnet/template"
 	"github.com/ksonnet/ksonnet/utils"
 
@@ -332,9 +333,8 @@ func overrideCluster(envName string, clientConfig clientcmd.ClientConfig, overri
 	if err != nil {
 		return err
 	}
-	wd := metadata.AbsPath(cwd)
 
-	metadataManager, err := metadata.Find(wd)
+	metadataManager, err := metadata.Find(cwd)
 	if err != nil {
 		return err
 	}
@@ -346,7 +346,7 @@ func overrideCluster(envName string, clientConfig clientcmd.ClientConfig, overri
 
 	var servers = make(map[string]string)
 	for name, cluster := range rawConfig.Clusters {
-		server, err := utils.NormalizeURL(cluster.Server)
+		server, err := str.NormalizeURL(cluster.Server)
 		if err != nil {
 			return err
 		}
@@ -366,7 +366,7 @@ func overrideCluster(envName string, clientConfig clientcmd.ClientConfig, overri
 	}
 
 	// TODO support multi-cluster deployment.
-	server, err := utils.NormalizeURL(env.Destinations[0].Server)
+	server, err := str.NormalizeURL(env.Destinations[0].Server)
 	if err != nil {
 		return err
 	}
@@ -392,7 +392,7 @@ type cmdObjExpanderConfig struct {
 	cmd        *cobra.Command
 	env        string
 	components []string
-	cwd        metadata.AbsPath
+	cwd        string
 }
 
 // cmdObjExpander finds and expands templates for the family of commands of
@@ -432,10 +432,13 @@ func (te *cmdObjExpander) Expand() ([]*unstructured.Unstructured, error) {
 		return nil, errors.Wrap(err, "find metadata")
 	}
 
-	libPath, vendorPath := manager.LibPaths()
-	metadataPath, mainPath, paramsPath := manager.EnvPaths(te.config.env)
+	_, vendorPath := manager.LibPaths()
+	libPath, mainPath, paramsPath, err := manager.EnvPaths(te.config.env)
+	if err != nil {
+		return nil, err
+	}
 
-	expander.FlagJpath = append([]string{string(libPath), string(vendorPath), string(metadataPath)}, expander.FlagJpath...)
+	expander.FlagJpath = append([]string{string(vendorPath), string(libPath)}, expander.FlagJpath...)
 
 	componentPaths, err := manager.ComponentPaths()
 	if err != nil {
@@ -524,7 +527,7 @@ func constructBaseObj(componentPaths, componentNames []string) (string, error) {
 
 		// Emit object field. Sanitize the name to guarantee we generate valid
 		// Jsonnet.
-		componentName = utils.QuoteNonASCII(componentName)
+		componentName = str.QuoteNonASCII(componentName)
 		fmt.Fprintf(&obj, "  %s: %s,\n", componentName, importExpr)
 	}
 
diff --git a/cmd/show.go b/cmd/show.go
index 42196e59af845ffa95c20615ac7643778be10113..1b98572cae9b992201b203a165373f8c11d7211e 100644
--- a/cmd/show.go
+++ b/cmd/show.go
@@ -21,7 +21,6 @@ import (
 
 	"github.com/spf13/cobra"
 
-	"github.com/ksonnet/ksonnet/metadata"
 	"github.com/ksonnet/ksonnet/pkg/kubecfg"
 )
 
@@ -97,13 +96,12 @@ ks show dev -c redis -c nginx-server
 		if err != nil {
 			return err
 		}
-		wd := metadata.AbsPath(cwd)
 
 		te := newCmdObjExpander(cmdObjExpanderConfig{
 			cmd:        cmd,
 			env:        env,
 			components: componentNames,
-			cwd:        wd,
+			cwd:        cwd,
 		})
 		objs, err := te.Expand()
 		if err != nil {
diff --git a/cmd/validate.go b/cmd/validate.go
index 64afd822ba3238ac4785575db2b32ff6d9ad1619..f84b4cb6bd892ddf3b06a6408d42bfc1c3d2b64e 100644
--- a/cmd/validate.go
+++ b/cmd/validate.go
@@ -21,7 +21,6 @@ import (
 
 	"github.com/spf13/cobra"
 
-	"github.com/ksonnet/ksonnet/metadata"
 	"github.com/ksonnet/ksonnet/pkg/kubecfg"
 )
 
@@ -54,7 +53,6 @@ var validateCmd = &cobra.Command{
 		if err != nil {
 			return err
 		}
-		wd := metadata.AbsPath(cwd)
 
 		componentNames, err := flags.GetStringArray(flagComponent)
 		if err != nil {
@@ -70,7 +68,7 @@ var validateCmd = &cobra.Command{
 			cmd:        cmd,
 			env:        env,
 			components: componentNames,
-			cwd:        wd,
+			cwd:        cwd,
 		})
 		objs, err := te.Expand()
 		if err != nil {
diff --git a/integration/fixtures/sampleapp/environments/default/.metadata/k.libsonnet b/integration/fixtures/sampleapp/lib/v1.7.0/k.libsonnet
similarity index 100%
rename from integration/fixtures/sampleapp/environments/default/.metadata/k.libsonnet
rename to integration/fixtures/sampleapp/lib/v1.7.0/k.libsonnet
diff --git a/integration/fixtures/sampleapp/environments/default/.metadata/k8s.libsonnet b/integration/fixtures/sampleapp/lib/v1.7.0/k8s.libsonnet
similarity index 100%
rename from integration/fixtures/sampleapp/environments/default/.metadata/k8s.libsonnet
rename to integration/fixtures/sampleapp/lib/v1.7.0/k8s.libsonnet
diff --git a/integration/fixtures/sampleapp/environments/default/.metadata/swagger.json b/integration/fixtures/sampleapp/lib/v1.7.0/swagger.json
similarity index 100%
rename from integration/fixtures/sampleapp/environments/default/.metadata/swagger.json
rename to integration/fixtures/sampleapp/lib/v1.7.0/swagger.json
diff --git a/integration/integration_suite_test.go b/integration/integration_suite_test.go
index 76998eadb0f6c94f39cf4bb6a4fea948d5e39d42..d848136eb1d326a90cd5245f8f7785c65033056b 100644
--- a/integration/integration_suite_test.go
+++ b/integration/integration_suite_test.go
@@ -100,6 +100,7 @@ func runKsonnetWith(flags []string, host, ns string) error {
 						Server:    host,
 					},
 				},
+				KubernetesVersion: "v1.7.0",
 			},
 		},
 	}
diff --git a/metadata/clusterspec.go b/metadata/clusterspec.go
deleted file mode 100644
index 57412a7eb6c1fa246feebf28438044c2887a7635..0000000000000000000000000000000000000000
--- a/metadata/clusterspec.go
+++ /dev/null
@@ -1,88 +0,0 @@
-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) OpenAPI() ([]byte, error) {
-	return afero.ReadFile(cs.fs, string(cs.specPath))
-}
-
-func (cs *clusterSpecFile) Resource() string {
-	return string(cs.specPath)
-}
-
-type clusterSpecLive struct {
-	apiServerURL string
-}
-
-func (cs *clusterSpecLive) OpenAPI() ([]byte, error) {
-	return nil, fmt.Errorf("Initializing from OpenAPI spec in live cluster is not implemented")
-}
-
-func (cs *clusterSpecLive) Resource() string {
-	return string(cs.apiServerURL)
-}
-
-type clusterSpecVersion struct {
-	k8sVersion string
-}
-
-func (cs *clusterSpecVersion) OpenAPI() ([]byte, error) {
-	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 {
-	return string(cs.k8sVersion)
-}
diff --git a/metadata/component.go b/metadata/component.go
index 709ee716b5d3331a0db025594a5e34a5e61b0644..b5ff387bf4cb4361152104e5a4ea733f833a0340 100644
--- a/metadata/component.go
+++ b/metadata/component.go
@@ -23,13 +23,14 @@ import (
 
 	param "github.com/ksonnet/ksonnet/metadata/params"
 	"github.com/ksonnet/ksonnet/prototype"
+	str "github.com/ksonnet/ksonnet/strings"
 	log "github.com/sirupsen/logrus"
 	"github.com/spf13/afero"
 )
 
-func (m *manager) ComponentPaths() (AbsPaths, error) {
-	paths := AbsPaths{}
-	err := afero.Walk(m.appFS, string(m.componentsPath), func(p string, info os.FileInfo, err error) error {
+func (m *manager) ComponentPaths() ([]string, error) {
+	paths := []string{}
+	err := afero.Walk(m.appFS, m.componentsPath, func(p string, info os.FileInfo, err error) error {
 		if err != nil {
 			return err
 		}
@@ -67,7 +68,7 @@ func (m *manager) CreateComponent(name string, text string, params param.Params,
 		return fmt.Errorf("Component name '%s' is not valid; must not contain punctuation, spaces, or begin or end with a slash", name)
 	}
 
-	componentPath := string(appendToAbsPath(m.componentsPath, name))
+	componentPath := str.AppendToPath(m.componentsPath, name)
 	switch templateType {
 	case prototype.YAML:
 		componentPath = componentPath + ".yaml"
@@ -105,7 +106,7 @@ func (m *manager) DeleteComponent(name string) error {
 	}
 
 	// Build the new component/params.libsonnet file.
-	componentParamsFile, err := afero.ReadFile(m.appFS, string(m.componentParamsPath))
+	componentParamsFile, err := afero.ReadFile(m.appFS, m.componentParamsPath)
 	if err != nil {
 		return err
 	}
@@ -122,8 +123,8 @@ func (m *manager) DeleteComponent(name string) error {
 		return err
 	}
 	for _, env := range envs {
-		path := appendToAbsPath(m.environmentsPath, env.Name, paramsFileName)
-		envParamsFile, err := afero.ReadFile(m.appFS, string(path))
+		path := str.AppendToPath(m.environmentsPath, env.Name, paramsFileName)
+		envParamsFile, err := afero.ReadFile(m.appFS, path)
 		if err != nil {
 			return err
 		}
@@ -141,16 +142,16 @@ func (m *manager) DeleteComponent(name string) error {
 
 	// Remove the references in component/params.libsonnet.
 	log.Debugf("... deleting references in %s", m.componentParamsPath)
-	err = afero.WriteFile(m.appFS, string(m.componentParamsPath), []byte(componentJsonnet), defaultFilePermissions)
+	err = afero.WriteFile(m.appFS, m.componentParamsPath, []byte(componentJsonnet), defaultFilePermissions)
 	if err != nil {
 		return err
 	}
 	// Remove the component references in each environment's
 	// environment/<env>/params.libsonnet.
 	for _, env := range envs {
-		path := appendToAbsPath(m.environmentsPath, env.Name, paramsFileName)
+		path := str.AppendToPath(m.environmentsPath, env.Name, paramsFileName)
 		log.Debugf("... deleting references in %s", path)
-		err = afero.WriteFile(m.appFS, string(path), []byte(envJsonnets[env.Name]), defaultFilePermissions)
+		err = afero.WriteFile(m.appFS, path, []byte(envJsonnets[env.Name]), defaultFilePermissions)
 		if err != nil {
 			return err
 		}
@@ -172,7 +173,7 @@ func (m *manager) DeleteComponent(name string) error {
 }
 
 func (m *manager) GetComponentParams(component string) (param.Params, error) {
-	text, err := afero.ReadFile(m.appFS, string(m.componentParamsPath))
+	text, err := afero.ReadFile(m.appFS, m.componentParamsPath)
 	if err != nil {
 		return nil, err
 	}
@@ -181,7 +182,7 @@ func (m *manager) GetComponentParams(component string) (param.Params, error) {
 }
 
 func (m *manager) GetAllComponentParams() (map[string]param.Params, error) {
-	text, err := afero.ReadFile(m.appFS, string(m.componentParamsPath))
+	text, err := afero.ReadFile(m.appFS, m.componentParamsPath)
 	if err != nil {
 		return nil, err
 	}
@@ -190,7 +191,7 @@ func (m *manager) GetAllComponentParams() (map[string]param.Params, error) {
 }
 
 func (m *manager) SetComponentParams(component string, params param.Params) error {
-	text, err := afero.ReadFile(m.appFS, string(m.componentParamsPath))
+	text, err := afero.ReadFile(m.appFS, m.componentParamsPath)
 	if err != nil {
 		return err
 	}
@@ -200,7 +201,7 @@ func (m *manager) SetComponentParams(component string, params param.Params) erro
 		return err
 	}
 
-	return afero.WriteFile(m.appFS, string(m.componentParamsPath), []byte(jsonnet), defaultFilePermissions)
+	return afero.WriteFile(m.appFS, m.componentParamsPath, []byte(jsonnet), defaultFilePermissions)
 }
 
 func (m *manager) findComponentPath(name string) (string, error) {
@@ -232,7 +233,7 @@ func (m *manager) findComponentPath(name string) (string, error) {
 }
 
 func (m *manager) writeComponentParams(componentName string, params param.Params) error {
-	text, err := afero.ReadFile(m.appFS, string(m.componentParamsPath))
+	text, err := afero.ReadFile(m.appFS, m.componentParamsPath)
 	if err != nil {
 		return err
 	}
@@ -242,7 +243,7 @@ func (m *manager) writeComponentParams(componentName string, params param.Params
 		return err
 	}
 
-	return afero.WriteFile(m.appFS, string(m.componentParamsPath), []byte(appended), defaultFilePermissions)
+	return afero.WriteFile(m.appFS, m.componentParamsPath, []byte(appended), defaultFilePermissions)
 }
 
 func genComponentParamsContent() []byte {
diff --git a/metadata/component_test.go b/metadata/component_test.go
index d5021bcd819ca377f810fcdab8d4df6396ca05ca..4027284e6c61e037b0975b74c772f2fb01b1a1d9 100644
--- a/metadata/component_test.go
+++ b/metadata/component_test.go
@@ -21,6 +21,8 @@ import (
 	"sort"
 	"strings"
 	"testing"
+
+	str "github.com/ksonnet/ksonnet/strings"
 )
 
 const (
@@ -31,42 +33,39 @@ const (
 )
 
 func populateComponentPaths(t *testing.T) *manager {
-	spec, err := parseClusterSpec(fmt.Sprintf("file:%s", blankSwagger), testFS)
-	if err != nil {
-		t.Fatalf("Failed to parse cluster spec: %v", err)
-	}
+	specFlag := fmt.Sprintf("file:%s", blankSwagger)
 
-	appPath := AbsPath(componentsPath)
+	appPath := componentsPath
 	reg := newMockRegistryManager("incubator")
-	m, err := initManager("componentPaths", appPath, spec, &mockAPIServer, &mockNamespace, reg, testFS)
+	m, err := initManager("componentPaths", appPath, &specFlag, &mockAPIServer, &mockNamespace, reg, testFS)
 	if err != nil {
 		t.Fatalf("Failed to init cluster spec: %v", err)
 	}
 
 	// Create empty app file.
-	components := appendToAbsPath(appPath, componentsDir)
-	appFile1 := appendToAbsPath(components, componentFile1)
-	f1, err := testFS.OpenFile(string(appFile1), os.O_RDONLY|os.O_CREATE, 0777)
+	components := str.AppendToPath(appPath, componentsDir)
+	appFile1 := str.AppendToPath(components, componentFile1)
+	f1, err := testFS.OpenFile(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, componentSubdir)
-	err = testFS.MkdirAll(string(appSubdir), os.ModePerm)
+	appSubdir := str.AppendToPath(components, componentSubdir)
+	err = testFS.MkdirAll(appSubdir, os.ModePerm)
 	if err != nil {
 		t.Fatalf("Failed to create directory '%s'\n%v", appSubdir, err)
 	}
-	appFile2 := appendToAbsPath(appSubdir, componentFile2)
-	f2, err := testFS.OpenFile(string(appFile2), os.O_RDONLY|os.O_CREATE, 0777)
+	appFile2 := str.AppendToPath(appSubdir, componentFile2)
+	f2, err := testFS.OpenFile(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"))
+	unlistedDir := str.AppendToPath(components, "doNotListMe")
 	err = testFS.MkdirAll(unlistedDir, os.ModePerm)
 	if err != nil {
 		t.Fatalf("Failed to create directory '%s'\n%v", unlistedDir, err)
diff --git a/metadata/environment.go b/metadata/environment.go
index e5474b32dafe563bf3215b472e855d477248244f..73900d805600d38b77aa14bf9732030f0ec41b4c 100644
--- a/metadata/environment.go
+++ b/metadata/environment.go
@@ -22,56 +22,42 @@ import (
 	"path/filepath"
 
 	"github.com/ksonnet/ksonnet/metadata/app"
+	"github.com/ksonnet/ksonnet/metadata/lib"
+	str "github.com/ksonnet/ksonnet/strings"
 
 	log "github.com/sirupsen/logrus"
 	"github.com/spf13/afero"
 
-	"github.com/ksonnet/ksonnet/generator"
 	param "github.com/ksonnet/ksonnet/metadata/params"
 )
 
 const (
-	defaultEnvName  = "default"
-	metadataDirName = ".metadata"
-
-	// hidden metadata files
-	schemaFilename        = "swagger.json"
-	extensionsLibFilename = "k.libsonnet"
-	k8sLibFilename        = "k8s.libsonnet"
+	defaultEnvName = "default"
 
 	// primary environment files
 	envFileName    = "main.jsonnet"
 	paramsFileName = "params.libsonnet"
-	specFilename   = "spec.json"
 )
 
 var envPaths = []string{
-	// metadata Dir.wh
-	metadataDirName,
 	// environment base override file
 	envFileName,
 	// params file
 	paramsFileName,
-	// spec file
-	specFilename,
 }
 
-func (m *manager) CreateEnvironment(name, server, namespace string, spec ClusterSpec) error {
-	b, err := spec.OpenAPI()
+func (m *manager) CreateEnvironment(name, server, namespace, k8sSpecFlag string) error {
+	// generate the lib data for this kubernetes version
+	libManager, err := lib.NewManagerWithSpec(k8sSpecFlag, m.appFS, m.libPath)
 	if err != nil {
 		return err
 	}
 
-	kl, err := generator.Ksonnet(b)
-	if err != nil {
-		log.Debugf("Failed to write '%s'", specFilename)
+	if err := libManager.GenerateLibData(); err != nil {
 		return err
 	}
 
-	return m.createEnvironment(name, server, namespace, kl.K, kl.K8s, kl.Swagger)
-}
-
-func (m *manager) createEnvironment(name, server, namespace string, extensionsLibData, k8sLibData, specData []byte) error {
+	// add the environment to the app spec
 	appSpec, err := m.AppSpec()
 	if err != nil {
 		return err
@@ -92,55 +78,32 @@ func (m *manager) createEnvironment(name, server, namespace string, extensionsLi
 
 	log.Infof("Creating environment '%s' with namespace '%s', pointing at server at address '%s'", name, namespace, server)
 
-	envPath := appendToAbsPath(m.environmentsPath, name)
-	err = m.appFS.MkdirAll(string(envPath), defaultFolderPermissions)
+	envPath := str.AppendToPath(m.environmentsPath, name)
+	err = m.appFS.MkdirAll(envPath, defaultFolderPermissions)
 	if err != nil {
 		return err
 	}
 
-	metadataPath := appendToAbsPath(envPath, metadataDirName)
-	err = m.appFS.MkdirAll(string(metadataPath), defaultFolderPermissions)
-	if err != nil {
-		return err
-	}
-
-	log.Infof("Generating environment metadata at path '%s'", envPath)
-
 	metadata := []struct {
-		path AbsPath
+		path string
 		data []byte
 	}{
-		{
-			// schema file
-			appendToAbsPath(metadataPath, schemaFilename),
-			specData,
-		},
-		{
-			// k8s file
-			appendToAbsPath(metadataPath, k8sLibFilename),
-			k8sLibData,
-		},
-		{
-			// extensions file
-			appendToAbsPath(metadataPath, extensionsLibFilename),
-			extensionsLibData,
-		},
 		{
 			// environment base override file
-			appendToAbsPath(envPath, envFileName),
+			str.AppendToPath(envPath, envFileName),
 			m.generateOverrideData(),
 		},
 		{
 			// params file
-			appendToAbsPath(envPath, paramsFileName),
+			str.AppendToPath(envPath, paramsFileName),
 			m.generateParamsData(),
 		},
 	}
 
 	for _, a := range metadata {
-		fileName := path.Base(string(a.path))
+		fileName := path.Base(a.path)
 		log.Debugf("Generating '%s', length: %d", fileName, len(a.data))
-		if err = afero.WriteFile(m.appFS, string(a.path), a.data, defaultFilePermissions); err != nil {
+		if err = afero.WriteFile(m.appFS, a.path, a.data, defaultFilePermissions); err != nil {
 			log.Debugf("Failed to write '%s'", fileName)
 			return err
 		}
@@ -156,7 +119,7 @@ func (m *manager) createEnvironment(name, server, namespace string, extensionsLi
 				Namespace: namespace,
 			},
 		},
-		// TODO specify k8s version once metadata is moved.
+		KubernetesVersion: libManager.K8sVersion,
 	})
 
 	if err != nil {
@@ -177,12 +140,12 @@ func (m *manager) DeleteEnvironment(name string) error {
 		return fmt.Errorf("Environment '%s' does not exist", name)
 	}
 
-	envPath := appendToAbsPath(m.environmentsPath, env.Path)
+	envPath := str.AppendToPath(m.environmentsPath, env.Path)
 
 	log.Infof("Deleting environment '%s' with metadata at path '%s'", name, envPath)
 
 	// Remove the directory and all files within the environment path.
-	err = m.appFS.RemoveAll(string(envPath))
+	err = m.appFS.RemoveAll(envPath)
 	if err != nil {
 		log.Debugf("Failed to remove environment directory at path '%s'", envPath)
 		return err
@@ -282,9 +245,9 @@ func (m *manager) SetEnvironment(name, desiredName string) error {
 	// reflect the change.
 	//
 
-	pathOld := appendToAbsPath(m.environmentsPath, name)
-	pathNew := appendToAbsPath(m.environmentsPath, desiredName)
-	exists, err = afero.DirExists(m.appFS, string(pathNew))
+	pathOld := str.AppendToPath(m.environmentsPath, name)
+	pathNew := str.AppendToPath(m.environmentsPath, desiredName)
+	exists, err = afero.DirExists(m.appFS, pathNew)
 	if err != nil {
 		return err
 	}
@@ -294,26 +257,26 @@ func (m *manager) SetEnvironment(name, desiredName string) error {
 		// the check earlier. This is an intermediate directory.
 		// We need to move the file contents.
 		m.tryMvEnvDir(pathOld, pathNew)
-	} else if filepath.HasPrefix(string(pathNew), string(pathOld)) {
+	} else if filepath.HasPrefix(pathNew, pathOld) {
 		// the new directory is a child of the old directory --
 		// rename won't work.
-		err = m.appFS.MkdirAll(string(pathNew), defaultFolderPermissions)
+		err = m.appFS.MkdirAll(pathNew, defaultFolderPermissions)
 		if err != nil {
 			return err
 		}
 		m.tryMvEnvDir(pathOld, pathNew)
 	} else {
 		// Need to first create subdirectories that don't exist
-		intermediatePath := path.Dir(string(pathNew))
-		log.Debugf("Moving directory at path '%s' to '%s'", string(pathOld), string(pathNew))
+		intermediatePath := path.Dir(pathNew)
+		log.Debugf("Moving directory at path '%s' to '%s'", pathOld, pathNew)
 		err = m.appFS.MkdirAll(intermediatePath, defaultFolderPermissions)
 		if err != nil {
 			return err
 		}
 		// finally, move the directory
-		err = m.appFS.Rename(string(pathOld), string(pathNew))
+		err = m.appFS.Rename(pathOld, pathNew)
 		if err != nil {
-			log.Debugf("Failed to move path '%s' to '%s", string(pathOld), string(pathNew))
+			log.Debugf("Failed to move path '%s' to '%s", pathOld, pathNew)
 			return err
 		}
 	}
@@ -340,8 +303,8 @@ func (m *manager) GetEnvironmentParams(name string) (map[string]param.Params, er
 	}
 
 	// Get the environment specific params
-	envParamsPath := appendToAbsPath(m.environmentsPath, name, paramsFileName)
-	envParamsText, err := afero.ReadFile(m.appFS, string(envParamsPath))
+	envParamsPath := str.AppendToPath(m.environmentsPath, name, paramsFileName)
+	envParamsText, err := afero.ReadFile(m.appFS, envParamsPath)
 	if err != nil {
 		return nil, err
 	}
@@ -369,9 +332,9 @@ func (m *manager) SetEnvironmentParams(env, component string, params param.Param
 		return fmt.Errorf("Environment '%s' does not exist", env)
 	}
 
-	path := appendToAbsPath(m.environmentsPath, env, paramsFileName)
+	path := str.AppendToPath(m.environmentsPath, env, paramsFileName)
 
-	text, err := afero.ReadFile(m.appFS, string(path))
+	text, err := afero.ReadFile(m.appFS, path)
 	if err != nil {
 		return err
 	}
@@ -381,7 +344,7 @@ func (m *manager) SetEnvironmentParams(env, component string, params param.Param
 		return err
 	}
 
-	err = afero.WriteFile(m.appFS, string(path), []byte(appended), defaultFilePermissions)
+	err = afero.WriteFile(m.appFS, path, []byte(appended), defaultFilePermissions)
 	if err != nil {
 		return err
 	}
@@ -390,10 +353,35 @@ func (m *manager) SetEnvironmentParams(env, component string, params param.Param
 	return nil
 }
 
-func (m *manager) tryMvEnvDir(dirPathOld, dirPathNew AbsPath) error {
+func (m *manager) EnvPaths(env string) (libPath, mainPath, paramsPath string, err error) {
+	app, err := m.AppSpec()
+	if err != nil {
+		return
+	}
+
+	envSpec, ok := app.GetEnvironmentSpec(env)
+	if !ok {
+		err = fmt.Errorf("Environment '%s' does not exist", env)
+		return
+	}
+
+	libManager := lib.NewManager(envSpec.KubernetesVersion, m.appFS, m.libPath)
+
+	envPath := str.AppendToPath(m.environmentsPath, env)
+
+	// main.jsonnet file
+	mainPath = str.AppendToPath(envPath, envFileName)
+	// params.libsonnet file
+	paramsPath = str.AppendToPath(envPath, componentParamsFile)
+	// ksonnet-lib file directory
+	libPath, err = libManager.GetLibPath()
+	return
+}
+
+func (m *manager) tryMvEnvDir(dirPathOld, dirPathNew string) error {
 	// first ensure none of these paths exists in the new directory
 	for _, p := range envPaths {
-		path := string(appendToAbsPath(dirPathNew, p))
+		path := str.AppendToPath(dirPathNew, p)
 		if exists, err := afero.Exists(m.appFS, path); err != nil {
 			return err
 		} else if exists {
@@ -404,16 +392,16 @@ func (m *manager) tryMvEnvDir(dirPathOld, dirPathNew AbsPath) error {
 	// note: afero and go does not provide simple ways to move the
 	// contents. We'll have to rename them individually.
 	for _, p := range envPaths {
-		err := m.appFS.Rename(string(appendToAbsPath(dirPathOld, p)), string(appendToAbsPath(dirPathNew, p)))
+		err := m.appFS.Rename(str.AppendToPath(dirPathOld, p), str.AppendToPath(dirPathNew, p))
 		if err != nil {
 			return err
 		}
 	}
 	// clean up the old directory if it is empty
-	if empty, err := afero.IsEmpty(m.appFS, string(dirPathOld)); err != nil {
+	if empty, err := afero.IsEmpty(m.appFS, dirPathOld); err != nil {
 		return err
 	} else if empty {
-		return m.appFS.RemoveAll(string(dirPathOld))
+		return m.appFS.RemoveAll(dirPathOld)
 	}
 	return nil
 }
@@ -424,7 +412,7 @@ func (m *manager) cleanEmptyParentDirs(name string) error {
 	parentDir := name
 	for parentDir != "." {
 		parentDir = filepath.Dir(parentDir)
-		parentPath := string(appendToAbsPath(m.environmentsPath, parentDir))
+		parentPath := str.AppendToPath(m.environmentsPath, parentDir)
 
 		isEmpty, err := afero.IsEmpty(m.appFS, parentPath)
 		if err != nil {
@@ -449,7 +437,7 @@ func (m *manager) generateOverrideData() []byte {
 
 	var buf bytes.Buffer
 	buf.WriteString(fmt.Sprintf("local base = import \"%s\";\n", relBaseLibsonnetPath))
-	buf.WriteString(fmt.Sprintf("local k = import \"%s\";\n\n", extensionsLibFilename))
+	buf.WriteString(fmt.Sprintf("local k = import \"%s\";\n\n", lib.ExtensionsLibFilename))
 	buf.WriteString("base + {\n")
 	buf.WriteString("  // Insert user-specified overrides here. For example if a component is named \"nginx-deployment\", you might have something like:\n")
 	buf.WriteString("  //   \"nginx-deployment\"+: k.deployment.mixin.metadata.labels({foo: \"bar\"})\n")
diff --git a/metadata/environment_test.go b/metadata/environment_test.go
index beea033ae70cd13c2e2c4b712948cdc212416c94..017ef0ef3457b0fde8e80e5e087dad12d6a89947 100644
--- a/metadata/environment_test.go
+++ b/metadata/environment_test.go
@@ -22,6 +22,7 @@ import (
 	"testing"
 
 	"github.com/ksonnet/ksonnet/metadata/app"
+	str "github.com/ksonnet/ksonnet/strings"
 
 	param "github.com/ksonnet/ksonnet/metadata/params"
 	"github.com/spf13/afero"
@@ -44,38 +45,34 @@ func mockEnvironments(t *testing.T, appName string) *manager {
 }
 
 func mockEnvironmentsWith(t *testing.T, appName string, envNames []string) *manager {
-	spec, err := parseClusterSpec(fmt.Sprintf("file:%s", blankSwagger), testFS)
-	if err != nil {
-		t.Fatalf("Failed to parse cluster spec: %v", err)
-	}
+	specFlag := fmt.Sprintf("file:%s", blankSwagger)
 
-	appPath := AbsPath(appName)
 	reg := newMockRegistryManager("incubator")
-	m, err := initManager(appName, appPath, spec, &mockAPIServer, &mockNamespace, reg, testFS)
+	m, err := initManager(appName, appName, &specFlag, &mockAPIServer, &mockNamespace, reg, testFS)
 	if err != nil {
 		t.Fatalf("Failed to init cluster spec: %v", err)
 	}
 
 	for _, env := range envNames {
-		envPath := appendToAbsPath(m.environmentsPath, env)
-		testFS.Mkdir(string(envPath), defaultFolderPermissions)
-		testDirExists(t, string(envPath))
+		envPath := str.AppendToPath(m.environmentsPath, env)
+		testFS.Mkdir(envPath, defaultFolderPermissions)
+		testDirExists(t, envPath)
 
-		envFilePath := appendToAbsPath(envPath, envFileName)
+		envFilePath := str.AppendToPath(envPath, envFileName)
 		envFileData := m.generateOverrideData()
-		err = afero.WriteFile(testFS, string(envFilePath), envFileData, defaultFilePermissions)
+		err = afero.WriteFile(testFS, envFilePath, envFileData, defaultFilePermissions)
 		if err != nil {
 			t.Fatalf("Could not write file at path: %s", envFilePath)
 		}
-		testFileExists(t, string(envFilePath))
+		testFileExists(t, envFilePath)
 
-		paramsPath := appendToAbsPath(envPath, paramsFileName)
+		paramsPath := str.AppendToPath(envPath, paramsFileName)
 		paramsData := m.generateParamsData()
-		err = afero.WriteFile(testFS, string(paramsPath), paramsData, defaultFilePermissions)
+		err = afero.WriteFile(testFS, paramsPath, paramsData, defaultFilePermissions)
 		if err != nil {
 			t.Fatalf("Could not write file at path: %s", paramsPath)
 		}
-		testFileExists(t, string(paramsPath))
+		testFileExists(t, paramsPath)
 
 		appSpec, err := m.AppSpec()
 		if err != nil {
@@ -129,26 +126,26 @@ func TestDeleteEnvironment(t *testing.T) {
 	m := mockEnvironments(t, appName)
 
 	// Test that both directory and empty parent directory is deleted.
-	expectedPath := appendToAbsPath(m.environmentsPath, mockEnvName3)
+	expectedPath := str.AppendToPath(m.environmentsPath, mockEnvName3)
 	parentDir := strings.Split(mockEnvName3, "/")[0]
-	expectedParentPath := appendToAbsPath(m.environmentsPath, parentDir)
+	expectedParentPath := str.AppendToPath(m.environmentsPath, parentDir)
 	err := m.DeleteEnvironment(mockEnvName3)
 	if err != nil {
 		t.Fatalf("Expected %s to be deleted but got err:\n  %s", mockEnvName3, err)
 	}
-	testDirNotExists(t, string(expectedPath))
-	testDirNotExists(t, string(expectedParentPath))
+	testDirNotExists(t, expectedPath)
+	testDirNotExists(t, expectedParentPath)
 
 	// Test that only leaf directory is deleted if parent directory is shared
-	expectedPath = appendToAbsPath(m.environmentsPath, mockEnvName2)
+	expectedPath = str.AppendToPath(m.environmentsPath, mockEnvName2)
 	parentDir = strings.Split(mockEnvName2, "/")[0]
-	expectedParentPath = appendToAbsPath(m.environmentsPath, parentDir)
+	expectedParentPath = str.AppendToPath(m.environmentsPath, parentDir)
 	err = m.DeleteEnvironment(mockEnvName2)
 	if err != nil {
 		t.Fatalf("Expected %s to be deleted but got err:\n  %s", mockEnvName3, err)
 	}
-	testDirNotExists(t, string(expectedPath))
-	testDirExists(t, string(expectedParentPath))
+	testDirNotExists(t, expectedPath)
+	testDirExists(t, expectedParentPath)
 }
 
 func TestGetEnvironments(t *testing.T) {
@@ -199,11 +196,11 @@ func TestSetEnvironment(t *testing.T) {
 	}
 
 	// Ensure new env directory is created, and old directory no longer exists.
-	envPath := appendToAbsPath(AbsPath(appName), environmentsDir)
-	expectedPathExists := appendToAbsPath(envPath, setName)
-	expectedPathNotExists := appendToAbsPath(envPath, mockEnvName)
-	testDirExists(t, string(expectedPathExists))
-	testDirNotExists(t, string(expectedPathNotExists))
+	envPath := str.AppendToPath(appName, environmentsDir)
+	expectedPathExists := str.AppendToPath(envPath, setName)
+	expectedPathNotExists := str.AppendToPath(envPath, mockEnvName)
+	testDirExists(t, expectedPathExists)
+	testDirNotExists(t, expectedPathNotExists)
 
 	// BUG: https://github.com/spf13/afero/issues/141
 	// we aren't able to test this until the above is fixed.
@@ -246,8 +243,8 @@ func TestSetEnvironment(t *testing.T) {
 			t.Fatalf("Could not set '%s', got:\n  %s", v.nameOld, err)
 		}
 		// Ensure new env directory is created
-		expectedPath := appendToAbsPath(AbsPath(v.appName), environmentsDir, v.nameNew)
-		testDirExists(t, string(expectedPath))
+		expectedPath := str.AppendToPath(v.appName, environmentsDir, v.nameNew)
+		testDirExists(t, expectedPath)
 	}
 }
 
diff --git a/metadata/interface.go b/metadata/interface.go
index d440f60f5def0cfb7c604faa403fa3091dbdfddc..9fe0996a4d33ce15ffcce6bddde62db971aea3ee 100644
--- a/metadata/interface.go
+++ b/metadata/interface.go
@@ -32,24 +32,16 @@ var appFS afero.Fs
 var defaultFolderPermissions = os.FileMode(0755)
 var defaultFilePermissions = os.FileMode(0644)
 
-// 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
-	LibPaths() (libPath, vendorPath AbsPath)
-	EnvPaths(env string) (metadataPath, mainPath, paramsPath AbsPath)
+	Root() string
+	LibPaths() (libPath, vendorPath string)
+	EnvPaths(env string) (libPath, mainPath, paramsPath string, err error)
 
 	// Components API.
-	ComponentPaths() (AbsPaths, error)
+	ComponentPaths() ([]string, error)
 	GetAllComponents() ([]string, error)
 	CreateComponent(name string, text string, params param.Params, templateType prototype.TemplateType) error
 	DeleteComponent(name string) error
@@ -66,7 +58,7 @@ type Manager interface {
 	SetEnvironmentParams(env, component string, params param.Params) error
 
 	// Environment API.
-	CreateEnvironment(name, uri, namespace string, spec ClusterSpec) error
+	CreateEnvironment(name, uri, namespace, spec string) error
 	DeleteEnvironment(name string) error
 	GetEnvironments() (app.EnvironmentSpecs, error)
 	GetEnvironment(name string) (*app.EnvironmentSpec, error)
@@ -88,14 +80,12 @@ type Manager interface {
 // Find will recursively search the current directory and its parents for a
 // `.ksonnet` folder, which marks the application root. Returns error if there
 // is no application root.
-func Find(path AbsPath) (Manager, error) {
+func Find(path string) (Manager, error) {
 	return findManager(path, afero.NewOsFs())
 }
 
-// Init will retrieve a cluster API specification, generate a
-// capabilities-compliant version of ksonnet-lib, and then generate the
-// directory tree for an application.
-func Init(name string, rootPath AbsPath, spec ClusterSpec, serverURI, namespace *string) (Manager, error) {
+// Init will generate the directory tree for a ksonnet project.
+func Init(name, rootPath string, k8sSpecFlag, serverURI, namespace *string) (Manager, error) {
 	// Generate `incubator` registry. We do this before before creating
 	// directory tree, in case the network call fails.
 	const (
@@ -112,24 +102,7 @@ func Init(name string, rootPath AbsPath, spec ClusterSpec, serverURI, namespace
 		return nil, err
 	}
 
-	return initManager(name, rootPath, spec, serverURI, namespace, gh, appFS)
-}
-
-// ClusterSpec represents the API supported by some cluster. There are several
-// ways to specify a cluster, including: querying the API server, reading an
-// OpenAPI spec in some file, or consulting the OpenAPI spec released in a
-// specific version of Kubernetes.
-type ClusterSpec interface {
-	OpenAPI() ([]byte, error)
-	Resource() string // For testing parsing logic.
-}
-
-// ParseClusterSpec will parse a cluster spec flag and output a well-formed
-// ClusterSpec object. For example, if the flag is `--version:v1.7.1`, then we
-// will output a ClusterSpec representing the cluster specification associated
-// with the `v1.7.1` build of Kubernetes.
-func ParseClusterSpec(specFlag string) (ClusterSpec, error) {
-	return parseClusterSpec(specFlag, appFS)
+	return initManager(name, rootPath, k8sSpecFlag, serverURI, namespace, gh, appFS)
 }
 
 // isValidName returns true if a name (e.g., for an environment) is valid.
diff --git a/metadata/lib/clusterspec.go b/metadata/lib/clusterspec.go
new file mode 100644
index 0000000000000000000000000000000000000000..6a0e45d0c6d429ff3c5910b0a8ec56a1cd873eae
--- /dev/null
+++ b/metadata/lib/clusterspec.go
@@ -0,0 +1,151 @@
+// Copyright 2017 The ksonnet 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 lib
+
+import (
+	"encoding/json"
+	"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"
+)
+
+// ClusterSpec represents the API supported by some cluster. There are several
+// ways to specify a cluster, including: querying the API server, reading an
+// OpenAPI spec in some file, or consulting the OpenAPI spec released in a
+// specific version of Kubernetes.
+type ClusterSpec interface {
+	OpenAPI() ([]byte, error)
+	Resource() string // For testing parsing logic.
+	Version() (string, error)
+}
+
+// ParseClusterSpec will parse a cluster spec flag and output a well-formed
+// ClusterSpec object. For example, if the flag is `--version:v1.7.1`, then we
+// will output a ClusterSpec representing the cluster specification associated
+// with the `v1.7.1` build of Kubernetes.
+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":
+		p, err := filepath.Abs(split[1])
+		if err != nil {
+			return nil, err
+		}
+		return &clusterSpecFile{specPath: p, 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 string
+	fs       afero.Fs
+}
+
+func (cs *clusterSpecFile) OpenAPI() ([]byte, error) {
+	return afero.ReadFile(cs.fs, string(cs.specPath))
+}
+
+func (cs *clusterSpecFile) Resource() string {
+	return string(cs.specPath)
+}
+
+func (cs *clusterSpecFile) Version() (string, error) {
+	//
+	// Condensed representation of the spec file, containing the minimal
+	// information necessary to retrieve the spec version.
+	//
+	type Info struct {
+		Version string `json:"version"`
+	}
+
+	type Spec struct {
+		Info Info `json:"info"`
+	}
+
+	bytes, err := cs.OpenAPI()
+	if err != nil {
+		return "", err
+	}
+
+	var spec *Spec
+	if err := json.Unmarshal(bytes, &spec); err != nil {
+		return "", err
+	}
+
+	return spec.Info.Version, nil
+}
+
+type clusterSpecLive struct {
+	apiServerURL string
+}
+
+func (cs *clusterSpecLive) OpenAPI() ([]byte, error) {
+	return nil, fmt.Errorf("Initializing from OpenAPI spec in live cluster is not implemented")
+}
+
+func (cs *clusterSpecLive) Resource() string {
+	return string(cs.apiServerURL)
+}
+
+func (cs *clusterSpecLive) Version() (string, error) {
+	return "", fmt.Errorf("Retrieving version spec in live cluster is not implemented")
+}
+
+type clusterSpecVersion struct {
+	k8sVersion string
+}
+
+func (cs *clusterSpecVersion) OpenAPI() ([]byte, error) {
+	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 {
+	return string(cs.k8sVersion)
+}
+
+func (cs *clusterSpecVersion) Version() (string, error) {
+	return string(cs.k8sVersion), nil
+}
diff --git a/metadata/clusterspec_test.go b/metadata/lib/clusterspec_test.go
similarity index 73%
rename from metadata/clusterspec_test.go
rename to metadata/lib/clusterspec_test.go
index f300b128cefdc32dd3362a17361c5894bab4a8fc..5c095cbf0c6aa33cfd41f330a381290ca630abc3 100644
--- a/metadata/clusterspec_test.go
+++ b/metadata/lib/clusterspec_test.go
@@ -1,4 +1,19 @@
-package metadata
+// Copyright 2017 The ksonnet 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 lib
 
 import (
 	"path/filepath"
@@ -18,7 +33,7 @@ var successTests = []parseSuccess{
 
 func TestClusterSpecParsingSuccess(t *testing.T) {
 	for _, test := range successTests {
-		parsed, err := parseClusterSpec(test.input, testFS)
+		parsed, err := ParseClusterSpec(test.input, testFS)
 		if err != nil {
 			t.Errorf("Failed to parse spec: %v", err)
 		}
@@ -66,7 +81,7 @@ var failureTests = []parseFailure{
 
 func TestClusterSpecParsingFailure(t *testing.T) {
 	for _, test := range failureTests {
-		_, err := parseClusterSpec(test.input, testFS)
+		_, 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/lib/lib.go b/metadata/lib/lib.go
new file mode 100644
index 0000000000000000000000000000000000000000..df5fd18600e4a236c582b18060a04475566e18f1
--- /dev/null
+++ b/metadata/lib/lib.go
@@ -0,0 +1,154 @@
+// Copyright 2017 The ksonnet 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 lib
+
+import (
+	"fmt"
+	"os"
+	"path"
+
+	str "github.com/ksonnet/ksonnet/strings"
+	log "github.com/sirupsen/logrus"
+	"github.com/spf13/afero"
+
+	"github.com/ksonnet/ksonnet/generator"
+)
+
+const (
+	schemaFilename = "swagger.json"
+	k8sLibFilename = "k8s.libsonnet"
+
+	// ExtensionsLibFilename is the file name with the contents of the
+	// generated ksonnet-lib
+	ExtensionsLibFilename = "k.libsonnet"
+)
+
+// Manager operates on the files in the lib directory of a ksonnet project.
+// This included generating the ksonnet-lib files needed to compile the
+// ksonnet (jsonnet) code.
+type Manager struct {
+	// K8sVersion is the Kubernetes version of the Open API spec.
+	K8sVersion string
+
+	spec    ClusterSpec
+	libPath string
+	fs      afero.Fs
+}
+
+// NewManager creates a crew instance of lib.Manager
+func NewManager(k8sVersion string, fs afero.Fs, libPath string) *Manager {
+	return &Manager{K8sVersion: k8sVersion, fs: fs, libPath: libPath}
+}
+
+// NewManagerWithSpec creates a new instance of lib.Manager with the cluster spec initialized.
+func NewManagerWithSpec(k8sSpecFlag string, fs afero.Fs, libPath string) (*Manager, error) {
+	//
+	// Generate the program text for ksonnet-lib.
+	//
+	spec, err := ParseClusterSpec(k8sSpecFlag, fs)
+	if err != nil {
+		return nil, err
+	}
+
+	version, err := spec.Version()
+	if err != nil {
+		return nil, err
+	}
+
+	return &Manager{K8sVersion: version, fs: fs, libPath: libPath, spec: spec}, nil
+}
+
+// GenerateLibData will generate the swagger and ksonnet-lib files in the lib
+// directory of a ksonnet project. The swagger and ksonnet-lib files are
+// unique to each Kubernetes API version. If the files already exist for a
+// specific Kubernetes API version, they won't be re-generated here.
+func (m *Manager) GenerateLibData() error {
+	if m.spec == nil {
+		return fmt.Errorf("Uninitialized ClusterSpec")
+	}
+
+	b, err := m.spec.OpenAPI()
+	if err != nil {
+		return err
+	}
+
+	kl, err := generator.Ksonnet(b)
+	if err != nil {
+		return err
+	}
+
+	versionPath := str.AppendToPath(m.libPath, m.K8sVersion)
+	ok, err := afero.DirExists(m.fs, string(versionPath))
+	if err != nil {
+		return err
+	}
+	if ok {
+		// Already have lib data for this k8s api version
+		return nil
+	}
+
+	err = m.fs.MkdirAll(string(versionPath), os.FileMode(0755))
+	if err != nil {
+		return err
+	}
+
+	files := []struct {
+		path string
+		data []byte
+	}{
+		{
+			// schema file
+			str.AppendToPath(versionPath, schemaFilename),
+			kl.Swagger,
+		},
+		{
+			// k8s file
+			str.AppendToPath(versionPath, k8sLibFilename),
+			kl.K8s,
+		},
+		{
+			// extensions file
+			str.AppendToPath(versionPath, ExtensionsLibFilename),
+			kl.K,
+		},
+	}
+
+	log.Infof("Generating ksonnet-lib data at path '%s'", versionPath)
+
+	for _, a := range files {
+		fileName := path.Base(string(a.path))
+		if err = afero.WriteFile(m.fs, string(a.path), a.data, os.FileMode(0644)); err != nil {
+			log.Debugf("Failed to write '%s'", fileName)
+			return err
+		}
+	}
+
+	return nil
+}
+
+// GetLibPath returns the absolute path pointing to the directory with the
+// metadata files for the provided k8sVersion.
+func (m *Manager) GetLibPath() (string, error) {
+	path := str.AppendToPath(m.libPath, m.K8sVersion)
+	ok, err := afero.DirExists(m.fs, string(path))
+	if err != nil {
+		return "", err
+	}
+	if !ok {
+		return "", fmt.Errorf("Expected lib directory '%s' but was not found", m.K8sVersion)
+	}
+	return path, err
+}
diff --git a/metadata/lib/lib_test.go b/metadata/lib/lib_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..b0792e9de22c7da5b65875164f3f9fb6b040202a
--- /dev/null
+++ b/metadata/lib/lib_test.go
@@ -0,0 +1,88 @@
+// Copyright 2017 The ksonnet 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 lib
+
+import (
+	"fmt"
+	"os"
+	"testing"
+
+	"github.com/spf13/afero"
+
+	str "github.com/ksonnet/ksonnet/strings"
+)
+
+const (
+	blankSwagger     = "/blankSwagger.json"
+	blankSwaggerData = `{
+  "swagger": "2.0",
+  "info": {
+   "title": "Kubernetes",
+   "version": "v1.7.0"
+  },
+  "paths": {
+  },
+  "definitions": {
+  }
+}`
+	blankK8sLib = `// AUTOGENERATED from the Kubernetes OpenAPI specification. DO NOT MODIFY.
+// Kubernetes version: v1.7.0
+
+{
+  local hidden = {
+  },
+}
+`
+)
+
+var testFS = afero.NewMemMapFs()
+
+func init() {
+	afero.WriteFile(testFS, blankSwagger, []byte(blankSwaggerData), os.ModePerm)
+}
+
+func TestGenerateLibData(t *testing.T) {
+	specFlag := fmt.Sprintf("file:%s", blankSwagger)
+	libPath := "lib"
+
+	libManager, err := NewManagerWithSpec(specFlag, testFS, libPath)
+	if err != nil {
+		t.Fatal("Failed to initialize lib.Manager")
+	}
+
+	err = libManager.GenerateLibData()
+	if err != nil {
+		t.Fatal("Failed to generate lib data")
+	}
+
+	// Verify contents of lib.
+	versionPath := str.AppendToPath(libPath, "v1.7.0")
+
+	schemaPath := str.AppendToPath(versionPath, schemaFilename)
+	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)
+	}
+
+	k8sLibPath := str.AppendToPath(versionPath, k8sLibFilename)
+	k8sLibBytes, err := afero.ReadFile(testFS, string(k8sLibPath))
+	if err != nil {
+		t.Fatalf("Failed to read ksonnet-lib file at '%s':\n%v", k8sLibPath, err)
+	} else if actualK8sLib := string(k8sLibBytes); actualK8sLib != blankK8sLib {
+		t.Fatalf("Expected swagger file at '%s' to have value: '%s', got: '%s'", k8sLibPath, blankK8sLib, actualK8sLib)
+	}
+}
diff --git a/metadata/manager.go b/metadata/manager.go
index 58c2d12f1816bc3f311dbf662e7b9254c9cdca7e..6c751d4545ce17ed9effe5b06b74c2002f3313fc 100644
--- a/metadata/manager.go
+++ b/metadata/manager.go
@@ -21,18 +21,13 @@ import (
 	"path"
 	"path/filepath"
 
-	"github.com/ksonnet/ksonnet/generator"
 	"github.com/ksonnet/ksonnet/metadata/app"
 	"github.com/ksonnet/ksonnet/metadata/registry"
+	str "github.com/ksonnet/ksonnet/strings"
 	log "github.com/sirupsen/logrus"
 	"github.com/spf13/afero"
 )
 
-func appendToAbsPath(originalPath AbsPath, toAppend ...string) AbsPath {
-	paths := append([]string{string(originalPath)}, toAppend...)
-	return AbsPath(path.Join(paths...))
-}
-
 const (
 	ksonnetDir      = ".ksonnet"
 	registriesDir   = ksonnetDir + "/registries"
@@ -64,26 +59,26 @@ type manager struct {
 	appFS afero.Fs
 
 	// Application paths.
-	rootPath         AbsPath
-	ksonnetPath      AbsPath
-	registriesPath   AbsPath
-	libPath          AbsPath
-	componentsPath   AbsPath
-	environmentsPath AbsPath
-	vendorPath       AbsPath
-
-	componentParamsPath AbsPath
-	baseLibsonnetPath   AbsPath
-	appYAMLPath         AbsPath
+	rootPath         string
+	ksonnetPath      string
+	registriesPath   string
+	libPath          string
+	componentsPath   string
+	environmentsPath string
+	vendorPath       string
+
+	componentParamsPath string
+	baseLibsonnetPath   string
+	appYAMLPath         string
 
 	// User-level paths.
-	userKsonnetRootPath AbsPath
-	pkgSrcCachePath     AbsPath
+	userKsonnetRootPath string
+	pkgSrcCachePath     string
 }
 
-func findManager(abs AbsPath, appFS afero.Fs) (*manager, error) {
+func findManager(p string, appFS afero.Fs) (*manager, error) {
 	var lastBase string
-	currBase := string(abs)
+	currBase := p
 
 	for {
 		currPath := path.Join(currBase, ksonnetDir)
@@ -92,7 +87,7 @@ func findManager(abs AbsPath, appFS afero.Fs) (*manager, error) {
 			return nil, err
 		}
 		if exists {
-			return newManager(AbsPath(currBase), appFS)
+			return newManager(currBase, appFS)
 		}
 
 		lastBase = currBase
@@ -103,30 +98,12 @@ func findManager(abs AbsPath, appFS afero.Fs) (*manager, error) {
 	}
 }
 
-func initManager(name string, rootPath AbsPath, spec ClusterSpec, serverURI, namespace *string, incubatorReg registry.Manager, appFS afero.Fs) (*manager, error) {
+func initManager(name, rootPath string, k8sSpecFlag, serverURI, namespace *string, incubatorReg registry.Manager, appFS afero.Fs) (*manager, error) {
 	m, err := newManager(rootPath, appFS)
 	if err != nil {
 		return nil, err
 	}
 
-	//
-	// Generate the program text for ksonnet-lib.
-	//
-	// 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.
-	//
-	b, err := spec.OpenAPI()
-	if err != nil {
-		return nil, err
-	}
-
-	kl, err := generator.Ksonnet(b)
-	if err != nil {
-		return nil, err
-	}
-
 	// Retrieve `registry.yaml`.
 	registryYAMLData, err := generateRegistryYAMLData(incubatorReg)
 	if err != nil {
@@ -154,7 +131,7 @@ func initManager(name string, rootPath AbsPath, spec ClusterSpec, serverURI, nam
 
 	// Initialize environment, and cache specification data.
 	if serverURI != nil {
-		err := m.createEnvironment(defaultEnvName, *serverURI, *namespace, kl.K, kl.K8s, kl.Swagger)
+		err := m.CreateEnvironment(defaultEnvName, *serverURI, *namespace, *k8sSpecFlag)
 		if err != nil {
 			return nil, errorOnCreateFailure(name, err)
 		}
@@ -170,64 +147,51 @@ func initManager(name string, rootPath AbsPath, spec ClusterSpec, serverURI, nam
 	return m, nil
 }
 
-func newManager(rootPath AbsPath, appFS afero.Fs) (*manager, error) {
+func newManager(rootPath string, appFS afero.Fs) (*manager, error) {
 	usr, err := user.Current()
 	if err != nil {
 		return nil, err
 	}
-	userRootPath := appendToAbsPath(AbsPath(usr.HomeDir), userKsonnetRootDir)
+	userRootPath := str.AppendToPath(usr.HomeDir, userKsonnetRootDir)
 
 	return &manager{
 		appFS: appFS,
 
 		// Application paths.
 		rootPath:         rootPath,
-		ksonnetPath:      appendToAbsPath(rootPath, ksonnetDir),
-		registriesPath:   appendToAbsPath(rootPath, registriesDir),
-		libPath:          appendToAbsPath(rootPath, libDir),
-		componentsPath:   appendToAbsPath(rootPath, componentsDir),
-		environmentsPath: appendToAbsPath(rootPath, environmentsDir),
-		vendorPath:       appendToAbsPath(rootPath, vendorDir),
+		ksonnetPath:      str.AppendToPath(rootPath, ksonnetDir),
+		registriesPath:   str.AppendToPath(rootPath, registriesDir),
+		libPath:          str.AppendToPath(rootPath, libDir),
+		componentsPath:   str.AppendToPath(rootPath, componentsDir),
+		environmentsPath: str.AppendToPath(rootPath, environmentsDir),
+		vendorPath:       str.AppendToPath(rootPath, vendorDir),
 
-		componentParamsPath: appendToAbsPath(rootPath, componentsDir, componentParamsFile),
-		baseLibsonnetPath:   appendToAbsPath(rootPath, environmentsDir, baseLibsonnetFile),
-		appYAMLPath:         appendToAbsPath(rootPath, appYAMLFile),
+		componentParamsPath: str.AppendToPath(rootPath, componentsDir, componentParamsFile),
+		baseLibsonnetPath:   str.AppendToPath(rootPath, environmentsDir, baseLibsonnetFile),
+		appYAMLPath:         str.AppendToPath(rootPath, appYAMLFile),
 
 		// User-level paths.
 		userKsonnetRootPath: userRootPath,
-		pkgSrcCachePath:     appendToAbsPath(userRootPath, pkgSrcCacheDir),
+		pkgSrcCachePath:     str.AppendToPath(userRootPath, pkgSrcCacheDir),
 	}, nil
 }
 
-func (m *manager) Root() AbsPath {
+func (m *manager) Root() string {
 	return m.rootPath
 }
 
-func (m *manager) LibPaths() (libPath, vendorPath AbsPath) {
+func (m *manager) LibPaths() (libPath, vendorPath string) {
 	return m.libPath, m.vendorPath
 }
 
-func (m *manager) EnvPaths(env string) (metadataPath, mainPath, paramsPath AbsPath) {
-	envPath := appendToAbsPath(m.environmentsPath, env)
-
-	// .metadata directory
-	metadataPath = appendToAbsPath(envPath, metadataDirName)
-	// main.jsonnet file
-	mainPath = appendToAbsPath(envPath, envFileName)
-	// params.libsonnet file
-	paramsPath = appendToAbsPath(envPath, componentParamsFile)
-
-	return
-}
-
 func (m *manager) createUserDirTree() error {
-	dirPaths := []AbsPath{
+	dirPaths := []string{
 		m.userKsonnetRootPath,
 		m.pkgSrcCachePath,
 	}
 
 	for _, p := range dirPaths {
-		if err := m.appFS.MkdirAll(string(p), defaultFolderPermissions); err != nil {
+		if err := m.appFS.MkdirAll(p, defaultFolderPermissions); err != nil {
 			return err
 		}
 	}
@@ -236,14 +200,14 @@ func (m *manager) createUserDirTree() error {
 }
 
 func (m *manager) createAppDirTree(name string, appYAMLData, baseLibData []byte, gh registry.Manager) error {
-	exists, err := afero.DirExists(m.appFS, string(m.rootPath))
+	exists, err := afero.DirExists(m.appFS, m.rootPath)
 	if err != nil {
 		return fmt.Errorf("Could not check existance of directory '%s':\n%v", m.rootPath, err)
 	} else if exists {
 		return fmt.Errorf("Could not create app; directory '%s' already exists", m.rootPath)
 	}
 
-	dirPaths := []AbsPath{
+	dirPaths := []string{
 		m.rootPath,
 		m.ksonnetPath,
 		m.registriesPath,
@@ -262,7 +226,7 @@ func (m *manager) createAppDirTree(name string, appYAMLData, baseLibData []byte,
 	}
 
 	filePaths := []struct {
-		path    AbsPath
+		path    string
 		content []byte
 	}{
 		{
@@ -285,7 +249,7 @@ func (m *manager) createAppDirTree(name string, appYAMLData, baseLibData []byte,
 
 	for _, f := range filePaths {
 		log.Debugf("Creating file '%s'", f.path)
-		if err := afero.WriteFile(m.appFS, string(f.path), f.content, defaultFilePermissions); err != nil {
+		if err := afero.WriteFile(m.appFS, f.path, f.content, defaultFilePermissions); err != nil {
 			return err
 		}
 	}
diff --git a/metadata/manager_test.go b/metadata/manager_test.go
index 254bced3175f2a3867dbb3031c131d215bf0b659..a8f02018acbd50f6fee94139c6fa520eaaa3de12 100644
--- a/metadata/manager_test.go
+++ b/metadata/manager_test.go
@@ -21,6 +21,7 @@ import (
 	"path"
 	"testing"
 
+	str "github.com/ksonnet/ksonnet/strings"
 	"github.com/spf13/afero"
 )
 
@@ -54,21 +55,18 @@ func init() {
 }
 
 func TestInitSuccess(t *testing.T) {
-	spec, err := parseClusterSpec(fmt.Sprintf("file:%s", blankSwagger), testFS)
-	if err != nil {
-		t.Fatalf("Failed to parse cluster spec: %v", err)
-	}
+	specFlag := fmt.Sprintf("file:%s", blankSwagger)
 
-	appPath := AbsPath("/fromEmptySwagger")
+	appPath := "/fromEmptySwagger"
 	reg := newMockRegistryManager("incubator")
-	_, err = initManager("fromEmptySwagger", appPath, spec, &mockAPIServer, &mockNamespace, reg, testFS)
+	_, err := initManager("fromEmptySwagger", appPath, &specFlag, &mockAPIServer, &mockNamespace, reg, testFS)
 	if err != nil {
 		t.Fatalf("Failed to init cluster spec: %v", err)
 	}
 
 	// Verify path locations.
-	defaultEnvDir := appendToAbsPath(environmentsDir, defaultEnvName)
-	paths := []AbsPath{
+	defaultEnvDir := str.AppendToPath(environmentsDir, defaultEnvName)
+	paths := []string{
 		ksonnetDir,
 		libDir,
 		componentsDir,
@@ -78,8 +76,8 @@ func TestInitSuccess(t *testing.T) {
 	}
 
 	for _, p := range paths {
-		path := appendToAbsPath(appPath, string(p))
-		exists, err := afero.DirExists(testFS, string(path))
+		path := str.AppendToPath(appPath, p)
+		exists, err := afero.DirExists(testFS, path)
 		if err != nil {
 			t.Fatalf("Expected to create directory '%s', but failed:\n%v", p, err)
 		} else if !exists {
@@ -87,7 +85,7 @@ func TestInitSuccess(t *testing.T) {
 		}
 	}
 
-	paths = []AbsPath{
+	paths = []string{
 		pkgSrcCacheDir,
 	}
 
@@ -95,11 +93,11 @@ func TestInitSuccess(t *testing.T) {
 	if err != nil {
 		t.Fatalf("Could not get user information:\n%v", err)
 	}
-	userRootPath := appendToAbsPath(AbsPath(usr.HomeDir), userKsonnetRootDir)
+	userRootPath := str.AppendToPath(usr.HomeDir, userKsonnetRootDir)
 
 	for _, p := range paths {
-		path := appendToAbsPath(userRootPath, string(p))
-		exists, err := afero.DirExists(testFS, string(path))
+		path := str.AppendToPath(userRootPath, p)
+		exists, err := afero.DirExists(testFS, path)
 		if err != nil {
 			t.Fatalf("Expected to create directory '%s', but failed:\n%v", p, err)
 		} else if !exists {
@@ -108,59 +106,34 @@ func TestInitSuccess(t *testing.T) {
 	}
 
 	// Verify contents of metadata.
-	envPath := appendToAbsPath(appPath, string(environmentsDir))
-	metadataPath := appendToAbsPath(appPath, string(defaultEnvDir), string(metadataDirName))
-
-	schemaPath := appendToAbsPath(metadataPath, schemaFilename)
-	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)
-	}
-
-	k8sLibPath := appendToAbsPath(metadataPath, k8sLibFilename)
-	k8sLibBytes, err := afero.ReadFile(testFS, string(k8sLibPath))
-	if err != nil {
-		t.Fatalf("Failed to read ksonnet-lib file at '%s':\n%v", k8sLibPath, err)
-	} else if actualK8sLib := string(k8sLibBytes); actualK8sLib != blankK8sLib {
-		t.Fatalf("Expected swagger file at '%s' to have value: '%s', got: '%s'", k8sLibPath, blankK8sLib, actualK8sLib)
-	}
-
-	extensionsLibPath := appendToAbsPath(metadataPath, extensionsLibFilename)
-	extensionsLibBytes, err := afero.ReadFile(testFS, string(extensionsLibPath))
-	if err != nil {
-		t.Fatalf("Failed to read ksonnet-lib file at '%s':\n%v", extensionsLibPath, err)
-	} else if string(extensionsLibBytes) == "" {
-		t.Fatalf("Expected extension library file at '%s' to be non-empty", extensionsLibPath)
-	}
+	envPath := str.AppendToPath(appPath, environmentsDir)
 
-	componentParamsPath := appendToAbsPath(appPath, string(componentsDir), componentParamsFile)
-	componentParamsBytes, err := afero.ReadFile(testFS, string(componentParamsPath))
+	componentParamsPath := str.AppendToPath(appPath, componentsDir, componentParamsFile)
+	componentParamsBytes, err := afero.ReadFile(testFS, componentParamsPath)
 	if err != nil {
 		t.Fatalf("Failed to read params.libsonnet file at '%s':\n%v", componentParamsPath, err)
 	} else if len(componentParamsBytes) == 0 {
 		t.Fatalf("Expected params.libsonnet at '%s' to be non-empty", componentParamsPath)
 	}
 
-	baseLibsonnetPath := appendToAbsPath(envPath, baseLibsonnetFile)
-	baseLibsonnetBytes, err := afero.ReadFile(testFS, string(baseLibsonnetPath))
+	baseLibsonnetPath := str.AppendToPath(envPath, baseLibsonnetFile)
+	baseLibsonnetBytes, err := afero.ReadFile(testFS, baseLibsonnetPath)
 	if err != nil {
 		t.Fatalf("Failed to read base.libsonnet file at '%s':\n%v", baseLibsonnetPath, err)
 	} else if len(baseLibsonnetBytes) == 0 {
 		t.Fatalf("Expected base.libsonnet at '%s' to be non-empty", baseLibsonnetPath)
 	}
 
-	appYAMLPath := appendToAbsPath(appPath, appYAMLFile)
-	appYAMLBytes, err := afero.ReadFile(testFS, string(appYAMLPath))
+	appYAMLPath := str.AppendToPath(appPath, appYAMLFile)
+	appYAMLBytes, err := afero.ReadFile(testFS, appYAMLPath)
 	if err != nil {
 		t.Fatalf("Failed to read app.yaml file at '%s':\n%v", appYAMLPath, err)
 	} else if len(appYAMLBytes) == 0 {
 		t.Fatalf("Expected app.yaml at '%s' to be non-empty", appYAMLPath)
 	}
 
-	registryYAMLPath := appendToAbsPath(appPath, registriesDir, "incubator", "master.yaml")
-	registryYAMLBytes, err := afero.ReadFile(testFS, string(registryYAMLPath))
+	registryYAMLPath := str.AppendToPath(appPath, registriesDir, "incubator", "master.yaml")
+	registryYAMLBytes, err := afero.ReadFile(testFS, registryYAMLPath)
 	if err != nil {
 		t.Fatalf("Failed to read registry.yaml file at '%s':\n%v", registryYAMLPath, err)
 	} else if len(registryYAMLBytes) == 0 {
@@ -169,7 +142,7 @@ func TestInitSuccess(t *testing.T) {
 }
 
 func TestFindSuccess(t *testing.T) {
-	findSuccess := func(t *testing.T, appDir, currDir AbsPath) {
+	findSuccess := func(t *testing.T, appDir, currDir string) {
 		m, err := findManager(currDir, testFS)
 		if err != nil {
 			t.Fatalf("Failed to find manager at path '%s':\n%v", currDir, err)
@@ -178,26 +151,23 @@ func TestFindSuccess(t *testing.T) {
 		}
 	}
 
-	spec, err := parseClusterSpec(fmt.Sprintf("file:%s", blankSwagger), testFS)
-	if err != nil {
-		t.Fatalf("Failed to parse cluster spec: %v", err)
-	}
+	specFlag := fmt.Sprintf("file:%s", blankSwagger)
 
-	appPath := AbsPath("/findSuccess")
+	appPath := "/findSuccess"
 	reg := newMockRegistryManager("incubator")
-	_, err = initManager("findSuccess", appPath, spec, &mockAPIServer, &mockNamespace, reg, testFS)
+	_, err := initManager("findSuccess", appPath, &specFlag, &mockAPIServer, &mockNamespace, reg, testFS)
 	if err != nil {
 		t.Fatalf("Failed to init cluster spec: %v", err)
 	}
 
 	findSuccess(t, appPath, appPath)
 
-	components := appendToAbsPath(appPath, componentsDir)
+	components := str.AppendToPath(appPath, componentsDir)
 	findSuccess(t, appPath, components)
 
 	// Create empty app file.
-	appFile := appendToAbsPath(components, "app.jsonnet")
-	f, err := testFS.OpenFile(string(appFile), os.O_RDONLY|os.O_CREATE, 0777)
+	appFile := str.AppendToPath(components, "app.jsonnet")
+	f, err := testFS.OpenFile(appFile, os.O_RDONLY|os.O_CREATE, 0777)
 	if err != nil {
 		t.Fatalf("Failed to touch app file '%s'\n%v", appFile, err)
 	}
@@ -213,36 +183,35 @@ func TestLibPaths(t *testing.T) {
 	m := mockEnvironments(t, appName)
 
 	libPath, vendorPath := m.LibPaths()
-	if string(libPath) != expectedLibPath {
+	if libPath != expectedLibPath {
 		t.Fatalf("Expected lib path to be:\n  '%s'\n, got:\n  '%s'", expectedLibPath, libPath)
 	}
-	if string(vendorPath) != expectedVendorPath {
+	if vendorPath != expectedVendorPath {
 		t.Fatalf("Expected vendor lib path to be:\n  '%s'\n, got:\n  '%s'", expectedVendorPath, vendorPath)
 	}
 }
 
 func TestEnvPaths(t *testing.T) {
 	appName := "test-env-paths"
-	expectedMetadataPath := path.Join(appName, environmentsDir, mockEnvName, metadataDirName)
 	expectedMainPath := path.Join(appName, environmentsDir, mockEnvName, envFileName)
 	expectedParamsPath := path.Join(appName, environmentsDir, mockEnvName, paramsFileName)
 	m := mockEnvironments(t, appName)
 
-	metadataPath, mainPath, paramsPath := m.EnvPaths(mockEnvName)
-
-	if string(metadataPath) != expectedMetadataPath {
-		t.Fatalf("Expected environment metadata dir path to be:\n  '%s'\n, got:\n  '%s'", expectedMetadataPath, metadataPath)
+	_, mainPath, paramsPath, err := m.EnvPaths(mockEnvName)
+	if err != nil {
+		t.Fatalf("Failure retrieving EnvPaths")
 	}
-	if string(mainPath) != expectedMainPath {
+
+	if mainPath != expectedMainPath {
 		t.Fatalf("Expected environment main path to be:\n  '%s'\n, got:\n  '%s'", expectedMainPath, mainPath)
 	}
-	if string(paramsPath) != expectedParamsPath {
+	if paramsPath != expectedParamsPath {
 		t.Fatalf("Expected environment params path to be:\n  '%s'\n, got:\n  '%s'", expectedParamsPath, paramsPath)
 	}
 }
 
 func TestFindFailure(t *testing.T) {
-	findFailure := func(t *testing.T, currDir AbsPath) {
+	findFailure := func(t *testing.T, currDir string) {
 		_, err := findManager(currDir, testFS)
 		if err == nil {
 			t.Fatalf("Expected to fail to find ksonnet app in '%s', but succeeded", currDir)
@@ -255,20 +224,17 @@ func TestFindFailure(t *testing.T) {
 }
 
 func TestDoubleNewFailure(t *testing.T) {
-	spec, err := parseClusterSpec(fmt.Sprintf("file:%s", blankSwagger), testFS)
-	if err != nil {
-		t.Fatalf("Failed to parse cluster spec: %v", err)
-	}
+	specFlag := fmt.Sprintf("file:%s", blankSwagger)
 
-	appPath := AbsPath("/doubleNew")
+	appPath := "/doubleNew"
 	reg := newMockRegistryManager("incubator")
-	_, err = initManager("doubleNew", appPath, spec, &mockAPIServer, &mockNamespace, reg, testFS)
+	_, err := initManager("doubleNew", appPath, &specFlag, &mockAPIServer, &mockNamespace, reg, 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("doubleNew", appPath, spec, &mockAPIServer, &mockNamespace, reg, testFS)
+	_, err = initManager("doubleNew", appPath, &specFlag, &mockAPIServer, &mockNamespace, reg, 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/metadata/params/params.go b/metadata/params/params.go
index 20a10ab105d90587396cbfd062cf08120e8f9570..da12d76f89dd78fa6830eb065bc4f1aedc09d204 100644
--- a/metadata/params/params.go
+++ b/metadata/params/params.go
@@ -22,7 +22,7 @@ import (
 	"strconv"
 	"strings"
 
-	"github.com/ksonnet/ksonnet/utils"
+	str "github.com/ksonnet/ksonnet/strings"
 
 	"github.com/google/go-jsonnet/ast"
 	"github.com/google/go-jsonnet/parser"
@@ -172,7 +172,7 @@ func writeParams(indent int, params Params) string {
 	buffer.WriteString("\n")
 	for i, key := range keys {
 		param := params[key]
-		key := utils.QuoteNonASCII(key)
+		key := str.QuoteNonASCII(key)
 
 		if strings.HasPrefix(param, "|||\n") {
 			// every line in a block string needs to be indented
@@ -245,7 +245,7 @@ func appendComponent(component, snippet string, params Params) (string, error) {
 
 	// Create the jsonnet resembling the component params
 	var buffer bytes.Buffer
-	buffer.WriteString("    " + utils.QuoteNonASCII(component) + ": {")
+	buffer.WriteString("    " + str.QuoteNonASCII(component) + ": {")
 	buffer.WriteString(writeParams(6, params))
 	buffer.WriteString("    },")
 
@@ -362,7 +362,7 @@ func setEnvironmentParams(component, snippet string, params Params) (string, err
 	lines := strings.Split(snippet, "\n")
 	if !hasComponent {
 		var buffer bytes.Buffer
-		buffer.WriteString(fmt.Sprintf("\n    %s +: {", utils.QuoteNonASCII(component)))
+		buffer.WriteString(fmt.Sprintf("\n    %s +: {", str.QuoteNonASCII(component)))
 		buffer.WriteString(writeParams(6, params))
 		buffer.WriteString("    },\n")
 		paramsSnippet = buffer.String()
diff --git a/metadata/registry.go b/metadata/registry.go
index 42e6a54432d4a121e046a5ce585e572b792d6e09..7c8f7652430c91b70fa1fabb088ef6a16be339fc 100644
--- a/metadata/registry.go
+++ b/metadata/registry.go
@@ -9,6 +9,7 @@ import (
 	"github.com/ksonnet/ksonnet/metadata/parts"
 	"github.com/ksonnet/ksonnet/metadata/registry"
 	"github.com/ksonnet/ksonnet/prototype"
+	str "github.com/ksonnet/ksonnet/strings"
 	log "github.com/sirupsen/logrus"
 	"github.com/spf13/afero"
 )
@@ -48,7 +49,7 @@ func (m *manager) AddRegistry(name, protocol, uri, version string) (*registry.Sp
 		return nil, err
 	}
 
-	err = afero.WriteFile(m.appFS, string(m.appYAMLPath), specBytes, defaultFilePermissions)
+	err = afero.WriteFile(m.appFS, m.appYAMLPath, specBytes, defaultFilePermissions)
 	if err != nil {
 		return nil, err
 	}
@@ -118,8 +119,8 @@ func (m *manager) GetDependency(libName string) (*parts.Spec, error) {
 		return nil, fmt.Errorf("Library '%s' is not a dependency in current ksonnet app", libName)
 	}
 
-	partsYAMLPath := appendToAbsPath(m.vendorPath, libRef.Registry, libName, partsYAMLFile)
-	partsBytes, err := afero.ReadFile(m.appFS, string(partsYAMLPath))
+	partsYAMLPath := str.AppendToPath(m.vendorPath, libRef.Registry, libName, partsYAMLFile)
+	partsBytes, err := afero.ReadFile(m.appFS, partsYAMLPath)
 	if err != nil {
 		return nil, err
 	}
@@ -166,18 +167,18 @@ func (m *manager) CacheDependency(registryName, libID, libName, libVersion strin
 	// Get all directories and files first, then write to disk. This
 	// protects us from failing with a half-cached dependency because of
 	// a network failure.
-	directories := []AbsPath{}
-	files := map[AbsPath][]byte{}
+	directories := []string{}
+	files := map[string][]byte{}
 	parts, libRef, err := registryManager.ResolveLibrary(
 		libID,
 		libName,
 		libVersion,
 		func(relPath string, contents []byte) error {
-			files[appendToAbsPath(m.vendorPath, relPath)] = contents
+			files[str.AppendToPath(m.vendorPath, relPath)] = contents
 			return nil
 		},
 		func(relPath string) error {
-			directories = append(directories, appendToAbsPath(m.vendorPath, relPath))
+			directories = append(directories, str.AppendToPath(m.vendorPath, relPath))
 			return nil
 		})
 	if err != nil {
@@ -195,18 +196,18 @@ func (m *manager) CacheDependency(registryName, libID, libName, libVersion strin
 	log.Infof("Retrieved %d files", len(files))
 
 	for _, dir := range directories {
-		if err := m.appFS.MkdirAll(string(dir), defaultFolderPermissions); err != nil {
+		if err := m.appFS.MkdirAll(dir, defaultFolderPermissions); err != nil {
 			return nil, err
 		}
 	}
 
 	for path, content := range files {
-		if err := afero.WriteFile(m.appFS, string(path), content, defaultFilePermissions); err != nil {
+		if err := afero.WriteFile(m.appFS, path, content, defaultFilePermissions); err != nil {
 			return nil, err
 		}
 	}
 
-	err = afero.WriteFile(m.appFS, string(m.appYAMLPath), appSpecData, defaultFilePermissions)
+	err = afero.WriteFile(m.appFS, m.appYAMLPath, appSpecData, defaultFilePermissions)
 	if err != nil {
 		return nil, err
 	}
@@ -217,7 +218,7 @@ func (m *manager) CacheDependency(registryName, libID, libName, libVersion strin
 func (m *manager) GetPrototypesForDependency(registryName, libID string) (prototype.SpecificationSchemas, error) {
 	// TODO: Remove `registryName` when we flatten vendor/.
 	specs := prototype.SpecificationSchemas{}
-	protos := string(appendToAbsPath(m.vendorPath, registryName, libID, "prototypes"))
+	protos := str.AppendToPath(m.vendorPath, registryName, libID, "prototypes")
 	exists, err := afero.DirExists(m.appFS, protos)
 	if err != nil {
 		return nil, err
@@ -270,12 +271,12 @@ func (m *manager) GetAllPrototypes() (prototype.SpecificationSchemas, error) {
 	return specs, nil
 }
 
-func (m *manager) registryDir(regManager registry.Manager) AbsPath {
-	return appendToAbsPath(m.registriesPath, regManager.RegistrySpecDir())
+func (m *manager) registryDir(regManager registry.Manager) string {
+	return str.AppendToPath(m.registriesPath, regManager.RegistrySpecDir())
 }
 
-func (m *manager) registryPath(regManager registry.Manager) AbsPath {
-	return appendToAbsPath(m.registriesPath, regManager.RegistrySpecFilePath())
+func (m *manager) registryPath(regManager registry.Manager) string {
+	return str.AppendToPath(m.registriesPath, regManager.RegistrySpecFilePath())
 }
 
 func (m *manager) getRegistryManager(registryName string) (registry.Manager, string, error) {
@@ -312,14 +313,13 @@ func (m *manager) getRegistryManagerFor(registryRefSpec *app.RegistryRefSpec) (r
 	return manager, protocol, nil
 }
 
-func (m *manager) registrySpecFromFile(path AbsPath) (*registry.Spec, bool, error) {
-	registrySpecFile := string(path)
-	exists, err := afero.Exists(m.appFS, registrySpecFile)
+func (m *manager) registrySpecFromFile(path string) (*registry.Spec, bool, error) {
+	exists, err := afero.Exists(m.appFS, path)
 	if err != nil {
 		return nil, false, err
 	}
 
-	isDir, err := afero.IsDir(m.appFS, registrySpecFile)
+	isDir, err := afero.IsDir(m.appFS, path)
 	if err != nil {
 		return nil, false, err
 	}
@@ -328,7 +328,7 @@ func (m *manager) registrySpecFromFile(path AbsPath) (*registry.Spec, bool, erro
 	// fine, most filesystems allow you to have a directory and file of
 	// the same name.
 	if exists && !isDir {
-		registrySpecBytes, err := afero.ReadFile(m.appFS, registrySpecFile)
+		registrySpecBytes, err := afero.ReadFile(m.appFS, path)
 		if err != nil {
 			return nil, false, err
 		}
@@ -362,13 +362,13 @@ func (m *manager) getOrCacheRegistry(gh registry.Manager) (*registry.Spec, error
 		// NOTE: We call mkdir after getting the registry spec, since a
 		// network call might fail and leave this half-initialized empty
 		// directory.
-		registrySpecDir := appendToAbsPath(m.registriesPath, gh.RegistrySpecDir())
-		err = m.appFS.MkdirAll(string(registrySpecDir), defaultFolderPermissions)
+		registrySpecDir := str.AppendToPath(m.registriesPath, gh.RegistrySpecDir())
+		err = m.appFS.MkdirAll(registrySpecDir, defaultFolderPermissions)
 		if err != nil {
 			return nil, err
 		}
 
-		err = afero.WriteFile(m.appFS, string(registrySpecFile), registrySpecBytes, defaultFilePermissions)
+		err = afero.WriteFile(m.appFS, registrySpecFile, registrySpecBytes, defaultFilePermissions)
 		if err != nil {
 			return nil, err
 		}
diff --git a/pkg/kubecfg/apply.go b/pkg/kubecfg/apply.go
index 86badd53fd19f5c0324f7bfa323961514d182662..df6b3dcf0de95a0250a1695f2a4ae9abe21bcf37 100644
--- a/pkg/kubecfg/apply.go
+++ b/pkg/kubecfg/apply.go
@@ -5,6 +5,7 @@ import (
 	"fmt"
 	"sort"
 
+	"github.com/ksonnet/ksonnet/utils"
 	log "github.com/sirupsen/logrus"
 	"k8s.io/apimachinery/pkg/api/errors"
 	"k8s.io/apimachinery/pkg/api/meta"
@@ -17,9 +18,6 @@ import (
 	"k8s.io/apimachinery/pkg/util/sets"
 	"k8s.io/client-go/discovery"
 	"k8s.io/client-go/dynamic"
-
-	"github.com/ksonnet/ksonnet/metadata"
-	"github.com/ksonnet/ksonnet/utils"
 )
 
 const (
@@ -51,7 +49,8 @@ type ApplyCmd struct {
 	DryRun bool
 }
 
-func (c ApplyCmd) Run(apiObjects []*unstructured.Unstructured, wd metadata.AbsPath) error {
+// Run applies the components to the designated environment cluster.
+func (c ApplyCmd) Run(apiObjects []*unstructured.Unstructured, wd string) error {
 	dryRunText := ""
 	if c.DryRun {
 		dryRunText = " (dry-run)"
diff --git a/pkg/kubecfg/component.go b/pkg/kubecfg/component.go
index 0c4fb9e994f8c0664f3bc114c39917c741504043..88a275507699ec30961fa224c55f0ecb9cfa6125 100644
--- a/pkg/kubecfg/component.go
+++ b/pkg/kubecfg/component.go
@@ -21,7 +21,7 @@ import (
 	"sort"
 	"strings"
 
-	"github.com/ksonnet/ksonnet/utils"
+	str "github.com/ksonnet/ksonnet/strings"
 )
 
 const (
@@ -85,7 +85,7 @@ func printComponents(out io.Writer, components []string) (string, error) {
 		rows = append(rows, []string{component})
 	}
 
-	formatted, err := utils.PadRows(rows)
+	formatted, err := str.PadRows(rows)
 	if err != nil {
 		return "", err
 	}
diff --git a/pkg/kubecfg/env.go b/pkg/kubecfg/env.go
index 67e2643980af2704272818457b3e54b5a5f134b9..c990fda88d3fda2e6a0d4414eebe0ced8abd60bb 100644
--- a/pkg/kubecfg/env.go
+++ b/pkg/kubecfg/env.go
@@ -23,29 +23,21 @@ import (
 
 	"github.com/ksonnet/ksonnet/metadata/app"
 
-	log "github.com/sirupsen/logrus"
-
 	"github.com/ksonnet/ksonnet/metadata"
-	"github.com/ksonnet/ksonnet/utils"
+	str "github.com/ksonnet/ksonnet/strings"
 )
 
 type EnvAddCmd struct {
 	name      string
 	server    string
 	namespace string
+	spec      string
 
-	spec    metadata.ClusterSpec
 	manager metadata.Manager
 }
 
 func NewEnvAddCmd(name, server, namespace, specFlag string, manager metadata.Manager) (*EnvAddCmd, error) {
-	spec, err := metadata.ParseClusterSpec(specFlag)
-	if err != nil {
-		return nil, err
-	}
-	log.Debugf("Generating ksonnetLib data with spec: %s", specFlag)
-
-	return &EnvAddCmd{name: name, server: server, namespace: namespace, spec: spec, manager: manager}, nil
+	return &EnvAddCmd{name: name, server: server, namespace: namespace, spec: specFlag, manager: manager}, nil
 }
 
 func (c *EnvAddCmd) Run() error {
@@ -114,7 +106,7 @@ func (c *EnvListCmd) Run(out io.Writer) error {
 		}
 	}
 
-	formattedEnvsList, err := utils.PadRows(rows)
+	formattedEnvsList, err := str.PadRows(rows)
 	if err != nil {
 		return err
 	}
diff --git a/pkg/kubecfg/init.go b/pkg/kubecfg/init.go
index 9189374b748a390d0035fdfcdb73bd3c65725362..f61ddea0b07c7d944492feccb5dc74c4ec00a5d7 100644
--- a/pkg/kubecfg/init.go
+++ b/pkg/kubecfg/init.go
@@ -6,27 +6,19 @@ import (
 )
 
 type InitCmd struct {
-	name      string
-	rootPath  metadata.AbsPath
-	spec      metadata.ClusterSpec
-	serverURI *string
-	namespace *string
+	name        string
+	rootPath    string
+	k8sSpecFlag *string
+	serverURI   *string
+	namespace   *string
 }
 
-func NewInitCmd(name string, rootPath metadata.AbsPath, specFlag string, serverURI, namespace *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{name: name, rootPath: rootPath, spec: spec, serverURI: serverURI, namespace: namespace}, nil
+func NewInitCmd(name, rootPath string, k8sSpecFlag, serverURI, namespace *string) (*InitCmd, error) {
+	return &InitCmd{name: name, rootPath: rootPath, k8sSpecFlag: k8sSpecFlag, serverURI: serverURI, namespace: namespace}, nil
 }
 
 func (c *InitCmd) Run() error {
-	_, err := metadata.Init(c.name, c.rootPath, c.spec, c.serverURI, c.namespace)
+	_, err := metadata.Init(c.name, c.rootPath, c.k8sSpecFlag, c.serverURI, c.namespace)
 	if err == nil {
 		log.Info("ksonnet app successfully created! Next, try creating a component with `ks generate`.")
 	}
diff --git a/pkg/kubecfg/param.go b/pkg/kubecfg/param.go
index f52c677131c03302838246f68dc504e8702b27e8..bcebf13502ff2482ca298fc1b5faa4a47008250d 100644
--- a/pkg/kubecfg/param.go
+++ b/pkg/kubecfg/param.go
@@ -24,7 +24,7 @@ import (
 	"strings"
 
 	param "github.com/ksonnet/ksonnet/metadata/params"
-	"github.com/ksonnet/ksonnet/utils"
+	str "github.com/ksonnet/ksonnet/strings"
 
 	"github.com/fatih/color"
 	log "github.com/sirupsen/logrus"
@@ -167,7 +167,7 @@ func outputParamsFor(component string, params param.Params, out io.Writer) error
 		rows = append(rows, []string{k, params[k]})
 	}
 
-	formatted, err := utils.PadRows(rows)
+	formatted, err := str.PadRows(rows)
 	if err != nil {
 		return err
 	}
@@ -194,7 +194,7 @@ func outputParams(params map[string]param.Params, out io.Writer) error {
 		}
 	}
 
-	formatted, err := utils.PadRows(rows)
+	formatted, err := str.PadRows(rows)
 	if err != nil {
 		return err
 	}
diff --git a/pkg/kubecfg/shared.go b/pkg/kubecfg/shared.go
index dba80a948e5488d566cc92d4d43c0f3005e75889..57efd0ed9bf34dd9bd5d0441de1cf6e815ef766e 100644
--- a/pkg/kubecfg/shared.go
+++ b/pkg/kubecfg/shared.go
@@ -26,7 +26,6 @@ func manager() (metadata.Manager, error) {
 	if err != nil {
 		return nil, err
 	}
-	appRoot := metadata.AbsPath(appDir)
 
-	return metadata.Find(appRoot)
+	return metadata.Find(appDir)
 }
diff --git a/prototype/specification.go b/prototype/specification.go
index 056ccbe136d62c617dd1553a75311be2df61fbb3..2302b63591a7ca966b2fa3f04258e19c44cf511e 100644
--- a/prototype/specification.go
+++ b/prototype/specification.go
@@ -8,7 +8,7 @@ import (
 	"strings"
 
 	"github.com/blang/semver"
-	"github.com/ksonnet/ksonnet/utils"
+	str "github.com/ksonnet/ksonnet/strings"
 	"github.com/pkg/errors"
 	log "github.com/sirupsen/logrus"
 )
@@ -173,7 +173,7 @@ func (ss SpecificationSchemas) String() string {
 		rows = append(rows, []string{proto.Name, proto.Template.ShortDescription})
 	}
 
-	formatted, err := utils.PadRows(rows)
+	formatted, err := str.PadRows(rows)
 	if err != nil {
 		log.Errorf("Failed to print spec rows:\n%v", err)
 	}
diff --git a/utils/strings.go b/strings/strings.go
similarity index 90%
rename from utils/strings.go
rename to strings/strings.go
index d239da447d6e9d9b802cebed2d2148ea7b03d46d..0edd61810b5fad96d740e2de114c6102c3806f20 100644
--- a/utils/strings.go
+++ b/strings/strings.go
@@ -1,4 +1,4 @@
-// Copyright 2017 The kubecfg authors
+// Copyright 2017 The ksonnet authors
 //
 //
 //    Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,11 +13,12 @@
 //    See the License for the specific language governing permissions and
 //    limitations under the License.
 
-package utils
+package strings
 
 import (
 	"bytes"
 	"fmt"
+	"path"
 	"strings"
 
 	"github.com/PuerkitoBio/purell"
@@ -107,3 +108,9 @@ func PadRows(rows [][]string) (string, error) {
 
 	return buf.String(), nil
 }
+
+// AppendToPath appends one or more paths to the specified original path.
+func AppendToPath(originalPath string, toAppend ...string) string {
+	paths := append([]string{originalPath}, toAppend...)
+	return path.Join(paths...)
+}
diff --git a/utils/strings_test.go b/strings/strings_test.go
similarity index 83%
rename from utils/strings_test.go
rename to strings/strings_test.go
index d002d70f6d4237f021519b59ffea63e1fa5cd6d2..7f9e47a6e3e34a2f54148d3256eefe50e8aeba36 100644
--- a/utils/strings_test.go
+++ b/strings/strings_test.go
@@ -13,7 +13,7 @@
 //    See the License for the specific language governing permissions and
 //    limitations under the License.
 
-package utils
+package strings
 
 import (
 	"fmt"
@@ -182,3 +182,32 @@ Hi World
 		require.EqualValues(t, test.expected, padded)
 	}
 }
+
+func TestAppendToPath(t *testing.T) {
+	tests := []struct {
+		originalPath string
+		toAppend     string
+		expected     string
+	}{
+		{
+			originalPath: "host/path/",
+			toAppend:     "appended",
+			expected:     "host/path/appended",
+		},
+		{
+			originalPath: "host/path",
+			toAppend:     "appended/",
+			expected:     "host/path/appended",
+		},
+		{
+			originalPath: "host/path/",
+			toAppend:     "//appended//",
+			expected:     "host/path/appended",
+		},
+	}
+	for _, test := range tests {
+		result := AppendToPath(test.originalPath, test.toAppend)
+
+		require.EqualValues(t, test.expected, result)
+	}
+}
diff --git a/testdata/testapp/app.yaml b/testdata/testapp/app.yaml
index 2240d9f397ac0e313c04df9a3ee9cdf8fecf1912..0f8e9878e3d922653b3b389135316695a5e6af87 100644
--- a/testdata/testapp/app.yaml
+++ b/testdata/testapp/app.yaml
@@ -13,6 +13,6 @@ environments:
     destinations:
     - namespace: foo
       server: foo
-    k8sVersion: "1.8.1"
+    k8sVersion: "v1.8.1"
     path: default
 version: 0.0.1
diff --git a/testdata/testapp/lib/v1.8.1/.keep b/testdata/testapp/lib/v1.8.1/.keep
new file mode 100644
index 0000000000000000000000000000000000000000..9470d4088dfef16c4eeb7d5e845e86a63e78627a
--- /dev/null
+++ b/testdata/testapp/lib/v1.8.1/.keep
@@ -0,0 +1,4 @@
+Git will not check in empty directories.
+
+This directory is needed for testing ks project structure. This file exists so
+that its parent directory can be checked in.