Ver Fonte

resolve merge conflicts

Justin Rhee há 3 anos atrás
pai
commit
faada3b65b

+ 0 - 3
dashboard/src/components/repo-selector/DetectContentsList.tsx

@@ -26,7 +26,6 @@ type PropsType = {
   dockerfilePath?: string;
   folderPath: string;
   porterYaml?: string;
-  buildConfig: BuildConfig;
   setActionConfig: (x: ActionConfigType) => void;
   setDockerfilePath: (x: string) => void;
   setFolderPath: (x: string) => void;
@@ -237,7 +236,6 @@ const DetectContentsList: React.FC<PropsType> = (props) => {
           actionConfig={props.actionConfig}
           branch={props.branch}
           folderPath={props.folderPath}
-          buildConfig={props.buildConfig}
         />
       ) : (
         <AdvancedBuildSettings
@@ -250,7 +248,6 @@ const DetectContentsList: React.FC<PropsType> = (props) => {
           actionConfig={props.actionConfig}
           branch={props.branch}
           folderPath={props.folderPath}
-          buildConfig={props.buildConfig}
         />
       )}
     </>

+ 151 - 18
dashboard/src/main/home/app-dashboard/expanded-app/ExpandedApp.tsx

@@ -2,6 +2,7 @@ import React, { useEffect, useState, useContext, useCallback } from "react";
 import { RouteComponentProps, withRouter } from "react-router";
 import styled from "styled-components";
 import yaml from "js-yaml";
+import { z } from "zod";
 
 import notFound from "assets/not-found.png";
 import web from "assets/web.png";
@@ -13,6 +14,7 @@ import loadingImg from "assets/loading.gif";
 import api from "shared/api";
 import { Context } from "shared/Context";
 import useAuth from "shared/auth/useAuth";
+import Error from "components/porter/Error";
 
 import Loading from "components/Loading";
 import Text from "components/porter/Text";
@@ -31,6 +33,9 @@ import ConfirmOverlay from "components/porter/ConfirmOverlay";
 import Fieldset from "components/porter/Fieldset";
 import Banner from "components/Banner";
 import AppEvents from "./AppEvents";
+import { createFinalPorterYaml } from "../new-app-flow/schema";
+import EnvGroupArray, { KeyValueType } from "main/home/cluster-dashboard/env-groups/EnvGroupArray";
+import { PorterYamlSchema } from "../new-app-flow/schema";
 
 type Props = RouteComponentProps & {};
 
@@ -46,7 +51,7 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
   const { currentCluster, currentProject, setCurrentError } = useContext(
     Context
   );
-
+  const [rawYaml, setRawYaml] = useState<string>("");
   const [isLoading, setIsLoading] = useState(true);
   const [deleting, setDeleting] = useState(false);
   const [appData, setAppData] = useState(null);
@@ -65,6 +70,14 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
   const [showRevisions, setShowRevisions] = useState<boolean>(false);
   const [newestImage, setNewestImage] = useState<string>(null);
   const [showDeleteOverlay, setShowDeleteOverlay] = useState<boolean>(false);
+  const [porterJson, setPorterJson] = useState<
+    z.infer<typeof PorterYamlSchema> | undefined
+  >(undefined);
+
+  const [services, setServices] = useState<Service[]>([]);
+  const [envVars, setEnvVars] = useState<KeyValueType[]>([]);
+  const [updating, setUpdating] = useState<boolean>(false);
+  const [updateError, setUpdateError] = useState<string>("");
 
   const getPorterApp = async () => {
     setIsLoading(true);
@@ -90,14 +103,24 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
           revision: 0,
         }
       );
-      setAppData({
+
+      const newAppData = {
         app: resPorterApp?.data,
         chart: resChartData?.data,
-      });
-      console.log(appData);
-      setIsLoading(false);
+      };
+      setAppData(newAppData);
+
+      const helmValues = resChartData?.data?.config;
+      const defaultValues = resChartData?.data?.chart?.values;
+      if ((defaultValues && Object.keys(defaultValues).length > 0) || (helmValues && Object.keys(helmValues).length > 0)) {
+        const svcs = Service.deserialize(helmValues, defaultValues);
+        setServices(svcs);
+        console.log(helmValues);
+      }
+      console.log(newAppData);
     } catch (err) {
       setError(err);
+    } finally {
       setIsLoading(false);
     }
   };
