feat: update service route

This commit is contained in:
Derock 2024-01-21 21:13:39 -05:00
parent 3c60018ca2
commit 6027de29e0
No known key found for this signature in database
5 changed files with 92 additions and 51 deletions

View file

@ -51,6 +51,7 @@
"dockerode": "^4.0.2",
"dotenv": "^16.3.1",
"drizzle-orm": "^0.29.3",
"drizzle-zod": "^0.5.1",
"extensionless": "^1.9.6",
"framer-motion": "^10.17.6",
"ipaddr.js": "^2.1.0",

View file

@ -107,6 +107,9 @@ dependencies:
drizzle-orm:
specifier: ^0.29.3
version: 0.29.3(@types/better-sqlite3@7.6.8)(@types/react@18.2.46)(better-sqlite3@9.2.2)(react@18.2.0)
drizzle-zod:
specifier: ^0.5.1
version: 0.5.1(drizzle-orm@0.29.3)(zod@3.22.4)
extensionless:
specifier: ^1.9.6
version: 1.9.6
@ -4287,6 +4290,16 @@ packages:
react: 18.2.0
dev: false
/drizzle-zod@0.5.1(drizzle-orm@0.29.3)(zod@3.22.4):
resolution: {integrity: sha512-C/8bvzUH/zSnVfwdSibOgFjLhtDtbKYmkbPbUCq46QZyZCH6kODIMSOgZ8R7rVjoI+tCj3k06MRJMDqsIeoS4A==}
peerDependencies:
drizzle-orm: '>=0.23.13'
zod: '*'
dependencies:
drizzle-orm: 0.29.3(@types/better-sqlite3@7.6.8)(@types/react@18.2.46)(better-sqlite3@9.2.2)(react@18.2.0)
zod: 3.22.4
dev: false
/eastasianwidth@0.2.0:
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}

View file

