فهرست منبع

Gpu slider (#3808)

Co-authored-by: Feroze Mohideen <feroze@porter.run>
sdess09 2 سال پیش
والد
کامیت
21bc3d2f7b

+ 11 - 2
dashboard/src/lib/hooks/useClusterResourceLimits.ts

@@ -14,6 +14,7 @@ const clusterDataValidator = z.object({
     const defaultResources = {
         maxCPU: AWS_INSTANCE_LIMITS["t3"]["medium"]["vCPU"],
         maxRAM: AWS_INSTANCE_LIMITS["t3"]["medium"]["RAM"],
+        instanceType: "t3.medium",
     };
     if (!data.labels) {
         return defaultResources;
@@ -33,6 +34,7 @@ const clusterDataValidator = z.object({
         return {
             maxCPU: vCPU,
             maxRAM: RAM,
+            instanceType: instanceType,
         };
     }
     return defaultResources;
@@ -52,11 +54,12 @@ export const useClusterResourceLimits = (
     // defaults indicate the resources assigned to new services
     defaultCPU: number,
     defaultRAM: number,
+    clusterContainsGPUNodes: boolean,
 } => {
     const SMALL_INSTANCE_UPPER_BOUND = 0.75;
     const LARGE_INSTANCE_UPPER_BOUND = 0.9;
     const DEFAULT_MULTIPLIER = 0.125;
-
+    const [clusterContainsGPUNodes, setGpuNodes] = useState(false);
     const [maxCPU, setMaxCPU] = useState(
         AWS_INSTANCE_LIMITS["t3"]["medium"]["vCPU"] * SMALL_INSTANCE_UPPER_BOUND
     ); //default is set to a t3 medium
@@ -117,7 +120,7 @@ export const useClusterResourceLimits = (
             // otherwise, we use 75%
             if (maxRAM > 4) {
                 maxMultiplier = LARGE_INSTANCE_UPPER_BOUND;
-            } 
+            }
             // round down to nearest 0.5 cores
             const newMaxCPU = Math.floor(maxCPU * maxMultiplier * 2) / 2;
             // round down to nearest 100 MB
@@ -128,6 +131,11 @@ export const useClusterResourceLimits = (
             setMaxRAM(newMaxRAM);
             setDefaultCPU(Number((newMaxCPU * DEFAULT_MULTIPLIER).toFixed(2)));
             setDefaultRAM(Number((newMaxRAM * DEFAULT_MULTIPLIER).toFixed(0)));
+
+            // Check if any instance type has "gd4n" and update clusterContainsGPUNodes accordingly
+            setGpuNodes(data.some(item =>
+                item.instanceType.includes("g4dn")
+            ));
         }
     }, [data])
 
@@ -137,6 +145,7 @@ export const useClusterResourceLimits = (
         maxRAM,
         defaultCPU,
         defaultRAM,
+        clusterContainsGPUNodes,
     }
 }
 

+ 9 - 9
dashboard/src/lib/porter-apps/index.ts

@@ -445,17 +445,17 @@ export function clientAppFromProto({
   const predeployOverrides = serializeService(overrides.predeploy);
   const predeploy = proto.predeploy
     ? [
-        deserializeService({
-          service: serializedServiceFromProto({
-            service: new Service({
-              ...proto.predeploy,
-              name: "pre-deploy",
-            }),
-            isPredeploy: true,
+      deserializeService({
+        service: serializedServiceFromProto({
+          service: new Service({
+            ...proto.predeploy,
+            name: "pre-deploy",
           }),
-          override: predeployOverrides,
+          isPredeploy: true,
         }),
-      ]
+        override: predeployOverrides,
+      }),
+    ]
     : undefined;
 
   return {

+ 64 - 59
dashboard/src/lib/porter-apps/services.ts

@@ -74,6 +74,7 @@ export const serviceValidator = z.object({
   port: serviceNumberValidator,
   cpuCores: serviceNumberValidator,
   ramMegabytes: serviceNumberValidator,
+  gpuCoresNvidia: serviceNumberValidator,
   smartOptimization: serviceBooleanValidator.optional(),
   config: z.discriminatedUnion("type", [
     webConfigValidator,
@@ -107,31 +108,32 @@ export type SerializedService = {
   cpuCores: number;
   ramMegabytes: number;
   smartOptimization?: boolean;
+  gpuCoresNvidia: number;
   config:
-    | {
-        type: "web";
-        domains: {
-          name: string;
-        }[];
-        autoscaling?: SerializedAutoscaling;
-        healthCheck?: SerializedHealthcheck;
-        private?: boolean;
-        ingressAnnotations: Record<string, string>;
-      }
-    | {
-        type: "worker";
-        autoscaling?: SerializedAutoscaling;
-      }
-    | {
-        type: "job";
-        allowConcurrent?: boolean;
-        cron: string;
-        suspendCron?: boolean;
-        timeoutSeconds: number;
-      }
-    | {
-        type: "predeploy";
-      };
+  | {
+    type: "web";
+    domains: {
+      name: string;
+    }[];
+    autoscaling?: SerializedAutoscaling;
+    healthCheck?: SerializedHealthcheck;
+    private?: boolean;
+    ingressAnnotations: Record<string, string>;
+  }
+  | {
+    type: "worker";
+    autoscaling?: SerializedAutoscaling;
+  }
+  | {
+    type: "job";
+    allowConcurrent?: boolean;
+    cron: string;
+    suspendCron?: boolean;
+    timeoutSeconds: number;
+  }
+  | {
+    type: "predeploy";
+  };
 };
 
 export function isPredeployService(service: SerializedService | ClientService) {
@@ -181,6 +183,7 @@ export function defaultSerialized({
     port: 3000,
     cpuCores: defaultCPU,
     ramMegabytes: defaultRAM,
+    gpuCoresNvidia: 0,
     smartOptimization: true,
   };
 
@@ -247,6 +250,7 @@ export function serializeService(service: ClientService): SerializedService {
     cpuCores: service.cpuCores.value,
     ramMegabytes: Math.round(service.ramMegabytes.value), // RAM must be an integer
     smartOptimization: service.smartOptimization?.value,
+    gpuCoresNvidia: service.gpuCoresNvidia.value,
     config: match(service.config)
       .with({ type: "web" }, (config) =>
         Object.freeze({
@@ -313,6 +317,7 @@ export function deserializeService({
     instances: ServiceField.number(service.instances, override?.instances),
     port: ServiceField.number(service.port, override?.port),
     cpuCores: ServiceField.number(service.cpuCores, override?.cpuCores),
+    gpuCoresNvidia: ServiceField.number(service.gpuCoresNvidia, override?.gpuCoresNvidia),
     ramMegabytes: ServiceField.number(
       service.ramMegabytes,
       override?.ramMegabytes
@@ -385,11 +390,11 @@ export function deserializeService({
           ingressAnnotations: uniqueAnnotations,
           private:
             typeof config.private === "boolean" ||
-            typeof overrideWebConfig?.private === "boolean"
+              typeof overrideWebConfig?.private === "boolean"
               ? ServiceField.boolean(config.private, overrideWebConfig?.private)
               : setDefaults
-              ? ServiceField.boolean(false, undefined)
-              : undefined,
+                ? ServiceField.boolean(false, undefined)
+                : undefined,
         },
       };
     })
@@ -419,34 +424,34 @@ export function deserializeService({
           type: "job" as const,
           allowConcurrent:
             typeof config.allowConcurrent === "boolean" ||
-            typeof overrideJobConfig?.allowConcurrent === "boolean"
+              typeof overrideJobConfig?.allowConcurrent === "boolean"
               ? ServiceField.boolean(
-                  config.allowConcurrent,
-                  overrideJobConfig?.allowConcurrent
-                )
+                config.allowConcurrent,
+                overrideJobConfig?.allowConcurrent
+              )
               : setDefaults
-              ? ServiceField.boolean(false, undefined)
-              : undefined,
+                ? ServiceField.boolean(false, undefined)
+                : undefined,
           cron: ServiceField.string(config.cron, overrideJobConfig?.cron),
           suspendCron:
             typeof config.suspendCron === "boolean" ||
-            typeof overrideJobConfig?.suspendCron === "boolean"
+              typeof overrideJobConfig?.suspendCron === "boolean"
               ? ServiceField.boolean(
-                  config.suspendCron,
-                  overrideJobConfig?.suspendCron
-                )
+                config.suspendCron,
+                overrideJobConfig?.suspendCron
+              )
               : setDefaults
-              ? ServiceField.boolean(false, undefined)
-              : undefined,
+                ? ServiceField.boolean(false, undefined)
+                : undefined,
           timeoutSeconds:
             config.timeoutSeconds != 0
               ? ServiceField.number(
-                  config.timeoutSeconds,
-                  overrideJobConfig?.timeoutSeconds
-                )
+                config.timeoutSeconds,
+                overrideJobConfig?.timeoutSeconds
+              )
               : setDefaults
-              ? ServiceField.number(3600, overrideJobConfig?.timeoutSeconds)
-              : ServiceField.number(0, overrideJobConfig?.timeoutSeconds),
+                ? ServiceField.number(3600, overrideJobConfig?.timeoutSeconds)
+                : ServiceField.number(0, overrideJobConfig?.timeoutSeconds),
         },
       };
     })
@@ -573,22 +578,22 @@ export function serializedServiceFromProto({
     .with({ case: "jobConfig" }, ({ value }) =>
       isPredeploy
         ? {
-            ...service,
-            run: service.runOptional ?? service.run,
-            config: {
-              type: "predeploy" as const,
-            },
-          }
+          ...service,
+          run: service.runOptional ?? service.run,
+          config: {
+            type: "predeploy" as const,
+          },
+        }
         : {
-            ...service,
-            run: service.runOptional ?? service.run,
-            config: {
-              type: "job" as const,
-              ...value,
-              allowConcurrent: value.allowConcurrentOptional,
-              timeoutSeconds: Number(value.timeoutSeconds),
-            },
-          }
+          ...service,
+          run: service.runOptional ?? service.run,
+          config: {
+            type: "job" as const,
+            ...value,
+            allowConcurrent: value.allowConcurrentOptional,
+            timeoutSeconds: Number(value.timeoutSeconds),
+          },
+        }
     )
     .exhaustive();
 }

+ 5 - 3
dashboard/src/main/home/app-dashboard/app-view/tabs/Overview.tsx

@@ -21,8 +21,8 @@ type Props = {
 const Overview: React.FC<Props> = ({ buttonStatus }) => {
   const { formState } = useFormContext<PorterAppFormData>();
 
-  const { currentClusterResources } = useClusterResources(); 
-  
+  const { currentClusterResources } = useClusterResources();
+
   const {
     porterApp,
     latestProto,
@@ -61,6 +61,7 @@ const Overview: React.FC<Props> = ({ buttonStatus }) => {
             fieldArrayName={"app.predeploy"}
             maxCPU={currentClusterResources.maxCPU}
             maxRAM={currentClusterResources.maxRAM}
+            clusterContainsGPUNodes={currentClusterResources.clusterContainsGPUNodes}
           />
           <Spacer y={0.5} />
         </>
@@ -74,10 +75,11 @@ const Overview: React.FC<Props> = ({ buttonStatus }) => {
         serviceVersionStatus={serviceVersionStatus}
         maxCPU={currentClusterResources.maxCPU}
         maxRAM={currentClusterResources.maxRAM}
+        clusterContainsGPUNodes={currentClusterResources.clusterContainsGPUNodes}
         internalNetworkingDetails={{
           namespace: deploymentTarget.namespace,
           appName: porterApp.name,
-        }}      
+        }}
       />
       <Spacer y={0.75} />
       <Button

+ 3 - 3
dashboard/src/main/home/app-dashboard/create-app/CreateApp.tsx

@@ -672,9 +672,8 @@ const CreateApp: React.FC<CreateAppProps> = ({ history }) => {
                             }
                           >
                             {detectedServices.count > 0
-                              ? `Detected ${detectedServices.count} service${
-                                  detectedServices.count > 1 ? "s" : ""
-                                } from porter.yaml.`
+                              ? `Detected ${detectedServices.count} service${detectedServices.count > 1 ? "s" : ""
+                              } from porter.yaml.`
                               : `Could not detect any services from porter.yaml. Make sure it exists in the root of your repo.`}
                           </Text>
                         </AppearingDiv>
@@ -686,6 +685,7 @@ const CreateApp: React.FC<CreateAppProps> = ({ history }) => {
                       fieldArrayName={"app.services"}
                       maxCPU={currentClusterResources.maxCPU}
                       maxRAM={currentClusterResources.maxRAM}
+                      clusterContainsGPUNodes={currentClusterResources.clusterContainsGPUNodes}
                     />
                   </>,
                   <>

+ 12 - 7
dashboard/src/main/home/app-dashboard/validate-apply/services-settings/ServiceContainer.tsx

@@ -27,6 +27,7 @@ interface ServiceProps {
   status?: PorterAppVersionStatus[];
   maxCPU: number;
   maxRAM: number;
+  clusterContainsGPUNodes: boolean;
   internalNetworkingDetails: {
     namespace: string;
     appName: string;
@@ -41,6 +42,7 @@ const ServiceContainer: React.FC<ServiceProps> = ({
   status,
   maxCPU,
   maxRAM,
+  clusterContainsGPUNodes,
   internalNetworkingDetails,
 }) => {
   const [height, setHeight] = useState<Height>(service.expanded ? "auto" : 0);
@@ -68,12 +70,13 @@ const ServiceContainer: React.FC<ServiceProps> = ({
   const renderTabs = (service: ClientService) => {
     return match(service)
       .with({ config: { type: "web" } }, (svc) => (
-        <WebTabs 
-          index={index} 
-          service={svc} 
-          maxCPU={maxCPU} 
-          maxRAM={maxRAM} 
-          internalNetworkingDetails={internalNetworkingDetails} 
+        <WebTabs
+          index={index}
+          service={svc}
+          maxCPU={maxCPU}
+          maxRAM={maxRAM}
+          clusterContainsGPUNodes={clusterContainsGPUNodes}
+          internalNetworkingDetails={internalNetworkingDetails}
         />
       ))
       .with({ config: { type: "worker" } }, (svc) => (
@@ -82,10 +85,11 @@ const ServiceContainer: React.FC<ServiceProps> = ({
           service={svc}
           maxCPU={maxCPU}
           maxRAM={maxRAM}
+          clusterContainsGPUNodes={clusterContainsGPUNodes}
         />
       ))
       .with({ config: { type: "job" } }, (svc) => (
-        <JobTabs index={index} service={svc} maxCPU={maxCPU} maxRAM={maxRAM} />
+        <JobTabs index={index} service={svc} maxCPU={maxCPU} maxRAM={maxRAM} clusterContainsGPUNodes={clusterContainsGPUNodes} />
       ))
       .with({ config: { type: "predeploy" } }, (svc) => (
         <JobTabs
@@ -93,6 +97,7 @@ const ServiceContainer: React.FC<ServiceProps> = ({
           service={svc}
           maxCPU={maxCPU}
           maxRAM={maxRAM}
+          clusterContainsGPUNodes={clusterContainsGPUNodes}
           isPredeploy
         />
       ))

+ 4 - 1
dashboard/src/main/home/app-dashboard/validate-apply/services-settings/ServiceList.tsx

@@ -51,6 +51,7 @@ type ServiceListProps = {
   serviceVersionStatus?: Record<string, PorterAppVersionStatus[]>;
   maxCPU: number;
   maxRAM: number;
+  clusterContainsGPUNodes: boolean;
   internalNetworkingDetails?: {
     namespace: string;
     appName: string;
@@ -66,6 +67,7 @@ const ServiceList: React.FC<ServiceListProps> = ({
   serviceVersionStatus,
   maxCPU,
   maxRAM,
+  clusterContainsGPUNodes,
   internalNetworkingDetails = {
     namespace: "",
     appName: "",
@@ -174,7 +176,7 @@ const ServiceList: React.FC<ServiceListProps> = ({
         expanded: true,
       })
     );
-    
+
     reset();
     setShowAddServiceModal(false);
   });
@@ -203,6 +205,7 @@ const ServiceList: React.FC<ServiceListProps> = ({
                 status={serviceVersionStatus?.[svc.name.value]}
                 maxCPU={maxCPU}
                 maxRAM={maxRAM}
+                clusterContainsGPUNodes={clusterContainsGPUNodes}
                 internalNetworkingDetails={internalNetworkingDetails}
               />
             ) : null;

+ 13 - 10
dashboard/src/main/home/app-dashboard/validate-apply/services-settings/tabs/JobTabs.tsx

@@ -11,7 +11,7 @@ import MainTab from "./Main";
 import Resources from "./Resources";
 import { Controller, useFormContext } from "react-hook-form";
 import { PorterAppFormData } from "lib/porter-apps";
-import {ControlledInput} from "../../../../../../components/porter/ControlledInput";
+import { ControlledInput } from "components/porter/ControlledInput";
 
 interface Props {
   index: number;
@@ -23,6 +23,7 @@ interface Props {
   chart?: any;
   maxRAM: number;
   maxCPU: number;
+  clusterContainsGPUNodes: boolean;
   isPredeploy?: boolean;
 }
 
@@ -30,6 +31,7 @@ const JobTabs: React.FC<Props> = ({
   index,
   service,
   maxRAM,
+  clusterContainsGPUNodes,
   maxCPU,
   isPredeploy,
 }) => {
@@ -63,6 +65,7 @@ const JobTabs: React.FC<Props> = ({
             index={index}
             maxCPU={maxCPU}
             maxRAM={maxRAM}
+            clusterContainsGPUNodes={clusterContainsGPUNodes}
             service={service}
             isPredeploy={isPredeploy}
           />
@@ -93,15 +96,15 @@ const JobTabs: React.FC<Props> = ({
             />
             <Spacer y={1} />
             <ControlledInput
-                type="text"
-                label="Timeout (seconds)"
-                placeholder="ex: 3600"
-                width="300px"
-                disabled={service.config.timeoutSeconds.readOnly}
-                disabledTooltip={
-                  "You may only edit this field in your porter.yaml."
-                }
-                {...register(`app.services.${index}.config.timeoutSeconds.value`)}
+              type="text"
+              label="Timeout (seconds)"
+              placeholder="ex: 3600"
+              width="300px"
+              disabled={service.config.timeoutSeconds.readOnly}
+              disabledTooltip={
+                "You may only edit this field in your porter.yaml."
+              }
+              {...register(`app.services.${index}.config.timeoutSeconds.value`)}
             />
           </>
         ))

+ 49 - 14
dashboard/src/main/home/app-dashboard/validate-apply/services-settings/tabs/Resources.tsx

@@ -20,6 +20,7 @@ type ResourcesProps = {
   maxRAM: number;
   service: ClientService;
   isPredeploy?: boolean;
+  clusterContainsGPUNodes: boolean;
 };
 
 const Resources: React.FC<ResourcesProps> = ({
@@ -27,6 +28,7 @@ const Resources: React.FC<ResourcesProps> = ({
   maxCPU,
   maxRAM,
   service,
+  clusterContainsGPUNodes,
   isPredeploy = false,
 }) => {
   const { control, register, watch, setValue } = useFormContext<PorterAppFormData>();
@@ -34,29 +36,29 @@ const Resources: React.FC<ResourcesProps> = ({
 
   const autoscalingEnabled = watch(
     `app.services.${index}.config.autoscaling.enabled`, {
-      readOnly: false,
-      value: false
-    }
+    readOnly: false,
+    value: false
+  }
   );
 
   const smartOpt = watch(
     `app.services.${index}.smartOptimization`, {
-      readOnly: false,
-      value: false
-    }
+    readOnly: false,
+    value: false
+  }
   );
 
   const memory = watch(
     `app.services.${index}.ramMegabytes`, {
-      readOnly: false,
-      value: 0
-    }
+    readOnly: false,
+    value: 0
+  }
   );
   const cpu = watch(
     `app.services.${index}.cpuCores`, {
-      readOnly: false,
-      value: 0
-    }
+    readOnly: false,
+    value: 0
+  }
   );
 
   return (
@@ -184,6 +186,37 @@ const Resources: React.FC<ResourcesProps> = ({
           />
         )}
       />
+      {clusterContainsGPUNodes && (
+        <>
+          <Spacer y={1} />
+          <Controller
+            name={`app.services.${index}.gpuCoresNvidia`}
+            control={control}
+            render={({ field: { value, onChange } }) => (
+              <InputSlider
+                label="GPUs: "
+                unit="Cores"
+                min={0}
+                max={1}
+                step={.1}
+                value={(value.value).toString()}
+                disabled={value.readOnly}
+                width="300px"
+                setValue={(e) => {
+                  onChange({
+                    ...value,
+                    value: e,
+                  });
+                }}
+                disabledTooltip={"You may only edit this field in your porter.yaml."
+                }
+              />
+            )}
+          />
+        </>
+      )
+
+      }
       {match(service.config)
         .with({ type: "job" }, () => null)
         .with({ type: "predeploy" }, () => null)
@@ -204,7 +237,8 @@ const Resources: React.FC<ResourcesProps> = ({
               {...register(`app.services.${index}.instances.value`)}
             />
             <Spacer y={1} />
-            <Controller
+
+            {!clusterContainsGPUNodes && (<Controller
               name={`app.services.${index}.config.autoscaling.enabled`}
               control={control}
               render={({ field: { value, onChange } }) => (
@@ -226,7 +260,8 @@ const Resources: React.FC<ResourcesProps> = ({
                   </Text>
                 </Checkbox>
               )}
-            />
+            />)}
+
 
             {autoscalingEnabled.value && (
               <>

+ 7 - 5
dashboard/src/main/home/app-dashboard/validate-apply/services-settings/tabs/WebTabs.tsx

@@ -18,13 +18,14 @@ interface Props {
   chart?: any;
   maxRAM: number;
   maxCPU: number;
+  clusterContainsGPUNodes: boolean;
   internalNetworkingDetails: {
     namespace: string;
     appName: string;
   };
 }
 
-const WebTabs: React.FC<Props> = ({ index, service, maxRAM, maxCPU, internalNetworkingDetails }) => {
+const WebTabs: React.FC<Props> = ({ index, service, maxRAM, maxCPU, clusterContainsGPUNodes, internalNetworkingDetails }) => {
   const [currentTab, setCurrentTab] = React.useState<
     "main" | "resources" | "networking" | "advanced"
   >("main");
@@ -44,10 +45,10 @@ const WebTabs: React.FC<Props> = ({ index, service, maxRAM, maxCPU, internalNetw
       {match(currentTab)
         .with("main", () => <MainTab index={index} service={service} />)
         .with("networking", () => (
-          <Networking 
-            index={index} 
-            service={service} 
-            internalNetworkingDetails={internalNetworkingDetails} 
+          <Networking
+            index={index}
+            service={service}
+            internalNetworkingDetails={internalNetworkingDetails}
           />
         ))
         .with("resources", () => (
@@ -55,6 +56,7 @@ const WebTabs: React.FC<Props> = ({ index, service, maxRAM, maxCPU, internalNetw
             index={index}
             maxCPU={maxCPU}
             maxRAM={maxRAM}
+            clusterContainsGPUNodes={clusterContainsGPUNodes}
             service={service}
           />
         ))

+ 3 - 1
dashboard/src/main/home/app-dashboard/validate-apply/services-settings/tabs/WorkerTabs.tsx

@@ -16,9 +16,10 @@ interface Props {
   chart?: any;
   maxRAM: number;
   maxCPU: number;
+  clusterContainsGPUNodes: boolean;
 }
 
-const WorkerTabs: React.FC<Props> = ({ index, service, maxCPU, maxRAM }) => {
+const WorkerTabs: React.FC<Props> = ({ index, service, maxCPU, maxRAM, clusterContainsGPUNodes }) => {
   const [currentTab, setCurrentTab] = React.useState<"main" | "resources">(
     "main"
   );
@@ -41,6 +42,7 @@ const WorkerTabs: React.FC<Props> = ({ index, service, maxCPU, maxRAM }) => {
             maxCPU={maxCPU}
             maxRAM={maxRAM}
             service={service}
+            clusterContainsGPUNodes={clusterContainsGPUNodes}
           />
         ))
         .exhaustive()}

+ 2 - 1
dashboard/src/main/home/cluster-dashboard/preview-environments/v2/setup-app/AppTemplateForm.tsx

@@ -52,7 +52,7 @@ const AppTemplateForm: React.FC<Props> = ({ existingTemplate }) => {
     variables: {},
     secrets: {},
   });
-  const { currentClusterResources } = useClusterResources(); 
+  const { currentClusterResources } = useClusterResources();
 
   const {
     porterApp,
@@ -280,6 +280,7 @@ const AppTemplateForm: React.FC<Props> = ({ existingTemplate }) => {
                 fieldArrayName={"app.services"}
                 maxCPU={currentClusterResources.maxCPU}
                 maxRAM={currentClusterResources.maxRAM}
+                clusterContainsGPUNodes={currentClusterResources.clusterContainsGPUNodes}
                 internalNetworkingDetails={{
                   namespace: deploymentTarget.namespace,
                   appName: porterApp.name,

+ 40 - 38
dashboard/src/shared/ClusterResourcesContext.tsx

@@ -4,48 +4,50 @@ import { createContext, useContext } from "react";
 import { Context } from "./Context";
 
 export type ClusterResources = {
-    maxCPU: number;
-    maxRAM: number;
-    defaultCPU: number;
-    defaultRAM: number;
-  };
+  maxCPU: number;
+  maxRAM: number;
+  defaultCPU: number;
+  defaultRAM: number;
+  clusterContainsGPUNodes: boolean;
+};
 
 export const ClusterResourcesContext = createContext<{
-    currentClusterResources: ClusterResources;
-  } | null>(null);
-  
+  currentClusterResources: ClusterResources;
+} | null>(null);
+
 export const useClusterResources = () => {
-    const context = useContext(ClusterResourcesContext);
-    if (context == null) {
-        throw new Error(
-        "useClusterResources must be used within a ClusterResourcesContext"
-        );
-    }
-    return context;
+  const context = useContext(ClusterResourcesContext);
+  if (context == null) {
+    throw new Error(
+      "useClusterResources must be used within a ClusterResourcesContext"
+    );
+  }
+  return context;
 };
-  
+
 const ClusterResourcesProvider = ({ children }: { children: JSX.Element }) => {
-    const { currentCluster, currentProject } = useContext(Context);
+  const { currentCluster, currentProject } = useContext(Context);
 
-    const { maxCPU, maxRAM, defaultCPU, defaultRAM } = useClusterResourceLimits({
-        projectId: currentProject?.id,
-        clusterId: currentCluster?.id,
-    });
-  
-    return (
-      <ClusterResourcesContext.Provider
-        value={{
-            currentClusterResources: {
-                maxCPU,
-                maxRAM,
-                defaultCPU,
-                defaultRAM,
-            },
-        }}
-      >
-        {children}
-      </ClusterResourcesContext.Provider>
-    );
-  };
+  const { maxCPU, maxRAM, defaultCPU, defaultRAM, clusterContainsGPUNodes } = useClusterResourceLimits({
+    projectId: currentProject?.id,
+    clusterId: currentCluster?.id,
+  });
+
+  return (
+    <ClusterResourcesContext.Provider
+      value={{
+        currentClusterResources: {
+          maxCPU,
+          maxRAM,
+          defaultCPU,
+          defaultRAM,
+          clusterContainsGPUNodes,
+        },
+      }}
+    >
+      {children}
+    </ClusterResourcesContext.Provider>
+  );
+};
 
-  export default ClusterResourcesProvider;
+export default ClusterResourcesProvider;

+ 85 - 72
internal/porter_app/test/parse_test.go

@@ -81,12 +81,13 @@ var result_nobuild = &porterv1.PorterApp{
 			Type: 1,
 		},
 		"example-wkr": {
-			Name:         "example-wkr",
-			RunOptional:  pointer.String("echo 'work'"),
-			Instances:    1,
-			Port:         80,
-			CpuCores:     0.1,
-			RamMegabytes: 256,
+			Name:           "example-wkr",
+			RunOptional:    pointer.String("echo 'work'"),
+			Instances:      1,
+			Port:           80,
+			CpuCores:       0.1,
+			RamMegabytes:   256,
+			GpuCoresNvidia: 0,
 			Config: &porterv1.Service_WorkerConfig{
 				WorkerConfig: &porterv1.WorkerServiceConfig{
 					Autoscaling: nil,
@@ -95,10 +96,11 @@ var result_nobuild = &porterv1.PorterApp{
 			Type: 2,
 		},
 		"example-job": {
-			Name:         "example-job",
-			RunOptional:  pointer.String("echo 'hello world'"),
-			CpuCores:     0.1,
-			RamMegabytes: 256,
+			Name:           "example-job",
+			RunOptional:    pointer.String("echo 'hello world'"),
+			CpuCores:       0.1,
+			RamMegabytes:   256,
+			GpuCoresNvidia: 0,
 			Config: &porterv1.Service_JobConfig{
 				JobConfig: &porterv1.JobServiceConfig{
 					AllowConcurrentOptional: pointer.Bool(true),
@@ -112,12 +114,13 @@ var result_nobuild = &porterv1.PorterApp{
 	},
 	ServiceList: []*porterv1.Service{
 		{
-			Name:         "example-web",
-			RunOptional:  pointer.String("node index.js"),
-			Instances:    0,
-			Port:         8080,
-			CpuCores:     0.1,
-			RamMegabytes: 256,
+			Name:           "example-web",
+			RunOptional:    pointer.String("node index.js"),
+			Instances:      0,
+			Port:           8080,
+			CpuCores:       0.1,
+			RamMegabytes:   256,
+			GpuCoresNvidia: 0,
 			Config: &porterv1.Service_WebConfig{
 				WebConfig: &porterv1.WebServiceConfig{
 					Autoscaling: &porterv1.Autoscaling{
@@ -144,12 +147,13 @@ var result_nobuild = &porterv1.PorterApp{
 			Type: 1,
 		},
 		{
-			Name:         "example-wkr",
-			RunOptional:  pointer.String("echo 'work'"),
-			Instances:    1,
-			Port:         80,
-			CpuCores:     0.1,
-			RamMegabytes: 256,
+			Name:           "example-wkr",
+			RunOptional:    pointer.String("echo 'work'"),
+			Instances:      1,
+			Port:           80,
+			CpuCores:       0.1,
+			RamMegabytes:   256,
+			GpuCoresNvidia: 0,
 			Config: &porterv1.Service_WorkerConfig{
 				WorkerConfig: &porterv1.WorkerServiceConfig{
 					Autoscaling: nil,
@@ -158,10 +162,11 @@ var result_nobuild = &porterv1.PorterApp{
 			Type: 2,
 		},
 		{
-			Name:         "example-job",
-			RunOptional:  pointer.String("echo 'hello world'"),
-			CpuCores:     0.1,
-			RamMegabytes: 256,
+			Name:           "example-job",
+			RunOptional:    pointer.String("echo 'hello world'"),
+			CpuCores:       0.1,
+			RamMegabytes:   256,
+			GpuCoresNvidia: 0,
 			Config: &porterv1.Service_JobConfig{
 				JobConfig: &porterv1.JobServiceConfig{
 					AllowConcurrentOptional: pointer.Bool(true),
@@ -174,13 +179,14 @@ var result_nobuild = &porterv1.PorterApp{
 		},
 	},
 	Predeploy: &porterv1.Service{
-		RunOptional:  pointer.String("ls"),
-		Instances:    0,
-		Port:         0,
-		CpuCores:     0,
-		RamMegabytes: 0,
-		Config:       &porterv1.Service_JobConfig{},
-		Type:         3,
+		RunOptional:    pointer.String("ls"),
+		Instances:      0,
+		Port:           0,
+		CpuCores:       0,
+		RamMegabytes:   0,
+		GpuCoresNvidia: 0,
+		Config:         &porterv1.Service_JobConfig{},
+		Type:           3,
 	},
 	Image: &porterv1.AppImage{
 		Repository: "nginx",
@@ -192,10 +198,11 @@ var v1_result_nobuild_no_image = &porterv1.PorterApp{
 	Name: "test-app",
 	Services: map[string]*porterv1.Service{
 		"example-job": {
-			Name:         "example-job",
-			RunOptional:  pointer.String("echo 'hello world'"),
-			CpuCores:     0.1,
-			RamMegabytes: 256,
+			Name:           "example-job",
+			RunOptional:    pointer.String("echo 'hello world'"),
+			CpuCores:       0.1,
+			RamMegabytes:   256,
+			GpuCoresNvidia: 0,
 			Config: &porterv1.Service_JobConfig{
 				JobConfig: &porterv1.JobServiceConfig{
 					AllowConcurrent: true,
@@ -205,12 +212,13 @@ var v1_result_nobuild_no_image = &porterv1.PorterApp{
 			Type: 3,
 		},
 		"example-wkr": {
-			Name:         "example-wkr",
-			RunOptional:  pointer.String("echo 'work'"),
-			Instances:    1,
-			Port:         80,
-			CpuCores:     0.1,
-			RamMegabytes: 256,
+			Name:           "example-wkr",
+			RunOptional:    pointer.String("echo 'work'"),
+			Instances:      1,
+			Port:           80,
+			CpuCores:       0.1,
+			RamMegabytes:   256,
+			GpuCoresNvidia: 0,
 			Config: &porterv1.Service_WorkerConfig{
 				WorkerConfig: &porterv1.WorkerServiceConfig{
 					Autoscaling: nil,
@@ -219,12 +227,13 @@ var v1_result_nobuild_no_image = &porterv1.PorterApp{
 			Type: 2,
 		},
 		"example-web": {
-			Name:         "example-web",
-			RunOptional:  pointer.String("node index.js"),
-			Instances:    0,
-			Port:         8080,
-			CpuCores:     0.1,
-			RamMegabytes: 256,
+			Name:           "example-web",
+			RunOptional:    pointer.String("node index.js"),
+			Instances:      0,
+			Port:           8080,
+			CpuCores:       0.1,
+			GpuCoresNvidia: 0,
+			RamMegabytes:   256,
 			Config: &porterv1.Service_WebConfig{
 				WebConfig: &porterv1.WebServiceConfig{
 					Autoscaling: &porterv1.Autoscaling{
@@ -254,10 +263,11 @@ var v1_result_nobuild_no_image = &porterv1.PorterApp{
 	},
 	ServiceList: []*porterv1.Service{
 		{
-			Name:         "example-job",
-			RunOptional:  pointer.String("echo 'hello world'"),
-			CpuCores:     0.1,
-			RamMegabytes: 256,
+			Name:           "example-job",
+			RunOptional:    pointer.String("echo 'hello world'"),
+			CpuCores:       0.1,
+			RamMegabytes:   256,
+			GpuCoresNvidia: 0,
 			Config: &porterv1.Service_JobConfig{
 				JobConfig: &porterv1.JobServiceConfig{
 					AllowConcurrent: true,
@@ -267,12 +277,13 @@ var v1_result_nobuild_no_image = &porterv1.PorterApp{
 			Type: 3,
 		},
 		{
-			Name:         "example-wkr",
-			RunOptional:  pointer.String("echo 'work'"),
-			Instances:    1,
-			Port:         80,
-			CpuCores:     0.1,
-			RamMegabytes: 256,
+			Name:           "example-wkr",
+			RunOptional:    pointer.String("echo 'work'"),
+			Instances:      1,
+			Port:           80,
+			CpuCores:       0.1,
+			RamMegabytes:   256,
+			GpuCoresNvidia: 0,
 			Config: &porterv1.Service_WorkerConfig{
 				WorkerConfig: &porterv1.WorkerServiceConfig{
 					Autoscaling: nil,
@@ -281,12 +292,13 @@ var v1_result_nobuild_no_image = &porterv1.PorterApp{
 			Type: 2,
 		},
 		{
-			Name:         "example-web",
-			RunOptional:  pointer.String("node index.js"),
-			Instances:    0,
-			Port:         8080,
-			CpuCores:     0.1,
-			RamMegabytes: 256,
+			Name:           "example-web",
+			RunOptional:    pointer.String("node index.js"),
+			Instances:      0,
+			Port:           8080,
+			CpuCores:       0.1,
+			RamMegabytes:   256,
+			GpuCoresNvidia: 0,
 			Config: &porterv1.Service_WebConfig{
 				WebConfig: &porterv1.WebServiceConfig{
 					Autoscaling: &porterv1.Autoscaling{
@@ -315,13 +327,14 @@ var v1_result_nobuild_no_image = &porterv1.PorterApp{
 		},
 	},
 	Predeploy: &porterv1.Service{
-		RunOptional:  pointer.String("ls"),
-		Instances:    0,
-		Port:         0,
-		CpuCores:     0,
-		RamMegabytes: 0,
-		Config:       &porterv1.Service_JobConfig{},
-		Type:         3,
+		RunOptional:    pointer.String("ls"),
+		Instances:      0,
+		Port:           0,
+		CpuCores:       0,
+		RamMegabytes:   0,
+		GpuCoresNvidia: 0,
+		Config:         &porterv1.Service_JobConfig{},
+		Type:           3,
 	},
 }
 

+ 3 - 0
internal/porter_app/v2/yaml.go

@@ -122,6 +122,7 @@ type Service struct {
 	Instances          int               `yaml:"instances,omitempty"`
 	CpuCores           float32           `yaml:"cpuCores,omitempty"`
 	RamMegabytes       int               `yaml:"ramMegabytes,omitempty"`
+	GpuCoresNvidia     float32           `yaml:"gpuCoresNvidia,omitempty"`
 	SmartOptimization  *bool             `yaml:"smartOptimization,omitempty"`
 	Port               int               `yaml:"port,omitempty"`
 	Autoscaling        *AutoScaling      `yaml:"autoscaling,omitempty" validate:"excluded_if=Type job"`
@@ -261,6 +262,7 @@ func serviceProtoFromConfig(service Service, serviceType porterv1.ServiceType) (
 		Instances:         int32(service.Instances),
 		CpuCores:          service.CpuCores,
 		RamMegabytes:      int32(service.RamMegabytes),
+		GpuCoresNvidia:    service.GpuCoresNvidia,
 		Port:              int32(service.Port),
 		SmartOptimization: service.SmartOptimization,
 		Type:              serviceType,
@@ -411,6 +413,7 @@ func appServiceFromProto(service *porterv1.Service) (Service, error) {
 		Instances:         int(service.Instances),
 		CpuCores:          service.CpuCores,
 		RamMegabytes:      int(service.RamMegabytes),
+		GpuCoresNvidia:    service.GpuCoresNvidia,
 		Port:              int(service.Port),
 		SmartOptimization: service.SmartOptimization,
 	}