Przeglądaj źródła

Merge pull request #891 from anukul/rename-env-group

Allow renaming an environment group
abelanger5 4 lat temu
rodzic
commit
4691dab5ca

+ 141 - 36
dashboard/src/main/home/cluster-dashboard/env-groups/ExpandedEnvGroup.tsx

@@ -6,6 +6,7 @@ import _ from "lodash";
 
 import { ChartType, StorageType, ClusterType } from "shared/types";
 import { Context } from "shared/Context";
+import { isAlphanumeric } from "shared/common";
 import api from "shared/api";
 
 import SaveButton from "components/SaveButton";
@@ -15,6 +16,7 @@ import TabRegion from "components/TabRegion";
 import EnvGroupArray, { KeyValueType } from "./EnvGroupArray";
 import Heading from "components/values-form/Heading";
 import Helper from "components/values-form/Helper";
+import InputRow from "components/values-form/InputRow";
 import { withAuth, WithAuthProps } from "shared/auth/AuthorizationHoc";
 
 type PropsType = WithAuthProps & {
@@ -30,8 +32,15 @@ type StateType = {
   showDeleteOverlay: boolean;
   deleting: boolean;
   saveValuesStatus: string | null;
-  envVariables: KeyValueType[];
+  envGroup: EnvGroup;
   tabOptions: { value: string; label: string }[];
+  newEnvGroupName: string;
+};
+
+type EnvGroup = {
+  name: string;
+  timestamp: string;
+  variables: KeyValueType[];
 };
 
 const tabOptions = [
@@ -46,29 +55,48 @@ class ExpandedEnvGroup extends Component<PropsType, StateType> {
     showDeleteOverlay: false,
     deleting: false,
     saveValuesStatus: null as string | null,
-    envVariables: [] as KeyValueType[],
+    envGroup: {
+      name: null as string,
+      timestamp: null as string,
+      variables: [] as KeyValueType[],
+    },
     tabOptions: [
       { value: "environment", label: "Environment Variables" },
       { value: "settings", label: "Settings" },
     ],
+    newEnvGroupName: null as string,
   };
 
-  componentDidMount() {
+  populateEnvGroup = (envGroup: any) => {
+    const {
+      metadata: { name, creationTimestamp: timestamp },
+      data,
+    } = envGroup;
     // parse env group props into values type
-    let envVariables = [] as KeyValueType[];
-    let envGroupData = this.props.envGroup.data;
+    const variables = [] as KeyValueType[];
 
-    for (const key in envGroupData) {
-      envVariables.push({
+    for (const key in data) {
+      variables.push({
         key: key,
-        value: envGroupData[key],
-        hidden: envGroupData[key].includes("PORTERSECRET"),
-        locked: envGroupData[key].includes("PORTERSECRET"),
+        value: data[key],
+        hidden: data[key].includes("PORTERSECRET"),
+        locked: data[key].includes("PORTERSECRET"),
         deleted: false,
       });
     }
 
-    this.setState({ envVariables });
+    this.setState({
+      envGroup: {
+        name,
+        timestamp,
+        variables,
+      },
+      newEnvGroupName: name,
+    });
+  };
+
+  componentDidMount() {
+    this.populateEnvGroup(this.props.envGroup);
 
     // Filter the settings tab options as for now it only shows the delete button.
     // In a future this should be removed and return to a constant if we want to show data
@@ -86,25 +114,49 @@ class ExpandedEnvGroup extends Component<PropsType, StateType> {
     });
   }
 
-  handleUpdateValues = () => {
-    let { envGroup } = this.props;
-    let name = envGroup.metadata.name;
-    let namespace = envGroup.metadata.namespace;
+  handleRename = () => {
+    const { namespace } = this.props;
+    const {
+      envGroup: { name },
+      newEnvGroupName: newName,
+    } = this.state;
 
-    let apiEnvVariables: Record<string, string> = {};
-    let secretEnvVariables: Record<string, string> = {};
+    api
+      .renameConfigMap(
+        "<token>",
+        {
+          name,
+          namespace,
+          new_name: newName,
+        },
+        {
+          id: this.context.currentProject.id,
+          cluster_id: this.props.currentCluster.id,
+        }
+      )
+      .then((res) => {
+        this.populateEnvGroup(res.data);
+      });
+  };
+
+  handleUpdateValues = () => {
+    const { namespace } = this.props;
+    const {
+      envGroup: { name, variables: envVariables },
+    } = this.state;
 
-    let envVariables = this.state.envVariables;
+    const apiEnvVariables: Record<string, string> = {};
+    const secretEnvVariables: Record<string, string> = {};
 
     envVariables
       .filter((envVar: KeyValueType, index: number, self: KeyValueType[]) => {
         // remove any collisions that are marked as deleted and are duplicates, unless they are
         // all delete collisions
-        let numDeleteCollisions = self.reduce((n, _envVar: KeyValueType) => {
+        const numDeleteCollisions = self.reduce((n, _envVar: KeyValueType) => {
           return n + (_envVar.key === envVar.key && envVar.deleted ? 1 : 0);
         }, 0);
 
-        let numCollisions = self.reduce((n, _envVar: KeyValueType) => {
+        const numCollisions = self.reduce((n, _envVar: KeyValueType) => {
           return n + (_envVar.key === envVar.key ? 1 : 0);
         }, 0);
 
@@ -171,9 +223,15 @@ class ExpandedEnvGroup extends Component<PropsType, StateType> {
   };
 
   renderTabContents = () => {
-    let currentTab = this.state.currentTab;
-    let { envGroup, namespace } = this.props;
-    let name = envGroup.metadata.name;
+    const { namespace } = this.props;
+    const {
+      envGroup: { name, variables },
+      newEnvGroupName: newName,
+      currentTab,
+    } = this.state;
+
+    const isEnvGroupNameValid = isAlphanumeric(newName) && newName !== "";
+    const isEnvGroupNameDifferent = newName !== name;
 
     switch (currentTab) {
       case "environment":
@@ -187,8 +245,12 @@ class ExpandedEnvGroup extends Component<PropsType, StateType> {
               </Helper>
               <EnvGroupArray
                 namespace={namespace}
-                values={this.state.envVariables}
-                setValues={(x: any) => this.setState({ envVariables: x })}
+                values={variables}
+                setValues={(x: any) =>
+                  this.setState((prevState) => ({
+                    envGroup: { ...prevState.envGroup, variables: x },
+                  }))
+                }
                 fileUpload={true}
                 secretOption={true}
                 disabled={
@@ -216,6 +278,29 @@ class ExpandedEnvGroup extends Component<PropsType, StateType> {
           <TabWrapper>
             {this.props.isAuthorized("env_group", "", ["get", "delete"]) && (
               <InnerWrapper full={true}>
+                <Heading>Name</Heading>
+                <Subtitle>
+                  <Warning makeFlush={true} highlight={!isEnvGroupNameValid}>
+                    Lowercase letters, numbers, and "-" only.
+                  </Warning>
+                </Subtitle>
+                <DarkMatter antiHeight="-29px" />
+                <InputRow
+                  type="text"
+                  value={newName}
+                  setValue={(x: string) =>
+                    this.setState({ newEnvGroupName: x })
+                  }
+                  placeholder="ex: doctor-scientist"
+                  width="100%"
+                />
+                <Button
+                  color="#616FEEcc"
+                  disabled={!(isEnvGroupNameDifferent && isEnvGroupNameValid)}
+                  onClick={this.handleRename}
+                >
+                  Rename {name}
+                </Button>
                 <Heading>Manage Environment Group</Heading>
                 <Helper>
                   Permanently delete this set of environment variables. This
@@ -235,9 +320,9 @@ class ExpandedEnvGroup extends Component<PropsType, StateType> {
   };
 
   readableDate = (s: string) => {
-    let ts = new Date(s);
-    let date = ts.toLocaleDateString();
-    let time = ts.toLocaleTimeString([], {
+    const ts = new Date(s);
+    const date = ts.toLocaleDateString();
+    const time = ts.toLocaleTimeString([], {
       hour: "numeric",
       minute: "2-digit",
     });
@@ -245,9 +330,10 @@ class ExpandedEnvGroup extends Component<PropsType, StateType> {
   };
 
   handleDeleteEnvGroup = () => {
-    let { envGroup } = this.props;
-    let name = envGroup.metadata.name;
-    let namespace = envGroup.metadata.namespace;
+    const { namespace } = this.props;
+    const {
+      envGroup: { name },
+    } = this.state;
 
     this.setState({ deleting: true });
     api
@@ -280,11 +366,10 @@ class ExpandedEnvGroup extends Component<PropsType, StateType> {
   };
 
   render() {
-    let { closeExpanded } = this.props;
-    let { envGroup } = this.props;
-    let name = envGroup.metadata.name;
-    let timestamp = envGroup.metadata.creationTimestamp;
-    let namespace = envGroup.metadata.namespace;
+    const { namespace, closeExpanded } = this.props;
+    const {
+      envGroup: { name, timestamp },
+    } = this.state;
 
     return (
       <>
@@ -571,3 +656,23 @@ const StyledExpandedChart = styled.div`
     }
   }
 `;
+
+const DarkMatter = styled.div<{ antiHeight?: string }>`
+  width: 100%;
+  margin-top: ${(props) => props.antiHeight || "-15px"};
+`;
+
+const Warning = styled.span<{ highlight: boolean; makeFlush?: boolean }>`
+  color: ${(props) => (props.highlight ? "#f5cb42" : "")};
+  margin-left: ${(props) => (props.makeFlush ? "" : "5px")};
+`;
+
+const Subtitle = styled.div`
+  padding: 11px 0px 16px;
+  font-family: "Work Sans", sans-serif;
+  font-size: 13px;
+  color: #aaaabb;
+  line-height: 1.6em;
+  display: flex;
+  align-items: center;
+`;

+ 13 - 0
dashboard/src/shared/api.tsx

@@ -864,6 +864,18 @@ const updateConfigMap = baseApi<
   return `/api/projects/${id}/k8s/configmap/update?cluster_id=${cluster_id}`;
 });
 
+const renameConfigMap = baseApi<
+  {
+    name: string;
+    namespace: string;
+    new_name: string;
+  },
+  { id: number; cluster_id: number }
+>("POST", (pathParams) => {
+  let { id, cluster_id } = pathParams;
+  return `/api/projects/${id}/k8s/configmap/rename?cluster_id=${cluster_id}`;
+});
+
 const deleteConfigMap = baseApi<
   {
     name: string;
@@ -1052,6 +1064,7 @@ export default {
   rollbackChart,
   uninstallTemplate,
   updateUser,
+  renameConfigMap,
   updateConfigMap,
   upgradeChartValues,
   deleteJob,

+ 6 - 0
internal/forms/k8s.go

@@ -45,6 +45,12 @@ type ConfigMapForm struct {
 	SecretEnvVariables map[string]string `json:"secret_variables"`
 }
 
+type RenameConfigMapForm struct {
+	Name      string `json:"name" form:"required"`
+	Namespace string `json:"namespace" form:"required"`
+	NewName   string `json:"new_name" form:"required"`
+}
+
 type NamespaceForm struct {
 	Name string `json:"name" form:"required"`
 }

+ 9 - 0
internal/kubernetes/agent.go

@@ -210,6 +210,15 @@ func (a *Agent) GetConfigMap(name string, namespace string) (*v1.ConfigMap, erro
 	)
 }
 
+// GetSecret retrieves the secret given its name and namespace
+func (a *Agent) GetSecret(name string, namespace string) (*v1.Secret, error) {
+	return a.Clientset.CoreV1().Secrets(namespace).Get(
+		context.TODO(),
+		name,
+		metav1.GetOptions{},
+	)
+}
+
 // ListConfigMaps simply lists namespaces
 func (a *Agent) ListConfigMaps(namespace string) (*v1.ConfigMapList, error) {
 	return a.Clientset.CoreV1().ConfigMaps(namespace).List(

+ 140 - 46
server/api/k8s_handler.go

@@ -238,7 +238,34 @@ func (app *App) HandleListPodEvents(w http.ResponseWriter, r *http.Request) {
 	}
 }
 
-// HandleCreateConfigMap deletes the pod given the name and namespace.
+func createConfigMap(agent *kubernetes.Agent, configMap *forms.ConfigMapForm) (*v1.ConfigMap, error) {
+	secretData := make(map[string][]byte)
+
+	for key, rawValue := range configMap.SecretEnvVariables {
+		// encodedValue := base64.StdEncoding.EncodeToString([]byte(rawValue))
+
+		// if err != nil {
+		// 	app.handleErrorInternal(err, w)
+		// 	return
+		// }
+
+		secretData[key] = []byte(rawValue)
+	}
+
+	// create secret first
+	if _, err := agent.CreateLinkedSecret(configMap.Name, configMap.Namespace, configMap.Name, secretData); err != nil {
+		return nil, err
+	}
+
+	// add all secret env variables to configmap with value PORTERSECRET_${configmap_name}
+	for key, _ := range configMap.SecretEnvVariables {
+		configMap.EnvVariables[key] = fmt.Sprintf("PORTERSECRET_%s", configMap.Name)
+	}
+
+	return agent.CreateConfigMap(configMap.Name, configMap.Namespace, configMap.EnvVariables)
+}
+
+// HandleCreateConfigMap creates a configmap (and secret) given the name, namespace and variables.
 func (app *App) HandleCreateConfigMap(w http.ResponseWriter, r *http.Request) {
 	vals, err := url.ParseQuery(r.URL.RawQuery)
 	if err != nil {
@@ -271,47 +298,19 @@ func (app *App) HandleCreateConfigMap(w http.ResponseWriter, r *http.Request) {
 		agent, err = kubernetes.GetAgentOutOfClusterConfig(form.OutOfClusterConfig)
 	}
 
-	configMap := &forms.ConfigMapForm{}
+	configMapForm := &forms.ConfigMapForm{}
 
-	if err := json.NewDecoder(r.Body).Decode(configMap); err != nil {
+	if err := json.NewDecoder(r.Body).Decode(configMapForm); err != nil {
 		app.handleErrorFormDecoding(err, ErrUserDecode, w)
 		return
 	}
 
-	secretData := make(map[string][]byte)
-
-	for key, rawValue := range configMap.SecretEnvVariables {
-		// encodedValue := base64.StdEncoding.EncodeToString([]byte(rawValue))
-
-		// if err != nil {
-		// 	app.handleErrorInternal(err, w)
-		// 	return
-		// }
-
-		secretData[key] = []byte(rawValue)
-	}
-
-	// create secret first
-	_, err = agent.CreateLinkedSecret(configMap.Name, configMap.Namespace, configMap.Name, secretData)
-
-	if err != nil {
-		app.handleErrorInternal(err, w)
-		return
-	}
-
-	// add all secret env variables to configmap with value PORTERSECRET_${configmap_name}
-	for key, _ := range configMap.SecretEnvVariables {
-		configMap.EnvVariables[key] = fmt.Sprintf("PORTERSECRET_%s", configMap.Name)
-	}
-
-	_, err = agent.CreateConfigMap(configMap.Name, configMap.Namespace, configMap.EnvVariables)
-
-	if err != nil {
+	if _, err := createConfigMap(agent, configMapForm); err != nil {
 		app.handleErrorInternal(err, w)
 		return
 	}
 
-	if err := json.NewEncoder(w).Encode(configMap); err != nil {
+	if err := json.NewEncoder(w).Encode(configMapForm); err != nil {
 		app.handleErrorFormDecoding(err, ErrEnvDecode, w)
 		return
 	}
@@ -420,7 +419,19 @@ func (app *App) HandleGetConfigMap(w http.ResponseWriter, r *http.Request) {
 	return
 }
 
-// HandleDeleteConfigMap deletes the pod given the name and namespace.
+func deleteConfigMap(agent *kubernetes.Agent, name string, namespace string) error {
+	if err := agent.DeleteLinkedSecret(name, namespace); err != nil {
+		return err
+	}
+
+	if err := agent.DeleteConfigMap(name, namespace); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// HandleDeleteConfigMap deletes the configmap (and secret) given the name and namespace.
 func (app *App) HandleDeleteConfigMap(w http.ResponseWriter, r *http.Request) {
 	vals, err := url.ParseQuery(r.URL.RawQuery)
 	if err != nil {
@@ -453,16 +464,7 @@ func (app *App) HandleDeleteConfigMap(w http.ResponseWriter, r *http.Request) {
 		agent, err = kubernetes.GetAgentOutOfClusterConfig(form.OutOfClusterConfig)
 	}
 
-	err = agent.DeleteLinkedSecret(vals["name"][0], vals["namespace"][0])
-
-	if err != nil {
-		app.handleErrorInternal(err, w)
-		return
-	}
-
-	err = agent.DeleteConfigMap(vals["name"][0], vals["namespace"][0])
-
-	if err != nil {
+	if err := deleteConfigMap(agent, vals["name"][0], vals["namespace"][0]); err != nil {
 		app.handleErrorInternal(err, w)
 		return
 	}
@@ -471,7 +473,7 @@ func (app *App) HandleDeleteConfigMap(w http.ResponseWriter, r *http.Request) {
 	return
 }
 
-// HandleUpdateConfigMap deletes the pod given the name and namespace.
+// HandleUpdateConfigMap updates the configmap (and secret) given the name, namespace and variables.
 func (app *App) HandleUpdateConfigMap(w http.ResponseWriter, r *http.Request) {
 	vals, err := url.ParseQuery(r.URL.RawQuery)
 
@@ -559,6 +561,98 @@ func (app *App) HandleUpdateConfigMap(w http.ResponseWriter, r *http.Request) {
 	return
 }
 
+// HandleRenameConfigMap renames the configmap name given the current name, namespace and new name.
+func (app *App) HandleRenameConfigMap(w http.ResponseWriter, r *http.Request) {
+	vals, err := url.ParseQuery(r.URL.RawQuery)
+
+	if err != nil {
+		app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
+		return
+	}
+
+	// get the filter options
+	form := &forms.K8sForm{
+		OutOfClusterConfig: &kubernetes.OutOfClusterConfig{
+			Repo:              app.Repo,
+			DigitalOceanOAuth: app.DOConf,
+		},
+	}
+
+	form.PopulateK8sOptionsFromQueryParams(vals, app.Repo.Cluster)
+
+	// validate the form
+	if err := app.validator.Struct(form); err != nil {
+		app.handleErrorFormValidation(err, ErrK8sValidate, w)
+		return
+	}
+
+	// create a new agent
+	var agent *kubernetes.Agent
+
+	if app.ServerConf.IsTesting {
+		agent = app.TestAgents.K8sAgent
+	} else {
+		agent, err = kubernetes.GetAgentOutOfClusterConfig(form.OutOfClusterConfig)
+	}
+
+	renameConfigMapForm := &forms.RenameConfigMapForm{}
+
+	if err := json.NewDecoder(r.Body).Decode(renameConfigMapForm); err != nil {
+		app.handleErrorFormDecoding(err, ErrEnvDecode, w)
+		return
+	}
+
+	configMap, err := agent.GetConfigMap(renameConfigMapForm.Name, renameConfigMapForm.Namespace)
+
+	if err != nil {
+		app.handleErrorInternal(err, w)
+		return
+	}
+
+	secret, err := agent.GetSecret(configMap.Name, configMap.Namespace)
+
+	if err != nil {
+		app.handleErrorInternal(err, w)
+		return
+	}
+
+	var decodedSecretData = make(map[string]string)
+	for k, v := range secret.Data {
+		decodedSecretData[k] = string(v)
+	}
+
+	newConfigMapForm := &forms.ConfigMapForm{
+		Name:               renameConfigMapForm.NewName,
+		Namespace:          configMap.Namespace,
+		EnvVariables:       configMap.Data,
+		SecretEnvVariables: decodedSecretData,
+	}
+
+	if newConfigMapForm.Name == configMap.Name {
+		w.WriteHeader(http.StatusBadRequest)
+		return
+	}
+
+	newConfigMap, err := createConfigMap(agent, newConfigMapForm)
+	if err != nil {
+		app.handleErrorInternal(err, w)
+		return
+	}
+
+	if err := deleteConfigMap(agent, configMap.Name, configMap.Namespace); err != nil {
+		app.handleErrorInternal(err, w)
+		return
+	}
+
+	if err := json.NewEncoder(w).Encode(newConfigMap); err != nil {
+		app.handleErrorFormDecoding(err, ErrEnvDecode, w)
+		return
+	}
+
+	w.WriteHeader(http.StatusOK)
+	return
+}
+
 // HandleGetPodLogs returns real-time logs of the pod via websockets
 // TODO: Refactor repeated calls.
 func (app *App) HandleGetPodLogs(w http.ResponseWriter, r *http.Request) {
@@ -855,7 +949,7 @@ func (app *App) HandleListJobsByChart(w http.ResponseWriter, r *http.Request) {
 	}
 }
 
-// HandleDeleteConfigMap deletes the pod given the name and namespace.
+// HandleDeleteJob deletes the job given the name and namespace.
 func (app *App) HandleDeleteJob(w http.ResponseWriter, r *http.Request) {
 	// get path parameters
 	namespace := chi.URLParam(r, "namespace")

+ 14 - 0
server/router/router.go

@@ -1543,6 +1543,20 @@ func New(a *api.App) *chi.Mux {
 				),
 			)
 
+			r.Method(
+				"POST",
+				"/projects/{project_id}/k8s/configmap/rename",
+				auth.DoesUserHaveProjectAccess(
+					auth.DoesUserHaveClusterAccess(
+						requestlog.NewHandler(a.HandleRenameConfigMap, l),
+						mw.URLParam,
+						mw.QueryParam,
+					),
+					mw.URLParam,
+					mw.WriteAccess,
+				),
+			)
+
 			r.Method(
 				"DELETE",
 				"/projects/{project_id}/k8s/jobs/{namespace}/{name}",