feat: add deploy databases to external server

This commit is contained in:
Mauricio Siu 2024-09-08 22:56:21 -06:00
parent 0a889c5db1
commit 6007427a6c
23 changed files with 4037 additions and 34 deletions

View File

@ -35,6 +35,15 @@ import { useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
type DbType = typeof mySchema._type.type;
@ -71,6 +80,9 @@ const baseDatabaseSchema = z.object({
databasePassword: z.string(),
dockerImage: z.string(),
description: z.string().nullable(),
serverId: z.string().min(1, {
message: "Server is required",
}),
});
const mySchema = z.discriminatedUnion("type", [
@ -145,6 +157,7 @@ export const AddDatabase = ({ projectId, projectName }: Props) => {
const utils = api.useUtils();
const [visible, setVisible] = useState(false);
const slug = slugify(projectName);
const { data: servers } = api.server.all.useQuery();
const postgresMutation = api.postgres.create.useMutation();
const mongoMutation = api.mongo.create.useMutation();
const redisMutation = api.redis.create.useMutation();
@ -183,6 +196,7 @@ export const AddDatabase = ({ projectId, projectName }: Props) => {
appName: data.appName,
dockerImage: defaultDockerImage,
projectId,
serverId: data.serverId,
description: data.description,
};
@ -191,8 +205,10 @@ export const AddDatabase = ({ projectId, projectName }: Props) => {
...commonParams,
databasePassword: data.databasePassword,
databaseName: data.databaseName,
databaseUser:
data.databaseUser || databasesUserDefaultPlaceholder[data.type],
serverId: data.serverId,
});
} else if (data.type === "mongo") {
promise = mongoMutation.mutateAsync({
@ -200,6 +216,7 @@ export const AddDatabase = ({ projectId, projectName }: Props) => {
databasePassword: data.databasePassword,
databaseUser:
data.databaseUser || databasesUserDefaultPlaceholder[data.type],
serverId: data.serverId,
});
} else if (data.type === "redis") {
promise = redisMutation.mutateAsync({
@ -215,6 +232,7 @@ export const AddDatabase = ({ projectId, projectName }: Props) => {
databaseName: data.databaseName,
databaseUser:
data.databaseUser || databasesUserDefaultPlaceholder[data.type],
serverId: data.serverId,
});
} else if (data.type === "mysql") {
promise = mysqlMutation.mutateAsync({
@ -224,6 +242,7 @@ export const AddDatabase = ({ projectId, projectName }: Props) => {
databaseUser:
data.databaseUser || databasesUserDefaultPlaceholder[data.type],
databaseRootPassword: data.databaseRootPassword,
serverId: data.serverId,
});
}
@ -352,6 +371,39 @@ export const AddDatabase = ({ projectId, projectName }: Props) => {
</FormItem>
)}
/>
<FormField
control={form.control}
name="serverId"
render={({ field }) => (
<FormItem>
<FormLabel>Select a Server</FormLabel>
<Select
onValueChange={field.onChange}
defaultValue={field.value}
>
<SelectTrigger>
<SelectValue placeholder="Select a Server" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
{servers?.map((server) => (
<SelectItem
key={server.serverId}
value={server.serverId}
>
{server.name}
</SelectItem>
))}
<SelectLabel>
Servers ({servers?.length})
</SelectLabel>
</SelectGroup>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="appName"

View File

@ -0,0 +1,34 @@
ALTER TABLE "postgres" ADD COLUMN "serverId" text;--> statement-breakpoint
ALTER TABLE "mariadb" ADD COLUMN "serverId" text;--> statement-breakpoint
ALTER TABLE "mongo" ADD COLUMN "serverId" text;--> statement-breakpoint
ALTER TABLE "mysql" ADD COLUMN "serverId" text;--> statement-breakpoint
ALTER TABLE "redis" ADD COLUMN "serverId" text;--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "postgres" ADD CONSTRAINT "postgres_serverId_server_serverId_fk" FOREIGN KEY ("serverId") REFERENCES "public"."server"("serverId") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "mariadb" ADD CONSTRAINT "mariadb_serverId_server_serverId_fk" FOREIGN KEY ("serverId") REFERENCES "public"."server"("serverId") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "mongo" ADD CONSTRAINT "mongo_serverId_server_serverId_fk" FOREIGN KEY ("serverId") REFERENCES "public"."server"("serverId") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "mysql" ADD CONSTRAINT "mysql_serverId_server_serverId_fk" FOREIGN KEY ("serverId") REFERENCES "public"."server"("serverId") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "redis" ADD CONSTRAINT "redis_serverId_server_serverId_fk" FOREIGN KEY ("serverId") REFERENCES "public"."server"("serverId") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;

File diff suppressed because it is too large Load Diff

View File

@ -295,6 +295,13 @@
"when": 1725830160928,
"tag": "0041_mute_polaris",
"breakpoints": true
},
{
"idx": 42,
"version": "6",
"when": 1725856996201,
"tag": "0042_wandering_inhumans",
"breakpoints": true
}
]
}

View File

@ -15,7 +15,6 @@ import {
stopService,
} from "@/server/utils/docker/utils";
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import { createMount } from "../services/mount";
import {
createMysql,

View File

@ -1,8 +1,4 @@
import {
cliProcedure,
createTRPCRouter,
protectedProcedure,
} from "@/server/api/trpc";
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
import { db } from "@/server/db";
import {
applications,
@ -34,7 +30,6 @@ import {
checkProjectAccess,
findUserByAuthId,
} from "../services/user";
import { findAdmin, findAdminByAuthId } from "../services/admin";
export const projectRouter = createTRPCRouter({
create: protectedProcedure

View File

@ -8,6 +8,7 @@ import { generatePassword } from "@/templates/utils";
import { TRPCError } from "@trpc/server";
import { eq, getTableColumns } from "drizzle-orm";
import { validUniqueServerAppName } from "./project";
import { executeCommand } from "@/server/utils/servers/command";
export type Mariadb = typeof mariadb.$inferSelect;
@ -56,6 +57,7 @@ export const findMariadbById = async (mariadbId: string) => {
with: {
project: true,
mounts: true,
server: true,
backups: {
with: {
destination: true,
@ -118,7 +120,15 @@ export const findMariadbByBackupId = async (backupId: string) => {
export const deployMariadb = async (mariadbId: string) => {
const mariadb = await findMariadbById(mariadbId);
try {
await pullImage(mariadb.dockerImage);
if (mariadb.serverId) {
await executeCommand(
mariadb.serverId,
`docker pull ${mariadb.dockerImage}`,
);
} else {
await pullImage(mariadb.dockerImage);
}
await buildMariadb(mariadb);
await updateMariadbById(mariadbId, {
applicationStatus: "done",

View File

@ -8,6 +8,7 @@ import { generatePassword } from "@/templates/utils";
import { TRPCError } from "@trpc/server";
import { eq, getTableColumns } from "drizzle-orm";
import { validUniqueServerAppName } from "./project";
import { executeCommand } from "@/server/utils/servers/command";
export type Mongo = typeof mongo.$inferSelect;
@ -52,6 +53,7 @@ export const findMongoById = async (mongoId: string) => {
with: {
project: true,
mounts: true,
server: true,
backups: {
with: {
destination: true,
@ -114,7 +116,12 @@ export const removeMongoById = async (mongoId: string) => {
export const deployMongo = async (mongoId: string) => {
const mongo = await findMongoById(mongoId);
try {
await pullImage(mongo.dockerImage);
if (mongo.serverId) {
await executeCommand(mongo.serverId, `docker pull ${mongo.dockerImage}`);
} else {
await pullImage(mongo.dockerImage);
}
await buildMongo(mongo);
await updateMongoById(mongoId, {
applicationStatus: "done",

View File

@ -8,6 +8,7 @@ import { generatePassword } from "@/templates/utils";
import { TRPCError } from "@trpc/server";
import { eq, getTableColumns } from "drizzle-orm";
import { validUniqueServerAppName } from "./project";
import { executeCommand } from "@/server/utils/servers/command";
export type MySql = typeof mysql.$inferSelect;
@ -57,6 +58,7 @@ export const findMySqlById = async (mysqlId: string) => {
with: {
project: true,
mounts: true,
server: true,
backups: {
with: {
destination: true,
@ -119,7 +121,12 @@ export const removeMySqlById = async (mysqlId: string) => {
export const deployMySql = async (mysqlId: string) => {
const mysql = await findMySqlById(mysqlId);
try {
await pullImage(mysql.dockerImage);
if (mysql.serverId) {
await executeCommand(mysql.serverId, `docker pull ${mysql.dockerImage}`);
} else {
await pullImage(mysql.dockerImage);
}
await buildMysql(mysql);
await updateMySqlById(mysqlId, {
applicationStatus: "done",

View File

@ -8,6 +8,7 @@ import { generatePassword } from "@/templates/utils";
import { TRPCError } from "@trpc/server";
import { eq, getTableColumns } from "drizzle-orm";
import { validUniqueServerAppName } from "./project";
import { executeCommand } from "@/server/utils/servers/command";
export type Postgres = typeof postgres.$inferSelect;
@ -45,13 +46,13 @@ export const createPostgres = async (input: typeof apiCreatePostgres._type) => {
return newPostgres;
};
export const findPostgresById = async (postgresId: string) => {
const result = await db.query.postgres.findFirst({
where: eq(postgres.postgresId, postgresId),
with: {
project: true,
mounts: true,
server: true,
backups: {
with: {
destination: true,
@ -114,7 +115,15 @@ export const removePostgresById = async (postgresId: string) => {
export const deployPostgres = async (postgresId: string) => {
const postgres = await findPostgresById(postgresId);
try {
await pullImage(postgres.dockerImage);
if (postgres.serverId) {
await executeCommand(
postgres.serverId,
`docker pull ${postgres.dockerImage}`,
);
} else {
await pullImage(postgres.dockerImage);
}
await buildPostgres(postgres);
await updatePostgresById(postgresId, {
applicationStatus: "done",

View File

@ -8,6 +8,7 @@ import { generatePassword } from "@/templates/utils";
import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";
import { validUniqueServerAppName } from "./project";
import { executeCommand } from "@/server/utils/servers/command";
export type Redis = typeof redis.$inferSelect;
@ -53,6 +54,7 @@ export const findRedisById = async (redisId: string) => {
with: {
project: true,
mounts: true,
server: true,
},
});
if (!result) {
@ -91,7 +93,12 @@ export const removeRedisById = async (redisId: string) => {
export const deployRedis = async (redisId: string) => {
const redis = await findRedisById(redisId);
try {
await pullImage(redis.dockerImage);
if (redis.serverId) {
await executeCommand(redis.serverId, `docker pull ${redis.dockerImage}`);
} else {
await pullImage(redis.dockerImage);
}
await buildRedis(redis);
await updateRedisById(redisId, {
applicationStatus: "done",

View File

@ -9,6 +9,7 @@ import { mounts } from "./mount";
import { projects } from "./project";
import { applicationStatus } from "./shared";
import { generateAppName } from "./utils";
import { server } from "./server";
export const mariadb = pgTable("mariadb", {
mariadbId: text("mariadbId")
@ -44,6 +45,9 @@ export const mariadb = pgTable("mariadb", {
projectId: text("projectId")
.notNull()
.references(() => projects.projectId, { onDelete: "cascade" }),
serverId: text("serverId").references(() => server.serverId, {
onDelete: "cascade",
}),
});
export const mariadbRelations = relations(mariadb, ({ one, many }) => ({
@ -53,6 +57,10 @@ export const mariadbRelations = relations(mariadb, ({ one, many }) => ({
}),
backups: many(backups),
mounts: many(mounts),
server: one(server, {
fields: [mariadb.serverId],
references: [server.serverId],
}),
}));
const createSchema = createInsertSchema(mariadb, {
@ -88,6 +96,7 @@ export const apiCreateMariaDB = createSchema
databaseName: true,
databaseUser: true,
databasePassword: true,
serverId: true,
})
.required();

View File

@ -9,6 +9,7 @@ import { mounts } from "./mount";
import { projects } from "./project";
import { applicationStatus } from "./shared";
import { generateAppName } from "./utils";
import { server } from "./server";
export const mongo = pgTable("mongo", {
mongoId: text("mongoId")
@ -40,6 +41,9 @@ export const mongo = pgTable("mongo", {
projectId: text("projectId")
.notNull()
.references(() => projects.projectId, { onDelete: "cascade" }),
serverId: text("serverId").references(() => server.serverId, {
onDelete: "cascade",
}),
});
export const mongoRelations = relations(mongo, ({ one, many }) => ({
@ -49,6 +53,10 @@ export const mongoRelations = relations(mongo, ({ one, many }) => ({
}),
backups: many(backups),
mounts: many(mounts),
server: one(server, {
fields: [mongo.serverId],
references: [server.serverId],
}),
}));
const createSchema = createInsertSchema(mongo, {
@ -80,6 +88,7 @@ export const apiCreateMongo = createSchema
description: true,
databaseUser: true,
databasePassword: true,
serverId: true,
})
.required();

View File

@ -1,4 +1,3 @@
import { generatePassword } from "@/templates/utils";
import { relations } from "drizzle-orm";
import { integer, pgTable, text } from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod";
@ -9,6 +8,7 @@ import { mounts } from "./mount";
import { projects } from "./project";
import { applicationStatus } from "./shared";
import { generateAppName } from "./utils";
import { server } from "./server";
export const mysql = pgTable("mysql", {
mysqlId: text("mysqlId")
@ -42,6 +42,9 @@ export const mysql = pgTable("mysql", {
projectId: text("projectId")
.notNull()
.references(() => projects.projectId, { onDelete: "cascade" }),
serverId: text("serverId").references(() => server.serverId, {
onDelete: "cascade",
}),
});
export const mysqlRelations = relations(mysql, ({ one, many }) => ({
@ -51,6 +54,10 @@ export const mysqlRelations = relations(mysql, ({ one, many }) => ({
}),
backups: many(backups),
mounts: many(mounts),
server: one(server, {
fields: [mysql.serverId],
references: [server.serverId],
}),
}));
const createSchema = createInsertSchema(mysql, {
@ -86,6 +93,7 @@ export const apiCreateMySql = createSchema
databaseUser: true,
databasePassword: true,
databaseRootPassword: true,
serverId: true,
})
.required();

View File

@ -9,6 +9,7 @@ import { mounts } from "./mount";
import { projects } from "./project";
import { applicationStatus } from "./shared";
import { generateAppName } from "./utils";
import { server } from "./server";
export const postgres = pgTable("postgres", {
postgresId: text("postgresId")
@ -41,6 +42,9 @@ export const postgres = pgTable("postgres", {
projectId: text("projectId")
.notNull()
.references(() => projects.projectId, { onDelete: "cascade" }),
serverId: text("serverId").references(() => server.serverId, {
onDelete: "cascade",
}),
});
export const postgresRelations = relations(postgres, ({ one, many }) => ({
@ -50,6 +54,10 @@ export const postgresRelations = relations(postgres, ({ one, many }) => ({
}),
backups: many(backups),
mounts: many(mounts),
server: one(server, {
fields: [postgres.serverId],
references: [server.serverId],
}),
}));
const createSchema = createInsertSchema(postgres, {
@ -82,6 +90,7 @@ export const apiCreatePostgres = createSchema
dockerImage: true,
projectId: true,
description: true,
serverId: true,
})
.required();

View File

@ -1,4 +1,3 @@
import { generatePassword } from "@/templates/utils";
import { relations } from "drizzle-orm";
import { integer, pgTable, text } from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod";
@ -8,6 +7,7 @@ import { mounts } from "./mount";
import { projects } from "./project";
import { applicationStatus } from "./shared";
import { generateAppName } from "./utils";
import { server } from "./server";
export const redis = pgTable("redis", {
redisId: text("redisId")
@ -38,6 +38,9 @@ export const redis = pgTable("redis", {
projectId: text("projectId")
.notNull()
.references(() => projects.projectId, { onDelete: "cascade" }),
serverId: text("serverId").references(() => server.serverId, {
onDelete: "cascade",
}),
});
export const redisRelations = relations(redis, ({ one, many }) => ({
@ -46,6 +49,10 @@ export const redisRelations = relations(redis, ({ one, many }) => ({
references: [projects.projectId],
}),
mounts: many(mounts),
server: one(server, {
fields: [redis.serverId],
references: [server.serverId],
}),
}));
const createSchema = createInsertSchema(redis, {
@ -75,6 +82,7 @@ export const apiCreateRedis = createSchema
dockerImage: true,
projectId: true,
description: true,
serverId: true,
})
.required();

View File

@ -7,7 +7,15 @@ import { admins } from "./admin";
import { generateAppName } from "./utils";
import { deployments } from "./deployment";
import { sshKeys } from "./ssh-key";
import { applications, compose } from ".";
import {
applications,
compose,
mariadb,
mongo,
mysql,
postgres,
redis,
} from ".";
export const server = pgTable("server", {
serverId: text("serverId")
@ -46,6 +54,11 @@ export const serverRelations = relations(server, ({ one, many }) => ({
}),
applications: many(applications),
compose: many(compose),
redis: many(redis),
mariadb: many(mariadb),
mongo: many(mongo),
mysql: many(mysql),
postgres: many(postgres),
}));
const createSchema = createInsertSchema(server, {

View File

@ -130,20 +130,6 @@ export const mechanizeDockerContainer = async (
const image = getImageName(application);
const authConfig = getAuthConfig(application);
const docker = await getRemoteDocker(application.serverId);
// const server = await findServerById(application.serverId);
// if (!server.sshKeyId) return;
// const keys = await readSSHKey(server.sshKeyId);
// const docker = new Dockerode({
// host: server.ipAddress,
// port: server.port,
// username: server.username,
// protocol: "ssh",
// sshOptions: {
// privateKey: keys.privateKey,
// },
// });
// const results = await docker2.listContainers();
// console.log(results);
const settings: CreateServiceOptions = {
authconfig: authConfig,

View File

@ -1,6 +1,5 @@
import type { Mariadb } from "@/server/api/services/mariadb";
import type { Mount } from "@/server/api/services/mount";
import { docker } from "@/server/constants";
import type { CreateServiceOptions } from "dockerode";
import {
calculateResources,
@ -9,6 +8,7 @@ import {
generateVolumeMounts,
prepareEnvironmentVariables,
} from "../docker/utils";
import { getRemoteDocker } from "../servers/remote-docker";
type MariadbWithMounts = Mariadb & {
mounts: Mount[];
@ -45,6 +45,8 @@ export const buildMariadb = async (mariadb: MariadbWithMounts) => {
const bindsMount = generateBindMounts(mounts);
const filesMount = generateFileMounts(appName, mounts);
const docker = await getRemoteDocker(mariadb.serverId);
const settings: CreateServiceOptions = {
Name: appName,
TaskTemplate: {

View File

@ -1,7 +1,5 @@
import type { Mongo } from "@/server/api/services/mongo";
import type { Mount } from "@/server/api/services/mount";
import type { Postgres } from "@/server/api/services/postgres";
import { docker } from "@/server/constants";
import type { CreateServiceOptions } from "dockerode";
import {
calculateResources,
@ -10,6 +8,7 @@ import {
generateVolumeMounts,
prepareEnvironmentVariables,
} from "../docker/utils";
import { getRemoteDocker } from "../servers/remote-docker";
type MongoWithMounts = Mongo & {
mounts: Mount[];
@ -45,6 +44,8 @@ export const buildMongo = async (mongo: MongoWithMounts) => {
const bindsMount = generateBindMounts(mounts);
const filesMount = generateFileMounts(appName, mounts);
const docker = await getRemoteDocker(mongo.serverId);
const settings: CreateServiceOptions = {
Name: appName,
TaskTemplate: {

View File

@ -9,6 +9,7 @@ import {
generateVolumeMounts,
prepareEnvironmentVariables,
} from "../docker/utils";
import { getRemoteDocker } from "../servers/remote-docker";
type MysqlWithMounts = MySql & {
mounts: Mount[];
@ -51,6 +52,8 @@ export const buildMysql = async (mysql: MysqlWithMounts) => {
const bindsMount = generateBindMounts(mounts);
const filesMount = generateFileMounts(appName, mounts);
const docker = await getRemoteDocker(mysql.serverId);
const settings: CreateServiceOptions = {
Name: appName,
TaskTemplate: {

View File

@ -1,6 +1,5 @@
import type { Mount } from "@/server/api/services/mount";
import type { Postgres } from "@/server/api/services/postgres";
import { docker } from "@/server/constants";
import type { CreateServiceOptions } from "dockerode";
import {
calculateResources,
@ -9,6 +8,7 @@ import {
generateVolumeMounts,
prepareEnvironmentVariables,
} from "../docker/utils";
import { getRemoteDocker } from "../servers/remote-docker";
type PostgresWithMounts = Postgres & {
mounts: Mount[];
@ -45,6 +45,8 @@ export const buildPostgres = async (postgres: PostgresWithMounts) => {
const bindsMount = generateBindMounts(mounts);
const filesMount = generateFileMounts(appName, mounts);
const docker = await getRemoteDocker(postgres.serverId);
const settings: CreateServiceOptions = {
Name: appName,
TaskTemplate: {

View File

@ -9,6 +9,7 @@ import {
generateVolumeMounts,
prepareEnvironmentVariables,
} from "../docker/utils";
import { getRemoteDocker } from "../servers/remote-docker";
type RedisWithMounts = Redis & {
mounts: Mount[];
@ -43,6 +44,8 @@ export const buildRedis = async (redis: RedisWithMounts) => {
const bindsMount = generateBindMounts(mounts);
const filesMount = generateFileMounts(appName, mounts);
const docker = await getRemoteDocker(redis.serverId);
const settings: CreateServiceOptions = {
Name: appName,
TaskTemplate: {