mirror of
https://github.com/andwati/dokploy.git
synced 2026-11-03 18:52:17 +00:00
[autofix.ci] apply automated fixes
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
import type { ComposeSpecification } from "@dokploy/server";
|
import type { ComposeSpecification } from "@dokploy/server";
|
||||||
import {
|
import {
|
||||||
addSuffixToServiceNetworks,
|
addSuffixToServiceNetworks,
|
||||||
generateRandomHash,
|
generateRandomHash,
|
||||||
} from "@dokploy/server";
|
} from "@dokploy/server";
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
import { parse } from "yaml";
|
import { parse } from "yaml";
|
||||||
@@ -23,30 +23,30 @@ services:
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
test("Add suffix to networks in services", () => {
|
test("Add suffix to networks in services", () => {
|
||||||
const composeData = parse(composeFile) as ComposeSpecification;
|
const composeData = parse(composeFile) as ComposeSpecification;
|
||||||
|
|
||||||
const suffix = generateRandomHash();
|
const suffix = generateRandomHash();
|
||||||
|
|
||||||
if (!composeData?.services) {
|
if (!composeData?.services) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const services = addSuffixToServiceNetworks(composeData.services, suffix);
|
const services = addSuffixToServiceNetworks(composeData.services, suffix);
|
||||||
const actualComposeData = { ...composeData, services };
|
const actualComposeData = { ...composeData, services };
|
||||||
|
|
||||||
expect(actualComposeData?.services?.web?.networks).toContain(
|
expect(actualComposeData?.services?.web?.networks).toContain(
|
||||||
`frontend-${suffix}`,
|
`frontend-${suffix}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(actualComposeData?.services?.api?.networks).toContain(
|
expect(actualComposeData?.services?.api?.networks).toContain(
|
||||||
`backend-${suffix}`,
|
`backend-${suffix}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
const apiNetworks = actualComposeData?.services?.api?.networks;
|
const apiNetworks = actualComposeData?.services?.api?.networks;
|
||||||
|
|
||||||
expect(apiNetworks).toBeDefined();
|
expect(apiNetworks).toBeDefined();
|
||||||
expect(actualComposeData?.services?.api?.networks).toContain(
|
expect(actualComposeData?.services?.api?.networks).toContain(
|
||||||
`backend-${suffix}`,
|
`backend-${suffix}`,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Caso 2: Objeto con aliases
|
// Caso 2: Objeto con aliases
|
||||||
@@ -67,29 +67,29 @@ networks:
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
test("Add suffix to networks in services with aliases", () => {
|
test("Add suffix to networks in services with aliases", () => {
|
||||||
const composeData = parse(composeFile2) as ComposeSpecification;
|
const composeData = parse(composeFile2) as ComposeSpecification;
|
||||||
|
|
||||||
const suffix = generateRandomHash();
|
const suffix = generateRandomHash();
|
||||||
|
|
||||||
if (!composeData?.services) {
|
if (!composeData?.services) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const services = addSuffixToServiceNetworks(composeData.services, suffix);
|
const services = addSuffixToServiceNetworks(composeData.services, suffix);
|
||||||
const actualComposeData = { ...composeData, services };
|
const actualComposeData = { ...composeData, services };
|
||||||
|
|
||||||
expect(actualComposeData.services?.api?.networks).toHaveProperty(
|
expect(actualComposeData.services?.api?.networks).toHaveProperty(
|
||||||
`frontend-${suffix}`,
|
`frontend-${suffix}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
const networkConfig = actualComposeData?.services?.api?.networks as {
|
const networkConfig = actualComposeData?.services?.api?.networks as {
|
||||||
[key: string]: { aliases?: string[] };
|
[key: string]: { aliases?: string[] };
|
||||||
};
|
};
|
||||||
expect(networkConfig[`frontend-${suffix}`]).toBeDefined();
|
expect(networkConfig[`frontend-${suffix}`]).toBeDefined();
|
||||||
expect(networkConfig[`frontend-${suffix}`]?.aliases).toContain("api");
|
expect(networkConfig[`frontend-${suffix}`]?.aliases).toContain("api");
|
||||||
|
|
||||||
expect(actualComposeData.services?.api?.networks).not.toHaveProperty(
|
expect(actualComposeData.services?.api?.networks).not.toHaveProperty(
|
||||||
"frontend-ash",
|
"frontend-ash",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const composeFile3 = `
|
const composeFile3 = `
|
||||||
@@ -107,19 +107,19 @@ networks:
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
test("Add suffix to networks in services (Object with simple networks)", () => {
|
test("Add suffix to networks in services (Object with simple networks)", () => {
|
||||||
const composeData = parse(composeFile3) as ComposeSpecification;
|
const composeData = parse(composeFile3) as ComposeSpecification;
|
||||||
|
|
||||||
const suffix = generateRandomHash();
|
const suffix = generateRandomHash();
|
||||||
|
|
||||||
if (!composeData?.services) {
|
if (!composeData?.services) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const services = addSuffixToServiceNetworks(composeData.services, suffix);
|
const services = addSuffixToServiceNetworks(composeData.services, suffix);
|
||||||
const actualComposeData = { ...composeData, services };
|
const actualComposeData = { ...composeData, services };
|
||||||
|
|
||||||
expect(actualComposeData.services?.redis?.networks).toHaveProperty(
|
expect(actualComposeData.services?.redis?.networks).toHaveProperty(
|
||||||
`backend-${suffix}`,
|
`backend-${suffix}`,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const composeFileCombined = `
|
const composeFileCombined = `
|
||||||
@@ -153,36 +153,36 @@ networks:
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
test("Add suffix to networks in services (combined case)", () => {
|
test("Add suffix to networks in services (combined case)", () => {
|
||||||
const composeData = parse(composeFileCombined) as ComposeSpecification;
|
const composeData = parse(composeFileCombined) as ComposeSpecification;
|
||||||
|
|
||||||
const suffix = generateRandomHash();
|
const suffix = generateRandomHash();
|
||||||
|
|
||||||
if (!composeData?.services) {
|
if (!composeData?.services) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const services = addSuffixToServiceNetworks(composeData.services, suffix);
|
const services = addSuffixToServiceNetworks(composeData.services, suffix);
|
||||||
const actualComposeData = { ...composeData, services };
|
const actualComposeData = { ...composeData, services };
|
||||||
|
|
||||||
// Caso 1: ListOfStrings
|
// Caso 1: ListOfStrings
|
||||||
expect(actualComposeData.services?.web?.networks).toContain(
|
expect(actualComposeData.services?.web?.networks).toContain(
|
||||||
`frontend-${suffix}`,
|
`frontend-${suffix}`,
|
||||||
);
|
);
|
||||||
expect(actualComposeData.services?.web?.networks).toContain(
|
expect(actualComposeData.services?.web?.networks).toContain(
|
||||||
`backend-${suffix}`,
|
`backend-${suffix}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Caso 2: Objeto con aliases
|
// Caso 2: Objeto con aliases
|
||||||
const apiNetworks = actualComposeData.services?.api?.networks as {
|
const apiNetworks = actualComposeData.services?.api?.networks as {
|
||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
};
|
};
|
||||||
expect(apiNetworks).toHaveProperty(`frontend-${suffix}`);
|
expect(apiNetworks).toHaveProperty(`frontend-${suffix}`);
|
||||||
expect(apiNetworks[`frontend-${suffix}`]).toBeDefined();
|
expect(apiNetworks[`frontend-${suffix}`]).toBeDefined();
|
||||||
expect(apiNetworks).not.toHaveProperty("frontend");
|
expect(apiNetworks).not.toHaveProperty("frontend");
|
||||||
|
|
||||||
// Caso 3: Objeto con redes simples
|
// Caso 3: Objeto con redes simples
|
||||||
const redisNetworks = actualComposeData.services?.redis?.networks;
|
const redisNetworks = actualComposeData.services?.redis?.networks;
|
||||||
expect(redisNetworks).toHaveProperty(`backend-${suffix}`);
|
expect(redisNetworks).toHaveProperty(`backend-${suffix}`);
|
||||||
expect(redisNetworks).not.toHaveProperty("backend");
|
expect(redisNetworks).not.toHaveProperty("backend");
|
||||||
});
|
});
|
||||||
|
|
||||||
const composeFile7 = `
|
const composeFile7 = `
|
||||||
@@ -196,18 +196,18 @@ services:
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
test("It shouldn't add suffix to dokploy-network in services", () => {
|
test("It shouldn't add suffix to dokploy-network in services", () => {
|
||||||
const composeData = parse(composeFile7) as ComposeSpecification;
|
const composeData = parse(composeFile7) as ComposeSpecification;
|
||||||
|
|
||||||
const suffix = generateRandomHash();
|
const suffix = generateRandomHash();
|
||||||
|
|
||||||
if (!composeData?.services) {
|
if (!composeData?.services) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const networks = addSuffixToServiceNetworks(composeData.services, suffix);
|
const networks = addSuffixToServiceNetworks(composeData.services, suffix);
|
||||||
const service = networks.web;
|
const service = networks.web;
|
||||||
|
|
||||||
expect(service).toBeDefined();
|
expect(service).toBeDefined();
|
||||||
expect(service?.networks).toContain("dokploy-network");
|
expect(service?.networks).toContain("dokploy-network");
|
||||||
});
|
});
|
||||||
|
|
||||||
const composeFile8 = `
|
const composeFile8 = `
|
||||||
@@ -245,31 +245,31 @@ services:
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
test("It shouldn't add suffix to dokploy-network in services multiples cases", () => {
|
test("It shouldn't add suffix to dokploy-network in services multiples cases", () => {
|
||||||
const composeData = parse(composeFile8) as ComposeSpecification;
|
const composeData = parse(composeFile8) as ComposeSpecification;
|
||||||
|
|
||||||
const suffix = generateRandomHash();
|
const suffix = generateRandomHash();
|
||||||
|
|
||||||
if (!composeData?.services) {
|
if (!composeData?.services) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const networks = addSuffixToServiceNetworks(composeData.services, suffix);
|
const networks = addSuffixToServiceNetworks(composeData.services, suffix);
|
||||||
const service = networks.web;
|
const service = networks.web;
|
||||||
const api = networks.api;
|
const api = networks.api;
|
||||||
const redis = networks.redis;
|
const redis = networks.redis;
|
||||||
const db = networks.db;
|
const db = networks.db;
|
||||||
|
|
||||||
const dbNetworks = db?.networks as {
|
const dbNetworks = db?.networks as {
|
||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
};
|
};
|
||||||
|
|
||||||
const apiNetworks = api?.networks as {
|
const apiNetworks = api?.networks as {
|
||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(service).toBeDefined();
|
expect(service).toBeDefined();
|
||||||
expect(service?.networks).toContain("dokploy-network");
|
expect(service?.networks).toContain("dokploy-network");
|
||||||
|
|
||||||
expect(redis?.networks).toHaveProperty("dokploy-network");
|
expect(redis?.networks).toHaveProperty("dokploy-network");
|
||||||
expect(dbNetworks["dokploy-network"]).toBeDefined();
|
expect(dbNetworks["dokploy-network"]).toBeDefined();
|
||||||
expect(apiNetworks["dokploy-network"]).toBeDefined();
|
expect(apiNetworks["dokploy-network"]).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|||||||
+97
-97
@@ -16,28 +16,28 @@ SECRET_KEY=env-secret-123
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
describe("getEnvironmentVariablesObject with environment variables (Stack compose)", () => {
|
describe("getEnvironmentVariablesObject with environment variables (Stack compose)", () => {
|
||||||
it("resolves environment variables correctly for Stack compose", () => {
|
it("resolves environment variables correctly for Stack compose", () => {
|
||||||
const serviceEnv = `
|
const serviceEnv = `
|
||||||
FOO=\${{environment.NODE_ENV}}
|
FOO=\${{environment.NODE_ENV}}
|
||||||
BAR=\${{environment.API_URL}}
|
BAR=\${{environment.API_URL}}
|
||||||
BAZ=test
|
BAZ=test
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const result = getEnvironmentVariablesObject(
|
const result = getEnvironmentVariablesObject(
|
||||||
serviceEnv,
|
serviceEnv,
|
||||||
projectEnv,
|
projectEnv,
|
||||||
environmentEnv,
|
environmentEnv,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
FOO: "development",
|
FOO: "development",
|
||||||
BAR: "https://api.dev.example.com",
|
BAR: "https://api.dev.example.com",
|
||||||
BAZ: "test",
|
BAZ: "test",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("resolves both project and environment variables for Stack compose", () => {
|
it("resolves both project and environment variables for Stack compose", () => {
|
||||||
const serviceEnv = `
|
const serviceEnv = `
|
||||||
ENVIRONMENT=\${{project.ENVIRONMENT}}
|
ENVIRONMENT=\${{project.ENVIRONMENT}}
|
||||||
NODE_ENV=\${{environment.NODE_ENV}}
|
NODE_ENV=\${{environment.NODE_ENV}}
|
||||||
API_URL=\${{environment.API_URL}}
|
API_URL=\${{environment.API_URL}}
|
||||||
@@ -45,140 +45,140 @@ DATABASE_URL=\${{project.DATABASE_URL}}
|
|||||||
SERVICE_PORT=4000
|
SERVICE_PORT=4000
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const result = getEnvironmentVariablesObject(
|
const result = getEnvironmentVariablesObject(
|
||||||
serviceEnv,
|
serviceEnv,
|
||||||
projectEnv,
|
projectEnv,
|
||||||
environmentEnv,
|
environmentEnv,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
ENVIRONMENT: "staging",
|
ENVIRONMENT: "staging",
|
||||||
NODE_ENV: "development",
|
NODE_ENV: "development",
|
||||||
API_URL: "https://api.dev.example.com",
|
API_URL: "https://api.dev.example.com",
|
||||||
DATABASE_URL: "postgres://postgres:postgres@localhost:5432/project_db",
|
DATABASE_URL: "postgres://postgres:postgres@localhost:5432/project_db",
|
||||||
SERVICE_PORT: "4000",
|
SERVICE_PORT: "4000",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("handles multiple environment references in single value for Stack compose", () => {
|
it("handles multiple environment references in single value for Stack compose", () => {
|
||||||
const multiRefEnv = `
|
const multiRefEnv = `
|
||||||
HOST=localhost
|
HOST=localhost
|
||||||
PORT=5432
|
PORT=5432
|
||||||
USERNAME=postgres
|
USERNAME=postgres
|
||||||
PASSWORD=secret123
|
PASSWORD=secret123
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const serviceEnv = `
|
const serviceEnv = `
|
||||||
DATABASE_URL=postgresql://\${{environment.USERNAME}}:\${{environment.PASSWORD}}@\${{environment.HOST}}:\${{environment.PORT}}/mydb
|
DATABASE_URL=postgresql://\${{environment.USERNAME}}:\${{environment.PASSWORD}}@\${{environment.HOST}}:\${{environment.PORT}}/mydb
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const result = getEnvironmentVariablesObject(serviceEnv, "", multiRefEnv);
|
const result = getEnvironmentVariablesObject(serviceEnv, "", multiRefEnv);
|
||||||
|
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
DATABASE_URL: "postgresql://postgres:secret123@localhost:5432/mydb",
|
DATABASE_URL: "postgresql://postgres:secret123@localhost:5432/mydb",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("throws error for undefined environment variables in Stack compose", () => {
|
it("throws error for undefined environment variables in Stack compose", () => {
|
||||||
const serviceWithUndefined = `
|
const serviceWithUndefined = `
|
||||||
UNDEFINED_VAR=\${{environment.UNDEFINED_VAR}}
|
UNDEFINED_VAR=\${{environment.UNDEFINED_VAR}}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
expect(() =>
|
expect(() =>
|
||||||
getEnvironmentVariablesObject(serviceWithUndefined, "", environmentEnv),
|
getEnvironmentVariablesObject(serviceWithUndefined, "", environmentEnv),
|
||||||
).toThrow("Invalid environment variable: environment.UNDEFINED_VAR");
|
).toThrow("Invalid environment variable: environment.UNDEFINED_VAR");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("allows service variables to override environment variables in Stack compose", () => {
|
it("allows service variables to override environment variables in Stack compose", () => {
|
||||||
const serviceOverrideEnv = `
|
const serviceOverrideEnv = `
|
||||||
NODE_ENV=production
|
NODE_ENV=production
|
||||||
API_URL=\${{environment.API_URL}}
|
API_URL=\${{environment.API_URL}}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const result = getEnvironmentVariablesObject(
|
const result = getEnvironmentVariablesObject(
|
||||||
serviceOverrideEnv,
|
serviceOverrideEnv,
|
||||||
"",
|
"",
|
||||||
environmentEnv,
|
environmentEnv,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
NODE_ENV: "production",
|
NODE_ENV: "production",
|
||||||
API_URL: "https://api.dev.example.com",
|
API_URL: "https://api.dev.example.com",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("resolves complex references with project, environment, and service variables for Stack compose", () => {
|
it("resolves complex references with project, environment, and service variables for Stack compose", () => {
|
||||||
const complexServiceEnv = `
|
const complexServiceEnv = `
|
||||||
FULL_DATABASE_URL=\${{project.DATABASE_URL}}/\${{environment.DATABASE_NAME}}
|
FULL_DATABASE_URL=\${{project.DATABASE_URL}}/\${{environment.DATABASE_NAME}}
|
||||||
API_ENDPOINT=\${{environment.API_URL}}/\${{project.ENVIRONMENT}}/api
|
API_ENDPOINT=\${{environment.API_URL}}/\${{project.ENVIRONMENT}}/api
|
||||||
SERVICE_NAME=my-service
|
SERVICE_NAME=my-service
|
||||||
COMPLEX_VAR=\${{SERVICE_NAME}}-\${{environment.NODE_ENV}}-\${{project.ENVIRONMENT}}
|
COMPLEX_VAR=\${{SERVICE_NAME}}-\${{environment.NODE_ENV}}-\${{project.ENVIRONMENT}}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const result = getEnvironmentVariablesObject(
|
const result = getEnvironmentVariablesObject(
|
||||||
complexServiceEnv,
|
complexServiceEnv,
|
||||||
projectEnv,
|
projectEnv,
|
||||||
environmentEnv,
|
environmentEnv,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
FULL_DATABASE_URL:
|
FULL_DATABASE_URL:
|
||||||
"postgres://postgres:postgres@localhost:5432/project_db/dev_database",
|
"postgres://postgres:postgres@localhost:5432/project_db/dev_database",
|
||||||
API_ENDPOINT: "https://api.dev.example.com/staging/api",
|
API_ENDPOINT: "https://api.dev.example.com/staging/api",
|
||||||
SERVICE_NAME: "my-service",
|
SERVICE_NAME: "my-service",
|
||||||
COMPLEX_VAR: "my-service-development-staging",
|
COMPLEX_VAR: "my-service-development-staging",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("maintains precedence: service > environment > project in Stack compose", () => {
|
it("maintains precedence: service > environment > project in Stack compose", () => {
|
||||||
const conflictingProjectEnv = `
|
const conflictingProjectEnv = `
|
||||||
NODE_ENV=production-project
|
NODE_ENV=production-project
|
||||||
API_URL=https://project.api.com
|
API_URL=https://project.api.com
|
||||||
DATABASE_NAME=project_db
|
DATABASE_NAME=project_db
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const conflictingEnvironmentEnv = `
|
const conflictingEnvironmentEnv = `
|
||||||
NODE_ENV=development-environment
|
NODE_ENV=development-environment
|
||||||
API_URL=https://environment.api.com
|
API_URL=https://environment.api.com
|
||||||
DATABASE_NAME=env_db
|
DATABASE_NAME=env_db
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const serviceWithConflicts = `
|
const serviceWithConflicts = `
|
||||||
NODE_ENV=service-override
|
NODE_ENV=service-override
|
||||||
PROJECT_ENV=\${{project.NODE_ENV}}
|
PROJECT_ENV=\${{project.NODE_ENV}}
|
||||||
ENV_VAR=\${{environment.API_URL}}
|
ENV_VAR=\${{environment.API_URL}}
|
||||||
DB_NAME=\${{environment.DATABASE_NAME}}
|
DB_NAME=\${{environment.DATABASE_NAME}}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const result = getEnvironmentVariablesObject(
|
const result = getEnvironmentVariablesObject(
|
||||||
serviceWithConflicts,
|
serviceWithConflicts,
|
||||||
conflictingProjectEnv,
|
conflictingProjectEnv,
|
||||||
conflictingEnvironmentEnv,
|
conflictingEnvironmentEnv,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
NODE_ENV: "service-override",
|
NODE_ENV: "service-override",
|
||||||
PROJECT_ENV: "production-project",
|
PROJECT_ENV: "production-project",
|
||||||
ENV_VAR: "https://environment.api.com",
|
ENV_VAR: "https://environment.api.com",
|
||||||
DB_NAME: "env_db",
|
DB_NAME: "env_db",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("handles empty environment variables in Stack compose", () => {
|
it("handles empty environment variables in Stack compose", () => {
|
||||||
const serviceWithEmpty = `
|
const serviceWithEmpty = `
|
||||||
SERVICE_VAR=test
|
SERVICE_VAR=test
|
||||||
PROJECT_VAR=\${{project.ENVIRONMENT}}
|
PROJECT_VAR=\${{project.ENVIRONMENT}}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const result = getEnvironmentVariablesObject(
|
const result = getEnvironmentVariablesObject(
|
||||||
serviceWithEmpty,
|
serviceWithEmpty,
|
||||||
projectEnv,
|
projectEnv,
|
||||||
"",
|
"",
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
SERVICE_VAR: "test",
|
SERVICE_VAR: "test",
|
||||||
PROJECT_VAR: "staging",
|
PROJECT_VAR: "staging",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,452 +7,452 @@ import { auth } from "../lib/auth";
|
|||||||
export type User = typeof user.$inferSelect;
|
export type User = typeof user.$inferSelect;
|
||||||
|
|
||||||
export const addNewProject = async (
|
export const addNewProject = async (
|
||||||
userId: string,
|
userId: string,
|
||||||
projectId: string,
|
projectId: string,
|
||||||
organizationId: string,
|
organizationId: string,
|
||||||
) => {
|
) => {
|
||||||
const userR = await findMemberById(userId, organizationId);
|
const userR = await findMemberById(userId, organizationId);
|
||||||
|
|
||||||
await db
|
await db
|
||||||
.update(member)
|
.update(member)
|
||||||
.set({
|
.set({
|
||||||
accessedProjects: [...userR.accessedProjects, projectId],
|
accessedProjects: [...userR.accessedProjects, projectId],
|
||||||
})
|
})
|
||||||
.where(
|
.where(
|
||||||
and(eq(member.id, userR.id), eq(member.organizationId, organizationId)),
|
and(eq(member.id, userR.id), eq(member.organizationId, organizationId)),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const addNewEnvironment = async (
|
export const addNewEnvironment = async (
|
||||||
userId: string,
|
userId: string,
|
||||||
environmentId: string,
|
environmentId: string,
|
||||||
organizationId: string,
|
organizationId: string,
|
||||||
) => {
|
) => {
|
||||||
const userR = await findMemberById(userId, organizationId);
|
const userR = await findMemberById(userId, organizationId);
|
||||||
|
|
||||||
await db
|
await db
|
||||||
.update(member)
|
.update(member)
|
||||||
.set({
|
.set({
|
||||||
accessedEnvironments: [...userR.accessedEnvironments, environmentId],
|
accessedEnvironments: [...userR.accessedEnvironments, environmentId],
|
||||||
})
|
})
|
||||||
.where(
|
.where(
|
||||||
and(eq(member.id, userR.id), eq(member.organizationId, organizationId)),
|
and(eq(member.id, userR.id), eq(member.organizationId, organizationId)),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const addNewService = async (
|
export const addNewService = async (
|
||||||
userId: string,
|
userId: string,
|
||||||
serviceId: string,
|
serviceId: string,
|
||||||
organizationId: string,
|
organizationId: string,
|
||||||
) => {
|
) => {
|
||||||
const userR = await findMemberById(userId, organizationId);
|
const userR = await findMemberById(userId, organizationId);
|
||||||
await db
|
await db
|
||||||
.update(member)
|
.update(member)
|
||||||
.set({
|
.set({
|
||||||
accessedServices: [...userR.accessedServices, serviceId],
|
accessedServices: [...userR.accessedServices, serviceId],
|
||||||
})
|
})
|
||||||
.where(
|
.where(
|
||||||
and(eq(member.id, userR.id), eq(member.organizationId, organizationId)),
|
and(eq(member.id, userR.id), eq(member.organizationId, organizationId)),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const canPerformCreationService = async (
|
export const canPerformCreationService = async (
|
||||||
userId: string,
|
userId: string,
|
||||||
projectId: string,
|
projectId: string,
|
||||||
organizationId: string,
|
organizationId: string,
|
||||||
) => {
|
) => {
|
||||||
const { accessedProjects, canCreateServices } = await findMemberById(
|
const { accessedProjects, canCreateServices } = await findMemberById(
|
||||||
userId,
|
userId,
|
||||||
organizationId,
|
organizationId,
|
||||||
);
|
);
|
||||||
const haveAccessToProject = accessedProjects.includes(projectId);
|
const haveAccessToProject = accessedProjects.includes(projectId);
|
||||||
|
|
||||||
if (canCreateServices && haveAccessToProject) {
|
if (canCreateServices && haveAccessToProject) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const canPerformAccessService = async (
|
export const canPerformAccessService = async (
|
||||||
userId: string,
|
userId: string,
|
||||||
serviceId: string,
|
serviceId: string,
|
||||||
organizationId: string,
|
organizationId: string,
|
||||||
) => {
|
) => {
|
||||||
const { accessedServices } = await findMemberById(userId, organizationId);
|
const { accessedServices } = await findMemberById(userId, organizationId);
|
||||||
const haveAccessToService = accessedServices.includes(serviceId);
|
const haveAccessToService = accessedServices.includes(serviceId);
|
||||||
|
|
||||||
if (haveAccessToService) {
|
if (haveAccessToService) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const canPerformDeleteService = async (
|
export const canPerformDeleteService = async (
|
||||||
userId: string,
|
userId: string,
|
||||||
serviceId: string,
|
serviceId: string,
|
||||||
organizationId: string,
|
organizationId: string,
|
||||||
) => {
|
) => {
|
||||||
const { accessedServices, canDeleteServices } = await findMemberById(
|
const { accessedServices, canDeleteServices } = await findMemberById(
|
||||||
userId,
|
userId,
|
||||||
organizationId,
|
organizationId,
|
||||||
);
|
);
|
||||||
const haveAccessToService = accessedServices.includes(serviceId);
|
const haveAccessToService = accessedServices.includes(serviceId);
|
||||||
|
|
||||||
if (canDeleteServices && haveAccessToService) {
|
if (canDeleteServices && haveAccessToService) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const canPerformCreationProject = async (
|
export const canPerformCreationProject = async (
|
||||||
userId: string,
|
userId: string,
|
||||||
organizationId: string,
|
organizationId: string,
|
||||||
) => {
|
) => {
|
||||||
const { canCreateProjects } = await findMemberById(userId, organizationId);
|
const { canCreateProjects } = await findMemberById(userId, organizationId);
|
||||||
|
|
||||||
if (canCreateProjects) {
|
if (canCreateProjects) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const canPerformDeleteProject = async (
|
export const canPerformDeleteProject = async (
|
||||||
userId: string,
|
userId: string,
|
||||||
organizationId: string,
|
organizationId: string,
|
||||||
) => {
|
) => {
|
||||||
const { canDeleteProjects } = await findMemberById(userId, organizationId);
|
const { canDeleteProjects } = await findMemberById(userId, organizationId);
|
||||||
|
|
||||||
if (canDeleteProjects) {
|
if (canDeleteProjects) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const canPerformAccessProject = async (
|
export const canPerformAccessProject = async (
|
||||||
userId: string,
|
userId: string,
|
||||||
projectId: string,
|
projectId: string,
|
||||||
organizationId: string,
|
organizationId: string,
|
||||||
) => {
|
) => {
|
||||||
const { accessedProjects } = await findMemberById(userId, organizationId);
|
const { accessedProjects } = await findMemberById(userId, organizationId);
|
||||||
|
|
||||||
const haveAccessToProject = accessedProjects.includes(projectId);
|
const haveAccessToProject = accessedProjects.includes(projectId);
|
||||||
|
|
||||||
if (haveAccessToProject) {
|
if (haveAccessToProject) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const canPerformAccessEnvironment = async (
|
export const canPerformAccessEnvironment = async (
|
||||||
userId: string,
|
userId: string,
|
||||||
environmentId: string,
|
environmentId: string,
|
||||||
organizationId: string,
|
organizationId: string,
|
||||||
) => {
|
) => {
|
||||||
const { accessedEnvironments } = await findMemberById(userId, organizationId);
|
const { accessedEnvironments } = await findMemberById(userId, organizationId);
|
||||||
const haveAccessToEnvironment = accessedEnvironments.includes(environmentId);
|
const haveAccessToEnvironment = accessedEnvironments.includes(environmentId);
|
||||||
|
|
||||||
if (haveAccessToEnvironment) {
|
if (haveAccessToEnvironment) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const canPerformDeleteEnvironment = async (
|
export const canPerformDeleteEnvironment = async (
|
||||||
userId: string,
|
userId: string,
|
||||||
projectId: string,
|
projectId: string,
|
||||||
organizationId: string,
|
organizationId: string,
|
||||||
) => {
|
) => {
|
||||||
const { accessedProjects, canDeleteEnvironments } = await findMemberById(
|
const { accessedProjects, canDeleteEnvironments } = await findMemberById(
|
||||||
userId,
|
userId,
|
||||||
organizationId,
|
organizationId,
|
||||||
);
|
);
|
||||||
const haveAccessToProject = accessedProjects.includes(projectId);
|
const haveAccessToProject = accessedProjects.includes(projectId);
|
||||||
|
|
||||||
if (canDeleteEnvironments && haveAccessToProject) {
|
if (canDeleteEnvironments && haveAccessToProject) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const canAccessToTraefikFiles = async (
|
export const canAccessToTraefikFiles = async (
|
||||||
userId: string,
|
userId: string,
|
||||||
organizationId: string,
|
organizationId: string,
|
||||||
) => {
|
) => {
|
||||||
const { canAccessToTraefikFiles } = await findMemberById(
|
const { canAccessToTraefikFiles } = await findMemberById(
|
||||||
userId,
|
userId,
|
||||||
organizationId,
|
organizationId,
|
||||||
);
|
);
|
||||||
return canAccessToTraefikFiles;
|
return canAccessToTraefikFiles;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const checkServiceAccess = async (
|
export const checkServiceAccess = async (
|
||||||
userId: string,
|
userId: string,
|
||||||
serviceId: string,
|
serviceId: string,
|
||||||
organizationId: string,
|
organizationId: string,
|
||||||
action = "access" as "access" | "create" | "delete",
|
action = "access" as "access" | "create" | "delete",
|
||||||
) => {
|
) => {
|
||||||
let hasPermission = false;
|
let hasPermission = false;
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case "create":
|
case "create":
|
||||||
hasPermission = await canPerformCreationService(
|
hasPermission = await canPerformCreationService(
|
||||||
userId,
|
userId,
|
||||||
serviceId,
|
serviceId,
|
||||||
organizationId,
|
organizationId,
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case "access":
|
case "access":
|
||||||
hasPermission = await canPerformAccessService(
|
hasPermission = await canPerformAccessService(
|
||||||
userId,
|
userId,
|
||||||
serviceId,
|
serviceId,
|
||||||
organizationId,
|
organizationId,
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case "delete":
|
case "delete":
|
||||||
hasPermission = await canPerformDeleteService(
|
hasPermission = await canPerformDeleteService(
|
||||||
userId,
|
userId,
|
||||||
serviceId,
|
serviceId,
|
||||||
organizationId,
|
organizationId,
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
hasPermission = false;
|
hasPermission = false;
|
||||||
}
|
}
|
||||||
if (!hasPermission) {
|
if (!hasPermission) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "Permission denied",
|
message: "Permission denied",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const checkEnvironmentAccess = async (
|
export const checkEnvironmentAccess = async (
|
||||||
userId: string,
|
userId: string,
|
||||||
environmentId: string,
|
environmentId: string,
|
||||||
organizationId: string,
|
organizationId: string,
|
||||||
action = "access" as const,
|
action = "access" as const,
|
||||||
) => {
|
) => {
|
||||||
let hasPermission = false;
|
let hasPermission = false;
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case "access":
|
case "access":
|
||||||
hasPermission = await canPerformAccessEnvironment(
|
hasPermission = await canPerformAccessEnvironment(
|
||||||
userId,
|
userId,
|
||||||
environmentId,
|
environmentId,
|
||||||
organizationId,
|
organizationId,
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
hasPermission = false;
|
hasPermission = false;
|
||||||
}
|
}
|
||||||
if (!hasPermission) {
|
if (!hasPermission) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "Permission denied",
|
message: "Permission denied",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const checkEnvironmentDeletionPermission = async (
|
export const checkEnvironmentDeletionPermission = async (
|
||||||
userId: string,
|
userId: string,
|
||||||
projectId: string,
|
projectId: string,
|
||||||
organizationId: string,
|
organizationId: string,
|
||||||
) => {
|
) => {
|
||||||
const member = await findMemberById(userId, organizationId);
|
const member = await findMemberById(userId, organizationId);
|
||||||
|
|
||||||
if (!member) {
|
if (!member) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "User not found in organization",
|
message: "User not found in organization",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (member.role === "owner" || member.role === "admin") {
|
if (member.role === "owner" || member.role === "admin") {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!member.canDeleteEnvironments) {
|
if (!member.canDeleteEnvironments) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You don't have permission to delete environments",
|
message: "You don't have permission to delete environments",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasProjectAccess = member.accessedProjects.includes(projectId);
|
const hasProjectAccess = member.accessedProjects.includes(projectId);
|
||||||
if (!hasProjectAccess) {
|
if (!hasProjectAccess) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You don't have access to this project",
|
message: "You don't have access to this project",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const checkProjectAccess = async (
|
export const checkProjectAccess = async (
|
||||||
authId: string,
|
authId: string,
|
||||||
action: "create" | "delete" | "access",
|
action: "create" | "delete" | "access",
|
||||||
organizationId: string,
|
organizationId: string,
|
||||||
projectId?: string,
|
projectId?: string,
|
||||||
) => {
|
) => {
|
||||||
let hasPermission = false;
|
let hasPermission = false;
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case "access":
|
case "access":
|
||||||
hasPermission = await canPerformAccessProject(
|
hasPermission = await canPerformAccessProject(
|
||||||
authId,
|
authId,
|
||||||
projectId as string,
|
projectId as string,
|
||||||
organizationId,
|
organizationId,
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case "create":
|
case "create":
|
||||||
hasPermission = await canPerformCreationProject(authId, organizationId);
|
hasPermission = await canPerformCreationProject(authId, organizationId);
|
||||||
break;
|
break;
|
||||||
case "delete":
|
case "delete":
|
||||||
hasPermission = await canPerformDeleteProject(authId, organizationId);
|
hasPermission = await canPerformDeleteProject(authId, organizationId);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
hasPermission = false;
|
hasPermission = false;
|
||||||
}
|
}
|
||||||
if (!hasPermission) {
|
if (!hasPermission) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "Permission denied",
|
message: "Permission denied",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const checkEnvironmentCreationPermission = async (
|
export const checkEnvironmentCreationPermission = async (
|
||||||
userId: string,
|
userId: string,
|
||||||
projectId: string,
|
projectId: string,
|
||||||
organizationId: string,
|
organizationId: string,
|
||||||
) => {
|
) => {
|
||||||
// Get user's member record
|
// Get user's member record
|
||||||
const member = await findMemberById(userId, organizationId);
|
const member = await findMemberById(userId, organizationId);
|
||||||
|
|
||||||
if (!member) {
|
if (!member) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "User not found in organization",
|
message: "User not found in organization",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Owners and admins can always create environments
|
// Owners and admins can always create environments
|
||||||
if (member.role === "owner" || member.role === "admin") {
|
if (member.role === "owner" || member.role === "admin") {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if user has canCreateEnvironments permission
|
// Check if user has canCreateEnvironments permission
|
||||||
if (!member.canCreateEnvironments) {
|
if (!member.canCreateEnvironments) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You don't have permission to create environments",
|
message: "You don't have permission to create environments",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if user has access to the project
|
// Check if user has access to the project
|
||||||
const hasProjectAccess = member.accessedProjects.includes(projectId);
|
const hasProjectAccess = member.accessedProjects.includes(projectId);
|
||||||
if (!hasProjectAccess) {
|
if (!hasProjectAccess) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "You don't have access to this project",
|
message: "You don't have access to this project",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const findMemberById = async (
|
export const findMemberById = async (
|
||||||
userId: string,
|
userId: string,
|
||||||
organizationId: string,
|
organizationId: string,
|
||||||
) => {
|
) => {
|
||||||
const result = await db.query.member.findFirst({
|
const result = await db.query.member.findFirst({
|
||||||
where: and(
|
where: and(
|
||||||
eq(member.userId, userId),
|
eq(member.userId, userId),
|
||||||
eq(member.organizationId, organizationId),
|
eq(member.organizationId, organizationId),
|
||||||
),
|
),
|
||||||
with: {
|
with: {
|
||||||
user: true,
|
user: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!result) {
|
if (!result) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "UNAUTHORIZED",
|
||||||
message: "Permission denied",
|
message: "Permission denied",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const updateUser = async (userId: string, userData: Partial<User>) => {
|
export const updateUser = async (userId: string, userData: Partial<User>) => {
|
||||||
// Validate email if it's being updated
|
// Validate email if it's being updated
|
||||||
if (userData.email !== undefined) {
|
if (userData.email !== undefined) {
|
||||||
if (!userData.email || userData.email.trim() === "") {
|
if (!userData.email || userData.email.trim() === "") {
|
||||||
throw new Error("Email is required and cannot be empty");
|
throw new Error("Email is required and cannot be empty");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Basic email format validation
|
// Basic email format validation
|
||||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||||
if (!emailRegex.test(userData.email)) {
|
if (!emailRegex.test(userData.email)) {
|
||||||
throw new Error("Please enter a valid email address");
|
throw new Error("Please enter a valid email address");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const userResult = await db
|
const userResult = await db
|
||||||
.update(user)
|
.update(user)
|
||||||
.set({
|
.set({
|
||||||
...userData,
|
...userData,
|
||||||
})
|
})
|
||||||
.where(eq(user.id, userId))
|
.where(eq(user.id, userId))
|
||||||
.returning()
|
.returning()
|
||||||
.then((res) => res[0]);
|
.then((res) => res[0]);
|
||||||
|
|
||||||
return userResult;
|
return userResult;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createApiKey = async (
|
export const createApiKey = async (
|
||||||
userId: string,
|
userId: string,
|
||||||
input: {
|
input: {
|
||||||
name: string;
|
name: string;
|
||||||
prefix?: string;
|
prefix?: string;
|
||||||
expiresIn?: number;
|
expiresIn?: number;
|
||||||
metadata: {
|
metadata: {
|
||||||
organizationId: string;
|
organizationId: string;
|
||||||
};
|
};
|
||||||
rateLimitEnabled?: boolean;
|
rateLimitEnabled?: boolean;
|
||||||
rateLimitTimeWindow?: number;
|
rateLimitTimeWindow?: number;
|
||||||
rateLimitMax?: number;
|
rateLimitMax?: number;
|
||||||
remaining?: number;
|
remaining?: number;
|
||||||
refillAmount?: number;
|
refillAmount?: number;
|
||||||
refillInterval?: number;
|
refillInterval?: number;
|
||||||
},
|
},
|
||||||
) => {
|
) => {
|
||||||
const result = await auth.createApiKey({
|
const result = await auth.createApiKey({
|
||||||
body: {
|
body: {
|
||||||
name: input.name,
|
name: input.name,
|
||||||
expiresIn: input.expiresIn,
|
expiresIn: input.expiresIn,
|
||||||
prefix: input.prefix,
|
prefix: input.prefix,
|
||||||
rateLimitEnabled: input.rateLimitEnabled,
|
rateLimitEnabled: input.rateLimitEnabled,
|
||||||
rateLimitTimeWindow: input.rateLimitTimeWindow,
|
rateLimitTimeWindow: input.rateLimitTimeWindow,
|
||||||
rateLimitMax: input.rateLimitMax,
|
rateLimitMax: input.rateLimitMax,
|
||||||
remaining: input.remaining,
|
remaining: input.remaining,
|
||||||
refillAmount: input.refillAmount,
|
refillAmount: input.refillAmount,
|
||||||
refillInterval: input.refillInterval,
|
refillInterval: input.refillInterval,
|
||||||
userId,
|
userId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (input.metadata) {
|
if (input.metadata) {
|
||||||
await db
|
await db
|
||||||
.update(apikey)
|
.update(apikey)
|
||||||
.set({ metadata: JSON.stringify(input.metadata) })
|
.set({ metadata: JSON.stringify(input.metadata) })
|
||||||
.where(eq(apikey.id, result.id));
|
.where(eq(apikey.id, result.id));
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,26 +5,26 @@ import boxen from "boxen";
|
|||||||
import { quote } from "shell-quote";
|
import { quote } from "shell-quote";
|
||||||
import { writeDomainsToCompose } from "../docker/domain";
|
import { writeDomainsToCompose } from "../docker/domain";
|
||||||
import {
|
import {
|
||||||
encodeBase64,
|
encodeBase64,
|
||||||
getEnvironmentVariablesObject,
|
getEnvironmentVariablesObject,
|
||||||
prepareEnvironmentVariables,
|
prepareEnvironmentVariables,
|
||||||
} from "../docker/utils";
|
} from "../docker/utils";
|
||||||
|
|
||||||
export type ComposeNested = InferResultType<
|
export type ComposeNested = InferResultType<
|
||||||
"compose",
|
"compose",
|
||||||
{ environment: { with: { project: true } }; mounts: true; domains: true }
|
{ environment: { with: { project: true } }; mounts: true; domains: true }
|
||||||
>;
|
>;
|
||||||
|
|
||||||
export const getBuildComposeCommand = async (compose: ComposeNested) => {
|
export const getBuildComposeCommand = async (compose: ComposeNested) => {
|
||||||
const { COMPOSE_PATH } = paths(!!compose.serverId);
|
const { COMPOSE_PATH } = paths(!!compose.serverId);
|
||||||
const { sourceType, appName, mounts, composeType, domains } = compose;
|
const { sourceType, appName, mounts, composeType, domains } = compose;
|
||||||
const command = createCommand(compose);
|
const command = createCommand(compose);
|
||||||
const envCommand = getCreateEnvFileCommand(compose);
|
const envCommand = getCreateEnvFileCommand(compose);
|
||||||
const projectPath = join(COMPOSE_PATH, compose.appName, "code");
|
const projectPath = join(COMPOSE_PATH, compose.appName, "code");
|
||||||
const exportEnvCommand = getExportEnvCommand(compose);
|
const exportEnvCommand = getExportEnvCommand(compose);
|
||||||
|
|
||||||
const newCompose = await writeDomainsToCompose(compose, domains);
|
const newCompose = await writeDomainsToCompose(compose, domains);
|
||||||
const logContent = `
|
const logContent = `
|
||||||
App Name: ${appName}
|
App Name: ${appName}
|
||||||
Build Compose 🐳
|
Build Compose 🐳
|
||||||
Detected: ${mounts.length} mounts 📂
|
Detected: ${mounts.length} mounts 📂
|
||||||
@@ -32,17 +32,17 @@ Command: docker ${command}
|
|||||||
Source Type: docker ${sourceType} ✅
|
Source Type: docker ${sourceType} ✅
|
||||||
Compose Type: ${composeType} ✅`;
|
Compose Type: ${composeType} ✅`;
|
||||||
|
|
||||||
const logBox = boxen(logContent, {
|
const logBox = boxen(logContent, {
|
||||||
padding: {
|
padding: {
|
||||||
left: 1,
|
left: 1,
|
||||||
right: 1,
|
right: 1,
|
||||||
bottom: 1,
|
bottom: 1,
|
||||||
},
|
},
|
||||||
width: 80,
|
width: 80,
|
||||||
borderStyle: "double",
|
borderStyle: "double",
|
||||||
});
|
});
|
||||||
|
|
||||||
const bashCommand = `
|
const bashCommand = `
|
||||||
set -e
|
set -e
|
||||||
{
|
{
|
||||||
echo "${logBox}";
|
echo "${logBox}";
|
||||||
@@ -64,81 +64,81 @@ Compose Type: ${composeType} ✅`;
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
return bashCommand;
|
return bashCommand;
|
||||||
};
|
};
|
||||||
|
|
||||||
const sanitizeCommand = (command: string) => {
|
const sanitizeCommand = (command: string) => {
|
||||||
const sanitizedCommand = command.trim();
|
const sanitizedCommand = command.trim();
|
||||||
|
|
||||||
const parts = sanitizedCommand.split(/\s+/);
|
const parts = sanitizedCommand.split(/\s+/);
|
||||||
|
|
||||||
const restCommand = parts.map((arg) => arg.replace(/^"(.*)"$/, "$1"));
|
const restCommand = parts.map((arg) => arg.replace(/^"(.*)"$/, "$1"));
|
||||||
|
|
||||||
return restCommand.join(" ");
|
return restCommand.join(" ");
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createCommand = (compose: ComposeNested) => {
|
export const createCommand = (compose: ComposeNested) => {
|
||||||
const { composeType, appName, sourceType } = compose;
|
const { composeType, appName, sourceType } = compose;
|
||||||
if (compose.command) {
|
if (compose.command) {
|
||||||
return `${sanitizeCommand(compose.command)}`;
|
return `${sanitizeCommand(compose.command)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const path =
|
const path =
|
||||||
sourceType === "raw" ? "docker-compose.yml" : compose.composePath;
|
sourceType === "raw" ? "docker-compose.yml" : compose.composePath;
|
||||||
let command = "";
|
let command = "";
|
||||||
|
|
||||||
if (composeType === "docker-compose") {
|
if (composeType === "docker-compose") {
|
||||||
command = `compose -p ${appName} -f ${path} up -d --build --remove-orphans`;
|
command = `compose -p ${appName} -f ${path} up -d --build --remove-orphans`;
|
||||||
} else if (composeType === "stack") {
|
} else if (composeType === "stack") {
|
||||||
command = `stack deploy -c ${path} ${appName} --prune --with-registry-auth`;
|
command = `stack deploy -c ${path} ${appName} --prune --with-registry-auth`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return command;
|
return command;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getCreateEnvFileCommand = (compose: ComposeNested) => {
|
export const getCreateEnvFileCommand = (compose: ComposeNested) => {
|
||||||
const { COMPOSE_PATH } = paths(!!compose.serverId);
|
const { COMPOSE_PATH } = paths(!!compose.serverId);
|
||||||
const { env, composePath, appName } = compose;
|
const { env, composePath, appName } = compose;
|
||||||
const composeFilePath =
|
const composeFilePath =
|
||||||
join(COMPOSE_PATH, appName, "code", composePath) ||
|
join(COMPOSE_PATH, appName, "code", composePath) ||
|
||||||
join(COMPOSE_PATH, appName, "code", "docker-compose.yml");
|
join(COMPOSE_PATH, appName, "code", "docker-compose.yml");
|
||||||
|
|
||||||
const envFilePath = join(dirname(composeFilePath), ".env");
|
const envFilePath = join(dirname(composeFilePath), ".env");
|
||||||
|
|
||||||
let envContent = `APP_NAME=${appName}\n`;
|
let envContent = `APP_NAME=${appName}\n`;
|
||||||
envContent += env || "";
|
envContent += env || "";
|
||||||
if (!envContent.includes("DOCKER_CONFIG")) {
|
if (!envContent.includes("DOCKER_CONFIG")) {
|
||||||
envContent += "\nDOCKER_CONFIG=/root/.docker";
|
envContent += "\nDOCKER_CONFIG=/root/.docker";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (compose.randomize) {
|
if (compose.randomize) {
|
||||||
envContent += `\nCOMPOSE_PREFIX=${compose.suffix}`;
|
envContent += `\nCOMPOSE_PREFIX=${compose.suffix}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const envFileContent = prepareEnvironmentVariables(
|
const envFileContent = prepareEnvironmentVariables(
|
||||||
envContent,
|
envContent,
|
||||||
compose.environment.project.env,
|
compose.environment.project.env,
|
||||||
compose.environment.env,
|
compose.environment.env,
|
||||||
).join("\n");
|
).join("\n");
|
||||||
|
|
||||||
const encodedContent = encodeBase64(envFileContent);
|
const encodedContent = encodeBase64(envFileContent);
|
||||||
return `
|
return `
|
||||||
touch ${envFilePath};
|
touch ${envFilePath};
|
||||||
echo "${encodedContent}" | base64 -d > "${envFilePath}";
|
echo "${encodedContent}" | base64 -d > "${envFilePath}";
|
||||||
`;
|
`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getExportEnvCommand = (compose: ComposeNested) => {
|
const getExportEnvCommand = (compose: ComposeNested) => {
|
||||||
if (compose.composeType !== "stack") return "";
|
if (compose.composeType !== "stack") return "";
|
||||||
|
|
||||||
const envVars = getEnvironmentVariablesObject(
|
const envVars = getEnvironmentVariablesObject(
|
||||||
compose.env,
|
compose.env,
|
||||||
compose.environment.project.env,
|
compose.environment.project.env,
|
||||||
compose.environment.env,
|
compose.environment.env,
|
||||||
);
|
);
|
||||||
const exports = Object.entries(envVars)
|
const exports = Object.entries(envVars)
|
||||||
.map(([key, value]) => `${key}=${quote([value])}`)
|
.map(([key, value]) => `${key}=${quote([value])}`)
|
||||||
.join(" ");
|
.join(" ");
|
||||||
|
|
||||||
return exports ? `${exports}` : "";
|
return exports ? `${exports}` : "";
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,90 +1,90 @@
|
|||||||
import {
|
import {
|
||||||
getEnvironmentVariablesObject,
|
getEnvironmentVariablesObject,
|
||||||
prepareEnvironmentVariablesForShell,
|
prepareEnvironmentVariablesForShell,
|
||||||
} from "@dokploy/server/utils/docker/utils";
|
} from "@dokploy/server/utils/docker/utils";
|
||||||
import { quote } from "shell-quote";
|
import { quote } from "shell-quote";
|
||||||
import {
|
import {
|
||||||
getBuildAppDirectory,
|
getBuildAppDirectory,
|
||||||
getDockerContextPath,
|
getDockerContextPath,
|
||||||
} from "../filesystem/directory";
|
} from "../filesystem/directory";
|
||||||
import type { ApplicationNested } from ".";
|
import type { ApplicationNested } from ".";
|
||||||
import { createEnvFileCommand } from "./utils";
|
import { createEnvFileCommand } from "./utils";
|
||||||
|
|
||||||
export const getDockerCommand = (application: ApplicationNested) => {
|
export const getDockerCommand = (application: ApplicationNested) => {
|
||||||
const {
|
const {
|
||||||
appName,
|
appName,
|
||||||
env,
|
env,
|
||||||
publishDirectory,
|
publishDirectory,
|
||||||
buildArgs,
|
buildArgs,
|
||||||
buildSecrets,
|
buildSecrets,
|
||||||
dockerBuildStage,
|
dockerBuildStage,
|
||||||
cleanCache,
|
cleanCache,
|
||||||
createEnvFile,
|
createEnvFile,
|
||||||
} = application;
|
} = application;
|
||||||
const dockerFilePath = getBuildAppDirectory(application);
|
const dockerFilePath = getBuildAppDirectory(application);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const image = `${appName}`;
|
const image = `${appName}`;
|
||||||
|
|
||||||
const defaultContextPath =
|
const defaultContextPath =
|
||||||
dockerFilePath.substring(0, dockerFilePath.lastIndexOf("/") + 1) || ".";
|
dockerFilePath.substring(0, dockerFilePath.lastIndexOf("/") + 1) || ".";
|
||||||
|
|
||||||
const dockerContextPath =
|
const dockerContextPath =
|
||||||
getDockerContextPath(application) || defaultContextPath;
|
getDockerContextPath(application) || defaultContextPath;
|
||||||
|
|
||||||
const commandArgs = ["build", "-t", image, "-f", dockerFilePath, "."];
|
const commandArgs = ["build", "-t", image, "-f", dockerFilePath, "."];
|
||||||
|
|
||||||
if (dockerBuildStage) {
|
if (dockerBuildStage) {
|
||||||
commandArgs.push("--target", dockerBuildStage);
|
commandArgs.push("--target", dockerBuildStage);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cleanCache) {
|
if (cleanCache) {
|
||||||
commandArgs.push("--no-cache");
|
commandArgs.push("--no-cache");
|
||||||
}
|
}
|
||||||
|
|
||||||
const args = prepareEnvironmentVariablesForShell(
|
const args = prepareEnvironmentVariablesForShell(
|
||||||
buildArgs,
|
buildArgs,
|
||||||
application.environment.project.env,
|
application.environment.project.env,
|
||||||
application.environment.env,
|
application.environment.env,
|
||||||
);
|
);
|
||||||
|
|
||||||
for (const arg of args) {
|
for (const arg of args) {
|
||||||
commandArgs.push("--build-arg", arg);
|
commandArgs.push("--build-arg", arg);
|
||||||
}
|
}
|
||||||
|
|
||||||
const secrets = getEnvironmentVariablesObject(
|
const secrets = getEnvironmentVariablesObject(
|
||||||
buildSecrets,
|
buildSecrets,
|
||||||
application.environment.project.env,
|
application.environment.project.env,
|
||||||
application.environment.env,
|
application.environment.env,
|
||||||
);
|
);
|
||||||
|
|
||||||
const joinedSecrets = Object.entries(secrets)
|
const joinedSecrets = Object.entries(secrets)
|
||||||
.map(([key, value]) => `${key}=${quote([value])}`)
|
.map(([key, value]) => `${key}=${quote([value])}`)
|
||||||
.join(" ");
|
.join(" ");
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Do not generate an environment file when publishDirectory is specified,
|
Do not generate an environment file when publishDirectory is specified,
|
||||||
as it could be publicly exposed.
|
as it could be publicly exposed.
|
||||||
Also respect the createEnvFile flag.
|
Also respect the createEnvFile flag.
|
||||||
*/
|
*/
|
||||||
let command = "";
|
let command = "";
|
||||||
if (!publishDirectory && createEnvFile) {
|
if (!publishDirectory && createEnvFile) {
|
||||||
command += createEnvFileCommand(
|
command += createEnvFileCommand(
|
||||||
dockerFilePath,
|
dockerFilePath,
|
||||||
env,
|
env,
|
||||||
application.environment.project.env,
|
application.environment.project.env,
|
||||||
application.environment.env,
|
application.environment.env,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const key in secrets) {
|
for (const key in secrets) {
|
||||||
// Although buildx is smart enough to know we may be referring to an environment variable name,
|
// Although buildx is smart enough to know we may be referring to an environment variable name,
|
||||||
// we still make sure it doesn't fall back to `type=file`.
|
// we still make sure it doesn't fall back to `type=file`.
|
||||||
// See: https://docs.docker.com/reference/cli/docker/buildx/build/#secret
|
// See: https://docs.docker.com/reference/cli/docker/buildx/build/#secret
|
||||||
commandArgs.push("--secret", `type=env,id=${key}`);
|
commandArgs.push("--secret", `type=env,id=${key}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
command += `
|
command += `
|
||||||
echo "Building ${appName}" ;
|
echo "Building ${appName}" ;
|
||||||
cd ${dockerContextPath} || {
|
cd ${dockerContextPath} || {
|
||||||
echo "❌ The path ${dockerContextPath} does not exist" ;
|
echo "❌ The path ${dockerContextPath} does not exist" ;
|
||||||
@@ -98,8 +98,8 @@ ${joinedSecrets} docker ${commandArgs.join(" ")} || {
|
|||||||
echo "✅ Docker build completed." ;
|
echo "✅ Docker build completed." ;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
return command;
|
return command;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user