diff --git a/cmd/diff.go b/cmd/diff.go
index 4d758544f0124ac8559c6d181fbf440f2e09d147..13bf1b8128c4bd27faac1c9f44f953eb71bb8e7b 100644
--- a/cmd/diff.go
+++ b/cmd/diff.go
@@ -321,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
@@ -338,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/init.go b/cmd/init.go
index 778f1f236511ceac72424c3f20cff3873ed1406d..b8361112e8ae4f5df52b37754b01b78e34a444ba 100644
--- a/cmd/init.go
+++ b/cmd/init.go
@@ -77,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/root.go b/cmd/root.go
index d5525c302fa61854acdf3a91c954fdd88e04f746..9dd9f72245b08a3f736b2ba90bcdf20dcb6b0a60 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -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 {
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 212731a43a7495df3c0bba478ceb93e16326e91b..0000000000000000000000000000000000000000
--- a/metadata/clusterspec.go
+++ /dev/null
@@ -1,87 +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":
-		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)
-}
-
-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_test.go b/metadata/component_test.go
index 1d0b27198a8bf4d58f0b53f0e8c668fea9981766..4027284e6c61e037b0975b74c772f2fb01b1a1d9 100644
--- a/metadata/component_test.go
+++ b/metadata/component_test.go
@@ -33,14 +33,11 @@ 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 := 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)
 	}
diff --git a/metadata/environment.go b/metadata/environment.go
index 9b5a565d062f57d7272e1565f168c704d0ac8ab4..73900d805600d38b77aa14bf9732030f0ec41b4c 100644
--- a/metadata/environment.go
+++ b/metadata/environment.go
@@ -22,57 +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
@@ -99,33 +84,10 @@ func (m *manager) createEnvironment(name, server, namespace string, extensionsLi
 		return err
 	}
 
