diff --git a/dashboard/package-lock.json b/dashboard/package-lock.json
index e869b52f69..f2d67bc298 100644
--- a/dashboard/package-lock.json
+++ b/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"
diff --git a/dashboard/package.json b/dashboard/package.json
index 1349371986..6001b21596 100644
--- a/dashboard/package.json
+++ b/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",
diff --git a/dashboard/src/assets/moon.svg b/dashboard/src/assets/moon.svg
new file mode 100644
index 0000000000..6abf6e5eef
--- /dev/null
+++ b/dashboard/src/assets/moon.svg
@@ -0,0 +1,3 @@
+
diff --git a/dashboard/src/lib/porter-apps/services.ts b/dashboard/src/lib/porter-apps/services.ts
index 4f4d423bc2..dd6838a702 100644
--- a/dashboard/src/lib/porter-apps/services.ts
+++ b/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,
diff --git a/dashboard/src/main/home/app-dashboard/validate-apply/services-settings/ServiceContainer.tsx b/dashboard/src/main/home/app-dashboard/validate-apply/services-settings/ServiceContainer.tsx
index b42c5d724f..6add174305 100644
--- a/dashboard/src/main/home/app-dashboard/validate-apply/services-settings/ServiceContainer.tsx
+++ b/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 = ({
>
)}
+ {service.sleep?.value && (
+ <>
+
+
+
+ Sleeping
+
+ >
+ )}
{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);
`;
diff --git a/dashboard/src/main/home/app-dashboard/validate-apply/services-settings/tabs/Resources.tsx b/dashboard/src/main/home/app-dashboard/validate-apply/services-settings/tabs/Resources.tsx
index f62fad9006..f8d29b4f6b 100644
--- a/dashboard/src/main/home/app-dashboard/validate-apply/services-settings/tabs/Resources.tsx
+++ b/dashboard/src/main/home/app-dashboard/validate-apply/services-settings/tabs/Resources.tsx
@@ -47,6 +47,20 @@ const Resources: React.FC = ({
}
);
+ 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 (
<>
@@ -75,10 +89,11 @@ const Resources: React.FC = ({
});
}}
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 = ({
});
}}
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 && }
+ {service.config.type !== "job" && (
+ <>
+
+
+ Sleep Service
+
+ (?)
+
+
+
+ (
+ {
+ onChange({
+ ...value,
+ value: !value?.value,
+ });
+ }}
+ >
+ Pause all instances.
+
+ )}
+ />
+ >
+ )}
{match(service.config)
.with({ type: "job" }, () => null)
.with({ type: "predeploy" }, () => null)
@@ -128,13 +176,18 @@ const Resources: React.FC = ({
@@ -162,10 +215,11 @@ const Resources: React.FC = ({
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
+ )}
>
Enable autoscaling (overrides instances)
@@ -182,15 +236,17 @@ const Resources: React.FC = ({
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 = ({
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 = ({
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 = ({
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 = ({
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 = ({
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
+ )}
/>
)}
/>
diff --git a/dashboard/src/shared/icons/MoonBase.tsx b/dashboard/src/shared/icons/MoonBase.tsx
new file mode 100644
index 0000000000..17f70227ba
--- /dev/null
+++ b/dashboard/src/shared/icons/MoonBase.tsx
@@ -0,0 +1,24 @@
+import React from "react";
+
+import { type IconProps } from "./types";
+
+const MoonBaseIcon: React.FC = ({ className, styles, fill }) => {
+ return (
+
+ );
+};
+
+export default MoonBaseIcon;
diff --git a/dashboard/src/shared/icons/PullRequest.tsx b/dashboard/src/shared/icons/PullRequest.tsx
index e2a7002e4a..d8c9bd45e0 100644
--- a/dashboard/src/shared/icons/PullRequest.tsx
+++ b/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 = ({ className, styles, fill }) => {
return (
diff --git a/dashboard/src/shared/icons/types.ts b/dashboard/src/shared/icons/types.ts
new file mode 100644
index 0000000000..d54a10e551
--- /dev/null
+++ b/dashboard/src/shared/icons/types.ts
@@ -0,0 +1,5 @@
+export type IconProps = {
+ className?: string;
+ styles?: React.CSSProperties;
+ fill?: string;
+};
diff --git a/go.mod b/go.mod
index 08d8ec7191..9f5a7e5d6d 100644
--- a/go.mod
+++ b/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
diff --git a/go.sum b/go.sum
index ccc6585dd2..76343a7672 100644
--- a/go.sum
+++ b/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=
diff --git a/internal/porter_app/v2/yaml.go b/internal/porter_app/v2/yaml.go
index 89e28082e5..3b693824bd 100644
--- a/internal/porter_app/v2/yaml.go
+++ b/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 {