@@ -132,6 +155,78 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
     }
   };
 
+  const updatePorterApp = async () => {
+    try {
+      setUpdating(true);
+      if (
+        appData != null
+        && currentCluster != null
+        && currentProject != null
+        && appData.app != null
+      ) {
+        const finalPorterYaml = createFinalPorterYaml(
+          services,
+          [],
+          undefined,
+          appData.app.name,
+          currentProject.id,
+          currentCluster.id,
+        )
+        const yamlString = yaml.dump(finalPorterYaml);
+        const base64Encoded = btoa(yamlString);
+        await api.updatePorterStack(
+          "<token>",
+          {
+            stack_name: appData.app.name,
+            porter_yaml: base64Encoded,
+          },
+          {
+            cluster_id: currentCluster.id,
+            project_id: currentProject.id,
+            stack_name: appData.app.name,
+          }
+        )
+      } else {
+        setUpdateError("Unable to update app, please try again later.");
+      }
+    } catch (err) {
+      // TODO: better error handling
+      console.log(err);
+      const errMessage = err?.response?.data?.error ?? err?.toString() ?? 'An error occurred while deploying your app. Please try again.'
+      setUpdateError(errMessage);
+    } finally {
+      setUpdating(false)
+    }
+  }
+
+  const fetchPorterYamlContent = async (porterYaml: string) => {
+    try {
+      const res = await api.getPorterYamlContents(
+        "<token>",
+        {
+          path: porterYaml,
+        },
+        {
+          project_id: appData.app.project_id,
+          git_repo_id: appData.app.git_repo_id,
+          owner: appData.app.repo_name?.split("/")[0],
+          name: appData.app.repo_name?.split("/")[1],
+          kind: "github",
+          branch: appData.app.git_branch,
+        }
+      );
+      setRawYaml(atob(res.data));
+      let parsedYaml;
+      parsedYaml = yaml.load(rawYaml);
+      const parsedData = PorterYamlSchema.parse(parsedYaml);
+      const porterYamlToJson = parsedData as z.infer<typeof PorterYamlSchema>;
+      setPorterJson(porterYamlToJson);
+      console.log(porterJson);
+    } catch (err) {
+      console.log(err);
+    }
+  };
+
   const renderIcon = (b: string, size?: string) => {
     var src = box;
     if (b) {
@@ -298,18 +393,28 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
   const renderTabContents = () => {
     switch (tab) {
       case "overview":
-        const helmValues = appData?.chart?.config;
-        const defaultValues = appData?.chart?.chart?.values;
-        if ((defaultValues && Object.keys(defaultValues).length > 0) || (helmValues && Object.keys(helmValues).length > 0)) {
-          const svcs = Service.deserialize(helmValues, defaultValues);
-          return <Services
-            setServices={(services: any[]) => {
-            }}
-            services={svcs}
-          />;
-        } else {
-          return <Text>No services found for this application yet.</Text>
-        }
+        return (
+          <>
+            <Services
+              setServices={setServices}
+              services={services}
+            />
+            <Spacer y={0.5} />
+            <Button
+              onClick={() => {
+                updatePorterApp();
+              }}
+              status={updating ? "loading" : updateError ? (
+                <Error message={updateError} />
+              ) : undefined}
+              loadingText={"Updating..."}
+              width={"150px"}
+            >
+              Update app
+            </Button>
+            <Spacer y={0.5} />
+          </>
+        )
       case "build-settings":
         return (
           <BuildSettingsTabStack
@@ -344,6 +449,34 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
             branchName={appData.app.git_branch}
           />
         );
+      case "environment-variables":
+        return (
+          <>
+            <Text size={16}>Environment variables</Text>
+            <Spacer y={0.5} />
+            <Text color="helper">
+              Shared among all services.
+            </Text>
+            <EnvGroupArray
+              values={envVars}
+              setValues={setEnvVars}
+              fileUpload={true}
+            />
+            <Spacer y={0.5} />
+            <Button
+              onClick={() => {
+                updatePorterApp();
+              }}
+              status={updating ? "loading" : updateError ? (
+                <Error message={updateError} />
+              ) : undefined}
+              loadingText={"Updating..."}
+              width={"150px"}
+            >
+              Update app
+            </Button>
+            <Spacer y={0.5} />
+          </>);
       default:
         return <div>dream on</div>;
     }
@@ -369,7 +502,7 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
         <StyledExpandedApp>
           <Back to="/apps" />
           <Container row>
-            {renderIcon(appData.app.build_packs)}
+            {renderIcon(appData.app?.build_packs)}
             <Text size={21}>{appData.app.name}</Text>
             {appData.app.repo_name && (
               <>

+ 1 - 4
dashboard/src/main/home/app-dashboard/expanded-app/SharedBuildSettings.tsx

@@ -21,7 +21,6 @@ type Props = {
   folderPath: string;
   setFolderPath: (x: string) => void;
   setBuildConfig: (x: any) => void;
-  buildConfig: BuildConfig;
   porterYaml: string;
   setPorterYaml: (x: any) => void;
   imageUrl: string;
@@ -31,7 +30,6 @@ type Props = {
 const SharedBuildSettings: React.FC<Props> = ({
   actionConfig,
   setActionConfig,
-  buildConfig,
   branch,
   setBranch,
   dockerfilePath,
@@ -101,7 +99,7 @@ const SharedBuildSettings: React.FC<Props> = ({
         width="100%"
         setValue={setFolderPath}
       />
-      {actionConfig.git_repo && branch && buildConfig.builder != "" && (
+      {actionConfig.git_repo && branch && (
         <DetectContentsList
           actionConfig={actionConfig}
           branch={branch}
@@ -110,7 +108,6 @@ const SharedBuildSettings: React.FC<Props> = ({
           setActionConfig={setActionConfig}
           setDockerfilePath={setDockerfilePath}
           setFolderPath={setFolderPath}
-          buildConfig={buildConfig}
           setBuildConfig={setBuildConfig}
           porterYaml={porterYaml}
           setPorterYaml={setPorterYaml}

+ 0 - 1
dashboard/src/main/home/app-dashboard/new-app-flow/AdvancedBuildSettings.tsx

@@ -26,7 +26,6 @@ interface AdvancedBuildSettingsProps {
   dockerfilePath?: string;
   setDockerfilePath: (x: string) => void;
   setBuildConfig: (x: any) => void;
-  buildConfig: BuildConfig;
 }
 
 type Buildpack = {

+ 11 - 8
dashboard/src/main/home/app-dashboard/new-app-flow/JobTabs.tsx

@@ -21,8 +21,6 @@ const JobTabs: React.FC<Props> = ({
   const [currentTab, setCurrentTab] = React.useState<string>('main');
 
   const renderMain = () => {
-    setHeight(244);
-
     return (
       <>
         <Spacer y={1} />
@@ -47,8 +45,6 @@ const JobTabs: React.FC<Props> = ({
   };
 
   const renderResources = () => {
-    setHeight(244);
-
     return (
       <>
         <Spacer y={1} />
@@ -61,7 +57,7 @@ const JobTabs: React.FC<Props> = ({
         />
         <Spacer y={1} />
         <Input
-          label="RAM (GB)"
+          label="RAM (MB)"
           placeholder="ex: 1"
           value={service.ram}
           width="300px"
@@ -72,8 +68,6 @@ const JobTabs: React.FC<Props> = ({
   };
 
   const renderAdvanced = () => {
-    setHeight(118.5);
-
     return (
       <>
         <Spacer y={1} />
@@ -96,7 +90,16 @@ const JobTabs: React.FC<Props> = ({
           { label: 'Advanced', value: 'advanced' },
         ]}
         currentTab={currentTab}
-        setCurrentTab={setCurrentTab}
+        setCurrentTab={(value: string) => {
+          if (value === 'main') {
+            setHeight(244);
+          } else if (value === 'resources') {
+            setHeight(244);
+          } else if (value === 'advanced') {
+            setHeight(118.5);
+          }
+          setCurrentTab(value);
+        }}
       />
       {currentTab === 'main' && renderMain()}
       {currentTab === 'resources' && renderResources()}

+ 36 - 85
dashboard/src/main/home/app-dashboard/new-app-flow/NewAppFlow.tsx

@@ -22,14 +22,11 @@ import EnvGroupArray, {
   KeyValueType,
 } from "main/home/cluster-dashboard/env-groups/EnvGroupArray";
 import GithubActionModal from "./GithubActionModal";
-import {
-  GithubActionConfigType,
-} from "shared/types";
+import { GithubActionConfigType } from "shared/types";
 import Error from "components/porter/Error";
 import { z } from "zod";
-import { AppsSchema, EnvSchema, PorterYamlSchema } from "./schema";
+import { PorterYamlSchema, createFinalPorterYaml } from "./schema";
 import { Service } from "./serviceTypes";
-import { overrideObjectValues } from "./utils";
 
 type Props = RouteComponentProps & {};
 
@@ -79,7 +76,9 @@ const NewAppFlow: React.FC<Props> = ({ ...props }) => {
   const [imageTag, setImageTag] = useState("latest");
   const { currentCluster, currentProject } = useContext(Context);
   const [deploying, setDeploying] = useState<boolean>(false);
-  const [deploymentError, setDeploymentError] = useState<string | undefined>(undefined);
+  const [deploymentError, setDeploymentError] = useState<string | undefined>(
+    undefined
+  );
   const [currentStep, setCurrentStep] = useState<number>(0);
   const [existingStep, setExistingStep] = useState<number>(0);
   const [formState, setFormState] = useState<FormState>(INITIAL_STATE);
@@ -143,8 +142,9 @@ const NewAppFlow: React.FC<Props> = ({ ...props }) => {
       ) {
         setDetected({
           detected: true,
-          message: `Detected ${Object.keys(porterYamlToJson.apps).length
-            } apps from porter.yaml`,
+          message: `Detected ${
+            Object.keys(porterYamlToJson.apps).length
+          } apps from porter.yaml`,
         });
       } else {
         setDetected({
@@ -192,20 +192,27 @@ const NewAppFlow: React.FC<Props> = ({ ...props }) => {
         currentProject.id == null ||
         currentCluster.id == null
       ) {
-        throw ("Project or cluster not found");
+        throw "Project or cluster not found";
       }
 
       // validate form data
-      const finalPorterYaml = createFinalPorterYaml();
+      const finalPorterYaml = createFinalPorterYaml(
+        formState.serviceList,
+        formState.envVariables,
+        porterJson,
+        formState.applicationName,
+        currentProject.id,
+        currentCluster.id
+      );
       const yamlString = yaml.dump(finalPorterYaml);
       const base64Encoded = btoa(yamlString);
       const imageInfo = imageUrl
         ? {
-          image_info: {
-            repository: imageUrl,
-            tag: imageTag,
-          },
-        }
+            image_info: {
+              repository: imageUrl,
+              tag: imageTag,
+            },
+          }
         : {};
 
       // write to the db
@@ -228,7 +235,7 @@ const NewAppFlow: React.FC<Props> = ({ ...props }) => {
         }
       );
 
-      await api.updatePorterStack(
+      await api.createPorterStack(
         "<token>",
         {
           stack_name: formState.applicationName,
@@ -239,7 +246,7 @@ const NewAppFlow: React.FC<Props> = ({ ...props }) => {
           cluster_id: currentCluster.id,
           project_id: currentProject.id,
         }
-      )
+      );
       if (!actionConfig?.git_repo) {
         props.history.push(`/apps/${formState.applicationName}`);
       }
@@ -247,7 +254,10 @@ const NewAppFlow: React.FC<Props> = ({ ...props }) => {
     } catch (err) {
       // TODO: better error handling
       console.log(err);
-      const errMessage = err?.response?.data?.error ?? err?.toString() ?? 'An error occurred while deploying your app. Please try again.'
+      const errMessage =
+        err?.response?.data?.error ??
+        err?.toString() ??
+        "An error occurred while deploying your app. Please try again.";
       setDeploymentError(errMessage);
 
       return false;
@@ -256,68 +266,6 @@ const NewAppFlow: React.FC<Props> = ({ ...props }) => {
     }
   };
 
-  const combineEnv = (
-    dashboardSetVariables: KeyValueType[],
-    porterYamlSetVariables: Record<string, string> | undefined
-  ): z.infer<typeof EnvSchema> => {
-    const env: z.infer<typeof EnvSchema> = {};
-    for (const { key, value } of dashboardSetVariables) {
-      env[key] = value;
-    }
-    if (porterYamlSetVariables != null) {
-      for (const [key, value] of Object.entries(porterYamlSetVariables)) {
-        env[key] = value;
-      }
-    }
-    return env;
-  };
-
-  const createApps = (serviceList: Service[]): z.infer<typeof AppsSchema> => {
-    const apps: z.infer<typeof AppsSchema> = {};
-    for (const service of serviceList) {
-      let config = Service.serialize(service);
-      // TODO: get rid of this block when we handle ingress on the backend
-      if (Service.isWeb(service)) {
-        const ingress = Service.handleWebIngress(
-          service,
-          formState.applicationName,
-          currentCluster?.id,
-          currentProject?.id
-        );
-        config = {
-          ...config,
-          ...ingress,
-        };
-      }
-      if (
-        porterJson != null &&
-        porterJson.apps[service.name] != null &&
-        porterJson.apps[service.name].config != null
-      ) {
-        config = overrideObjectValues(
-          config,
-          porterJson.apps[service.name].config
-        );
-      }
-      // required because of https://github.com/helm/helm/issues/9214
-      apps[Service.toHelmName(service)] = {
-        type: service.type,
-        run: service.startCommand.value,
-        config,
-      };
-    }
-
-    return apps;
-  };
-
-  const createFinalPorterYaml = (): z.infer<typeof PorterYamlSchema> => {
-    return {
-      version: "v1stack",
-      env: combineEnv(formState.envVariables, porterJson?.env),
-      apps: createApps(formState.serviceList),
-    };
-  };
-
   return (
     <CenterWrapper>
       <Div>
@@ -393,7 +341,6 @@ const NewAppFlow: React.FC<Props> = ({ ...props }) => {
                   procfilePath={procfilePath}
                   setProcfilePath={setProcfilePath}
                   setBuildConfig={setBuildConfig}
-                  buildConfig={buildConfig}
                   porterYaml={porterYaml}
                   setPorterYaml={(newYaml: string) => {
                     validatePorterYaml(newYaml);
@@ -472,13 +419,17 @@ const NewAppFlow: React.FC<Props> = ({ ...props }) => {
                   if (imageUrl) {
                     deployPorterApp();
                   } else {
-                    setDeploymentError(undefined)
+                    setDeploymentError(undefined);
                     setShowGHAModal(true);
                   }
                 }}
-                status={deploying ? "loading" : deploymentError ? (
-                  <Error message={deploymentError} />
-                ) : undefined}
+                status={
+                  deploying ? (
+                    "loading"
+                  ) : deploymentError ? (
+                    <Error message={deploymentError} />
+                  ) : undefined
+                }
                 loadingText={"Deploying..."}
                 width={"150px"}
               >

+ 0 - 3
dashboard/src/main/home/app-dashboard/new-app-flow/SourceSettings.tsx

@@ -33,7 +33,6 @@ type Props = {
   setProcfilePath: (x: string) => void;
   folderPath: string | null;
   setFolderPath: (x: string) => void;
-  buildConfig: BuildConfig;
   setBuildConfig: (x: any) => void;
   porterYaml: string;
   setPorterYaml: (x: any) => void;
@@ -41,7 +40,6 @@ type Props = {
 
 const SourceSettings: React.FC<Props> = ({
   source,
-  buildConfig,
   imageUrl,
   setImageUrl,
   imageTag,
@@ -118,7 +116,6 @@ const SourceSettings: React.FC<Props> = ({
         <div>
           {source === "github" ? (
             <SharedBuildSettings
-              buildConfig={buildConfig}
               actionConfig={actionConfig}
               branch={branch}
               dockerfilePath={dockerfilePath}

+ 11 - 8
dashboard/src/main/home/app-dashboard/new-app-flow/WebTabs.tsx

@@ -21,8 +21,6 @@ const WebTabs: React.FC<Props> = ({
   const [currentTab, setCurrentTab] = React.useState<string>('main');
 
   const renderMain = () => {
-    setHeight(300);
-
     return (
       <>
         <Spacer y={1} />
@@ -54,8 +52,6 @@ const WebTabs: React.FC<Props> = ({
   };
 
   const renderResources = () => {
-    setHeight(713.5);
-
     return (
       <>
         <Spacer y={1} />
@@ -68,7 +64,7 @@ const WebTabs: React.FC<Props> = ({
         />
         <Spacer y={1} />
         <Input
-          label="RAM (GB)"
+          label="RAM (MB)"
           placeholder="ex: 1"
           value={service.ram}
           width="300px"
@@ -126,8 +122,6 @@ const WebTabs: React.FC<Props> = ({
   };
 
   const renderAdvanced = () => {
-    setHeight(159);
-
     return (
       <>
         <Spacer y={1} />
@@ -151,7 +145,16 @@ const WebTabs: React.FC<Props> = ({
           { label: 'Advanced', value: 'advanced' },
         ]}
         currentTab={currentTab}
-        setCurrentTab={setCurrentTab}
+        setCurrentTab={(value: string) => {
+          if (value === 'main') {
+            setHeight(300);
+          } else if (value === 'resources') {
+            setHeight(713.5);
+          } else if (value === 'advanced') {
+            setHeight(159);
+          }
+          setCurrentTab(value);
+        }}
       />
       {currentTab === 'main' && renderMain()}
       {currentTab === 'resources' && renderResources()}

+ 9 - 6
dashboard/src/main/home/app-dashboard/new-app-flow/WorkerTabs.tsx

@@ -21,8 +21,6 @@ const WorkerTabs: React.FC<Props> = ({
   const [currentTab, setCurrentTab] = React.useState<string>('main');
 
   const renderMain = () => {
-    setHeight(159);
-
     return (
       <>
         <Spacer y={1} />
@@ -39,8 +37,6 @@ const WorkerTabs: React.FC<Props> = ({
   };
 
   const renderResources = () => {
-    setHeight(713.5);
-
     return (
       <>
         <Spacer y={1} />
@@ -53,7 +49,7 @@ const WorkerTabs: React.FC<Props> = ({
         />
         <Spacer y={1} />
         <Input
-          label="RAM (GB)"
+          label="RAM (MB)"
           placeholder="ex: 1"
           value={service.ram}
           width="300px"
@@ -126,7 +122,14 @@ const WorkerTabs: React.FC<Props> = ({
           // { label: 'Advanced', value: 'advanced' },
         ]}
         currentTab={currentTab}
-        setCurrentTab={setCurrentTab}
+        setCurrentTab={(value: string) => {
+          if (value === 'main') {
+            setHeight(159);
+          } else if (value === 'resources') {
+            setHeight(713.5);
+          }
+          setCurrentTab(value);
+        }}
       />
       {currentTab === 'main' && renderMain()}
       {currentTab === 'resources' && renderResources()}

+ 78 - 0
dashboard/src/main/home/app-dashboard/new-app-flow/schema.tsx

@@ -1,4 +1,7 @@
+import { KeyValueType } from "main/home/cluster-dashboard/env-groups/EnvGroupArray";
 import * as z from "zod";
+import { Service } from "./serviceTypes";
+import { overrideObjectValues } from "./utils";
 
 const appConfigSchema = z.object({
     run: z.string().min(1),
@@ -39,3 +42,78 @@ export const PorterYamlSchema = z.object({
     apps: AppsSchema,
     release: z.string().optional(),
 });
+
+export const createFinalPorterYaml = (
+    services: Service[],
+    dashboardSetEnvVariables: KeyValueType[],
+    porterJson: z.infer<typeof PorterYamlSchema> | undefined,
+    stackName: string,
+    projectId: number,
+    clusterId: number,
+): z.infer<typeof PorterYamlSchema> => {
+    return {
+        version: "v1stack",
+        env: combineEnv(dashboardSetEnvVariables, porterJson?.env),
+        apps: createApps(services, porterJson, stackName, projectId, clusterId),
+    };
+};
+
+const combineEnv = (
+    dashboardSetVariables: KeyValueType[],
+    porterYamlSetVariables: Record<string, string> | undefined
+): z.infer<typeof EnvSchema> => {
+    const env: z.infer<typeof EnvSchema> = {};
+    for (const { key, value } of dashboardSetVariables) {
+        env[key] = value;
+    }
+    if (porterYamlSetVariables != null) {
+        for (const [key, value] of Object.entries(porterYamlSetVariables)) {
+            env[key] = value;
+        }
+    }
+    return env;
+};
+
+const createApps = (
+    serviceList: Service[],
+    porterJson: z.infer<typeof PorterYamlSchema> | undefined,
+    stackName: string,
+    projectId: number,
+    clusterId: number,
+): z.infer<typeof AppsSchema> => {
+    const apps: z.infer<typeof AppsSchema> = {};
+    for (const service of serviceList) {
+        let config = Service.serialize(service);
+        // TODO: get rid of this block when we handle ingress on the backend
+        if (Service.isWeb(service)) {
+            const ingress = Service.handleWebIngress(
+                service,
+                stackName,
+                clusterId,
+                projectId
+            );
+            config = {
+                ...config,
+                ...ingress,
+            };
+        }
+        if (
+            porterJson != null &&
+            porterJson.apps[service.name] != null &&
+            porterJson.apps[service.name].config != null
+        ) {
+            config = overrideObjectValues(
+                config,
+                porterJson.apps[service.name].config
+            );
+        }
+        // required because of https://github.com/helm/helm/issues/9214
+        apps[Service.toHelmName(service)] = {
+            type: service.type,
+            run: service.startCommand.value,
+            config,
+        };
+    }
+
+    return apps;
+};

+ 28 - 8
dashboard/src/shared/api.tsx

@@ -165,14 +165,14 @@ const createEmailVerification = baseApi<{}, {}>("POST", (pathParams) => {
 });
 
 const getPorterApps = baseApi<
-{},
-{
-  project_id: number;
-  cluster_id: number;
-}
+  {},
+  {
+    project_id: number;
+    cluster_id: number;
+  }
 >("GET", (pathParams) => {
-let { project_id, cluster_id } = pathParams;
-return `/api/projects/${project_id}/clusters/${cluster_id}/stacks`;
+  let { project_id, cluster_id } = pathParams;
+  return `/api/projects/${project_id}/clusters/${cluster_id}/stacks`;
 });
 
 const getPorterApp = baseApi<
@@ -240,7 +240,7 @@ const deletePorterApp = baseApi<
   return `/api/projects/${project_id}/clusters/${cluster_id}/stacks/${name}`;
 });
 
-const updatePorterStack = baseApi<
+const createPorterStack = baseApi<
   {
     stack_name: string;
     porter_yaml: string;
@@ -258,6 +258,25 @@ const updatePorterStack = baseApi<
   return `/api/projects/${project_id}/clusters/${cluster_id}/stacks`;
 });
 
+const updatePorterStack = baseApi<
+  {
+    stack_name: string;
+    porter_yaml: string;
+    image_info?: {
+      repository: string;
+      tag: string;
+    }
+  },
+  {
+    project_id: number;
+    cluster_id: number;
+    stack_name: string;
+  }
+>("PATCH", (pathParams) => {
+  let { project_id, cluster_id, stack_name } = pathParams;
+  return `/api/projects/${project_id}/clusters/${cluster_id}/stacks/${stack_name}`;
+});
+
 const createEnvironment = baseApi<
   {
     name: string;
@@ -2579,6 +2598,7 @@ export default {
   createPorterApp,
   updatePorterApp,
   deletePorterApp,
+  createPorterStack,
   updatePorterStack,
   createConfigMap,
   deleteCluster,