Browse Source

support immutable secret values for deployments

Alexander Belanger 5 years ago
parent
commit
e581701a79

+ 0 - 1
cli/cmd/github/release.go

@@ -156,7 +156,6 @@ func (z *ZIPReleaseGetter) getDownloadRegexp() (*regexp.Regexp, error) {
 // // it, and adds the binary to the porter directory
 // func DownloadLatestServerRelease(porterDir string) error {
 // 	releaseURL, staticReleaseURL, err := getLatestReleaseDownloadURL()
-// 	fmt.Println(releaseURL)
 
 // 	if err != nil {
 // 		return err

+ 27 - 3
dashboard/src/components/values-form/KeyValueArray.tsx

@@ -18,6 +18,7 @@ type PropsType = {
   clusterId?: number;
   envLoader?: boolean;
   fileUpload?: boolean;
+  secretOption?: boolean;
 };
 
 type StateType = {
@@ -80,6 +81,16 @@ export default class KeyValueArray extends Component<PropsType, StateType> {
     }
   };
 
+  renderHiddenOption = (hidden: boolean, i: number) => {
+    if (this.props.secretOption && hidden) {
+      return (
+        <HideButton>
+          <i className="material-icons">lock</i>
+        </HideButton>
+      );
+    }
+  };
+
   renderInputList = () => {
     return (
       <>
@@ -97,7 +108,7 @@ export default class KeyValueArray extends Component<PropsType, StateType> {
                   let obj = this.valuesToObject();
                   this.props.setValues(obj);
                 }}
-                disabled={this.props.disabled}
+                disabled={this.props.disabled || entry.value.includes("PORTERSECRET") }
               />
               <Spacer />
               <Input
@@ -111,9 +122,11 @@ export default class KeyValueArray extends Component<PropsType, StateType> {
                   let obj = this.valuesToObject();
                   this.props.setValues(obj);
                 }}
-                disabled={this.props.disabled}
+                disabled={this.props.disabled || entry.value.includes("PORTERSECRET") }
+                type={entry.value.includes("PORTERSECRET") ? "password" : "text"}
               />
               {this.renderDeleteButton(i)}
+              {this.renderHiddenOption(entry.value.includes("PORTERSECRET"), i)}
             </InputWrapper>
           );
         })}
@@ -367,6 +380,17 @@ const DeleteButton = styled.div`
   }
 `;
 