@ -1,20 +1,15 @@
import assert from "assert";
import { randomBytes } from "crypto";
import { eq } from "drizzle-orm";
import { createInsertSchema } from "drizzle-zod";
import { z } from "zod";
import { env } from "~/env";
import { projectMiddleware } from "~/server/api/middleware/project";
import { serviceMiddleware } from "~/server/api/middleware/service";
import { authenticatedProcedure, createTRPCRouter } from "~/server/api/trpc";
import { service } from "~/server/db/schema";
import {
DOCKER_DEPLOY_MODE_MAP,
DockerDeployMode,
DockerRestartCondition,
ServiceBuildMethod,
ServiceSource,
} from "~/server/db/types";
import { zDockerName } from "~/server/utils/zod";
import { DOCKER_DEPLOY_MODE_MAP, ServiceSource } from "~/server/db/types";
import { zDockerDuration, zDockerImage, zDockerName } from "~/server/utils/zod";
import { getServiceContainers } from "./containers";
export const serviceRouter = createTRPCRouter({
@ -61,39 +56,44 @@ export const serviceRouter = createTRPCRouter({
},
})
.input(
// sometimes i forget how powerful zod is
z
.object({
projectId: z.string(),
serviceId: z.string(),
})
.merge(
z
.object({
source: z.nativeEnum(ServiceSource),
environment: z.string(),
dockerImage: z.string(),
dockerRegistryUsername: z.string(),
dockerRegistryPassword: z.string(),
// TODO: restrict to valid github url
githubUrl: z.string().url(),
githubBranch: z.string(),
gitUrl: z.string(),
gitBranch: z.string(),
buildMethod: z.nativeEnum(ServiceBuildMethod),
buildPath: z.string(),
command: z.string(),
entrypoint: z.string(),
replicas: z.number(),
maxReplicasPerNode: z.number(),
deployMode: z.nativeEnum(DockerDeployMode),
zeroDowntime: z.boolean(),
max_cpu: z.number(),
max_memory: z.string(),
max_pids: z.number(),
restart: z.nativeEnum(DockerRestartCondition),
createInsertSchema(service, {
dockerImage: zDockerImage,
gitUrl: (schema) =>
schema.gitUrl.regex(
// https://www.debuggex.com/r/fFggA8Uc4YYKjl34 from https://stackoverflow.com/a/22312124
// /(?P<host>(git@|https:\/\/)([\w\.@]+)(\/|:))(?P<owner>[\w,\-,\_]+)\/(?P<repo>[\w,\-,\_]+)(.git){0,1}((\/){0,1})/,
/(git@|https:\/\/)([\w\.@]+)(\/|:)([\w,\-,\_]+)\/([\w,\-,\_]+)(.git){0,1}(\/{0,1})/,
{
message: "Must be a valid git url. (Regex failed)",
},
),
zeroDowntime: z.boolean().transform((val) => (val ? 1 : 0)),
restartDelay: zDockerDuration,
healthcheckEnabled: z.boolean().transform((val) => (val ? 1 : 0)),
healthcheckInterval: zDockerDuration,
healthcheckTimeout: zDockerDuration,
healthcheckStartPeriod: zDockerDuration,
loggingMaxSize: (schema) =>
schema.loggingMaxSize.regex(/^\d+[kmg]$/, {
message: "Must be an integer plus a modifier (k, m, or g).",
}),
loggingMaxFiles: (schema) => schema.loggingMaxFiles.positive(),
})
.omit({
id: true,
projectId: true,
name: true,
})
.partial(),
),
)
.strict(),
)
.use(projectMiddleware)
.use(serviceMiddleware)
@ -101,12 +101,14 @@ export const serviceRouter = createTRPCRouter({
await ctx.db
.update(service)
.set({
name: input.name,
zeroDowntime: input.zeroDowntime ? 1 : 0,
deployMode: input.deployMode,
...input,
projectId: undefined,
id: undefined,
})
.where(eq(service.id, ctx.service.id))
.execute();
return true;
}),
create: authenticatedProcedure

View file

@ -10,7 +10,7 @@ import {
} from "drizzle-orm/sqlite-core";
import {
DockerDeployMode,
type DockerRestartCondition,
DockerRestartCondition,
type DockerVolumeType,
type ServiceBuildMethod,
type ServicePortType,
@ -147,7 +147,9 @@ export const service = sqliteTable(
dockerRegistryPassword: text("docker_registry_password"),
// for github source
githubUrl: text("github_url"),
// https://github.com/{username}/{repo}
githubUsername: text("github_username"),
githubRepository: text("github_repository"),
githubBranch: text("github_branch"),
// for git source
@ -170,26 +172,34 @@ export const service = sqliteTable(
zeroDowntime: integer("zero_downtime").default(0).notNull(),
// deployment usage limits
max_cpu: real("max_cpu"),
max_memory: text("max_memory"),
max_pids: integer("max_pids"),
max_cpu: real("max_cpu").default(0).notNull(),
max_memory: text("max_memory").default("0").notNull(),
max_pids: integer("max_pids").default(0).notNull(),
// restart policy
restart: integer("restart").$type<DockerRestartCondition>(),
restartDelay: text("restart_delay"),
restartMaxAttempts: integer("restart_max_attempts"),
restart: integer("restart")
.$type<DockerRestartCondition>()
.default(DockerRestartCondition.OnFailure)
.notNull(),
// format: https://docs.docker.com/compose/compose-file/compose-file-v3/#specifying-durations
restartDelay: text("restart_delay").default("5s"),
restartMaxAttempts: integer("restart_max_attempts").default(-1).notNull(),
// healthcheck
healtcheckEnabled: integer("healthcheck_enabled").default(0).notNull(),
healthcheckEnabled: integer("healthcheck_enabled").default(0).notNull(),
healthcheckCommand: text("healthcheck_command"),
healthcheckInterval: text("healthcheck_interval"),
healthcheckTimeout: text("healthcheck_timeout"),
healthcheckRetries: integer("healthcheck_retries"),
healthcheckStartPeriod: text("healthcheck_start_period"),
healthcheckInterval: text("healthcheck_interval").default("30s").notNull(),
healthcheckTimeout: text("healthcheck_timeout").default("30s").notNull(),
healthcheckRetries: integer("healthcheck_retries").default(3).notNull(),
healthcheckStartPeriod: text("healthcheck_start_period")
.default("0s")
.notNull(),
// logging
loggingMaxSize: text("logging_max_size"),
loggingMaxFiles: integer("logging_max_files"),
// https://docs.docker.com/config/containers/logging/json-file/#options
loggingMaxSize: text("logging_max_size").default("-1").notNull(),
loggingMaxFiles: integer("logging_max_files").default(1).notNull(),
createdAt: integer("created_at").default(now).notNull(),
},

View file

@ -3,3 +3,18 @@ import z from "zod";
export const zDockerName = z.string().regex(/^[a-z0-9-]+$/, {
message: "Must be lowercase alphanumeric with dashes.",
});
export const zDockerImage = z
.string()
.regex(
/^(?:(?=[^:\/]{1,253})(?!-)[a-zA-Z0-9-]{1,63}(?<!-)(?:\.(?!-)[a-zA-Z0-9-]{1,63}(?<!-))*(?::[0-9]{1,5})?\/)?((?![._-])(?:[a-z0-9._-]*)(?<![._-])(?:\/(?![._-])[a-z0-9._-]*(?<![._-]))*)(?::(?![.-])[a-zA-Z0-9_.-]{1,128})?$/,
{
message: "Must be a valid Docker image name. (Regex failed)",
},
);
export const zDockerDuration = z
.string()
.regex(/(?:[\d.]+h)?(?:[\d.]+m)?(?:[\d.]+s)?/, {
message: "Must be a valid duration. (Regex failed)",
});