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", "dockerode": "^4.0.2",
"dotenv": "^16.3.1", "dotenv": "^16.3.1",
"drizzle-orm": "^0.29.3", "drizzle-orm": "^0.29.3",
"drizzle-zod": "^0.5.1",
"extensionless": "^1.9.6", "extensionless": "^1.9.6",
"framer-motion": "^10.17.6", "framer-motion": "^10.17.6",
"ipaddr.js": "^2.1.0", "ipaddr.js": "^2.1.0",

View file

@ -107,6 +107,9 @@ dependencies:
drizzle-orm: drizzle-orm:
specifier: ^0.29.3 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) 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: extensionless:
specifier: ^1.9.6 specifier: ^1.9.6
version: 1.9.6 version: 1.9.6
@ -4287,6 +4290,16 @@ packages:
react: 18.2.0 react: 18.2.0
dev: false 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: /eastasianwidth@0.2.0:
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}

View file

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

View file

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