Răsfoiți Sursa

[POR-1919] Make cluster resources a context provider so we can add services with default ram/cpu depending on the nodes (#3789)

Feroze Mohideen 2 ani în urmă
părinte
comite
974edc1076

+ 41 - 21
dashboard/src/lib/hooks/useClusterResourceLimits.ts

@@ -37,6 +37,7 @@ const clusterDataValidator = z.object({
     }
     return defaultResources;
 });
+
 export const useClusterResourceLimits = (
     {
         projectId,
@@ -45,24 +46,42 @@ export const useClusterResourceLimits = (
         projectId: number | undefined,
         clusterId: number | undefined,
     }
-) => {
-    const UPPER_BOUND = 0.75;
+): {
+    maxCPU: number,
+    maxRAM: number,
+    // defaults indicate the resources assigned to new services
+    defaultCPU: number,
+    defaultRAM: number,
+} => {
+    const SMALL_INSTANCE_UPPER_BOUND = 0.75;
+    const LARGE_INSTANCE_UPPER_BOUND = 0.9;
+    const DEFAULT_MULTIPLIER = 0.125;
 
     const [maxCPU, setMaxCPU] = useState(
-        AWS_INSTANCE_LIMITS["t3"]["medium"]["vCPU"] * UPPER_BOUND
+        AWS_INSTANCE_LIMITS["t3"]["medium"]["vCPU"] * SMALL_INSTANCE_UPPER_BOUND
     ); //default is set to a t3 medium
     const [maxRAM, setMaxRAM] = useState(
         // round to nearest 100
         Math.round(
             convert(AWS_INSTANCE_LIMITS["t3"]["medium"]["RAM"], "GiB").to("MB") *
-            UPPER_BOUND / 100
+            SMALL_INSTANCE_UPPER_BOUND / 100
+        ) * 100
+    ); //default is set to a t3 medium
+    const [defaultCPU, setDefaultCPU] = useState(
+        AWS_INSTANCE_LIMITS["t3"]["medium"]["vCPU"] * DEFAULT_MULTIPLIER
+    ); //default is set to a t3 medium
+    const [defaultRAM, setDefaultRAM] = useState(
+        // round to nearest 100
+        Math.round(
+            convert(AWS_INSTANCE_LIMITS["t3"]["medium"]["RAM"], "GiB").to("MB") *
+            DEFAULT_MULTIPLIER / 100
         ) * 100
     ); //default is set to a t3 medium
 
     const { data } = useQuery(
         ["getClusterNodes", projectId, clusterId],
         async () => {
-            if (!projectId || !clusterId) {
+            if (!projectId || !clusterId || clusterId === -1) {
                 return Promise.resolve([]);
             }
 
@@ -80,6 +99,7 @@ export const useClusterResourceLimits = (
         {
             enabled: !!projectId && !!clusterId,
             refetchOnWindowFocus: false,
+            retry: false,
         }
     );
 
@@ -92,31 +112,31 @@ export const useClusterResourceLimits = (
             const maxRAM = data.reduce((acc, curr) => {
                 return Math.max(acc, curr.maxRAM);
             }, 0);
+            let maxMultiplier = SMALL_INSTANCE_UPPER_BOUND;
             // if the instance type has more than 4 GB ram, we use 90% of the ram/cpu
             // otherwise, we use 75%
             if (maxRAM > 4) {
-                // round down to nearest 0.5 cores
-                setMaxCPU(Math.floor(maxCPU * 0.9 * 2) / 2);
-                setMaxRAM(
-                    Math.round(
-                        convert(maxRAM, "GiB").to("MB") * 0.9 / 100
-                    ) * 100
-                );
-            } else {
-                setMaxCPU(Math.floor(maxCPU * UPPER_BOUND * 2) / 2);
-                setMaxRAM(
-                    Math.round(
-                        convert(maxRAM, "GiB").to("MB") * UPPER_BOUND / 100
-                    ) * 100
-                );
-            }
+                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
+            const newMaxRAM = Math.round(
+                convert(maxRAM, "GiB").to("MB") * maxMultiplier / 100
+            ) * 100;
+            setMaxCPU(newMaxCPU);
+            setMaxRAM(newMaxRAM);
+            setDefaultCPU(Number((newMaxCPU * DEFAULT_MULTIPLIER).toFixed(2)));
+            setDefaultRAM(Number((newMaxRAM * DEFAULT_MULTIPLIER).toFixed(0)));
         }
     }, [data])
 
 
     return {
         maxCPU,
-        maxRAM
+        maxRAM,
+        defaultCPU,
+        defaultRAM,
     }
 }
 

+ 4 - 0
dashboard/src/lib/hooks/usePorterYaml.ts

@@ -3,6 +3,7 @@ import { useQuery } from "@tanstack/react-query";
 import { SourceOptions, serviceOverrides } from "lib/porter-apps";
 import { DetectedServices } from "lib/porter-apps/services";
 import { useCallback, useContext, useEffect, useState } from "react";
+import { useClusterResources } from "shared/ClusterResourcesContext";
 import { Context } from "shared/Context";
 import api from "shared/api";
 import { z } from "zod";
@@ -38,6 +39,7 @@ export const usePorterYaml = ({
   useDefaults?: boolean;
 }): PorterYamlStatus => {
   const { currentProject, currentCluster } = useContext(Context);
+  const { currentClusterResources } = useClusterResources();
   const [
     detectedServices,
     setDetectedServices,
@@ -132,6 +134,8 @@ export const usePorterYaml = ({
         const { services, predeploy, build } = serviceOverrides({
           overrides: proto,
           useDefaults,
+          defaultCPU: currentClusterResources.defaultCPU,
+          defaultRAM: currentClusterResources.defaultRAM,
         });
 
         if (services.length || predeploy || build) {

+ 8 - 0
dashboard/src/lib/porter-apps/index.ts

@@ -112,9 +112,13 @@ export type PorterAppFormData = z.infer<typeof porterAppFormValidator>;
 export function serviceOverrides({
   overrides,
   useDefaults = true,
+  defaultCPU = 0.1,
+  defaultRAM = 256,
 }: {
   overrides: PorterApp;
   useDefaults?: boolean;
+  defaultCPU?: number;
+  defaultRAM?: number;
 }): DetectedServices {
   const services = Object.entries(overrides.services)
     .map(([name, service]) => serializedServiceFromProto({ name, service }))
@@ -124,6 +128,8 @@ export function serviceOverrides({
           service: defaultSerialized({
             name: svc.name,
             type: svc.config.type,
+            defaultCPU,
+            defaultRAM,
           }),
           override: svc,
           expanded: true,
@@ -158,6 +164,8 @@ export function serviceOverrides({
         service: defaultSerialized({
           name: "pre-deploy",
           type: "predeploy",
+          defaultCPU,
+          defaultRAM,
         }),
         override: serializedServiceFromProto({
           name: "pre-deploy",

+ 6 - 2
dashboard/src/lib/porter-apps/services.ts

@@ -142,17 +142,21 @@ export function prefixSubdomain(subdomain: string) {
 export function defaultSerialized({
   name,
   type,
+  defaultCPU = 0.1,
+  defaultRAM = 256,
 }: {
   name: string;
   type: ClientServiceType;
+  defaultCPU?: number;
+  defaultRAM?: number;
 }): SerializedService {
   const baseService = {
     name,
     run: "",
     instances: 1,
     port: 3000,
-    cpuCores: 0.1,
-    ramMegabytes: 256,
+    cpuCores: defaultCPU,
+    ramMegabytes: defaultRAM,
     smartOptimization: true,
   };
 

+ 203 - 200
dashboard/src/main/home/Home.tsx

@@ -45,6 +45,7 @@ import Apps from "./app-dashboard/apps/Apps";
 import DeploymentTargetProvider from "shared/DeploymentTargetContext";
 import PreviewEnvs from "./cluster-dashboard/preview-environments/v2/PreviewEnvs";
 import SetupApp from "./cluster-dashboard/preview-environments/v2/setup-app/SetupApp";
+import ClusterResourcesProvider from "shared/ClusterResourcesContext";
 
 // Guarded components
 const GuardedProjectSettings = fakeGuardedRoute("settings", "", [
@@ -388,220 +389,222 @@ const Home: React.FC<Props> = (props) => {
     <ThemeProvider
       theme={currentProject?.simplified_view_enabled ? midnight : standard}
     >
-      <DeploymentTargetProvider>
-        <StyledHome>
-          <ModalHandler setRefreshClusters={setForceRefreshClusters} />
-          {currentOverlay &&
-            createPortal(
-              <ConfirmOverlay
-                show={true}
-                message={currentOverlay.message}
-                onYes={currentOverlay.onYes}
-                onNo={currentOverlay.onNo}
-              />,
-              document.body
+      <ClusterResourcesProvider>
+        <DeploymentTargetProvider>
+          <StyledHome>
+            <ModalHandler setRefreshClusters={setForceRefreshClusters} />
+            {currentOverlay &&
+              createPortal(
+                <ConfirmOverlay
+                  show={true}
+                  message={currentOverlay.message}
+                  onYes={currentOverlay.onYes}
+                  onNo={currentOverlay.onNo}
+                />,
+                document.body
+              )}
+            {/* Render sidebar when there's at least one project */}
+            {projects?.length > 0 && baseRoute !== "new-project" ? (
+              <Sidebar
+                key="sidebar"
+                forceSidebar={forceSidebar}
+                setWelcome={setShowWelcome}
+                currentView={props.currentRoute}
+                forceRefreshClusters={forceRefreshClusters}
+                setRefreshClusters={setForceRefreshClusters}
+              />
+            ) : (
+              <DiscordButton href="https://discord.gg/34n7NN7FJ7" target="_blank">
+                <Icon src={discordLogo} />
+                Join Our Discord
+              </DiscordButton>
             )}
-          {/* Render sidebar when there's at least one project */}
-          {projects?.length > 0 && baseRoute !== "new-project" ? (
-            <Sidebar
-              key="sidebar"
-              forceSidebar={forceSidebar}
-              setWelcome={setShowWelcome}
-              currentView={props.currentRoute}
-              forceRefreshClusters={forceRefreshClusters}
-              setRefreshClusters={setForceRefreshClusters}
-            />
-          ) : (
-            <DiscordButton href="https://discord.gg/34n7NN7FJ7" target="_blank">
-              <Icon src={discordLogo} />
-              Join Our Discord
-            </DiscordButton>
-          )}
-          <ViewWrapper id="HomeViewWrapper">
-            <Navbar
-              logOut={props.logOut}
-              currentView={props.currentRoute} // For form feedback
-            />
-
-            <Switch>
-              <Route path="/apps/new/app">
-                {currentProject?.validate_apply_v2 ? (
-                  <CreateApp />
-                ) : (
-                  <NewAppFlow />
-                )}
-              </Route>
-              <Route path="/apps/:appName/:tab">
-                {currentProject?.validate_apply_v2 ? (
-                  <AppView />
-                ) : (
-                  <ExpandedApp />
-                )}
-              </Route>
-              <Route path="/apps/:appName">
-                {currentProject?.validate_apply_v2 ? (
-                  <AppView />
-                ) : (
-                  <ExpandedApp />
-                )}
-              </Route>
-              <Route path="/apps">
-                {currentProject?.validate_apply_v2 ? (
-                  <Apps />
-                ) : (
-                  <AppDashboard />
-                )}
-              </Route>
-
-              <Route path="/addons/new">
-                <NewAddOnFlow />
-              </Route>
-              <Route path="/addons">
-                <AddOnDashboard />
-              </Route>
-              <Route
-                path="/new-project"
-                render={() => {
-                  return <NewProjectFC />;
-                }}
-              ></Route>
-              <Route
-                path="/onboarding"
-                render={() => {
-                  return <Onboarding />;
-                }}
+            <ViewWrapper id="HomeViewWrapper">
+              <Navbar
+                logOut={props.logOut}
+                currentView={props.currentRoute} // For form feedback
               />
-              {(user?.isPorterUser ||
-                overrideInfraTabEnabled({
-                  projectID: currentProject?.id,
-                })) && (
+
+              <Switch>
+                <Route path="/apps/new/app">
+                  {currentProject?.validate_apply_v2 ? (
+                    <CreateApp />
+                  ) : (
+                    <NewAppFlow />
+                  )}
+                </Route>
+                <Route path="/apps/:appName/:tab">
+                  {currentProject?.validate_apply_v2 ? (
+                    <AppView />
+                  ) : (
+                    <ExpandedApp />
+                  )}
+                </Route>
+                <Route path="/apps/:appName">
+                  {currentProject?.validate_apply_v2 ? (
+                    <AppView />
+                  ) : (
+                    <ExpandedApp />
+                  )}
+                </Route>
+                <Route path="/apps">
+                  {currentProject?.validate_apply_v2 ? (
+                    <Apps />
+                  ) : (
+                    <AppDashboard />
+                  )}
+                </Route>
+
+                <Route path="/addons/new">
+                  <NewAddOnFlow />
+                </Route>
+                <Route path="/addons">
+                  <AddOnDashboard />
+                </Route>
+                <Route
+                  path="/new-project"
+                  render={() => {
+                    return <NewProjectFC />;
+                  }}
+                ></Route>
                 <Route
-                  path="/infrastructure"
+                  path="/onboarding"
+                  render={() => {
+                    return <Onboarding />;
+                  }}
+                />
+                {(user?.isPorterUser ||
+                  overrideInfraTabEnabled({
+                    projectID: currentProject?.id,
+                  })) && (
+                  <Route
+                    path="/infrastructure"
+                    render={() => {
+                      return (
+                        <DashboardWrapper>
+                          <InfrastructureRouter />
+                        </DashboardWrapper>
+                      );
+                    }}
+                  />
+                )}
+                <Route
+                  path="/dashboard"
                   render={() => {
                     return (
                       <DashboardWrapper>
-                        <InfrastructureRouter />
+                        <Dashboard
+                          projectId={currentProject?.id}
+                          setRefreshClusters={setForceRefreshClusters}
+                        />
                       </DashboardWrapper>
                     );
                   }}
                 />
-              )}
-              <Route
-                path="/dashboard"
-                render={() => {
-                  return (
-                    <DashboardWrapper>
-                      <Dashboard
-                        projectId={currentProject?.id}
-                        setRefreshClusters={setForceRefreshClusters}
-                      />
-                    </DashboardWrapper>
-                  );
-                }}
-              />
-              <Route
-                path={[
-                  "/cluster-dashboard",
-                  "/applications",
-                  "/jobs",
-                  "/env-groups",
-                  "/databases",
-                  ...(!currentProject?.validate_apply_v2
-                    ? ["/preview-environments"]
-                    : []),
-                  "/stacks",
-                ]}
-                render={() => {
-                  if (currentCluster?.id === -1) {
-                    return <Loading />;
-                  } else if (!currentCluster || !currentCluster.name) {
+                <Route
+                  path={[
+                    "/cluster-dashboard",
+                    "/applications",
+                    "/jobs",
+                    "/env-groups",
+                    "/databases",
+                    ...(!currentProject?.validate_apply_v2
+                      ? ["/preview-environments"]
+                      : []),
+                    "/stacks",
+                  ]}
+                  render={() => {
+                    if (currentCluster?.id === -1) {
+                      return <Loading />;
+                    } else if (!currentCluster || !currentCluster.name) {
+                      return (
+                        <DashboardWrapper>
+                          <NoClusterPlaceHolder></NoClusterPlaceHolder>
+                        </DashboardWrapper>
+                      );
+                    }
                     return (
                       <DashboardWrapper>
-                        <NoClusterPlaceHolder></NoClusterPlaceHolder>
+                        <DashboardRouter
+                          currentCluster={currentCluster}
+                          setSidebar={setForceSidebar}
+                          currentView={props.currentRoute}
+                        />
                       </DashboardWrapper>
                     );
-                  }
-                  return (
-                    <DashboardWrapper>
-                      <DashboardRouter
-                        currentCluster={currentCluster}
-                        setSidebar={setForceSidebar}
-                        currentView={props.currentRoute}
-                      />
-                    </DashboardWrapper>
-                  );
-                }}
-              />
-              <Route
-                path={"/integrations"}
-                render={() => <GuardedIntegrations />}
-              />
-              <Route
-                exact
-                path={"/project-settings"}
-                render={() => <GuardedProjectSettings />}
-              />
-              {currentProject?.validate_apply_v2 &&
-              currentProject.preview_envs_enabled ? (
-                <>
-                  <Route exact path="/preview-environments/configure">
-                    <SetupApp />
-                  </Route>
-                  <Route
-                    exact
-                    path={`/preview-environments/apps/:appName/:tab`}
-                  >
-                    <AppView />
-                  </Route>
-                  <Route exact path="/preview-environments/apps/:appName">
-                    <AppView />
-                  </Route>
-                  <Route exact path={`/preview-environments/apps`}>
-                    <Apps />
-                  </Route>
-                  <Route exact path={`/preview-environments`}>
-                    <PreviewEnvs />
-                  </Route>
-                </>
-              ) : null}
-              <Route path={"*"} render={() => <LaunchWrapper />} />
-            </Switch>
-          </ViewWrapper>
-          {createPortal(
-            <ConfirmOverlay
-              show={currentModal === "UpdateProjectModal"}
-              message={
-                currentProject
-                  ? `Are you sure you want to delete ${currentProject.name}?`
-                  : ""
-              }
-              onYes={handleDelete}
-              onNo={() => setCurrentModal(null, null)}
-            />,
-            document.body
-          )}
-          {showWrongEmailModal && (
-            <Modal>
-              <Text size={16}>
-                Oops! This invite link wasn't for {user?.email}
-              </Text>
-              <Spacer y={1} />
-              <Text color="helper">
-                Your account email does not match the email associated with this
-                project invite. Please log out and sign up again with the
-                correct email using the invite link.
-              </Text>
-              <Spacer y={1} />
-              <Text color="helper">
-                You should reach out to the person who sent you the invite link
-                to get the correct email.
-              </Text>
-              <Spacer y={1} />
-              <Button onClick={props.logOut}>Log out</Button>
-            </Modal>
-          )}
-        </StyledHome>
-      </DeploymentTargetProvider>
+                  }}
+                />
+                <Route
+                  path={"/integrations"}
+                  render={() => <GuardedIntegrations />}
+                />
+                <Route
+                  exact
+                  path={"/project-settings"}
+                  render={() => <GuardedProjectSettings />}
+                />
+                {currentProject?.validate_apply_v2 &&
+                currentProject.preview_envs_enabled ? (
+                  <>
+                    <Route exact path="/preview-environments/configure">
+                      <SetupApp />
+                    </Route>
+                    <Route
+                      exact
+                      path={`/preview-environments/apps/:appName/:tab`}
+                    >
+                      <AppView />
+                    </Route>
+                    <Route exact path="/preview-environments/apps/:appName">
+                      <AppView />
+                    </Route>
+                    <Route exact path={`/preview-environments/apps`}>
+                      <Apps />
+                    </Route>
+                    <Route exact path={`/preview-environments`}>
+                      <PreviewEnvs />
+                    </Route>
+                  </>
+                ) : null}
+                <Route path={"*"} render={() => <LaunchWrapper />} />
+              </Switch>
+            </ViewWrapper>
+            {createPortal(
+              <ConfirmOverlay
+                show={currentModal === "UpdateProjectModal"}
+                message={
+                  currentProject
+                    ? `Are you sure you want to delete ${currentProject.name}?`
+                    : ""
+                }
+                onYes={handleDelete}
+                onNo={() => setCurrentModal(null, null)}
+              />,
+              document.body
+            )}
+            {showWrongEmailModal && (
+              <Modal>
+                <Text size={16}>
+                  Oops! This invite link wasn't for {user?.email}
+                </Text>
+                <Spacer y={1} />
+                <Text color="helper">
+                  Your account email does not match the email associated with this
+                  project invite. Please log out and sign up again with the
+                  correct email using the invite link.
+                </Text>
+                <Spacer y={1} />
+                <Text color="helper">
+                  You should reach out to the person who sent you the invite link
+                  to get the correct email.
+                </Text>
+                <Spacer y={1} />
+                <Button onClick={props.logOut}>Log out</Button>
+              </Modal>
+            )}
+          </StyledHome>
+        </DeploymentTargetProvider>
+      </ClusterResourcesProvider>
     </ThemeProvider>
   );
 };

+ 0 - 5
dashboard/src/main/home/app-dashboard/app-view/AppDataContainer.tsx

@@ -35,7 +35,6 @@ import JobsTab from "./tabs/JobsTab";
 import ConfirmRedeployModal from "./ConfirmRedeployModal";
 import ImageSettingsTab from "./tabs/ImageSettingsTab";
 import { useAppAnalytics } from "lib/hooks/useAppAnalytics";
-import { useClusterResourceLimits } from "lib/hooks/useClusterResourceLimits";
 import { Error as ErrorComponent } from "components/porter/Error";
 import _ from "lodash";
 import axios from "axios";
@@ -89,8 +88,6 @@ const AppDataContainer: React.FC<AppDataContainerProps> = ({ tabParam }) => {
     deploymentTargetID: deploymentTarget.id,
   });
 
-  const { maxCPU, maxRAM } = useClusterResourceLimits({ projectId, clusterId });
-
   const currentTab = useMemo(() => {
     if (tabParam && validTabs.includes(tabParam as ValidTab)) {
       return tabParam as ValidTab;
@@ -520,8 +517,6 @@ const AppDataContainer: React.FC<AppDataContainerProps> = ({ tabParam }) => {
           .with("activity", () => <Activity />)
           .with("overview", () => (
             <Overview
-              maxCPU={maxCPU}
-              maxRAM={maxRAM}
               buttonStatus={buttonStatus}
             />
           ))

+ 13 - 10
dashboard/src/main/home/app-dashboard/app-view/tabs/Overview.tsx

@@ -2,27 +2,28 @@ import { PorterApp } from "@porter-dev/api-contracts";
 import Spacer from "components/porter/Spacer";
 import Text from "components/porter/Text";
 import { PorterAppFormData } from "lib/porter-apps";
-import React, { useEffect, useMemo } from "react";
-import { useFormContext, useFormState } from "react-hook-form";
+import React from "react";
+import { useFormContext } from "react-hook-form";
 import ServiceList from "../../validate-apply/services-settings/ServiceList";
 import {
   defaultSerialized,
   deserializeService,
 } from "lib/porter-apps/services";
-import Error from "components/porter/Error";
 import Button from "components/porter/Button";
 import { useLatestRevision } from "../LatestRevisionContext";
 import { useAppStatus } from "lib/hooks/useAppStatus";
 import { ButtonStatus } from "../AppDataContainer";
+import { useClusterResources } from "shared/ClusterResourcesContext";
 
 type Props = {
-  maxCPU: number;
-  maxRAM: number;
   buttonStatus: ButtonStatus;
 };
 
-const Overview: React.FC<Props> = ({ maxCPU, maxRAM, buttonStatus }) => {
+const Overview: React.FC<Props> = ({ buttonStatus }) => {
   const { formState } = useFormContext<PorterAppFormData>();
+
+  const { currentClusterResources } = useClusterResources(); 
+  
   const {
     porterApp,
     latestProto,
@@ -52,13 +53,15 @@ const Overview: React.FC<Props> = ({ maxCPU, maxRAM, buttonStatus }) => {
               service: defaultSerialized({
                 name: "pre-deploy",
                 type: "predeploy",
+                defaultCPU: currentClusterResources.defaultCPU,
+                defaultRAM: currentClusterResources.defaultRAM,
               }),
             })}
             existingServiceNames={latestProto.predeploy ? ["pre-deploy"] : []}
             isPredeploy
             fieldArrayName={"app.predeploy"}
-            maxCPU={maxCPU}
-            maxRAM={maxRAM}
+            maxCPU={currentClusterResources.maxCPU}
+            maxRAM={currentClusterResources.maxRAM}
           />
           <Spacer y={0.5} />
         </>
@@ -70,8 +73,8 @@ const Overview: React.FC<Props> = ({ maxCPU, maxRAM, buttonStatus }) => {
         fieldArrayName={"app.services"}
         existingServiceNames={Object.keys(latestProto.services)}
         serviceVersionStatus={serviceVersionStatus}
-        maxCPU={maxCPU}
-        maxRAM={maxRAM}
+        maxCPU={currentClusterResources.maxCPU}
+        maxRAM={currentClusterResources.maxRAM}
       />
       <Spacer y={0.75} />
       <Button

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

@@ -47,7 +47,7 @@ import {
 } from "../validate-apply/app-settings/types";
 import EnvSettings from "../validate-apply/app-settings/EnvSettings";
 import ImageSettings from "../image-settings/ImageSettings";
-import { useClusterResourceLimits } from "lib/hooks/useClusterResourceLimits";
+import { useClusterResources } from "shared/ClusterResourcesContext";
 
 type CreateAppProps = {} & RouteComponentProps;
 
@@ -77,10 +77,6 @@ const CreateApp: React.FC<CreateAppProps> = ({ history }) => {
     variables: {},
     secrets: {},
   });
-  const { maxCPU, maxRAM } = useClusterResourceLimits({
-    projectId: currentProject?.id,
-    clusterId: currentCluster?.id,
-  });
 
   const { data: porterApps = [] } = useQuery<string[]>(
     ["getPorterApps", currentProject?.id, currentCluster?.id],
@@ -192,6 +188,7 @@ const CreateApp: React.FC<CreateAppProps> = ({ history }) => {
     deploymentTargetID: deploymentTarget?.deployment_target_id,
     creating: true,
   });
+  const { currentClusterResources} = useClusterResources();
 
   const onSubmit = handleSubmit(async (data) => {
     try {
@@ -639,8 +636,8 @@ const CreateApp: React.FC<CreateAppProps> = ({ history }) => {
                     <ServiceList
                       addNewText={"Add a new service"}
                       fieldArrayName={"app.services"}
-                      maxCPU={maxCPU}
-                      maxRAM={maxRAM}
+                      maxCPU={currentClusterResources.maxCPU}
+                      maxRAM={currentClusterResources.maxRAM}
                     />
                   </>,
                   <>
@@ -667,13 +664,15 @@ const CreateApp: React.FC<CreateAppProps> = ({ history }) => {
                           service: defaultSerialized({
                             name: "pre-deploy",
                             type: "predeploy",
+                            defaultCPU: currentClusterResources.defaultCPU,
+                            defaultRAM: currentClusterResources.defaultRAM,
                           }),
                           expanded: true,
                         })}
                         isPredeploy
                         fieldArrayName={"app.predeploy"}
-                        maxCPU={maxCPU}
-                        maxRAM={maxRAM}
+                        maxCPU={currentClusterResources.maxCPU}
+                        maxRAM={currentClusterResources.maxRAM}
                       />
                     </>
                   ),

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

@@ -28,6 +28,7 @@ import {
 import { ControlledInput } from "components/porter/ControlledInput";
 import { PorterAppVersionStatus } from "lib/hooks/useAppStatus";
 import { zodResolver } from "@hookform/resolvers/zod";
+import { useClusterResources } from "shared/ClusterResourcesContext";
 
 const addServiceFormValidator = z.object({
   name: z
@@ -65,6 +66,8 @@ const ServiceList: React.FC<ServiceListProps> = ({
   // top level app form
   const { control: appControl } = useFormContext<PorterAppFormData>();
 
+  const { currentClusterResources } = useClusterResources();
+
   // add service modal form
   const {
     register,
@@ -154,8 +157,16 @@ const ServiceList: React.FC<ServiceListProps> = ({
     }
 
     append(
-      deserializeService({ service: defaultSerialized(data), expanded: true })
+      deserializeService({
+        service: defaultSerialized({
+          ...data,
+          defaultCPU: currentClusterResources.defaultCPU,
+          defaultRAM: currentClusterResources.defaultRAM,
+        }),
+        expanded: true,
+      })
     );
+    
     reset();
     setShowAddServiceModal(false);
   });

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

@@ -30,7 +30,7 @@ import { useAppValidation } from "lib/hooks/useAppValidation";
 import { PorterApp } from "@porter-dev/api-contracts";
 import axios from "axios";
 import GithubActionModal from "main/home/app-dashboard/new-app-flow/GithubActionModal";
-import { useClusterResourceLimits } from "lib/hooks/useClusterResourceLimits";
+import { useClusterResources } from "shared/ClusterResourcesContext";
 
 const AppTemplateForm: React.FC = () => {
   const [step, setStep] = useState(0);
@@ -46,6 +46,7 @@ const AppTemplateForm: React.FC = () => {
     variables: {},
     secrets: {},
   });
+  const { currentClusterResources } = useClusterResources(); 
 
   const {
     porterApp,
@@ -56,7 +57,6 @@ const AppTemplateForm: React.FC = () => {
     projectId,
     deploymentTarget,
   } = useLatestRevision();
-  const { maxCPU, maxRAM } = useClusterResourceLimits({ projectId, clusterId });
 
   const { data: baseEnvGroups = [] } = useQuery(
     ["getAllEnvGroups", projectId, clusterId],
@@ -238,8 +238,8 @@ const AppTemplateForm: React.FC = () => {
               <ServiceList
                 addNewText={"Add a new service"}
                 fieldArrayName={"app.services"}
-                maxCPU={maxCPU}
-                maxRAM={maxRAM}
+                maxCPU={currentClusterResources.maxCPU}
+                maxRAM={currentClusterResources.maxRAM}
               />
             </>,
             <>
@@ -265,6 +265,8 @@ const AppTemplateForm: React.FC = () => {
                   service: defaultSerialized({
                     name: "pre-deploy",
                     type: "predeploy",
+                    defaultCPU: currentClusterResources.defaultCPU,
+                    defaultRAM: currentClusterResources.defaultRAM,
                   }),
                 })}
                 existingServiceNames={
@@ -272,8 +274,8 @@ const AppTemplateForm: React.FC = () => {
                 }
                 isPredeploy
                 fieldArrayName={"app.predeploy"}
-                maxCPU={maxCPU}
-                maxRAM={maxRAM}
+                maxCPU={currentClusterResources.maxCPU}
+                maxRAM={currentClusterResources.maxRAM}
               />
             </>,
             <Button type="submit" loadingText={"Deploying..."} width={"150px"}>

+ 51 - 0
dashboard/src/shared/ClusterResourcesContext.tsx

@@ -0,0 +1,51 @@
+import React from "react";
+import { useClusterResourceLimits } from "lib/hooks/useClusterResourceLimits";
+import { createContext, useContext } from "react";
+import { Context } from "./Context";
+
+export type ClusterResources = {
+    maxCPU: number;
+    maxRAM: number;
+    defaultCPU: number;
+    defaultRAM: number;
+  };
+
+export const ClusterResourcesContext = createContext<{
+    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 ClusterResourcesProvider = ({ children }: { children: JSX.Element }) => {
+    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>
+    );
+  };
+
+  export default ClusterResourcesProvider;