Merge branch 'canary' into feat/requests

This commit is contained in:
Mauricio Siu
2024-09-05 00:17:40 -06:00
189 changed files with 22323 additions and 2667 deletions

View File

@@ -1,4 +1,4 @@
import { addPrefixToAllProperties } from "@/server/utils/docker/compose";
import { addSuffixToAllProperties } from "@/server/utils/docker/compose";
import type { ComposeSpecification } from "@/server/utils/docker/types";
import { load } from "js-yaml";
import { expect, test } from "vitest";
@@ -119,11 +119,11 @@ secrets:
file: ./db_password.txt
`) as ComposeSpecification;
test("Add prefix to all properties in compose file 1", () => {
test("Add suffix to all properties in compose file 1", () => {
const composeData = load(composeFile1) as ComposeSpecification;
const prefix = "testhash";
const suffix = "testhash";
const updatedComposeData = addPrefixToAllProperties(composeData, prefix);
const updatedComposeData = addSuffixToAllProperties(composeData, suffix);
expect(updatedComposeData).toEqual(expectedComposeFile1);
});
@@ -242,11 +242,11 @@ secrets:
file: ./db_password.txt
`) as ComposeSpecification;
test("Add prefix to all properties in compose file 2", () => {
test("Add suffix to all properties in compose file 2", () => {
const composeData = load(composeFile2) as ComposeSpecification;
const prefix = "testhash";
const suffix = "testhash";
const updatedComposeData = addPrefixToAllProperties(composeData, prefix);
const updatedComposeData = addSuffixToAllProperties(composeData, suffix);
expect(updatedComposeData).toEqual(expectedComposeFile2);
});
@@ -365,11 +365,11 @@ secrets:
file: ./service_secret.txt
`) as ComposeSpecification;
test("Add prefix to all properties in compose file 3", () => {
test("Add suffix to all properties in compose file 3", () => {
const composeData = load(composeFile3) as ComposeSpecification;
const prefix = "testhash";
const suffix = "testhash";
const updatedComposeData = addPrefixToAllProperties(composeData, prefix);
const updatedComposeData = addSuffixToAllProperties(composeData, suffix);
expect(updatedComposeData).toEqual(expectedComposeFile3);
});
@@ -466,11 +466,11 @@ volumes:
driver: local
`) as ComposeSpecification;
test("Add prefix to all properties in Plausible compose file", () => {
test("Add suffix to all properties in Plausible compose file", () => {
const composeData = load(composeFile) as ComposeSpecification;
const prefix = "testhash";
const suffix = "testhash";
const updatedComposeData = addPrefixToAllProperties(composeData, prefix);
const updatedComposeData = addSuffixToAllProperties(composeData, suffix);
expect(updatedComposeData).toEqual(expectedComposeFile);
});

View File

@@ -1,5 +1,5 @@
import { generateRandomHash } from "@/server/utils/docker/compose";
import { addPrefixToConfigsRoot } from "@/server/utils/docker/compose/configs";
import { addSuffixToConfigsRoot } from "@/server/utils/docker/compose/configs";
import type { ComposeSpecification } from "@/server/utils/docker/types";
import { load } from "js-yaml";
import { expect, test } from "vitest";
@@ -23,19 +23,19 @@ configs:
file: ./web-config.yml
`;
test("Add prefix to configs in root property", () => {
test("Add suffix to configs in root property", () => {
const composeData = load(composeFile) as ComposeSpecification;
const prefix = generateRandomHash();
const suffix = generateRandomHash();
if (!composeData?.configs) {
return;
}
const configs = addPrefixToConfigsRoot(composeData.configs, prefix);
const configs = addSuffixToConfigsRoot(composeData.configs, suffix);
expect(configs).toBeDefined();
for (const configKey of Object.keys(configs)) {
expect(configKey).toContain(`-${prefix}`);
expect(configKey).toContain(`-${suffix}`);
expect(configs[configKey]).toBeDefined();
}
});
@@ -59,23 +59,23 @@ configs:
file: ./another-config.yml
`;
test("Add prefix to multiple configs in root property", () => {
test("Add suffix to multiple configs in root property", () => {
const composeData = load(composeFileMultipleConfigs) as ComposeSpecification;
const prefix = generateRandomHash();
const suffix = generateRandomHash();
if (!composeData?.configs) {
return;
}
const configs = addPrefixToConfigsRoot(composeData.configs, prefix);
const configs = addSuffixToConfigsRoot(composeData.configs, suffix);
expect(configs).toBeDefined();
for (const configKey of Object.keys(configs)) {
expect(configKey).toContain(`-${prefix}`);
expect(configKey).toContain(`-${suffix}`);
expect(configs[configKey]).toBeDefined();
}
expect(configs).toHaveProperty(`web-config-${prefix}`);
expect(configs).toHaveProperty(`another-config-${prefix}`);
expect(configs).toHaveProperty(`web-config-${suffix}`);
expect(configs).toHaveProperty(`another-config-${suffix}`);
});
const composeFileDifferentProperties = `
@@ -92,25 +92,25 @@ configs:
external: true
`;
test("Add prefix to configs with different properties in root property", () => {
test("Add suffix to configs with different properties in root property", () => {
const composeData = load(
composeFileDifferentProperties,
) as ComposeSpecification;
const prefix = generateRandomHash();
const suffix = generateRandomHash();
if (!composeData?.configs) {
return;
}
const configs = addPrefixToConfigsRoot(composeData.configs, prefix);
const configs = addSuffixToConfigsRoot(composeData.configs, suffix);
expect(configs).toBeDefined();
for (const configKey of Object.keys(configs)) {
expect(configKey).toContain(`-${prefix}`);
expect(configKey).toContain(`-${suffix}`);
expect(configs[configKey]).toBeDefined();
}
expect(configs).toHaveProperty(`web-config-${prefix}`);
expect(configs).toHaveProperty(`special-config-${prefix}`);
expect(configs).toHaveProperty(`web-config-${suffix}`);
expect(configs).toHaveProperty(`special-config-${suffix}`);
});
const composeFileConfigRoot = `
@@ -162,15 +162,15 @@ configs:
file: ./db-config.yml
`) as ComposeSpecification;
test("Add prefix to configs in root property", () => {
test("Add suffix to configs in root property", () => {
const composeData = load(composeFileConfigRoot) as ComposeSpecification;
const prefix = "testhash";
const suffix = "testhash";
if (!composeData?.configs) {
return;
}
const configs = addPrefixToConfigsRoot(composeData.configs, prefix);
const configs = addSuffixToConfigsRoot(composeData.configs, suffix);
const updatedComposeData = { ...composeData, configs };
// Verificar que el resultado coincide con el archivo esperado

View File

@@ -1,5 +1,5 @@
import { generateRandomHash } from "@/server/utils/docker/compose";
import { addPrefixToConfigsInServices } from "@/server/utils/docker/compose/configs";
import { addSuffixToConfigsInServices } from "@/server/utils/docker/compose/configs";
import type { ComposeSpecification } from "@/server/utils/docker/types";
import { load } from "js-yaml";
import { expect, test } from "vitest";
@@ -19,19 +19,19 @@ configs:
file: ./web-config.yml
`;
test("Add prefix to configs in services", () => {
test("Add suffix to configs in services", () => {
const composeData = load(composeFile) as ComposeSpecification;
const prefix = generateRandomHash();
const suffix = generateRandomHash();
if (!composeData?.services) {
return;
}
const services = addPrefixToConfigsInServices(composeData.services, prefix);
const services = addSuffixToConfigsInServices(composeData.services, suffix);
const actualComposeData = { ...composeData, services };
expect(actualComposeData.services?.web?.configs).toContainEqual({
source: `web-config-${prefix}`,
source: `web-config-${suffix}`,
target: "/etc/nginx/nginx.conf",
});
});
@@ -51,17 +51,17 @@ configs:
file: ./web-config.yml
`;
test("Add prefix to configs in services with single config", () => {
test("Add suffix to configs in services with single config", () => {
const composeData = load(
composeFileSingleServiceConfig,
) as ComposeSpecification;
const prefix = generateRandomHash();
const suffix = generateRandomHash();
if (!composeData?.services) {
return;
}
const services = addPrefixToConfigsInServices(composeData.services, prefix);
const services = addSuffixToConfigsInServices(composeData.services, suffix);
expect(services).toBeDefined();
for (const serviceKey of Object.keys(services)) {
@@ -69,7 +69,7 @@ test("Add prefix to configs in services with single config", () => {
if (serviceConfigs) {
for (const config of serviceConfigs) {
if (typeof config === "object") {
expect(config.source).toContain(`-${prefix}`);
expect(config.source).toContain(`-${suffix}`);
}
}
}
@@ -105,17 +105,17 @@ configs:
file: ./common-config.yml
`;
test("Add prefix to configs in services with multiple configs", () => {
test("Add suffix to configs in services with multiple configs", () => {
const composeData = load(
composeFileMultipleServicesConfigs,
) as ComposeSpecification;
const prefix = generateRandomHash();
const suffix = generateRandomHash();
if (!composeData?.services) {
return;
}
const services = addPrefixToConfigsInServices(composeData.services, prefix);
const services = addSuffixToConfigsInServices(composeData.services, suffix);
expect(services).toBeDefined();
for (const serviceKey of Object.keys(services)) {
@@ -123,7 +123,7 @@ test("Add prefix to configs in services with multiple configs", () => {
if (serviceConfigs) {
for (const config of serviceConfigs) {
if (typeof config === "object") {
expect(config.source).toContain(`-${prefix}`);
expect(config.source).toContain(`-${suffix}`);
}
}
}
@@ -179,17 +179,17 @@ services:
`) as ComposeSpecification;
test("Add prefix to configs in services", () => {
test("Add suffix to configs in services", () => {
const composeData = load(composeFileConfigServices) as ComposeSpecification;
const prefix = "testhash";
const suffix = "testhash";
if (!composeData?.services) {
return;
}
const updatedComposeData = addPrefixToConfigsInServices(
const updatedComposeData = addSuffixToConfigsInServices(
composeData.services,
prefix,
suffix,
);
const actualComposeData = { ...composeData, services: updatedComposeData };

View File

@@ -1,7 +1,7 @@
import { generateRandomHash } from "@/server/utils/docker/compose";
import {
addPrefixToAllConfigs,
addPrefixToConfigsRoot,
addSuffixToAllConfigs,
addSuffixToConfigsRoot,
} from "@/server/utils/docker/compose/configs";
import type { ComposeSpecification } from "@/server/utils/docker/types";
import { load } from "js-yaml";
@@ -80,12 +80,12 @@ configs:
file: ./db-config.yml
`) as ComposeSpecification;
test("Add prefix to all configs in root and services", () => {
test("Add suffix to all configs in root and services", () => {
const composeData = load(composeFileCombinedConfigs) as ComposeSpecification;
const prefix = "testhash";
const suffix = "testhash";
const updatedComposeData = addPrefixToAllConfigs(composeData, prefix);
const updatedComposeData = addSuffixToAllConfigs(composeData, suffix);
expect(updatedComposeData).toEqual(expectedComposeFileCombinedConfigs);
});
@@ -162,14 +162,14 @@ configs:
file: ./db-config.yml
`) as ComposeSpecification;
test("Add prefix to configs with environment and external", () => {
test("Add suffix to configs with environment and external", () => {
const composeData = load(
composeFileWithEnvAndExternal,
) as ComposeSpecification;
const prefix = "testhash";
const suffix = "testhash";
const updatedComposeData = addPrefixToAllConfigs(composeData, prefix);
const updatedComposeData = addSuffixToAllConfigs(composeData, suffix);
expect(updatedComposeData).toEqual(expectedComposeFileWithEnvAndExternal);
});
@@ -234,14 +234,14 @@ configs:
file: ./db-config.yml
`) as ComposeSpecification;
test("Add prefix to configs with template driver and labels", () => {
test("Add suffix to configs with template driver and labels", () => {
const composeData = load(
composeFileWithTemplateDriverAndLabels,
) as ComposeSpecification;
const prefix = "testhash";
const suffix = "testhash";
const updatedComposeData = addPrefixToAllConfigs(composeData, prefix);
const updatedComposeData = addSuffixToAllConfigs(composeData, suffix);
expect(updatedComposeData).toEqual(
expectedComposeFileWithTemplateDriverAndLabels,

View File

@@ -1,5 +1,5 @@
import { generateRandomHash } from "@/server/utils/docker/compose";
import { addPrefixToNetworksRoot } from "@/server/utils/docker/compose/network";
import { addSuffixToNetworksRoot } from "@/server/utils/docker/compose/network";
import type { ComposeSpecification } from "@/server/utils/docker/types";
import { load } from "js-yaml";
import { expect, test } from "vitest";
@@ -35,19 +35,19 @@ test("Generate random hash with 8 characters", () => {
expect(hash.length).toBe(8);
});
test("Add prefix to networks root property", () => {
test("Add suffix to networks root property", () => {
const composeData = load(composeFile) as ComposeSpecification;
const prefix = generateRandomHash();
const suffix = generateRandomHash();
if (!composeData?.networks) {
return;
}
const networks = addPrefixToNetworksRoot(composeData.networks, prefix);
const networks = addSuffixToNetworksRoot(composeData.networks, suffix);
expect(networks).toBeDefined();
for (const volumeKey of Object.keys(networks)) {
expect(volumeKey).toContain(`-${prefix}`);
expect(volumeKey).toContain(`-${suffix}`);
}
});
@@ -79,19 +79,19 @@ networks:
internal: true
`;
test("Add prefix to advanced networks root property (2 TRY)", () => {
test("Add suffix to advanced networks root property (2 TRY)", () => {
const composeData = load(composeFile2) as ComposeSpecification;
const prefix = generateRandomHash();
const suffix = generateRandomHash();
if (!composeData?.networks) {
return;
}
const networks = addPrefixToNetworksRoot(composeData.networks, prefix);
const networks = addSuffixToNetworksRoot(composeData.networks, suffix);
expect(networks).toBeDefined();
for (const networkKey of Object.keys(networks)) {
expect(networkKey).toContain(`-${prefix}`);
expect(networkKey).toContain(`-${suffix}`);
}
});
@@ -120,19 +120,19 @@ networks:
external: true
`;
test("Add prefix to networks with external properties", () => {
test("Add suffix to networks with external properties", () => {
const composeData = load(composeFile3) as ComposeSpecification;
const prefix = generateRandomHash();
const suffix = generateRandomHash();
if (!composeData?.networks) {
return;
}
const networks = addPrefixToNetworksRoot(composeData.networks, prefix);
const networks = addSuffixToNetworksRoot(composeData.networks, suffix);
expect(networks).toBeDefined();
for (const networkKey of Object.keys(networks)) {
expect(networkKey).toContain(`-${prefix}`);
expect(networkKey).toContain(`-${suffix}`);
}
});
@@ -160,19 +160,19 @@ networks:
external: true
`;
test("Add prefix to networks with IPAM configurations", () => {
test("Add suffix to networks with IPAM configurations", () => {
const composeData = load(composeFile4) as ComposeSpecification;
const prefix = generateRandomHash();
const suffix = generateRandomHash();
if (!composeData?.networks) {
return;
}
const networks = addPrefixToNetworksRoot(composeData.networks, prefix);
const networks = addSuffixToNetworksRoot(composeData.networks, suffix);
expect(networks).toBeDefined();
for (const networkKey of Object.keys(networks)) {
expect(networkKey).toContain(`-${prefix}`);
expect(networkKey).toContain(`-${suffix}`);
}
});
@@ -201,19 +201,19 @@ networks:
external: true
`;
test("Add prefix to networks with custom options", () => {
test("Add suffix to networks with custom options", () => {
const composeData = load(composeFile5) as ComposeSpecification;
const prefix = generateRandomHash();
const suffix = generateRandomHash();
if (!composeData?.networks) {
return;
}
const networks = addPrefixToNetworksRoot(composeData.networks, prefix);
const networks = addSuffixToNetworksRoot(composeData.networks, suffix);
expect(networks).toBeDefined();
for (const networkKey of Object.keys(networks)) {
expect(networkKey).toContain(`-${prefix}`);
expect(networkKey).toContain(`-${suffix}`);
}
});
@@ -240,7 +240,7 @@ networks:
external: true
`;
// Expected compose file with static prefix `testhash`
// Expected compose file with static suffix `testhash`
const expectedComposeFile6 = `
version: "3.8"
@@ -264,18 +264,70 @@ networks:
external: true
`;
test("Add prefix to networks with static prefix", () => {
test("Add suffix to networks with static suffix", () => {
const composeData = load(composeFile6) as ComposeSpecification;
const prefix = "testhash";
const suffix = "testhash";
if (!composeData?.networks) {
return;
}
const networks = addPrefixToNetworksRoot(composeData.networks, prefix);
const networks = addSuffixToNetworksRoot(composeData.networks, suffix);
const expectedComposeData = load(
expectedComposeFile6,
) as ComposeSpecification;
expect(networks).toStrictEqual(expectedComposeData.networks);
});
const composeFile7 = `
version: "3.8"
services:
web:
image: nginx:latest
networks:
- dokploy-network
networks:
dokploy-network:
`;
const expectedComposeFile7 = `
version: "3.8"
services:
web:
image: nginx:latest
networks:
- dokploy-network
networks:
dokploy-network:
driver: bridge
driver_opts:
com.docker.network.driver.mtu: 1200
backend:
driver: bridge
attachable: true
external_network:
external: true
name: dokploy-network
`;
test("It shoudn't add suffix to dokploy-network", () => {
const composeData = load(composeFile7) as ComposeSpecification;
const suffix = generateRandomHash();
if (!composeData?.networks) {
return;
}
const networks = addSuffixToNetworksRoot(composeData.networks, suffix);
expect(networks).toBeDefined();
for (const networkKey of Object.keys(networks)) {
expect(networkKey).toContain("dokploy-network");
}
});

View File

@@ -1,5 +1,5 @@
import { generateRandomHash } from "@/server/utils/docker/compose";
import { addPrefixToServiceNetworks } from "@/server/utils/docker/compose/network";
import { addSuffixToServiceNetworks } from "@/server/utils/docker/compose/network";
import type { ComposeSpecification } from "@/server/utils/docker/types";
import { load } from "js-yaml";
import { expect, test } from "vitest";
@@ -20,30 +20,30 @@ services:
- backend
`;
test("Add prefix to networks in services", () => {
test("Add suffix to networks in services", () => {
const composeData = load(composeFile) as ComposeSpecification;
const prefix = generateRandomHash();
const suffix = generateRandomHash();
if (!composeData?.services) {
return;
}
const services = addPrefixToServiceNetworks(composeData.services, prefix);
const services = addSuffixToServiceNetworks(composeData.services, suffix);
const actualComposeData = { ...composeData, services };
expect(actualComposeData?.services?.web?.networks).toContain(
`frontend-${prefix}`,
`frontend-${suffix}`,
);
expect(actualComposeData?.services?.api?.networks).toContain(
`backend-${prefix}`,
`backend-${suffix}`,
);
const apiNetworks = actualComposeData?.services?.api?.networks;
expect(apiNetworks).toBeDefined();
expect(actualComposeData?.services?.api?.networks).toContain(
`backend-${prefix}`,
`backend-${suffix}`,
);
});
@@ -64,26 +64,26 @@ networks:
driver: bridge
`;
test("Add prefix to networks in services with aliases", () => {
test("Add suffix to networks in services with aliases", () => {
const composeData = load(composeFile2) as ComposeSpecification;
const prefix = generateRandomHash();
const suffix = generateRandomHash();
if (!composeData?.services) {
return;
}
const services = addPrefixToServiceNetworks(composeData.services, prefix);
const services = addSuffixToServiceNetworks(composeData.services, suffix);
const actualComposeData = { ...composeData, services };
expect(actualComposeData.services?.api?.networks).toHaveProperty(
`frontend-${prefix}`,
`frontend-${suffix}`,
);
const networkConfig = actualComposeData?.services?.api?.networks as {
[key: string]: { aliases?: string[] };
};
expect(networkConfig[`frontend-${prefix}`]).toBeDefined();
expect(networkConfig[`frontend-${prefix}`]?.aliases).toContain("api");
expect(networkConfig[`frontend-${suffix}`]).toBeDefined();
expect(networkConfig[`frontend-${suffix}`]?.aliases).toContain("api");
expect(actualComposeData.services?.api?.networks).not.toHaveProperty(
"frontend-ash",
@@ -104,19 +104,19 @@ networks:
driver: bridge
`;
test("Add prefix to networks in services (Object with simple networks)", () => {
test("Add suffix to networks in services (Object with simple networks)", () => {
const composeData = load(composeFile3) as ComposeSpecification;
const prefix = generateRandomHash();
const suffix = generateRandomHash();
if (!composeData?.services) {
return;
}
const services = addPrefixToServiceNetworks(composeData.services, prefix);
const services = addSuffixToServiceNetworks(composeData.services, suffix);
const actualComposeData = { ...composeData, services };
expect(actualComposeData.services?.redis?.networks).toHaveProperty(
`backend-${prefix}`,
`backend-${suffix}`,
);
});
@@ -150,35 +150,124 @@ networks:
driver: bridge
`;
test("Add prefix to networks in services (combined case)", () => {
test("Add suffix to networks in services (combined case)", () => {
const composeData = load(composeFileCombined) as ComposeSpecification;
const prefix = generateRandomHash();
const suffix = generateRandomHash();
if (!composeData?.services) {
return;
}
const services = addPrefixToServiceNetworks(composeData.services, prefix);
const services = addSuffixToServiceNetworks(composeData.services, suffix);
const actualComposeData = { ...composeData, services };
// Caso 1: ListOfStrings
expect(actualComposeData.services?.web?.networks).toContain(
`frontend-${prefix}`,
`frontend-${suffix}`,
);
expect(actualComposeData.services?.web?.networks).toContain(
`backend-${prefix}`,
`backend-${suffix}`,
);
// Caso 2: Objeto con aliases
const apiNetworks = actualComposeData.services?.api?.networks as {
[key: string]: unknown;
};
expect(apiNetworks).toHaveProperty(`frontend-${prefix}`);
expect(apiNetworks[`frontend-${prefix}`]).toBeDefined();
expect(apiNetworks).toHaveProperty(`frontend-${suffix}`);
expect(apiNetworks[`frontend-${suffix}`]).toBeDefined();
expect(apiNetworks).not.toHaveProperty("frontend");
// Caso 3: Objeto con redes simples
const redisNetworks = actualComposeData.services?.redis?.networks;
expect(redisNetworks).toHaveProperty(`backend-${prefix}`);
expect(redisNetworks).toHaveProperty(`backend-${suffix}`);
expect(redisNetworks).not.toHaveProperty("backend");
});
const composeFile7 = `
version: "3.8"
services:
web:
image: nginx:latest
networks:
- dokploy-network
`;
test("It shoudn't add suffix to dokploy-network in services", () => {
const composeData = load(composeFile7) as ComposeSpecification;
const suffix = generateRandomHash();
if (!composeData?.services) {
return;
}
const networks = addSuffixToServiceNetworks(composeData.services, suffix);
const service = networks.web;
expect(service).toBeDefined();
expect(service?.networks).toContain("dokploy-network");
});
const composeFile8 = `
version: "3.8"
services:
web:
image: nginx:latest
networks:
- frontend
- backend
- dokploy-network
api:
image: myapi:latest
networks:
frontend:
aliases:
- api
dokploy-network:
aliases:
- api
redis:
image: redis:alpine
networks:
dokploy-network:
db:
image: myapi:latest
networks:
dokploy-network:
aliases:
- apid
`;
test("It shoudn't add suffix to dokploy-network in services multiples cases", () => {
const composeData = load(composeFile8) as ComposeSpecification;
const suffix = generateRandomHash();
if (!composeData?.services) {
return;
}
const networks = addSuffixToServiceNetworks(composeData.services, suffix);
const service = networks.web;
const api = networks.api;
const redis = networks.redis;
const db = networks.db;
const dbNetworks = db?.networks as {
[key: string]: unknown;
};
const apiNetworks = api?.networks as {
[key: string]: unknown;
};
expect(service).toBeDefined();
expect(service?.networks).toContain("dokploy-network");
expect(redis?.networks).toHaveProperty("dokploy-network");
expect(dbNetworks["dokploy-network"]).toBeDefined();
expect(apiNetworks["dokploy-network"]).toBeDefined();
});

View File

@@ -1,9 +1,9 @@
import { generateRandomHash } from "@/server/utils/docker/compose";
import {
addPrefixToAllNetworks,
addPrefixToServiceNetworks,
addSuffixToAllNetworks,
addSuffixToServiceNetworks,
} from "@/server/utils/docker/compose/network";
import { addPrefixToNetworksRoot } from "@/server/utils/docker/compose/network";
import { addSuffixToNetworksRoot } from "@/server/utils/docker/compose/network";
import type { ComposeSpecification } from "@/server/utils/docker/types";
import { load } from "js-yaml";
import { expect, test } from "vitest";
@@ -38,54 +38,54 @@ networks:
driver: bridge
`;
test("Add prefix to networks in services and root (combined case)", () => {
test("Add suffix to networks in services and root (combined case)", () => {
const composeData = load(composeFileCombined) as ComposeSpecification;
const prefix = generateRandomHash();
const suffix = generateRandomHash();
// Prefijo para redes definidas en el root
if (composeData.networks) {
composeData.networks = addPrefixToNetworksRoot(
composeData.networks = addSuffixToNetworksRoot(
composeData.networks,
prefix,
suffix,
);
}
// Prefijo para redes definidas en los servicios
if (composeData.services) {
composeData.services = addPrefixToServiceNetworks(
composeData.services = addSuffixToServiceNetworks(
composeData.services,
prefix,
suffix,
);
}
const actualComposeData = { ...composeData };
// Verificar redes en root
expect(actualComposeData.networks).toHaveProperty(`frontend-${prefix}`);
expect(actualComposeData.networks).toHaveProperty(`backend-${prefix}`);
expect(actualComposeData.networks).toHaveProperty(`frontend-${suffix}`);
expect(actualComposeData.networks).toHaveProperty(`backend-${suffix}`);
expect(actualComposeData.networks).not.toHaveProperty("frontend");
expect(actualComposeData.networks).not.toHaveProperty("backend");
// Caso 1: ListOfStrings
expect(actualComposeData.services?.web?.networks).toContain(
`frontend-${prefix}`,
`frontend-${suffix}`,
);
expect(actualComposeData.services?.web?.networks).toContain(
`backend-${prefix}`,
`backend-${suffix}`,
);
// Caso 2: Objeto con aliases
const apiNetworks = actualComposeData.services?.api?.networks as {
[key: string]: { aliases?: string[] };
};
expect(apiNetworks).toHaveProperty(`frontend-${prefix}`);
expect(apiNetworks?.[`frontend-${prefix}`]?.aliases).toContain("api");
expect(apiNetworks).toHaveProperty(`frontend-${suffix}`);
expect(apiNetworks?.[`frontend-${suffix}`]?.aliases).toContain("api");
expect(apiNetworks).not.toHaveProperty("frontend");
// Caso 3: Objeto con redes simples
const redisNetworks = actualComposeData.services?.redis?.networks;
expect(redisNetworks).toHaveProperty(`backend-${prefix}`);
expect(redisNetworks).toHaveProperty(`backend-${suffix}`);
expect(redisNetworks).not.toHaveProperty("backend");
});
@@ -119,14 +119,14 @@ networks:
driver: bridge
`);
test("Add prefix to networks in compose file", () => {
test("Add suffix to networks in compose file", () => {
const composeData = load(composeFileCombined) as ComposeSpecification;
const prefix = "testhash";
const suffix = "testhash";
if (!composeData?.networks) {
return;
}
const updatedComposeData = addPrefixToAllNetworks(composeData, prefix);
const updatedComposeData = addSuffixToAllNetworks(composeData, suffix);
expect(updatedComposeData).toEqual(expectedComposeFile);
});
@@ -181,11 +181,11 @@ networks:
driver: bridge
`);
test("Add prefix to networks in compose file with external and internal networks", () => {
test("Add suffix to networks in compose file with external and internal networks", () => {
const composeData = load(composeFile2) as ComposeSpecification;
const prefix = "testhash";
const updatedComposeData = addPrefixToAllNetworks(composeData, prefix);
const suffix = "testhash";
const updatedComposeData = addSuffixToAllNetworks(composeData, suffix);
expect(updatedComposeData).toEqual(expectedComposeFile2);
});
@@ -246,11 +246,90 @@ networks:
com.docker.network.bridge.enable_icc: "true"
`);
test("Add prefix to networks in compose file with multiple services and complex network configurations", () => {
test("Add suffix to networks in compose file with multiple services and complex network configurations", () => {
const composeData = load(composeFile3) as ComposeSpecification;
const prefix = "testhash";
const updatedComposeData = addPrefixToAllNetworks(composeData, prefix);
const suffix = "testhash";
const updatedComposeData = addSuffixToAllNetworks(composeData, suffix);
expect(updatedComposeData).toEqual(expectedComposeFile3);
});
const composeFile4 = `
version: "3.8"
services:
app:
image: myapp:latest
networks:
frontend:
aliases:
- app
backend:
dokploy-network:
worker:
image: worker:latest
networks:
- backend
- dokploy-network
networks:
frontend:
driver: bridge
attachable: true
backend:
driver: bridge
driver_opts:
com.docker.network.bridge.enable_icc: "true"
dokploy-network:
driver: bridge
`;
const expectedComposeFile4 = load(`
version: "3.8"
services:
app:
image: myapp:latest
networks:
frontend-testhash:
aliases:
- app
backend-testhash:
dokploy-network:
worker:
image: worker:latest
networks:
- backend-testhash
- dokploy-network
networks:
frontend-testhash:
driver: bridge
attachable: true
backend-testhash:
driver: bridge
driver_opts:
com.docker.network.bridge.enable_icc: "true"
dokploy-network:
driver: bridge
`);
test("Expect don't add suffix to dokploy-network in compose file with multiple services and complex network configurations", () => {
const composeData = load(composeFile4) as ComposeSpecification;
const suffix = "testhash";
const updatedComposeData = addSuffixToAllNetworks(composeData, suffix);
console.log(updatedComposeData);
expect(updatedComposeData).toEqual(expectedComposeFile4);
});

View File

@@ -1,5 +1,5 @@
import { generateRandomHash } from "@/server/utils/docker/compose";
import { addPrefixToSecretsRoot } from "@/server/utils/docker/compose/secrets";
import { addSuffixToSecretsRoot } from "@/server/utils/docker/compose/secrets";
import type { ComposeSpecification } from "@/server/utils/docker/types";
import { dump, load } from "js-yaml";
import { expect, test } from "vitest";
@@ -23,18 +23,18 @@ secrets:
file: ./db_password.txt
`;
test("Add prefix to secrets in root property", () => {
test("Add suffix to secrets in root property", () => {
const composeData = load(composeFileSecretsRoot) as ComposeSpecification;
const prefix = generateRandomHash();
const suffix = generateRandomHash();
if (!composeData?.secrets) {
return;
}
const secrets = addPrefixToSecretsRoot(composeData.secrets, prefix);
const secrets = addSuffixToSecretsRoot(composeData.secrets, suffix);
expect(secrets).toBeDefined();
if (secrets) {
for (const secretKey of Object.keys(secrets)) {
expect(secretKey).toContain(`-${prefix}`);
expect(secretKey).toContain(`-${suffix}`);
expect(secrets[secretKey]).toBeDefined();
}
}
@@ -52,19 +52,19 @@ secrets:
file: ./api_key.txt
`;
test("Add prefix to secrets in root property (Test 1)", () => {
test("Add suffix to secrets in root property (Test 1)", () => {
const composeData = load(composeFileSecretsRoot1) as ComposeSpecification;
const prefix = generateRandomHash();
const suffix = generateRandomHash();
if (!composeData?.secrets) {
return;
}
const secrets = addPrefixToSecretsRoot(composeData.secrets, prefix);
const secrets = addSuffixToSecretsRoot(composeData.secrets, suffix);
expect(secrets).toBeDefined();
if (secrets) {
for (const secretKey of Object.keys(secrets)) {
expect(secretKey).toContain(`-${prefix}`);
expect(secretKey).toContain(`-${suffix}`);
expect(secrets[secretKey]).toBeDefined();
}
}
@@ -84,19 +84,19 @@ secrets:
external: true
`;
test("Add prefix to secrets in root property (Test 2)", () => {
test("Add suffix to secrets in root property (Test 2)", () => {
const composeData = load(composeFileSecretsRoot2) as ComposeSpecification;
const prefix = generateRandomHash();
const suffix = generateRandomHash();
if (!composeData?.secrets) {
return;
}
const secrets = addPrefixToSecretsRoot(composeData.secrets, prefix);
const secrets = addSuffixToSecretsRoot(composeData.secrets, suffix);
expect(secrets).toBeDefined();
if (secrets) {
for (const secretKey of Object.keys(secrets)) {
expect(secretKey).toContain(`-${prefix}`);
expect(secretKey).toContain(`-${suffix}`);
expect(secrets[secretKey]).toBeDefined();
}
}

View File

@@ -1,5 +1,5 @@
import { generateRandomHash } from "@/server/utils/docker/compose";
import { addPrefixToSecretsInServices } from "@/server/utils/docker/compose/secrets";
import { addSuffixToSecretsInServices } from "@/server/utils/docker/compose/secrets";
import type { ComposeSpecification } from "@/server/utils/docker/types";
import { load } from "js-yaml";
import { expect, test } from "vitest";
@@ -18,22 +18,22 @@ secrets:
file: ./db_password.txt
`;
test("Add prefix to secrets in services", () => {
test("Add suffix to secrets in services", () => {
const composeData = load(composeFileSecretsServices) as ComposeSpecification;
const prefix = generateRandomHash();
const suffix = generateRandomHash();
if (!composeData.services) {
return;
}
const updatedComposeData = addPrefixToSecretsInServices(
const updatedComposeData = addSuffixToSecretsInServices(
composeData.services,
prefix,
suffix,
);
const actualComposeData = { ...composeData, services: updatedComposeData };
expect(actualComposeData.services?.db?.secrets).toContain(
`db_password-${prefix}`,
`db_password-${suffix}`,
);
});
@@ -51,22 +51,22 @@ secrets:
file: ./app_secret.txt
`;
test("Add prefix to secrets in services (Test 1)", () => {
test("Add suffix to secrets in services (Test 1)", () => {
const composeData = load(composeFileSecretsServices1) as ComposeSpecification;
const prefix = generateRandomHash();
const suffix = generateRandomHash();
if (!composeData.services) {
return;
}
const updatedComposeData = addPrefixToSecretsInServices(
const updatedComposeData = addSuffixToSecretsInServices(
composeData.services,
prefix,
suffix,
);
const actualComposeData = { ...composeData, services: updatedComposeData };
expect(actualComposeData.services?.app?.secrets).toContain(
`app_secret-${prefix}`,
`app_secret-${suffix}`,
);
});
@@ -90,24 +90,24 @@ secrets:
file: ./frontend_secret.txt
`;
test("Add prefix to secrets in services (Test 2)", () => {
test("Add suffix to secrets in services (Test 2)", () => {
const composeData = load(composeFileSecretsServices2) as ComposeSpecification;
const prefix = generateRandomHash();
const suffix = generateRandomHash();
if (!composeData.services) {
return;
}
const updatedComposeData = addPrefixToSecretsInServices(
const updatedComposeData = addSuffixToSecretsInServices(
composeData.services,
prefix,
suffix,
);
const actualComposeData = { ...composeData, services: updatedComposeData };
expect(actualComposeData.services?.backend?.secrets).toContain(
`backend_secret-${prefix}`,
`backend_secret-${suffix}`,
);
expect(actualComposeData.services?.frontend?.secrets).toContain(
`frontend_secret-${prefix}`,
`frontend_secret-${suffix}`,
);
});

View File

@@ -1,4 +1,4 @@
import { addPrefixToAllSecrets } from "@/server/utils/docker/compose/secrets";
import { addSuffixToAllSecrets } from "@/server/utils/docker/compose/secrets";
import type { ComposeSpecification } from "@/server/utils/docker/types";
import { load } from "js-yaml";
import { expect, test } from "vitest";
@@ -47,11 +47,11 @@ secrets:
file: ./app_secret.txt
`) as ComposeSpecification;
test("Add prefix to all secrets", () => {
test("Add suffix to all secrets", () => {
const composeData = load(composeFileCombinedSecrets) as ComposeSpecification;
const prefix = "testhash";
const suffix = "testhash";
const updatedComposeData = addPrefixToAllSecrets(composeData, prefix);
const updatedComposeData = addSuffixToAllSecrets(composeData, suffix);
expect(updatedComposeData).toEqual(expectedComposeFileCombinedSecrets);
});
@@ -98,11 +98,11 @@ secrets:
file: ./cache_secret.txt
`) as ComposeSpecification;
test("Add prefix to all secrets (3rd Case)", () => {
test("Add suffix to all secrets (3rd Case)", () => {
const composeData = load(composeFileCombinedSecrets3) as ComposeSpecification;
const prefix = "testhash";
const suffix = "testhash";
const updatedComposeData = addPrefixToAllSecrets(composeData, prefix);
const updatedComposeData = addSuffixToAllSecrets(composeData, suffix);
expect(updatedComposeData).toEqual(expectedComposeFileCombinedSecrets3);
});
@@ -149,11 +149,11 @@ secrets:
file: ./db_password.txt
`) as ComposeSpecification;
test("Add prefix to all secrets (4th Case)", () => {
test("Add suffix to all secrets (4th Case)", () => {
const composeData = load(composeFileCombinedSecrets4) as ComposeSpecification;
const prefix = "testhash";
const suffix = "testhash";
const updatedComposeData = addPrefixToAllSecrets(composeData, prefix);
const updatedComposeData = addSuffixToAllSecrets(composeData, suffix);
expect(updatedComposeData).toEqual(expectedComposeFileCombinedSecrets4);
});

View File

@@ -1,5 +1,5 @@
import { generateRandomHash } from "@/server/utils/docker/compose";
import { addPrefixToServiceNames } from "@/server/utils/docker/compose/service";
import { addSuffixToServiceNames } from "@/server/utils/docker/compose/service";
import type { ComposeSpecification } from "@/server/utils/docker/types";
import { load } from "js-yaml";
import { expect, test } from "vitest";
@@ -27,33 +27,33 @@ test("Generate random hash with 8 characters", () => {
expect(hash.length).toBe(8);
});
test("Add prefix to service names with container_name in compose file", () => {
test("Add suffix to service names with container_name in compose file", () => {
const composeData = load(composeFile) as ComposeSpecification;
const prefix = generateRandomHash();
const suffix = generateRandomHash();
if (!composeData.services) {
return;
}
const updatedComposeData = addPrefixToServiceNames(
const updatedComposeData = addSuffixToServiceNames(
composeData.services,
prefix,
suffix,
);
const actualComposeData = { ...composeData, services: updatedComposeData };
// Verificar que el nombre del contenedor ha cambiado correctamente
expect(actualComposeData.services?.[`web-${prefix}`]?.container_name).toBe(
`web_container-${prefix}`,
expect(actualComposeData.services?.[`web-${suffix}`]?.container_name).toBe(
`web_container-${suffix}`,
);
// Verificar que la nueva clave del servicio tiene el prefijo y la vieja clave no existe
expect(actualComposeData.services).toHaveProperty(`web-${prefix}`);
expect(actualComposeData.services).toHaveProperty(`web-${suffix}`);
expect(actualComposeData.services).not.toHaveProperty("web");
// Verificar que la configuración de la imagen sigue igual
expect(actualComposeData.services?.[`web-${prefix}`]?.image).toBe(
expect(actualComposeData.services?.[`web-${suffix}`]?.image).toBe(
"nginx:latest",
);
expect(actualComposeData.services?.[`api-${prefix}`]?.image).toBe(
expect(actualComposeData.services?.[`api-${suffix}`]?.image).toBe(
"myapi:latest",
);
});

View File

@@ -1,5 +1,5 @@
import { generateRandomHash } from "@/server/utils/docker/compose";
import { addPrefixToServiceNames } from "@/server/utils/docker/compose/service";
import { addSuffixToServiceNames } from "@/server/utils/docker/compose/service";
import type { ComposeSpecification } from "@/server/utils/docker/types";
import { load } from "js-yaml";
import { expect, test } from "vitest";
@@ -32,49 +32,49 @@ networks:
driver: bridge
`;
test("Add prefix to service names with depends_on (array) in compose file", () => {
test("Add suffix to service names with depends_on (array) in compose file", () => {
const composeData = load(composeFile4) as ComposeSpecification;
const prefix = generateRandomHash();
const suffix = generateRandomHash();
if (!composeData.services) {
return;
}
const updatedComposeData = addPrefixToServiceNames(
const updatedComposeData = addSuffixToServiceNames(
composeData.services,
prefix,
suffix,
);
const actualComposeData = { ...composeData, services: updatedComposeData };
// Verificar que la nueva clave del servicio tiene el prefijo y la vieja clave no existe
expect(actualComposeData.services).toHaveProperty(`web-${prefix}`);
expect(actualComposeData.services).toHaveProperty(`web-${suffix}`);
expect(actualComposeData.services).not.toHaveProperty("web");
// Verificar que la configuración de la imagen sigue igual
expect(actualComposeData.services?.[`web-${prefix}`]?.image).toBe(
expect(actualComposeData.services?.[`web-${suffix}`]?.image).toBe(
"nginx:latest",
);
expect(actualComposeData.services?.[`api-${prefix}`]?.image).toBe(
expect(actualComposeData.services?.[`api-${suffix}`]?.image).toBe(
"myapi:latest",
);
// Verificar que los nombres en depends_on tienen el prefijo
expect(actualComposeData.services?.[`web-${prefix}`]?.depends_on).toContain(
`db-${prefix}`,
expect(actualComposeData.services?.[`web-${suffix}`]?.depends_on).toContain(
`db-${suffix}`,
);
expect(actualComposeData.services?.[`web-${prefix}`]?.depends_on).toContain(
`api-${prefix}`,
expect(actualComposeData.services?.[`web-${suffix}`]?.depends_on).toContain(
`api-${suffix}`,
);
// Verificar que los servicios `db` y `api` también tienen el prefijo
expect(actualComposeData.services).toHaveProperty(`db-${prefix}`);
expect(actualComposeData.services).toHaveProperty(`db-${suffix}`);
expect(actualComposeData.services).not.toHaveProperty("db");
expect(actualComposeData.services?.[`db-${prefix}`]?.image).toBe(
expect(actualComposeData.services?.[`db-${suffix}`]?.image).toBe(
"postgres:latest",
);
expect(actualComposeData.services).toHaveProperty(`api-${prefix}`);
expect(actualComposeData.services).toHaveProperty(`api-${suffix}`);
expect(actualComposeData.services).not.toHaveProperty("api");
expect(actualComposeData.services?.[`api-${prefix}`]?.image).toBe(
expect(actualComposeData.services?.[`api-${suffix}`]?.image).toBe(
"myapi:latest",
);
});
@@ -102,49 +102,49 @@ networks:
driver: bridge
`;
test("Add prefix to service names with depends_on (object) in compose file", () => {
test("Add suffix to service names with depends_on (object) in compose file", () => {
const composeData = load(composeFile5) as ComposeSpecification;
const prefix = generateRandomHash();
const suffix = generateRandomHash();
if (!composeData.services) {
return;
}
const updatedComposeData = addPrefixToServiceNames(
const updatedComposeData = addSuffixToServiceNames(
composeData.services,
prefix,
suffix,
);
const actualComposeData = { ...composeData, services: updatedComposeData };
// Verificar que la nueva clave del servicio tiene el prefijo y la vieja clave no existe
expect(actualComposeData.services).toHaveProperty(`web-${prefix}`);
expect(actualComposeData.services).toHaveProperty(`web-${suffix}`);
expect(actualComposeData.services).not.toHaveProperty("web");
// Verificar que la configuración de la imagen sigue igual
expect(actualComposeData.services?.[`web-${prefix}`]?.image).toBe(
expect(actualComposeData.services?.[`web-${suffix}`]?.image).toBe(
"nginx:latest",
);
expect(actualComposeData.services?.[`api-${prefix}`]?.image).toBe(
expect(actualComposeData.services?.[`api-${suffix}`]?.image).toBe(
"myapi:latest",
);
// Verificar que los nombres en depends_on tienen el prefijo
const webDependsOn = actualComposeData.services?.[`web-${prefix}`]
const webDependsOn = actualComposeData.services?.[`web-${suffix}`]
?.depends_on as Record<string, any>;
expect(webDependsOn).toHaveProperty(`db-${prefix}`);
expect(webDependsOn).toHaveProperty(`api-${prefix}`);
expect(webDependsOn[`db-${prefix}`].condition).toBe("service_healthy");
expect(webDependsOn[`api-${prefix}`].condition).toBe("service_started");
expect(webDependsOn).toHaveProperty(`db-${suffix}`);
expect(webDependsOn).toHaveProperty(`api-${suffix}`);
expect(webDependsOn[`db-${suffix}`].condition).toBe("service_healthy");
expect(webDependsOn[`api-${suffix}`].condition).toBe("service_started");
// Verificar que los servicios `db` y `api` también tienen el prefijo
expect(actualComposeData.services).toHaveProperty(`db-${prefix}`);
expect(actualComposeData.services).toHaveProperty(`db-${suffix}`);
expect(actualComposeData.services).not.toHaveProperty("db");
expect(actualComposeData.services?.[`db-${prefix}`]?.image).toBe(
expect(actualComposeData.services?.[`db-${suffix}`]?.image).toBe(
"postgres:latest",
);
expect(actualComposeData.services).toHaveProperty(`api-${prefix}`);
expect(actualComposeData.services).toHaveProperty(`api-${suffix}`);
expect(actualComposeData.services).not.toHaveProperty("api");
expect(actualComposeData.services?.[`api-${prefix}`]?.image).toBe(
expect(actualComposeData.services?.[`api-${suffix}`]?.image).toBe(
"myapi:latest",
);
});

View File

@@ -1,5 +1,5 @@
import { generateRandomHash } from "@/server/utils/docker/compose";
import { addPrefixToServiceNames } from "@/server/utils/docker/compose/service";
import { addSuffixToServiceNames } from "@/server/utils/docker/compose/service";
import type { ComposeSpecification } from "@/server/utils/docker/types";
import { load } from "js-yaml";
import { expect, test } from "vitest";
@@ -30,41 +30,41 @@ networks:
driver: bridge
`;
test("Add prefix to service names with extends (string) in compose file", () => {
test("Add suffix to service names with extends (string) in compose file", () => {
const composeData = load(composeFile6) as ComposeSpecification;
const prefix = generateRandomHash();
const suffix = generateRandomHash();
if (!composeData.services) {
return;
}
const updatedComposeData = addPrefixToServiceNames(
const updatedComposeData = addSuffixToServiceNames(
composeData.services,
prefix,
suffix,
);
const actualComposeData = { ...composeData, services: updatedComposeData };
// Verificar que la nueva clave del servicio tiene el prefijo y la vieja clave no existe
expect(actualComposeData.services).toHaveProperty(`web-${prefix}`);
expect(actualComposeData.services).toHaveProperty(`web-${suffix}`);
expect(actualComposeData.services).not.toHaveProperty("web");
// Verificar que la configuración de la imagen sigue igual
expect(actualComposeData.services?.[`web-${prefix}`]?.image).toBe(
expect(actualComposeData.services?.[`web-${suffix}`]?.image).toBe(
"nginx:latest",
);
expect(actualComposeData.services?.[`api-${prefix}`]?.image).toBe(
expect(actualComposeData.services?.[`api-${suffix}`]?.image).toBe(
"myapi:latest",
);
// Verificar que el nombre en extends tiene el prefijo
expect(actualComposeData.services?.[`web-${prefix}`]?.extends).toBe(
`base_service-${prefix}`,
expect(actualComposeData.services?.[`web-${suffix}`]?.extends).toBe(
`base_service-${suffix}`,
);
// Verificar que el servicio `base_service` también tiene el prefijo
expect(actualComposeData.services).toHaveProperty(`base_service-${prefix}`);
expect(actualComposeData.services).toHaveProperty(`base_service-${suffix}`);
expect(actualComposeData.services).not.toHaveProperty("base_service");
expect(actualComposeData.services?.[`base_service-${prefix}`]?.image).toBe(
expect(actualComposeData.services?.[`base_service-${suffix}`]?.image).toBe(
"base:latest",
);
});
@@ -90,42 +90,42 @@ networks:
driver: bridge
`;
test("Add prefix to service names with extends (object) in compose file", () => {
test("Add suffix to service names with extends (object) in compose file", () => {
const composeData = load(composeFile7) as ComposeSpecification;
const prefix = generateRandomHash();
const suffix = generateRandomHash();
if (!composeData.services) {
return;
}
const updatedComposeData = addPrefixToServiceNames(
const updatedComposeData = addSuffixToServiceNames(
composeData.services,
prefix,
suffix,
);
const actualComposeData = { ...composeData, services: updatedComposeData };
// Verificar que la nueva clave del servicio tiene el prefijo y la vieja clave no existe
expect(actualComposeData.services).toHaveProperty(`web-${prefix}`);
expect(actualComposeData.services).toHaveProperty(`web-${suffix}`);
expect(actualComposeData.services).not.toHaveProperty("web");
// Verificar que la configuración de la imagen sigue igual
expect(actualComposeData.services?.[`web-${prefix}`]?.image).toBe(
expect(actualComposeData.services?.[`web-${suffix}`]?.image).toBe(
"nginx:latest",
);
expect(actualComposeData.services?.[`api-${prefix}`]?.image).toBe(
expect(actualComposeData.services?.[`api-${suffix}`]?.image).toBe(
"myapi:latest",
);
// Verificar que el nombre en extends.service tiene el prefijo
const webExtends = actualComposeData.services?.[`web-${prefix}`]?.extends;
const webExtends = actualComposeData.services?.[`web-${suffix}`]?.extends;
if (typeof webExtends !== "string") {
expect(webExtends?.service).toBe(`base_service-${prefix}`);
expect(webExtends?.service).toBe(`base_service-${suffix}`);
}
// Verificar que el servicio `base_service` también tiene el prefijo
expect(actualComposeData.services).toHaveProperty(`base_service-${prefix}`);
expect(actualComposeData.services).toHaveProperty(`base_service-${suffix}`);
expect(actualComposeData.services).not.toHaveProperty("base_service");
expect(actualComposeData.services?.[`base_service-${prefix}`]?.image).toBe(
expect(actualComposeData.services?.[`base_service-${suffix}`]?.image).toBe(
"base:latest",
);
});

View File

@@ -1,5 +1,5 @@
import { generateRandomHash } from "@/server/utils/docker/compose";
import { addPrefixToServiceNames } from "@/server/utils/docker/compose/service";
import { addSuffixToServiceNames } from "@/server/utils/docker/compose/service";
import type { ComposeSpecification } from "@/server/utils/docker/types";
import { load } from "js-yaml";
import { expect, test } from "vitest";
@@ -31,46 +31,46 @@ networks:
driver: bridge
`;
test("Add prefix to service names with links in compose file", () => {
test("Add suffix to service names with links in compose file", () => {
const composeData = load(composeFile2) as ComposeSpecification;
const prefix = generateRandomHash();
const suffix = generateRandomHash();
if (!composeData.services) {
return;
}
const updatedComposeData = addPrefixToServiceNames(
const updatedComposeData = addSuffixToServiceNames(
composeData.services,
prefix,
suffix,
);
const actualComposeData = { ...composeData, services: updatedComposeData };
// Verificar que la nueva clave del servicio tiene el prefijo y la vieja clave no existe
expect(actualComposeData.services).toHaveProperty(`web-${prefix}`);
expect(actualComposeData.services).toHaveProperty(`web-${suffix}`);
expect(actualComposeData.services).not.toHaveProperty("web");
// Verificar que la configuración de la imagen sigue igual
expect(actualComposeData.services?.[`web-${prefix}`]?.image).toBe(
expect(actualComposeData.services?.[`web-${suffix}`]?.image).toBe(
"nginx:latest",
);
expect(actualComposeData.services?.[`api-${prefix}`]?.image).toBe(
expect(actualComposeData.services?.[`api-${suffix}`]?.image).toBe(
"myapi:latest",
);
// Verificar que los nombres en links tienen el prefijo
expect(actualComposeData.services?.[`web-${prefix}`]?.links).toContain(
`db-${prefix}`,
expect(actualComposeData.services?.[`web-${suffix}`]?.links).toContain(
`db-${suffix}`,
);
// Verificar que los servicios `db` y `api` también tienen el prefijo
expect(actualComposeData.services).toHaveProperty(`db-${prefix}`);
expect(actualComposeData.services).toHaveProperty(`db-${suffix}`);
expect(actualComposeData.services).not.toHaveProperty("db");
expect(actualComposeData.services?.[`db-${prefix}`]?.image).toBe(
expect(actualComposeData.services?.[`db-${suffix}`]?.image).toBe(
"postgres:latest",
);
expect(actualComposeData.services).toHaveProperty(`api-${prefix}`);
expect(actualComposeData.services).toHaveProperty(`api-${suffix}`);
expect(actualComposeData.services).not.toHaveProperty("api");
expect(actualComposeData.services?.[`api-${prefix}`]?.image).toBe(
expect(actualComposeData.services?.[`api-${suffix}`]?.image).toBe(
"myapi:latest",
);
});

View File

@@ -1,5 +1,5 @@
import { generateRandomHash } from "@/server/utils/docker/compose";
import { addPrefixToServiceNames } from "@/server/utils/docker/compose/service";
import { addSuffixToServiceNames } from "@/server/utils/docker/compose/service";
import type { ComposeSpecification } from "@/server/utils/docker/types";
import { load } from "js-yaml";
import { expect, test } from "vitest";
@@ -26,23 +26,23 @@ networks:
driver: bridge
`;
test("Add prefix to service names in compose file", () => {
test("Add suffix to service names in compose file", () => {
const composeData = load(composeFile) as ComposeSpecification;
const prefix = generateRandomHash();
const suffix = generateRandomHash();
if (!composeData.services) {
return;
}
const updatedComposeData = addPrefixToServiceNames(
const updatedComposeData = addSuffixToServiceNames(
composeData.services,
prefix,
suffix,
);
const actualComposeData = { ...composeData, services: updatedComposeData };
// Verificar que los nombres de los servicios han cambiado correctamente
expect(actualComposeData.services).toHaveProperty(`web-${prefix}`);
expect(actualComposeData.services).toHaveProperty(`api-${prefix}`);
expect(actualComposeData.services).toHaveProperty(`web-${suffix}`);
expect(actualComposeData.services).toHaveProperty(`api-${suffix}`);
// Verificar que las claves originales no existen
expect(actualComposeData.services).not.toHaveProperty("web");
expect(actualComposeData.services).not.toHaveProperty("api");

View File

@@ -1,6 +1,6 @@
import {
addPrefixToAllServiceNames,
addPrefixToServiceNames,
addSuffixToAllServiceNames,
addSuffixToServiceNames,
} from "@/server/utils/docker/compose/service";
import type { ComposeSpecification } from "@/server/utils/docker/types";
import { load } from "js-yaml";
@@ -70,17 +70,17 @@ networks:
driver: bridge
`);
test("Add prefix to all service names in compose file", () => {
test("Add suffix to all service names in compose file", () => {
const composeData = load(composeFileCombinedAllCases) as ComposeSpecification;
const prefix = "testhash";
const suffix = "testhash";
if (!composeData.services) {
return;
}
const updatedComposeData = addPrefixToServiceNames(
const updatedComposeData = addSuffixToServiceNames(
composeData.services,
prefix,
suffix,
);
const actualComposeData = { ...composeData, services: updatedComposeData };
@@ -175,11 +175,11 @@ networks:
driver: bridge
`) as ComposeSpecification;
test("Add prefix to all service names in compose file 1", () => {
test("Add suffix to all service names in compose file 1", () => {
const composeData = load(composeFile1) as ComposeSpecification;
const prefix = "testhash";
const suffix = "testhash";
const updatedComposeData = addPrefixToAllServiceNames(composeData, prefix);
const updatedComposeData = addSuffixToAllServiceNames(composeData, suffix);
expect(updatedComposeData).toEqual(expectedComposeFile1);
});
@@ -270,11 +270,11 @@ networks:
driver: bridge
`) as ComposeSpecification;
test("Add prefix to all service names in compose file 2", () => {
test("Add suffix to all service names in compose file 2", () => {
const composeData = load(composeFile2) as ComposeSpecification;
const prefix = "testhash";
const suffix = "testhash";
const updatedComposeData = addPrefixToAllServiceNames(composeData, prefix);
const updatedComposeData = addSuffixToAllServiceNames(composeData, suffix);
expect(updatedComposeData).toEqual(expectedComposeFile2);
});
@@ -365,11 +365,11 @@ networks:
driver: bridge
`) as ComposeSpecification;
test("Add prefix to all service names in compose file 3", () => {
test("Add suffix to all service names in compose file 3", () => {
const composeData = load(composeFile3) as ComposeSpecification;
const prefix = "testhash";
const suffix = "testhash";
const updatedComposeData = addPrefixToAllServiceNames(composeData, prefix);
const updatedComposeData = addSuffixToAllServiceNames(composeData, suffix);
expect(updatedComposeData).toEqual(expectedComposeFile3);
});

View File

@@ -1,5 +1,5 @@
import { generateRandomHash } from "@/server/utils/docker/compose";
import { addPrefixToServiceNames } from "@/server/utils/docker/compose/service";
import { addSuffixToServiceNames } from "@/server/utils/docker/compose/service";
import type { ComposeSpecification } from "@/server/utils/docker/types";
import { load } from "js-yaml";
import { expect, test } from "vitest";
@@ -35,44 +35,44 @@ networks:
driver: bridge
`;
test("Add prefix to service names with volumes_from in compose file", () => {
test("Add suffix to service names with volumes_from in compose file", () => {
const composeData = load(composeFile3) as ComposeSpecification;
const prefix = generateRandomHash();
const suffix = generateRandomHash();
if (!composeData.services) {
return;
}
const updatedComposeData = addPrefixToServiceNames(
const updatedComposeData = addSuffixToServiceNames(
composeData.services,
prefix,
suffix,
);
const actualComposeData = { ...composeData, services: updatedComposeData };
// Verificar que la nueva clave del servicio tiene el prefijo y la vieja clave no existe
expect(actualComposeData.services).toHaveProperty(`web-${prefix}`);
expect(actualComposeData.services).toHaveProperty(`web-${suffix}`);
expect(actualComposeData.services).not.toHaveProperty("web");
// Verificar que la configuración de la imagen sigue igual
expect(actualComposeData.services?.[`web-${prefix}`]?.image).toBe(
expect(actualComposeData.services?.[`web-${suffix}`]?.image).toBe(
"nginx:latest",
);
expect(actualComposeData.services?.[`api-${prefix}`]?.image).toBe(
expect(actualComposeData.services?.[`api-${suffix}`]?.image).toBe(
"myapi:latest",
);
// Verificar que los nombres en volumes_from tienen el prefijo
expect(actualComposeData.services?.[`web-${prefix}`]?.volumes_from).toContain(
`shared-${prefix}`,
expect(actualComposeData.services?.[`web-${suffix}`]?.volumes_from).toContain(
`shared-${suffix}`,
);
expect(actualComposeData.services?.[`api-${prefix}`]?.volumes_from).toContain(
`shared-${prefix}`,
expect(actualComposeData.services?.[`api-${suffix}`]?.volumes_from).toContain(
`shared-${suffix}`,
);
// Verificar que el servicio shared también tiene el prefijo
expect(actualComposeData.services).toHaveProperty(`shared-${prefix}`);
expect(actualComposeData.services).toHaveProperty(`shared-${suffix}`);
expect(actualComposeData.services).not.toHaveProperty("shared");
expect(actualComposeData.services?.[`shared-${prefix}`]?.image).toBe(
expect(actualComposeData.services?.[`shared-${suffix}`]?.image).toBe(
"busybox",
);
});

View File

@@ -1,7 +1,7 @@
import { generateRandomHash } from "@/server/utils/docker/compose";
import {
addPrefixToAllVolumes,
addPrefixToVolumesRoot,
addSuffixToAllVolumes,
addSuffixToVolumesRoot,
} from "@/server/utils/docker/compose/volume";
import type { ComposeSpecification } from "@/server/utils/docker/types";
import { load } from "js-yaml";
@@ -142,15 +142,15 @@ test("Generate random hash with 8 characters", () => {
// Docker compose needs unique names for services, volumes, networks and containers
// So base on a input which is a dockercompose file, it should replace the name with a hash and return a new dockercompose file
test("Add prefix to volumes root property", () => {
test("Add suffix to volumes root property", () => {
const composeData = load(composeFile) as ComposeSpecification;
const prefix = generateRandomHash();
const suffix = generateRandomHash();
if (!composeData?.volumes) {
return;
}
const volumes = addPrefixToVolumesRoot(composeData.volumes, prefix);
const volumes = addSuffixToVolumesRoot(composeData.volumes, suffix);
// {
// 'db-data-af045046': { driver: 'local' },
@@ -160,15 +160,15 @@ test("Add prefix to volumes root property", () => {
expect(volumes).toBeDefined();
for (const volumeKey of Object.keys(volumes)) {
expect(volumeKey).toContain(`-${prefix}`);
expect(volumeKey).toContain(`-${suffix}`);
}
});
test("Expect to change the prefix in all the possible places", () => {
test("Expect to change the suffix in all the possible places", () => {
const composeData = load(composeFile) as ComposeSpecification;
const prefix = "testhash";
const suffix = "testhash";
const updatedComposeData = addPrefixToAllVolumes(composeData, prefix);
const updatedComposeData = addSuffixToAllVolumes(composeData, suffix);
expect(updatedComposeData).toEqual(expectedDockerCompose);
});
@@ -217,11 +217,11 @@ volumes:
mongo-data-testhash:
`) as ComposeSpecification;
test("Expect to change the prefix in all the possible places (2 Try)", () => {
test("Expect to change the suffix in all the possible places (2 Try)", () => {
const composeData = load(composeFile2) as ComposeSpecification;
const prefix = "testhash";
const suffix = "testhash";
const updatedComposeData = addPrefixToAllVolumes(composeData, prefix);
const updatedComposeData = addSuffixToAllVolumes(composeData, suffix);
expect(updatedComposeData).toEqual(expectedDockerCompose2);
});
@@ -270,11 +270,11 @@ volumes:
mongo-data-testhash:
`) as ComposeSpecification;
test("Expect to change the prefix in all the possible places (3 Try)", () => {
test("Expect to change the suffix in all the possible places (3 Try)", () => {
const composeData = load(composeFile3) as ComposeSpecification;
const prefix = "testhash";
const suffix = "testhash";
const updatedComposeData = addPrefixToAllVolumes(composeData, prefix);
const updatedComposeData = addSuffixToAllVolumes(composeData, suffix);
expect(updatedComposeData).toEqual(expectedDockerCompose3);
});
@@ -1011,11 +1011,11 @@ volumes:
db-config-testhash:
`) as ComposeSpecification;
test("Expect to change the prefix in all the possible places (4 Try)", () => {
test("Expect to change the suffix in all the possible places (4 Try)", () => {
const composeData = load(composeFileComplex) as ComposeSpecification;
const prefix = "testhash";
const suffix = "testhash";
const updatedComposeData = addPrefixToAllVolumes(composeData, prefix);
const updatedComposeData = addSuffixToAllVolumes(composeData, suffix);
expect(updatedComposeData).toEqual(expectedDockerComposeComplex);
});
@@ -1110,11 +1110,11 @@ volumes:
db-data-testhash:
`) as ComposeSpecification;
test("Expect to change the prefix in all the possible places (5 Try)", () => {
test("Expect to change the suffix in all the possible places (5 Try)", () => {
const composeData = load(composeFileExample1) as ComposeSpecification;
const prefix = "testhash";
const suffix = "testhash";
const updatedComposeData = addPrefixToAllVolumes(composeData, prefix);
const updatedComposeData = addSuffixToAllVolumes(composeData, suffix);
expect(updatedComposeData).toEqual(expectedDockerComposeExample1);
});

View File

@@ -1,5 +1,5 @@
import { generateRandomHash } from "@/server/utils/docker/compose";
import { addPrefixToVolumesRoot } from "@/server/utils/docker/compose/volume";
import { addSuffixToVolumesRoot } from "@/server/utils/docker/compose/volume";
import type { ComposeSpecification } from "@/server/utils/docker/types";
import { load } from "js-yaml";
import { expect, test } from "vitest";
@@ -29,18 +29,18 @@ test("Generate random hash with 8 characters", () => {
expect(hash.length).toBe(8);
});
test("Add prefix to volumes in root property", () => {
test("Add suffix to volumes in root property", () => {
const composeData = load(composeFile) as ComposeSpecification;
const prefix = generateRandomHash();
const suffix = generateRandomHash();
if (!composeData?.volumes) {
return;
}
const volumes = addPrefixToVolumesRoot(composeData.volumes, prefix);
const volumes = addSuffixToVolumesRoot(composeData.volumes, suffix);
expect(volumes).toBeDefined();
for (const volumeKey of Object.keys(volumes)) {
expect(volumeKey).toContain(`-${prefix}`);
expect(volumeKey).toContain(`-${suffix}`);
expect(volumes[volumeKey]).toBeDefined();
}
});
@@ -67,18 +67,18 @@ networks:
driver: bridge
`;
test("Add prefix to volumes in root property (Case 2)", () => {
test("Add suffix to volumes in root property (Case 2)", () => {
const composeData = load(composeFile2) as ComposeSpecification;
const prefix = generateRandomHash();
const suffix = generateRandomHash();
if (!composeData?.volumes) {
return;
}
const volumes = addPrefixToVolumesRoot(composeData.volumes, prefix);
const volumes = addSuffixToVolumesRoot(composeData.volumes, suffix);
expect(volumes).toBeDefined();
for (const volumeKey of Object.keys(volumes)) {
expect(volumeKey).toContain(`-${prefix}`);
expect(volumeKey).toContain(`-${suffix}`);
expect(volumes[volumeKey]).toBeDefined();
}
});
@@ -101,19 +101,19 @@ networks:
driver: bridge
`;
test("Add prefix to volumes in root property (Case 3)", () => {
test("Add suffix to volumes in root property (Case 3)", () => {
const composeData = load(composeFile3) as ComposeSpecification;
const prefix = generateRandomHash();
const suffix = generateRandomHash();
if (!composeData?.volumes) {
return;
}
const volumes = addPrefixToVolumesRoot(composeData.volumes, prefix);
const volumes = addSuffixToVolumesRoot(composeData.volumes, suffix);
expect(volumes).toBeDefined();
for (const volumeKey of Object.keys(volumes)) {
expect(volumeKey).toContain(`-${prefix}`);
expect(volumeKey).toContain(`-${suffix}`);
expect(volumes[volumeKey]).toBeDefined();
}
});
@@ -179,15 +179,15 @@ volumes:
`) as ComposeSpecification;
test("Add prefix to volumes in root property", () => {
test("Add suffix to volumes in root property", () => {
const composeData = load(composeFile4) as ComposeSpecification;
const prefix = "testhash";
const suffix = "testhash";
if (!composeData?.volumes) {
return;
}
const volumes = addPrefixToVolumesRoot(composeData.volumes, prefix);
const volumes = addSuffixToVolumesRoot(composeData.volumes, suffix);
const updatedComposeData = { ...composeData, volumes };
// Verificar que el resultado coincide con el archivo esperado

View File

@@ -1,5 +1,5 @@
import { generateRandomHash } from "@/server/utils/docker/compose";
import { addPrefixToVolumesInServices } from "@/server/utils/docker/compose/volume";
import { addSuffixToVolumesInServices } from "@/server/utils/docker/compose/volume";
import type { ComposeSpecification } from "@/server/utils/docker/types";
import { load } from "js-yaml";
import { expect, test } from "vitest";
@@ -21,22 +21,22 @@ services:
- db_data:/var/lib/postgresql/data
`;
test("Add prefix to volumes declared directly in services", () => {
test("Add suffix to volumes declared directly in services", () => {
const composeData = load(composeFile1) as ComposeSpecification;
const prefix = generateRandomHash();
const suffix = generateRandomHash();
if (!composeData.services) {
return;
}
const updatedComposeData = addPrefixToVolumesInServices(
const updatedComposeData = addSuffixToVolumesInServices(
composeData.services,
prefix,
suffix,
);
const actualComposeData = { ...composeData, services: updatedComposeData };
expect(actualComposeData.services?.db?.volumes).toContain(
`db_data-${prefix}:/var/lib/postgresql/data`,
`db_data-${suffix}:/var/lib/postgresql/data`,
);
});
@@ -56,25 +56,25 @@ volumes:
driver: local
`;
test("Add prefix to volumes declared directly in services (Case 2)", () => {
test("Add suffix to volumes declared directly in services (Case 2)", () => {
const composeData = load(composeFileTypeVolume) as ComposeSpecification;
const prefix = generateRandomHash();
const suffix = generateRandomHash();
if (!composeData.services) {
return;
}
const updatedComposeData = addPrefixToVolumesInServices(
const updatedComposeData = addSuffixToVolumesInServices(
composeData.services,
prefix,
suffix,
);
const actualComposeData = { ...composeData, services: updatedComposeData };
expect(actualComposeData.services?.db?.volumes).toEqual([
{
type: "volume",
source: `db-test-${prefix}`,
source: `db-test-${suffix}`,
target: "/var/lib/postgresql/data",
},
]);

View File

@@ -1,7 +1,7 @@
import { generateRandomHash } from "@/server/utils/docker/compose";
import {
addPrefixToAllVolumes,
addPrefixToVolumesInServices,
addSuffixToAllVolumes,
addSuffixToVolumesInServices,
} from "@/server/utils/docker/compose/volume";
import type { ComposeSpecification } from "@/server/utils/docker/types";
import { load } from "js-yaml";
@@ -47,12 +47,12 @@ volumes:
driver: local
`) as ComposeSpecification;
test("Add prefix to volumes with type: volume in services", () => {
test("Add suffix to volumes with type: volume in services", () => {
const composeData = load(composeFileTypeVolume) as ComposeSpecification;
const prefix = "testhash";
const suffix = "testhash";
const updatedComposeData = addPrefixToAllVolumes(composeData, prefix);
const updatedComposeData = addSuffixToAllVolumes(composeData, suffix);
const actualComposeData = { ...composeData, ...updatedComposeData };
expect(actualComposeData).toEqual(expectedComposeFileTypeVolume);
@@ -96,12 +96,12 @@ volumes:
driver: local
`) as ComposeSpecification;
test("Add prefix to mixed volumes in services", () => {
test("Add suffix to mixed volumes in services", () => {
const composeData = load(composeFileTypeVolume1) as ComposeSpecification;
const prefix = "testhash";
const suffix = "testhash";
const updatedComposeData = addPrefixToAllVolumes(composeData, prefix);
const updatedComposeData = addSuffixToAllVolumes(composeData, suffix);
const actualComposeData = { ...composeData, ...updatedComposeData };
expect(actualComposeData).toEqual(expectedComposeFileTypeVolume1);
@@ -157,12 +157,12 @@ volumes:
device: /path/to/app/logs
`) as ComposeSpecification;
test("Add prefix to complex volume configurations in services", () => {
test("Add suffix to complex volume configurations in services", () => {
const composeData = load(composeFileTypeVolume2) as ComposeSpecification;
const prefix = "testhash";
const suffix = "testhash";
const updatedComposeData = addPrefixToAllVolumes(composeData, prefix);
const updatedComposeData = addSuffixToAllVolumes(composeData, suffix);
const actualComposeData = { ...composeData, ...updatedComposeData };
expect(actualComposeData).toEqual(expectedComposeFileTypeVolume2);
@@ -276,12 +276,12 @@ volumes:
device: /path/to/shared/logs
`) as ComposeSpecification;
test("Add prefix to complex nested volumes configuration in services", () => {
test("Add suffix to complex nested volumes configuration in services", () => {
const composeData = load(composeFileTypeVolume3) as ComposeSpecification;
const prefix = "testhash";
const suffix = "testhash";
const updatedComposeData = addPrefixToAllVolumes(composeData, prefix);
const updatedComposeData = addSuffixToAllVolumes(composeData, suffix);
const actualComposeData = { ...composeData, ...updatedComposeData };
expect(actualComposeData).toEqual(expectedComposeFileTypeVolume3);

View File

@@ -16,16 +16,9 @@ const baseAdmin: Admin = {
createdAt: "",
authId: "",
adminId: "string",
githubAppId: null,
githubAppName: null,
serverIp: null,
certificateType: "none",
host: null,
githubClientId: null,
githubClientSecret: null,
githubInstallationId: null,
githubPrivateKey: null,
githubWebhookSecret: null,
letsEncryptEmail: null,
sshPrivateKey: null,
enableDockerCleanup: false,

View File

@@ -10,9 +10,23 @@ const baseApp: ApplicationNested = {
appName: "",
autoDeploy: true,
branch: null,
dockerBuildStage: "",
buildArgs: null,
buildPath: "/",
gitlabPathNamespace: "",
buildType: "nixpacks",
bitbucketBranch: "",
bitbucketBuildPath: "",
bitbucketId: "",
bitbucketRepository: "",
bitbucketOwner: "",
githubId: "",
gitlabProjectId: 0,
gitlabBranch: "",
gitlabBuildPath: "",
gitlabId: "",
gitlabRepository: "",
gitlabOwner: "",
command: null,
cpuLimit: null,
cpuReservation: null,

View File

@@ -37,6 +37,7 @@ const mySchema = z.discriminatedUnion("buildType", [
})
.min(1, "Dockerfile required"),
dockerContextPath: z.string().nullable().default(""),
dockerBuildStage: z.string().nullable().default(""),
}),
z.object({
buildType: z.literal("heroku_buildpacks"),
@@ -86,6 +87,7 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => {
...(data.buildType && {
dockerfile: data.dockerfile || "",
dockerContextPath: data.dockerContextPath || "",
dockerBuildStage: data.dockerBuildStage || "",
}),
});
} else {
@@ -106,6 +108,8 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => {
dockerfile: data.buildType === "dockerfile" ? data.dockerfile : null,
dockerContextPath:
data.buildType === "dockerfile" ? data.dockerContextPath : null,
dockerBuildStage:
data.buildType === "dockerfile" ? data.dockerBuildStage : null,
})
.then(async () => {
toast.success("Build type saved");
@@ -241,6 +245,32 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => {
);
}}
/>
<FormField
control={form.control}
name="dockerBuildStage"
render={({ field }) => {
return (
<FormItem>
<div className="space-y-0.5">
<FormLabel>Docker Build Stage</FormLabel>
<FormDescription>
Allows you to target a specific stage in a
Multi-stage Dockerfile. If empty, Docker defaults to
build the last defined stage.
</FormDescription>
</div>
<FormControl>
<Input
placeholder={"E.g. production"}
{...field}
value={field.value ?? ""}
/>
</FormControl>
</FormItem>
);
}}
/>
</>
)}

View File

@@ -161,7 +161,7 @@ export const AddDomain = ({
render={({ field }) => (
<FormItem>
<FormLabel>Host</FormLabel>
<div className="flex max-lg:flex-wrap sm:flex-row gap-2">
<div className="flex gap-2">
<FormControl>
<Input placeholder="api.dokploy.com" {...field} />
</FormControl>

View File

@@ -0,0 +1,378 @@
import { AlertBlock } from "@/components/shared/alert-block";
import { Button } from "@/components/ui/button";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
} from "@/components/ui/command";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { ScrollArea } from "@/components/ui/scroll-area";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { cn } from "@/lib/utils";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { CheckIcon, ChevronsUpDown } from "lucide-react";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
const BitbucketProviderSchema = z.object({
buildPath: z.string().min(1, "Path is required").default("/"),
repository: z
.object({
repo: z.string().min(1, "Repo is required"),
owner: z.string().min(1, "Owner is required"),
})
.required(),
branch: z.string().min(1, "Branch is required"),
bitbucketId: z.string().min(1, "Bitbucket Provider is required"),
});
type BitbucketProvider = z.infer<typeof BitbucketProviderSchema>;
interface Props {
applicationId: string;
}
export const SaveBitbucketProvider = ({ applicationId }: Props) => {
const { data: bitbucketProviders } =
api.bitbucket.bitbucketProviders.useQuery();
const { data, refetch } = api.application.one.useQuery({ applicationId });
const { mutateAsync, isLoading: isSavingBitbucketProvider } =
api.application.saveBitbucketProvider.useMutation();
const form = useForm<BitbucketProvider>({
defaultValues: {
buildPath: "/",
repository: {
owner: "",
repo: "",
},
bitbucketId: "",
branch: "",
},
resolver: zodResolver(BitbucketProviderSchema),
});
const repository = form.watch("repository");
const bitbucketId = form.watch("bitbucketId");
const {
data: repositories,
isLoading: isLoadingRepositories,
error,
isError,
} = api.bitbucket.getBitbucketRepositories.useQuery(
{
bitbucketId,
},
{
enabled: !!bitbucketId,
},
);
const {
data: branches,
fetchStatus,
status,
} = api.bitbucket.getBitbucketBranches.useQuery(
{
owner: repository?.owner,
repo: repository?.repo,
bitbucketId,
},
{
enabled: !!repository?.owner && !!repository?.repo && !!bitbucketId,
},
);
useEffect(() => {
if (data) {
form.reset({
branch: data.bitbucketBranch || "",
repository: {
repo: data.bitbucketRepository || "",
owner: data.bitbucketOwner || "",
},
buildPath: data.bitbucketBuildPath || "/",
bitbucketId: data.bitbucketId || "",
});
}
}, [form.reset, data, form]);
const onSubmit = async (data: BitbucketProvider) => {
await mutateAsync({
bitbucketBranch: data.branch,
bitbucketRepository: data.repository.repo,
bitbucketOwner: data.repository.owner,
bitbucketBuildPath: data.buildPath,
bitbucketId: data.bitbucketId,
applicationId,
})
.then(async () => {
toast.success("Service Provided Saved");
await refetch();
})
.catch(() => {
toast.error("Error to save the Bitbucket provider");
});
};
return (
<div>
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="grid w-full gap-4 py-3"
>
{error && (
<AlertBlock type="error">Repositories: {error.message}</AlertBlock>
)}
<div className="grid md:grid-cols-2 gap-4">
<FormField
control={form.control}
name="bitbucketId"
render={({ field }) => (
<FormItem className="md:col-span-2 flex flex-col">
<FormLabel>Bitbucket Account</FormLabel>
<Select
onValueChange={(value) => {
field.onChange(value);
form.setValue("repository", {
owner: "",
repo: "",
});
form.setValue("branch", "");
}}
defaultValue={field.value}
value={field.value}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a Bitbucket Account" />
</SelectTrigger>
</FormControl>
<SelectContent>
{bitbucketProviders?.map((bitbucketProvider) => (
<SelectItem
key={bitbucketProvider.bitbucketId}
value={bitbucketProvider.bitbucketId}
>
{bitbucketProvider.gitProvider.name}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="repository"
render={({ field }) => (
<FormItem className="md:col-span-2 flex flex-col">
<FormLabel>Repository</FormLabel>
<Popover>
<PopoverTrigger asChild>
<FormControl>
<Button
variant="outline"
role="combobox"
className={cn(
"w-full justify-between !bg-input",
!field.value && "text-muted-foreground",
)}
>
{isLoadingRepositories
? "Loading...."
: field.value.owner
? repositories?.find(
(repo) => repo.name === field.value.repo,
)?.name
: "Select repository"}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent className="p-0" align="start">
<Command>
<CommandInput
placeholder="Search repository..."
className="h-9"
/>
{isLoadingRepositories && (
<span className="py-6 text-center text-sm">
Loading Repositories....
</span>
)}
<CommandEmpty>No repositories found.</CommandEmpty>
<ScrollArea className="h-96">
<CommandGroup>
{repositories?.map((repo) => (
<CommandItem
value={repo.url}
key={repo.url}
onSelect={() => {
form.setValue("repository", {
owner: repo.owner.username as string,
repo: repo.name,
});
form.setValue("branch", "");
}}
>
{repo.name}
<CheckIcon
className={cn(
"ml-auto h-4 w-4",
repo.name === field.value.repo
? "opacity-100"
: "opacity-0",
)}
/>
</CommandItem>
))}
</CommandGroup>
</ScrollArea>
</Command>
</PopoverContent>
</Popover>
{form.formState.errors.repository && (
<p className={cn("text-sm font-medium text-destructive")}>
Repository is required
</p>
)}
</FormItem>
)}
/>
<FormField
control={form.control}
name="branch"
render={({ field }) => (
<FormItem className="block w-full">
<FormLabel>Branch</FormLabel>
<Popover>
<PopoverTrigger asChild>
<FormControl>
<Button
variant="outline"
role="combobox"
className={cn(
" w-full justify-between !bg-input",
!field.value && "text-muted-foreground",
)}
>
{status === "loading" && fetchStatus === "fetching"
? "Loading...."
: field.value
? branches?.find(
(branch) => branch.name === field.value,
)?.name
: "Select branch"}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent className="p-0" align="start">
<Command>
<CommandInput
placeholder="Search branch..."
className="h-9"
/>
{status === "loading" && fetchStatus === "fetching" && (
<span className="py-6 text-center text-sm text-muted-foreground">
Loading Branches....
</span>
)}
{!repository?.owner && (
<span className="py-6 text-center text-sm text-muted-foreground">
Select a repository
</span>
)}
<ScrollArea className="h-96">
<CommandEmpty>No branch found.</CommandEmpty>
<CommandGroup>
{branches?.map((branch) => (
<CommandItem
value={branch.name}
key={branch.commit.sha}
onSelect={() => {
form.setValue("branch", branch.name);
}}
>
{branch.name}
<CheckIcon
className={cn(
"ml-auto h-4 w-4",
branch.name === field.value
? "opacity-100"
: "opacity-0",
)}
/>
</CommandItem>
))}
</CommandGroup>
</ScrollArea>
</Command>
</PopoverContent>
<FormMessage />
</Popover>
</FormItem>
)}
/>
<FormField
control={form.control}
name="buildPath"
render={({ field }) => (
<FormItem>
<FormLabel>Build Path</FormLabel>
<FormControl>
<Input placeholder="/" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="flex w-full justify-end">
<Button
isLoading={isSavingBitbucketProvider}
type="submit"
className="w-fit"
>
Save
</Button>
</div>
</form>
</Form>
</div>
);
};

View File

@@ -21,6 +21,13 @@ import {
PopoverTrigger,
} from "@/components/ui/popover";
import { ScrollArea } from "@/components/ui/scroll-area";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { cn } from "@/lib/utils";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
@@ -39,6 +46,7 @@ const GithubProviderSchema = z.object({
})
.required(),
branch: z.string().min(1, "Branch is required"),
githubId: z.string().min(1, "Github Provider is required"),
});
type GithubProvider = z.infer<typeof GithubProviderSchema>;
@@ -48,6 +56,7 @@ interface Props {
}
export const SaveGithubProvider = ({ applicationId }: Props) => {
const { data: githubProviders } = api.github.githubProviders.useQuery();
const { data, refetch } = api.application.one.useQuery({ applicationId });
const { mutateAsync, isLoading: isSavingGithubProvider } =
@@ -60,26 +69,38 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
owner: "",
repo: "",
},
githubId: "",
branch: "",
},
resolver: zodResolver(GithubProviderSchema),
});
const repository = form.watch("repository");
const githubId = form.watch("githubId");
const { data: repositories, isLoading: isLoadingRepositories } =
api.admin.getRepositories.useQuery();
api.github.getGithubRepositories.useQuery(
{
githubId,
},
{
enabled: !!githubId,
},
);
const {
data: branches,
fetchStatus,
status,
} = api.admin.getBranches.useQuery(
} = api.github.getGithubBranches.useQuery(
{
owner: repository?.owner,
repo: repository?.repo,
githubId,
},
{
enabled: !!repository?.owner && !!repository?.repo && !!githubId,
},
{ enabled: !!repository?.owner && !!repository?.repo },
);
useEffect(() => {
@@ -91,6 +112,7 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
owner: data.owner || "",
},
buildPath: data.buildPath || "/",
githubId: data.githubId || "",
});
}
}, [form.reset, data, form]);
@@ -102,6 +124,7 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
applicationId,
owner: data.repository.owner,
buildPath: data.buildPath,
githubId: data.githubId,
})
.then(async () => {
toast.success("Service Provided Saved");
@@ -120,6 +143,45 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
className="grid w-full gap-4 py-3"
>
<div className="grid md:grid-cols-2 gap-4">
<FormField
control={form.control}
name="githubId"
render={({ field }) => (
<FormItem className="md:col-span-2 flex flex-col">
<FormLabel>Github Account</FormLabel>
<Select
onValueChange={(value) => {
field.onChange(value);
form.setValue("repository", {
owner: "",
repo: "",
});
form.setValue("branch", "");
}}
defaultValue={field.value}
value={field.value}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a Github Account" />
</SelectTrigger>
</FormControl>
<SelectContent>
{githubProviders?.map((githubProvider) => (
<SelectItem
key={githubProvider.githubId}
value={githubProvider.githubId}
>
{githubProvider.gitProvider.name}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="repository"

View File

@@ -0,0 +1,394 @@
import { AlertBlock } from "@/components/shared/alert-block";
import { Button } from "@/components/ui/button";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
} from "@/components/ui/command";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { ScrollArea } from "@/components/ui/scroll-area";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { cn } from "@/lib/utils";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { CheckIcon, ChevronsUpDown } from "lucide-react";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
const GitlabProviderSchema = z.object({
buildPath: z.string().min(1, "Path is required").default("/"),
repository: z
.object({
repo: z.string().min(1, "Repo is required"),
owner: z.string().min(1, "Owner is required"),
gitlabPathNamespace: z.string().min(1),
id: z.number().nullable(),
})
.required(),
branch: z.string().min(1, "Branch is required"),
gitlabId: z.string().min(1, "Gitlab Provider is required"),
});
type GitlabProvider = z.infer<typeof GitlabProviderSchema>;
interface Props {
applicationId: string;
}
export const SaveGitlabProvider = ({ applicationId }: Props) => {
const { data: gitlabProviders } = api.gitlab.gitlabProviders.useQuery();
const { data, refetch } = api.application.one.useQuery({ applicationId });
const { mutateAsync, isLoading: isSavingGitlabProvider } =
api.application.saveGitlabProvider.useMutation();
const form = useForm<GitlabProvider>({
defaultValues: {
buildPath: "/",
repository: {
owner: "",
repo: "",
gitlabPathNamespace: "",
id: null,
},
gitlabId: "",
branch: "",
},
resolver: zodResolver(GitlabProviderSchema),
});
const repository = form.watch("repository");
const gitlabId = form.watch("gitlabId");
const {
data: repositories,
isLoading: isLoadingRepositories,
error,
} = api.gitlab.getGitlabRepositories.useQuery(
{
gitlabId,
},
{
enabled: !!gitlabId,
},
);
const {
data: branches,
fetchStatus,
status,
} = api.gitlab.getGitlabBranches.useQuery(
{
owner: repository?.owner,
repo: repository?.repo,
id: repository?.id || 0,
gitlabId: gitlabId,
},
{
enabled: !!repository?.owner && !!repository?.repo && !!gitlabId,
},
);
useEffect(() => {
if (data) {
form.reset({
branch: data.gitlabBranch || "",
repository: {
repo: data.gitlabRepository || "",
owner: data.gitlabOwner || "",
gitlabPathNamespace: data.gitlabPathNamespace || "",
id: data.gitlabProjectId,
},
buildPath: data.gitlabBuildPath || "/",
gitlabId: data.gitlabId || "",
});
}
}, [form.reset, data, form]);
const onSubmit = async (data: GitlabProvider) => {
await mutateAsync({
gitlabBranch: data.branch,
gitlabRepository: data.repository.repo,
gitlabOwner: data.repository.owner,
gitlabBuildPath: data.buildPath,
gitlabId: data.gitlabId,
applicationId,
gitlabProjectId: data.repository.id,
gitlabPathNamespace: data.repository.gitlabPathNamespace,
})
.then(async () => {
toast.success("Service Provided Saved");
await refetch();
})
.catch(() => {
toast.error("Error to save the gitlab provider");
});
};
return (
<div>
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="grid w-full gap-4 py-3"
>
{error && <AlertBlock type="error">{error?.message}</AlertBlock>}
<div className="grid md:grid-cols-2 gap-4">
<FormField
control={form.control}
name="gitlabId"
render={({ field }) => (
<FormItem className="md:col-span-2 flex flex-col">
<FormLabel>Gitlab Account</FormLabel>
<Select
onValueChange={(value) => {
field.onChange(value);
form.setValue("repository", {
owner: "",
repo: "",
id: null,
gitlabPathNamespace: "",
});
form.setValue("branch", "");
}}
defaultValue={field.value}
value={field.value}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a Gitlab Account" />
</SelectTrigger>
</FormControl>
<SelectContent>
{gitlabProviders?.map((gitlabProvider) => (
<SelectItem
key={gitlabProvider.gitlabId}
value={gitlabProvider.gitlabId}
>
{gitlabProvider.gitProvider.name}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="repository"
render={({ field }) => (
<FormItem className="md:col-span-2 flex flex-col">
<FormLabel>Repository</FormLabel>
<Popover>
<PopoverTrigger asChild>
<FormControl>
<Button
variant="outline"
role="combobox"
className={cn(
"w-full justify-between !bg-input",
!field.value && "text-muted-foreground",
)}
>
{isLoadingRepositories
? "Loading...."
: field.value.owner
? repositories?.find(
(repo) => repo.name === field.value.repo,
)?.name
: "Select repository"}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent className="p-0" align="start">
<Command>
<CommandInput
placeholder="Search repository..."
className="h-9"
/>
{isLoadingRepositories && (
<span className="py-6 text-center text-sm">
Loading Repositories....
</span>
)}
<CommandEmpty>No repositories found.</CommandEmpty>
<ScrollArea className="h-96">
<CommandGroup>
{repositories && repositories.length === 0 && (
<CommandEmpty>
No repositories found.
</CommandEmpty>
)}
{repositories?.map((repo) => {
return (
<CommandItem
value={repo.url}
key={repo.url}
onSelect={() => {
form.setValue("repository", {
owner: repo.owner.username as string,
repo: repo.name,
id: repo.id,
gitlabPathNamespace: repo.url,
});
form.setValue("branch", "");
}}
>
{repo.name}
<CheckIcon
className={cn(
"ml-auto h-4 w-4",
repo.name === field.value.repo
? "opacity-100"
: "opacity-0",
)}
/>
</CommandItem>
);
})}
</CommandGroup>
</ScrollArea>
</Command>
</PopoverContent>
</Popover>
{form.formState.errors.repository && (
<p className={cn("text-sm font-medium text-destructive")}>
Repository is required
</p>
)}
</FormItem>
)}
/>
<FormField
control={form.control}
name="branch"
render={({ field }) => (
<FormItem className="block w-full">
<FormLabel>Branch</FormLabel>
<Popover>
<PopoverTrigger asChild>
<FormControl>
<Button
variant="outline"
role="combobox"
className={cn(
" w-full justify-between !bg-input",
!field.value && "text-muted-foreground",
)}
>
{status === "loading" && fetchStatus === "fetching"
? "Loading...."
: field.value
? branches?.find(
(branch) => branch.name === field.value,
)?.name
: "Select branch"}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent className="p-0" align="start">
<Command>
<CommandInput
placeholder="Search branch..."
className="h-9"
/>
{status === "loading" && fetchStatus === "fetching" && (
<span className="py-6 text-center text-sm text-muted-foreground">
Loading Branches....
</span>
)}
{!repository?.owner && (
<span className="py-6 text-center text-sm text-muted-foreground">
Select a repository
</span>
)}
<ScrollArea className="h-96">
<CommandEmpty>No branch found.</CommandEmpty>
<CommandGroup>
{branches?.map((branch) => (
<CommandItem
value={branch.name}
key={branch.commit.id}
onSelect={() => {
form.setValue("branch", branch.name);
}}
>
{branch.name}
<CheckIcon
className={cn(
"ml-auto h-4 w-4",
branch.name === field.value
? "opacity-100"
: "opacity-0",
)}
/>
</CommandItem>
))}
</CommandGroup>
</ScrollArea>
</Command>
</PopoverContent>
<FormMessage />
</Popover>
</FormItem>
)}
/>
<FormField
control={form.control}
name="buildPath"
render={({ field }) => (
<FormItem>
<FormLabel>Build Path</FormLabel>
<FormControl>
<Input placeholder="/" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="flex w-full justify-end">
<Button
isLoading={isSavingGitlabProvider}
type="submit"
className="w-fit"
>
Save
</Button>
</div>
</form>
</Form>
</div>
);
};

View File

@@ -1,23 +1,34 @@
import { SaveDockerProvider } from "@/components/dashboard/application/general/generic/save-docker-provider";
import { SaveGitProvider } from "@/components/dashboard/application/general/generic/save-git-provider";
import { SaveGithubProvider } from "@/components/dashboard/application/general/generic/save-github-provider";
import {
BitbucketIcon,
DockerIcon,
GitIcon,
GithubIcon,
GitlabIcon,
} from "@/components/icons/data-tools-icons";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { api } from "@/utils/api";
import { GitBranch, LockIcon } from "lucide-react";
import { GitBranch, LockIcon, UploadCloud } from "lucide-react";
import Link from "next/link";
import { useState } from "react";
import { SaveBitbucketProvider } from "./save-bitbucket-provider";
import { SaveDragNDrop } from "./save-drag-n-drop";
import { SaveGitlabProvider } from "./save-gitlab-provider";
type TabState = "github" | "docker" | "git" | "drop";
type TabState = "github" | "docker" | "git" | "drop" | "gitlab" | "bitbucket";
interface Props {
applicationId: string;
}
export const ShowProviderForm = ({ applicationId }: Props) => {
const { data: haveGithubConfigured } =
api.admin.haveGithubConfigured.useQuery();
const { data: githubProviders } = api.github.githubProviders.useQuery();
const { data: gitlabProviders } = api.gitlab.gitlabProviders.useQuery();
const { data: bitbucketProviders } =
api.bitbucket.bitbucketProviders.useQuery();
const { data: application } = api.application.one.useQuery({ applicationId });
const [tab, setSab] = useState<TabState>(application?.sourceType || "github");
@@ -44,43 +55,104 @@ export const ShowProviderForm = ({ applicationId }: Props) => {
setSab(e as TabState);
}}
>
<TabsList className="grid w-fit grid-cols-4 bg-transparent">
<TabsTrigger
value="github"
className="rounded-none border-b-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
>
Github
</TabsTrigger>
<TabsTrigger
value="docker"
className="rounded-none border-b-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
>
Docker
</TabsTrigger>
<TabsTrigger
value="git"
className="rounded-none border-b-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
>
Git
</TabsTrigger>
<TabsTrigger
value="drop"
className="rounded-none border-b-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
>
Drop
</TabsTrigger>
</TabsList>
<div className="flex flex-row items-center justify-between w-full gap-4">
<TabsList className="md:grid md:w-fit md:grid-cols-7 max-md:overflow-x-scroll justify-start bg-transparent overflow-y-hidden">
<TabsTrigger
value="github"
className="rounded-none border-b-2 gap-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
>
<GithubIcon className="size-4 text-current fill-current" />
Github
</TabsTrigger>
<TabsTrigger
value="gitlab"
className="rounded-none border-b-2 gap-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
>
<GitlabIcon className="size-4 text-current fill-current" />
Gitlab
</TabsTrigger>
<TabsTrigger
value="bitbucket"
className="rounded-none border-b-2 gap-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
>
<BitbucketIcon className="size-4 text-current fill-current" />
Bitbucket
</TabsTrigger>
<TabsTrigger
value="docker"
className="rounded-none border-b-2 gap-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
>
<DockerIcon className="size-5 text-current" />
Docker
</TabsTrigger>
<TabsTrigger
value="git"
className="rounded-none border-b-2 gap-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
>
<GitIcon />
Git
</TabsTrigger>
<TabsTrigger
value="drop"
className="rounded-none border-b-2 gap-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
>
<UploadCloud className="size-5 text-current" />
Drop
</TabsTrigger>
</TabsList>
</div>
<TabsContent value="github" className="w-full p-2">
{haveGithubConfigured ? (
{githubProviders && githubProviders?.length > 0 ? (
<SaveGithubProvider applicationId={applicationId} />
) : (
<div className="flex flex-col items-center gap-3">
<LockIcon className="size-8 text-muted-foreground" />
<div className="flex flex-col items-center gap-3 min-h-[15vh] justify-center">
<GithubIcon className="size-8 text-muted-foreground" />
<span className="text-base text-muted-foreground">
To deploy using GitHub, you need to configure your account
first. Please, go to{" "}
<Link
href="/dashboard/settings/server"
href="/dashboard/settings/git-providers"
className="text-foreground"
>
Settings
</Link>{" "}
to do so.
</span>
</div>
)}
</TabsContent>
<TabsContent value="gitlab" className="w-full p-2">
{gitlabProviders && gitlabProviders?.length > 0 ? (
<SaveGitlabProvider applicationId={applicationId} />
) : (
<div className="flex flex-col items-center gap-3 min-h-[15vh] justify-center">
<GitlabIcon className="size-8 text-muted-foreground" />
<span className="text-base text-muted-foreground">
To deploy using GitLab, you need to configure your account
first. Please, go to{" "}
<Link
href="/dashboard/settings/git-providers"
className="text-foreground"
>
Settings
</Link>{" "}
to do so.
</span>
</div>
)}
</TabsContent>
<TabsContent value="bitbucket" className="w-full p-2">
{bitbucketProviders && bitbucketProviders?.length > 0 ? (
<SaveBitbucketProvider applicationId={applicationId} />
) : (
<div className="flex flex-col items-center gap-3 min-h-[15vh] justify-center">
<BitbucketIcon className="size-8 text-muted-foreground" />
<span className="text-base text-muted-foreground">
To deploy using Bitbucket, you need to configure your account
first. Please, go to{" "}
<Link
href="/dashboard/settings/git-providers"
className="text-foreground"
>
Settings
@@ -93,6 +165,7 @@ export const ShowProviderForm = ({ applicationId }: Props) => {
<TabsContent value="docker" className="w-full p-2">
<SaveDockerProvider applicationId={applicationId} />
</TabsContent>
<TabsContent value="git" className="w-full p-2">
<SaveGitProvider applicationId={applicationId} />
</TabsContent>

View File

@@ -166,7 +166,13 @@ export const AddDomainCompose = ({
<DialogTitle>Domain</DialogTitle>
<DialogDescription>{dictionary.dialogDescription}</DialogDescription>
</DialogHeader>
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
<div className="flex flex-col gap-4">
<AlertBlock type="info">
Deploy is required to apply changes after creating or updating a
domain.
</AlertBlock>
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
</div>
<Form {...form}>
<form
@@ -191,7 +197,7 @@ export const AddDomainCompose = ({
render={({ field }) => (
<FormItem className="w-full">
<FormLabel>Service Name</FormLabel>
<div className="flex max-lg:flex-wrap sm:flex-row gap-2">
<div className="flex gap-2">
<Select
onValueChange={field.onChange}
defaultValue={field.value || ""}
@@ -291,7 +297,7 @@ export const AddDomainCompose = ({
render={({ field }) => (
<FormItem>
<FormLabel>Host</FormLabel>
<div className="flex max-lg:flex-wrap sm:flex-row gap-2">
<div className="flex gap-2">
<FormControl>
<Input placeholder="api.dokploy.com" {...field} />
</FormControl>

View File

@@ -83,9 +83,10 @@ export const ComposeFileEditor = ({ composeId }: Props) => {
};
return (
<>
<div className="w-full flex flex-col lg:flex-row gap-4 ">
<div className="w-full flex flex-col gap-4 ">
<Form {...form}>
<form
id="hook-form-save-compose-file"
onSubmit={form.handleSubmit(onSubmit)}
className="w-full relative space-y-4"
>
@@ -121,21 +122,21 @@ services:
</FormItem>
)}
/>
<div className="flex justify-between flex-col lg:flex-row gap-2">
<div className="w-full flex flex-col lg:flex-row gap-4 items-end">
<RandomizeCompose composeId={composeId} />
</div>
<Button
type="submit"
isLoading={isLoading}
className="lg:w-fit w-full"
>
Save
</Button>
</div>
</form>
</Form>
<div className="flex justify-between flex-col lg:flex-row gap-2">
<div className="w-full flex flex-col lg:flex-row gap-4 items-end">
<RandomizeCompose composeId={composeId} />
</div>
<Button
type="submit"
form="hook-form-save-compose-file"
isLoading={isLoading}
className="lg:w-fit w-full"
>
Save
</Button>
</div>
</div>
</>
);

View File

@@ -0,0 +1,380 @@
import { AlertBlock } from "@/components/shared/alert-block";
import { Button } from "@/components/ui/button";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
} from "@/components/ui/command";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { ScrollArea } from "@/components/ui/scroll-area";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { cn } from "@/lib/utils";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { CheckIcon, ChevronsUpDown } from "lucide-react";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
const BitbucketProviderSchema = z.object({
composePath: z.string().min(1),
repository: z
.object({
repo: z.string().min(1, "Repo is required"),
owner: z.string().min(1, "Owner is required"),
})
.required(),
branch: z.string().min(1, "Branch is required"),
bitbucketId: z.string().min(1, "Bitbucket Provider is required"),
});
type BitbucketProvider = z.infer<typeof BitbucketProviderSchema>;
interface Props {
composeId: string;
}
export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
const { data: bitbucketProviders } =
api.bitbucket.bitbucketProviders.useQuery();
const { data, refetch } = api.compose.one.useQuery({ composeId });
const { mutateAsync, isLoading: isSavingBitbucketProvider } =
api.compose.update.useMutation();
const form = useForm<BitbucketProvider>({
defaultValues: {
composePath: "./docker-compose.yml",
repository: {
owner: "",
repo: "",
},
bitbucketId: "",
branch: "",
},
resolver: zodResolver(BitbucketProviderSchema),
});
const repository = form.watch("repository");
const bitbucketId = form.watch("bitbucketId");
const {
data: repositories,
isLoading: isLoadingRepositories,
error,
isError,
} = api.bitbucket.getBitbucketRepositories.useQuery(
{
bitbucketId,
},
{
enabled: !!bitbucketId,
},
);
const {
data: branches,
fetchStatus,
status,
} = api.bitbucket.getBitbucketBranches.useQuery(
{
owner: repository?.owner,
repo: repository?.repo,
bitbucketId,
},
{
enabled: !!repository?.owner && !!repository?.repo && !!bitbucketId,
},
);
useEffect(() => {
if (data) {
form.reset({
branch: data.bitbucketBranch || "",
repository: {
repo: data.bitbucketRepository || "",
owner: data.bitbucketOwner || "",
},
composePath: data.composePath,
bitbucketId: data.bitbucketId || "",
});
}
}, [form.reset, data, form]);
const onSubmit = async (data: BitbucketProvider) => {
await mutateAsync({
bitbucketBranch: data.branch,
bitbucketRepository: data.repository.repo,
bitbucketOwner: data.repository.owner,
bitbucketId: data.bitbucketId,
composePath: data.composePath,
composeId,
sourceType: "bitbucket",
composeStatus: "idle",
})
.then(async () => {
toast.success("Service Provided Saved");
await refetch();
})
.catch(() => {
toast.error("Error to save the Bitbucket provider");
});
};
return (
<div>
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="grid w-full gap-4 py-3"
>
{error && (
<AlertBlock type="error">Repositories: {error.message}</AlertBlock>
)}
<div className="grid md:grid-cols-2 gap-4">
<FormField
control={form.control}
name="bitbucketId"
render={({ field }) => (
<FormItem className="md:col-span-2 flex flex-col">
<FormLabel>Bitbucket Account</FormLabel>
<Select
onValueChange={(value) => {
field.onChange(value);
form.setValue("repository", {
owner: "",
repo: "",
});
form.setValue("branch", "");
}}
defaultValue={field.value}
value={field.value}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a Bitbucket Account" />
</SelectTrigger>
</FormControl>
<SelectContent>
{bitbucketProviders?.map((bitbucketProvider) => (
<SelectItem
key={bitbucketProvider.bitbucketId}
value={bitbucketProvider.bitbucketId}
>
{bitbucketProvider.gitProvider.name}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="repository"
render={({ field }) => (
<FormItem className="md:col-span-2 flex flex-col">
<FormLabel>Repository</FormLabel>
<Popover>
<PopoverTrigger asChild>
<FormControl>
<Button
variant="outline"
role="combobox"
className={cn(
"w-full justify-between !bg-input",
!field.value && "text-muted-foreground",
)}
>
{isLoadingRepositories
? "Loading...."
: field.value.owner
? repositories?.find(
(repo) => repo.name === field.value.repo,
)?.name
: "Select repository"}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent className="p-0" align="start">
<Command>
<CommandInput
placeholder="Search repository..."
className="h-9"
/>
{isLoadingRepositories && (
<span className="py-6 text-center text-sm">
Loading Repositories....
</span>
)}
<CommandEmpty>No repositories found.</CommandEmpty>
<ScrollArea className="h-96">
<CommandGroup>
{repositories?.map((repo) => (
<CommandItem
value={repo.url}
key={repo.url}
onSelect={() => {
form.setValue("repository", {
owner: repo.owner.username as string,
repo: repo.name,
});
form.setValue("branch", "");
}}
>
{repo.name}
<CheckIcon
className={cn(
"ml-auto h-4 w-4",
repo.name === field.value.repo
? "opacity-100"
: "opacity-0",
)}
/>
</CommandItem>
))}
</CommandGroup>
</ScrollArea>
</Command>
</PopoverContent>
</Popover>
{form.formState.errors.repository && (
<p className={cn("text-sm font-medium text-destructive")}>
Repository is required
</p>
)}
</FormItem>
)}
/>
<FormField
control={form.control}
name="branch"
render={({ field }) => (
<FormItem className="block w-full">
<FormLabel>Branch</FormLabel>
<Popover>
<PopoverTrigger asChild>
<FormControl>
<Button
variant="outline"
role="combobox"
className={cn(
" w-full justify-between !bg-input",
!field.value && "text-muted-foreground",
)}
>
{status === "loading" && fetchStatus === "fetching"
? "Loading...."
: field.value
? branches?.find(
(branch) => branch.name === field.value,
)?.name
: "Select branch"}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent className="p-0" align="start">
<Command>
<CommandInput
placeholder="Search branch..."
className="h-9"
/>
{status === "loading" && fetchStatus === "fetching" && (
<span className="py-6 text-center text-sm text-muted-foreground">
Loading Branches....
</span>
)}
{!repository?.owner && (
<span className="py-6 text-center text-sm text-muted-foreground">
Select a repository
</span>
)}
<ScrollArea className="h-96">
<CommandEmpty>No branch found.</CommandEmpty>
<CommandGroup>
{branches?.map((branch) => (
<CommandItem
value={branch.name}
key={branch.commit.sha}
onSelect={() => {
form.setValue("branch", branch.name);
}}
>
{branch.name}
<CheckIcon
className={cn(
"ml-auto h-4 w-4",
branch.name === field.value
? "opacity-100"
: "opacity-0",
)}
/>
</CommandItem>
))}
</CommandGroup>
</ScrollArea>
</Command>
</PopoverContent>
<FormMessage />
</Popover>
</FormItem>
)}
/>
<FormField
control={form.control}
name="composePath"
render={({ field }) => (
<FormItem>
<FormLabel>Compose Path</FormLabel>
<FormControl>
<Input placeholder="docker-compose.yml" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="flex w-full justify-end">
<Button
isLoading={isSavingBitbucketProvider}
type="submit"
className="w-fit"
>
Save
</Button>
</div>
</form>
</Form>
</div>
);
};

View File

@@ -21,6 +21,13 @@ import {
PopoverTrigger,
} from "@/components/ui/popover";
import { ScrollArea } from "@/components/ui/scroll-area";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { cn } from "@/lib/utils";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
@@ -39,6 +46,7 @@ const GithubProviderSchema = z.object({
})
.required(),
branch: z.string().min(1, "Branch is required"),
githubId: z.string().min(1, "Github Provider is required"),
});
type GithubProvider = z.infer<typeof GithubProviderSchema>;
@@ -48,6 +56,7 @@ interface Props {
}
export const SaveGithubProviderCompose = ({ composeId }: Props) => {
const { data: githubProviders } = api.github.githubProviders.useQuery();
const { data, refetch } = api.compose.one.useQuery({ composeId });
const { mutateAsync, isLoading: isSavingGithubProvider } =
@@ -60,26 +69,38 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
owner: "",
repo: "",
},
githubId: "",
branch: "",
},
resolver: zodResolver(GithubProviderSchema),
});
const repository = form.watch("repository");
const githubId = form.watch("githubId");
const { data: repositories, isLoading: isLoadingRepositories } =
api.admin.getRepositories.useQuery();
api.github.getGithubRepositories.useQuery(
{
githubId,
},
{
enabled: !!githubId,
},
);
const {
data: branches,
fetchStatus,
status,
} = api.admin.getBranches.useQuery(
} = api.github.getGithubBranches.useQuery(
{
owner: repository?.owner,
repo: repository?.repo,
githubId,
},
{
enabled: !!repository?.owner && !!repository?.repo && !!githubId,
},
{ enabled: !!repository?.owner && !!repository?.repo },
);
useEffect(() => {
@@ -91,19 +112,21 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
owner: data.owner || "",
},
composePath: data.composePath,
githubId: data.githubId || "",
});
}
}, [form.reset, data, form]);
const onSubmit = async (data: GithubProvider) => {
console.log(data);
await mutateAsync({
branch: data.branch,
repository: data.repository.repo,
composeId: composeId,
composeId,
owner: data.repository.owner,
sourceType: "github",
composePath: data.composePath,
githubId: data.githubId,
sourceType: "github",
composeStatus: "idle",
})
.then(async () => {
toast.success("Service Provided Saved");
@@ -122,6 +145,45 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
className="grid w-full gap-4 py-3"
>
<div className="grid md:grid-cols-2 gap-4">
<FormField
control={form.control}
name="githubId"
render={({ field }) => (
<FormItem className="md:col-span-2 flex flex-col">
<FormLabel>Github Account</FormLabel>
<Select
onValueChange={(value) => {
field.onChange(value);
form.setValue("repository", {
owner: "",
repo: "",
});
form.setValue("branch", "");
}}
defaultValue={field.value}
value={field.value}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a Github Account" />
</SelectTrigger>
</FormControl>
<SelectContent>
{githubProviders?.map((githubProvider) => (
<SelectItem
key={githubProvider.githubId}
value={githubProvider.githubId}
>
{githubProvider.gitProvider.name}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="repository"
@@ -278,7 +340,6 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
</FormItem>
)}
/>
<FormField
control={form.control}
name="composePath"

View File

@@ -0,0 +1,396 @@
import { AlertBlock } from "@/components/shared/alert-block";
import { Button } from "@/components/ui/button";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
} from "@/components/ui/command";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { ScrollArea } from "@/components/ui/scroll-area";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { cn } from "@/lib/utils";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { CheckIcon, ChevronsUpDown } from "lucide-react";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
const GitlabProviderSchema = z.object({
composePath: z.string().min(1),
repository: z
.object({
repo: z.string().min(1, "Repo is required"),
owner: z.string().min(1, "Owner is required"),
id: z.number().nullable(),
gitlabPathNamespace: z.string().min(1),
})
.required(),
branch: z.string().min(1, "Branch is required"),
gitlabId: z.string().min(1, "Gitlab Provider is required"),
});
type GitlabProvider = z.infer<typeof GitlabProviderSchema>;
interface Props {
composeId: string;
}
export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
const { data: gitlabProviders } = api.gitlab.gitlabProviders.useQuery();
const { data, refetch } = api.compose.one.useQuery({ composeId });
const { mutateAsync, isLoading: isSavingGitlabProvider } =
api.compose.update.useMutation();
const form = useForm<GitlabProvider>({
defaultValues: {
composePath: "./docker-compose.yml",
repository: {
owner: "",
repo: "",
gitlabPathNamespace: "",
id: null,
},
gitlabId: "",
branch: "",
},
resolver: zodResolver(GitlabProviderSchema),
});
const repository = form.watch("repository");
const gitlabId = form.watch("gitlabId");
const {
data: repositories,
isLoading: isLoadingRepositories,
error,
} = api.gitlab.getGitlabRepositories.useQuery(
{
gitlabId,
},
{
enabled: !!gitlabId,
},
);
const {
data: branches,
fetchStatus,
status,
} = api.gitlab.getGitlabBranches.useQuery(
{
owner: repository?.owner,
repo: repository?.repo,
id: repository?.id || 0,
gitlabId: gitlabId,
},
{
enabled: !!repository?.owner && !!repository?.repo && !!gitlabId,
},
);
useEffect(() => {
if (data) {
form.reset({
branch: data.gitlabBranch || "",
repository: {
repo: data.gitlabRepository || "",
owner: data.gitlabOwner || "",
id: data.gitlabProjectId,
gitlabPathNamespace: data.gitlabPathNamespace || "",
},
composePath: data.composePath,
gitlabId: data.gitlabId || "",
});
}
}, [form.reset, data, form]);
const onSubmit = async (data: GitlabProvider) => {
await mutateAsync({
gitlabBranch: data.branch,
gitlabRepository: data.repository.repo,
gitlabOwner: data.repository.owner,
composePath: data.composePath,
gitlabId: data.gitlabId,
composeId,
gitlabProjectId: data.repository.id,
gitlabPathNamespace: data.repository.gitlabPathNamespace,
sourceType: "gitlab",
composeStatus: "idle",
})
.then(async () => {
toast.success("Service Provided Saved");
await refetch();
})
.catch(() => {
toast.error("Error to save the gitlab provider");
});
};
return (
<div>
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="grid w-full gap-4 py-3"
>
{error && <AlertBlock type="error">{error?.message}</AlertBlock>}
<div className="grid md:grid-cols-2 gap-4">
<FormField
control={form.control}
name="gitlabId"
render={({ field }) => (
<FormItem className="md:col-span-2 flex flex-col">
<FormLabel>Gitlab Account</FormLabel>
<Select
onValueChange={(value) => {
field.onChange(value);
form.setValue("repository", {
owner: "",
repo: "",
gitlabPathNamespace: "",
id: null,
});
form.setValue("branch", "");
}}
defaultValue={field.value}
value={field.value}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a Gitlab Account" />
</SelectTrigger>
</FormControl>
<SelectContent>
{gitlabProviders?.map((gitlabProvider) => (
<SelectItem
key={gitlabProvider.gitlabId}
value={gitlabProvider.gitlabId}
>
{gitlabProvider.gitProvider.name}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="repository"
render={({ field }) => (
<FormItem className="md:col-span-2 flex flex-col">
<FormLabel>Repository</FormLabel>
<Popover>
<PopoverTrigger asChild>
<FormControl>
<Button
variant="outline"
role="combobox"
className={cn(
"w-full justify-between !bg-input",
!field.value && "text-muted-foreground",
)}
>
{isLoadingRepositories
? "Loading...."
: field.value.owner
? repositories?.find(
(repo) => repo.name === field.value.repo,
)?.name
: "Select repository"}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent className="p-0" align="start">
<Command>
<CommandInput
placeholder="Search repository..."
className="h-9"
/>
{isLoadingRepositories && (
<span className="py-6 text-center text-sm">
Loading Repositories....
</span>
)}
<CommandEmpty>No repositories found.</CommandEmpty>
<ScrollArea className="h-96">
<CommandGroup>
{repositories && repositories.length === 0 && (
<CommandEmpty>
No repositories found.
</CommandEmpty>
)}
{repositories?.map((repo) => {
return (
<CommandItem
value={repo.url}
key={repo.url}
onSelect={() => {
form.setValue("repository", {
owner: repo.owner.username as string,
repo: repo.name,
id: repo.id,
gitlabPathNamespace: repo.url,
});
form.setValue("branch", "");
}}
>
{repo.name}
<CheckIcon
className={cn(
"ml-auto h-4 w-4",
repo.name === field.value.repo
? "opacity-100"
: "opacity-0",
)}
/>
</CommandItem>
);
})}
</CommandGroup>
</ScrollArea>
</Command>
</PopoverContent>
</Popover>
{form.formState.errors.repository && (
<p className={cn("text-sm font-medium text-destructive")}>
Repository is required
</p>
)}
</FormItem>
)}
/>
<FormField
control={form.control}
name="branch"
render={({ field }) => (
<FormItem className="block w-full">
<FormLabel>Branch</FormLabel>
<Popover>
<PopoverTrigger asChild>
<FormControl>
<Button
variant="outline"
role="combobox"
className={cn(
" w-full justify-between !bg-input",
!field.value && "text-muted-foreground",
)}
>
{status === "loading" && fetchStatus === "fetching"
? "Loading...."
: field.value
? branches?.find(
(branch) => branch.name === field.value,
)?.name
: "Select branch"}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent className="p-0" align="start">
<Command>
<CommandInput
placeholder="Search branch..."
className="h-9"
/>
{status === "loading" && fetchStatus === "fetching" && (
<span className="py-6 text-center text-sm text-muted-foreground">
Loading Branches....
</span>
)}
{!repository?.owner && (
<span className="py-6 text-center text-sm text-muted-foreground">
Select a repository
</span>
)}
<ScrollArea className="h-96">
<CommandEmpty>No branch found.</CommandEmpty>
<CommandGroup>
{branches?.map((branch) => (
<CommandItem
value={branch.name}
key={branch.commit.id}
onSelect={() => {
form.setValue("branch", branch.name);
}}
>
{branch.name}
<CheckIcon
className={cn(
"ml-auto h-4 w-4",
branch.name === field.value
? "opacity-100"
: "opacity-0",
)}
/>
</CommandItem>
))}
</CommandGroup>
</ScrollArea>
</Command>
</PopoverContent>
<FormMessage />
</Popover>
</FormItem>
)}
/>
<FormField
control={form.control}
name="composePath"
render={({ field }) => (
<FormItem>
<FormLabel>Compose Path</FormLabel>
<FormControl>
<Input placeholder="docker-compose.yml" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="flex w-full justify-end">
<Button
isLoading={isSavingGitlabProvider}
type="submit"
className="w-fit"
>
Save
</Button>
</div>
</form>
</Form>
</div>
);
};

View File

@@ -1,22 +1,32 @@
import {
BitbucketIcon,
GitIcon,
GithubIcon,
GitlabIcon,
} from "@/components/icons/data-tools-icons";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { api } from "@/utils/api";
import { GitBranch, LockIcon } from "lucide-react";
import { CodeIcon, GitBranch, LockIcon } from "lucide-react";
import Link from "next/link";
import { useState } from "react";
import { ComposeFileEditor } from "../compose-file-editor";
import { ShowConvertedCompose } from "../show-converted-compose";
import { SaveBitbucketProviderCompose } from "./save-bitbucket-provider-compose";
import { SaveGitProviderCompose } from "./save-git-provider-compose";
import { SaveGithubProviderCompose } from "./save-github-provider-compose";
import { SaveGitlabProviderCompose } from "./save-gitlab-provider-compose";
type TabState = "github" | "git" | "raw";
type TabState = "github" | "git" | "raw" | "gitlab" | "bitbucket";
interface Props {
composeId: string;
}
export const ShowProviderFormCompose = ({ composeId }: Props) => {
const { data: haveGithubConfigured } =
api.admin.haveGithubConfigured.useQuery();
const { data: githubProviders } = api.github.githubProviders.useQuery();
const { data: gitlabProviders } = api.gitlab.gitlabProviders.useQuery();
const { data: bitbucketProviders } =
api.bitbucket.bitbucketProviders.useQuery();
const { data: compose } = api.compose.one.useQuery({ composeId });
const [tab, setSab] = useState<TabState>(compose?.sourceType || "github");
@@ -44,38 +54,97 @@ export const ShowProviderFormCompose = ({ composeId }: Props) => {
setSab(e as TabState);
}}
>
<TabsList className="grid w-fit grid-cols-4 bg-transparent">
<TabsTrigger
value="github"
className="rounded-none border-b-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
>
Github
</TabsTrigger>
<div className="flex flex-row items-center justify-between w-full gap-4">
<TabsList className="md:grid md:w-fit md:grid-cols-5 max-md:overflow-x-scroll justify-start bg-transparent overflow-y-hidden">
<TabsTrigger
value="github"
className="rounded-none border-b-2 gap-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
>
<GithubIcon className="size-4 text-current fill-current" />
Github
</TabsTrigger>
<TabsTrigger
value="gitlab"
className="rounded-none border-b-2 gap-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
>
<GitlabIcon className="size-4 text-current fill-current" />
Gitlab
</TabsTrigger>
<TabsTrigger
value="bitbucket"
className="rounded-none border-b-2 gap-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
>
<BitbucketIcon className="size-4 text-current fill-current" />
Bitbucket
</TabsTrigger>
<TabsTrigger
value="git"
className="rounded-none border-b-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
>
Git
</TabsTrigger>
<TabsTrigger
value="raw"
className="rounded-none border-b-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
>
Raw
</TabsTrigger>
</TabsList>
<TabsTrigger
value="git"
className="rounded-none border-b-2 gap-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
>
<GitIcon />
Git
</TabsTrigger>
<TabsTrigger
value="raw"
className="rounded-none border-b-2 gap-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
>
<CodeIcon className="size-4 " />
Raw
</TabsTrigger>
</TabsList>
</div>
<TabsContent value="github" className="w-full p-2">
{haveGithubConfigured ? (
{githubProviders && githubProviders?.length > 0 ? (
<SaveGithubProviderCompose composeId={composeId} />
) : (
<div className="flex flex-col items-center gap-3">
<LockIcon className="size-8 text-muted-foreground" />
<div className="flex flex-col items-center gap-3 min-h-[15vh] justify-center">
<GithubIcon className="size-8 text-muted-foreground" />
<span className="text-base text-muted-foreground">
To deploy using GitHub, you need to configure your account
first. Please, go to{" "}
<Link
href="/dashboard/settings/server"
href="/dashboard/settings/git-providers"
className="text-foreground"
>
Settings
</Link>{" "}
to do so.
</span>
</div>
)}
</TabsContent>
<TabsContent value="gitlab" className="w-full p-2">
{gitlabProviders && gitlabProviders?.length > 0 ? (
<SaveGitlabProviderCompose composeId={composeId} />
) : (
<div className="flex flex-col items-center gap-3 min-h-[15vh] justify-center">
<GitlabIcon className="size-8 text-muted-foreground" />
<span className="text-base text-muted-foreground">
To deploy using GitLab, you need to configure your account
first. Please, go to{" "}
<Link
href="/dashboard/settings/git-providers"
className="text-foreground"
>
Settings
</Link>{" "}
to do so.
</span>
</div>
)}
</TabsContent>
<TabsContent value="bitbucket" className="w-full p-2">
{bitbucketProviders && bitbucketProviders?.length > 0 ? (
<SaveBitbucketProviderCompose composeId={composeId} />
) : (
<div className="flex flex-col items-center gap-3 min-h-[15vh] justify-center">
<BitbucketIcon className="size-8 text-muted-foreground" />
<span className="text-base text-muted-foreground">
To deploy using Bitbucket, you need to configure your account
first. Please, go to{" "}
<Link
href="/dashboard/settings/git-providers"
className="text-foreground"
>
Settings

View File

@@ -1,5 +1,7 @@
import { AlertBlock } from "@/components/shared/alert-block";
import { CodeEditor } from "@/components/shared/code-editor";
import { Button } from "@/components/ui/button";
import { CardTitle } from "@/components/ui/card";
import {
Dialog,
DialogContent,
@@ -8,28 +10,94 @@ import {
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import {
InputOTP,
InputOTPGroup,
InputOTPSlot,
} from "@/components/ui/input-otp";
import { Switch } from "@/components/ui/switch";
import { api } from "@/utils/api";
import { Dices } from "lucide-react";
import { useState } from "react";
import { zodResolver } from "@hookform/resolvers/zod";
import { AlertTriangle, Dices } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
interface Props {
composeId: string;
}
const schema = z.object({
suffix: z.string(),
randomize: z.boolean().optional(),
});
type Schema = z.infer<typeof schema>;
export const RandomizeCompose = ({ composeId }: Props) => {
const utils = api.useUtils();
const [prefix, setPrefix] = useState<string>("");
const [compose, setCompose] = useState<string>("");
const [isOpen, setIsOpen] = useState(false);
const { mutateAsync, error, isError } =
api.compose.randomizeCompose.useMutation();
const onSubmit = async () => {
const { mutateAsync: updateCompose } = api.compose.update.useMutation();
const { data, refetch } = api.compose.one.useQuery(
{ composeId },
{ enabled: !!composeId },
);
const form = useForm<Schema>({
defaultValues: {
suffix: "",
randomize: false,
},
resolver: zodResolver(schema),
});
const suffix = form.watch("suffix");
useEffect(() => {
if (data) {
form.reset({
suffix: data?.suffix || "",
randomize: data?.randomize || false,
});
}
}, [form, form.reset, form.formState.isSubmitSuccessful, data]);
const onSubmit = async (formData: Schema) => {
await updateCompose({
composeId,
suffix: formData?.suffix || "",
randomize: formData?.randomize || false,
})
.then(async (data) => {
randomizeCompose();
refetch();
toast.success("Compose updated");
})
.catch(() => {
toast.error("Error to randomize the compose");
});
};
const randomizeCompose = async () => {
await mutateAsync({
composeId,
prefix,
suffix,
})
.then(async (data) => {
await utils.project.all.invalidate();
@@ -43,7 +111,7 @@ export const RandomizeCompose = ({ composeId }: Props) => {
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild onClick={() => onSubmit()}>
<DialogTrigger asChild onClick={() => randomizeCompose()}>
<Button className="max-lg:w-full" variant="outline">
<Dices className="h-4 w-4" />
Randomize Compose
@@ -59,7 +127,7 @@ export const RandomizeCompose = ({ composeId }: Props) => {
</DialogHeader>
<div className="text-sm text-muted-foreground flex flex-col gap-2">
<span>
This will randomize the compose file and will add a prefix to the
This will randomize the compose file and will add a suffix to the
property to avoid conflicts
</span>
<ul className="list-disc list-inside">
@@ -69,29 +137,98 @@ export const RandomizeCompose = ({ composeId }: Props) => {
<li>configs</li>
<li>secrets</li>
</ul>
</div>
<div className="flex flex-col lg:flex-row gap-2">
<Input
placeholder="Enter a prefix (Optional, example: prod)"
onChange={(e) => setPrefix(e.target.value)}
/>
<Button
type="submit"
onClick={async () => {
await onSubmit();
}}
className="lg:w-fit w-full"
>
Random
</Button>
<AlertBlock type="info">
When you activate this option, we will include a env
`COMPOSE_PREFIX` variable to the compose file so you can use it in
your compose file.
</AlertBlock>
</div>
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
id="hook-form-add-project"
className="grid w-full gap-4"
>
{isError && (
<div className="flex flex-row gap-4 rounded-lg items-center bg-red-50 p-2 dark:bg-red-950">
<AlertTriangle className="text-red-600 dark:text-red-400" />
<span className="text-sm text-red-600 dark:text-red-400">
{error?.message}
</span>
</div>
)}
<div className="p-4 bg-secondary rounded-lg">
<pre>
<code className="language-yaml">{compose}</code>
</pre>
</div>
<div className="flex flex-col lg:flex-col gap-4 w-full ">
<div>
<FormField
control={form.control}
name="suffix"
render={({ field }) => (
<FormItem className="flex flex-col justify-center max-sm:items-center w-full">
<FormLabel>Suffix</FormLabel>
<FormControl>
<Input
placeholder="Enter a suffix (Optional, example: prod)"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="randomize"
render={({ field }) => (
<FormItem className="mt-4 flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
<div className="space-y-0.5">
<FormLabel>Apply Randomize</FormLabel>
<FormDescription>
Apply randomize to the compose file.
</FormDescription>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
</div>
<div className="flex flex-col lg:flex-row gap-4 w-full items-end justify-end">
<Button
form="hook-form-add-project"
type="submit"
className="lg:w-fit"
>
Save
</Button>
<Button
type="button"
variant="secondary"
onClick={async () => {
await randomizeCompose();
}}
className="lg:w-fit"
>
Random
</Button>
</div>
</div>
<pre>
<CodeEditor
value={compose || ""}
language="yaml"
readOnly
height="50rem"
/>
</pre>
</form>
</Form>
</DialogContent>
</Dialog>
);

View File

@@ -1,3 +1,4 @@
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
@@ -17,6 +18,7 @@ import {
} from "@/components/ui/select";
import { api } from "@/utils/api";
import { useEffect, useState } from "react";
import { toast } from "sonner";
import { DockerMonitoring } from "../../monitoring/docker/show";
interface Props {
@@ -42,9 +44,15 @@ export const ShowMonitoringCompose = ({
string | undefined
>();
const [containerId, setContainerId] = useState<string | undefined>();
const { mutateAsync: restart, isLoading } =
api.docker.restartContainer.useMutation();
useEffect(() => {
if (data && data?.length > 0) {
setContainerAppName(data[0]?.name);
setContainerId(data[0]?.containerId);
}
}, [data]);
@@ -57,24 +65,48 @@ export const ShowMonitoringCompose = ({
</CardHeader>
<CardContent className="flex flex-col gap-4">
<Label>Select a container to watch the monitoring</Label>
<Select onValueChange={setContainerAppName} value={containerAppName}>
<SelectTrigger>
<SelectValue placeholder="Select a container" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
{data?.map((container) => (
<SelectItem
key={container.containerId}
value={container.name}
>
{container.name} ({container.containerId}) {container.state}
</SelectItem>
))}
<SelectLabel>Containers ({data?.length})</SelectLabel>
</SelectGroup>
</SelectContent>
</Select>
<div className="flex flex-row gap-4">
<Select
onValueChange={(value) => {
setContainerAppName(value);
setContainerId(
data?.find((container) => container.name === value)
?.containerId,
);
}}
value={containerAppName}
>
<SelectTrigger>
<SelectValue placeholder="Select a container" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
{data?.map((container) => (
<SelectItem
key={container.containerId}
value={container.name}
>
{container.name} ({container.containerId}){" "}
{container.state}
</SelectItem>
))}
<SelectLabel>Containers ({data?.length})</SelectLabel>
</SelectGroup>
</SelectContent>
</Select>
<Button
isLoading={isLoading}
onClick={async () => {
if (!containerId) return;
toast.success(`Restarting container ${containerAppName}`);
await restart({ containerId }).then(() => {
toast.success("Container restarted");
});
}}
>
Restart
</Button>
</div>
<DockerMonitoring
appName={containerAppName || ""}
appType={appType}

View File

@@ -0,0 +1,229 @@
import {
BitbucketIcon,
GithubIcon,
GitlabIcon,
} from "@/components/icons/data-tools-icons";
import { AlertBlock } from "@/components/shared/alert-block";
import { Button } from "@/components/ui/button";
import { CardContent } from "@/components/ui/card";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { api } from "@/utils/api";
import { useUrl } from "@/utils/hooks/use-url";
import { zodResolver } from "@hookform/resolvers/zod";
import { ExternalLink } from "lucide-react";
import Link from "next/link";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
const Schema = z.object({
name: z.string().min(1, {
message: "Name is required",
}),
username: z.string().min(1, {
message: "Username is required",
}),
password: z.string().min(1, {
message: "App Password is required",
}),
workspaceName: z.string().optional(),
});
type Schema = z.infer<typeof Schema>;
export const AddBitbucketProvider = () => {
const utils = api.useUtils();
const [isOpen, setIsOpen] = useState(false);
const url = useUrl();
const { mutateAsync, error, isError } = api.bitbucket.create.useMutation();
const { data: auth } = api.auth.get.useQuery();
const router = useRouter();
const form = useForm<Schema>({
defaultValues: {
username: "",
password: "",
workspaceName: "",
},
resolver: zodResolver(Schema),
});
useEffect(() => {
form.reset({
username: "",
password: "",
workspaceName: "",
});
}, [form, isOpen]);
const onSubmit = async (data: Schema) => {
await mutateAsync({
bitbucketUsername: data.username,
appPassword: data.password,
bitbucketWorkspaceName: data.workspaceName || "",
authId: auth?.id || "",
name: data.name || "",
})
.then(async () => {
await utils.gitProvider.getAll.invalidate();
toast.success("Bitbucket configured successfully");
setIsOpen(false);
})
.catch(() => {
toast.error("Error configuring Bitbucket");
});
};
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<Button
variant="secondary"
className="flex items-center space-x-1 bg-blue-700 text-white hover:bg-blue-600"
>
<BitbucketIcon />
<span>Bitbucket</span>
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-2xl overflow-y-auto max-h-screen">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
Bitbucket Provider <BitbucketIcon className="size-5" />
</DialogTitle>
</DialogHeader>
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
<Form {...form}>
<form
id="hook-form-add-bitbucket"
onSubmit={form.handleSubmit(onSubmit)}
className="grid w-full gap-1"
>
<CardContent className="p-0">
<div className="flex flex-col gap-4">
<p className="text-muted-foreground text-sm">
To integrate your Bitbucket account, you need to create a new
App Password in your Bitbucket settings. Follow these steps:
</p>
<ol className="list-decimal list-inside text-sm text-muted-foreground">
<li className="flex flex-row gap-2 items-center">
Create new App Password{" "}
<Link
href="https://bitbucket.org/account/settings/app-passwords/new"
target="_blank"
>
<ExternalLink className="w-fit text-primary size-4" />
</Link>
</li>
<li>
When creating the App Password, ensure you grant the
following permissions:
<ul className="list-disc list-inside ml-4">
<li>Account: Read</li>
<li>Workspace membership: Read</li>
<li>Projects: Read</li>
<li>Repositories: Read</li>
<li>Pull requests: Read</li>
<li>Webhooks: Read and write</li>
</ul>
</li>
<li>
After creating, you'll receive an App Password. Copy it and
paste it below along with your Bitbucket username.
</li>
</ol>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input
placeholder="Random Name eg(my-personal-account)"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>Bitbucket Username</FormLabel>
<FormControl>
<Input
placeholder="Your Bitbucket username"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>App Password</FormLabel>
<FormControl>
<Input
type="password"
placeholder="ATBBPDYUC94nR96Nj7Cqpp4pfwKk03573DD2"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="workspaceName"
render={({ field }) => (
<FormItem>
<FormLabel>Workspace Name (Optional)</FormLabel>
<FormControl>
<Input
placeholder="For organization accounts"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button isLoading={form.formState.isSubmitting}>
Configure Bitbucket
</Button>
</div>
</CardContent>
</form>
</Form>
</DialogContent>
</Dialog>
);
};

View File

@@ -0,0 +1,201 @@
import { BitbucketIcon } from "@/components/icons/data-tools-icons";
import { AlertBlock } from "@/components/shared/alert-block";
import { Button } from "@/components/ui/button";
import { CardContent } from "@/components/ui/card";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { Edit } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
const Schema = z.object({
name: z.string().min(1, {
message: "Name is required",
}),
username: z.string().min(1, {
message: "Username is required",
}),
workspaceName: z.string().optional(),
});
type Schema = z.infer<typeof Schema>;
interface Props {
bitbucketId: string;
}
export const EditBitbucketProvider = ({ bitbucketId }: Props) => {
const { data: bitbucket } = api.bitbucket.one.useQuery(
{
bitbucketId,
},
{
enabled: !!bitbucketId,
},
);
const utils = api.useUtils();
const [isOpen, setIsOpen] = useState(false);
const { mutateAsync, error, isError } = api.bitbucket.update.useMutation();
const { mutateAsync: testConnection, isLoading } =
api.bitbucket.testConnection.useMutation();
const form = useForm<Schema>({
defaultValues: {
username: "",
workspaceName: "",
},
resolver: zodResolver(Schema),
});
const username = form.watch("username");
const workspaceName = form.watch("workspaceName");
useEffect(() => {
form.reset({
username: bitbucket?.bitbucketUsername || "",
workspaceName: bitbucket?.bitbucketWorkspaceName || "",
name: bitbucket?.gitProvider.name || "",
});
}, [form, isOpen]);
const onSubmit = async (data: Schema) => {
await mutateAsync({
bitbucketId,
gitProviderId: bitbucket?.gitProviderId || "",
bitbucketUsername: data.username,
bitbucketWorkspaceName: data.workspaceName || "",
name: data.name || "",
})
.then(async () => {
await utils.gitProvider.getAll.invalidate();
toast.success("Bitbucket updated successfully");
setIsOpen(false);
})
.catch(() => {
toast.error("Error to update Bitbucket");
});
};
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<Button variant="ghost">
<Edit className="size-4" />
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-2xl overflow-y-auto max-h-screen">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
Update Bitbucket Provider <BitbucketIcon className="size-5" />
</DialogTitle>
</DialogHeader>
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
<Form {...form}>
<form
id="hook-form-add-bitbucket"
onSubmit={form.handleSubmit(onSubmit)}
className="grid w-full gap-1"
>
<CardContent className="p-0">
<div className="flex flex-col gap-4">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input
placeholder="Random Name eg(my-personal-account)"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>Bitbucket Username</FormLabel>
<FormControl>
<Input
placeholder="Your Bitbucket username"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="workspaceName"
render={({ field }) => (
<FormItem>
<FormLabel>Workspace Name (Optional)</FormLabel>
<FormControl>
<Input
placeholder="For organization accounts"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="flex w-full justify-end gap-4 mt-4">
<Button
type="button"
variant={"secondary"}
isLoading={isLoading}
onClick={async () => {
await testConnection({
bitbucketId,
bitbucketUsername: username,
workspaceName: workspaceName,
})
.then(async (message) => {
toast.info(`Message: ${message}`);
})
.catch((error) => {
toast.error(`Error: ${error.message}`);
});
}}
>
Test Connection
</Button>
<Button type="submit" isLoading={form.formState.isSubmitting}>
Update
</Button>
</div>
</div>
</CardContent>
</form>
</Form>
</DialogContent>
</Dialog>
);
};

View File

@@ -0,0 +1,128 @@
import { GithubIcon } from "@/components/icons/data-tools-icons";
import { Button } from "@/components/ui/button";
import { CardContent } from "@/components/ui/card";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Switch } from "@/components/ui/switch";
import { api } from "@/utils/api";
import { useUrl } from "@/utils/hooks/use-url";
import { format } from "date-fns";
import { useEffect, useState } from "react";
export const AddGithubProvider = () => {
const [isOpen, setIsOpen] = useState(false);
const url = useUrl();
const { data } = api.auth.get.useQuery();
const [manifest, setManifest] = useState("");
const [isOrganization, setIsOrganization] = useState(false);
const [organizationName, setOrganization] = useState("");
useEffect(() => {
const url = document.location.origin;
const manifest = JSON.stringify(
{
redirect_url: `${origin}/api/providers/github/setup?authId=${data?.id}`,
name: `Dokploy-${format(new Date(), "yyyy-MM-dd")}`,
url: origin,
hook_attributes: {
url: `${url}/api/deploy/github`,
},
callback_urls: [`${origin}/api/providers/github/setup`],
public: false,
request_oauth_on_install: true,
default_permissions: {
contents: "read",
metadata: "read",
emails: "read",
pull_requests: "write",
},
default_events: ["pull_request", "push"],
},
null,
4,
);
setManifest(manifest);
}, [data?.id]);
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<Button variant="secondary" className="flex items-center space-x-1">
<GithubIcon />
<span>Github</span>
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-2xl ">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
Github Provider <GithubIcon className="size-5" />
</DialogTitle>
</DialogHeader>
<div id="hook-form-add-project" className="grid w-full gap-1">
<CardContent className="p-0">
<div className="flex flex-col ">
<p className="text-muted-foreground text-sm">
To integrate your GitHub account with our services, you'll need
to create and install a GitHub app. This process is
straightforward and only takes a few minutes. Click the button
below to get started.
</p>
<div className="mt-4 flex flex-col gap-4">
<div className="flex flex-row gap-4">
<span>Organization?</span>
<Switch
checked={isOrganization}
onCheckedChange={(checked) => setIsOrganization(checked)}
/>
</div>
{isOrganization && (
<Input
required
placeholder="Organization name"
onChange={(e) => setOrganization(e.target.value)}
/>
)}
</div>
<form
action={
isOrganization
? `https://github.com/organizations/${organizationName}/settings/apps/new?state=gh_init:${data?.id}`
: `https://github.com/settings/apps/new?state=gh_init:${data?.id}`
}
method="post"
>
<input
type="text"
name="manifest"
id="manifest"
defaultValue={manifest}
className="invisible"
/>
<br />
<div className="flex w-full justify-end">
<Button
disabled={isOrganization && organizationName.length < 1}
type="submit"
className="self-end"
>
Create GitHub App
</Button>
</div>
</form>
</div>
</CardContent>
</div>
</DialogContent>
</Dialog>
);
};

View File

@@ -0,0 +1,154 @@
import { GithubIcon } from "@/components/icons/data-tools-icons";
import { AlertBlock } from "@/components/shared/alert-block";
import { Button } from "@/components/ui/button";
import { CardContent } from "@/components/ui/card";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { Edit } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
const Schema = z.object({
name: z.string().min(1, {
message: "Name is required",
}),
});
type Schema = z.infer<typeof Schema>;
interface Props {
githubId: string;
}
export const EditGithubProvider = ({ githubId }: Props) => {
const { data: github } = api.github.one.useQuery(
{
githubId,
},
{
enabled: !!githubId,
},
);
const utils = api.useUtils();
const [isOpen, setIsOpen] = useState(false);
const { mutateAsync, error, isError } = api.github.update.useMutation();
const { mutateAsync: testConnection, isLoading } =
api.github.testConnection.useMutation();
const form = useForm<Schema>({
defaultValues: {
name: "",
},
resolver: zodResolver(Schema),
});
useEffect(() => {
form.reset({
name: github?.gitProvider.name || "",
});
}, [form, isOpen]);
const onSubmit = async (data: Schema) => {
await mutateAsync({
githubId,
name: data.name || "",
gitProviderId: github?.gitProviderId || "",
})
.then(async () => {
await utils.gitProvider.getAll.invalidate();
toast.success("Github updated successfully");
setIsOpen(false);
})
.catch(() => {
toast.error("Error to update Github");
});
};
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<Button variant="ghost">
<Edit className="size-4" />
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-2xl overflow-y-auto max-h-screen">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
Update Github Provider <GithubIcon className="size-5" />
</DialogTitle>
</DialogHeader>
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
<Form {...form}>
<form
id="hook-form-add-github"
onSubmit={form.handleSubmit(onSubmit)}
className="grid w-full gap-1"
>
<CardContent className="p-0">
<div className="flex flex-col gap-4">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input
placeholder="Random Name eg(my-personal-account)"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="flex w-full justify-end gap-4 mt-4">
<Button
type="button"
variant={"secondary"}
isLoading={isLoading}
onClick={async () => {
await testConnection({
githubId,
})
.then(async (message) => {
toast.info(`Message: ${message}`);
})
.catch((error) => {
toast.error(`Error: ${error.message}`);
});
}}
>
Test Connection
</Button>
<Button type="submit" isLoading={form.formState.isSubmitting}>
Update
</Button>
</div>
</div>
</CardContent>
</form>
</Form>
</DialogContent>
</Dialog>
);
};

View File

@@ -0,0 +1,249 @@
import { GitlabIcon } from "@/components/icons/data-tools-icons";
import { AlertBlock } from "@/components/shared/alert-block";
import { Button } from "@/components/ui/button";
import { CardContent } from "@/components/ui/card";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { api } from "@/utils/api";
import { useUrl } from "@/utils/hooks/use-url";
import { zodResolver } from "@hookform/resolvers/zod";
import { ExternalLink } from "lucide-react";
import Link from "next/link";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
const Schema = z.object({
name: z.string().min(1, {
message: "Name is required",
}),
applicationId: z.string().min(1, {
message: "Application ID is required",
}),
applicationSecret: z.string().min(1, {
message: "Application Secret is required",
}),
redirectUri: z.string().min(1, {
message: "Redirect URI is required",
}),
groupName: z.string().optional(),
});
type Schema = z.infer<typeof Schema>;
export const AddGitlabProvider = () => {
const utils = api.useUtils();
const [isOpen, setIsOpen] = useState(false);
const url = useUrl();
const { data: auth } = api.auth.get.useQuery();
const { mutateAsync, error, isError } = api.gitlab.create.useMutation();
const webhookUrl = `${url}/api/providers/gitlab/callback`;
const form = useForm<Schema>({
defaultValues: {
applicationId: "",
applicationSecret: "",
groupName: "",
redirectUri: webhookUrl,
},
resolver: zodResolver(Schema),
});
useEffect(() => {
form.reset({
applicationId: "",
applicationSecret: "",
groupName: "",
redirectUri: webhookUrl,
});
}, [form, isOpen]);
const onSubmit = async (data: Schema) => {
await mutateAsync({
applicationId: data.applicationId || "",
secret: data.applicationSecret || "",
groupName: data.groupName || "",
authId: auth?.id || "",
name: data.name || "",
redirectUri: data.redirectUri || "",
})
.then(async () => {
await utils.gitProvider.getAll.invalidate();
toast.success("GitLab created successfully");
setIsOpen(false);
})
.catch(() => {
toast.error("Error configuring GitLab");
});
};
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<Button
variant="default"
className="flex items-center space-x-1 bg-purple-700 text-white hover:bg-purple-600"
>
<GitlabIcon />
<span>GitLab</span>
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-2xl overflow-y-auto max-h-screen ">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
GitLab Provider <GitlabIcon className="size-5" />
</DialogTitle>
</DialogHeader>
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
<Form {...form}>
<form
id="hook-form-add-gitlab"
onSubmit={form.handleSubmit(onSubmit)}
className="grid w-full gap-1"
>
<CardContent className="p-0">
<div className="flex flex-col gap-4">
<p className="text-muted-foreground text-sm">
To integrate your GitLab account, you need to create a new
application in your GitLab settings. Follow these steps:
</p>
<ol className="list-decimal list-inside text-sm text-muted-foreground">
<li className="flex flex-row gap-2 items-center">
Go to your GitLab profile settings{" "}
<Link
href="https://gitlab.com/-/profile/applications"
target="_blank"
>
<ExternalLink className="w-fit text-primary size-4" />
</Link>
</li>
<li>Navigate to Applications</li>
<li>
Create a new application with the following details:
<ul className="list-disc list-inside ml-4">
<li>Name: Dokploy</li>
<li>
Redirect URI:{" "}
<span className="text-primary">{webhookUrl}</span>{" "}
</li>
<li>Scopes: api, read_user, read_repository</li>
</ul>
</li>
<li>
After creating, you'll receive an Application ID and Secret,
copy them and paste them below.
</li>
</ol>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input
placeholder="Random Name eg(my-personal-account)"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="redirectUri"
render={({ field }) => (
<FormItem>
<FormLabel>Redirect URI</FormLabel>
<FormControl>
<Input
disabled
placeholder="Random Name eg(my-personal-account)"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="applicationId"
render={({ field }) => (
<FormItem>
<FormLabel>Application ID</FormLabel>
<FormControl>
<Input placeholder="Application ID" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="applicationSecret"
render={({ field }) => (
<FormItem>
<FormLabel>Application Secret</FormLabel>
<FormControl>
<Input
type="password"
placeholder="Application Secret"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="groupName"
render={({ field }) => (
<FormItem>
<FormLabel>Group Name (Optional)</FormLabel>
<FormControl>
<Input
placeholder="For organization/group access"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button isLoading={form.formState.isSubmitting}>
Configure GitLab App
</Button>
</div>
</CardContent>
</form>
</Form>
</DialogContent>
</Dialog>
);
};

View File

@@ -0,0 +1,180 @@
import { GitlabIcon } from "@/components/icons/data-tools-icons";
import { AlertBlock } from "@/components/shared/alert-block";
import { Button } from "@/components/ui/button";
import { CardContent } from "@/components/ui/card";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { api } from "@/utils/api";
import { useUrl } from "@/utils/hooks/use-url";
import { zodResolver } from "@hookform/resolvers/zod";
import { Edit } from "lucide-react";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
const Schema = z.object({
name: z.string().min(1, {
message: "Name is required",
}),
groupName: z.string().optional(),
});
type Schema = z.infer<typeof Schema>;
interface Props {
gitlabId: string;
}
export const EditGitlabProvider = ({ gitlabId }: Props) => {
const { data: gitlab } = api.gitlab.one.useQuery(
{
gitlabId,
},
{
enabled: !!gitlabId,
},
);
const utils = api.useUtils();
const [isOpen, setIsOpen] = useState(false);
const { mutateAsync, error, isError } = api.gitlab.update.useMutation();
const { mutateAsync: testConnection, isLoading } =
api.gitlab.testConnection.useMutation();
const form = useForm<Schema>({
defaultValues: {
groupName: "",
name: "",
},
resolver: zodResolver(Schema),
});
const groupName = form.watch("groupName");
useEffect(() => {
form.reset({
groupName: gitlab?.groupName || "",
name: gitlab?.gitProvider.name || "",
});
}, [form, isOpen]);
const onSubmit = async (data: Schema) => {
await mutateAsync({
gitlabId,
gitProviderId: gitlab?.gitProviderId || "",
groupName: data.groupName || "",
name: data.name || "",
})
.then(async () => {
await utils.gitProvider.getAll.invalidate();
toast.success("Gitlab updated successfully");
setIsOpen(false);
})
.catch(() => {
toast.error("Error to update Gitlab");
});
};
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<Button variant="ghost">
<Edit className="size-4" />
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-2xl overflow-y-auto max-h-screen">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
Update GitLab Provider <GitlabIcon className="size-5" />
</DialogTitle>
</DialogHeader>
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
<Form {...form}>
<form
id="hook-form-add-bitbucket"
onSubmit={form.handleSubmit(onSubmit)}
className="grid w-full gap-1"
>
<CardContent className="p-0">
<div className="flex flex-col gap-4">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input
placeholder="Random Name eg(my-personal-account)"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="groupName"
render={({ field }) => (
<FormItem>
<FormLabel>Group Name (Optional)</FormLabel>
<FormControl>
<Input
placeholder="For organization/group access"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="flex w-full justify-end gap-4 mt-4">
<Button
type="button"
variant={"secondary"}
isLoading={isLoading}
onClick={async () => {
await testConnection({
gitlabId,
groupName: groupName || "",
})
.then(async (message) => {
toast.info(`Message: ${message}`);
})
.catch((error) => {
toast.error(`Error: ${error.message}`);
});
}}
>
Test Connection
</Button>
<Button type="submit" isLoading={form.formState.isSubmitting}>
Update
</Button>
</div>
</div>
</CardContent>
</form>
</Form>
</DialogContent>
</Dialog>
);
};

View File

@@ -17,31 +17,40 @@ import {
TooltipTrigger,
} from "@/components/ui/tooltip";
import { api } from "@/utils/api";
import { InfoIcon } from "lucide-react";
import { InfoIcon, TrashIcon } from "lucide-react";
import React from "react";
import { toast } from "sonner";
export const RemoveGithubApp = () => {
const { refetch } = api.auth.get.useQuery();
interface Props {
gitProviderId: string;
gitProviderType: "github" | "gitlab" | "bitbucket";
}
export const RemoveGitProvider = ({
gitProviderId,
gitProviderType,
}: Props) => {
const utils = api.useUtils();
const { mutateAsync } = api.admin.cleanGithubApp.useMutation();
const { mutateAsync } = api.gitProvider.remove.useMutation();
return (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="destructive">
Remove Current Github App
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<InfoIcon className="size-4 fill-muted-destructive text-muted-destructive" />
</TooltipTrigger>
<TooltipContent>
We recommend deleting the GitHub app first, and then removing
the current one from here.
</TooltipContent>
</Tooltip>
</TooltipProvider>
<Button variant="ghost">
<TrashIcon className="size-4 text-muted-destructive" />
{gitProviderType === "github" && (
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<InfoIcon className="size-4 fill-muted-destructive text-muted-destructive" />
</TooltipTrigger>
<TooltipContent>
We recommend deleting the GitHub app first, and then removing
the current one from here.
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
@@ -56,15 +65,15 @@ export const RemoveGithubApp = () => {
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={async () => {
await mutateAsync()
await mutateAsync({
gitProviderId: gitProviderId,
})
.then(async () => {
await refetch();
utils.admin.one.invalidate();
await utils.admin.haveGithubConfigured.invalidate();
toast.success("Github application deleted succesfully.");
utils.gitProvider.getAll.invalidate();
toast.success("Git Provider deleted succesfully.");
})
.catch(() => {
toast.error("Error to delete your github application.");
toast.error("Error to delete your git provider.");
});
}}
>

View File

@@ -0,0 +1,185 @@
import {
BitbucketIcon,
GitIcon,
GithubIcon,
GitlabIcon,
} from "@/components/icons/data-tools-icons";
import { buttonVariants } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import { api } from "@/utils/api";
import { useUrl } from "@/utils/hooks/use-url";
import { formatDate } from "date-fns";
import Link from "next/link";
import { AddBitbucketProvider } from "./bitbucket/add-bitbucket-provider";
import { EditBitbucketProvider } from "./bitbucket/edit-bitbucket-provider";
import { AddGithubProvider } from "./github/add-github-provider";
import { EditGithubProvider } from "./github/edit-github-provider";
import { AddGitlabProvider } from "./gitlab/add-gitlab-provider";
import { EditGitlabProvider } from "./gitlab/edit-gitlab-provider";
import { RemoveGitProvider } from "./remove-git-provider";
export const ShowGitProviders = () => {
const { data } = api.gitProvider.getAll.useQuery();
const url = useUrl();
const getGitlabUrl = (clientId: string, gitlabId: string) => {
const redirectUri = `${url}/api/providers/gitlab/callback?gitlabId=${gitlabId}`;
const scope = "api read_user read_repository";
const authUrl = `https://gitlab.com/oauth/authorize?client_id=${clientId}&redirect_uri=${encodeURIComponent(redirectUri)}&response_type=code&scope=${encodeURIComponent(scope)}`;
return authUrl;
};
return (
<div className="p-6 space-y-6">
<div className="space-y-2">
<h1 className="text-2xl font-bold">Git Providers</h1>
<p className="text-muted-foreground">
Connect your Git provider for authentication.
</p>
</div>
<Card className=" bg-transparent">
<CardContent className="p-4">
<div className="flex gap-4 sm:flex-row flex-col w-full">
<AddGithubProvider />
<AddGitlabProvider />
<AddBitbucketProvider />
</div>
</CardContent>
</Card>
<div className="grid gap-4 sm:grid-cols-1 md:grid-cols-1">
{data && data.length === 0 && (
<div className="flex flex-col items-center gap-3 min-h-[25vh] justify-center">
<GitIcon className="size-8" />
<span className="text-base text-muted-foreground">
No Git Providers found. To add a provider, create a new one such
as GitHub, GitLab, or Bitbucket.
</span>
</div>
)}
{data?.map((gitProvider, index) => {
const isGithub = gitProvider.providerType === "github";
const isGitlab = gitProvider.providerType === "gitlab";
const isBitbucket = gitProvider.providerType === "bitbucket";
const haveGithubRequirements =
gitProvider.providerType === "github" &&
gitProvider.github?.githubPrivateKey &&
gitProvider.github?.githubAppId &&
gitProvider.github?.githubInstallationId;
const haveGitlabRequirements =
gitProvider.gitlab?.accessToken && gitProvider.gitlab?.refreshToken;
return (
<div
className="space-y-4"
key={`${gitProvider.gitProviderId}-${index}`}
>
<Card className="flex sm:flex-row max-sm:gap-2 flex-col justify-between items-center p-4 h-full bg-transparent">
<div className="flex items-center space-x-4 w-full">
{gitProvider.providerType === "github" && (
<GithubIcon className="w-6 h-6" />
)}
{gitProvider.providerType === "gitlab" && (
<GitlabIcon className="w-6 h-6" />
)}
{gitProvider.providerType === "bitbucket" && (
<BitbucketIcon className="w-6 h-6" />
)}
<div className="flex flex-col gap-1">
<p className="font-medium">
{gitProvider.providerType === "github"
? "GitHub"
: gitProvider.providerType === "gitlab"
? "GitLab"
: "Bitbucket"}
</p>
<p className="text-sm text-muted-foreground">
{gitProvider.name}
</p>
<span>
<p className="text-sm text-muted-foreground">
Created{" "}
{formatDate(
gitProvider.createdAt,
"yyyy-MM-dd hh:mm:ss a",
)}
</p>
</span>
</div>
</div>
<div className="flex sm:gap-4 sm:flex-row flex-col">
{!haveGithubRequirements && isGithub && (
<div className="flex flex-col gap-1">
<Link
href={`${gitProvider?.github?.githubAppName}/installations/new?state=gh_setup:${gitProvider?.github.githubId}`}
className={buttonVariants({ className: "w-fit" })}
>
Install
</Link>
</div>
)}
{haveGithubRequirements && isGithub && (
<div className="flex flex-col gap-1">
<Link
href={`${gitProvider?.github?.githubAppName}`}
target="_blank"
className={buttonVariants({
className: "w-fit",
variant: "secondary",
})}
>
<span className="text-sm">Manage</span>
</Link>
</div>
)}
{!haveGitlabRequirements && isGitlab && (
<div className="flex flex-col gap-1">
<Link
href={getGitlabUrl(
gitProvider.gitlab?.applicationId || "",
gitProvider.gitlab?.gitlabId || "",
)}
target="_blank"
className={buttonVariants({
className: "w-fit",
variant: "secondary",
})}
>
<span className="text-sm">Install</span>
</Link>
</div>
)}
<div className="flex flex-row gap-1">
{isBitbucket && (
<EditBitbucketProvider
bitbucketId={gitProvider.bitbucket.bitbucketId}
/>
)}
{isGitlab && haveGitlabRequirements && (
<EditGitlabProvider
gitlabId={gitProvider.gitlab.gitlabId}
/>
)}
{isGithub && haveGithubRequirements && (
<EditGithubProvider
githubId={gitProvider.github.githubId}
/>
)}
<RemoveGitProvider
gitProviderId={gitProvider.gitProviderId}
gitProviderType={gitProvider.providerType}
/>
</div>
</div>
</Card>
</div>
);
})}
</div>
</div>
);
};

View File

@@ -1,166 +0,0 @@
import { Button, buttonVariants } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Switch } from "@/components/ui/switch";
import { api } from "@/utils/api";
import { format } from "date-fns";
import { BadgeCheck } from "lucide-react";
import Link from "next/link";
import React, { useEffect, useState } from "react";
import { RemoveGithubApp } from "./remove-github-app";
export const GithubSetup = () => {
const [isOrganization, setIsOrganization] = useState(false);
const { data: haveGithubConfigured } =
api.admin.haveGithubConfigured.useQuery();
const [manifest, setManifest] = useState<string>("");
const [organizationName, setOrganization] = useState<string>("");
const { data } = api.admin.one.useQuery();
useEffect(() => {
const url = document.location.origin;
const manifest = JSON.stringify(
{
redirect_url: `${origin}/api/redirect?authId=${data?.authId}`,
name: `Dokploy-${format(new Date(), "yyyy-MM-dd")}`,
url: origin,
hook_attributes: {
url: `${url}/api/deploy/github`,
// url: `${origin}/api/webhook`, // Aquí especificas la URL del endpoint de tu webhook
},
callback_urls: [`${origin}/api/redirect`], // Los URLs de callback para procesos de autenticación
public: false,
request_oauth_on_install: true,
default_permissions: {
contents: "read",
metadata: "read",
emails: "read",
pull_requests: "write",
},
default_events: ["pull_request", "push"],
},
null,
4,
);
setManifest(manifest);
}, [data?.authId]);
return (
<Card className="bg-transparent">
<CardHeader>
<CardTitle className="text-xl">Configure Github </CardTitle>
<CardDescription>
Setup your github account to access to your repositories.
</CardDescription>
</CardHeader>
<CardContent className="h-full space-y-2">
{haveGithubConfigured ? (
<div className="flex flex-col gap-2">
<div className="flex items-center gap-4">
<span className="text-muted-foreground text-sm">
Github account configured succesfully.
</span>
<BadgeCheck className="size-4 text-green-700" />
</div>
<div className="flex items-end gap-4 flex-wrap">
<RemoveGithubApp />
<Link
href={`${data?.githubAppName}`}
target="_blank"
className={buttonVariants({
className: "w-fit",
variant: "secondary",
})}
>
<span className="text-sm">Manage Github App</span>
</Link>
</div>
</div>
) : (
<>
{data?.githubAppName ? (
<div className="flex w-fit flex-col gap-4">
<span className="text-muted-foreground">
You've successfully created a github app named{" "}
<strong>{data.githubAppName}</strong>! The next step is to
install this app in your GitHub account.
</span>
<div className="flex flex-row gap-4">
<Link
href={`${
data.githubAppName
}/installations/new?state=gh_setup:${data?.authId}`}
className={buttonVariants({ className: "w-fit" })}
>
Install Github App
</Link>
<RemoveGithubApp />
</div>
</div>
) : (
<div>
<div className="flex items-center gap-2">
<p className="text-muted-foreground text-sm">
To integrate your GitHub account with our services, you'll
need to create and install a GitHub app. This process is
straightforward and only takes a few minutes. Click the
button below to get started.
</p>
</div>
<div className="mt-4 flex flex-col gap-4">
<div className="flex flex-row gap-4">
<span>Organization?</span>
<Switch
checked={isOrganization}
onCheckedChange={(checked) => setIsOrganization(checked)}
/>
</div>
{isOrganization && (
<Input
required
placeholder="Organization name"
onChange={(e) => setOrganization(e.target.value)}
/>
)}
</div>
<form
action={
isOrganization
? `https://github.com/organizations/${organizationName}/settings/apps/new?state=gh_init:${data?.authId}`
: `https://github.com/settings/apps/new?state=gh_init:${data?.authId}`
}
method="post"
>
<input
type="text"
name="manifest"
id="manifest"
defaultValue={manifest}
className="invisible"
/>
<br />
<Button
disabled={isOrganization && organizationName.length < 1}
type="submit"
>
Create GitHub App
</Button>
</form>
</div>
)}
</>
)}
</CardContent>
</Card>
);
};

View File

@@ -1,40 +0,0 @@
import { cn } from "@/lib/utils";
import { api } from "@/utils/api";
import React from "react";
import { AppearanceForm } from "./appearance-form";
import { ShowCertificates } from "./certificates/show-certificates";
import { ShowDestinations } from "./destination/show-destinations";
import { GithubSetup } from "./github/github-setup";
import { ProfileForm } from "./profile/profile-form";
import { ShowUsers } from "./users/show-users";
import { WebDomain } from "./web-domain";
import { WebServer } from "./web-server";
export const ShowSettings = () => {
const { data } = api.auth.get.useQuery();
return (
<div
className={cn(
"mt-6 md:grid flex flex-col gap-4 pb-20 md:grid-cols-2",
data?.rol === "user" && "col-span-2",
)}
>
<div className={cn(data?.rol === "user" && "col-span-2")}>
<ProfileForm />
</div>
{data?.rol === "admin" && (
<>
<GithubSetup />
<AppearanceForm />
<ShowDestinations />
<ShowCertificates />
<WebDomain />
<WebServer />
<ShowUsers />
</>
)}
</div>
);
};

View File

@@ -40,6 +40,7 @@ const addPermissions = z.object({
canAccessToDocker: z.boolean().optional().default(false),
canAccessToAPI: z.boolean().optional().default(false),
canAccessToSSHKeys: z.boolean().optional().default(false),
canAccessToGitProviders: z.boolean().optional().default(false),
});
type AddPermissions = z.infer<typeof addPermissions>;
@@ -84,6 +85,7 @@ export const AddUserPermissions = ({ userId }: Props) => {
canAccessToDocker: data.canAccessToDocker,
canAccessToAPI: data.canAccessToAPI,
canAccessToSSHKeys: data.canAccessToSSHKeys,
canAccessToGitProviders: data.canAccessToGitProviders,
});
}
}, [form, form.formState.isSubmitSuccessful, form.reset, data]);
@@ -101,6 +103,7 @@ export const AddUserPermissions = ({ userId }: Props) => {
canAccessToDocker: data.canAccessToDocker,
canAccessToAPI: data.canAccessToAPI,
canAccessToSSHKeys: data.canAccessToSSHKeys,
canAccessToGitProviders: data.canAccessToGitProviders,
})
.then(async () => {
toast.success("Permissions updated");
@@ -293,6 +296,26 @@ export const AddUserPermissions = ({ userId }: Props) => {
</FormItem>
)}
/>
<FormField
control={form.control}
name="canAccessToGitProviders"
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
<div className="space-y-0.5">
<FormLabel>Access to Git Providers</FormLabel>
<FormDescription>
Allow to users to access to the Git Providers section
</FormDescription>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name="accesedProjects"

View File

@@ -60,7 +60,9 @@ export const EditTraefikEnv = ({ children }: Props) => {
}, [form, form.reset, data]);
const onSubmit = async (data: Schema) => {
await mutateAsync(data.env)
await mutateAsync({
env: data.env,
})
.then(async () => {
toast.success("Traefik Env Updated");
})

View File

@@ -1,3 +1,4 @@
import { cn } from "@/lib/utils";
import React from "react";
// https://worldvectorlogo.com/downloaded/redis Ref
@@ -155,3 +156,118 @@ export const RedisIcon = ({ className }: Props) => {
</svg>
);
};
export const GitlabIcon = ({ className }: Props) => {
return (
<svg
aria-label="gitlab"
height="14"
viewBox="0 0 24 22"
width="14"
className={cn("fill-white text-white", className)}
>
<path
d="M1.279 8.29L.044 12.294c-.117.367 0 .78.325 1.014l11.323 8.23-.009-.012-.03-.039L1.279 8.29zM22.992 13.308a.905.905 0 00.325-1.014L22.085 8.29 11.693 21.52l11.299-8.212z"
fill="currentColor"
/>
<path
d="M1.279 8.29l10.374 13.197.03.039.01-.006L22.085 8.29H1.28z"
fill="currentColor"
opacity="0.4"
/>
<path
d="M15.982 8.29l-4.299 13.236-.004.011.014-.017L22.085 8.29h-6.103zM7.376 8.29H1.279l10.374 13.197L7.376 8.29z"
fill="currentColor"
opacity="0.6"
/>
<path
d="M18.582.308l-2.6 7.982h6.103L19.48.308c-.133-.41-.764-.41-.897 0zM1.279 8.29L3.88.308c.133-.41.764-.41.897 0l2.6 7.982H1.279z"
fill="currentColor"
opacity="0.4"
/>
</svg>
);
};
export const GithubIcon = ({ className }: Props) => {
return (
<svg
aria-label="github"
height="18"
viewBox="0 0 14 14"
width="18"
className={className}
>
<path
d="M7 .175c-3.872 0-7 3.128-7 7 0 3.084 2.013 5.71 4.79 6.65.35.066.482-.153.482-.328v-1.181c-1.947.415-2.363-.941-2.363-.941-.328-.81-.787-1.028-.787-1.028-.634-.438.044-.416.044-.416.7.044 1.071.722 1.071.722.635 1.072 1.641.766 2.035.59.066-.459.24-.765.437-.94-1.553-.175-3.193-.787-3.193-3.456 0-.766.262-1.378.721-1.881-.065-.175-.306-.897.066-1.86 0 0 .59-.197 1.925.722a6.754 6.754 0 0 1 1.75-.24c.59 0 1.203.087 1.75.24 1.335-.897 1.925-.722 1.925-.722.372.963.131 1.685.066 1.86.46.48.722 1.115.722 1.88 0 2.691-1.641 3.282-3.194 3.457.24.219.481.634.481 1.29v1.926c0 .197.131.415.481.328C11.988 12.884 14 10.259 14 7.175c0-3.872-3.128-7-7-7z"
fill="#fff"
fillRule="nonzero"
/>
</svg>
);
};
export const BitbucketIcon = ({ className }: Props) => {
return (
<svg height="14" viewBox="-2 -2 65 59" width="14" className={className}>
<defs>
<linearGradient
id="bitbucket-:R7aq37rqjt7rrrmpjtuj7l9qjtsr:"
x1="104.953%"
x2="46.569%"
y1="21.921%"
y2="75.234%"
>
<stop offset="7%" stopColor="currentColor" stopOpacity=".4" />
<stop offset="100%" stopColor="currentColor" />
</linearGradient>
</defs>
<path
d="M59.696 18.86h-18.77l-3.15 18.39h-13L9.426 55.47a2.71 2.71 0 001.75.66h40.74a2 2 0 002-1.68l5.78-35.59z"
fill="url(#bitbucket-:R7aq37rqjt7rrrmpjtuj7l9qjtsr:)"
fillRule="nonzero"
transform="translate(-.026 .82)"
/>
<path
d="M2 .82a2 2 0 00-2 2.32l8.49 51.54a2.7 2.7 0 00.91 1.61 2.71 2.71 0 001.75.66l15.76-18.88H24.7l-3.47-18.39h38.44l2.7-16.53a2 2 0 00-2-2.32L2 .82z"
fill="currentColor"
fillRule="nonzero"
/>
</svg>
);
};
export const DockerIcon = ({ className }: Props) => {
return (
<svg
height="24"
viewBox="-.557 117.607 598.543 423.631"
width="24"
xmlns="http://www.w3.org/2000/svg"
className={className}
>
<g fill="#0091e2">
<path d="m592.162 277.804c-1.664-1.37-16.642-12.597-48.815-12.597-8.321 0-16.92.822-25.24 2.191-6.102-41.898-41.327-62.162-42.714-63.257l-8.598-4.93-5.547 7.942c-6.934 10.68-12.204 22.729-15.255 35.052-5.824 23.824-2.219 46.279 9.985 65.447-14.7 8.216-38.553 10.133-43.545 10.406h-393.853c-10.262 0-18.583 8.216-18.583 18.348-.554 33.956 5.27 67.912 17.197 99.951 13.59 35.052 33.838 61.067 59.91 76.95 29.4 17.799 77.383 27.931 131.468 27.931 24.408 0 48.815-2.19 72.946-6.572 33.56-6.025 65.734-17.526 95.412-34.23a260.485 260.485 0 0 0 64.902-52.577c31.342-34.778 49.925-73.663 63.515-108.167h5.547c34.116 0 55.195-13.418 66.844-24.92 7.766-7.12 13.59-15.882 17.751-25.74l2.497-7.12z" />
<path d="m55.193 306.83h52.698c2.497 0 4.716-1.916 4.716-4.654v-46.553c0-2.465-1.942-4.655-4.716-4.655h-52.698c-2.496 0-4.715 1.916-4.715 4.655v46.553c.277 2.738 2.219 4.655 4.715 4.655zm72.668 0h52.699c2.496 0 4.715-1.916 4.715-4.654v-46.553c0-2.465-1.942-4.655-4.715-4.655h-52.7c-2.496 0-4.715 1.916-4.715 4.655v46.553c.278 2.738 2.22 4.655 4.715 4.655m74.055 0h52.699c2.496 0 4.715-1.917 4.715-4.655v-46.553c0-2.465-1.942-4.655-4.715-4.655h-52.699c-2.496 0-4.715 1.916-4.715 4.655v46.553c0 2.738 1.942 4.655 4.715 4.655zm72.946 0h52.699c2.496 0 4.715-1.917 4.715-4.655v-46.553c0-2.465-1.942-4.655-4.715-4.655h-52.699c-2.496 0-4.715 1.916-4.715 4.655v46.553c0 2.738 2.219 4.655 4.715 4.655zm-147-66.543h52.698c2.496 0 4.715-2.19 4.715-4.655v-46.553c0-2.465-1.942-4.656-4.715-4.656h-52.699c-2.496 0-4.715 1.917-4.715 4.656v46.553c.278 2.464 2.22 4.655 4.715 4.655m74.055 0h52.699c2.496 0 4.715-2.19 4.715-4.655v-46.553c0-2.465-1.942-4.656-4.715-4.656h-52.699c-2.496 0-4.715 1.917-4.715 4.656v46.553c0 2.464 1.942 4.655 4.715 4.655m72.946 0h52.699c2.496 0 4.715-2.19 4.715-4.655v-46.553c0-2.465-2.22-4.656-4.715-4.656h-52.699c-2.496 0-4.715 1.917-4.715 4.656v46.553c0 2.464 2.219 4.655 4.715 4.655m0-66.817h52.699c2.496 0 4.715-1.917 4.715-4.655v-46.553c0-2.465-2.22-4.656-4.715-4.656h-52.699c-2.496 0-4.715 1.917-4.715 4.656v46.553c0 2.464 2.219 4.655 4.715 4.655m73.5 133.36h52.699c2.496 0 4.715-1.917 4.715-4.655v-46.553c0-2.465-1.941-4.655-4.715-4.655h-52.698c-2.497 0-4.716 1.916-4.716 4.655v46.553c.278 2.738 2.22 4.655 4.716 4.655" />
</g>
</svg>
);
};
export const GitIcon = ({ className }: Props) => {
return (
<svg
width="20"
height="20"
viewBox="0 0 256 256"
xmlns="http://www.w3.org/2000/svg"
preserveAspectRatio="xMinYMin meet"
className={className}
>
<path
d="M251.172 116.594L139.4 4.828c-6.433-6.437-16.873-6.437-23.314 0l-23.21 23.21 29.443 29.443c6.842-2.312 14.688-.761 20.142 4.693 5.48 5.489 7.02 13.402 4.652 20.266l28.375 28.376c6.865-2.365 14.786-.835 20.269 4.657 7.663 7.66 7.663 20.075 0 27.74-7.665 7.666-20.08 7.666-27.749 0-5.764-5.77-7.188-14.235-4.27-21.336l-26.462-26.462-.003 69.637a19.82 19.82 0 0 1 5.188 3.71c7.663 7.66 7.663 20.076 0 27.747-7.665 7.662-20.086 7.662-27.74 0-7.663-7.671-7.663-20.086 0-27.746a19.654 19.654 0 0 1 6.421-4.281V94.196a19.378 19.378 0 0 1-6.421-4.281c-5.806-5.798-7.202-14.317-4.227-21.446L81.47 39.442l-76.64 76.635c-6.44 6.443-6.44 16.884 0 23.322l111.774 111.768c6.435 6.438 16.873 6.438 23.316 0l111.251-111.249c6.438-6.44 6.438-16.887 0-23.324"
fill="#DE4C36"
/>
</svg>
);
};

View File

@@ -59,6 +59,12 @@ export const SettingsLayout = ({ children }: Props) => {
icon: KeyRound,
href: "/dashboard/settings/ssh-keys",
},
{
title: "Git ",
label: "",
icon: GitBranch,
href: "/dashboard/settings/git-providers",
},
{
title: "Users",
label: "",
@@ -95,6 +101,16 @@ export const SettingsLayout = ({ children }: Props) => {
},
]
: []),
...(user?.canAccessToGitProviders
? [
{
title: "Git",
label: "",
icon: GitBranch,
href: "/dashboard/settings/git-providers",
},
]
: []),
]}
/>
</div>
@@ -109,6 +125,7 @@ import {
Bell,
Database,
KeyIcon,
GitBranch,
KeyRound,
type LucideIcon,
Route,

View File

@@ -0,0 +1,142 @@
DO $$ BEGIN
CREATE TYPE "public"."gitProviderType" AS ENUM('github', 'gitlab', 'bitbucket');
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
ALTER TYPE "sourceType" ADD VALUE 'gitlab';--> statement-breakpoint
ALTER TYPE "sourceType" ADD VALUE 'bitbucket';--> statement-breakpoint
ALTER TYPE "sourceTypeCompose" ADD VALUE 'gitlab';--> statement-breakpoint
ALTER TYPE "sourceTypeCompose" ADD VALUE 'bitbucket';--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "git_provider" (
"gitProviderId" text PRIMARY KEY NOT NULL,
"name" text NOT NULL,
"providerType" "gitProviderType" DEFAULT 'github' NOT NULL,
"createdAt" text NOT NULL,
"authId" text NOT NULL
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "bitbucket" (
"bitbucketId" text PRIMARY KEY NOT NULL,
"bitbucketUsername" text,
"appPassword" text,
"bitbucketWorkspaceName" text,
"gitProviderId" text NOT NULL
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "github" (
"githubId" text PRIMARY KEY NOT NULL,
"githubAppName" text,
"githubAppId" integer,
"githubClientId" text,
"githubClientSecret" text,
"githubInstallationId" text,
"githubPrivateKey" text,
"githubWebhookSecret" text,
"gitProviderId" text NOT NULL
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "gitlab" (
"gitlabId" text PRIMARY KEY NOT NULL,
"application_id" text,
"redirect_uri" text,
"secret" text,
"access_token" text,
"refresh_token" text,
"group_name" text,
"expires_at" integer,
"gitProviderId" text NOT NULL
);
--> statement-breakpoint
ALTER TABLE "application" ADD COLUMN "gitlabProjectId" integer;--> statement-breakpoint
ALTER TABLE "application" ADD COLUMN "gitlabRepository" text;--> statement-breakpoint
ALTER TABLE "application" ADD COLUMN "gitlabOwner" text;--> statement-breakpoint
ALTER TABLE "application" ADD COLUMN "gitlabBranch" text;--> statement-breakpoint
ALTER TABLE "application" ADD COLUMN "gitlabBuildPath" text DEFAULT '/';--> statement-breakpoint
ALTER TABLE "application" ADD COLUMN "gitlabPathNamespace" text;--> statement-breakpoint
ALTER TABLE "application" ADD COLUMN "bitbucketRepository" text;--> statement-breakpoint
ALTER TABLE "application" ADD COLUMN "bitbucketOwner" text;--> statement-breakpoint
ALTER TABLE "application" ADD COLUMN "bitbucketBranch" text;--> statement-breakpoint
ALTER TABLE "application" ADD COLUMN "bitbucketBuildPath" text DEFAULT '/';--> statement-breakpoint
ALTER TABLE "application" ADD COLUMN "githubId" text;--> statement-breakpoint
ALTER TABLE "application" ADD COLUMN "gitlabId" text;--> statement-breakpoint
ALTER TABLE "application" ADD COLUMN "bitbucketId" text;--> statement-breakpoint
ALTER TABLE "user" ADD COLUMN "canAccessToGitProviders" boolean DEFAULT false NOT NULL;--> statement-breakpoint
ALTER TABLE "compose" ADD COLUMN "gitlabProjectId" integer;--> statement-breakpoint
ALTER TABLE "compose" ADD COLUMN "gitlabRepository" text;--> statement-breakpoint
ALTER TABLE "compose" ADD COLUMN "gitlabOwner" text;--> statement-breakpoint
ALTER TABLE "compose" ADD COLUMN "gitlabBranch" text;--> statement-breakpoint
ALTER TABLE "compose" ADD COLUMN "gitlabPathNamespace" text;--> statement-breakpoint
ALTER TABLE "compose" ADD COLUMN "bitbucketRepository" text;--> statement-breakpoint
ALTER TABLE "compose" ADD COLUMN "bitbucketOwner" text;--> statement-breakpoint
ALTER TABLE "compose" ADD COLUMN "bitbucketBranch" text;--> statement-breakpoint
ALTER TABLE "compose" ADD COLUMN "githubId" text;--> statement-breakpoint
ALTER TABLE "compose" ADD COLUMN "gitlabId" text;--> statement-breakpoint
ALTER TABLE "compose" ADD COLUMN "bitbucketId" text;--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "git_provider" ADD CONSTRAINT "git_provider_authId_auth_id_fk" FOREIGN KEY ("authId") REFERENCES "public"."auth"("id") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "bitbucket" ADD CONSTRAINT "bitbucket_gitProviderId_git_provider_gitProviderId_fk" FOREIGN KEY ("gitProviderId") REFERENCES "public"."git_provider"("gitProviderId") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "github" ADD CONSTRAINT "github_gitProviderId_git_provider_gitProviderId_fk" FOREIGN KEY ("gitProviderId") REFERENCES "public"."git_provider"("gitProviderId") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "gitlab" ADD CONSTRAINT "gitlab_gitProviderId_git_provider_gitProviderId_fk" FOREIGN KEY ("gitProviderId") REFERENCES "public"."git_provider"("gitProviderId") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "application" ADD CONSTRAINT "application_githubId_github_githubId_fk" FOREIGN KEY ("githubId") REFERENCES "public"."github"("githubId") ON DELETE set null ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "application" ADD CONSTRAINT "application_gitlabId_gitlab_gitlabId_fk" FOREIGN KEY ("gitlabId") REFERENCES "public"."gitlab"("gitlabId") ON DELETE set null ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "application" ADD CONSTRAINT "application_bitbucketId_bitbucket_bitbucketId_fk" FOREIGN KEY ("bitbucketId") REFERENCES "public"."bitbucket"("bitbucketId") ON DELETE set null ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "compose" ADD CONSTRAINT "compose_githubId_github_githubId_fk" FOREIGN KEY ("githubId") REFERENCES "public"."github"("githubId") ON DELETE set null ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "compose" ADD CONSTRAINT "compose_gitlabId_gitlab_gitlabId_fk" FOREIGN KEY ("gitlabId") REFERENCES "public"."gitlab"("gitlabId") ON DELETE set null ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "compose" ADD CONSTRAINT "compose_bitbucketId_bitbucket_bitbucketId_fk" FOREIGN KEY ("bitbucketId") REFERENCES "public"."bitbucket"("bitbucketId") ON DELETE set null ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
ALTER TABLE "admin" DROP COLUMN IF EXISTS "githubAppId";--> statement-breakpoint
ALTER TABLE "admin" DROP COLUMN IF EXISTS "githubAppName";--> statement-breakpoint
ALTER TABLE "admin" DROP COLUMN IF EXISTS "githubClientId";--> statement-breakpoint
ALTER TABLE "admin" DROP COLUMN IF EXISTS "githubClientSecret";--> statement-breakpoint
ALTER TABLE "admin" DROP COLUMN IF EXISTS "githubInstallationId";--> statement-breakpoint
ALTER TABLE "admin" DROP COLUMN IF EXISTS "githubPrivateKey";--> statement-breakpoint
ALTER TABLE "admin" DROP COLUMN IF EXISTS "githubWebhookSecret";

View File

@@ -0,0 +1 @@
ALTER TABLE "application" ADD COLUMN "dockerBuildStage" text;

View File

@@ -0,0 +1,2 @@
ALTER TABLE "compose" ADD COLUMN "suffix" text DEFAULT '' NOT NULL;--> statement-breakpoint
ALTER TABLE "compose" ADD COLUMN "randomize" boolean DEFAULT false NOT NULL;

View File

@@ -1,5 +1,5 @@
{
"id": "1db6392e-e3bf-424b-bbcd-0e687b2a3ff5",
"id": "4b757666-9e18-454e-9cfa-762d03bf378f",
"prevId": "ce8a8861-2970-4889-ac2e-3cfe60d12736",
"version": "6",
"dialect": "postgresql",
@@ -137,6 +137,68 @@
"primaryKey": false,
"notNull": false
},
"gitlabProjectId": {
"name": "gitlabProjectId",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"gitlabRepository": {
"name": "gitlabRepository",
"type": "text",
"primaryKey": false,
"notNull": false
},
"gitlabOwner": {
"name": "gitlabOwner",
"type": "text",
"primaryKey": false,
"notNull": false
},
"gitlabBranch": {
"name": "gitlabBranch",
"type": "text",
"primaryKey": false,
"notNull": false
},
"gitlabBuildPath": {
"name": "gitlabBuildPath",
"type": "text",
"primaryKey": false,
"notNull": false,
"default": "'/'"
},
"gitlabPathNamespace": {
"name": "gitlabPathNamespace",
"type": "text",
"primaryKey": false,
"notNull": false
},
"bitbucketRepository": {
"name": "bitbucketRepository",
"type": "text",
"primaryKey": false,
"notNull": false
},
"bitbucketOwner": {
"name": "bitbucketOwner",
"type": "text",
"primaryKey": false,
"notNull": false
},
"bitbucketBranch": {
"name": "bitbucketBranch",
"type": "text",
"primaryKey": false,
"notNull": false
},
"bitbucketBuildPath": {
"name": "bitbucketBuildPath",
"type": "text",
"primaryKey": false,
"notNull": false,
"default": "'/'"
},
"username": {
"name": "username",
"type": "text",
@@ -291,6 +353,24 @@
"type": "text",
"primaryKey": false,
"notNull": true
},
"githubId": {
"name": "githubId",
"type": "text",
"primaryKey": false,
"notNull": false
},
"gitlabId": {
"name": "gitlabId",
"type": "text",
"primaryKey": false,
"notNull": false
},
"bitbucketId": {
"name": "bitbucketId",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
@@ -333,6 +413,45 @@
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"application_githubId_github_githubId_fk": {
"name": "application_githubId_github_githubId_fk",
"tableFrom": "application",
"tableTo": "github",
"columnsFrom": [
"githubId"
],
"columnsTo": [
"githubId"
],
"onDelete": "set null",
"onUpdate": "no action"
},
"application_gitlabId_gitlab_gitlabId_fk": {
"name": "application_gitlabId_gitlab_gitlabId_fk",
"tableFrom": "application",
"tableTo": "gitlab",
"columnsFrom": [
"gitlabId"
],
"columnsTo": [
"gitlabId"
],
"onDelete": "set null",
"onUpdate": "no action"
},
"application_bitbucketId_bitbucket_bitbucketId_fk": {
"name": "application_bitbucketId_bitbucket_bitbucketId_fk",
"tableFrom": "application",
"tableTo": "bitbucket",
"columnsFrom": [
"bitbucketId"
],
"columnsTo": [
"bitbucketId"
],
"onDelete": "set null",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
@@ -572,6 +691,13 @@
"notNull": true,
"default": false
},
"canAccessToGitProviders": {
"name": "canAccessToGitProviders",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"canAccessToTraefikFiles": {
"name": "canAccessToTraefikFiles",
"type": "boolean",
@@ -648,18 +774,6 @@
"primaryKey": true,
"notNull": true
},
"githubAppId": {
"name": "githubAppId",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"githubAppName": {
"name": "githubAppName",
"type": "text",
"primaryKey": false,
"notNull": false
},
"serverIp": {
"name": "serverIp",
"type": "text",
@@ -680,36 +794,6 @@
"primaryKey": false,
"notNull": false
},
"githubClientId": {
"name": "githubClientId",
"type": "text",
"primaryKey": false,
"notNull": false
},
"githubClientSecret": {
"name": "githubClientSecret",
"type": "text",
"primaryKey": false,
"notNull": false
},
"githubInstallationId": {
"name": "githubInstallationId",
"type": "text",
"primaryKey": false,
"notNull": false
},
"githubPrivateKey": {
"name": "githubPrivateKey",
"type": "text",
"primaryKey": false,
"notNull": false
},
"githubWebhookSecret": {
"name": "githubWebhookSecret",
"type": "text",
"primaryKey": false,
"notNull": false
},
"letsEncryptEmail": {
"name": "letsEncryptEmail",
"type": "text",
@@ -729,13 +813,6 @@
"notNull": true,
"default": false
},
"enableLogRotation": {
"name": "enableLogRotation",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"authId": {
"name": "authId",
"type": "text",
@@ -2439,6 +2516,54 @@
"primaryKey": false,
"notNull": false
},
"gitlabProjectId": {
"name": "gitlabProjectId",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"gitlabRepository": {
"name": "gitlabRepository",
"type": "text",
"primaryKey": false,
"notNull": false
},
"gitlabOwner": {
"name": "gitlabOwner",
"type": "text",
"primaryKey": false,
"notNull": false
},
"gitlabBranch": {
"name": "gitlabBranch",
"type": "text",
"primaryKey": false,
"notNull": false
},
"gitlabPathNamespace": {
"name": "gitlabPathNamespace",
"type": "text",
"primaryKey": false,
"notNull": false
},
"bitbucketRepository": {
"name": "bitbucketRepository",
"type": "text",
"primaryKey": false,
"notNull": false
},
"bitbucketOwner": {
"name": "bitbucketOwner",
"type": "text",
"primaryKey": false,
"notNull": false
},
"bitbucketBranch": {
"name": "bitbucketBranch",
"type": "text",
"primaryKey": false,
"notNull": false
},
"customGitUrl": {
"name": "customGitUrl",
"type": "text",
@@ -2490,6 +2615,24 @@
"type": "text",
"primaryKey": false,
"notNull": true
},
"githubId": {
"name": "githubId",
"type": "text",
"primaryKey": false,
"notNull": false
},
"gitlabId": {
"name": "gitlabId",
"type": "text",
"primaryKey": false,
"notNull": false
},
"bitbucketId": {
"name": "bitbucketId",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
@@ -2519,6 +2662,45 @@
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"compose_githubId_github_githubId_fk": {
"name": "compose_githubId_github_githubId_fk",
"tableFrom": "compose",
"tableTo": "github",
"columnsFrom": [
"githubId"
],
"columnsTo": [
"githubId"
],
"onDelete": "set null",
"onUpdate": "no action"
},
"compose_gitlabId_gitlab_gitlabId_fk": {
"name": "compose_gitlabId_gitlab_gitlabId_fk",
"tableFrom": "compose",
"tableTo": "gitlab",
"columnsFrom": [
"gitlabId"
],
"columnsTo": [
"gitlabId"
],
"onDelete": "set null",
"onUpdate": "no action"
},
"compose_bitbucketId_bitbucket_bitbucketId_fk": {
"name": "compose_bitbucketId_bitbucket_bitbucketId_fk",
"tableFrom": "compose",
"tableTo": "bitbucket",
"columnsFrom": [
"bitbucketId"
],
"columnsTo": [
"bitbucketId"
],
"onDelete": "set null",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
@@ -2926,6 +3108,272 @@
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"public.git_provider": {
"name": "git_provider",
"schema": "",
"columns": {
"gitProviderId": {
"name": "gitProviderId",
"type": "text",
"primaryKey": true,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"providerType": {
"name": "providerType",
"type": "gitProviderType",
"typeSchema": "public",
"primaryKey": false,
"notNull": true,
"default": "'github'"
},
"createdAt": {
"name": "createdAt",
"type": "text",
"primaryKey": false,
"notNull": true
},
"authId": {
"name": "authId",
"type": "text",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"git_provider_authId_auth_id_fk": {
"name": "git_provider_authId_auth_id_fk",
"tableFrom": "git_provider",
"tableTo": "auth",
"columnsFrom": [
"authId"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"public.bitbucket": {
"name": "bitbucket",
"schema": "",
"columns": {
"bitbucketId": {
"name": "bitbucketId",
"type": "text",
"primaryKey": true,
"notNull": true
},
"bitbucketUsername": {
"name": "bitbucketUsername",
"type": "text",
"primaryKey": false,
"notNull": false
},
"appPassword": {
"name": "appPassword",
"type": "text",
"primaryKey": false,
"notNull": false
},
"bitbucketWorkspaceName": {
"name": "bitbucketWorkspaceName",
"type": "text",
"primaryKey": false,
"notNull": false
},
"gitProviderId": {
"name": "gitProviderId",
"type": "text",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"bitbucket_gitProviderId_git_provider_gitProviderId_fk": {
"name": "bitbucket_gitProviderId_git_provider_gitProviderId_fk",
"tableFrom": "bitbucket",
"tableTo": "git_provider",
"columnsFrom": [
"gitProviderId"
],
"columnsTo": [
"gitProviderId"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"public.github": {
"name": "github",
"schema": "",
"columns": {
"githubId": {
"name": "githubId",
"type": "text",
"primaryKey": true,
"notNull": true
},
"githubAppName": {
"name": "githubAppName",
"type": "text",
"primaryKey": false,
"notNull": false
},
"githubAppId": {
"name": "githubAppId",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"githubClientId": {
"name": "githubClientId",
"type": "text",
"primaryKey": false,
"notNull": false
},
"githubClientSecret": {
"name": "githubClientSecret",
"type": "text",
"primaryKey": false,
"notNull": false
},
"githubInstallationId": {
"name": "githubInstallationId",
"type": "text",
"primaryKey": false,
"notNull": false
},
"githubPrivateKey": {
"name": "githubPrivateKey",
"type": "text",
"primaryKey": false,
"notNull": false
},
"githubWebhookSecret": {
"name": "githubWebhookSecret",
"type": "text",
"primaryKey": false,
"notNull": false
},
"gitProviderId": {
"name": "gitProviderId",
"type": "text",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"github_gitProviderId_git_provider_gitProviderId_fk": {
"name": "github_gitProviderId_git_provider_gitProviderId_fk",
"tableFrom": "github",
"tableTo": "git_provider",
"columnsFrom": [
"gitProviderId"
],
"columnsTo": [
"gitProviderId"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"public.gitlab": {
"name": "gitlab",
"schema": "",
"columns": {
"gitlabId": {
"name": "gitlabId",
"type": "text",
"primaryKey": true,
"notNull": true
},
"application_id": {
"name": "application_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"redirect_uri": {
"name": "redirect_uri",
"type": "text",
"primaryKey": false,
"notNull": false
},
"secret": {
"name": "secret",
"type": "text",
"primaryKey": false,
"notNull": false
},
"access_token": {
"name": "access_token",
"type": "text",
"primaryKey": false,
"notNull": false
},
"refresh_token": {
"name": "refresh_token",
"type": "text",
"primaryKey": false,
"notNull": false
},
"group_name": {
"name": "group_name",
"type": "text",
"primaryKey": false,
"notNull": false
},
"expires_at": {
"name": "expires_at",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"gitProviderId": {
"name": "gitProviderId",
"type": "text",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"gitlab_gitProviderId_git_provider_gitProviderId_fk": {
"name": "gitlab_gitProviderId_git_provider_gitProviderId_fk",
"tableFrom": "gitlab",
"tableTo": "git_provider",
"columnsFrom": [
"gitProviderId"
],
"columnsTo": [
"gitProviderId"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
}
},
"enums": {
@@ -2947,6 +3395,8 @@
"docker",
"git",
"github",
"gitlab",
"bitbucket",
"drop"
]
},
@@ -3047,6 +3497,8 @@
"values": [
"git",
"github",
"gitlab",
"bitbucket",
"raw"
]
},
@@ -3067,6 +3519,15 @@
"discord",
"email"
]
},
"public.gitProviderType": {
"name": "gitProviderType",
"schema": "public",
"values": [
"github",
"gitlab",
"bitbucket"
]
}
},
"schemas": {},

View File

@@ -1,6 +1,6 @@
{
"id": "d9f4f668-e2a8-4a4e-bccf-67011b0a4a0d",
"prevId": "1db6392e-e3bf-424b-bbcd-0e687b2a3ff5",
"id": "6a7ce86e-f628-4786-ab30-e160dcdb0a39",
"prevId": "4b757666-9e18-454e-9cfa-762d03bf378f",
"version": "6",
"dialect": "postgresql",
"tables": {
@@ -137,6 +137,68 @@
"primaryKey": false,
"notNull": false
},
"gitlabProjectId": {
"name": "gitlabProjectId",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"gitlabRepository": {
"name": "gitlabRepository",
"type": "text",
"primaryKey": false,
"notNull": false
},
"gitlabOwner": {
"name": "gitlabOwner",
"type": "text",
"primaryKey": false,
"notNull": false
},
"gitlabBranch": {
"name": "gitlabBranch",
"type": "text",
"primaryKey": false,
"notNull": false
},
"gitlabBuildPath": {
"name": "gitlabBuildPath",
"type": "text",
"primaryKey": false,
"notNull": false,
"default": "'/'"
},
"gitlabPathNamespace": {
"name": "gitlabPathNamespace",
"type": "text",
"primaryKey": false,
"notNull": false
},
"bitbucketRepository": {
"name": "bitbucketRepository",
"type": "text",
"primaryKey": false,
"notNull": false
},
"bitbucketOwner": {
"name": "bitbucketOwner",
"type": "text",
"primaryKey": false,
"notNull": false
},
"bitbucketBranch": {
"name": "bitbucketBranch",
"type": "text",
"primaryKey": false,
"notNull": false
},
"bitbucketBuildPath": {
"name": "bitbucketBuildPath",
"type": "text",
"primaryKey": false,
"notNull": false,
"default": "'/'"
},
"username": {
"name": "username",
"type": "text",
@@ -191,6 +253,12 @@
"primaryKey": false,
"notNull": false
},
"dockerBuildStage": {
"name": "dockerBuildStage",
"type": "text",
"primaryKey": false,
"notNull": false
},
"dropBuildPath": {
"name": "dropBuildPath",
"type": "text",
@@ -291,6 +359,24 @@
"type": "text",
"primaryKey": false,
"notNull": true
},
"githubId": {
"name": "githubId",
"type": "text",
"primaryKey": false,
"notNull": false
},
"gitlabId": {
"name": "gitlabId",
"type": "text",
"primaryKey": false,
"notNull": false
},
"bitbucketId": {
"name": "bitbucketId",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
@@ -333,6 +419,45 @@
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"application_githubId_github_githubId_fk": {
"name": "application_githubId_github_githubId_fk",
"tableFrom": "application",
"tableTo": "github",
"columnsFrom": [
"githubId"
],
"columnsTo": [
"githubId"
],
"onDelete": "set null",
"onUpdate": "no action"
},
"application_gitlabId_gitlab_gitlabId_fk": {
"name": "application_gitlabId_gitlab_gitlabId_fk",
"tableFrom": "application",
"tableTo": "gitlab",
"columnsFrom": [
"gitlabId"
],
"columnsTo": [
"gitlabId"
],
"onDelete": "set null",
"onUpdate": "no action"
},
"application_bitbucketId_bitbucket_bitbucketId_fk": {
"name": "application_bitbucketId_bitbucket_bitbucketId_fk",
"tableFrom": "application",
"tableTo": "bitbucket",
"columnsFrom": [
"bitbucketId"
],
"columnsTo": [
"bitbucketId"
],
"onDelete": "set null",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
@@ -572,6 +697,13 @@
"notNull": true,
"default": false
},
"canAccessToGitProviders": {
"name": "canAccessToGitProviders",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"canAccessToTraefikFiles": {
"name": "canAccessToTraefikFiles",
"type": "boolean",
@@ -648,18 +780,6 @@
"primaryKey": true,
"notNull": true
},
"githubAppId": {
"name": "githubAppId",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"githubAppName": {
"name": "githubAppName",
"type": "text",
"primaryKey": false,
"notNull": false
},
"serverIp": {
"name": "serverIp",
"type": "text",
@@ -680,36 +800,6 @@
"primaryKey": false,
"notNull": false
},
"githubClientId": {
"name": "githubClientId",
"type": "text",
"primaryKey": false,
"notNull": false
},
"githubClientSecret": {
"name": "githubClientSecret",
"type": "text",
"primaryKey": false,
"notNull": false
},
"githubInstallationId": {
"name": "githubInstallationId",
"type": "text",
"primaryKey": false,
"notNull": false
},
"githubPrivateKey": {
"name": "githubPrivateKey",
"type": "text",
"primaryKey": false,
"notNull": false
},
"githubWebhookSecret": {
"name": "githubWebhookSecret",
"type": "text",
"primaryKey": false,
"notNull": false
},
"letsEncryptEmail": {
"name": "letsEncryptEmail",
"type": "text",
@@ -729,19 +819,6 @@
"notNull": true,
"default": false
},
"enableLogRotation": {
"name": "enableLogRotation",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"licenseKey": {
"name": "licenseKey",
"type": "text",
"primaryKey": false,
"notNull": false
},
"authId": {
"name": "authId",
"type": "text",
@@ -2445,6 +2522,54 @@
"primaryKey": false,
"notNull": false
},
"gitlabProjectId": {
"name": "gitlabProjectId",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"gitlabRepository": {
"name": "gitlabRepository",
"type": "text",
"primaryKey": false,
"notNull": false
},
"gitlabOwner": {
"name": "gitlabOwner",
"type": "text",
"primaryKey": false,
"notNull": false
},
"gitlabBranch": {
"name": "gitlabBranch",
"type": "text",
"primaryKey": false,
"notNull": false
},
"gitlabPathNamespace": {
"name": "gitlabPathNamespace",
"type": "text",
"primaryKey": false,
"notNull": false
},
"bitbucketRepository": {
"name": "bitbucketRepository",
"type": "text",
"primaryKey": false,
"notNull": false
},
"bitbucketOwner": {
"name": "bitbucketOwner",
"type": "text",
"primaryKey": false,
"notNull": false
},
"bitbucketBranch": {
"name": "bitbucketBranch",
"type": "text",
"primaryKey": false,
"notNull": false
},
"customGitUrl": {
"name": "customGitUrl",
"type": "text",
@@ -2496,6 +2621,24 @@
"type": "text",
"primaryKey": false,
"notNull": true
},
"githubId": {
"name": "githubId",
"type": "text",
"primaryKey": false,
"notNull": false
},
"gitlabId": {
"name": "gitlabId",
"type": "text",
"primaryKey": false,
"notNull": false
},
"bitbucketId": {
"name": "bitbucketId",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
@@ -2525,6 +2668,45 @@
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"compose_githubId_github_githubId_fk": {
"name": "compose_githubId_github_githubId_fk",
"tableFrom": "compose",
"tableTo": "github",
"columnsFrom": [
"githubId"
],
"columnsTo": [
"githubId"
],
"onDelete": "set null",
"onUpdate": "no action"
},
"compose_gitlabId_gitlab_gitlabId_fk": {
"name": "compose_gitlabId_gitlab_gitlabId_fk",
"tableFrom": "compose",
"tableTo": "gitlab",
"columnsFrom": [
"gitlabId"
],
"columnsTo": [
"gitlabId"
],
"onDelete": "set null",
"onUpdate": "no action"
},
"compose_bitbucketId_bitbucket_bitbucketId_fk": {
"name": "compose_bitbucketId_bitbucket_bitbucketId_fk",
"tableFrom": "compose",
"tableTo": "bitbucket",
"columnsFrom": [
"bitbucketId"
],
"columnsTo": [
"bitbucketId"
],
"onDelete": "set null",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
@@ -2932,6 +3114,272 @@
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"public.git_provider": {
"name": "git_provider",
"schema": "",
"columns": {
"gitProviderId": {
"name": "gitProviderId",
"type": "text",
"primaryKey": true,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"providerType": {
"name": "providerType",
"type": "gitProviderType",
"typeSchema": "public",
"primaryKey": false,
"notNull": true,
"default": "'github'"
},
"createdAt": {
"name": "createdAt",
"type": "text",
"primaryKey": false,
"notNull": true
},
"authId": {
"name": "authId",
"type": "text",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"git_provider_authId_auth_id_fk": {
"name": "git_provider_authId_auth_id_fk",
"tableFrom": "git_provider",
"tableTo": "auth",
"columnsFrom": [
"authId"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"public.bitbucket": {
"name": "bitbucket",
"schema": "",
"columns": {
"bitbucketId": {
"name": "bitbucketId",
"type": "text",
"primaryKey": true,
"notNull": true
},
"bitbucketUsername": {
"name": "bitbucketUsername",
"type": "text",
"primaryKey": false,
"notNull": false
},
"appPassword": {
"name": "appPassword",
"type": "text",
"primaryKey": false,
"notNull": false
},
"bitbucketWorkspaceName": {
"name": "bitbucketWorkspaceName",
"type": "text",
"primaryKey": false,
"notNull": false
},
"gitProviderId": {
"name": "gitProviderId",
"type": "text",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"bitbucket_gitProviderId_git_provider_gitProviderId_fk": {
"name": "bitbucket_gitProviderId_git_provider_gitProviderId_fk",
"tableFrom": "bitbucket",
"tableTo": "git_provider",
"columnsFrom": [
"gitProviderId"
],
"columnsTo": [
"gitProviderId"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"public.github": {
"name": "github",
"schema": "",
"columns": {
"githubId": {
"name": "githubId",
"type": "text",
"primaryKey": true,
"notNull": true
},
"githubAppName": {
"name": "githubAppName",
"type": "text",
"primaryKey": false,
"notNull": false
},
"githubAppId": {
"name": "githubAppId",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"githubClientId": {
"name": "githubClientId",
"type": "text",
"primaryKey": false,
"notNull": false
},
"githubClientSecret": {
"name": "githubClientSecret",
"type": "text",
"primaryKey": false,
"notNull": false
},
"githubInstallationId": {
"name": "githubInstallationId",
"type": "text",
"primaryKey": false,
"notNull": false
},
"githubPrivateKey": {
"name": "githubPrivateKey",
"type": "text",
"primaryKey": false,
"notNull": false
},
"githubWebhookSecret": {
"name": "githubWebhookSecret",
"type": "text",
"primaryKey": false,
"notNull": false
},
"gitProviderId": {
"name": "gitProviderId",
"type": "text",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"github_gitProviderId_git_provider_gitProviderId_fk": {
"name": "github_gitProviderId_git_provider_gitProviderId_fk",
"tableFrom": "github",
"tableTo": "git_provider",
"columnsFrom": [
"gitProviderId"
],
"columnsTo": [
"gitProviderId"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"public.gitlab": {
"name": "gitlab",
"schema": "",
"columns": {
"gitlabId": {
"name": "gitlabId",
"type": "text",
"primaryKey": true,
"notNull": true
},
"application_id": {
"name": "application_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"redirect_uri": {
"name": "redirect_uri",
"type": "text",
"primaryKey": false,
"notNull": false
},
"secret": {
"name": "secret",
"type": "text",
"primaryKey": false,
"notNull": false
},
"access_token": {
"name": "access_token",
"type": "text",
"primaryKey": false,
"notNull": false
},
"refresh_token": {
"name": "refresh_token",
"type": "text",
"primaryKey": false,
"notNull": false
},
"group_name": {
"name": "group_name",
"type": "text",
"primaryKey": false,
"notNull": false
},
"expires_at": {
"name": "expires_at",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"gitProviderId": {
"name": "gitProviderId",
"type": "text",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"gitlab_gitProviderId_git_provider_gitProviderId_fk": {
"name": "gitlab_gitProviderId_git_provider_gitProviderId_fk",
"tableFrom": "gitlab",
"tableTo": "git_provider",
"columnsFrom": [
"gitProviderId"
],
"columnsTo": [
"gitProviderId"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
}
},
"enums": {
@@ -2953,6 +3401,8 @@
"docker",
"git",
"github",
"gitlab",
"bitbucket",
"drop"
]
},
@@ -3053,6 +3503,8 @@
"values": [
"git",
"github",
"gitlab",
"bitbucket",
"raw"
]
},
@@ -3073,6 +3525,15 @@
"discord",
"email"
]
},
"public.gitProviderType": {
"name": "gitProviderType",
"schema": "public",
"values": [
"github",
"gitlab",
"bitbucket"
]
}
},
"schemas": {},

File diff suppressed because it is too large Load Diff

View File

@@ -236,15 +236,22 @@
{
"idx": 33,
"version": "6",
"when": 1724555040199,
"tag": "0033_sweet_black_bird",
"when": 1725250322137,
"tag": "0033_white_hawkeye",
"breakpoints": true
},
{
"idx": 34,
"version": "6",
"when": 1724631497207,
"tag": "0034_silent_slayback",
"when": 1725256397019,
"tag": "0034_aspiring_secret_warriors",
"breakpoints": true
},
{
"idx": 35,
"version": "6",
"when": 1725429324584,
"tag": "0035_cool_gravity",
"breakpoints": true
}
]

View File

@@ -66,6 +66,18 @@ export default async function handler(
res.status(301).json({ message: "Branch Not Match" });
return;
}
} else if (sourceType === "gitlab") {
const branchName = extractBranchName(req.headers, req.body);
if (!branchName || branchName !== application.gitlabBranch) {
res.status(301).json({ message: "Branch Not Match" });
return;
}
} else if (sourceType === "bitbucket") {
const branchName = extractBranchName(req.headers, req.body);
if (!branchName || branchName !== application.bitbucketBranch) {
res.status(301).json({ message: "Branch Not Match" });
return;
}
}
try {

View File

@@ -1,6 +1,6 @@
import { findAdmin } from "@/server/api/services/admin";
import { db } from "@/server/db";
import { applications, compose } from "@/server/db/schema";
import { applications, compose, github } from "@/server/db/schema";
import type { DeploymentJob } from "@/server/queues/deployments-queue";
import { myQueue } from "@/server/queues/queueSetup";
import { Webhooks } from "@octokit/webhooks";
@@ -19,20 +19,33 @@ export default async function handler(
return;
}
if (!admin.githubWebhookSecret) {
res.status(200).json({ message: "Github Webhook Secret not set" });
const signature = req.headers["x-hub-signature-256"];
const githubBody = req.body;
if (!githubBody?.installation.id) {
res.status(400).json({ message: "Github Installation not found" });
return;
}
const webhooks = new Webhooks({
secret: admin.githubWebhookSecret,
const githubResult = await db.query.github.findFirst({
where: eq(github.githubInstallationId, githubBody.installation.id),
});
const signature = req.headers["x-hub-signature-256"];
const github = req.body;
if (!githubResult) {
res.status(400).json({ message: "Github Installation not found" });
return;
}
if (!githubResult.githubWebhookSecret) {
res.status(400).json({ message: "Github Webhook Secret not set" });
return;
}
const webhooks = new Webhooks({
secret: githubResult.githubWebhookSecret,
});
const verified = await webhooks.verify(
JSON.stringify(github),
JSON.stringify(githubBody),
signature as string,
);
@@ -52,8 +65,8 @@ export default async function handler(
}
try {
const branchName = github?.ref?.replace("refs/heads/", "");
const repository = github?.repository?.name;
const branchName = githubBody?.ref?.replace("refs/heads/", "");
const repository = githubBody?.repository?.name;
const deploymentTitle = extractCommitMessage(req.headers, req.body);
const deploymentHash = extractHash(req.headers, req.body);

View File

@@ -1,5 +1,6 @@
import { createGithub } from "@/server/api/services/github";
import { db } from "@/server/db";
import { admins } from "@/server/db/schema";
import { github } from "@/server/db/schema";
import { eq } from "drizzle-orm";
import type { NextApiRequest, NextApiResponse } from "next";
import { Octokit } from "octokit";
@@ -17,10 +18,12 @@ export default async function handler(
) {
const { code, state, installation_id, setup_action }: Query =
req.query as Query;
if (!code) {
return res.status(400).json({ error: "Missing code parameter" });
}
const [action, authId] = state?.split(":");
const [action, value] = state?.split(":");
// Value could be the authId or the githubProviderId
if (action === "gh_init") {
const octokit = new Octokit({});
@@ -31,27 +34,25 @@ export default async function handler(
},
);
const result = await db
.update(admins)
.set({
githubAppId: data.id,
githubAppName: data.html_url,
githubClientId: data.client_id,
githubClientSecret: data.client_secret,
githubWebhookSecret: data.webhook_secret,
githubPrivateKey: data.pem,
})
.where(eq(admins.authId, authId as string))
.returning();
await createGithub({
name: data.name,
githubAppName: data.html_url,
githubAppId: data.id,
githubClientId: data.client_id,
githubClientSecret: data.client_secret,
githubWebhookSecret: data.webhook_secret,
githubPrivateKey: data.pem,
authId: value as string,
});
} else if (action === "gh_setup") {
await db
.update(admins)
.update(github)
.set({
githubInstallationId: installation_id,
})
.where(eq(admins.authId, authId as string))
.where(eq(github.githubId, value as string))
.returning();
}
res.redirect(307, "/dashboard/settings/server");
res.redirect(307, "/dashboard/settings/git-providers");
}

View File

@@ -8,9 +8,9 @@ export default async function handler(
const xGitHubEvent = req.headers["x-github-event"];
if (xGitHubEvent === "ping") {
res.redirect(307, "/dashboard/settings");
res.redirect(307, "/dashboard/settings/git-providers");
} else {
res.redirect(307, "/dashboard/settings");
res.redirect(307, "/dashboard/settings/git-providers");
}
} else {
res.setHeader("Allow", ["POST"]);

View File

@@ -0,0 +1,44 @@
import { findGitlabById, updateGitlab } from "@/server/api/services/gitlab";
import type { NextApiRequest, NextApiResponse } from "next";
export default async function handler(
req: NextApiRequest,
res: NextApiResponse,
) {
const { code, gitlabId } = req.query;
if (!code || Array.isArray(code)) {
return res.status(400).json({ error: "Missing or invalid code" });
}
const gitlab = await findGitlabById(gitlabId as string);
const response = await fetch("https://gitlab.com/oauth/token", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({
client_id: gitlab.applicationId as string,
client_secret: gitlab.secret as string,
code: code as string,
grant_type: "authorization_code",
redirect_uri: `${gitlab.redirectUri}?gitlabId=${gitlabId}`,
}),
});
const result = await response.json();
if (!result.access_token || !result.refresh_token) {
return res.status(400).json({ error: "Missing or invalid code" });
}
const expiresAt = Math.floor(Date.now() / 1000) + result.expires_in;
await updateGitlab(gitlab.gitlabId, {
accessToken: result.access_token,
refreshToken: result.refresh_token,
expiresAt,
});
return res.redirect(307, "/dashboard/settings/git-providers");
}

View File

@@ -0,0 +1,81 @@
import { ShowGitProviders } from "@/components/dashboard/settings/git/show-git-providers";
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { SettingsLayout } from "@/components/layouts/settings-layout";
import { appRouter } from "@/server/api/root";
import { validateRequest } from "@/server/auth/auth";
import { createServerSideHelpers } from "@trpc/react-query/server";
import type { GetServerSidePropsContext } from "next";
import React, { type ReactElement } from "react";
import superjson from "superjson";
const Page = () => {
return (
<div className="flex flex-col gap-4 w-full">
<ShowGitProviders />
</div>
);
};
export default Page;
Page.getLayout = (page: ReactElement) => {
return (
<DashboardLayout tab={"settings"}>
<SettingsLayout>{page}</SettingsLayout>
</DashboardLayout>
);
};
export async function getServerSideProps(
ctx: GetServerSidePropsContext<{ serviceId: string }>,
) {
const { user, session } = await validateRequest(ctx.req, ctx.res);
if (!user) {
return {
redirect: {
permanent: true,
destination: "/",
},
};
}
const { req, res } = ctx;
const helpers = createServerSideHelpers({
router: appRouter,
ctx: {
req: req as any,
res: res as any,
db: null as any,
session: session,
user: user,
},
transformer: superjson,
});
try {
await helpers.project.all.prefetch();
const auth = await helpers.auth.get.fetch();
if (auth.rol === "user") {
const user = await helpers.user.byAuthId.fetch({
authId: auth.id,
});
if (!user.canAccessToGitProviders) {
return {
redirect: {
permanent: true,
destination: "/",
},
};
}
}
return {
props: {
trpcState: helpers.dehydrate(),
},
};
} catch (error) {
return {
props: {},
};
}
}

View File

@@ -1,4 +1,3 @@
import { GithubSetup } from "@/components/dashboard/settings/github/github-setup";
import { WebDomain } from "@/components/dashboard/settings/web-domain";
import { WebServer } from "@/components/dashboard/settings/web-server";
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
@@ -11,7 +10,6 @@ const Page = () => {
return (
<div className="flex flex-col gap-4 w-full">
<WebDomain />
<GithubSetup />
<WebServer />
</div>
);

View File

@@ -3,6 +3,7 @@ import { createTRPCRouter } from "../api/trpc";
import { adminRouter } from "./routers/admin";
import { applicationRouter } from "./routers/application";
import { backupRouter } from "./routers/backup";
import { bitbucketRouter } from "./routers/bitbucket";
import { certificateRouter } from "./routers/certificate";
import { clusterRouter } from "./routers/cluster";
import { composeRouter } from "./routers/compose";
@@ -10,6 +11,9 @@ import { deploymentRouter } from "./routers/deployment";
import { destinationRouter } from "./routers/destination";
import { dockerRouter } from "./routers/docker";
import { domainRouter } from "./routers/domain";
import { gitProviderRouter } from "./routers/git-provider";
import { githubRouter } from "./routers/github";
import { gitlabRouter } from "./routers/gitlab";
import { mariadbRouter } from "./routers/mariadb";
import { mongoRouter } from "./routers/mongo";
import { mountRouter } from "./routers/mount";
@@ -60,6 +64,10 @@ export const appRouter = createTRPCRouter({
notification: notificationRouter,
sshKey: sshRouter,
license: licenseRouter,
gitProvider: gitProviderRouter,
bitbucket: bitbucketRouter,
gitlab: gitlabRouter,
github: githubRouter,
});
// export type definition of API

View File

@@ -3,28 +3,18 @@ import {
apiAssignPermissions,
apiCreateUserInvitation,
apiFindOneToken,
apiGetBranches,
apiRemoveUser,
users,
} from "@/server/db/schema";
import { haveGithubRequirements } from "@/server/utils/providers/github";
import { createAppAuth } from "@octokit/auth-app";
import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";
import { Octokit } from "octokit";
import {
createInvitation,
findAdmin,
getUserByToken,
removeUserByAuthId,
updateAdmin,
} from "../services/admin";
import {
adminProcedure,
createTRPCRouter,
protectedProcedure,
publicProcedure,
} from "../trpc";
import { adminProcedure, createTRPCRouter, publicProcedure } from "../trpc";
export const adminRouter = createTRPCRouter({
one: adminProcedure.query(async () => {
@@ -83,91 +73,4 @@ export const adminRouter = createTRPCRouter({
});
}
}),
cleanGithubApp: adminProcedure.mutation(async ({ ctx }) => {
try {
return await updateAdmin(ctx.user.authId, {
githubAppName: "",
githubClientId: "",
githubClientSecret: "",
githubInstallationId: "",
});
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error to delete this github app",
cause: error,
});
}
}),
getRepositories: protectedProcedure.query(async () => {
const admin = await findAdmin();
const completeRequirements = haveGithubRequirements(admin);
if (!completeRequirements) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Admin need to setup correctly github account",
});
}
const octokit = new Octokit({
authStrategy: createAppAuth,
auth: {
appId: admin.githubAppId,
privateKey: admin.githubPrivateKey,
installationId: admin.githubInstallationId,
},
});
const repositories = (await octokit.paginate(
octokit.rest.apps.listReposAccessibleToInstallation,
)) as unknown as Awaited<
ReturnType<typeof octokit.rest.apps.listReposAccessibleToInstallation>
>["data"]["repositories"];
return repositories;
}),
getBranches: protectedProcedure
.input(apiGetBranches)
.query(async ({ input }) => {
const admin = await findAdmin();
const completeRequirements = haveGithubRequirements(admin);
if (!completeRequirements) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Admin need to setup correctly github account",
});
}
const octokit = new Octokit({
authStrategy: createAppAuth,
auth: {
appId: admin.githubAppId,
privateKey: admin.githubPrivateKey,
installationId: admin.githubInstallationId,
},
});
const branches = (await octokit.paginate(
octokit.rest.repos.listBranches,
{
owner: input.owner,
repo: input.repo,
},
)) as unknown as Awaited<
ReturnType<typeof octokit.rest.repos.listBranches>
>["data"];
return branches;
}),
haveGithubConfigured: protectedProcedure.query(async () => {
const adminResponse = await findAdmin();
return haveGithubRequirements(adminResponse);
}),
});

View File

@@ -9,11 +9,13 @@ import {
apiFindMonitoringStats,
apiFindOneApplication,
apiReloadApplication,
apiSaveBitbucketProvider,
apiSaveBuildType,
apiSaveDockerProvider,
apiSaveEnvironmentVariables,
apiSaveGitProvider,
apiSaveGithubProvider,
apiSaveGitlabProvider,
apiUpdateApplication,
applications,
} from "@/server/db/schema/application";
@@ -193,6 +195,7 @@ export const applicationRouter = createTRPCRouter({
dockerfile: input.dockerfile,
publishDirectory: input.publishDirectory,
dockerContextPath: input.dockerContextPath,
dockerBuildStage: input.dockerBuildStage,
});
return true;
@@ -207,6 +210,39 @@ export const applicationRouter = createTRPCRouter({
owner: input.owner,
buildPath: input.buildPath,
applicationStatus: "idle",
githubId: input.githubId,
});
return true;
}),
saveGitlabProvider: protectedProcedure
.input(apiSaveGitlabProvider)
.mutation(async ({ input }) => {
await updateApplication(input.applicationId, {
gitlabRepository: input.gitlabRepository,
gitlabOwner: input.gitlabOwner,
gitlabBranch: input.gitlabBranch,
gitlabBuildPath: input.gitlabBuildPath,
sourceType: "gitlab",
applicationStatus: "idle",
gitlabId: input.gitlabId,
gitlabProjectId: input.gitlabProjectId,
gitlabPathNamespace: input.gitlabPathNamespace,
});
return true;
}),
saveBitbucketProvider: protectedProcedure
.input(apiSaveBitbucketProvider)
.mutation(async ({ input }) => {
await updateApplication(input.applicationId, {
bitbucketRepository: input.bitbucketRepository,
bitbucketOwner: input.bitbucketOwner,
bitbucketBranch: input.bitbucketBranch,
bitbucketBuildPath: input.bitbucketBuildPath,
sourceType: "bitbucket",
applicationStatus: "idle",
bitbucketId: input.bitbucketId,
});
return true;

View File

@@ -13,6 +13,7 @@ import {
import { TRPCError } from "@trpc/server";
import * as bcrypt from "bcrypt";
import { db } from "../../db";
import { getUserByToken } from "../services/admin";
import {
createAdmin,
createUser,
@@ -61,6 +62,13 @@ export const authRouter = createTRPCRouter({
.input(apiCreateUser)
.mutation(async ({ ctx, input }) => {
try {
const token = await getUserByToken(input.token);
if (token.isExpired) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Invalid token",
});
}
const newUser = await createUser(input);
const session = await lucia.createSession(newUser?.authId || "", {});
ctx.res.appendHeader(

View File

@@ -0,0 +1,82 @@
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
import { db } from "@/server/db";
import {
apiBitbucketTestConnection,
apiCreateBitbucket,
apiFindBitbucketBranches,
apiFindOneBitbucket,
apiUpdateBitbucket,
} from "@/server/db/schema";
import {
getBitbucketBranches,
getBitbucketRepositories,
testBitbucketConnection,
} from "@/server/utils/providers/bitbucket";
import { TRPCError } from "@trpc/server";
import {
createBitbucket,
findBitbucketById,
updateBitbucket,
} from "../services/bitbucket";
export const bitbucketRouter = createTRPCRouter({
create: protectedProcedure
.input(apiCreateBitbucket)
.mutation(async ({ input }) => {
try {
return await createBitbucket(input);
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error to create this bitbucket provider",
cause: error,
});
}
}),
one: protectedProcedure
.input(apiFindOneBitbucket)
.query(async ({ input }) => {
return await findBitbucketById(input.bitbucketId);
}),
bitbucketProviders: protectedProcedure.query(async () => {
const result = await db.query.bitbucket.findMany({
with: {
gitProvider: true,
},
columns: {
bitbucketId: true,
},
});
return result;
}),
getBitbucketRepositories: protectedProcedure
.input(apiFindOneBitbucket)
.query(async ({ input }) => {
return await getBitbucketRepositories(input.bitbucketId);
}),
getBitbucketBranches: protectedProcedure
.input(apiFindBitbucketBranches)
.query(async ({ input }) => {
return await getBitbucketBranches(input);
}),
testConnection: protectedProcedure
.input(apiBitbucketTestConnection)
.mutation(async ({ input }) => {
try {
const result = await testBitbucketConnection(input);
return `Found ${result} repositories`;
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: error instanceof Error ? error?.message : `Error: ${error}`,
});
}
}),
update: protectedProcedure
.input(apiUpdateBitbucket)
.mutation(async ({ input }) => {
return await updateBitbucket(input.bitbucketId, input);
}),
});

View File

@@ -15,7 +15,10 @@ import {
} from "@/server/queues/deployments-queue";
import { myQueue } from "@/server/queues/queueSetup";
import { createCommand } from "@/server/utils/builders/compose";
import { randomizeComposeFile } from "@/server/utils/docker/compose";
import {
randomizeComposeFile,
randomizeSpecificationFile,
} from "@/server/utils/docker/compose";
import { addDomainToCompose, cloneCompose } from "@/server/utils/docker/domain";
import { removeComposeDirectory } from "@/server/utils/filesystem/directory";
import { templates } from "@/templates/templates";
@@ -141,7 +144,7 @@ export const composeRouter = createTRPCRouter({
randomizeCompose: protectedProcedure
.input(apiRandomizeCompose)
.mutation(async ({ input }) => {
return await randomizeComposeFile(input.composeId, input.prefix);
return await randomizeComposeFile(input.composeId, input.suffix);
}),
getConvertedCompose: protectedProcedure
.input(apiFindCompose)
@@ -150,6 +153,7 @@ export const composeRouter = createTRPCRouter({
const domains = await findDomainsByComposeId(input.composeId);
const composeFile = await addDomainToCompose(compose, domains);
return dump(composeFile, {
lineWidth: 1000,
});

View File

@@ -1,5 +1,6 @@
import { z } from "zod";
import {
containerRestart,
getConfig,
getContainers,
getContainersByAppLabel,
@@ -12,6 +13,16 @@ export const dockerRouter = createTRPCRouter({
return await getContainers();
}),
restartContainer: protectedProcedure
.input(
z.object({
containerId: z.string().min(1),
}),
)
.mutation(async ({ input }) => {
return await containerRestart(input.containerId);
}),
getConfig: protectedProcedure
.input(
z.object({

View File

@@ -0,0 +1,31 @@
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
import { db } from "@/server/db";
import { apiRemoveGitProvider, gitProvider } from "@/server/db/schema";
import { TRPCError } from "@trpc/server";
import { asc, desc } from "drizzle-orm";
import { removeGitProvider } from "../services/git-provider";
export const gitProviderRouter = createTRPCRouter({
getAll: protectedProcedure.query(async () => {
return await db.query.gitProvider.findMany({
with: {
gitlab: true,
bitbucket: true,
github: true,
},
orderBy: desc(gitProvider.createdAt),
});
}),
remove: protectedProcedure
.input(apiRemoveGitProvider)
.mutation(async ({ input }) => {
try {
return await removeGitProvider(input.gitProviderId);
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error to delete this git provider",
});
}
}),
});

View File

@@ -0,0 +1,71 @@
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
import { db } from "@/server/db";
import {
apiFindGithubBranches,
apiFindOneGithub,
apiUpdateGithub,
} from "@/server/db/schema";
import {
getGithubBranches,
getGithubRepositories,
} from "@/server/utils/providers/github";
import { TRPCError } from "@trpc/server";
import { updateGitProvider } from "../services/git-provider";
import { findGithubById, haveGithubRequirements } from "../services/github";
export const githubRouter = createTRPCRouter({
one: protectedProcedure.input(apiFindOneGithub).query(async ({ input }) => {
return await findGithubById(input.githubId);
}),
getGithubRepositories: protectedProcedure
.input(apiFindOneGithub)
.query(async ({ input }) => {
return await getGithubRepositories(input.githubId);
}),
getGithubBranches: protectedProcedure
.input(apiFindGithubBranches)
.query(async ({ input }) => {
return await getGithubBranches(input);
}),
githubProviders: protectedProcedure.query(async () => {
const result = await db.query.github.findMany({
with: {
gitProvider: true,
},
});
const filtered = result
.filter((provider) => haveGithubRequirements(provider))
.map((provider) => {
return {
githubId: provider.githubId,
gitProvider: {
...provider.gitProvider,
},
};
});
return filtered;
}),
testConnection: protectedProcedure
.input(apiFindOneGithub)
.mutation(async ({ input }) => {
try {
const result = await getGithubRepositories(input.githubId);
return `Found ${result.length} repositories`;
} catch (err) {
throw new TRPCError({
code: "BAD_REQUEST",
message: err instanceof Error ? err?.message : `Error: ${err}`,
});
}
}),
update: protectedProcedure
.input(apiUpdateGithub)
.mutation(async ({ input }) => {
await updateGitProvider(input.gitProviderId, {
name: input.name,
});
}),
});

View File

@@ -0,0 +1,93 @@
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
import {
apiCreateGitlab,
apiFindGitlabBranches,
apiFindOneGitlab,
apiGitlabTestConnection,
apiUpdateGitlab,
} from "@/server/db/schema";
import { db } from "@/server/db";
import {
getGitlabBranches,
getGitlabRepositories,
haveGitlabRequirements,
testGitlabConnection,
} from "@/server/utils/providers/gitlab";
import { TRPCError } from "@trpc/server";
import { updateGitProvider } from "../services/git-provider";
import { createGitlab, findGitlabById, updateGitlab } from "../services/gitlab";
export const gitlabRouter = createTRPCRouter({
create: protectedProcedure
.input(apiCreateGitlab)
.mutation(async ({ input }) => {
try {
return await createGitlab(input);
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error to create this gitlab provider",
cause: error,
});
}
}),
one: protectedProcedure.input(apiFindOneGitlab).query(async ({ input }) => {
return await findGitlabById(input.gitlabId);
}),
gitlabProviders: protectedProcedure.query(async () => {
const result = await db.query.gitlab.findMany({
with: {
gitProvider: true,
},
});
const filtered = result
.filter((provider) => haveGitlabRequirements(provider))
.map((provider) => {
return {
gitlabId: provider.gitlabId,
gitProvider: {
...provider.gitProvider,
},
};
});
return filtered;
}),
getGitlabRepositories: protectedProcedure
.input(apiFindOneGitlab)
.query(async ({ input }) => {
return await getGitlabRepositories(input.gitlabId);
}),
getGitlabBranches: protectedProcedure
.input(apiFindGitlabBranches)
.query(async ({ input }) => {
return await getGitlabBranches(input);
}),
testConnection: protectedProcedure
.input(apiGitlabTestConnection)
.mutation(async ({ input }) => {
try {
const result = await testGitlabConnection(input);
return `Found ${result} repositories`;
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: error instanceof Error ? error?.message : `Error: ${error}`,
});
}
}),
update: protectedProcedure
.input(apiUpdateGitlab)
.mutation(async ({ input }) => {
if (input.name) {
await updateGitProvider(input.gitProviderId, {
name: input.name,
});
} else {
await updateGitlab(input.gitlabId, input);
}
}),
});

View File

@@ -305,6 +305,10 @@ export const settingsRouter = createTRPCRouter({
"mongo",
"mariadb",
"sshRouter",
"gitProvider",
"bitbucket",
"github",
"gitlab",
],
});
@@ -326,9 +330,9 @@ export const settingsRouter = createTRPCRouter({
}),
writeTraefikEnv: adminProcedure
.input(z.string())
.input(z.object({ env: z.string() }))
.mutation(async ({ input }) => {
const envs = prepareEnvironmentVariables(input);
const envs = prepareEnvironmentVariables(input.env);
await initializeTraefik({
env: envs,
});

View File

@@ -129,13 +129,9 @@ export const getUserByToken = async (token: string) => {
message: "Invitation not found",
});
}
const now = new Date();
const isExpired = isAfter(now, new Date(user.expirationDate));
return {
...user,
isExpired,
isExpired: user.isRegistered,
};
};

View File

@@ -20,6 +20,8 @@ import { createDeployment, updateDeploymentStatus } from "./deployment";
import { sendBuildErrorNotifications } from "@/server/utils/notifications/build-error";
import { sendBuildSuccessNotifications } from "@/server/utils/notifications/build-success";
import { cloneBitbucketRepository } from "@/server/utils/providers/bitbucket";
import { cloneGitlabRepository } from "@/server/utils/providers/gitlab";
import { validUniqueServerAppName } from "./project";
export type Application = typeof applications.$inferSelect;
@@ -81,6 +83,9 @@ export const findApplicationById = async (applicationId: string) => {
security: true,
ports: true,
registry: true,
gitlab: true,
github: true,
bitbucket: true,
},
});
if (!application) {
@@ -141,7 +146,6 @@ export const deployApplication = async ({
}) => {
const application = await findApplicationById(applicationId);
const buildLink = `${await getDokployUrl()}/dashboard/project/${application.projectId}/services/application/${application.applicationId}?tab=deployments`;
const admin = await findAdmin();
const deployment = await createDeployment({
applicationId: applicationId,
title: titleLog,
@@ -150,7 +154,13 @@ export const deployApplication = async ({
try {
if (application.sourceType === "github") {
await cloneGithubRepository(admin, application, deployment.logPath);
await cloneGithubRepository(application, deployment.logPath);
await buildApplication(application, deployment.logPath);
} else if (application.sourceType === "gitlab") {
await cloneGitlabRepository(application, deployment.logPath);
await buildApplication(application, deployment.logPath);
} else if (application.sourceType === "bitbucket") {
await cloneBitbucketRepository(application, deployment.logPath);
await buildApplication(application, deployment.logPath);
} else if (application.sourceType === "docker") {
await buildDocker(application, deployment.logPath);
@@ -214,6 +224,10 @@ export const rebuildApplication = async ({
try {
if (application.sourceType === "github") {
await buildApplication(application, deployment.logPath);
} else if (application.sourceType === "gitlab") {
await buildApplication(application, deployment.logPath);
} else if (application.sourceType === "bitbucket") {
await buildApplication(application, deployment.logPath);
} else if (application.sourceType === "docker") {
await buildDocker(application, deployment.logPath);
} else if (application.sourceType === "git") {

View File

@@ -72,7 +72,7 @@ export const createUser = async (input: typeof apiCreateUser._type) => {
.update(users)
.set({
isRegistered: true,
expirationDate: new Date().toISOString(),
expirationDate: undefined,
})
.where(eq(users.token, input.token))
.returning()

View File

@@ -0,0 +1,88 @@
import { db } from "@/server/db";
import {
type apiCreateBitbucket,
type apiUpdateBitbucket,
bitbucket,
gitProvider,
} from "@/server/db/schema";
import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";
export type Bitbucket = typeof bitbucket.$inferSelect;
export const createBitbucket = async (
input: typeof apiCreateBitbucket._type,
) => {
return await db.transaction(async (tx) => {
const newGitProvider = await tx
.insert(gitProvider)
.values({
providerType: "bitbucket",
authId: input.authId,
name: input.name,
})
.returning()
.then((response) => response[0]);
if (!newGitProvider) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error to create the git provider",
});
}
await tx
.insert(bitbucket)
.values({
...input,
gitProviderId: newGitProvider?.gitProviderId,
})
.returning()
.then((response) => response[0]);
});
};
export const findBitbucketById = async (bitbucketId: string) => {
const bitbucketProviderResult = await db.query.bitbucket.findFirst({
where: eq(bitbucket.bitbucketId, bitbucketId),
with: {
gitProvider: true,
},
});
if (!bitbucketProviderResult) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Bitbucket Provider not found",
});
}
return bitbucketProviderResult;
};
export const updateBitbucket = async (
bitbucketId: string,
input: typeof apiUpdateBitbucket._type,
) => {
return await db.transaction(async (tx) => {
const result = await tx
.update(bitbucket)
.set({
...input,
})
.where(eq(bitbucket.bitbucketId, bitbucketId))
.returning();
if (input.name) {
await tx
.update(gitProvider)
.set({
name: input.name,
})
.where(eq(gitProvider.gitProviderId, input.gitProviderId))
.returning();
}
return result[0];
});
};

View File

@@ -4,18 +4,20 @@ import { db } from "@/server/db";
import { type apiCreateCompose, compose } from "@/server/db/schema";
import { generateAppName } from "@/server/db/schema/utils";
import { buildCompose } from "@/server/utils/builders/compose";
import { randomizeSpecificationFile } from "@/server/utils/docker/compose";
import { cloneCompose, loadDockerCompose } from "@/server/utils/docker/domain";
import type { ComposeSpecification } from "@/server/utils/docker/types";
import { sendBuildErrorNotifications } from "@/server/utils/notifications/build-error";
import { sendBuildSuccessNotifications } from "@/server/utils/notifications/build-success";
import { execAsync } from "@/server/utils/process/execAsync";
import { cloneBitbucketRepository } from "@/server/utils/providers/bitbucket";
import { cloneGitRepository } from "@/server/utils/providers/git";
import { cloneGithubRepository } from "@/server/utils/providers/github";
import { cloneGitlabRepository } from "@/server/utils/providers/gitlab";
import { createComposeFile } from "@/server/utils/providers/raw";
import { generatePassword } from "@/templates/utils";
import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";
import { findAdmin, getDokployUrl } from "./admin";
import { getDokployUrl } from "./admin";
import { createDeploymentCompose, updateDeploymentStatus } from "./deployment";
import { validUniqueServerAppName } from "./project";
@@ -92,6 +94,9 @@ export const findComposeById = async (composeId: string) => {
deployments: true,
mounts: true,
domains: true,
github: true,
gitlab: true,
bitbucket: true,
},
});
if (!result) {
@@ -113,7 +118,16 @@ export const loadServices = async (
await cloneCompose(compose);
}
const composeData = await loadDockerCompose(compose);
let composeData = await loadDockerCompose(compose);
if (compose.randomize && composeData) {
const randomizedCompose = randomizeSpecificationFile(
composeData,
compose.suffix,
);
composeData = randomizedCompose;
}
if (!composeData?.services) {
throw new TRPCError({
code: "NOT_FOUND",
@@ -151,7 +165,6 @@ export const deployCompose = async ({
descriptionLog: string;
}) => {
const compose = await findComposeById(composeId);
const admin = await findAdmin();
const buildLink = `${await getDokployUrl()}/dashboard/project/${compose.projectId}/services/compose/${compose.composeId}?tab=deployments`;
const deployment = await createDeploymentCompose({
composeId: composeId,
@@ -161,7 +174,11 @@ export const deployCompose = async ({
try {
if (compose.sourceType === "github") {
await cloneGithubRepository(admin, compose, deployment.logPath, true);
await cloneGithubRepository(compose, deployment.logPath);
} else if (compose.sourceType === "gitlab") {
await cloneGitlabRepository(compose, deployment.logPath);
} else if (compose.sourceType === "bitbucket") {
await cloneBitbucketRepository(compose, deployment.logPath);
} else if (compose.sourceType === "git") {
await cloneGitRepository(compose, deployment.logPath, true);
} else if (compose.sourceType === "raw") {

View File

@@ -150,3 +150,20 @@ export const getContainersByAppLabel = async (appName: string) => {
return [];
};
export const containerRestart = async (containerId: string) => {
try {
const { stdout, stderr } = await execAsync(
`docker container restart ${containerId}`,
);
if (stderr) {
console.error(`Error: ${stderr}`);
return;
}
const config = JSON.parse(stdout);
return config;
} catch (error) {}
};

View File

@@ -0,0 +1,29 @@
import { db } from "@/server/db";
import { type apiCreateGithub, gitProvider, github } from "@/server/db/schema";
import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";
export type GitProvider = typeof gitProvider.$inferSelect;
export const removeGitProvider = async (gitProviderId: string) => {
const result = await db
.delete(gitProvider)
.where(eq(gitProvider.gitProviderId, gitProviderId))
.returning();
return result[0];
};
export const updateGitProvider = async (
gitProviderId: string,
input: Partial<GitProvider>,
) => {
return await db
.update(gitProvider)
.set({
...input,
})
.where(eq(gitProvider.gitProviderId, gitProviderId))
.returning()
.then((response) => response[0]);
};

View File

@@ -0,0 +1,75 @@
import { db } from "@/server/db";
import { type apiCreateGithub, gitProvider, github } from "@/server/db/schema";
import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";
export type Github = typeof github.$inferSelect;
export const createGithub = async (input: typeof apiCreateGithub._type) => {
return await db.transaction(async (tx) => {
const newGitProvider = await tx
.insert(gitProvider)
.values({
providerType: "github",
authId: input.authId,
name: input.name,
})
.returning()
.then((response) => response[0]);
if (!newGitProvider) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error to create the git provider",
});
}
return await tx
.insert(github)
.values({
...input,
gitProviderId: newGitProvider?.gitProviderId,
})
.returning()
.then((response) => response[0]);
});
};
export const findGithubById = async (githubId: string) => {
const githubProviderResult = await db.query.github.findFirst({
where: eq(github.githubId, githubId),
with: {
gitProvider: true,
},
});
if (!githubProviderResult) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Github Provider not found",
});
}
return githubProviderResult;
};
export const haveGithubRequirements = (github: Github) => {
return !!(
github?.githubAppId &&
github?.githubPrivateKey &&
github?.githubInstallationId
);
};
export const updateGithub = async (
githubId: string,
input: Partial<Github>,
) => {
return await db
.update(github)
.set({
...input,
})
.where(eq(github.githubId, githubId))
.returning()
.then((response) => response[0]);
};

View File

@@ -0,0 +1,76 @@
import { db } from "@/server/db";
import {
type apiCreateGitlab,
type bitbucket,
gitProvider,
type github,
gitlab,
} from "@/server/db/schema";
import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";
export type Github = typeof github.$inferSelect;
export type Bitbucket = typeof bitbucket.$inferSelect;
export type Gitlab = typeof gitlab.$inferSelect;
export const createGitlab = async (input: typeof apiCreateGitlab._type) => {
return await db.transaction(async (tx) => {
const newGitProvider = await tx
.insert(gitProvider)
.values({
providerType: "gitlab",
authId: input.authId,
name: input.name,
})
.returning()
.then((response) => response[0]);
if (!newGitProvider) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error to create the git provider",
});
}
await tx
.insert(gitlab)
.values({
...input,
gitProviderId: newGitProvider?.gitProviderId,
})
.returning()
.then((response) => response[0]);
});
};
export const findGitlabById = async (gitlabId: string) => {
const gitlabProviderResult = await db.query.gitlab.findFirst({
where: eq(gitlab.gitlabId, gitlabId),
with: {
gitProvider: true,
},
});
if (!gitlabProviderResult) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Gitlab Provider not found",
});
}
return gitlabProviderResult;
};
export const updateGitlab = async (
gitlabId: string,
input: Partial<Gitlab>,
) => {
return await db
.update(gitlab)
.set({
...input,
})
.where(eq(gitlab.gitlabId, gitlabId))
.returning()
.then((response) => response[0]);
};

View File

@@ -1,5 +1,5 @@
import { relations } from "drizzle-orm";
import { boolean, integer, pgTable, text } from "drizzle-orm/pg-core";
import { boolean, pgTable, text } from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid";
import { z } from "zod";
@@ -13,17 +13,9 @@ export const admins = pgTable("admin", {
.notNull()
.primaryKey()
.$defaultFn(() => nanoid()),
githubAppId: integer("githubAppId"),
githubAppName: text("githubAppName"),
serverIp: text("serverIp"),
certificateType: certificateType("certificateType").notNull().default("none"),
host: text("host"),
githubClientId: text("githubClientId"),
githubClientSecret: text("githubClientSecret"),
githubInstallationId: text("githubInstallationId"),
githubPrivateKey: text("githubPrivateKey"),
githubWebhookSecret: text("githubWebhookSecret"),
letsEncryptEmail: text("letsEncryptEmail"),
sshPrivateKey: text("sshPrivateKey"),
enableDockerCleanup: boolean("enableDockerCleanup").notNull().default(false),
@@ -48,12 +40,6 @@ export const adminsRelations = relations(admins, ({ one, many }) => ({
const createSchema = createInsertSchema(admins, {
adminId: z.string(),
githubAppName: z.string().optional(),
githubClientId: z.string().optional(),
githubClientSecret: z.string().optional(),
githubInstallationId: z.string().optional(),
githubPrivateKey: z.string().optional(),
githubAppId: z.number().optional(),
enableDockerCleanup: z.boolean().optional(),
sshPrivateKey: z.string().optional(),
certificateType: z.enum(["letsencrypt", "none"]).default("none"),
@@ -84,10 +70,6 @@ export const apiTraefikConfig = z.object({
traefikConfig: z.string().min(1),
});
export const apiGetBranches = z.object({
repo: z.string().min(1),
owner: z.string().min(1),
});
export const apiModifyTraefikConfig = z.object({
path: z.string().min(1),
traefikConfig: z.string().min(1),

View File

@@ -1,4 +1,3 @@
import { generatePassword } from "@/templates/utils";
import { relations } from "drizzle-orm";
import {
boolean,
@@ -11,6 +10,7 @@ import {
import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid";
import { z } from "zod";
import { bitbucket, github, gitlab } from ".";
import { deployments } from "./deployment";
import { domains } from "./domain";
import { mounts } from "./mount";
@@ -27,6 +27,8 @@ export const sourceType = pgEnum("sourceType", [
"docker",
"git",
"github",
"gitlab",
"bitbucket",
"drop",
]);
@@ -126,6 +128,18 @@ export const applications = pgTable("application", {
branch: text("branch"),
buildPath: text("buildPath").default("/"),
autoDeploy: boolean("autoDeploy").$defaultFn(() => true),
// Gitlab
gitlabProjectId: integer("gitlabProjectId"),
gitlabRepository: text("gitlabRepository"),
gitlabOwner: text("gitlabOwner"),
gitlabBranch: text("gitlabBranch"),
gitlabBuildPath: text("gitlabBuildPath").default("/"),
gitlabPathNamespace: text("gitlabPathNamespace"),
// Bitbucket
bitbucketRepository: text("bitbucketRepository"),
bitbucketOwner: text("bitbucketOwner"),
bitbucketBranch: text("bitbucketBranch"),
bitbucketBuildPath: text("bitbucketBuildPath").default("/"),
// Docker
username: text("username"),
password: text("password"),
@@ -142,6 +156,7 @@ export const applications = pgTable("application", {
),
dockerfile: text("dockerfile"),
dockerContextPath: text("dockerContextPath"),
dockerBuildStage: text("dockerBuildStage"),
// Drop
dropBuildPath: text("dropBuildPath"),
// Docker swarm json
@@ -169,6 +184,15 @@ export const applications = pgTable("application", {
projectId: text("projectId")
.notNull()
.references(() => projects.projectId, { onDelete: "cascade" }),
githubId: text("githubId").references(() => github.githubId, {
onDelete: "set null",
}),
gitlabId: text("gitlabId").references(() => gitlab.gitlabId, {
onDelete: "set null",
}),
bitbucketId: text("bitbucketId").references(() => bitbucket.bitbucketId, {
onDelete: "set null",
}),
});
export const applicationsRelations = relations(
@@ -192,6 +216,18 @@ export const applicationsRelations = relations(
fields: [applications.registryId],
references: [registry.registryId],
}),
github: one(github, {
fields: [applications.githubId],
references: [github.githubId],
}),
gitlab: one(gitlab, {
fields: [applications.gitlabId],
references: [gitlab.gitlabId],
}),
bitbucket: one(bitbucket, {
fields: [applications.bitbucketId],
references: [bitbucket.bitbucketId],
}),
}),
);
@@ -358,6 +394,7 @@ export const apiSaveBuildType = createSchema
buildType: true,
dockerfile: true,
dockerContextPath: true,
dockerBuildStage: true,
})
.required()
.merge(createSchema.pick({ publishDirectory: true }));
@@ -369,6 +406,31 @@ export const apiSaveGithubProvider = createSchema
branch: true,
owner: true,
buildPath: true,
githubId: true,
})
.required();
export const apiSaveGitlabProvider = createSchema
.pick({
applicationId: true,
gitlabBranch: true,
gitlabBuildPath: true,
gitlabOwner: true,
gitlabRepository: true,
gitlabId: true,
gitlabProjectId: true,
gitlabPathNamespace: true,
})
.required();
export const apiSaveBitbucketProvider = createSchema
.pick({
bitbucketBranch: true,
bitbucketBuildPath: true,
bitbucketOwner: true,
bitbucketRepository: true,
bitbucketId: true,
applicationId: true,
})
.required();

View File

@@ -70,6 +70,7 @@ export const apiCreateUser = createSchema
.pick({
password: true,
id: true,
token: true,
})
.required()
.extend({

View File

@@ -0,0 +1,64 @@
import { relations } from "drizzle-orm";
import { pgTable, text } from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid";
import { z } from "zod";
import { gitProvider } from "./git-provider";
export const bitbucket = pgTable("bitbucket", {
bitbucketId: text("bitbucketId")
.notNull()
.primaryKey()
.$defaultFn(() => nanoid()),
bitbucketUsername: text("bitbucketUsername"),
appPassword: text("appPassword"),
bitbucketWorkspaceName: text("bitbucketWorkspaceName"),
gitProviderId: text("gitProviderId")
.notNull()
.references(() => gitProvider.gitProviderId, { onDelete: "cascade" }),
});
export const bitbucketProviderRelations = relations(bitbucket, ({ one }) => ({
gitProvider: one(gitProvider, {
fields: [bitbucket.gitProviderId],
references: [gitProvider.gitProviderId],
}),
}));
const createSchema = createInsertSchema(bitbucket);
export const apiCreateBitbucket = createSchema.extend({
bitbucketUsername: z.string().optional(),
appPassword: z.string().optional(),
bitbucketWorkspaceName: z.string().optional(),
gitProviderId: z.string().optional(),
authId: z.string().min(1),
name: z.string().min(1),
});
export const apiFindOneBitbucket = createSchema
.extend({
bitbucketId: z.string().min(1),
})
.pick({ bitbucketId: true });
export const apiBitbucketTestConnection = createSchema
.extend({
bitbucketId: z.string().min(1),
bitbucketUsername: z.string().optional(),
workspaceName: z.string().optional(),
})
.pick({ bitbucketId: true, bitbucketUsername: true, workspaceName: true });
export const apiFindBitbucketBranches = z.object({
owner: z.string(),
repo: z.string(),
bitbucketId: z.string().optional(),
});
export const apiUpdateBitbucket = createSchema.extend({
bitbucketId: z.string().min(1),
name: z.string().min(1),
bitbucketUsername: z.string().optional(),
bitbucketWorkspaceName: z.string().optional(),
});

View File

@@ -1,9 +1,10 @@
import { sshKeys } from "@/server/db/schema/ssh-key";
import { relations } from "drizzle-orm";
import { boolean, pgEnum, pgTable, text } from "drizzle-orm/pg-core";
import { boolean, integer, pgEnum, pgTable, text } from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid";
import { z } from "zod";
import { bitbucket, github, gitlab } from ".";
import { deployments } from "./deployment";
import { domains } from "./domain";
import { mounts } from "./mount";
@@ -14,6 +15,8 @@ import { generateAppName } from "./utils";
export const sourceTypeCompose = pgEnum("sourceTypeCompose", [
"git",
"github",
"gitlab",
"bitbucket",
"raw",
]);
@@ -39,6 +42,16 @@ export const compose = pgTable("compose", {
owner: text("owner"),
branch: text("branch"),
autoDeploy: boolean("autoDeploy").$defaultFn(() => true),
// Gitlab
gitlabProjectId: integer("gitlabProjectId"),
gitlabRepository: text("gitlabRepository"),
gitlabOwner: text("gitlabOwner"),
gitlabBranch: text("gitlabBranch"),
gitlabPathNamespace: text("gitlabPathNamespace"),
// Bitbucket
bitbucketRepository: text("bitbucketRepository"),
bitbucketOwner: text("bitbucketOwner"),
bitbucketBranch: text("bitbucketBranch"),
// Git
customGitUrl: text("customGitUrl"),
customGitBranch: text("customGitBranch"),
@@ -48,10 +61,11 @@ export const compose = pgTable("compose", {
onDelete: "set null",
},
),
//
command: text("command").notNull().default(""),
//
composePath: text("composePath").notNull().default("./docker-compose.yml"),
suffix: text("suffix").notNull().default(""),
randomize: boolean("randomize").notNull().default(false),
composeStatus: applicationStatus("composeStatus").notNull().default("idle"),
projectId: text("projectId")
.notNull()
@@ -59,6 +73,16 @@ export const compose = pgTable("compose", {
createdAt: text("createdAt")
.notNull()
.$defaultFn(() => new Date().toISOString()),
githubId: text("githubId").references(() => github.githubId, {
onDelete: "set null",
}),
gitlabId: text("gitlabId").references(() => gitlab.gitlabId, {
onDelete: "set null",
}),
bitbucketId: text("bitbucketId").references(() => bitbucket.bitbucketId, {
onDelete: "set null",
}),
});
export const composeRelations = relations(compose, ({ one, many }) => ({
@@ -73,6 +97,18 @@ export const composeRelations = relations(compose, ({ one, many }) => ({
references: [sshKeys.sshKeyId],
}),
domains: many(domains),
github: one(github, {
fields: [compose.githubId],
references: [github.githubId],
}),
gitlab: one(gitlab, {
fields: [compose.gitlabId],
references: [gitlab.gitlabId],
}),
bitbucket: one(bitbucket, {
fields: [compose.bitbucketId],
references: [bitbucket.bitbucketId],
}),
}));
const createSchema = createInsertSchema(compose, {
@@ -123,6 +159,6 @@ export const apiRandomizeCompose = createSchema
composeId: true,
})
.extend({
prefix: z.string().optional(),
suffix: z.string().optional(),
composeId: z.string().min(1),
});

View File

@@ -0,0 +1,57 @@
import { relations } from "drizzle-orm";
import { pgEnum, pgTable, text } from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid";
import { z } from "zod";
import { auth } from "./auth";
import { bitbucket } from "./bitbucket";
import { github } from "./github";
import { gitlab } from "./gitlab";
export const gitProviderType = pgEnum("gitProviderType", [
"github",
"gitlab",
"bitbucket",
]);
export const gitProvider = pgTable("git_provider", {
gitProviderId: text("gitProviderId")
.notNull()
.primaryKey()
.$defaultFn(() => nanoid()),
name: text("name").notNull(),
providerType: gitProviderType("providerType").notNull().default("github"),
createdAt: text("createdAt")
.notNull()
.$defaultFn(() => new Date().toISOString()),
authId: text("authId")
.notNull()
.references(() => auth.id, { onDelete: "cascade" }),
});
export const gitProviderRelations = relations(gitProvider, ({ one, many }) => ({
github: one(github, {
fields: [gitProvider.gitProviderId],
references: [github.gitProviderId],
}),
gitlab: one(gitlab, {
fields: [gitProvider.gitProviderId],
references: [gitlab.gitProviderId],
}),
bitbucket: one(bitbucket, {
fields: [gitProvider.gitProviderId],
references: [bitbucket.gitProviderId],
}),
auth: one(auth, {
fields: [gitProvider.authId],
references: [auth.id],
}),
}));
const createSchema = createInsertSchema(gitProvider);
export const apiRemoveGitProvider = createSchema
.extend({
gitProviderId: z.string().min(1),
})
.pick({ gitProviderId: true });

View File

@@ -0,0 +1,62 @@
import { relations } from "drizzle-orm";
import { integer, pgTable, text } from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid";
import { z } from "zod";
import { gitProvider } from "./git-provider";
export const github = pgTable("github", {
githubId: text("githubId")
.notNull()
.primaryKey()
.$defaultFn(() => nanoid()),
githubAppName: text("githubAppName"),
githubAppId: integer("githubAppId"),
githubClientId: text("githubClientId"),
githubClientSecret: text("githubClientSecret"),
githubInstallationId: text("githubInstallationId"),
githubPrivateKey: text("githubPrivateKey"),
githubWebhookSecret: text("githubWebhookSecret"),
gitProviderId: text("gitProviderId")
.notNull()
.references(() => gitProvider.gitProviderId, { onDelete: "cascade" }),
});
export const githubProviderRelations = relations(github, ({ one }) => ({
gitProvider: one(gitProvider, {
fields: [github.gitProviderId],
references: [gitProvider.gitProviderId],
}),
}));
const createSchema = createInsertSchema(github);
export const apiCreateGithub = createSchema.extend({
githubAppName: z.string().optional(),
githubAppId: z.number().optional(),
githubClientId: z.string().optional(),
githubClientSecret: z.string().optional(),
githubInstallationId: z.string().optional(),
githubPrivateKey: z.string().optional(),
githubWebhookSecret: z.string().nullable(),
gitProviderId: z.string().optional(),
name: z.string().min(1),
authId: z.string().min(1),
});
export const apiFindGithubBranches = z.object({
repo: z.string().min(1),
owner: z.string().min(1),
githubId: z.string().optional(),
});
export const apiFindOneGithub = createSchema
.extend({
githubId: z.string().min(1),
})
.pick({ githubId: true });
export const apiUpdateGithub = createSchema.extend({
githubId: z.string().min(1),
name: z.string().min(1),
gitProviderId: z.string().min(1),
});

View File

@@ -0,0 +1,70 @@
import { relations } from "drizzle-orm";
import { integer, pgTable, text } from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid";
import { z } from "zod";
import { gitProvider } from "./git-provider";
export const gitlab = pgTable("gitlab", {
gitlabId: text("gitlabId")
.notNull()
.primaryKey()
.$defaultFn(() => nanoid()),
applicationId: text("application_id"),
redirectUri: text("redirect_uri"),
secret: text("secret"),
accessToken: text("access_token"),
refreshToken: text("refresh_token"),
groupName: text("group_name"),
expiresAt: integer("expires_at"),
gitProviderId: text("gitProviderId")
.notNull()
.references(() => gitProvider.gitProviderId, { onDelete: "cascade" }),
});
export const gitlabProviderRelations = relations(gitlab, ({ one }) => ({
gitProvider: one(gitProvider, {
fields: [gitlab.gitProviderId],
references: [gitProvider.gitProviderId],
}),
}));
const createSchema = createInsertSchema(gitlab);
export const apiCreateGitlab = createSchema.extend({
applicationId: z.string().optional(),
secret: z.string().optional(),
groupName: z.string().optional(),
gitProviderId: z.string().optional(),
redirectUri: z.string().optional(),
authId: z.string().min(1),
name: z.string().min(1),
});
export const apiFindOneGitlab = createSchema
.extend({
gitlabId: z.string().min(1),
})
.pick({ gitlabId: true });
export const apiGitlabTestConnection = createSchema
.extend({
groupName: z.string().optional(),
})
.pick({ gitlabId: true, groupName: true });
export const apiFindGitlabBranches = z.object({
id: z.number().optional(),
owner: z.string(),
repo: z.string(),
gitlabId: z.string().optional(),
});
export const apiUpdateGitlab = createSchema.extend({
applicationId: z.string().optional(),
secret: z.string().optional(),
groupName: z.string().optional(),
redirectUri: z.string().optional(),
name: z.string().min(1),
gitlabId: z.string().min(1),
});

View File

@@ -23,3 +23,7 @@ export * from "./compose";
export * from "./registry";
export * from "./notification";
export * from "./ssh-key";
export * from "./git-provider";
export * from "./bitbucket";
export * from "./github";
export * from "./gitlab";

View File

@@ -34,6 +34,9 @@ export const users = pgTable("user", {
canDeleteServices: boolean("canDeleteServices").notNull().default(false),
canAccessToDocker: boolean("canAccessToDocker").notNull().default(false),
canAccessToAPI: boolean("canAccessToAPI").notNull().default(false),
canAccessToGitProviders: boolean("canAccessToGitProviders")
.notNull()
.default(false),
canAccessToTraefikFiles: boolean("canAccessToTraefikFiles")
.notNull()
.default(false),
@@ -109,6 +112,7 @@ export const apiAssignPermissions = createSchema
canAccessToDocker: true,
canAccessToAPI: true,
canAccessToSSHKeys: true,
canAccessToGitProviders: true,
})
.required();

View File

@@ -20,7 +20,7 @@ export const initializeTraefik = async ({
enableDashboard = false,
env,
}: TraefikOptions = {}) => {
const imageName = "traefik:v2.5";
const imageName = "traefik:v3.1.2";
const containerName = "dokploy-traefik";
const settings: CreateServiceOptions = {
Name: containerName,
@@ -56,6 +56,9 @@ export const initializeTraefik = async ({
Replicas: 1,
},
},
Labels: {
"traefik.enable": "true",
},
EndpointSpec: {
Ports: [
{
@@ -171,6 +174,10 @@ export const createDefaultTraefikConfig = () => {
},
}
: {
swarm: {
exposedByDefault: false,
watch: false,
},
docker: {
exposedByDefault: false,
},

View File

@@ -18,15 +18,7 @@ export type ComposeNested = InferResultType<
>;
export const buildCompose = async (compose: ComposeNested, logPath: string) => {
const writeStream = createWriteStream(logPath, { flags: "a" });
const {
sourceType,
appName,
mounts,
composeType,
env,
composePath,
domains,
} = compose;
const { sourceType, appName, mounts, composeType, domains } = compose;
try {
const command = createCommand(compose);
await writeDomainsToCompose(compose, domains);
@@ -117,6 +109,10 @@ const createEnvFile = (compose: ComposeNested) => {
envContent += "\nDOCKER_CONFIG=/root/.docker/config.json";
}
if (compose.randomize) {
envContent += `\nCOMPOSE_PREFIX=${compose.suffix}`;
}
const envFileContent = prepareEnvironmentVariables(envContent).join("\n");
if (!existsSync(dirname(envFilePath))) {

View File

@@ -12,7 +12,8 @@ export const buildCustomDocker = async (
application: ApplicationNested,
writeStream: WriteStream,
) => {
const { appName, env, publishDirectory, buildArgs } = application;
const { appName, env, publishDirectory, buildArgs, dockerBuildStage } =
application;
const dockerFilePath = getBuildAppDirectory(application);
try {
const image = `${appName}`;
@@ -25,6 +26,10 @@ export const buildCustomDocker = async (
const commandArgs = ["build", "-t", image, "-f", dockerFilePath, "."];
if (dockerBuildStage) {
commandArgs.push("--target", dockerBuildStage);
}
for (const arg of args) {
commandArgs.push("--build-arg", arg);
}

View File

@@ -1,11 +1,11 @@
import crypto from "node:crypto";
import { findComposeById } from "@/server/api/services/compose";
import { dump, load } from "js-yaml";
import { addPrefixToAllConfigs } from "./compose/configs";
import { addPrefixToAllNetworks } from "./compose/network";
import { addPrefixToAllSecrets } from "./compose/secrets";
import { addPrefixToAllServiceNames } from "./compose/service";
import { addPrefixToAllVolumes } from "./compose/volume";
import { addSuffixToAllConfigs } from "./compose/configs";
import { addSuffixToAllNetworks } from "./compose/network";
import { addSuffixToAllSecrets } from "./compose/secrets";
import { addSuffixToAllServiceNames } from "./compose/service";
import { addSuffixToAllVolumes } from "./compose/volume";
import type { ComposeSpecification } from "./types";
export const generateRandomHash = (): string => {
@@ -14,32 +14,43 @@ export const generateRandomHash = (): string => {
export const randomizeComposeFile = async (
composeId: string,
prefix?: string,
suffix?: string,
) => {
const compose = await findComposeById(composeId);
const composeFile = compose.composeFile;
const composeData = load(composeFile) as ComposeSpecification;
const randomPrefix = prefix || generateRandomHash();
const randomSuffix = suffix || generateRandomHash();
const newComposeFile = addPrefixToAllProperties(composeData, randomPrefix);
const newComposeFile = addSuffixToAllProperties(composeData, randomSuffix);
return dump(newComposeFile);
};
export const addPrefixToAllProperties = (
export const randomizeSpecificationFile = (
composeSpec: ComposeSpecification,
suffix?: string,
) => {
if (!suffix) {
return composeSpec;
}
const newComposeFile = addSuffixToAllProperties(composeSpec, suffix);
return newComposeFile;
};
export const addSuffixToAllProperties = (
composeData: ComposeSpecification,
prefix: string,
suffix: string,
): ComposeSpecification => {
let updatedComposeData = { ...composeData };
updatedComposeData = addPrefixToAllServiceNames(updatedComposeData, prefix);
updatedComposeData = addSuffixToAllServiceNames(updatedComposeData, suffix);
updatedComposeData = addPrefixToAllVolumes(updatedComposeData, prefix);
updatedComposeData = addSuffixToAllVolumes(updatedComposeData, suffix);
updatedComposeData = addPrefixToAllNetworks(updatedComposeData, prefix);
updatedComposeData = addPrefixToAllConfigs(updatedComposeData, prefix);
updatedComposeData = addSuffixToAllNetworks(updatedComposeData, suffix);
updatedComposeData = addSuffixToAllConfigs(updatedComposeData, suffix);
updatedComposeData = addPrefixToAllSecrets(updatedComposeData, prefix);
updatedComposeData = addSuffixToAllSecrets(updatedComposeData, suffix);
return updatedComposeData;
};

View File

@@ -5,23 +5,23 @@ import type {
DefinitionsService,
} from "../types";
export const addPrefixToConfigsRoot = (
export const addSuffixToConfigsRoot = (
configs: { [key: string]: DefinitionsConfig },
prefix: string,
suffix: string,
): { [key: string]: DefinitionsConfig } => {
const newConfigs: { [key: string]: DefinitionsConfig } = {};
_.forEach(configs, (config, configName) => {
const newConfigName = `${configName}-${prefix}`;
const newConfigName = `${configName}-${suffix}`;
newConfigs[newConfigName] = _.cloneDeep(config);
});
return newConfigs;
};
export const addPrefixToConfigsInServices = (
export const addSuffixToConfigsInServices = (
services: { [key: string]: DefinitionsService },
prefix: string,
suffix: string,
): { [key: string]: DefinitionsService } => {
const newServices: { [key: string]: DefinitionsService } = {};
@@ -32,12 +32,12 @@ export const addPrefixToConfigsInServices = (
if (_.has(newServiceConfig, "configs")) {
newServiceConfig.configs = _.map(newServiceConfig.configs, (config) => {
if (_.isString(config)) {
return `${config}-${prefix}`;
return `${config}-${suffix}`;
}
if (_.isObject(config) && config.source) {
return {
...config,
source: `${config.source}-${prefix}`,
source: `${config.source}-${suffix}`,
};
}
return config;
@@ -50,22 +50,22 @@ export const addPrefixToConfigsInServices = (
return newServices;
};
export const addPrefixToAllConfigs = (
export const addSuffixToAllConfigs = (
composeData: ComposeSpecification,
prefix: string,
suffix: string,
): ComposeSpecification => {
const updatedComposeData = { ...composeData };
if (composeData?.configs) {
updatedComposeData.configs = addPrefixToConfigsRoot(
updatedComposeData.configs = addSuffixToConfigsRoot(
composeData.configs,
prefix,
suffix,
);
}
if (composeData?.services) {
updatedComposeData.services = addPrefixToConfigsInServices(
updatedComposeData.services = addSuffixToConfigsInServices(
composeData.services,
prefix,
suffix,
);
}

Some files were not shown because too many files have changed in this diff Show More