diff --git a/env/rename.go b/env/rename.go
index 51880a5154357e4a9eeef985aee822a36710194d..753c803a951c692688a2e7463c4ac42424623b34 100644
--- a/env/rename.go
+++ b/env/rename.go
@@ -47,46 +47,13 @@ func (r *renamer) Rename(from, to string) error {
 		return err
 	}
 
-	pathFrom := envPath(r.AppRoot, from)
-	pathTo := envPath(r.AppRoot, to)
-
-	exists, err := afero.DirExists(r.Fs, pathFrom)
-	if err != nil {
-		return err
-	}
-
-	if !exists {
-		return errors.Errorf("environment directory for %q does not exist", from)
-	}
-
 	log.Infof("Setting environment name from %q to %q", from, to)
 
-	current, err := Retrieve(r.App, from)
-	if err != nil {
-		return err
-	}
-
-	update := &app.EnvironmentSpec{
-		Destination: &app.EnvironmentDestinationSpec{
-			Namespace: current.Destination.Namespace(),
-			Server:    current.Destination.Server(),
-		},
-		KubernetesVersion: current.KubernetesVersion,
-		Targets:           current.Targets,
-		Path:              to,
-	}
-
-	k8sSpecFlag := fmt.Sprintf("version:%s", current.KubernetesVersion)
-
-	if err = r.App.AddEnvironment(from, k8sSpecFlag, update); err != nil {
-		return err
-	}
-
-	if err = moveDir(r.Fs, pathFrom, pathTo); err != nil {
+	if err := r.App.RenameEnvironment(from, to); err != nil {
 		return err
 	}
 
-	if err = cleanEmptyDirs(r.Fs, r.AppRoot); err != nil {
+	if err := cleanEmptyDirs(r.Fs, r.AppRoot); err != nil {
 		return errors.Wrap(err, "clean empty directories")
 	}
 
diff --git a/env/rename_test.go b/env/rename_test.go
index a490275d4ca18466ac122042baed635e7facf975..abb410f0cb0cdfd4d1ebeb9a8155ca049b2e43af 100644
--- a/env/rename_test.go
+++ b/env/rename_test.go
@@ -3,12 +3,9 @@ package env
 import (
 	"testing"
 
-	"github.com/stretchr/testify/mock"
-
 	"github.com/spf13/afero"
 	"github.com/stretchr/testify/require"
 
-	"github.com/ksonnet/ksonnet/metadata/app"
 	"github.com/ksonnet/ksonnet/metadata/app/mocks"
 )
 
@@ -16,18 +13,7 @@ func TestRename(t *testing.T) {
 	withEnv(t, func(fs afero.Fs) {
 		appMock := &mocks.App{}
 
-		envSpec := &app.EnvironmentSpec{
-			Path:              "env1",
-			Destination:       &app.EnvironmentDestinationSpec{Namespace: "default", Server: "http://example.com"},
-			KubernetesVersion: "v1.9.2",
-		}
-		appMock.On("Environment", "env1").Return(envSpec, nil)
-
-		appMock.On(
-			"AddEnvironment",
-			"env1",
-			"version:v1.9.2",
-			mock.AnythingOfType("*app.EnvironmentSpec")).Return(nil)
+		appMock.On("RenameEnvironment", "env1", "env1-updated").Return(nil)
 
 		config := RenameConfig{
 			App:     appMock,
@@ -35,12 +21,7 @@ func TestRename(t *testing.T) {
 			Fs:      fs,
 		}
 
-		checkExists(t, fs, "/environments/env1")
-
 		err := Rename("env1", "env1-updated", config)
 		require.NoError(t, err)
-
-		checkNotExists(t, fs, "/environments/env1")
-		checkExists(t, fs, "/environments/env1-updated/main.jsonnet")
 	})
 }
diff --git a/metadata/app/app.go b/metadata/app/app.go
index 87a79a728130e9d1026af61aa49f996b1c7b89c1..9a30f2856f28ae90a33ef53d8f9a8d1664b299ee 100644
--- a/metadata/app/app.go
+++ b/metadata/app/app.go
@@ -3,6 +3,7 @@ package app
 import (
 	"os"
 	"path/filepath"
+	"sort"
 
 	"github.com/ksonnet/ksonnet/metadata/lib"
 	"github.com/pkg/errors"
@@ -40,6 +41,7 @@ type App interface {
 	LibPath(envName string) (string, error)
 	Init() error
 	Registries() RegistryRefSpecs
+	RenameEnvironment(from, to string) error
 	RemoveEnvironment(name string) error
 	Upgrade(dryRun bool) error
 }
@@ -61,7 +63,7 @@ func Load(fs afero.Fs, appRoot string) (App, error) {
 	}
 }
 
-func updateLibData(fs afero.Fs, k8sSpecFlag string, libPath string, useVersionPath bool) (string, error) {
+func updateLibData(fs afero.Fs, k8sSpecFlag, libPath string, useVersionPath bool) (string, error) {
 	lm, err := lib.NewManager(k8sSpecFlag, fs, libPath)
 	if err != nil {
 		return "", err
@@ -79,6 +81,88 @@ func app010LibPath(root string) string {
 }
 
 // StubUpdateLibData always returns no error.
-func StubUpdateLibData(fs afero.Fs, k8sSpecFlag string, libPath string, useVersionPath bool) (string, error) {
+func StubUpdateLibData(fs afero.Fs, k8sSpecFlag, libPath string, useVersionPath bool) (string, error) {
 	return "v1.8.7", nil
 }
+
+func moveEnvironment(fs afero.Fs, root, from, to string) error {
+	toPath := filepath.Join(root, EnvironmentDirName, to)
+
+	exists, err := afero.Exists(fs, filepath.Join(toPath, "main.jsonnet"))
+	if err != nil {
+		return err
+	}
+
+	if exists {
+		return errors.Errorf("unable to rename %q because %q exists", from, to)
+	}
+
+	fromPath := filepath.Join(root, EnvironmentDirName, from)
+	exists, err = afero.Exists(fs, fromPath)
+	if err != nil {
+		return err
+	}
+
+	if !exists {
+		return errors.Errorf("environment %q does not exist", from)
+	}
+
+	// create to directory
+	if err = fs.MkdirAll(toPath, DefaultFolderPermissions); err != nil {
+		return err
+	}
+
+	fis, err := afero.ReadDir(fs, fromPath)
+	if err != nil {
+		return err
+	}
+
+	for _, fi := range fis {
+		if fi.IsDir() && fi.Name() != ".metadata" {
+			continue
+		}
+
+		oldPath := filepath.Join(fromPath, fi.Name())
+		newPath := filepath.Join(toPath, fi.Name())
+		if err := fs.Rename(oldPath, newPath); err != nil {
+			return err
+		}
+	}
+
+	return cleanEnv(fs, root)
+}
+
+func cleanEnv(fs afero.Fs, root string) error {
+	var dirs []string
+
+	envDir := filepath.Join(root, EnvironmentDirName)
+	err := afero.Walk(fs, envDir, func(path string, fi os.FileInfo, err error) error {
+		if !fi.IsDir() {
+			return nil
+		}
+
+		dirs = append(dirs, path)
+		return nil
+	})
+
+	if err != nil {
+		return err
+	}
+
+	sort.Sort(sort.Reverse(sort.StringSlice(dirs)))
+
+	for _, dir := range dirs {
+		fis, err := afero.ReadDir(fs, dir)
+		if err != nil {
+			return err
+		}
+
+		if len(fis) == 0 {
+			if err := fs.RemoveAll(dir); err != nil {
+				return err
+			}
+		}
+	}
+
+	return nil
+}
diff --git a/metadata/app/app001.go b/metadata/app/app001.go
index 345c1717f07a7b9cb2f700a242e007041e8b8b66..200696f86789c63844ad75feb1a41b2b0909d2dc 100644
--- a/metadata/app/app001.go
+++ b/metadata/app/app001.go
@@ -76,6 +76,11 @@ func (a *App001) AddEnvironment(name, k8sSpecFlag string, spec *EnvironmentSpec)
 	return err
 }
 
+// RenameEnvironment renames environments.
+func (a *App001) RenameEnvironment(from, to string) error {
+	return moveEnvironment(a.fs, a.root, from, to)
+}
+
 // Registries returns application registries.
 func (a *App001) Registries() RegistryRefSpecs {
 	return a.spec.Registries
diff --git a/metadata/app/app001_test.go b/metadata/app/app001_test.go
index 9e7205159d6f8362c38b39aa318f5f6b49ba2aaa..72f0f145ffd2a8c5f059ad44d4434c34d8f8b82c 100644
--- a/metadata/app/app001_test.go
+++ b/metadata/app/app001_test.go
@@ -2,14 +2,80 @@ package app
 
 import (
 	"bytes"
+	"io/ioutil"
 	"os"
 	"path/filepath"
 	"testing"
 
 	"github.com/spf13/afero"
+	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 )
 
+func TestApp001_RenameEnvironment(t *testing.T) {
+	cases := []struct {
+		name           string
+		from           string
+		to             string
+		shouldExist    []string
+		shouldNotExist []string
+	}{
+		{
+			name: "rename",
+			from: "default",
+			to:   "renamed",
+			shouldExist: []string{
+				"/environments/renamed/.metadata",
+				"/environments/renamed/spec.json",
+			},
+			shouldNotExist: []string{
+				"/environments/default",
+			},
+		},
+		{
+			name: "rename to nested",
+			from: "default",
+			to:   "default/nested",
+			shouldExist: []string{
+				"/environments/default/nested/.metadata",
+				"/environments/default/nested/spec.json",
+			},
+			shouldNotExist: []string{
+				"/environments/default/.metadata",
+			},
+		},
+	}
+
+	for _, tc := range cases {
+		withApp001Fs(t, "app001_app.yaml", func(fs afero.Fs) {
+			t.Run(tc.name, func(t *testing.T) {
+				app, err := NewApp001(fs, "/")
+				require.NoError(t, err)
+
+				err = app.RenameEnvironment(tc.from, tc.to)
+				require.NoError(t, err)
+
+				for _, p := range tc.shouldExist {
+					checkExist(t, fs, p)
+				}
+
+				for _, p := range tc.shouldNotExist {
+					checkNotExist(t, fs, p)
+				}
+
+				app.load()
+				require.NoError(t, err)
+
+				_, err = app.Environment(tc.from)
+				assert.Error(t, err)
+
+				_, err = app.Environment(tc.to)
+				assert.NoError(t, err)
+			})
+		})
+	}
+}
+
 func TestApp001_Environments(t *testing.T) {
 	withApp001Fs(t, "app001_app.yaml", func(fs afero.Fs) {
 		app, err := NewApp001(fs, "/")
@@ -171,7 +237,12 @@ func withApp001Fs(t *testing.T, appName string, fn func(fs afero.Fs)) {
 		LibUpdater = ogLibUpdater
 	}()
 
-	fs := afero.NewMemMapFs()
+	dir, err := ioutil.TempDir("", "")
+	require.NoError(t, err)
+
+	defer os.RemoveAll(dir)
+
+	fs := afero.NewBasePathFs(afero.NewOsFs(), dir)
 
 	envDirs := []string{
 		"default",
diff --git a/metadata/app/app010.go b/metadata/app/app010.go
index 4fb5466093651d0d1b59b11b4d5968f323a7aca3..a6fb27ae17e689e6fb42f97919b3317c81cad4d6 100644
--- a/metadata/app/app010.go
+++ b/metadata/app/app010.go
@@ -102,6 +102,24 @@ func (a *App010) AddEnvironment(name, k8sSpecFlag string, spec *EnvironmentSpec)
 	return a.save()
 }
 
+// RenameEnvironment renames environments.
+func (a *App010) RenameEnvironment(from, to string) error {
+	if err := moveEnvironment(a.fs, a.root, from, to); err != nil {
+		return err
+	}
+
+	if err := a.load(); err != nil {
+		return err
+	}
+
+	a.spec.Environments[to] = a.spec.Environments[from]
+	delete(a.spec.Environments, from)
+
+	a.spec.Environments[to].Path = to
+
+	return a.save()
+}
+
 // Registries returns application registries.
 func (a *App010) Registries() RegistryRefSpecs {
 	return a.spec.Registries
diff --git a/metadata/app/app010_test.go b/metadata/app/app010_test.go
index 14d6eacd74236e7ef5c5cb0d33ac2dc87e8e8a37..8451ffeb6ade60291dfadcad4898b99aff21607d 100644
--- a/metadata/app/app010_test.go
+++ b/metadata/app/app010_test.go
@@ -2,13 +2,85 @@ package app
 
 import (
 	"io/ioutil"
+	"os"
 	"path/filepath"
 	"testing"
 
 	"github.com/spf13/afero"
+	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 )
 
+func TestApp010_RenameEnvironment(t *testing.T) {
+	cases := []struct {
+		name           string
+		from           string
+		to             string
+		shouldExist    []string
+		shouldNotExist []string
+	}{
+		{
+			name: "rename",
+			from: "default",
+			to:   "renamed",
+			shouldExist: []string{
+				"/environments/renamed/main.jsonnet",
+			},
+			shouldNotExist: []string{
+				"/environments/default",
+			},
+		},
+		{
+			name: "rename to nested",
+			from: "default",
+			to:   "default/nested",
+			shouldExist: []string{
+				"/environments/default/nested/main.jsonnet",
+			},
+			shouldNotExist: []string{
+				"/environments/default/main.jsonnet",
+			},
+		},
+		{
+			name: "un-nest",
+			from: "us-east/test",
+			to:   "us-east",
+			shouldExist: []string{
+				"/environments/us-east/main.jsonnet",
+			},
+			shouldNotExist: []string{
+				"/environments/us-east/test",
+			},
+		},
+	}
+
+	for _, tc := range cases {
+		withApp010Fs(t, "app010_app.yaml", func(fs afero.Fs) {
+			t.Run(tc.name, func(t *testing.T) {
+				app, err := NewApp010(fs, "/")
+				require.NoError(t, err)
+
+				err = app.RenameEnvironment(tc.from, tc.to)
+				require.NoError(t, err)
+
+				for _, p := range tc.shouldExist {
+					checkExist(t, fs, p)
+				}
+
+				for _, p := range tc.shouldNotExist {
+					checkNotExist(t, fs, p)
+				}
+
+				_, err = app.Environment(tc.from)
+				assert.Error(t, err)
+
+				_, err = app.Environment(tc.to)
+				assert.NoError(t, err)
+			})
+		})
+	}
+}
+
 func TestApp0101_Environments(t *testing.T) {
 	withApp010Fs(t, "app010_app.yaml", func(fs afero.Fs) {
 		app, err := NewApp010(fs, "/")
@@ -28,21 +100,24 @@ func TestApp0101_Environments(t *testing.T) {
 					Namespace: "some-namespace",
 					Server:    "http://example.com",
 				},
-				Path: "us-east/test",
+				KubernetesVersion: "v1.7.0",
+				Path:              "us-east/test",
 			},
 			"us-west/test": &EnvironmentSpec{
 				Destination: &EnvironmentDestinationSpec{
 					Namespace: "some-namespace",
 					Server:    "http://example.com",
 				},
-				Path: "us-west/test",
+				KubernetesVersion: "v1.7.0",
+				Path:              "us-west/test",
 			},
 			"us-west/prod": &EnvironmentSpec{
 				Destination: &EnvironmentDestinationSpec{
 					Namespace: "some-namespace",
 					Server:    "http://example.com",
 				},
-				Path: "us-west/prod",
+				KubernetesVersion: "v1.7.0",
+				Path:              "us-west/prod",
 			},
 		}
 		envs, err := app.Environments()
@@ -148,22 +223,30 @@ func withApp010Fs(t *testing.T, appName string, fn func(fs afero.Fs)) {
 		LibUpdater = ogLibUpdater
 	}()
 
-	fs := afero.NewMemMapFs()
-	stageFile(t, fs, appName, "/app.yaml")
+	dir, err := ioutil.TempDir("", "")
+	require.NoError(t, err)
 
-	fn(fs)
-}
+	defer os.RemoveAll(dir)
 
-func stageFile(t *testing.T, fs afero.Fs, src, dest string) {
-	in := filepath.Join("testdata", src)
+	fs := afero.NewBasePathFs(afero.NewOsFs(), dir)
 
-	b, err := ioutil.ReadFile(in)
-	require.NoError(t, err)
+	envDirs := []string{
+		"default",
+		"us-east/test",
+		"us-west/test",
+		"us-west/prod",
+	}
 
-	dir := filepath.Dir(dest)
-	err = fs.MkdirAll(dir, 0755)
-	require.NoError(t, err)
+	for _, dir := range envDirs {
+		path := filepath.Join("/environments", dir)
+		err := fs.MkdirAll(path, DefaultFolderPermissions)
+		require.NoError(t, err)
 
-	err = afero.WriteFile(fs, dest, b, 0644)
-	require.NoError(t, err)
+		swaggerPath := filepath.Join(path, "main.jsonnet")
+		stageFile(t, fs, "main.jsonnet", swaggerPath)
+	}
+
+	stageFile(t, fs, appName, "/app.yaml")
+
+	fn(fs)
 }
diff --git a/metadata/app/helpers_test.go b/metadata/app/helpers_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..da40858a39cb2f6d62574411b5e76324ca89d1bc
--- /dev/null
+++ b/metadata/app/helpers_test.go
@@ -0,0 +1,38 @@
+package app
+
+import (
+	"io/ioutil"
+	"path/filepath"
+	"testing"
+
+	"github.com/spf13/afero"
+	"github.com/stretchr/testify/require"
+)
+
+func stageFile(t *testing.T, fs afero.Fs, src, dest string) {
+	in := filepath.Join("testdata", src)
+
+	b, err := ioutil.ReadFile(in)
+	require.NoError(t, err)
+
+	dir := filepath.Dir(dest)
+	err = fs.MkdirAll(dir, 0755)
+	require.NoError(t, err)
+
+	err = afero.WriteFile(fs, dest, b, 0644)
+	require.NoError(t, err)
+}
+
+func checkExist(t *testing.T, fs afero.Fs, path string) {
+	exists, err := afero.Exists(fs, path)
+	require.NoError(t, err)
+
+	require.True(t, exists)
+}
+
+func checkNotExist(t *testing.T, fs afero.Fs, path string) {
+	exists, err := afero.Exists(fs, path)
+	require.NoError(t, err)
+
+	require.False(t, exists)
+}
diff --git a/metadata/app/mocks/App.go b/metadata/app/mocks/App.go
index 898067f2c048b38e0afd84b7980d2e10ed76bb6a..75d4e9056fb1e4dc1f398fa032ce0aa245d4f27c 100644
--- a/metadata/app/mocks/App.go
+++ b/metadata/app/mocks/App.go
@@ -150,6 +150,20 @@ func (_m *App) RemoveEnvironment(name string) error {
 	return r0
 }
 
+// RenameEnvironment provides a mock function with given fields: from, to
+func (_m *App) RenameEnvironment(from string, to string) error {
+	ret := _m.Called(from, to)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(string, string) error); ok {
+		r0 = rf(from, to)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
 // Upgrade provides a mock function with given fields: dryRun
 func (_m *App) Upgrade(dryRun bool) error {
 	ret := _m.Called(dryRun)
diff --git a/metadata/app/testdata/app010_app.yaml b/metadata/app/testdata/app010_app.yaml
index c30e5fe5f2fdcaaccdeaa65cbfe09b7a4778052f..6832564869dc743ef7fd65d20e37fc7947e38205 100644
--- a/metadata/app/testdata/app010_app.yaml
+++ b/metadata/app/testdata/app010_app.yaml
@@ -10,19 +10,19 @@ environments:
     destination:
       namespace: some-namespace
       server: http://example.com
-    k8sVersion: ""
+    k8sVersion: v1.7.0
     path: us-east/test
   us-west/prod:
     destination:
       namespace: some-namespace
       server: http://example.com
-    k8sVersion: ""
+    k8sVersion: v1.7.0
     path: us-west/prod
   us-west/test:
     destination:
       namespace: some-namespace
       server: http://example.com
-    k8sVersion: ""
+    k8sVersion: v1.7.0
     path: us-west/test
 kind: ksonnet.io/app
 name: test-get-envs
diff --git a/metadata/app/testdata/main.jsonnet b/metadata/app/testdata/main.jsonnet
new file mode 100644
index 0000000000000000000000000000000000000000..23010d3d5012f8954d1cb7b4b4ad62af5d56e9e6
--- /dev/null
+++ b/metadata/app/testdata/main.jsonnet
@@ -0,0 +1,7 @@
+local base = import "../base.libsonnet";
+local k = import "k.libsonnet";
+
+base + {
+  // Insert user-specified overrides here. For example if a component is named "nginx-deployment", you might have something like:
+  //   "nginx-deployment"+: k.deployment.mixin.metadata.labels({foo: "bar"})
+}