Merge branch 'canary' into 57-dokploy-api-or-cli

This commit is contained in:
Mauricio Siu
2024-06-12 00:23:53 -06:00
59 changed files with 763 additions and 448 deletions

View File

@@ -66,9 +66,10 @@ export const applicationRouter = createTRPCRouter({
if (ctx.user.rol === "user") {
await addNewService(ctx.user.authId, newApplication.applicationId);
}
return newApplication;
} catch (error) {
} catch (error: unknown) {
if (error instanceof TRPCError) {
throw error;
}
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error to create the application",

View File

@@ -34,13 +34,18 @@ import { nanoid } from "nanoid";
import { removeDeploymentsByComposeId } from "../services/deployment";
import { removeComposeDirectory } from "@/server/utils/filesystem/directory";
import { createCommand } from "@/server/utils/builders/compose";
import { loadTemplateModule, readComposeFile } from "@/templates/utils";
import {
generatePassword,
loadTemplateModule,
readComposeFile,
} from "@/templates/utils";
import { findAdmin } from "../services/admin";
import { TRPCError } from "@trpc/server";
import { findProjectById, slugifyProjectName } from "../services/project";
import { findProjectById } from "../services/project";
import { createMount } from "../services/mount";
import type { TemplatesKeys } from "@/templates/types/templates-data.type";
import { templates } from "@/templates/templates";
import { slugify } from "@/lib/slug";
export const composeRouter = createTRPCRouter({
create: protectedProcedure
@@ -229,7 +234,7 @@ export const composeRouter = createTRPCRouter({
const project = await findProjectById(input.projectId);
const projectName = slugifyProjectName(`${project.name}-${input.id}`);
const projectName = slugify(`${project.name} ${input.id}`);
const { envs, mounts } = generate({
serverIp: admin.serverIp,
projectName: projectName,
@@ -241,6 +246,7 @@ export const composeRouter = createTRPCRouter({
env: envs.join("\n"),
name: input.id,
sourceType: "raw",
appName: `${projectName}-${generatePassword(6)}`,
});
if (ctx.user.rol === "user") {

View File

@@ -49,6 +49,9 @@ export const mariadbRouter = createTRPCRouter({
return true;
} catch (error) {
if (error instanceof TRPCError) {
throw error;
}
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error input: Inserting mariadb database",

View File

@@ -49,6 +49,9 @@ export const mongoRouter = createTRPCRouter({
return true;
} catch (error) {
if (error instanceof TRPCError) {
throw error;
}
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error input: Inserting mongo database",

View File

@@ -50,6 +50,9 @@ export const mysqlRouter = createTRPCRouter({
return true;
} catch (error) {
if (error instanceof TRPCError) {
throw error;
}
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error input: Inserting mysql database",

View File

@@ -49,6 +49,9 @@ export const postgresRouter = createTRPCRouter({
return true;
} catch (error) {
if (error instanceof TRPCError) {
throw error;
}
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error input: Inserting postgresql database",

View File

@@ -17,7 +17,7 @@ import { adminProcedure, createTRPCRouter, protectedProcedure } from "../trpc";
import { TRPCError } from "@trpc/server";
import { manageRegistry } from "@/server/utils/traefik/registry";
import { initializeRegistry } from "@/server/setup/registry-setup";
import { docker } from "@/server/constants";
import { execAsync } from "@/server/utils/process/execAsync";
export const registryRouter = createTRPCRouter({
create: adminProcedure
@@ -57,15 +57,11 @@ export const registryRouter = createTRPCRouter({
.input(apiTestRegistry)
.mutation(async ({ input }) => {
try {
const result = await docker.checkAuth({
username: input.username,
password: input.password,
serveraddress: input.registryUrl,
});
const loginCommand = `echo ${input.password} | docker login ${input.registryUrl} --username ${input.username} --password-stdin`;
await execAsync(loginCommand);
return true;
} catch (error) {
console.log(error);
console.log("Error Registry:", error);
return false;
}
}),

View File

@@ -15,11 +15,23 @@ import { findAdmin } from "./admin";
import { createTraefikConfig } from "@/server/utils/traefik/application";
import { docker } from "@/server/constants";
import { getAdvancedStats } from "@/server/monitoring/utilts";
import { validUniqueServerAppName } from "./project";
export type Application = typeof applications.$inferSelect;
export const createApplication = async (
input: typeof apiCreateApplication._type,
) => {
if (input.appName) {
const valid = await validUniqueServerAppName(input.appName);
if (!valid) {
throw new TRPCError({
code: "CONFLICT",
message: "Application with this 'AppName' already exists",
});
}
}
return await db.transaction(async (tx) => {
const newApplication = await tx
.insert(applications)

View File

@@ -13,10 +13,21 @@ import { join } from "node:path";
import { COMPOSE_PATH } from "@/server/constants";
import { cloneGithubRepository } from "@/server/utils/providers/github";
import { cloneGitRepository } from "@/server/utils/providers/git";
import { validUniqueServerAppName } from "./project";
export type Compose = typeof compose.$inferSelect;
export const createCompose = async (input: typeof apiCreateCompose._type) => {
if (input.appName) {
const valid = await validUniqueServerAppName(input.appName);
if (!valid) {
throw new TRPCError({
code: "CONFLICT",
message: "Service with this 'AppName' already exists",
});
}
}
const newDestination = await db
.insert(compose)
.values({
@@ -39,6 +50,16 @@ export const createCompose = async (input: typeof apiCreateCompose._type) => {
export const createComposeByTemplate = async (
input: typeof compose.$inferInsert,
) => {
if (input.appName) {
const valid = await validUniqueServerAppName(input.appName);
if (!valid) {
throw new TRPCError({
code: "CONFLICT",
message: "Service with this 'AppName' already exists",
});
}
}
const newDestination = await db
.insert(compose)
.values({

View File

@@ -46,9 +46,7 @@ export const getContainers = async () => {
.filter((container) => !container.name.includes("dokploy"));
return containers;
} catch (error) {
console.error(`Execution error: ${error}`);
}
} catch (error) {}
};
export const getConfig = async (containerId: string) => {
@@ -65,9 +63,7 @@ export const getConfig = async (containerId: string) => {
const config = JSON.parse(stdout);
return config;
} catch (error) {
console.error(`Execution error: ${error}`);
}
} catch (error) {}
};
export const getContainersByAppNameMatch = async (appName: string) => {
@@ -103,9 +99,7 @@ export const getContainersByAppNameMatch = async (appName: string) => {
});
return containers || [];
} catch (error) {
console.error(`Execution error: ${error}`);
}
} catch (error) {}
return [];
};
@@ -144,9 +138,7 @@ export const getContainersByAppLabel = async (appName: string) => {
});
return containers || [];
} catch (error) {
console.error(`Execution error: ${error}`);
}
} catch (error) {}
return [];
};

View File

@@ -5,10 +5,22 @@ import { buildMariadb } from "@/server/utils/databases/mariadb";
import { pullImage } from "@/server/utils/docker/utils";
import { TRPCError } from "@trpc/server";
import { eq, getTableColumns } from "drizzle-orm";
import { validUniqueServerAppName } from "./project";
export type Mariadb = typeof mariadb.$inferSelect;
export const createMariadb = async (input: typeof apiCreateMariaDB._type) => {
if (input.appName) {
const valid = await validUniqueServerAppName(input.appName);
if (!valid) {
throw new TRPCError({
code: "CONFLICT",
message: "Service with this 'AppName' already exists",
});
}
}
const newMariadb = await db
.insert(mariadb)
.values({

View File

@@ -5,10 +5,22 @@ import { buildMongo } from "@/server/utils/databases/mongo";
import { pullImage } from "@/server/utils/docker/utils";
import { TRPCError } from "@trpc/server";
import { eq, getTableColumns } from "drizzle-orm";
import { validUniqueServerAppName } from "./project";
export type Mongo = typeof mongo.$inferSelect;
export const createMongo = async (input: typeof apiCreateMongo._type) => {
if (input.appName) {
const valid = await validUniqueServerAppName(input.appName);
if (!valid) {
throw new TRPCError({
code: "CONFLICT",
message: "Service with this 'AppName' already exists",
});
}
}
const newMongo = await db
.insert(mongo)
.values({

View File

@@ -5,11 +5,22 @@ import { buildMysql } from "@/server/utils/databases/mysql";
import { pullImage } from "@/server/utils/docker/utils";
import { TRPCError } from "@trpc/server";
import { eq, getTableColumns } from "drizzle-orm";
import { nanoid } from "nanoid";
import { validUniqueServerAppName } from "./project";
export type MySql = typeof mysql.$inferSelect;
export const createMysql = async (input: typeof apiCreateMySql._type) => {
if (input.appName) {
const valid = await validUniqueServerAppName(input.appName);
if (!valid) {
throw new TRPCError({
code: "CONFLICT",
message: "Service with this 'AppName' already exists",
});
}
}
const newMysql = await db
.insert(mysql)
.values({

View File

@@ -5,10 +5,22 @@ import { buildPostgres } from "@/server/utils/databases/postgres";
import { pullImage } from "@/server/utils/docker/utils";
import { TRPCError } from "@trpc/server";
import { eq, getTableColumns } from "drizzle-orm";
import { validUniqueServerAppName } from "./project";
export type Postgres = typeof postgres.$inferSelect;
export const createPostgres = async (input: typeof apiCreatePostgres._type) => {
if (input.appName) {
const valid = await validUniqueServerAppName(input.appName);
if (!valid) {
throw new TRPCError({
code: "CONFLICT",
message: "Service with this 'AppName' already exists",
});
}
}
const newPostgres = await db
.insert(postgres)
.values({

View File

@@ -1,5 +1,14 @@
import { db } from "@/server/db";
import { type apiCreateProject, projects } from "@/server/db/schema";
import {
type apiCreateProject,
applications,
mariadb,
mongo,
mysql,
postgres,
projects,
redis,
} from "@/server/db/schema";
import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";
import { findAdmin } from "./admin";
@@ -75,12 +84,40 @@ export const updateProjectById = async (
return result;
};
export const slugifyProjectName = (projectName: string): string => {
return projectName
.toLowerCase()
.replace(/[0-9]/g, "")
.replace(/[^a-z\s-]/g, "")
.replace(/\s+/g, "-")
.replace(/-+/g, "-")
.replace(/^-+|-+$/g, "");
export const validUniqueServerAppName = async (appName: string) => {
const query = await db.query.projects.findMany({
with: {
applications: {
where: eq(applications.appName, appName),
},
mariadb: {
where: eq(mariadb.appName, appName),
},
mongo: {
where: eq(mongo.appName, appName),
},
mysql: {
where: eq(mysql.appName, appName),
},
postgres: {
where: eq(postgres.appName, appName),
},
redis: {
where: eq(redis.appName, appName),
},
},
});
// Filter out items with non-empty fields
const nonEmptyProjects = query.filter(
(project) =>
project.applications.length > 0 ||
project.mariadb.length > 0 ||
project.mongo.length > 0 ||
project.mysql.length > 0 ||
project.postgres.length > 0 ||
project.redis.length > 0,
);
return nonEmptyProjects.length === 0;
};

View File

@@ -5,11 +5,23 @@ import { buildRedis } from "@/server/utils/databases/redis";
import { pullImage } from "@/server/utils/docker/utils";
import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";
import { validUniqueServerAppName } from "./project";
export type Redis = typeof redis.$inferSelect;
// https://github.com/drizzle-team/drizzle-orm/discussions/1483#discussioncomment-7523881
export const createRedis = async (input: typeof apiCreateRedis._type) => {
if (input.appName) {
const valid = await validUniqueServerAppName(input.appName);
if (!valid) {
throw new TRPCError({
code: "CONFLICT",
message: "Service with this 'AppName' already exists",
});
}
}
const newRedis = await db
.insert(redis)
.values({

View File

@@ -9,28 +9,35 @@ import {
} from "@/server/utils/traefik/registry";
import { removeService } from "@/server/utils/docker/utils";
import { initializeRegistry } from "@/server/setup/registry-setup";
import { execAsync } from "@/server/utils/process/execAsync";
export type Registry = typeof registry.$inferSelect;
export const createRegistry = async (input: typeof apiCreateRegistry._type) => {
const admin = await findAdmin();
const newRegistry = await db
.insert(registry)
.values({
...input,
adminId: admin.adminId,
})
.returning()
.then((value) => value[0]);
return await db.transaction(async (tx) => {
const newRegistry = await tx
.insert(registry)
.values({
...input,
adminId: admin.adminId,
})
.returning()
.then((value) => value[0]);
if (!newRegistry) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error input: Inserting registry",
});
}
return newRegistry;
if (!newRegistry) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error input: Inserting registry",
});
}
const loginCommand = `echo ${input.password} | docker login ${input.registryUrl} --username ${input.username} --password-stdin`;
await execAsync(loginCommand);
return newRegistry;
});
};
export const removeRegistry = async (registryId: string) => {
@@ -53,6 +60,8 @@ export const removeRegistry = async (registryId: string) => {
await removeService("dokploy-registry");
}
await execAsync(`docker logout ${response.registryUrl}`);
return response;
} catch (error) {
throw new TRPCError({

View File

@@ -20,6 +20,7 @@ import {
} from "drizzle-orm/pg-core";
import { generateAppName } from "./utils";
import { registry } from "./registry";
import { generatePassword } from "@/templates/utils";
export const sourceType = pgEnum("sourceType", ["docker", "git", "github"]);
@@ -307,11 +308,17 @@ const createSchema = createInsertSchema(applications, {
networkSwarm: NetworkSwarmSchema.nullable(),
});
export const apiCreateApplication = createSchema.pick({
name: true,
description: true,
projectId: true,
});
export const apiCreateApplication = createSchema
.pick({
name: true,
appName: true,
description: true,
projectId: true,
})
.transform((data) => ({
...data,
appName: `${data.appName}-${generatePassword(6)}` || generateAppName("app"),
}));
export const apiFindOneApplication = createSchema
.pick({

View File

@@ -8,6 +8,7 @@ import { deployments } from "./deployment";
import { generateAppName } from "./utils";
import { applicationStatus } from "./shared";
import { mounts } from "./mount";
import { generatePassword } from "@/templates/utils";
export const sourceTypeCompose = pgEnum("sourceTypeCompose", [
"git",
@@ -74,12 +75,19 @@ const createSchema = createInsertSchema(compose, {
composeType: z.enum(["docker-compose", "stack"]).optional(),
});
export const apiCreateCompose = createSchema.pick({
name: true,
description: true,
projectId: true,
composeType: true,
});
export const apiCreateCompose = createSchema
.pick({
name: true,
description: true,
projectId: true,
composeType: true,
appName: true,
})
.transform((data) => ({
...data,
appName:
`${data.appName}-${generatePassword(6)}` || generateAppName("compose"),
}));
export const apiCreateComposeByTemplate = createSchema
.pick({

View File

@@ -8,6 +8,7 @@ import { projects } from "./project";
import { backups } from "./backups";
import { mounts } from "./mount";
import { generateAppName } from "./utils";
import { generatePassword } from "@/templates/utils";
export const mariadb = pgTable("mariadb", {
mariadbId: text("mariadbId")
@@ -79,6 +80,7 @@ const createSchema = createInsertSchema(mariadb, {
export const apiCreateMariaDB = createSchema
.pick({
name: true,
appName: true,
dockerImage: true,
databaseRootPassword: true,
projectId: true,
@@ -87,7 +89,12 @@ export const apiCreateMariaDB = createSchema
databaseUser: true,
databasePassword: true,
})
.required();
.required()
.transform((data) => ({
...data,
appName:
`${data.appName}-${generatePassword(6)}` || generateAppName("mariadb"),
}));
export const apiFindOneMariaDB = createSchema
.pick({

View File

@@ -8,6 +8,7 @@ import { projects } from "./project";
import { backups } from "./backups";
import { mounts } from "./mount";
import { generateAppName } from "./utils";
import { generatePassword } from "@/templates/utils";
export const mongo = pgTable("mongo", {
mongoId: text("mongoId")
@@ -73,13 +74,19 @@ const createSchema = createInsertSchema(mongo, {
export const apiCreateMongo = createSchema
.pick({
name: true,
appName: true,
dockerImage: true,
projectId: true,
description: true,
databaseUser: true,
databasePassword: true,
})
.required();
.required()
.transform((data) => ({
...data,
appName:
`${data.appName}-${generatePassword(6)}` || generateAppName("postgres"),
}));
export const apiFindOneMongo = createSchema
.pick({

View File

@@ -8,6 +8,7 @@ import { projects } from "./project";
import { backups } from "./backups";
import { mounts } from "./mount";
import { generateAppName } from "./utils";
import { generatePassword } from "@/templates/utils";
export const mysql = pgTable("mysql", {
mysqlId: text("mysqlId")
@@ -77,6 +78,7 @@ const createSchema = createInsertSchema(mysql, {
export const apiCreateMySql = createSchema
.pick({
name: true,
appName: true,
dockerImage: true,
projectId: true,
description: true,
@@ -85,7 +87,12 @@ export const apiCreateMySql = createSchema
databasePassword: true,
databaseRootPassword: true,
})
.required();
.required()
.transform((data) => ({
...data,
appName:
`${data.appName}-${generatePassword(6)}` || generateAppName("mysql"),
}));
export const apiFindOneMySql = createSchema
.pick({

View File

@@ -8,6 +8,7 @@ import { projects } from "./project";
import { backups } from "./backups";
import { mounts } from "./mount";
import { generateAppName } from "./utils";
import { generatePassword } from "@/templates/utils";
export const postgres = pgTable("postgres", {
postgresId: text("postgresId")
@@ -74,6 +75,7 @@ const createSchema = createInsertSchema(postgres, {
export const apiCreatePostgres = createSchema
.pick({
name: true,
appName: true,
databaseName: true,
databaseUser: true,
databasePassword: true,
@@ -81,7 +83,12 @@ export const apiCreatePostgres = createSchema
projectId: true,
description: true,
})
.required();
.required()
.transform((data) => ({
...data,
appName:
`${data.appName}-${generatePassword(6)}` || generateAppName("postgres"),
}));
export const apiFindOnePostgres = createSchema
.pick({

View File

@@ -7,6 +7,7 @@ import { integer, pgTable, text } from "drizzle-orm/pg-core";
import { projects } from "./project";
import { mounts } from "./mount";
import { generateAppName } from "./utils";
import { generatePassword } from "@/templates/utils";
export const redis = pgTable("redis", {
redisId: text("redisId")
@@ -69,13 +70,18 @@ const createSchema = createInsertSchema(redis, {
export const apiCreateRedis = createSchema
.pick({
name: true,
appName: true,
databasePassword: true,
dockerImage: true,
projectId: true,
description: true,
})
.required();
.required()
.transform((data) => ({
...data,
appName:
`${data.appName}-${generatePassword(6)}` || generateAppName("redis"),
}));
export const apiFindOneRedis = createSchema
.pick({

View File

@@ -7,6 +7,10 @@ import type { MainTraefikConfig } from "../utils/traefik/types";
import type { FileConfig } from "../utils/traefik/file-types";
import type { CreateServiceOptions } from "dockerode";
const TRAEFIK_SSL_PORT =
Number.parseInt(process.env.TRAEFIK_SSL_PORT ?? "", 10) || 443;
const TRAEFIK_PORT = Number.parseInt(process.env.TRAEFIK_PORT ?? "", 10) || 80;
export const initializeTraefik = async () => {
const imageName = "traefik:v2.5";
const containerName = "dokploy-traefik";
@@ -47,12 +51,12 @@ export const initializeTraefik = async () => {
Ports: [
{
TargetPort: 443,
PublishedPort: 443,
PublishedPort: TRAEFIK_SSL_PORT,
PublishMode: "host",
},
{
TargetPort: 80,
PublishedPort: 80,
PublishedPort: TRAEFIK_PORT,
PublishMode: "host",
},
{
@@ -146,10 +150,10 @@ export const createDefaultTraefikConfig = () => {
},
entryPoints: {
web: {
address: ":80",
address: `:${TRAEFIK_PORT}`,
},
websecure: {
address: ":443",
address: `:${TRAEFIK_SSL_PORT}`,
...(process.env.NODE_ENV === "production" && {
http: {
tls: {

View File

@@ -108,7 +108,12 @@ const createEnvFile = (compose: ComposeNested) => {
join(COMPOSE_PATH, appName, "docker-compose.yml");
const envFilePath = join(dirname(composeFilePath), ".env");
const envFileContent = prepareEnvironmentVariables(env).join("\n");
let envContent = env || "";
if (!envContent.includes("DOCKER_CONFIG")) {
envContent += "\nDOCKER_CONFIG=/root/.docker/config.json";
}
const envFileContent = prepareEnvironmentVariables(envContent).join("\n");
if (!existsSync(dirname(envFilePath))) {
mkdirSync(dirname(envFilePath), { recursive: true });

View File

@@ -148,7 +148,6 @@ export const mechanizeDockerContainer = async (
},
});
} catch (error) {
console.log(error);
await docker.createService(settings);
}
};

View File

@@ -3,47 +3,47 @@ import { type ApplicationNested, mechanizeDockerContainer } from "../builders";
import { pullImage } from "../docker/utils";
interface RegistryAuth {
username: string;
password: string;
serveraddress: string;
username: string;
password: string;
serveraddress: string;
}
export const buildDocker = async (
application: ApplicationNested,
logPath: string,
application: ApplicationNested,
logPath: string,
): Promise<void> => {
const { buildType, dockerImage, username, password } = application;
const authConfig: Partial<RegistryAuth> = {
username: username || "",
password: password || "",
};
const { buildType, dockerImage, username, password } = application;
const authConfig: Partial<RegistryAuth> = {
username: username || "",
password: password || "",
};
const writeStream = createWriteStream(logPath, { flags: "a" });
const writeStream = createWriteStream(logPath, { flags: "a" });
writeStream.write(`\nBuild ${buildType}\n`);
writeStream.write(`\nBuild ${buildType}\n`);
writeStream.write(`Pulling ${dockerImage}: ✅\n`);
writeStream.write(`Pulling ${dockerImage}: ✅\n`);
try {
if (!dockerImage) {
throw new Error("Docker image not found");
}
try {
if (!dockerImage) {
throw new Error("Docker image not found");
}
await pullImage(
dockerImage,
(data) => {
if (writeStream.writable) {
writeStream.write(`${data.status}\n`);
}
},
authConfig,
);
await mechanizeDockerContainer(application);
writeStream.write("\nDocker Deployed: ✅\n");
} catch (error) {
writeStream.write(`ERROR: ${error}: ❌`);
throw error;
} finally {
writeStream.end();
}
await pullImage(
dockerImage,
(data) => {
if (writeStream.writable) {
writeStream.write(`${data.status}\n`);
}
},
authConfig,
);
await mechanizeDockerContainer(application);
writeStream.write("\nDocker Deployed: ✅\n");
} catch (error) {
writeStream.write(`ERROR: ${error}: ❌`);
throw error;
} finally {
writeStream.end();
}
};