Forráskód Böngészése

adding support for custom domain

Feroze Mohideen 3 éve
szülő
commit
7deeeb0871

+ 52 - 8
dashboard/src/main/home/app-dashboard/new-app-flow/NewAppFlow.tsx

@@ -21,7 +21,6 @@ import Placeholder from "components/Placeholder";
 import Button from "components/porter/Button";
 import { generateSlug } from "random-word-slugs";
 import { RouteComponentProps, withRouter } from "react-router";
-import Error from "components/porter/Error";
 import SourceSelector, { SourceType } from "./SourceSelector";
 import SourceSettings from "./SourceSettings";
 import Services from "./Services";
@@ -104,8 +103,8 @@ const NewAppFlow: React.FC<Props> = ({ ...props }) => {
   const [porterYaml, setPorterYaml] = useState("");
   const [showGHAModal, setShowGHAModal] = useState<boolean>(false);
   const [porterJson, setPorterJson] = useState<
-    z.infer<typeof PorterYamlSchema>
-  >(null);
+    z.infer<typeof PorterYamlSchema> | undefined
+  >(undefined);
   const [detected, setDetected] = useState<Detected | undefined>(undefined);
 
   const validatePorterYaml = (yamlString: string) => {
@@ -183,7 +182,44 @@ const NewAppFlow: React.FC<Props> = ({ ...props }) => {
   };
   const deployPorterApp = async () => {
     try {
-      const finalPorterYaml = createFinalPorterYaml();
+      if (currentProject == null || currentCluster == null || currentProject.id == null || currentCluster.id == null) {
+        throw new Error("Project or cluster not found");
+      }
+
+      // create namespace first so we can create subdomain later if necessary
+      const res = await api
+        .getNamespaces(
+          "<token>",
+          {},
+          {
+            id: currentProject.id,
+            cluster_id: currentCluster.id,
+          }
+        )
+      if (res == null || res.data == null) {
+        throw new Error("Namespaces not found");
+      };
+      const stackNamespace = `porter-stack-${formState.applicationName}`;
+      // TODO: clean up types
+      const namespaceExistsAlready = res.data.some((namespace: any) => {
+        return namespace.name == stackNamespace;
+      })
+      if (!namespaceExistsAlready) {
+        await api
+          .createNamespace(
+            "<token>",
+            {
+              name: stackNamespace,
+            },
+            {
+              id: currentProject.id,
+              cluster_id: currentCluster.id,
+            }
+          );
+      }
+
+      // validate form data
+      const finalPorterYaml = await createFinalPorterYaml();
       const yamlString = yaml.dump(finalPorterYaml);
       const base64Encoded = btoa(yamlString);
       const imageInfo = imageUrl ? {
@@ -193,7 +229,7 @@ const NewAppFlow: React.FC<Props> = ({ ...props }) => {
         }
       } : {}
 
-      // only deploy + write to DB if we can create a final porter yaml
+      // write to the db + deploy
       await Promise.all([
         api.createPorterApp(
           "<token>",
@@ -226,6 +262,7 @@ const NewAppFlow: React.FC<Props> = ({ ...props }) => {
         ),
       ])
     } catch (err) {
+      // TODO: better error handling
       console.log(err);
     }
   };
@@ -246,10 +283,17 @@ const NewAppFlow: React.FC<Props> = ({ ...props }) => {
     return env;
   };
 
-  const createApps = (serviceList: Service[]): z.infer<typeof AppsSchema> => {
+  const createApps = async (serviceList: Service[]): Promise<z.infer<typeof AppsSchema>> => {
     const apps: z.infer<typeof AppsSchema> = {};
     for (const service of serviceList) {
       let config = Service.serialize(service);
+      if (Service.isWeb(service) && service.generateUrlForExternalTraffic) {
+        const ingress = await Service.handleWebIngress(service, formState.applicationName, currentCluster?.id, currentProject?.id);
+        config = {
+          ...config,
+          ...ingress,
+        };
+      }
       if (
         porterJson != null &&
         porterJson.apps[service.name] != null &&
@@ -270,11 +314,11 @@ const NewAppFlow: React.FC<Props> = ({ ...props }) => {
     return apps;
   };
 
-  const createFinalPorterYaml = (): z.infer<typeof PorterYamlSchema> => {
+  const createFinalPorterYaml = async (): Promise<z.infer<typeof PorterYamlSchema>> => {
     return {
       version: "v1stack",
       env: combineEnv(formState.envVariables, porterJson?.env),
-      apps: createApps(formState.serviceList),
+      apps: await createApps(formState.serviceList),
     };
   };
 

+ 50 - 13
dashboard/src/main/home/app-dashboard/new-app-flow/serviceTypes.ts

@@ -1,3 +1,5 @@
+import api from "shared/api";
+
 export type Service = WorkerService | WebService | JobService;
 export type ServiceType = 'web' | 'worker' | 'job';
 
@@ -80,10 +82,10 @@ const WebService = {
         targetCPUUtilizationPercentage: '50',
         targetRAMUtilizationPercentage: '50',
         port: '80',
-        generateUrlForExternalTraffic: true,
+        generateUrlForExternalTraffic: false,
         customDomain: '',
     }),
-    serialize: (service: WebService) => {
+    serialize: async (service: WebService) => {
         const autoscaling = service.autoscalingOn ? {
             autoscaling: {
                 enabled: true,
@@ -93,13 +95,6 @@ const WebService = {
                 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: {
@@ -115,7 +110,6 @@ const WebService = {
                 port: service.port,
             },
             ...autoscaling,
-            ...ingress,
         }
     }
 }
@@ -164,14 +158,57 @@ export const Service = {
                 return JobService.default(name, startCommand);
         }
     },
-    serialize: (service: Service) => {
+    serialize: async (service: Service) => {
         switch (service.type) {
             case 'web':
-                return WebService.serialize(service);
+                return await WebService.serialize(service);
             case 'worker':
                 return WorkerService.serialize(service);
             case 'job':
                 return JobService.serialize(service);
         }
+    },
+    isWeb: (service: Service): service is WebService => service.type === 'web',
+    isWorker: (service: Service): service is WorkerService => service.type === 'worker',
+    isJob: (service: Service): service is JobService => service.type === 'job',
+    handleWebIngress: async (service: WebService, stackName: string, projectId?: number, clusterId?: number) => {
+        if (projectId == null || clusterId == null) {
+            throw new Error('Project ID and Cluster ID must be provided to handle web ingress');
+        }
+        const ingress: Ingress = {
+            enabled: true,
+            hosts: [],
+            custom_domain: false,
+            porter_hosts: [],
+        };
+        if (service.customDomain) {
+            ingress.hosts.push(service.customDomain);
+            ingress.custom_domain = true;
+        } else {
+            const res = await api
+                .createSubdomain(
+                    "<token>",
+                    {},
+                    {
+                        id: projectId,
+                        cluster_id: clusterId,
+                        release_name: stackName,
+                        namespace: `porter-stack-${stackName}`,
+                    }
+                )
+            if (res == null || res.data == null || res.data.external_url == null) {
+                throw new Error('Failed to create subdomain for web service');
+            }
+            ingress.porter_hosts.push(res.data.external_url)
+        }
+
+        return ingress;
     }
-} 
+}
+
+type Ingress = {
+    enabled: boolean;
+    hosts: string[];
+    custom_domain: boolean;
+    porter_hosts: string[];
+}