+const HideButton = styled(DeleteButton)`
+  margin-top: -5px;
+  > i {
+    font-size: 19px;
+    cursor: default;
+    :hover {
+      color: #ffffff44;
+    }
+  }
+`
+
 const InputWrapper = styled.div`
   display: flex;
   align-items: center;
@@ -397,4 +421,4 @@ const Label = styled.div`
 const StyledInputArray = styled.div`
   margin-bottom: 15px;
   margin-top: 22px;
-`;
+`;

+ 1 - 0
dashboard/src/components/values-form/ValuesForm.tsx

@@ -108,6 +108,7 @@ export default class ValuesForm extends Component<PropsType, StateType> {
               }}
               label={item.label}
               disabled={this.props.disabled}
+              secretOption={true}
             />
           );
         case "key-value-array":

+ 19 - 5
dashboard/src/main/home/cluster-dashboard/env-groups/CreateEnvGroup.tsx

@@ -8,7 +8,7 @@ import { Context } from "shared/Context";
 import { ClusterType } from "shared/types";
 
 import InputRow from "components/values-form/InputRow";
-import KeyValueArray from "components/values-form/KeyValueArray";
+import EnvGroupArray, { KeyValueType } from "./EnvGroupArray";
 import Selector from "components/Selector";
 import Helper from "components/values-form/Helper";
 import SaveButton from "components/SaveButton";
@@ -25,7 +25,7 @@ type StateType = {
   envGroupName: string;
   selectedNamespace: string;
   namespaceOptions: any[];
-  envVariables: any;
+  envVariables: KeyValueType[];
   submitStatus: string;
 };
 
@@ -36,7 +36,7 @@ export default class CreateEnvGroup extends Component<PropsType, StateType> {
     envGroupName: "",
     selectedNamespace: "default",
     namespaceOptions: [] as any[],
-    envVariables: {} as any,
+    envVariables: [] as KeyValueType[],
     submitStatus: "",
   };
 
@@ -52,13 +52,26 @@ export default class CreateEnvGroup extends Component<PropsType, StateType> {
 
   onSubmit = () => {
     this.setState({ submitStatus: "loading" });
+
+    let apiEnvVariables : Record<string, string> = {}
+    let secretEnvVariables : Record<string, string> = {}
+
+    this.state.envVariables.forEach((envVar: KeyValueType) => {
+      if (envVar.hidden) {
+        secretEnvVariables[envVar.key] = envVar.value
+      } else {
+        apiEnvVariables[envVar.key] = envVar.value
+      }
+    })
+
     api
       .createConfigMap(
         "<token>",
         {
           name: this.state.envGroupName,
           namespace: this.state.selectedNamespace,
-          variables: this.state.envVariables,
+          variables: apiEnvVariables,
+          secret_variables: secretEnvVariables,
         },
         {
           id: this.context.currentProject.id,
@@ -159,11 +172,12 @@ export default class CreateEnvGroup extends Component<PropsType, StateType> {
             Set environment variables for your secrets and environment-specific
             configuration.
           </Helper>
-          <KeyValueArray
+          <EnvGroupArray
             namespace={this.state.selectedNamespace}
             values={this.state.envVariables}
             setValues={(x: any) => this.setState({ envVariables: x })}
             fileUpload={true}
+            secretOption={true}
           />
           <SaveButton
             disabled={this.isDisabled()}

+ 398 - 0
dashboard/src/main/home/cluster-dashboard/env-groups/EnvGroupArray.tsx

@@ -0,0 +1,398 @@
+import React, { Component } from "react";
+import styled from "styled-components";
+import Modal from "main/home/modals/Modal";
+import EnvEditorModal from "main/home/modals/EnvEditorModal";
+
+import sliders from "assets/sliders.svg";
+import upload from "assets/upload.svg";
+import { keysIn } from "lodash";
+
+export type KeyValueType = {
+    key: string;
+    value: string;
+    hidden: boolean;
+    locked: boolean;
+    deleted: boolean;
+}
+
+type PropsType = {
+  label?: string;
+  values: KeyValueType[];
+  setValues: (x: KeyValueType[]) => void;
+  width?: string;
+  disabled?: boolean;
+  namespace?: string;
+  clusterId?: number;
+  envLoader?: boolean;
+  fileUpload?: boolean;
+  secretOption?: boolean;
+};
+
+type StateType = {
+  showEnvModal: boolean;
+  showEditorModal: boolean;
+};
+
+export default class KeyValueArray extends Component<PropsType, StateType> {
+  state = {
+    showEnvModal: false,
+    showEditorModal: false,
+  };
+
+  componentDidMount() {
+      if (!this.props.values) {
+          let _values = [] as KeyValueType[];
+          this.props.setValues(_values)
+      }
+  }
+
+  renderDeleteButton = (i: number) => {
+    if (!this.props.disabled) {
+      return (
+        <DeleteButton
+          onClick={() => {
+            let _values = this.props.values
+            _values[i].deleted = true
+            this.props.setValues(_values);
+          }}
+        >
+          <i className="material-icons">cancel</i>
+        </DeleteButton>
+      );
+    }
+  };
+
+  renderHiddenOption = (hidden: boolean, locked: boolean, i: number) => {
+    if (this.props.secretOption) {
+      let icon = <i className="material-icons">lock_open</i>
+
+      if (hidden) {
+        icon = <i className="material-icons">lock</i>
+      }
+
+      return (
+        <HideButton
+          onClick={() => {
+              if (!locked) {
+                let  _values = this.props.values
+                _values[i].hidden = !_values[i].hidden;
+                this.props.setValues(_values)
+              }
+          }}
+          disabled={locked}
+        >
+          {icon}
+        </HideButton>
+      );
+    }
+  };
+
+  renderInputList = () => {
+    return (
+      <>
+        {this.props.values.map((entry: KeyValueType, i: number) => {
+            if (!entry.deleted) {
+                return (
+                    <InputWrapper key={i}>
+                      <Input
+                        placeholder="ex: key"
+                        width="270px"
+                        value={entry.key}
+                        onChange={(e: any) => {
+                          let _values = this.props.values
+                          _values[i].key = e.target.value;
+                          this.props.setValues(_values);
+                        }}
+                        disabled={this.props.disabled || entry.locked}
+                      />
+                      <Spacer />
+                      <Input
+                        placeholder="ex: value"
+                        width="270px"
+                        value={entry.value}
+                        onChange={(e: any) => {
+                          let _values = this.props.values
+                          _values[i].value = e.target.value;
+                          this.props.setValues(_values);
+                        }}
+                        disabled={this.props.disabled || entry.locked}
+                        type={entry.hidden ? "password" : "text"}
+                      />
+                      {this.renderHiddenOption(entry.hidden, entry.locked, i)}
+                      {this.renderDeleteButton(i)}
+                    </InputWrapper>
+                  );
+            }
+        })}
+      </>
+    );
+  };
+
+  renderEditorModal = () => {
+    if (this.state.showEditorModal) {
+      return (
+        <Modal
+          onRequestClose={() => this.setState({ showEditorModal: false })}
+          width="60%"
+          height="80%"
+        >
+          <EnvEditorModal
+            closeModal={() => this.setState({ showEditorModal: false })}
+            setEnvVariables={(envFile: string) => this.readFile(envFile)}
+          />
+        </Modal>
+      );
+    }
+  };
+
+    // Parses src into an Object
+  parseEnv = (src: any, options: any) => {
+    const debug = Boolean(options && options.debug)
+    const obj = {} as Record<string, string>
+    const NEWLINE = '\n'
+    const RE_INI_KEY_VAL = /^\s*([\w.-]+)\s*=\s*(.*)?\s*$/
+    const RE_NEWLINES = /\\n/g
+    const NEWLINES_MATCH = /\n|\r|\r\n/
+
+    // convert Buffers before splitting into lines and processing
+    src.toString().split(NEWLINES_MATCH).forEach(function (line: any, idx: any) {
+      // matching "KEY' and 'VAL' in 'KEY=VAL'
+      const keyValueArr = line.match(RE_INI_KEY_VAL)
+      // matched?
+      if (keyValueArr != null) {
+        const key = keyValueArr[1]
+        // default undefined or missing values to empty string
+        let val = (keyValueArr[2] || '')
+        const end = val.length - 1
+        const isDoubleQuoted = val[0] === '"' && val[end] === '"'
+        const isSingleQuoted = val[0] === "'" && val[end] === "'"
+
+        // if single or double quoted, remove quotes
+        if (isSingleQuoted || isDoubleQuoted) {
+          val = val.substring(1, end)
+
+          // if double quoted, expand newlines
+          if (isDoubleQuoted) {
+            val = val.replace(RE_NEWLINES, NEWLINE)
+          }
+        } else {
+          // remove surrounding whitespace
+          val = val.trim()
+        }
+
+        obj[key] = val
+      } else if (debug) {
+        console.log(`did not match key and value when parsing line ${idx + 1}: ${line}`)
+      }
+    })
+
+    return obj
+  }
+
+  readFile = (env: string) => {
+    let envObj = this.parseEnv(env, null)
+    let push = true;
+    let _values = this.props.values
+
+    for (let key in envObj) {
+      for (var i = 0; i < this.props.values.length; i++) {
+        let existingKey = this.props.values[i]["key"]
+        if (key === existingKey) {
+            _values[i]["value"] = envObj[key]
+          push = false;
+        }
+      }
+
+      if (push) {
+        _values.push({ key, value: envObj[key], hidden: false, locked: false, deleted: false });
+      }
+
+    }
+
+    this.props.setValues(_values);
+  }
+
+  render() {
+      if (this.props.values) {
+        return (
+            <>
+              <StyledInputArray>
+                <Label>{this.props.label}</Label>
+                {this.props.values.length === 0 ? <></> : this.renderInputList()}
+                {this.props.disabled ? (
+                  <></>
+                ) : (
+                  <InputWrapper>
+                    <AddRowButton
+                      onClick={() => {
+                        let _values = this.props.values
+                        _values.push({ key: "", value: "", hidden: false, locked: false, deleted: false });
+                        this.props.setValues(_values)
+                      }}
+                    >
+                      <i className="material-icons">add</i> Add Row
+                    </AddRowButton>
+                    <Spacer />
+                    {this.props.namespace && this.props.envLoader && (
+                      <LoadButton
+                        onClick={() =>
+                          this.setState({ showEnvModal: !this.state.showEnvModal })
+                        }
+                      >
+                        <img src={sliders} /> Load from Env Group
+                      </LoadButton>
+                    )}
+                    {this.props.fileUpload && (
+                      <UploadButton
+                        onClick={()=>{
+                          this.setState({ showEditorModal: true });
+                        }}
+                      >
+                        <img src={upload} /> Copy from File
+                      </UploadButton>
+                    )}
+                  </InputWrapper>
+                )}
+              </StyledInputArray>
+              {this.renderEditorModal()}
+            </>
+          );
+      }
+
+      return null
+  }
+}
+
+const Spacer = styled.div`
+  width: 10px;
+  height: 20px;
+`;
+
+const AddRowButton = styled.div`
+  display: flex;
+  align-items: center;
+  width: 270px;
+  font-size: 13px;
+  color: #aaaabb;
+  height: 32px;
+  border-radius: 3px;
+  cursor: pointer;
+  background: #ffffff11;
+  :hover {
+    background: #ffffff22;
+  }
+
+  > i {
+    color: #ffffff44;
+    font-size: 16px;
+    margin-left: 8px;
+    margin-right: 10px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+  }
+`;
+
+const LoadButton = styled(AddRowButton)`
+  background: none;
+  border: 1px solid #ffffff55;
+  > i {
+    color: #ffffff44;
+    font-size: 16px;
+    margin-left: 8px;
+    margin-right: 10px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+  }
+  > img {
+    width: 14px;
+    margin-left: 10px;
+    margin-right: 12px;
+  }
+`;
+
+const UploadButton = styled(AddRowButton)`
+  background: none;
+  position: relative;
+  border: 1px solid #ffffff55;
+  > i {
+    color: #ffffff44;
+    font-size: 16px;
+    margin-left: 8px;
+    margin-right: 10px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+  }
+  > img {
+    width: 14px;
+    margin-left: 10px;
+    margin-right: 12px;
+  }
+`;
+
+const DeleteButton = styled.div`
+  width: 15px;
+  height: 15px;
+  display: flex;
+  align-items: center;
+  margin-left: 8px;
+  margin-top: -3px;
+  justify-content: center;
+
+  > i {
+    font-size: 17px;
+    color: #ffffff44;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    cursor: pointer;
+    :hover {
+      color: #ffffff88;
+    }
+  }
+`;
+
+const HideButton = styled(DeleteButton)`
+  margin-top: -5px;
+  > i {
+    font-size: 19px;
+    cursor: ${(props : {disabled: boolean}) => props.disabled ? "default" : "pointer"};
+    :hover {
+        color: ${(props : {disabled: boolean}) => props.disabled ? "#ffffff44" : "#ffffff88"};
+      }
+  }
+`
+
+const InputWrapper = styled.div`
+  display: flex;
+  align-items: center;
+  margin-top: 5px;
+`;
+
+const Input = styled.input`
+  outline: none;
+  border: none;
+  margin-bottom: 5px;
+  font-size: 13px;
+  background: #ffffff11;
+  border: 1px solid #ffffff55;
+  border-radius: 3px;
+  width: ${(props: { disabled?: boolean; width: string }) =>
+    props.width ? props.width : "270px"};
+  color: ${(props: { disabled?: boolean; width: string }) =>
+    props.disabled ? "#ffffff44" : "white"};
+  padding: 5px 10px;
+  height: 35px;
+`;
+
+const Label = styled.div`
+  color: #ffffff;
+  margin-bottom: 10px;
+`;
+
+const StyledInputArray = styled.div`
+  margin-bottom: 15px;
+  margin-top: 22px;
+`;

+ 46 - 9
dashboard/src/main/home/cluster-dashboard/env-groups/ExpandedEnvGroup.tsx

@@ -13,7 +13,7 @@ import SaveButton from "components/SaveButton";
 import ConfirmOverlay from "components/ConfirmOverlay";
 import Loading from "components/Loading";
 import TabRegion from "components/TabRegion";
-import KeyValueArray from "components/values-form/KeyValueArray";
+import EnvGroupArray, { KeyValueType } from "./EnvGroupArray";
 import Heading from "components/values-form/Heading";
 import Helper from "components/values-form/Helper";
 
@@ -30,7 +30,7 @@ type StateType = {
   showDeleteOverlay: boolean;
   deleting: boolean;
   saveValuesStatus: string | null;
-  values: any;
+  envVariables: KeyValueType[];
 };
 
 const tabOptions = [
@@ -45,14 +45,51 @@ export default class ExpandedEnvGroup extends Component<PropsType, StateType> {
     showDeleteOverlay: false,
     deleting: false,
     saveValuesStatus: null as string | null,
-    values: this.props.envGroup.data as any,
+    envVariables: [] as KeyValueType[],
   };
 
+  componentDidMount() {
+    // parse env group props into values type
+    let envVariables = [] as KeyValueType[]
+    let envGroupData = this.props.envGroup.data
+
+    for (const key in envGroupData) {
+      envVariables.push({
+        key: key,
+        value: envGroupData[key],
+        hidden: envGroupData[key].includes("PORTERSECRET"),
+        locked: envGroupData[key].includes("PORTERSECRET"),
+        deleted: false,
+      })
+    }
+
+    this.setState({ envVariables })
+  }
+
   handleUpdateValues = () => {
     let { envGroup } = this.props;
     let name = envGroup.metadata.name;
     let namespace = envGroup.metadata.namespace;
 
+    let apiEnvVariables : Record<string, string> = {}
+    let secretEnvVariables : Record<string, string> = {}
+
+    this.state.envVariables.forEach((envVar: KeyValueType) => {
+      if (envVar.hidden) {
+        if (envVar.deleted) {
+          secretEnvVariables[envVar.key] = null
+        } else if (envVar.value.includes("PORTERSECRET")) {
+          secretEnvVariables[envVar.key] = envVar.value
+        }
+      } else {
+        if (envVar.deleted) {
+          apiEnvVariables[envVar.key] = null
+        } else {
+          apiEnvVariables[envVar.key] = envVar.value
+        }
+      }
+    })
+
     this.setState({ saveValuesStatus: "loading" });
     api
       .updateConfigMap(
@@ -60,7 +97,8 @@ export default class ExpandedEnvGroup extends Component<PropsType, StateType> {
         {
           name,
           namespace,
-          variables: this.state.values,
+          variables: apiEnvVariables,
+          secret_variables: secretEnvVariables,
         },
         {
           id: this.context.currentProject.id,
@@ -90,11 +128,12 @@ export default class ExpandedEnvGroup extends Component<PropsType, StateType> {
                 Set environment variables for your secrets and
                 environment-specific configuration.
               </Helper>
-              <KeyValueArray
+              <EnvGroupArray
                 namespace={namespace}
-                values={this.state.values || {}}
-                setValues={(x: any) => this.setState({ values: x }, () => {console.log(this.state.values)})}
+                values={this.state.envVariables}
+                setValues={(x: any) => this.setState({ envVariables: x })}
                 fileUpload={true}
+                secretOption={true}
               />
             </InnerWrapper>
             <SaveButton
@@ -155,11 +194,9 @@ export default class ExpandedEnvGroup extends Component<PropsType, StateType> {
       .then((res) => {
         this.props.closeExpanded();
         this.setState({ deleting: false });
-        // console.log("CONFIGMAP", res);
       })
       .catch((err) => {
         this.setState({ deleting: false, showDeleteOverlay: false });
-        // console.log("CONFIGMAP", err);
       });
   };
 

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

@@ -753,6 +753,7 @@ const createConfigMap = baseApi<
     name: string;
     namespace: string;
     variables: Record<string, string>;
+    secret_variables?: Record<string, string>;
   },
   { id: number; cluster_id: number }
 >("POST", (pathParams) => {
@@ -765,6 +766,7 @@ const updateConfigMap = baseApi<
     name: string;
     namespace: string;
     variables: Record<string, string>;
+    secret_variables?: Record<string, string>;
   },
   { id: number; cluster_id: number }
 >("POST", (pathParams) => {

+ 5 - 4
internal/forms/k8s.go

@@ -39,7 +39,8 @@ func (kf *K8sForm) PopulateK8sOptionsFromQueryParams(
 }
 
 type ConfigMapForm struct {
-	Name string `json:"name" form:"required"`
-	Namespace string `json:"namespace" form:"required"`
-	EnvVariables map[string]string `json:"variables"`
-}
+	Name               string            `json:"name" form:"required"`
+	Namespace          string            `json:"namespace" form:"required"`
+	EnvVariables       map[string]string `json:"variables"`
+	SecretEnvVariables map[string]string `json:"secret_variables"`
+}

+ 97 - 10
internal/kubernetes/agent.go

@@ -4,6 +4,7 @@ import (
 	"bufio"
 	"bytes"
 	"context"
+	"encoding/json"
 	"fmt"
 	"io"
 	"strings"
@@ -33,6 +34,7 @@ import (
 	v1beta1 "k8s.io/api/extensions/v1beta1"
 	"k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/types"
 	"k8s.io/cli-runtime/pkg/genericclioptions"
 	"k8s.io/client-go/informers"
 	"k8s.io/client-go/kubernetes"
@@ -64,7 +66,7 @@ func (a *Agent) CreateConfigMap(name string, namespace string, configMap map[str
 		context.TODO(),
 		&v1.ConfigMap{
 			ObjectMeta: metav1.ObjectMeta{
-				Name: name,
+				Name:      name,
 				Namespace: namespace,
 				Labels: map[string]string{
 					"porter": "true",
@@ -76,26 +78,102 @@ func (a *Agent) CreateConfigMap(name string, namespace string, configMap map[str
 	)
 }
 
-// UpdateConfigMap updates the configmap given its name and namespace
-func (a *Agent) UpdateConfigMap(name string, namespace string, configMap map[string]string) (*v1.ConfigMap, error) {
-	return a.Clientset.CoreV1().ConfigMaps(namespace).Update(
+// CreateLinkedSecret creates a secret given the key-value pairs and namespace. Values are
+// base64 encoded
+func (a *Agent) CreateLinkedSecret(name, namespace, cmName string, data map[string][]byte) (*v1.Secret, error) {
+	return a.Clientset.CoreV1().Secrets(namespace).Create(
 		context.TODO(),
-		&v1.ConfigMap{
+		&v1.Secret{
 			ObjectMeta: metav1.ObjectMeta{
-				Name: name,
+				Name:      name,
 				Namespace: namespace,
 				Labels: map[string]string{
-					"porter": "true",
+					"porter":    "true",
+					"configmap": cmName,
 				},
 			},
-			Data: configMap,
+			Data: data,
 		},
-		metav1.UpdateOptions{},
+		metav1.CreateOptions{},
 	)
 }
 
+type mergeConfigMapData struct {
+	Data map[string]*string `json:"data"`
+}
+
+// UpdateConfigMap updates the configmap given its name and namespace
+func (a *Agent) UpdateConfigMap(name string, namespace string, configMap map[string]string) error {
+	cmData := make(map[string]*string)
+
+	for key, val := range configMap {
+		cmData[key] = &val
+
+		if len(val) == 0 {
+			cmData[key] = nil
+		}
+	}
+
+	mergeCM := &mergeConfigMapData{
+		Data: cmData,
+	}
+
+	patchBytes, err := json.Marshal(mergeCM)
+
+	if err != nil {
+		return err
+	}
+
+	_, err = a.Clientset.CoreV1().ConfigMaps(namespace).Patch(
+		context.Background(),
+		name,
+		types.MergePatchType,
+		patchBytes,
+		metav1.PatchOptions{},
+	)
+
+	return err
+}
+
+type mergeLinkedSecretData struct {
+	Data map[string]*[]byte `json:"data"`
+}
+
+// UpdateLinkedSecret updates the secret given its name and namespace
+func (a *Agent) UpdateLinkedSecret(name, namespace, cmName string, data map[string][]byte) error {
+	secretData := make(map[string]*[]byte)
+
+	for key, val := range data {
+		secretData[key] = &val
+
+		if len(val) == 0 {
+			secretData[key] = nil
+		}
+	}
+
+	mergeSecret := &mergeLinkedSecretData{
+		Data: secretData,
+	}
+
+	patchBytes, err := json.Marshal(mergeSecret)
+
+	if err != nil {
+		return err
+	}
+
+	_, err = a.Clientset.CoreV1().Secrets(namespace).Patch(
+		context.TODO(),
+		name,
+		types.MergePatchType,
+		patchBytes,
+		metav1.PatchOptions{},
+	)
+
+	return err
+}
+
 // DeleteConfigMap deletes the configmap given its name and namespace
-func (a *Agent) DeleteConfigMap(name string, namespace string) (error) {
+func (a *Agent) DeleteConfigMap(name string, namespace string) error {
 	return a.Clientset.CoreV1().ConfigMaps(namespace).Delete(
 		context.TODO(),
 		name,
@@ -103,6 +181,15 @@ func (a *Agent) DeleteConfigMap(name string, namespace string) (error) {
 	)
 }
 
+// DeleteLinkedSecret deletes the secret given its name and namespace
+func (a *Agent) DeleteLinkedSecret(name, namespace string) error {
+	return a.Clientset.CoreV1().Secrets(namespace).Delete(
+		context.TODO(),
+		name,
+		metav1.DeleteOptions{},
+	)
+}
+
 // GetConfigMap retrieves the configmap given its name and namespace
 func (a *Agent) GetConfigMap(name string, namespace string) (*v1.ConfigMap, error) {
 	return a.Clientset.CoreV1().ConfigMaps(namespace).Get(

+ 67 - 4
server/api/k8s_handler.go

@@ -77,7 +77,6 @@ func (app *App) HandleListNamespaces(w http.ResponseWriter, r *http.Request) {
 
 // HandleCreateConfigMap deletes the pod given the name and namespace.
 func (app *App) HandleCreateConfigMap(w http.ResponseWriter, r *http.Request) {
-	fmt.Println("CREATING CONFGIMAP")
 	vals, err := url.ParseQuery(r.URL.RawQuery)
 	if err != nil {
 		app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
@@ -116,6 +115,32 @@ func (app *App) HandleCreateConfigMap(w http.ResponseWriter, r *http.Request) {
 		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 {
@@ -135,7 +160,7 @@ func (app *App) HandleCreateConfigMap(w http.ResponseWriter, r *http.Request) {
 // HandleListConfigMaps lists all configmaps in a namespace.
 func (app *App) HandleListConfigMaps(w http.ResponseWriter, r *http.Request) {
 	vals, err := url.ParseQuery(r.URL.RawQuery)
-	
+
 	if err != nil {
 		app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
 		return
@@ -185,7 +210,7 @@ func (app *App) HandleListConfigMaps(w http.ResponseWriter, r *http.Request) {
 // HandleGetConfigMap retreives the configmap given the name and namespace.
 func (app *App) HandleGetConfigMap(w http.ResponseWriter, r *http.Request) {
 	vals, err := url.ParseQuery(r.URL.RawQuery)
-	
+
 	if err != nil {
 		app.handleErrorFormDecoding(err, ErrReleaseDecode, w)
 		return
@@ -265,6 +290,13 @@ 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 {
@@ -317,7 +349,38 @@ func (app *App) HandleUpdateConfigMap(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	_, err = agent.UpdateConfigMap(configMap.Name, configMap.Namespace, configMap.EnvVariables)
+	secretData := make(map[string][]byte)
+
+	for key, rawValue := range configMap.SecretEnvVariables {
+		// encodedValue, err := base64.StdEncoding.DecodeString(rawValue)
+
+		// if err != nil {
+		// 	app.handleErrorInternal(err, w)
+		// 	return
+		// }
+
+		secretData[key] = []byte(rawValue)
+	}
+
+	// create secret first
+	err = agent.UpdateLinkedSecret(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, val := range configMap.SecretEnvVariables {
+		configMap.EnvVariables[key] = fmt.Sprintf("PORTERSECRET_%s", configMap.Name)
+
+		// if val is empty, set to empty
+		if val == "" {
+			configMap.EnvVariables[key] = ""
+		}
+	}
+
+	err = agent.UpdateConfigMap(configMap.Name, configMap.Namespace, configMap.EnvVariables)
 
 	if err != nil {
 		app.handleErrorInternal(err, w)