From 385da08e76ee60a3dbcaa2b1d52234539eb47785 Mon Sep 17 00:00:00 2001
From: Jessica Yuen <im.jessicayuen@gmail.com>
Date: Thu, 16 Nov 2017 14:35:26 -0800
Subject: [PATCH] Support renaming of envs to parent & child directories

Currently, there are limitations around the file system we are using
that does not easily allow renaming of `us-west/prod` to `us-west`, or
vice versa - `us-west` to `us-west/prod`.

This commit will handle the logic to allow for that by moving the file
contents.
---
 metadata/environment.go      | 81 +++++++++++++++++++++++++++++-------
 metadata/environment_test.go | 36 +++++++++++++++-
 2 files changed, 102 insertions(+), 15 deletions(-)

diff --git a/metadata/environment.go b/metadata/environment.go
index b180e37a..6463f4f9 100644
--- a/metadata/environment.go
+++ b/metadata/environment.go
@@ -47,6 +47,17 @@ const (
 	specFilename   = "spec.json"
 )
 
+var envPaths = []string{
+	// metadata Dir.wh
+	metadataDirName,
+	// environment base override file
+	envFileName,
+	// params file
+	paramsFileName,
+	// spec file
+	specFilename,
+}
+
 // Environment represents all fields of a ksonnet environment
 type Environment struct {
 	Path      string
@@ -292,22 +303,36 @@ func (m *manager) SetEnvironment(name string, desired *Environment) error {
 		if err != nil {
 			return err
 		}
+
 		if exists {
-			return fmt.Errorf("Failed to create environment, directory exists at path: %s", string(pathNew))
-		}
-		// 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))
-		err = m.appFS.MkdirAll(intermediatePath, defaultFolderPermissions)
-		if err != nil {
-			return err
-		}
-		// finally, move the directory
-		err = m.appFS.Rename(string(pathOld), string(pathNew))
-		if err != nil {
-			log.Debugf("Failed to move path '%s' to '%s", string(pathOld), string(pathNew))
-			return err
+			// we know that the desired path is not an environment from
+			// 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)) {
+			// the new directory is a child of the old directory --
+			// rename won't work.
+			err = m.appFS.MkdirAll(string(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))
+			err = m.appFS.MkdirAll(intermediatePath, defaultFolderPermissions)
+			if err != nil {
+				return err
+			}
+			// finally, move the directory
+			err = m.appFS.Rename(string(pathOld), string(pathNew))
+			if err != nil {
+				log.Debugf("Failed to move path '%s' to '%s", string(pathOld), string(pathNew))
+				return err
+			}
 		}
+
 		// clean up any empty parent directory paths
 		err = m.cleanEmptyParentDirs(name)
 		if err != nil {
@@ -414,6 +439,34 @@ func (m *manager) SetEnvironmentParams(env, component string, params param.Param
 	return nil
 }
 
+func (m *manager) tryMvEnvDir(dirPathOld, dirPathNew AbsPath) error {
+	// first ensure none of these paths exists in the new directory
+	for _, p := range envPaths {
+		path := string(appendToAbsPath(dirPathNew, p))
+		if exists, err := afero.Exists(m.appFS, path); err != nil {
+			return err
+		} else if exists {
+			return fmt.Errorf("%s already exists", path)
+		}
+	}
+
+	// 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)))
+		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 {
+		return err
+	} else if empty {
+		return m.appFS.RemoveAll(string(dirPathOld))
+	}
+	return nil
+}
+
 func (m *manager) cleanEmptyParentDirs(name string) error {
 	// clean up any empty parent directory paths
 	log.Debug("Removing empty parent directories, if any")
diff --git a/metadata/environment_test.go b/metadata/environment_test.go
index 5d90a299..128a7526 100644
--- a/metadata/environment_test.go
+++ b/metadata/environment_test.go
@@ -36,8 +36,13 @@ const (
 
 var mockAPIServer = "http://example.com"
 var mockNamespace = "some-namespace"
+var mockEnvs = []string{defaultEnvName, mockEnvName, mockEnvName2, mockEnvName3}
 
 func mockEnvironments(t *testing.T, appName string) *manager {
+	return mockEnvironmentsWith(t, appName, mockEnvs)
+}
+
+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)
@@ -50,7 +55,6 @@ func mockEnvironments(t *testing.T, appName string) *manager {
 		t.Fatalf("Failed to init cluster spec: %v", err)
 	}
 
-	envNames := []string{defaultEnvName, mockEnvName, mockEnvName2, mockEnvName3}
 	for _, env := range envNames {
 		envPath := appendToAbsPath(m.environmentsPath, env)
 		testFS.Mkdir(string(envPath), defaultFolderPermissions)
@@ -226,6 +230,36 @@ func TestSetEnvironment(t *testing.T) {
 	if envSpec.Namespace != set.Namespace {
 		t.Fatalf("Expected namespace to be set to '%s', got: '%s'", set.Namespace, envSpec.Namespace)
 	}
+
+	tests := []struct {
+		appName string
+		nameOld string
+		nameNew string
+	}{
+		// Test changing the name of an env 'us-west' to 'us-west/dev'
+		{
+			"test-set-to-child",
+			"us-west",
+			"us-west/dev",
+		},
+		// Test changing the name of an env 'us-west/dev' to 'us-west'
+		{
+			"test-set-to-parent",
+			"us-west/dev",
+			"us-west",
+		},
+	}
+
+	for _, v := range tests {
+		m = mockEnvironmentsWith(t, v.appName, []string{v.nameOld})
+		err = m.SetEnvironment(v.nameOld, &Environment{Name: v.nameNew})
+		if err != nil {
+			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))
+	}
 }
 
 func TestGenerateOverrideData(t *testing.T) {
-- 
GitLab