Sfoglia il codice sorgente

i think it works end to end now

Feroze Mohideen 3 anni fa
parent
commit
3e09ab5b6f

+ 6 - 5
dashboard/src/main/home/app-dashboard/new-app-flow/GithubActionModal.tsx

@@ -6,10 +6,8 @@ import ExpandableSection from "components/porter/ExpandableSection";
 import Fieldset from "components/porter/Fieldset";
 import styled from "styled-components";
 import Button from "components/porter/Button";
-import Input from "components/porter/Input";
 import Select from "components/porter/Select";
 import api from "shared/api";
-import { Context } from "shared/Context";
 
 interface GithubActionModalProps {
   closeModal: () => void;
@@ -38,12 +36,15 @@ const GithubActionModal: React.FC<GithubActionModalProps> = ({
 }) => {
   const [choice, setChoice] = React.useState<Choice>("open_pr");
   const [loading, setLoading] = React.useState<boolean>(false);
-  const { currentProject, currentCluster } = useContext(Context);
 
   const submit = async () => {
     if (githubAppInstallationID && githubRepoOwner && githubRepoName && branch && stackName) {
       try {
         setLoading(true)
+        // this creates the dummy chart
+        deployPorterApp();
+
+        // this creates the secret and possily the PR
         const res = await api.createSecretAndOpenGitHubPullRequest(
           "<token>",
           {
@@ -60,7 +61,7 @@ const GithubActionModal: React.FC<GithubActionModalProps> = ({
           }
         );
         if (res?.data?.url) {
-            window.open(res.data.url, "_blank", "noreferrer")
+          window.open(res.data.url, "_blank", "noreferrer")
         }
       } catch (error) {
         console.log(error)
@@ -116,7 +117,7 @@ const GithubActionModal: React.FC<GithubActionModalProps> = ({
           { label: "I authorize Porter to open a PR on my behalf", value: "open_pr" },
           { label: "I will copy the file into my repository myself", value: "copy" },
         ]}
-        setValue={(x: Choice) => setChoice(x)}
+        setValue={(x: string) => setChoice(x as Choice)}
         width="100%"
       />
       <Button

+ 79 - 34
dashboard/src/main/home/app-dashboard/new-app-flow/NewAppFlow.tsx

@@ -37,8 +37,9 @@ import {
   GithubActionConfigType,
 } from "shared/types";
 import { z } from "zod";
-import { PorterYamlSchema } from "./schema";
-import { createDefaultService } from "./serviceTypes";
+import { AppsSchema, EnvSchema, PorterYamlSchema } from "./schema";
+import { Service } from "./serviceTypes";
+import { overrideObjectValues } from "./utils";
 
 type Props = RouteComponentProps & {};
 
@@ -53,7 +54,7 @@ const defaultActionConfig: GithubActionConfigType = {
 interface FormState {
   applicationName: string;
   selectedSourceType: SourceType | undefined;
-  serviceList: any[];
+  serviceList: Service[];
   envVariables: KeyValueType[];
   releaseCommand: string;
 }
@@ -71,7 +72,7 @@ const Validators: {
 } = {
   applicationName: (value: string) => value.trim().length > 0,
   selectedSourceType: (value: SourceType | undefined) => value !== undefined,
-  serviceList: (value: any[]) => value.length > 0,
+  serviceList: (value: Service[]) => value.length > 0,
   envVariables: (value: KeyValueType[]) => true,
   releaseCommand: (value: string) => true,
 };
@@ -95,24 +96,8 @@ const NewAppFlow: React.FC<Props> = ({ ...props }) => {
   const [dockerfilePath, setDockerfilePath] = useState(null);
   const [procfilePath, setProcfilePath] = useState(null);
   const [folderPath, setFolderPath] = useState(null);
-  const [selectedRegistry, setSelectedRegistry] = useState(null);
-  const [shouldCreateWorkflow, setShouldCreateWorkflow] = useState(true);
   const [buildConfig, setBuildConfig] = useState();
   const [porterYaml, setPorterYaml] = useState("");
-  const getFullActionConfig = (): FullGithubActionConfigType => {
-    let imageRepoURI = `${selectedRegistry?.url}/${templateName}`;
-    return {
-      kind: "github",
-      git_repo: actionConfig.git_repo,
-      git_branch: branch,
-      registry_id: selectedRegistry?.id,
-      dockerfile_path: dockerfilePath,
-      folder_path: folderPath,
-      image_repo_uri: imageRepoURI,
-      git_repo_id: actionConfig.git_repo_id,
-      should_create_workflow: shouldCreateWorkflow,
-    };
-  };
   const [showGHAModal, setShowGHAModal] = useState<boolean>(false);
   const [porterJson, setPorterJson] = useState<z.infer<typeof PorterYamlSchema>>(null);
 
@@ -121,21 +106,26 @@ const NewAppFlow: React.FC<Props> = ({ ...props }) => {
     try {
       parsedYaml = yaml.load(yamlString);
       const parsedData = PorterYamlSchema.parse(parsedYaml);
-      const porterYaml = parsedData as z.infer<typeof PorterYamlSchema>;
-      setPorterJson(porterYaml)
-      // go through key value pairs and create services from them
+      const porterYamlToJson = parsedData as z.infer<typeof PorterYamlSchema>;
+      setPorterJson(porterYamlToJson)
+      console.log(porterYamlToJson)
+      // go through key value pairs and create services from them, if they don't already exist
       const newServices = [];
-      for (const [name, app] of Object.entries(porterYaml.apps)) {
-        if (app.type) {
-          newServices.push(createDefaultService(name, app.type, { readOnly: true, value: app.run }))
-        } else if (name.includes('web')) {
-          newServices.push(createDefaultService(name, 'web', { readOnly: true, value: app.run }))
-        } else {
-          newServices.push(createDefaultService(name, 'worker', { readOnly: true, value: app.run }))
+      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 }))
+          } else if (name.includes('web')) {
+            newServices.push(Service.default(name, 'web', { readOnly: true, value: app.run }))
+          } else {
+            newServices.push(Service.default(name, 'worker', { readOnly: true, value: app.run }))
+          }
         }
       }
-      setFormState({ ...formState, serviceList: [...formState.serviceList, ...newServices] });
-      if (Validators.serviceList(formState.serviceList)) {
+      const newServiceList = [...formState.serviceList, ...newServices];
+      setFormState({ ...formState, serviceList: newServiceList });
+      if (Validators.serviceList(newServiceList)) {
         setCurrentStep(Math.max(currentStep, 4));
       }
     } catch (error) {
@@ -168,7 +158,7 @@ const NewAppFlow: React.FC<Props> = ({ ...props }) => {
   };
   const deployPorterApp = async () => {
     try {
-      // Write build settings to the DB
+      //Write build settings to the DB
       const res = await api.createPorterApp(
         "<token>",
         {
@@ -186,6 +176,23 @@ const NewAppFlow: React.FC<Props> = ({ ...props }) => {
           project_id: currentProject.id,
         }
       );
+
+      const finalPorterYaml = createFinalPorterYaml();
+      const yamlString = yaml.dump(finalPorterYaml);
+      const base64Encoded = btoa(yamlString);
+
+      //create dummy chart
+      await api.updatePorterStack(
+        "<token>",
+        {
+          stack_name: formState.applicationName,
+          porter_yaml: base64Encoded,
+        },
+        {
+          cluster_id: currentCluster.id,
+          project_id: currentProject.id,
+        }
+      )
     } catch (err) {
       console.log(err);
     }
@@ -193,6 +200,44 @@ const NewAppFlow: React.FC<Props> = ({ ...props }) => {
     // TODO: update Porter stack
   };
 
+  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);
+      if (porterJson != null && porterJson.apps[service.name] != null && porterJson.apps[service.name].config != null) {
+        config = overrideObjectValues(config, porterJson.apps[service.name].config)
+      }
+      apps[service.name] = {
+        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>
@@ -281,7 +326,7 @@ const NewAppFlow: React.FC<Props> = ({ ...props }) => {
                 <Spacer y={0.5} />
                 {porterJson && porterJson.apps && Object.keys(porterJson.apps).length > 0 &&
                   <AppearingDiv>
-                    <Text size={16} color={"green"}>Autodetected {Object.keys(porterJson.apps).length} services from porter.yml</Text>
+                    <Text size={16} color={"green"}>Auto-detected {Object.keys(porterJson.apps).length} services from porter.yaml!</Text>
                     <Spacer y={1} />
                   </AppearingDiv>
                 }

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

@@ -12,7 +12,7 @@ import Button from "components/porter/Button";
 import web from "assets/web.png";
 import worker from "assets/worker.png";
 import job from "assets/job.png";
-import { Service, ServiceType, createDefaultService } from "./serviceTypes";
+import { Service, ServiceType } from "./serviceTypes";
 
 interface ServicesProps {
   services: Service[];
@@ -99,7 +99,7 @@ const Services: React.FC<ServicesProps> = ({ services, setServices }) => {
             onClick={() => {
               setServices([
                 ...services,
-                createDefaultService(serviceName, serviceType, { readOnly: false, value: '' }),
+                Service.default(serviceName, serviceType, { readOnly: false, value: '' }),
               ]);
               setShowAddServiceModal(false);
               setServiceName("");

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

@@ -6,11 +6,11 @@ const appConfigSchema = z.object({
     type: z.enum(['web', 'worker', 'job']).optional(),
 });
 
-const appsSchema = z.record(appConfigSchema);
+export const AppsSchema = z.record(appConfigSchema);
 
-const envSchema = z.record(z.string());
+export const EnvSchema = z.record(z.string());
 
-const buildSchema = z.object({
+export const BuildSchema = z.object({
     method: z.string().refine(value => ["pack", "docker", "registry"].includes(value)),
     context: z.string().optional(),
     builder: z.string().optional(),
@@ -34,8 +34,8 @@ const buildSchema = z.object({
 
 export const PorterYamlSchema = z.object({
     version: z.string().optional(),
-    build: buildSchema.optional(),
-    env: envSchema.optional(),
-    apps: appsSchema,
+    build: BuildSchema.optional(),
+    env: EnvSchema.optional(),
+    apps: AppsSchema,
     release: z.string().optional(),
 });

+ 95 - 10
dashboard/src/main/home/app-dashboard/new-app-flow/serviceTypes.ts

@@ -37,13 +37,34 @@ const WorkerService = {
         targetCPUUtilizationPercentage: '50',
         targetRAMUtilizationPercentage: '50',
     }),
+    serialize: (service: WorkerService) => {
+        const autoscaling = service.autoscalingOn ? {
+            autoscaling: {
+                enabled: true,
+                minReplicas: service.minReplicas,
+                maxReplicas: service.maxReplicas,
+                targetCPUUtilizationPercentage: service.targetCPUUtilizationPercentage,
+                targetMemoryUtilizationPercentage: service.targetRAMUtilizationPercentage,
+            }
+        } : {};
+        return {
+            replicaCount: service.replicas,
+            resources: {
+                requests: {
+                    cpu: service.cpu + 'm',
+                    memory: service.ram + 'Mi',
+                }
+            },
+            ...autoscaling,
+        }
+    }
 }
 
 export type WebService = SharedServiceParams & Omit<WorkerService, 'type'> & {
     type: 'web';
     port: string;
     generateUrlForExternalTraffic: boolean;
-    customDomain?: string;
+    customDomain: string;
 }
 const WebService = {
     default: (name: string, startCommand: ServiceReadOnlyField): WebService => ({
@@ -60,7 +81,43 @@ const WebService = {
         targetRAMUtilizationPercentage: '50',
         port: '80',
         generateUrlForExternalTraffic: true,
+        customDomain: '',
     }),
+    serialize: (service: WebService) => {
+        const autoscaling = service.autoscalingOn ? {
+            autoscaling: {
+                enabled: true,
+                minReplicas: service.minReplicas,
+                maxReplicas: service.maxReplicas,
+                targetCPUUtilizationPercentage: service.targetCPUUtilizationPercentage,
+                targetMemoryUtilizationPercentage: service.targetRAMUtilizationPercentage,
+            }
+        } : {};
+        const ingress = service.generateUrlForExternalTraffic ? {
+            ingress: {
+                enabled: true,
+                custom_domain: service.customDomain ? true : false,
+                hosts: service.customDomain ? [service.customDomain] : [],
+            }
+        } : {};
+        return {
+            replicaCount: service.replicas,
+            resources: {
+                requests: {
+                    cpu: service.cpu + 'm',
+                    memory: service.ram + 'Mi',
+                }
+            },
+            container: {
+                port: service.port,
+            },
+            service: {
+                port: service.port,
+            },
+            ...autoscaling,
+            ...ingress,
+        }
+    }
 }
 
 export type JobService = SharedServiceParams & {
@@ -78,15 +135,43 @@ const JobService = {
         jobsExecuteConcurrently: false,
         cronSchedule: '',
     }),
+    serialize: (service: JobService) => {
+        const schedule = service.cronSchedule ? {
+            enabled: true,
+            value: service.cronSchedule,
+        } : {};
+        return {
+            allowConcurrent: service.jobsExecuteConcurrently,
+            resources: {
+                requests: {
+                    cpu: service.cpu + 'm',
+                    memory: service.ram + 'Mi',
+                }
+            },
+            ...schedule,
+        }
+    }
 }
 
-export const createDefaultService = (name: string, type: ServiceType, startCommand: ServiceReadOnlyField) => {
-    switch (type) {
-        case 'web':
-            return WebService.default(name, startCommand);
-        case 'worker':
-            return WorkerService.default(name, startCommand);
-        case 'job':
-            return JobService.default(name, startCommand);
+export const Service = {
+    default: (name: string, type: ServiceType, startCommand: ServiceReadOnlyField) => {
+        switch (type) {
+            case 'web':
+                return WebService.default(name, startCommand);
+            case 'worker':
+                return WorkerService.default(name, startCommand);
+            case 'job':
+                return JobService.default(name, startCommand);
+        }
+    },
+    serialize: (service: Service) => {
+        switch (service.type) {
+            case 'web':
+                return WebService.serialize(service);
+            case 'worker':
+                return WorkerService.serialize(service);
+            case 'job':
+                return JobService.serialize(service);
+        }
     }
-}
+} 

+ 16 - 0
dashboard/src/main/home/app-dashboard/new-app-flow/utils.tsx

@@ -0,0 +1,16 @@
+export const overrideObjectValues = (obj1: any, obj2: any) => {
+    // Iterate over the keys in obj2
+    for (const key in obj2) {
+        // Check if the key exists in obj1 and if its value is an object
+        if (key in obj1 && typeof obj1[key] === 'object' && typeof obj2[key] === 'object') {
+            // Recursively call the function to handle the nested object
+            obj1[key] = overrideObjectValues(obj1[key], obj2[key]);
+        } else {
+            // Otherwise, just assign the value from obj2 to obj1
+            obj1[key] = obj2[key];
+        }
+    }
+
+    // Return the merged object
+    return obj1;
+};

+ 17 - 33
dashboard/src/shared/api.tsx

@@ -198,13 +198,7 @@ const createPorterApp = baseApi<
 const updatePorterStack = baseApi<
   {
     stack_name: string;
-    dependencies: {
-      name: string;
-      alias: string;
-      version: string;
-      repository: string;
-    }[];
-    values: any;
+    porter_yaml: string;
   },
   {
     project_id: number;
@@ -637,11 +631,9 @@ const detectBuildpack = baseApi<
     branch: string;
   }
 >("GET", (pathParams) => {
-  return `/api/projects/${pathParams.project_id}/gitrepos/${
-    pathParams.git_repo_id
-  }/repos/${pathParams.kind}/${pathParams.owner}/${
-    pathParams.name
-  }/${encodeURIComponent(pathParams.branch)}/buildpack/detect`;
+  return `/api/projects/${pathParams.project_id}/gitrepos/${pathParams.git_repo_id
+    }/repos/${pathParams.kind}/${pathParams.owner}/${pathParams.name
+    }/${encodeURIComponent(pathParams.branch)}/buildpack/detect`;
 });
 
 const detectGitlabBuildpack = baseApi<
@@ -672,11 +664,9 @@ const getBranchContents = baseApi<
     branch: string;
   }
 >("GET", (pathParams) => {
-  return `/api/projects/${pathParams.project_id}/gitrepos/${
-    pathParams.git_repo_id
-  }/repos/${pathParams.kind}/${pathParams.owner}/${
-    pathParams.name
-  }/${encodeURIComponent(pathParams.branch)}/contents`;
+  return `/api/projects/${pathParams.project_id}/gitrepos/${pathParams.git_repo_id
+    }/repos/${pathParams.kind}/${pathParams.owner}/${pathParams.name
+    }/${encodeURIComponent(pathParams.branch)}/contents`;
 });
 
 const getProcfileContents = baseApi<
@@ -692,11 +682,9 @@ const getProcfileContents = baseApi<
     branch: string;
   }
 >("GET", (pathParams) => {
-  return `/api/projects/${pathParams.project_id}/gitrepos/${
-    pathParams.git_repo_id
-  }/repos/${pathParams.kind}/${pathParams.owner}/${
-    pathParams.name
-  }/${encodeURIComponent(pathParams.branch)}/procfile`;
+  return `/api/projects/${pathParams.project_id}/gitrepos/${pathParams.git_repo_id
+    }/repos/${pathParams.kind}/${pathParams.owner}/${pathParams.name
+    }/${encodeURIComponent(pathParams.branch)}/procfile`;
 });
 
 const getPorterYamlContents = baseApi<
@@ -712,11 +700,9 @@ const getPorterYamlContents = baseApi<
     branch: string;
   }
 >("GET", (pathParams) => {
-  return `/api/projects/${pathParams.project_id}/gitrepos/${
-    pathParams.git_repo_id
-  }/repos/${pathParams.kind}/${pathParams.owner}/${
-    pathParams.name
-  }/${encodeURIComponent(pathParams.branch)}/porteryaml`;
+  return `/api/projects/${pathParams.project_id}/gitrepos/${pathParams.git_repo_id
+    }/repos/${pathParams.kind}/${pathParams.owner}/${pathParams.name
+    }/${encodeURIComponent(pathParams.branch)}/porteryaml`;
 });
 
 const getGitlabProcfileContents = baseApi<
@@ -1570,11 +1556,9 @@ const getEnvGroup = baseApi<
     version?: number;
   }
 >("GET", (pathParams) => {
-  return `/api/projects/${pathParams.id}/clusters/${
-    pathParams.cluster_id
-  }/namespaces/${pathParams.namespace}/envgroup?name=${pathParams.name}${
-    pathParams.version ? "&version=" + pathParams.version : ""
-  }`;
+  return `/api/projects/${pathParams.id}/clusters/${pathParams.cluster_id
+    }/namespaces/${pathParams.namespace}/envgroup?name=${pathParams.name}${pathParams.version ? "&version=" + pathParams.version : ""
+    }`;
 });
 
 const getConfigMap = baseApi<
@@ -2487,7 +2471,7 @@ const removeStackEnvGroup = baseApi<
     `/api/v1/projects/${project_id}/clusters/${cluster_id}/namespaces/${namespace}/stacks/${stack_id}/remove_env_group/${env_group_name}`
 );
 
-const getGithubStatus = baseApi<{}, {}>("GET", ({}) => `/api/status/github`);
+const getGithubStatus = baseApi<{}, {}>("GET", ({ }) => `/api/status/github`);
 
 const createSecretAndOpenGitHubPullRequest = baseApi<
   {