| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459 |
- import { match } from "ts-pattern";
- import { z } from "zod";
- import {
- SerializedAutoscaling,
- SerializedHealthcheck,
- autoscalingValidator,
- healthcheckValidator,
- deserializeAutoscaling,
- deserializeHealthCheck,
- serializeAutoscaling,
- serializeHealth,
- domainsValidator,
- serviceStringValidator,
- serviceNumberValidator,
- serviceBooleanValidator,
- ServiceField,
- } from "./values";
- import { Service, ServiceType } from "@porter-dev/api-contracts";
- export type DetectedServices = {
- services: ClientService[];
- predeploy?: ClientService;
- };
- type ClientServiceType = "web" | "worker" | "job" | "predeploy";
- // serviceValidator is the validator for a ClientService
- // This is used to validate a service when creating or updating an app
- export const serviceValidator = z.object({
- expanded: z.boolean().default(false).optional(),
- canDelete: z.boolean().default(true).optional(),
- name: serviceStringValidator,
- run: serviceStringValidator,
- instances: serviceNumberValidator,
- port: serviceNumberValidator,
- cpuCores: serviceNumberValidator,
- ramMegabytes: serviceNumberValidator,
- config: z.discriminatedUnion("type", [
- z.object({
- type: z.literal("web"),
- autoscaling: autoscalingValidator.optional(),
- domains: domainsValidator,
- healthCheck: healthcheckValidator.optional(),
- private: serviceBooleanValidator.default({
- value: false,
- readOnly: false,
- }),
- }),
- z.object({
- type: z.literal("worker"),
- autoscaling: autoscalingValidator.optional(),
- }),
- z.object({
- type: z.literal("job"),
- allowConcurrent: serviceBooleanValidator,
- cron: serviceStringValidator,
- }),
- z.object({
- type: z.literal("predeploy"),
- }),
- ]),
- });
- export type ClientService = z.infer<typeof serviceValidator>;
- // SerializedService is just the values of a Service without any override information
- // This is used as an intermediate step to convert a ClientService to a protobuf Service
- export type SerializedService = {
- name: string;
- run: string;
- instances: number;
- port: number;
- cpuCores: number;
- ramMegabytes: number;
- config:
- | {
- type: "web";
- domains: {
- name: string;
- }[];
- autoscaling?: SerializedAutoscaling;
- healthCheck?: SerializedHealthcheck;
- private: boolean;
- }
- | {
- type: "worker";
- autoscaling?: SerializedAutoscaling;
- }
- | {
- type: "job";
- allowConcurrent: boolean;
- cron: string;
- }
- | {
- type: "predeploy";
- };
- };
- export function isPredeployService(service: SerializedService | ClientService) {
- return service.config.type == "predeploy";
- }
- export function prefixSubdomain(subdomain: string) {
- if (subdomain.startsWith("https://") || subdomain.startsWith("http://")) {
- return subdomain;
- }
- return "https://" + subdomain;
- }
- export function defaultSerialized({
- name,
- type,
- }: {
- name: string;
- type: ClientServiceType;
- }): SerializedService {
- const baseService = {
- name,
- run: "",
- instances: 1,
- port: 3000,
- cpuCores: 0.1,
- ramMegabytes: 256,
- };
- const defaultAutoscaling: SerializedAutoscaling = {
- enabled: false,
- minInstances: 1,
- maxInstances: 10,
- cpuThresholdPercent: 50,
- memoryThresholdPercent: 50,
- };
- const defaultHealthCheck: SerializedHealthcheck = {
- enabled: false,
- httpPath: "/healthz",
- };
- return match(type)
- .with("web", () => ({
- ...baseService,
- config: {
- type: "web" as const,
- autoscaling: defaultAutoscaling,
- healthCheck: defaultHealthCheck,
- domains: [],
- private: false,
- },
- }))
- .with("worker", () => ({
- ...baseService,
- config: {
- type: "worker" as const,
- autoscaling: defaultAutoscaling,
- },
- }))
- .with("job", () => ({
- ...baseService,
- config: {
- type: "job" as const,
- allowConcurrent: false,
- cron: "",
- },
- }))
- .with("predeploy", () => ({
- ...baseService,
- config: {
- type: "predeploy" as const,
- },
- }))
- .exhaustive();
- }
- // serializeService converts a ClientService to a SerializedService
- // A SerializedService holds just the values of a ClientService
- // These values can be used to create a protobuf Service
- export function serializeService(service: ClientService): SerializedService {
- return match(service.config)
- .with({ type: "web" }, (config) =>
- Object.freeze({
- name: service.name.value,
- run: service.run.value,
- instances: service.instances.value,
- port: service.port.value,
- cpuCores: service.cpuCores.value,
- ramMegabytes: service.ramMegabytes.value,
- config: {
- type: "web" as const,
- autoscaling: serializeAutoscaling({
- autoscaling: config.autoscaling,
- }),
- healthCheck: serializeHealth({ health: config.healthCheck }),
- domains: config.domains.map((domain) => ({
- name: domain.name.value,
- })),
- private: config.private.value,
- },
- })
- )
- .with({ type: "worker" }, (config) =>
- Object.freeze({
- name: service.name.value,
- run: service.run.value,
- instances: service.instances.value,
- port: service.port.value,
- cpuCores: service.cpuCores.value,
- ramMegabytes: service.ramMegabytes.value,
- config: {
- type: "worker" as const,
- autoscaling: serializeAutoscaling({
- autoscaling: config.autoscaling,
- }),
- },
- })
- )
- .with({ type: "job" }, (config) =>
- Object.freeze({
- name: service.name.value,
- run: service.run.value,
- instances: service.instances.value,
- port: service.port.value,
- cpuCores: service.cpuCores.value,
- ramMegabytes: service.ramMegabytes.value,
- config: {
- type: "job" as const,
- allowConcurrent: config.allowConcurrent.value,
- cron: config.cron.value,
- },
- })
- )
- .with({ type: "predeploy" }, () =>
- Object.freeze({
- name: service.name.value,
- run: service.run.value,
- instances: service.instances.value,
- port: service.port.value,
- cpuCores: service.cpuCores.value,
- ramMegabytes: service.ramMegabytes.value,
- config: {
- type: "predeploy" as const,
- },
- })
- )
- .exhaustive();
- }
- // deserializeService converts a SerializedService to a ClientService
- // A deserialized ClientService represents the state of a service in the UI and which fields are editable
- export function deserializeService({
- service,
- override,
- expanded,
- }: {
- service: SerializedService;
- override?: SerializedService;
- expanded?: boolean;
- }): ClientService {
- const baseService = {
- expanded,
- canDelete: !override,
- name: ServiceField.string(service.name, override?.name),
- run: ServiceField.string(service.run, override?.run),
- instances: ServiceField.number(service.instances, override?.instances),
- port: ServiceField.number(service.port, override?.port),
- cpuCores: ServiceField.number(service.cpuCores, override?.cpuCores),
- ramMegabytes: ServiceField.number(
- service.ramMegabytes,
- override?.ramMegabytes
- ),
- };
- return match(service.config)
- .with({ type: "web" }, (config) => {
- const overrideWebConfig =
- override?.config.type == "web" ? override.config : undefined;
- return {
- ...baseService,
- config: {
- type: "web" as const,
- autoscaling: deserializeAutoscaling({
- autoscaling: config.autoscaling,
- override: overrideWebConfig?.autoscaling,
- }),
- healthCheck: deserializeHealthCheck({
- health: config.healthCheck,
- override: overrideWebConfig?.healthCheck,
- }),
- domains: config.domains.map((domain) => ({
- name: ServiceField.string(
- domain.name,
- overrideWebConfig?.domains.find(
- (overrideDomain) => overrideDomain.name == domain.name
- )?.name
- ),
- })),
- private: ServiceField.boolean(
- config.private,
- overrideWebConfig?.private
- ),
- },
- };
- })
- .with({ type: "worker" }, (config) => {
- const overrideWorkerConfig =
- override?.config.type == "worker" ? override.config : undefined;
- return {
- ...baseService,
- config: {
- type: "worker" as const,
- autoscaling: deserializeAutoscaling({
- autoscaling: config.autoscaling,
- override: overrideWorkerConfig?.autoscaling,
- }),
- },
- };
- })
- .with({ type: "job" }, (config) => {
- const overrideJobConfig =
- override?.config.type == "job" ? override.config : undefined;
- return {
- ...baseService,
- config: {
- type: "job" as const,
- allowConcurrent: ServiceField.boolean(
- config.allowConcurrent,
- overrideJobConfig?.allowConcurrent
- ),
- cron: ServiceField.string(config.cron, overrideJobConfig?.cron),
- },
- };
- })
- .with({ type: "predeploy" }, () => ({
- ...baseService,
- config: {
- type: "predeploy" as const,
- },
- }))
- .exhaustive();
- }
- // getServiceTypeEnumProto converts the type of a ClientService to the protobuf ServiceType enum
- export const serviceTypeEnumProto = (type: ClientServiceType): ServiceType => {
- return match(type)
- .with("web", () => ServiceType.WEB)
- .with("worker", () => ServiceType.WORKER)
- .with("job", () => ServiceType.JOB)
- .with("predeploy", () => ServiceType.JOB)
- .exhaustive();
- };
- // serviceProto converts a SerializedService to the protobuf Service
- // This is used as an intermediate step to convert a ClientService to a protobuf Service
- export function serviceProto(service: SerializedService): Service {
- return match(service.config)
- .with(
- { type: "web" },
- (config) =>
- new Service({
- ...service,
- type: serviceTypeEnumProto(config.type),
- config: {
- value: {
- ...config,
- },
- case: "webConfig",
- },
- })
- )
- .with(
- { type: "worker" },
- (config) =>
- new Service({
- ...service,
- type: serviceTypeEnumProto(config.type),
- config: {
- value: {
- ...config,
- },
- case: "workerConfig",
- },
- })
- )
- .with(
- { type: "job" },
- (config) =>
- new Service({
- ...service,
- type: serviceTypeEnumProto(config.type),
- config: {
- value: {
- ...config,
- },
- case: "jobConfig",
- },
- })
- )
- .with(
- { type: "predeploy" },
- (config) =>
- new Service({
- ...service,
- type: serviceTypeEnumProto(config.type),
- config: {
- value: {},
- case: "jobConfig",
- },
- })
- )
- .exhaustive();
- }
- // serializedServiceFromProto converts a protobuf Service to a SerializedService
- // This is used as an intermediate step to convert a protobuf Service to a ClientService
- export function serializedServiceFromProto({
- service,
- name,
- isPredeploy,
- }: {
- service: Service;
- name: string;
- isPredeploy?: boolean;
- }): SerializedService {
- const config = service.config;
- if (!config.case) {
- throw new Error("No case found on service config");
- }
- return match(config)
- .with({ case: "webConfig" }, ({ value }) => ({
- ...service,
- name,
- config: {
- type: "web" as const,
- autoscaling: value.autoscaling ? value.autoscaling : undefined,
- healthCheck: value.healthCheck ? value.healthCheck : undefined,
- ...value,
- },
- }))
- .with({ case: "workerConfig" }, ({ value }) => ({
- ...service,
- name,
- config: {
- type: "worker" as const,
- autoscaling: value.autoscaling ? value.autoscaling : undefined,
- ...value,
- },
- }))
- .with({ case: "jobConfig" }, ({ value }) => ({
- ...service,
- name,
- config: {
- type: isPredeploy ? ("predeploy" as const) : ("job" as const),
- ...value,
- },
- }))
- .exhaustive();
- }
|