Parcourir la source

set service to sleep mode (#4427)

ianedwards il y a 2 ans
Parent
commit
9eef6453c4

+ 13 - 13
dashboard/package-lock.json

@@ -95,7 +95,7 @@
         "@babel/preset-typescript": "^7.15.0",
         "@ianvs/prettier-plugin-sort-imports": "^4.1.1",
         "@pmmmwh/react-refresh-webpack-plugin": "^0.4.3",
-        "@porter-dev/api-contracts": "^0.2.118",
+        "@porter-dev/api-contracts": "^0.2.131",
         "@testing-library/jest-dom": "^4.2.4",
         "@testing-library/react": "^9.3.2",
         "@testing-library/user-event": "^7.1.2",
@@ -2072,9 +2072,9 @@
       }
     },
     "node_modules/@bufbuild/protobuf": {
-      "version": "1.7.2",
-      "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-1.7.2.tgz",
-      "integrity": "sha512-i5GE2Dk5ekdlK1TR7SugY4LWRrKSfb5T1Qn4unpIMbfxoeGKERKQ59HG3iYewacGD10SR7UzevfPnh6my4tNmQ==",
+      "version": "1.8.0",
+      "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-1.8.0.tgz",
+      "integrity": "sha512-qR9FwI8QKIveDnUYutvfzbC21UZJJryYrLuZGjeZ/VGz+vXelUkK+xgkOHsvPEdYEdxtgUUq4313N8QtOehJ1Q==",
       "dev": true
     },
     "node_modules/@discoveryjs/json-ext": {
@@ -2754,9 +2754,9 @@
       }
     },
     "node_modules/@porter-dev/api-contracts": {
-      "version": "0.2.118",
-      "resolved": "https://registry.npmjs.org/@porter-dev/api-contracts/-/api-contracts-0.2.118.tgz",
-      "integrity": "sha512-A5cPRfTNKfC7qQ6gHFLyLRWU1bTDj4mHIB2XL4l3CqUl3KsX6p7EgwjEI3YX5sVwoUcGnlatiZ+BqgrLhlf4cg==",
+      "version": "0.2.131",
+      "resolved": "https://registry.npmjs.org/@porter-dev/api-contracts/-/api-contracts-0.2.131.tgz",
+      "integrity": "sha512-Ui66wdOQmWik6c6uvXn1m6SkcO7LO67BAqbkg8kY5F4YMHvusDhXEbq1Pl9FoaRQUsRk2OaZgxWXyTGj3wPYHQ==",
       "dev": true,
       "dependencies": {
         "@bufbuild/protobuf": "^1.1.0"
@@ -19584,9 +19584,9 @@
       }
     },
     "@bufbuild/protobuf": {
-      "version": "1.7.2",
-      "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-1.7.2.tgz",
-      "integrity": "sha512-i5GE2Dk5ekdlK1TR7SugY4LWRrKSfb5T1Qn4unpIMbfxoeGKERKQ59HG3iYewacGD10SR7UzevfPnh6my4tNmQ==",
+      "version": "1.8.0",
+      "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-1.8.0.tgz",
+      "integrity": "sha512-qR9FwI8QKIveDnUYutvfzbC21UZJJryYrLuZGjeZ/VGz+vXelUkK+xgkOHsvPEdYEdxtgUUq4313N8QtOehJ1Q==",
       "dev": true
     },
     "@discoveryjs/json-ext": {
@@ -20056,9 +20056,9 @@
       "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A=="
     },
     "@porter-dev/api-contracts": {
-      "version": "0.2.118",
-      "resolved": "https://registry.npmjs.org/@porter-dev/api-contracts/-/api-contracts-0.2.118.tgz",
-      "integrity": "sha512-A5cPRfTNKfC7qQ6gHFLyLRWU1bTDj4mHIB2XL4l3CqUl3KsX6p7EgwjEI3YX5sVwoUcGnlatiZ+BqgrLhlf4cg==",
+      "version": "0.2.131",
+      "resolved": "https://registry.npmjs.org/@porter-dev/api-contracts/-/api-contracts-0.2.131.tgz",
+      "integrity": "sha512-Ui66wdOQmWik6c6uvXn1m6SkcO7LO67BAqbkg8kY5F4YMHvusDhXEbq1Pl9FoaRQUsRk2OaZgxWXyTGj3wPYHQ==",
       "dev": true,
       "requires": {
         "@bufbuild/protobuf": "^1.1.0"

+ 1 - 1
dashboard/package.json

@@ -102,7 +102,7 @@
     "@babel/preset-typescript": "^7.15.0",
     "@ianvs/prettier-plugin-sort-imports": "^4.1.1",
     "@pmmmwh/react-refresh-webpack-plugin": "^0.4.3",
-    "@porter-dev/api-contracts": "^0.2.118",
+    "@porter-dev/api-contracts": "^0.2.131",
     "@testing-library/jest-dom": "^4.2.4",
     "@testing-library/react": "^9.3.2",
     "@testing-library/user-event": "^7.1.2",

+ 3 - 0
dashboard/src/assets/moon.svg

@@ -0,0 +1,3 @@
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M18.9 11.7001V8.7001M18.9 8.7001V5.7001M18.9 8.7001L21.9 8.7001M18.9 8.7001H15.9M14.1 5.7001V3.9001M14.1 3.9001V2.1001M14.1 3.9001L15.9 3.9001M14.1 3.9001L12.3 3.9001M21.2999 14.9395C20.3976 15.2132 19.4403 15.3603 18.4487 15.3603C13.0307 15.3603 8.63851 10.9681 8.63851 5.55013C8.63851 4.55891 8.78552 3.60203 9.05892 2.7001C5.03148 3.92148 2.09998 7.66314 2.09998 12.0895C2.09998 17.5075 6.49213 21.8996 11.9101 21.8996C16.3369 21.8996 20.0789 18.9676 21.2999 14.9395Z" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

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

@@ -110,6 +110,7 @@ export const serviceValidator = z.object({
   }),
   smartOptimization: serviceBooleanValidator.optional(),
   terminationGracePeriodSeconds: serviceNumberValidator.optional(),
+  sleep: serviceBooleanValidator.optional(),
   config: z.discriminatedUnion("type", [
     webConfigValidator,
     workerConfigValidator,
@@ -148,6 +149,7 @@ export type SerializedService = {
     gpuCoresNvidia: number;
   };
   terminationGracePeriodSeconds?: number;
+  sleep?: boolean;
   config:
     | {
         type: "web";
@@ -314,6 +316,7 @@ export function serializeService(service: ClientService): SerializedService {
       gpuCoresNvidia: service.gpu.gpuCoresNvidia.value,
     },
     terminationGracePeriodSeconds: service.terminationGracePeriodSeconds?.value,
+    sleep: service.sleep?.value,
     config: match(service.config)
       .with({ type: "web" }, (config) =>
         Object.freeze({
@@ -386,6 +389,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),
+    sleep: ServiceField.boolean(service.sleep, override?.sleep),
     gpu: {
       enabled: ServiceField.boolean(
         service.gpu?.enabled,
@@ -600,6 +604,7 @@ export function serviceProto(service: SerializedService): Service {
           runOptional: service.run,
           instancesOptional: service.instances,
           type: serviceTypeEnumProto(config.type),
+          sleep: service.sleep,
           config: {
             value: {
               ...config,
@@ -616,6 +621,7 @@ export function serviceProto(service: SerializedService): Service {
           runOptional: service.run,
           instancesOptional: service.instances,
           type: serviceTypeEnumProto(config.type),
+          sleep: service.sleep,
           config: {
             value: {
               ...config,
@@ -678,6 +684,7 @@ export function serializedServiceFromProto({
       ...service,
       run: service.runOptional ?? service.run,
       instances: service.instancesOptional ?? service.instances,
+      sleep: service.sleep,
       config: {
         type: "web" as const,
         autoscaling: value.autoscaling ? value.autoscaling : undefined,
@@ -690,6 +697,7 @@ export function serializedServiceFromProto({
       ...service,
       run: service.runOptional ?? service.run,
       instances: service.instancesOptional ?? service.instances,
+      sleep: service.sleep,
       config: {
         type: "worker" as const,
         autoscaling: value.autoscaling ? value.autoscaling : undefined,

+ 15 - 2
dashboard/src/main/home/app-dashboard/validate-apply/services-settings/ServiceContainer.tsx

@@ -15,6 +15,7 @@ import {
 
 import chip from "assets/computer-chip.svg";
 import job from "assets/job.png";
+import moon from "assets/moon.svg";
 import web from "assets/web.png";
 import worker from "assets/worker.png";
 
@@ -112,6 +113,15 @@ const ServiceContainer: React.FC<ServiceProps> = ({
               </TagContainer>
             </>
           )}
+          {service.sleep?.value && (
+            <>
+              <Spacer inline x={1.5} />
+              <TagContainer disableAnimation>
+                <ChipIcon src={moon} alt="Moon" />
+                <TagText>Sleeping</TagText>
+              </TagContainer>
+            </>
+          )}
         </ServiceTitle>
 
         {service.canDelete && (
@@ -260,7 +270,9 @@ const reflectiveGleam = keyframes`
   }
 `;
 
-const TagContainer = styled.div`
+const TagContainer = styled.div<{
+  disableAnimation?: boolean;
+}>`
   box-sizing: border-box;
   display: flex;
   flex-direction: row;
@@ -277,7 +289,8 @@ const TagContainer = styled.div`
   );
   background-size: 200% 200%;
   border-radius: 10px;
-  animation: ${reflectiveGleam} 4s infinite linear;
+  animation: ${reflectiveGleam} ${(props) =>
+    props.disableAnimation ? "" : "4s infinite"}
   border: 1px solid rgba(255, 255, 255, 0.2);
 `;
 

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

@@ -47,6 +47,20 @@ const Resources: React.FC<ResourcesProps> = ({
     }
   );
 
+  const sleepEnabled = watch(`app.services.${index}.sleep`, {
+    readOnly: false,
+    value: false,
+  });
+
+  const disabledMessage = (
+    defaultMessage: string,
+    isAsleep?: boolean
+  ): string => {
+    return isAsleep
+      ? "This service is asleep. Disable sleep mode to edit resources."
+      : defaultMessage;
+  };
+
   return (
     <>
       <Spacer y={1} />
@@ -75,10 +89,11 @@ const Resources: React.FC<ResourcesProps> = ({
               });
             }}
             step={0.01}
-            disabled={value.readOnly}
-            disabledTooltip={
-              "You may only edit this field in your porter.yaml."
-            }
+            disabled={value.readOnly || sleepEnabled?.value}
+            disabledTooltip={disabledMessage(
+              "You may only edit this field in your porter.yaml.",
+              sleepEnabled?.value
+            )}
             isSmartOptimizationOn={false}
             decimalsToRoundTo={2}
           />
@@ -107,16 +122,49 @@ const Resources: React.FC<ResourcesProps> = ({
               });
             }}
             step={10}
-            disabled={value.readOnly}
-            disabledTooltip={
-              "You may only edit this field in your porter.yaml."
-            }
+            disabled={value.readOnly || sleepEnabled?.value}
+            disabledTooltip={disabledMessage(
+              "You may only edit this field in your porter.yaml.",
+              sleepEnabled?.value
+            )}
             isSmartOptimizationOn={false}
           />
         )}
       />
-
       {currentProject?.gpu_enabled && <GPUResources index={index} />}
+      {service.config.type !== "job" && (
+        <>
+          <Spacer y={1} />
+          <Text>
+            Sleep Service
+            <a
+              href="https://docs.porter.run/configure/basic-configuration#sleep-mode"
+              target="_blank"
+              rel="noreferrer"
+            >
+              &nbsp;(?)
+            </a>
+          </Text>
+          <Spacer y={0.5} />
+          <Controller
+            name={`app.services.${index}.sleep`}
+            control={control}
+            render={({ field: { value, onChange } }) => (
+              <Checkbox
+                checked={Boolean(value?.value)}
+                toggleChecked={() => {
+                  onChange({
+                    ...value,
+                    value: !value?.value,
+                  });
+                }}
+              >
+                <Text color="helper">Pause all instances.</Text>
+              </Checkbox>
+            )}
+          />
+        </>
+      )}
       {match(service.config)
         .with({ type: "job" }, () => null)
         .with({ type: "predeploy" }, () => null)
@@ -128,13 +176,18 @@ const Resources: React.FC<ResourcesProps> = ({
             <ControlledInput
               type="text"
               placeholder="ex: 1"
-              disabled={service.instances.readOnly || autoscalingEnabled.value}
+              disabled={
+                service.instances.readOnly ||
+                autoscalingEnabled.value ||
+                sleepEnabled?.value
+              }
               width="300px"
-              disabledTooltip={
+              disabledTooltip={disabledMessage(
                 service.instances.readOnly
                   ? "You may only edit this field in your porter.yaml."
-                  : "Disable autoscaling to specify instances."
-              }
+                  : "Disable autoscaling to specify instances.",
+                sleepEnabled?.value
+              )}
               {...register(`app.services.${index}.instances.value`)}
             />
             <Spacer y={1} />
@@ -162,10 +215,11 @@ const Resources: React.FC<ResourcesProps> = ({
                         value: !value.value,
                       });
                     }}
-                    disabled={value.readOnly}
-                    disabledTooltip={
-                      "You may only edit this field in your porter.yaml."
-                    }
+                    disabled={value.readOnly || sleepEnabled?.value}
+                    disabledTooltip={disabledMessage(
+                      "You may only edit this field in your porter.yaml.",
+                      sleepEnabled?.value
+                    )}
                   >
                     <Text color="helper">
                       Enable autoscaling (overrides instances)
@@ -182,15 +236,17 @@ const Resources: React.FC<ResourcesProps> = ({
                   label="Min instances"
                   placeholder="ex: 1"
                   disabled={
-                    config.autoscaling?.minInstances?.readOnly ??
-                    !config.autoscaling?.enabled.value
+                    (config.autoscaling?.minInstances?.readOnly ??
+                      !config.autoscaling?.enabled.value) ||
+                    sleepEnabled?.value
                   }
                   width="300px"
-                  disabledTooltip={
+                  disabledTooltip={disabledMessage(
                     config.autoscaling?.minInstances?.readOnly
                       ? "You may only edit this field in your porter.yaml."
-                      : "Enable autoscaling to specify min instances."
-                  }
+                      : "Enable autoscaling to specify min instances.",
+                    sleepEnabled?.value
+                  )}
                   {...register(
                     `app.services.${index}.config.autoscaling.minInstances.value`
                   )}
@@ -201,15 +257,17 @@ const Resources: React.FC<ResourcesProps> = ({
                   label="Max instances"
                   placeholder="ex: 10"
                   disabled={
-                    config.autoscaling?.maxInstances?.readOnly ??
-                    !config.autoscaling?.enabled.value
+                    (config.autoscaling?.maxInstances?.readOnly ??
+                      !config.autoscaling?.enabled.value) ||
+                    sleepEnabled?.value
                   }
                   width="300px"
-                  disabledTooltip={
+                  disabledTooltip={disabledMessage(
                     config.autoscaling?.maxInstances?.readOnly
                       ? "You may only edit this field in your porter.yaml."
-                      : "Enable autoscaling to specify max instances."
-                  }
+                      : "Enable autoscaling to specify max instances.",
+                    sleepEnabled?.value
+                  )}
                   {...register(
                     `app.services.${index}.config.autoscaling.maxInstances.value`
                   )}
@@ -225,7 +283,11 @@ const Resources: React.FC<ResourcesProps> = ({
                       min={0}
                       max={100}
                       value={value?.value.toString() ?? "50"}
-                      disabled={value?.readOnly || !config.autoscaling?.enabled}
+                      disabled={
+                        value?.readOnly ||
+                        !config.autoscaling?.enabled ||
+                        sleepEnabled?.value
+                      }
                       width="300px"
                       setValue={(e) => {
                         onChange({
@@ -233,11 +295,12 @@ const Resources: React.FC<ResourcesProps> = ({
                           value: e,
                         });
                       }}
-                      disabledTooltip={
+                      disabledTooltip={disabledMessage(
                         value?.readOnly
                           ? "You may only edit this field in your porter.yaml."
-                          : "Enable autoscaling to specify CPU threshold."
-                      }
+                          : "Enable autoscaling to specify CPU threshold.",
+                        sleepEnabled?.value
+                      )}
                     />
                   )}
                 />
@@ -252,7 +315,11 @@ const Resources: React.FC<ResourcesProps> = ({
                       min={0}
                       max={100}
                       value={value?.value.toString() ?? "50"}
-                      disabled={value?.readOnly || !config.autoscaling?.enabled}
+                      disabled={
+                        value?.readOnly ||
+                        !config.autoscaling?.enabled ||
+                        sleepEnabled?.value
+                      }
                       width="300px"
                       setValue={(e) => {
                         onChange({
@@ -260,11 +327,12 @@ const Resources: React.FC<ResourcesProps> = ({
                           value: e,
                         });
                       }}
-                      disabledTooltip={
+                      disabledTooltip={disabledMessage(
                         value?.readOnly
                           ? "You may only edit this field in your porter.yaml."
-                          : "Enable autoscaling to specify RAM threshold."
-                      }
+                          : "Enable autoscaling to specify RAM threshold.",
+                        sleepEnabled?.value
+                      )}
                     />
                   )}
                 />

+ 24 - 0
dashboard/src/shared/icons/MoonBase.tsx

@@ -0,0 +1,24 @@
+import React from "react";
+
+import { type IconProps } from "./types";
+
+const MoonBaseIcon: React.FC<IconProps> = ({ className, styles, fill }) => {
+  return (
+    <svg
+      viewBox="0 0 24 24"
+      xmlns="http://www.w3.org/2000/svg"
+      className={className}
+      style={styles}
+      fill={fill}
+    >
+      <path
+        d="M21.5999 14.6398C20.6977 14.9134 19.7404 15.0606 18.7487 15.0606C13.3307 15.0606 8.93856 10.6684 8.93856 5.25042C8.93856 4.2592 9.08557 3.30232 9.35897 2.40039C5.33153 3.62177 2.40002 7.36343 2.40002 11.7898C2.40002 17.2078 6.79218 21.5999 12.2102 21.5999C16.637 21.5999 20.3789 18.6679 21.5999 14.6398Z"
+        stroke="black"
+        strokeWidth="2"
+        strokeLinejoin="round"
+      />
+    </svg>
+  );
+};
+
+export default MoonBaseIcon;

+ 2 - 5
dashboard/src/shared/icons/PullRequest.tsx

@@ -1,9 +1,6 @@
 import React from "react";
-type IconProps = {
-  className?: string;
-  styles?: React.CSSProperties;
-  fill?: string;
-};
+
+import { type IconProps } from "./types";
 
 const PullRequestIcon: React.FC<IconProps> = ({ className, styles, fill }) => {
   return (

+ 5 - 0
dashboard/src/shared/icons/types.ts

@@ -0,0 +1,5 @@
+export type IconProps = {
+  className?: string;
+  styles?: React.CSSProperties;
+  fill?: string;
+};

+ 1 - 1
go.mod

@@ -84,7 +84,7 @@ require (
 	github.com/matryer/is v1.4.0
 	github.com/nats-io/nats.go v1.24.0
 	github.com/open-policy-agent/opa v0.44.0
-	github.com/porter-dev/api-contracts v0.2.127
+	github.com/porter-dev/api-contracts v0.2.131
 	github.com/riandyrn/otelchi v0.5.1
 	github.com/santhosh-tekuri/jsonschema/v5 v5.0.1
 	github.com/stefanmcshane/helm v0.0.0-20221213002717-88a4a2c6e77d

+ 2 - 2
go.sum

@@ -1525,8 +1525,8 @@ github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/polyfloyd/go-errorlint v0.0.0-20210722154253-910bb7978349/go.mod h1:wi9BfjxjF/bwiZ701TzmfKu6UKC357IOAtNr0Td0Lvw=
-github.com/porter-dev/api-contracts v0.2.127 h1:pF2vV9sohSzBIauBunII1n7f2puqMXdEGza4GdSkQBQ=
-github.com/porter-dev/api-contracts v0.2.127/go.mod h1:fX6JmP5QuzxDLvqP3evFOTXjI4dHxsG0+VKNTjImZU8=
+github.com/porter-dev/api-contracts v0.2.131 h1:WxungE4EL5F8oacVB52i3vKuxyf1UaebNlA4eJmcKLM=
+github.com/porter-dev/api-contracts v0.2.131/go.mod h1:fX6JmP5QuzxDLvqP3evFOTXjI4dHxsG0+VKNTjImZU8=
 github.com/porter-dev/switchboard v0.0.3 h1:dBuYkiVLa5Ce7059d6qTe9a1C2XEORFEanhbtV92R+M=
 github.com/porter-dev/switchboard v0.0.3/go.mod h1:xSPzqSFMQ6OSbp42fhCi4AbGbQbsm6nRvOkrblFeXU4=
 github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=

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

@@ -189,6 +189,7 @@ type Service struct {
 	Private                       *bool             `yaml:"private,omitempty" validate:"excluded_unless=Type web"`
 	IngressAnnotations            map[string]string `yaml:"ingressAnnotations,omitempty" validate:"excluded_unless=Type web"`
 	DisableTLS                    *bool             `yaml:"disableTLS,omitempty" validate:"excluded_unless=Type web"`
+	Sleep                         *bool             `yaml:"sleep,omitempty" validate:"excluded_unless=Type job"`
 }
 
 // AutoScaling represents the autoscaling settings for web services
@@ -444,6 +445,9 @@ func serviceProtoFromConfig(service Service, serviceType porterv1.ServiceType) (
 		if service.DisableTLS != nil {
 			webConfig.DisableTls = service.DisableTLS
 		}
+		if service.Sleep != nil {
+			serviceProto.Sleep = service.Sleep
+		}
 
 		serviceProto.Config = &porterv1.Service_WebConfig{
 			WebConfig: webConfig,
@@ -475,6 +479,10 @@ func serviceProtoFromConfig(service Service, serviceType porterv1.ServiceType) (
 		}
 		workerConfig.HealthCheck = healthCheck
 
+		if service.Sleep != nil {
+			serviceProto.Sleep = service.Sleep
+		}
+
 		serviceProto.Config = &porterv1.Service_WorkerConfig{
 			WorkerConfig: workerConfig,
 		}
@@ -582,6 +590,7 @@ func appServiceFromProto(service *porterv1.Service) (Service, error) {
 		SmartOptimization:             service.SmartOptimization,
 		GPU:                           gpu,
 		TerminationGracePeriodSeconds: service.TerminationGracePeriodSeconds,
+		Sleep:                         service.Sleep,
 	}
 
 	switch service.Type {