Feroze Mohideen 3 лет назад
Родитель
Сommit
2ac182324f

+ 13 - 11
dashboard/src/main/home/app-dashboard/expanded-app/ExpandedApp.tsx

@@ -33,7 +33,7 @@ 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 { PorterJson, createFinalPorterYaml } from "../new-app-flow/schema";
 import EnvGroupArray, { KeyValueType } from "main/home/cluster-dashboard/env-groups/EnvGroupArray";
 import { PorterYamlSchema } from "../new-app-flow/schema";
 
@@ -105,13 +105,14 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
           revision: 0,
         }
       );
-
       const newAppData = {
         app: resPorterApp?.data,
         chart: resChartData?.data,
       };
+      const porterJson = await fetchPorterYamlContent('porter.yaml', newAppData);
+      setPorterJson(porterJson);
       setAppData(newAppData);
-      updateServicesAndEnvVariables(resChartData?.data);
+      updateServicesAndEnvVariables(resChartData?.data, porterJson);
     } catch (err) {
       setError(err);
       console.log(err);
@@ -194,7 +195,7 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
     }
   }
 
-  const fetchPorterYamlContent = async (porterYaml: string) => {
+  const fetchPorterYamlContent = async (porterYaml: string, appData: any): Promise<PorterJson | undefined> => {
     try {
       const res = await api.getPorterYamlContents(
         "<token>",
@@ -210,11 +211,13 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
           branch: appData.app.git_branch,
         }
       );
+      if (res.data == null || res.data == "") {
+        return undefined;
+      }
       const parsedYaml = yaml.load(atob(res.data));
       const parsedData = PorterYamlSchema.parse(parsedYaml);
-      const porterYamlToJson = parsedData as z.infer<typeof PorterYamlSchema>;
-      setPorterJson(porterYamlToJson);
-      console.log(porterJson);
+      const porterYamlToJson = parsedData as PorterJson;
+      return porterYamlToJson;
     } catch (err) {
       console.log(err);
     }
@@ -244,11 +247,11 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
     return <Icon src={src} />;
   };
 
-  const updateServicesAndEnvVariables = (currentChart?: ChartType) => {
+  const updateServicesAndEnvVariables = async (currentChart?: ChartType, porterJson?: PorterJson) => {
     const helmValues = currentChart?.config;
     const defaultValues = currentChart?.chart?.values;
     if ((defaultValues && Object.keys(defaultValues).length > 0) || (helmValues && Object.keys(helmValues).length > 0)) {
-      const svcs = Service.deserialize(helmValues, defaultValues);
+      const svcs = Service.deserialize(helmValues, defaultValues, porterJson);
       setServices(svcs);
       if (helmValues && Object.keys(helmValues).length > 0) {
         const envs = Service.retrieveEnvFromHelmValues(helmValues);
@@ -272,7 +275,7 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
         }
       );
       setComponents(res.data.Objects);
-      updateServicesAndEnvVariables(currentChart);
+      updateServicesAndEnvVariables(currentChart, porterJson);
       setLoading(false);
     } catch (error) {
       console.log(error);
@@ -388,7 +391,6 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
     const { appName } = props.match.params as any;
     if (currentCluster && appName && currentProject) {
       getPorterApp();
-      fetchPorterYamlContent('porter.yaml');
     }
   }, [currentCluster]);
 

+ 12 - 9
dashboard/src/main/home/app-dashboard/new-app-flow/JobTabs.tsx