-	metadataPath := str.AppendToPath(envPath, metadataDirName)
-	err = m.appFS.MkdirAll(metadataPath, defaultFolderPermissions)
-	if err != nil {
-		return err
-	}
-
-	log.Infof("Generating environment metadata at path '%s'", envPath)
-
 	metadata := []struct {
 		path string
 		data []byte
 	}{
-		{
-			// schema file
-			str.AppendToPath(metadataPath, schemaFilename),
-			specData,
-		},
-		{
-			// k8s file
-			str.AppendToPath(metadataPath, k8sLibFilename),
-			k8sLibData,
-		},
-		{
-			// extensions file
-			str.AppendToPath(metadataPath, extensionsLibFilename),
-			extensionsLibData,
-		},
 		{
 			// environment base override file
 			str.AppendToPath(envPath, envFileName),
@@ -157,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 {
@@ -391,6 +353,31 @@ func (m *manager) SetEnvironmentParams(env, component string, params param.Param
 	return nil
 }
 
+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 {
@@ -450,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 52a0e4e0a383d2a3e7124a4d51ecc739b45a2aa4..017ef0ef3457b0fde8e80e5e087dad12d6a89947 100644
--- a/metadata/environment_test.go
+++ b/metadata/environment_test.go
@@ -45,13 +45,10 @@ 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)
 
 	reg := newMockRegistryManager("incubator")
-	m, err := initManager(appName, appName, 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)
 	}
diff --git a/metadata/interface.go b/metadata/interface.go
index b9246357feedec2b3d489a5021e420572e02c3ac..9fe0996a4d33ce15ffcce6bddde62db971aea3ee 100644
--- a/metadata/interface.go
+++ b/metadata/interface.go
@@ -38,7 +38,7 @@ var defaultFilePermissions = os.FileMode(0644)
 type Manager interface {
 	Root() string
 	LibPaths() (libPath, vendorPath string)
-	EnvPaths(env string) (metadataPath, mainPath, paramsPath string)
+	EnvPaths(env string) (libPath, mainPath, paramsPath string, err error)
 
 	// Components API.
 	ComponentPaths() ([]string, error)
@@ -58,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)
@@ -84,10 +84,8 @@ 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, rootPath string, 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 (
@@ -104,24 +102,7 @@ func Init(name, rootPath string, spec ClusterSpec, serverURI, namespace *string)
 		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 f4c2420adf9001c921fe620507c2159a90f61649..6c751d4545ce17ed9effe5b06b74c2002f3313fc 100644
--- a/metadata/manager.go
+++ b/metadata/manager.go
@@ -21,7 +21,6 @@ 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"
@@ -99,30 +98,12 @@ func findManager(p string, appFS afero.Fs) (*manager, error) {
 	}
 }
 
-func initManager(name, rootPath string, 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 {
@@ -150,7 +131,7 @@ func initManager(name, rootPath string, spec ClusterSpec, serverURI, namespace *
 
 	// 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)
 		}
@@ -203,19 +184,6 @@ func (m *manager) LibPaths() (libPath, vendorPath string) {
 	return m.libPath, m.vendorPath
 }
 
-func (m *manager) EnvPaths(env string) (metadataPath, mainPath, paramsPath string) {
-	envPath := str.AppendToPath(m.environmentsPath, env)
-
-	// .metadata directory
-	metadataPath = str.AppendToPath(envPath, metadataDirName)
-	// main.jsonnet file
-	mainPath = str.AppendToPath(envPath, envFileName)
-	// params.libsonnet file
-	paramsPath = str.AppendToPath(envPath, componentParamsFile)
-
-	return
-}
-
 func (m *manager) createUserDirTree() error {
 	dirPaths := []string{
 		m.userKsonnetRootPath,
diff --git a/metadata/manager_test.go b/metadata/manager_test.go
index 0a8728f6a2585e38a283c6d5491850b52ddfbb28..a8f02018acbd50f6fee94139c6fa520eaaa3de12 100644
--- a/metadata/manager_test.go
+++ b/metadata/manager_test.go
@@ -55,14 +55,11 @@ 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 := "/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)
 	}
@@ -110,31 +107,6 @@ func TestInitSuccess(t *testing.T) {
 
 	// Verify contents of metadata.
 	envPath := str.AppendToPath(appPath, environmentsDir)
-	metadataPath := str.AppendToPath(appPath, defaultEnvDir, metadataDirName)
-
-	schemaPath := str.AppendToPath(metadataPath, schemaFilename)
-	bytes, err := afero.ReadFile(testFS, 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(metadataPath, k8sLibFilename)
-	k8sLibBytes, err := afero.ReadFile(testFS, 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 := str.AppendToPath(metadataPath, extensionsLibFilename)
-	extensionsLibBytes, err := afero.ReadFile(testFS, 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)
-	}
 
 	componentParamsPath := str.AppendToPath(appPath, componentsDir, componentParamsFile)
 	componentParamsBytes, err := afero.ReadFile(testFS, componentParamsPath)
@@ -179,14 +151,11 @@ 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 := "/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)
 	}
@@ -224,16 +193,15 @@ func TestLibPaths(t *testing.T) {
 
 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 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 mainPath != expectedMainPath {
 		t.Fatalf("Expected environment main path to be:\n  '%s'\n, got:\n  '%s'", expectedMainPath, mainPath)
 	}
@@ -256,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 := "/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/pkg/kubecfg/env.go b/pkg/kubecfg/env.go
index a68a8f161766a348eb1a02de96dd0cfeb51c1505..c990fda88d3fda2e6a0d4414eebe0ced8abd60bb 100644
--- a/pkg/kubecfg/env.go
+++ b/pkg/kubecfg/env.go
@@ -23,8 +23,6 @@ import (
 
 	"github.com/ksonnet/ksonnet/metadata/app"
 
-	log "github.com/sirupsen/logrus"
-
 	"github.com/ksonnet/ksonnet/metadata"
 	str "github.com/ksonnet/ksonnet/strings"
 )
@@ -33,19 +31,13 @@ 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 {
diff --git a/pkg/kubecfg/init.go b/pkg/kubecfg/init.go
index f6bf91647d965136337e512746a0cba8f3dccc18..f61ddea0b07c7d944492feccb5dc74c4ec00a5d7 100644
--- a/pkg/kubecfg/init.go
+++ b/pkg/kubecfg/init.go
@@ -6,27 +6,19 @@ import (
 )
 
 type InitCmd struct {
-	name      string
-	rootPath  string
-	spec      metadata.ClusterSpec
-	serverURI *string
-	namespace *string
+	name        string
+	rootPath    string
+	k8sSpecFlag *string
+	serverURI   *string
+	namespace   *string
 }
 
-func NewInitCmd(name, rootPath, 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/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.