feat: update service route
This commit is contained in:
parent
3c60018ca2
commit
6027de29e0
|
@ -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",
|
||||||
|
|
|
@ -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==}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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(),
|
||||||
},
|
},
|
||||||
|
|
|
@ -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)",
|
||||||
|
});
|
||||||
|
|
Loading…
Reference in a new issue