@@ -36,9 +36,10 @@ const JobTabs: React.FC<Props> = ({
         <Input
           label="Cron schedule (leave blank to run manually)"
           placeholder="ex: */5 * * * *"
-          value={service.cronSchedule}
+          value={service.cronSchedule.value}
+          disabled={service.cronSchedule.readOnly}
           width="300px"
-          setValue={(e) => { editService({ ...service, cronSchedule: e }) }}
+          setValue={(e) => { editService({ ...service, cronSchedule: { readOnly: false, value: e } }) }}
         />
       </>
     )
@@ -49,19 +50,21 @@ const JobTabs: React.FC<Props> = ({
       <>
         <Spacer y={1} />
         <Input
-          label="CPUs"
+          label="CPUs (Mi)"
           placeholder="ex: 0.5"
-          value={service.cpu}
+          value={service.cpu.value}
+          disabled={service.cpu.readOnly}
           width="300px"
-          setValue={(e) => { editService({ ...service, cpu: e }) }}
+          setValue={(e) => { editService({ ...service, cpu: { readOnly: false, value: e } }) }}
         />
         <Spacer y={1} />
         <Input
           label="RAM (MB)"
           placeholder="ex: 1"
-          value={service.ram}
+          value={service.ram.value}
+          disabled={service.ram.readOnly}
           width="300px"
-          setValue={(e) => { editService({ ...service, ram: e }) }}
+          setValue={(e) => { editService({ ...service, ram: { readOnly: false, value: e } }) }}
         />
       </>
     )
@@ -72,8 +75,8 @@ const JobTabs: React.FC<Props> = ({
       <>
         <Spacer y={1} />
         <Checkbox
-          checked={service.jobsExecuteConcurrently}
-          toggleChecked={() => { editService({ ...service, jobsExecuteConcurrently: !service.jobsExecuteConcurrently }) }}
+          checked={service.jobsExecuteConcurrently.value}
+          toggleChecked={() => { editService({ ...service, jobsExecuteConcurrently: { readOnly: false, value: !service.jobsExecuteConcurrently.value } }) }}
         >
           <Text color="helper">Allow jobs to execute concurrently</Text>
         </Checkbox>

+ 7 - 20
dashboard/src/main/home/app-dashboard/new-app-flow/NewAppFlow.tsx

@@ -25,7 +25,7 @@ import GithubActionModal from "./GithubActionModal";
 import { GithubActionConfigType } from "shared/types";
 import Error from "components/porter/Error";
 import { z } from "zod";
-import { PorterYamlSchema, createFinalPorterYaml } from "./schema";
+import { PorterJson, PorterYamlSchema, createFinalPorterYaml } from "./schema";
 import { Service } from "./serviceTypes";
 
 type Props = RouteComponentProps & {};
@@ -92,9 +92,7 @@ const NewAppFlow: React.FC<Props> = ({ ...props }) => {
   const [buildConfig, setBuildConfig] = useState({});
   const [porterYaml, setPorterYaml] = useState("");
   const [showGHAModal, setShowGHAModal] = useState<boolean>(false);
-  const [porterJson, setPorterJson] = useState<
-    z.infer<typeof PorterYamlSchema> | undefined
-  >(undefined);
+  const [porterJson, setPorterJson] = useState<PorterJson | undefined>(undefined);
   const [detected, setDetected] = useState<Detected | undefined>(undefined);
 
   const validatePorterYaml = (yamlString: string) => {
@@ -102,30 +100,18 @@ const NewAppFlow: React.FC<Props> = ({ ...props }) => {
     try {
       parsedYaml = yaml.load(yamlString);
       const parsedData = PorterYamlSchema.parse(parsedYaml);
-      const porterYamlToJson = parsedData as z.infer<typeof PorterYamlSchema>;
+      const porterYamlToJson = parsedData as PorterJson;
       setPorterJson(porterYamlToJson);
       const newServices = [];
       const existingServices = formState.serviceList.map((s) => s.name);
       for (const [name, app] of Object.entries(porterYamlToJson.apps)) {
         if (!existingServices.includes(name)) {
           if (app.type) {
-            newServices.push(
-              Service.default(name, app.type, {
-                readOnly: true,
-                value: app.run,
-              })
-            );
+            newServices.push(Service.default(name, app.type, porterYamlToJson));
           } else if (name.includes("web")) {
-            newServices.push(
-              Service.default(name, "web", { readOnly: true, value: app.run })
-            );
+            newServices.push(Service.default(name, "web", porterYamlToJson));
           } else {
-            newServices.push(
-              Service.default(name, "worker", {
-                readOnly: true,
-                value: app.run,
-              })
-            );
+            newServices.push(Service.default(name, "worker", porterYamlToJson));
           }
         }
       }
@@ -202,6 +188,7 @@ const NewAppFlow: React.FC<Props> = ({ ...props }) => {
         currentProject.id,
         currentCluster.id
       );
+
       const yamlString = yaml.dump(finalPorterYaml);
       const base64Encoded = btoa(yamlString);
       const imageInfo = imageUrl

+ 2 - 2
dashboard/src/main/home/app-dashboard/new-app-flow/ServiceContainer.tsx

@@ -62,11 +62,11 @@ const ServiceContainer: React.FC<ServiceProps> = ({
           {renderIcon(service)}
           {service.name.trim().length > 0 ? service.name : "New Service"}
         </ServiceTitle>
-        <ActionButton onClick={(e) => {
+        {service.canDelete && <ActionButton onClick={(e) => {
           deleteService();
         }}>
           <span className="material-icons">delete</span>
-        </ActionButton>
+        </ActionButton>}
       </ServiceHeader>
       <AnimateHeight
         height={showExpanded ? height : 0}

+ 32 - 22
dashboard/src/main/home/app-dashboard/new-app-flow/WebTabs.tsx

@@ -36,14 +36,16 @@ const WebTabs: React.FC<Props> = ({
         <Input
           label="Container port"
           placeholder="ex: 80"
-          value={service.port}
+          value={service.port.value}
+          disabled={service.port.readOnly}
           width="300px"
-          setValue={(e) => { editService({ ...service, port: e }) }}
+          setValue={(e) => { editService({ ...service, port: { readOnly: false, value: e } }) }}
         />
         <Spacer y={1} />
         <Checkbox
-          checked={service.generateUrlForExternalTraffic}
-          toggleChecked={() => { editService({ ...service, generateUrlForExternalTraffic: !service.generateUrlForExternalTraffic }) }}
+          checked={service.generateUrlForExternalTraffic.value}
+          // disabled={service.generateUrlForExternalTraffic.readOnly}
+          toggleChecked={() => { editService({ ...service, generateUrlForExternalTraffic: { readOnly: false, value: !service.generateUrlForExternalTraffic.value } }) }}
         >
           <Text color="helper">Generate a Porter URL for external traffic</Text>
         </Checkbox>
@@ -58,30 +60,33 @@ const WebTabs: React.FC<Props> = ({
         <Input
           label="CPUs"
           placeholder="ex: 0.5"
-          value={service.cpu}
+          value={service.cpu.value}
+          disabled={service.cpu.readOnly}
           width="300px"
-          setValue={(e) => { editService({ ...service, cpu: e }) }}
+          setValue={(e) => { editService({ ...service, cpu: { readOnly: false, value: e } }) }}
         />
         <Spacer y={1} />
         <Input
           label="RAM (MB)"
           placeholder="ex: 1"
-          value={service.ram}
+          value={service.ram.value}
+          disabled={service.ram.readOnly}
           width="300px"
-          setValue={(e) => { editService({ ...service, ram: e }) }}
+          setValue={(e) => { editService({ ...service, ram: { readOnly: false, value: e } }) }}
         />
         <Spacer y={1} />
         <Input
           label="Replicas"
           placeholder="ex: 1"
-          value={service.replicas}
+          value={service.replicas.value}
+          disabled={service.replicas.readOnly}
           width="300px"
-          setValue={(e) => { editService({ ...service, replicas: e }) }}
+          setValue={(e) => { editService({ ...service, replicas: { readOnly: false, value: e } }) }}
         />
         <Spacer y={1} />
         <Checkbox
-          checked={service.autoscalingOn}
-          toggleChecked={() => { editService({ ...service, autoscalingOn: !service.autoscalingOn }) }}
+          checked={service.autoscalingOn.value}
+          toggleChecked={() => { editService({ ...service, autoscalingOn: { readOnly: false, value: !service.autoscalingOn.value } }) }}
         >
           <Text color="helper">Enable autoscaling (overrides replicas)</Text>
         </Checkbox>
@@ -89,33 +94,37 @@ const WebTabs: React.FC<Props> = ({
         <Input
           label="Min replicas"
           placeholder="ex: 1"
-          value={service.minReplicas}
+          value={service.minReplicas.value}
+          disabled={service.minReplicas.readOnly}
           width="300px"
-          setValue={(e) => { editService({ ...service, minReplicas: e }) }}
+          setValue={(e) => { editService({ ...service, minReplicas: { readOnly: false, value: e } }) }}
         />
         <Spacer y={1} />
         <Input
           label="Max replicas"
           placeholder="ex: 10"
-          value={service.maxReplicas}
+          value={service.maxReplicas.value}
+          disabled={service.maxReplicas.readOnly}
           width="300px"
-          setValue={(e) => { editService({ ...service, maxReplicas: e }) }}
+          setValue={(e) => { editService({ ...service, maxReplicas: { readOnly: false, value: e } }) }}
         />
         <Spacer y={1} />
         <Input
           label="Target CPU utilization (%)"
           placeholder="ex: 50"
-          value={service.targetCPUUtilizationPercentage}
+          value={service.targetCPUUtilizationPercentage.value}
+          disabled={service.targetCPUUtilizationPercentage.readOnly}
           width="300px"
-          setValue={(e) => { editService({ ...service, targetCPUUtilizationPercentage: e }) }}
+          setValue={(e) => { editService({ ...service, targetCPUUtilizationPercentage: { readOnly: false, value: e } }) }}
         />
         <Spacer y={1} />
         <Input
           label="Target RAM utilization (%)"
           placeholder="ex: 50"
-          value={service.targetRAMUtilizationPercentage}
+          value={service.targetRAMUtilizationPercentage.value}
+          disabled={service.targetRAMUtilizationPercentage.readOnly}
           width="300px"
-          setValue={(e) => { editService({ ...service, targetRAMUtilizationPercentage: e }) }}
+          setValue={(e) => { editService({ ...service, targetRAMUtilizationPercentage: { readOnly: false, value: e } }) }}
         />
       </>
     )
@@ -128,9 +137,10 @@ const WebTabs: React.FC<Props> = ({
         <Input
           label="Custom domain"
           placeholder="ex: my-app.my-domain.com"
-          value={service.customDomain ?? ''}
+          value={service.customDomain.value}
+          disabled={service.customDomain.readOnly}
           width="300px"
-          setValue={(e) => { editService({ ...service, customDomain: e }) }}
+          setValue={(e) => { editService({ ...service, customDomain: { readOnly: false, value: e } }) }}
         />
       </>
     );

+ 23 - 25
dashboard/src/main/home/app-dashboard/new-app-flow/WorkerTabs.tsx

@@ -43,30 +43,33 @@ const WorkerTabs: React.FC<Props> = ({
         <Input
           label="CPUs"
           placeholder="ex: 0.5"
-          value={service.cpu}
+          value={service.cpu.value}
+          disabled={service.cpu.readOnly}
           width="300px"
-          setValue={(e) => { editService({ ...service, cpu: e }) }}
+          setValue={(e) => { editService({ ...service, cpu: { readOnly: false, value: e } }) }}
         />
         <Spacer y={1} />
         <Input
           label="RAM (MB)"
           placeholder="ex: 1"
-          value={service.ram}
+          value={service.ram.value}
+          disabled={service.ram.readOnly}
           width="300px"
-          setValue={(e) => { editService({ ...service, ram: e }) }}
+          setValue={(e) => { editService({ ...service, ram: { readOnly: false, value: e } }) }}
         />
         <Spacer y={1} />
         <Input
           label="Replicas"
           placeholder="ex: 1"
-          value={service.replicas}
+          value={service.replicas.value}
+          disabled={service.replicas.readOnly}
           width="300px"
-          setValue={(e) => { editService({ ...service, replicas: e }) }}
+          setValue={(e) => { editService({ ...service, replicas: { readOnly: false, value: e } }) }}
         />
         <Spacer y={1} />
         <Checkbox
-          checked={service.autoscalingOn}
-          toggleChecked={() => { editService({ ...service, autoscalingOn: !service.autoscalingOn }) }}
+          checked={service.autoscalingOn.value}
+          toggleChecked={() => { editService({ ...service, autoscalingOn: { readOnly: false, value: !service.autoscalingOn.value } }) }}
         >
           <Text color="helper">Enable autoscaling (overrides replicas)</Text>
         </Checkbox>
@@ -74,52 +77,48 @@ const WorkerTabs: React.FC<Props> = ({
         <Input
           label="Min replicas"
           placeholder="ex: 1"
-          value={service.minReplicas}
+          value={service.minReplicas.value}
+          disabled={service.minReplicas.readOnly}
           width="300px"
-          setValue={(e) => { editService({ ...service, minReplicas: e }) }}
+          setValue={(e) => { editService({ ...service, minReplicas: { readOnly: false, value: e } }) }}
         />
         <Spacer y={1} />
         <Input
           label="Max replicas"
           placeholder="ex: 10"
-          value={service.maxReplicas}
+          value={service.maxReplicas.value}
+          disabled={service.maxReplicas.readOnly}
           width="300px"
-          setValue={(e) => { editService({ ...service, maxReplicas: e }) }}
+          setValue={(e) => { editService({ ...service, maxReplicas: { readOnly: false, value: e } }) }}
         />
         <Spacer y={1} />
         <Input
           label="Target CPU utilization (%)"
           placeholder="ex: 50"
-          value={service.targetCPUUtilizationPercentage}
+          value={service.targetCPUUtilizationPercentage.value}
+          disabled={service.targetCPUUtilizationPercentage.readOnly}
           width="300px"
-          setValue={(e) => { editService({ ...service, targetCPUUtilizationPercentage: e }) }}
+          setValue={(e) => { editService({ ...service, targetCPUUtilizationPercentage: { readOnly: false, value: e } }) }}
         />
         <Spacer y={1} />
         <Input
           label="Target RAM utilization (%)"
           placeholder="ex: 50"
-          value={service.targetRAMUtilizationPercentage}
+          value={service.targetRAMUtilizationPercentage.value}
+          disabled={service.targetRAMUtilizationPercentage.readOnly}
           width="300px"
-          setValue={(e) => { editService({ ...service, targetRAMUtilizationPercentage: e }) }}
+          setValue={(e) => { editService({ ...service, targetRAMUtilizationPercentage: { readOnly: false, value: e } }) }}
         />
       </>
     )
   };
 
-  const renderAdvanced = () => {
-    return (
-      <>
-      </>
-    );
-  };
-
   return (
     <>
       <TabSelector
         options={[
           { label: 'Main', value: 'main' },
           { label: 'Resources', value: 'resources' },
-          // { label: 'Advanced', value: 'advanced' },
         ]}
         currentTab={currentTab}
         setCurrentTab={(value: string) => {
@@ -133,7 +132,6 @@ const WorkerTabs: React.FC<Props> = ({
       />
       {currentTab === 'main' && renderMain()}
       {currentTab === 'resources' && renderResources()}
-      {/* currentTab === 'advanced' && renderAdvanced() */}
     </>
   )
 }

+ 5 - 3
dashboard/src/main/home/app-dashboard/new-app-flow/schema.tsx

@@ -46,11 +46,11 @@ export const PorterYamlSchema = z.object({
 export const createFinalPorterYaml = (
     services: Service[],
     dashboardSetEnvVariables: KeyValueType[],
-    porterJson: z.infer<typeof PorterYamlSchema> | undefined,
+    porterJson: PorterJson | undefined,
     stackName: string,
     projectId: number,
     clusterId: number,
-): z.infer<typeof PorterYamlSchema> => {
+): PorterJson => {
     return {
         version: "v1stack",
         env: combineEnv(dashboardSetEnvVariables, porterJson?.env),
@@ -76,7 +76,7 @@ const combineEnv = (
 
 const createApps = (
     serviceList: Service[],
-    porterJson: z.infer<typeof PorterYamlSchema> | undefined,
+    porterJson: PorterJson | undefined,
     stackName: string,
     projectId: number,
     clusterId: number,
@@ -117,3 +117,5 @@ const createApps = (
 
     return apps;
 };
+
+export type PorterJson = z.infer<typeof PorterYamlSchema>;

+ 163 - 134
dashboard/src/main/home/app-dashboard/new-app-flow/serviceTypes.ts

@@ -1,210 +1,230 @@
 import _ from "lodash";
 import { overrideObjectValues } from "./utils";
 import { KeyValueType } from "main/home/cluster-dashboard/env-groups/EnvGroupArray";
+import { PorterJson } from "./schema";
 
 export type Service = WorkerService | WebService | JobService;
 export type ServiceType = 'web' | 'worker' | 'job';
 
-type ServiceReadOnlyField = {
+type ServiceString = {
     readOnly: boolean;
     value: string;
 }
+type ServiceBoolean = {
+    readOnly: boolean;
+    value: boolean;
+}
+
+const ServiceField = {
+    string: (defaultValue: string, overrideValue?: string): ServiceString => {
+        return {
+            readOnly: overrideValue != null,
+            value: overrideValue ?? defaultValue,
+        }
+    },
+    boolean: (defaultValue: boolean, overrideValue?: boolean): ServiceBoolean => {
+        return {
+            readOnly: overrideValue != null,
+            value: overrideValue ?? defaultValue,
+        }
+    },
+}
 
 type SharedServiceParams = {
     name: string;
-    cpu: string;
-    ram: string;
-    startCommand: ServiceReadOnlyField;
+    cpu: ServiceString;
+    ram: ServiceString;
+    startCommand: ServiceString;
     type: ServiceType;
+    canDelete: boolean;
 }
 
 export type WorkerService = SharedServiceParams & {
     type: 'worker';
-    replicas: string;
-    autoscalingOn: boolean;
-    minReplicas: string;
-    maxReplicas: string;
-    targetCPUUtilizationPercentage: string;
-    targetRAMUtilizationPercentage: string;
+    replicas: ServiceString;
+    autoscalingOn: ServiceBoolean;
+    minReplicas: ServiceString;
+    maxReplicas: ServiceString;
+    targetCPUUtilizationPercentage: ServiceString;
+    targetRAMUtilizationPercentage: ServiceString;
 }
 const WorkerService = {
-    default: (name: string, startCommand: ServiceReadOnlyField): WorkerService => ({
+    default: (name: string, porterJson?: PorterJson): WorkerService => ({
         name,
-        cpu: '100',
-        ram: '256',
-        startCommand: startCommand,
+        cpu: ServiceField.string('100', porterJson?.apps?.[name]?.config?.resources?.requests?.cpu ? porterJson?.apps?.[name]?.config?.resources?.requests?.cpu.replace('m', '') : undefined),
+        ram: ServiceField.string('256', porterJson?.apps?.[name]?.config?.resources?.requests?.ram ? porterJson?.apps?.[name]?.config?.resources?.requests?.ram.replace('Mi', '') : undefined),
+        startCommand: ServiceField.string('', porterJson?.apps?.[name]?.run),
         type: 'worker',
-        replicas: '1',
-        autoscalingOn: false,
-        minReplicas: '1',
-        maxReplicas: '10',
-        targetCPUUtilizationPercentage: '50',
-        targetRAMUtilizationPercentage: '50',
+        replicas: ServiceField.string('1', porterJson?.apps?.[name]?.config?.replicaCount),
+        autoscalingOn: ServiceField.boolean(false, porterJson?.apps?.[name]?.config?.autoscaling?.enabled),
+        minReplicas: ServiceField.string('1', porterJson?.apps?.[name]?.config?.autoscaling?.minReplicas),
+        maxReplicas: ServiceField.string('10', porterJson?.apps?.[name]?.config?.autoscaling?.maxReplicas),
+        targetCPUUtilizationPercentage: ServiceField.string('50', porterJson?.apps?.[name]?.config?.autoscaling?.targetCPUUtilizationPercentage),
+        targetRAMUtilizationPercentage: ServiceField.string('50', porterJson?.apps?.[name]?.config?.autoscaling?.targetMemoryUtilizationPercentage),
+        canDelete: porterJson?.apps?.[name] == null,
     }),
     serialize: (service: WorkerService) => {
-        const autoscaling = service.autoscalingOn ? {
+        const autoscaling = service.autoscalingOn.value ? {
             autoscaling: {
                 enabled: true,
-                minReplicas: service.minReplicas,
-                maxReplicas: service.maxReplicas,
-                targetCPUUtilizationPercentage: service.targetCPUUtilizationPercentage,
-                targetMemoryUtilizationPercentage: service.targetRAMUtilizationPercentage,
+                minReplicas: service.minReplicas.value,
+                maxReplicas: service.maxReplicas.value,
+                targetCPUUtilizationPercentage: service.targetCPUUtilizationPercentage.value,
+                targetMemoryUtilizationPercentage: service.targetRAMUtilizationPercentage.value,
             }
         } : {};
         return {
-            replicaCount: service.replicas,
+            replicaCount: service.replicas.value,
             container: {
                 command: service.startCommand.value,
             },
             resources: {
                 requests: {
-                    cpu: service.cpu + 'm',
-                    memory: service.ram + 'Mi',
+                    cpu: service.cpu.value + 'm',
+                    memory: service.ram.value + 'Mi',
                 }
             },
             ...autoscaling,
         }
     },
-    deserialize: (name: string, values: any): WorkerService => {
+    deserialize: (name: string, values: any, porterJson?: PorterJson): WorkerService => {
         return {
             name,
-            cpu: values.resources?.requests?.cpu?.replace('m', '') ?? '',
-            ram: values.resources?.requests?.memory?.replace('Mi', '') ?? '',
-            startCommand: {
-                readOnly: false,
-                value: values.container?.command ?? '',
-            },
+            cpu: ServiceField.string(values.resources?.requests?.cpu?.replace('m', ''), porterJson?.apps?.[name]?.config?.resources?.requests?.cpu ? porterJson?.apps?.[name]?.config?.resources?.requests?.cpu.replace('m', '') : undefined),
+            ram: ServiceField.string(values.resources?.requests?.memory?.replace('Mi', '') ?? '', porterJson?.apps?.[name]?.config?.resources?.requests?.ram ? porterJson?.apps?.[name]?.config?.resources?.requests?.ram.replace('Mi', '') : undefined),
+            startCommand: ServiceField.string(values.container?.command ?? '', porterJson?.apps?.[name]?.run),
             type: 'worker',
-            replicas: values.replicaCount ?? '',
-            autoscalingOn: values.autoscaling?.enabled ?? false,
-            minReplicas: values.autoscaling?.minReplicas ?? '',
-            maxReplicas: values.autoscaling?.maxReplicas ?? '',
-            targetCPUUtilizationPercentage: values.autoscaling?.targetCPUUtilizationPercentage ?? '',
-            targetRAMUtilizationPercentage: values.autoscaling?.targetMemoryUtilizationPercentage ?? '',
+            replicas: ServiceField.string(values.replicaCount ?? '', porterJson?.apps?.[name]?.config?.replicaCount),
+            autoscalingOn: ServiceField.boolean(values.autoscaling?.enabled ?? false, porterJson?.apps?.[name]?.config?.autoscaling?.enabled),
+            minReplicas: ServiceField.string(values.autoscaling?.minReplicas ?? '', porterJson?.apps?.[name]?.config?.autoscaling?.minReplicas),
+            maxReplicas: ServiceField.string(values.autoscaling?.maxReplicas ?? '', porterJson?.apps?.[name]?.config?.autoscaling?.maxReplicas),
+            targetCPUUtilizationPercentage: ServiceField.string(values.autoscaling?.targetCPUUtilizationPercentage ?? '', porterJson?.apps?.[name]?.config?.autoscaling?.targetCPUUtilizationPercentage),
+            targetRAMUtilizationPercentage: ServiceField.string(values.autoscaling?.targetMemoryUtilizationPercentage ?? '', porterJson?.apps?.[name]?.config?.autoscaling?.targetMemoryUtilizationPercentage),
+            canDelete: porterJson?.apps?.[name] == null,
         }
     }
 }
 
 export type WebService = SharedServiceParams & Omit<WorkerService, 'type'> & {
     type: 'web';
-    port: string;
-    generateUrlForExternalTraffic: boolean;
-    customDomain: string;
+    port: ServiceString;
+    generateUrlForExternalTraffic: ServiceBoolean;
+    customDomain: ServiceString;
 }
 const WebService = {
-    default: (name: string, startCommand: ServiceReadOnlyField): WebService => ({
+    default: (name: string, porterJson?: PorterJson): WebService => ({
         name,
-        cpu: '100',
-        ram: '256',
-        startCommand: startCommand,
+        cpu: ServiceField.string('100', porterJson?.apps?.[name]?.config?.resources?.requests?.cpu ? porterJson?.apps?.[name]?.config?.resources?.requests?.cpu.replace('m', '') : undefined),
+        ram: ServiceField.string('256', porterJson?.apps?.[name]?.config?.resources?.requests?.ram ? porterJson?.apps?.[name]?.config?.resources?.requests?.ram.replace('Mi', '') : undefined),
+        startCommand: ServiceField.string('', porterJson?.apps?.[name]?.run),
         type: 'web',
-        replicas: '1',
-        autoscalingOn: false,
-        minReplicas: '1',
-        maxReplicas: '10',
-        targetCPUUtilizationPercentage: '50',
-        targetRAMUtilizationPercentage: '50',
-        port: '80',
-        generateUrlForExternalTraffic: false,
-        customDomain: '',
+        replicas: ServiceField.string('1', porterJson?.apps?.[name]?.config?.replicaCount),
+        autoscalingOn: ServiceField.boolean(false, porterJson?.apps?.[name]?.config?.autoscaling?.enabled),
+        minReplicas: ServiceField.string('1', porterJson?.apps?.[name]?.config?.autoscaling?.minReplicas),
+        maxReplicas: ServiceField.string('10', porterJson?.apps?.[name]?.config?.autoscaling?.maxReplicas),
+        targetCPUUtilizationPercentage: ServiceField.string('50', porterJson?.apps?.[name]?.config?.autoscaling?.targetCPUUtilizationPercentage),
+        targetRAMUtilizationPercentage: ServiceField.string('50', porterJson?.apps?.[name]?.config?.autoscaling?.targetMemoryUtilizationPercentage),
+        port: ServiceField.string('8080', porterJson?.apps?.[name]?.config?.container?.port),
+        generateUrlForExternalTraffic: ServiceField.boolean(false, porterJson?.apps?.[name]?.config?.ingress?.enabled),
+        customDomain: ServiceField.string('', porterJson?.apps?.[name]?.config?.ingress?.hosts?.length ? porterJson?.apps?.[name]?.config?.ingress?.hosts[0] : undefined),
+        canDelete: porterJson?.apps?.[name] == null,
     }),
     serialize: (service: WebService) => {
-        const autoscaling = service.autoscalingOn ? {
+        const autoscaling = service.autoscalingOn.value ? {
             autoscaling: {
                 enabled: true,
-                minReplicas: service.minReplicas,
-                maxReplicas: service.maxReplicas,
-                targetCPUUtilizationPercentage: service.targetCPUUtilizationPercentage,
-                targetMemoryUtilizationPercentage: service.targetRAMUtilizationPercentage,
+                minReplicas: service.minReplicas.value,
+                maxReplicas: service.maxReplicas.value,
+                targetCPUUtilizationPercentage: service.targetCPUUtilizationPercentage.value,
+                targetMemoryUtilizationPercentage: service.targetRAMUtilizationPercentage.value,
             }
         } : {};
         return {
-            replicaCount: service.replicas,
+            replicaCount: service.replicas.value,
             resources: {
                 requests: {
-                    cpu: service.cpu + 'm',
-                    memory: service.ram + 'Mi',
+                    cpu: service.cpu.value + 'm',
+                    memory: service.ram.value + 'Mi',
                 }
             },
             container: {
                 command: service.startCommand.value,
-                port: service.port,
+                port: service.port.value,
             },
             service: {
-                port: service.port,
+                port: service.port.value,
             },
             ...autoscaling,
         }
     },
-    deserialize: (name: string, values: any): WebService => {
+    deserialize: (name: string, values: any, porterJson?: PorterJson): WebService => {
         return {
             name,
-            cpu: values.resources?.requests?.cpu?.replace('m', '') ?? '',
-            ram: values.resources?.requests?.memory?.replace('Mi', '') ?? '',
-            startCommand: {
-                readOnly: false,
-                value: values.container?.command ?? ''
-            },
+            cpu: ServiceField.string(values.resources?.requests?.cpu?.replace('m', ''), porterJson?.apps?.[name]?.config?.resources?.requests?.cpu ? porterJson?.apps?.[name]?.config?.resources?.requests?.cpu.replace('m', '') : undefined),
+            ram: ServiceField.string(values.resources?.requests?.memory?.replace('Mi', '') ?? '', porterJson?.apps?.[name]?.config?.resources?.requests?.ram ? porterJson?.apps?.[name]?.config?.resources?.requests?.ram.replace('Mi', '') : undefined),
+            startCommand: ServiceField.string(values.container?.command ?? '', porterJson?.apps?.[name]?.run),
             type: 'web',
-            replicas: values.replicaCount ?? '',
-            autoscalingOn: values.autoscaling?.enabled ?? false,
-            minReplicas: values.autoscaling?.minReplicas ?? '',
-            maxReplicas: values.autoscaling?.maxReplicas ?? '',
-            targetCPUUtilizationPercentage: values.autoscaling?.targetCPUUtilizationPercentage ?? '',
-            targetRAMUtilizationPercentage: values.autoscaling?.targetMemoryUtilizationPercentage ?? '',
-            port: values.container?.port ?? '',
-            generateUrlForExternalTraffic: values.ingress?.enabled ?? false,
-            customDomain: values.ingress?.hosts?.length ? values.ingress.hosts[0] : '',
+            replicas: ServiceField.string(values.replicaCount ?? '', porterJson?.apps?.[name]?.config?.replicaCount),
+            autoscalingOn: ServiceField.boolean(values.autoscaling?.enabled ?? false, porterJson?.apps?.[name]?.config?.autoscaling?.enabled),
+            minReplicas: ServiceField.string(values.autoscaling?.minReplicas ?? '', porterJson?.apps?.[name]?.config?.autoscaling?.minReplicas),
+            maxReplicas: ServiceField.string(values.autoscaling?.maxReplicas ?? '', porterJson?.apps?.[name]?.config?.autoscaling?.maxReplicas),
+            targetCPUUtilizationPercentage: ServiceField.string(values.autoscaling?.targetCPUUtilizationPercentage ?? '', porterJson?.apps?.[name]?.config?.autoscaling?.targetCPUUtilizationPercentage),
+            targetRAMUtilizationPercentage: ServiceField.string(values.autoscaling?.targetMemoryUtilizationPercentage ?? '', porterJson?.apps?.[name]?.config?.autoscaling?.targetMemoryUtilizationPercentage),
+            port: ServiceField.string(values.container?.port ?? '', porterJson?.apps?.[name]?.config?.container?.port),
+            generateUrlForExternalTraffic: ServiceField.boolean(values.ingress?.enabled ?? false, porterJson?.apps?.[name]?.config?.ingress?.enabled),
+            customDomain: ServiceField.string(values.ingress?.hosts?.length ? values.ingress.hosts[0] : '', porterJson?.apps?.[name]?.config?.ingress?.hosts?.length ? porterJson?.apps?.[name]?.config?.ingress?.hosts[0] : undefined),
+            canDelete: porterJson?.apps?.[name] == null,
         }
     }
 }
 
 export type JobService = SharedServiceParams & {
     type: 'job';
-    jobsExecuteConcurrently: boolean;
-    cronSchedule: string;
+    jobsExecuteConcurrently: ServiceBoolean;
+    cronSchedule: ServiceString;
 }
 const JobService = {
-    default: (name: string, startCommand: ServiceReadOnlyField): JobService => ({
+    default: (name: string, porterJson?: PorterJson): JobService => ({
         name,
-        cpu: '100',
-        ram: '256',
-        startCommand: startCommand,
+        cpu: ServiceField.string('100', porterJson?.apps?.[name]?.config?.resources?.requests?.cpu ? porterJson?.apps?.[name]?.config?.resources?.requests?.cpu.replace('m', '') : undefined),
+        ram: ServiceField.string('256', porterJson?.apps?.[name]?.config?.resources?.requests?.ram ? porterJson?.apps?.[name]?.config?.resources?.requests?.ram.replace('Mi', '') : undefined),
+        startCommand: ServiceField.string('', porterJson?.apps?.[name]?.run),
         type: 'job',
-        jobsExecuteConcurrently: false,
-        cronSchedule: '',
+        jobsExecuteConcurrently: ServiceField.boolean(false, porterJson?.apps?.[name]?.config?.allowConcurrent),
+        cronSchedule: ServiceField.string('', porterJson?.apps?.[name]?.config?.schedule?.value),
+        canDelete: porterJson?.apps?.[name] == null,
     }),
     serialize: (service: JobService) => {
-        const schedule = service.cronSchedule ? {
-            enabled: true,
-            value: service.cronSchedule,
+        const schedule = service.cronSchedule.value ? {
+            schedule: {
+                enabled: true,
+                value: service.cronSchedule.value,
+            }
         } : {};
         return {
-            allowConcurrent: service.jobsExecuteConcurrently,
+            allowConcurrent: service.jobsExecuteConcurrently.value,
             container: {
                 command: service.startCommand.value,
             },
             resources: {
                 requests: {
-                    cpu: service.cpu + 'm',
-                    memory: service.ram + 'Mi',
+                    cpu: service.cpu.value + 'm',
+                    memory: service.ram.value + 'Mi',
                 }
             },
             ...schedule,
         }
     },
-    deserialize: (name: string, values: any): JobService => {
+    deserialize: (name: string, values: any, porterJson?: PorterJson): JobService => {
         return {
             name,
-            cpu: values.resources?.requests?.cpu?.replace('m', '') ?? '',
-            ram: values.resources?.requests?.memory?.replace('Mi', '') ?? '',
-            startCommand: {
-                readOnly: false,
-                value: values.container?.command ?? ''
-            },
+            cpu: ServiceField.string(values.resources?.requests?.cpu?.replace('m', '') ?? '', porterJson?.apps?.[name]?.config?.resources?.requests?.cpu),
+            ram: ServiceField.string(values.resources?.requests?.memory?.replace('Mi', '') ?? '', porterJson?.apps?.[name]?.config?.resources?.requests?.ram),
+            startCommand: ServiceField.string(values.container?.command ?? '', porterJson?.apps?.[name]?.run),
             type: 'job',
-            jobsExecuteConcurrently: values.allowConcurrent ?? false,
-            cronSchedule: values.schedule?.value ?? '',
+            jobsExecuteConcurrently: ServiceField.boolean(values.allowConcurrent ?? false, porterJson?.apps?.[name]?.config?.allowConcurrent),
+            cronSchedule: ServiceField.string(values.schedule?.value ?? '', porterJson?.apps?.[name]?.config?.schedule?.value),
+            canDelete: porterJson?.apps?.[name] == null,
         }
     }
 }
@@ -222,14 +242,14 @@ const SUFFIX_TO_TYPE: Record<string, ServiceType> = {
 
 export const Service = {
     // populates an empty service
-    default: (name: string, type: ServiceType, startCommand: ServiceReadOnlyField) => {
+    default: (name: string, type: ServiceType, porterJson?: PorterJson) => {
         switch (type) {
             case 'web':
-                return WebService.default(name, startCommand);
+                return WebService.default(name, porterJson);
             case 'worker':
-                return WorkerService.default(name, startCommand);
+                return WorkerService.default(name, porterJson);
             case 'job':
-                return JobService.default(name, startCommand);
+                return JobService.default(name, porterJson);
         }
     },
 
@@ -245,8 +265,8 @@ export const Service = {
         }
     },
 
-    // converts a helm values object to a service
-    deserialize: (helmValues: any, defaultValues: any): Service[] => {
+    // converts a helm values object and porter json (from their repo) to a service
+    deserialize: (helmValues: any, defaultValues: any, porterJson?: PorterJson): Service[] => {
         return Object.keys(defaultValues).map((name: string) => {
             const suffix = name.slice(-4);
             if (suffix in SUFFIX_TO_TYPE) {
@@ -258,11 +278,11 @@ export const Service = {
                 );
                 switch (type) {
                     case 'web':
-                        return WebService.deserialize(appName, coalescedValues);
+                        return WebService.deserialize(appName, coalescedValues, porterJson);
                     case 'worker':
-                        return WorkerService.deserialize(appName, coalescedValues);
+                        return WorkerService.deserialize(appName, coalescedValues, porterJson);
                     case 'job':
-                        return JobService.deserialize(appName, coalescedValues);
+                        return JobService.deserialize(appName, coalescedValues, porterJson);
                 }
             }
         }).filter((service: Service | undefined): service is Service => service != null);
@@ -278,18 +298,20 @@ export const Service = {
         if (projectId == null || clusterId == null) {
             throw new Error('Project ID and Cluster ID must be provided to handle web ingress');
         }
-        if (!service.generateUrlForExternalTraffic) {
+        if (!service.generateUrlForExternalTraffic.value) {
             return {}
         }
         const ingress: Ingress = {
-            enabled: true,
-            hosts: [],
-            custom_domain: false,
-            porter_hosts: [],
+            ingress: {
+                enabled: true,
+                hosts: [],
+                custom_domain: false,
+                porter_hosts: [],
+            }
         };
-        if (service.customDomain) {
-            ingress.hosts.push(service.customDomain);
-            ingress.custom_domain = true;
+        if (service.customDomain.value) {
+            ingress.ingress.hosts.push(service.customDomain.value);
+            ingress.ingress.custom_domain = true;
         } else {
             // const res = await api
             //     .createSubdomain(
@@ -323,19 +345,26 @@ export const Service = {
         if (env == null) {
             return [];
         }
-        return Object.keys(env).map((key: string) => ({
-            key,
-            value: env[key],
-            hidden: false,
-            locked: false,
-            deleted: false,
-        }));
+        try {
+            return Object.keys(env).map((key: string) => ({
+                key,
+                value: env[key],
+                hidden: false,
+                locked: false,
+                deleted: false,
+            }));
+        } catch (err) {
+            // TODO: handle error
+            return [];
+        }
     }
 }
 
 type Ingress = {
-    enabled: boolean;
-    hosts: string[];
-    custom_domain: boolean;
-    porter_hosts: string[];
+    ingress: {
+        enabled: boolean;
+        hosts: string[];
+        custom_domain: boolean;
+        porter_hosts: string[];
+    }
 }