mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
refactor: rename builders to server
This commit is contained in:
361
packages/server/src/utils/providers/bitbucket.ts
Normal file
361
packages/server/src/utils/providers/bitbucket.ts
Normal file
@@ -0,0 +1,361 @@
|
||||
import { createWriteStream } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { paths } from "@/server/constants";
|
||||
import type {
|
||||
apiBitbucketTestConnection,
|
||||
apiFindBitbucketBranches,
|
||||
} from "@/server/db/schema";
|
||||
import { findBitbucketById } from "@/server/services/bitbucket";
|
||||
import type { Compose } from "@/server/services/compose";
|
||||
import type { InferResultType } from "@/server/types/with";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { recreateDirectory } from "../filesystem/directory";
|
||||
import { execAsyncRemote } from "../process/execAsync";
|
||||
import { spawnAsync } from "../process/spawnAsync";
|
||||
|
||||
export type ApplicationWithBitbucket = InferResultType<
|
||||
"applications",
|
||||
{ bitbucket: true }
|
||||
>;
|
||||
|
||||
export type ComposeWithBitbucket = InferResultType<
|
||||
"compose",
|
||||
{ bitbucket: true }
|
||||
>;
|
||||
|
||||
export const cloneBitbucketRepository = async (
|
||||
entity: ApplicationWithBitbucket | ComposeWithBitbucket,
|
||||
logPath: string,
|
||||
isCompose = false,
|
||||
) => {
|
||||
const { COMPOSE_PATH, APPLICATIONS_PATH } = paths();
|
||||
const writeStream = createWriteStream(logPath, { flags: "a" });
|
||||
const {
|
||||
appName,
|
||||
bitbucketRepository,
|
||||
bitbucketOwner,
|
||||
bitbucketBranch,
|
||||
bitbucketId,
|
||||
bitbucket,
|
||||
} = entity;
|
||||
|
||||
if (!bitbucketId) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Bitbucket Provider not found",
|
||||
});
|
||||
}
|
||||
|
||||
const basePath = isCompose ? COMPOSE_PATH : APPLICATIONS_PATH;
|
||||
const outputPath = join(basePath, appName, "code");
|
||||
await recreateDirectory(outputPath);
|
||||
const repoclone = `bitbucket.org/${bitbucketOwner}/${bitbucketRepository}.git`;
|
||||
const cloneUrl = `https://${bitbucket?.bitbucketUsername}:${bitbucket?.appPassword}@${repoclone}`;
|
||||
try {
|
||||
writeStream.write(`\nCloning Repo ${repoclone} to ${outputPath}: ✅\n`);
|
||||
await spawnAsync(
|
||||
"git",
|
||||
[
|
||||
"clone",
|
||||
"--branch",
|
||||
bitbucketBranch!,
|
||||
"--depth",
|
||||
"1",
|
||||
cloneUrl,
|
||||
outputPath,
|
||||
"--progress",
|
||||
],
|
||||
(data) => {
|
||||
if (writeStream.writable) {
|
||||
writeStream.write(data);
|
||||
}
|
||||
},
|
||||
);
|
||||
writeStream.write(`\nCloned ${repoclone} to ${outputPath}: ✅\n`);
|
||||
} catch (error) {
|
||||
writeStream.write(`ERROR Clonning: ${error}: ❌`);
|
||||
throw error;
|
||||
} finally {
|
||||
writeStream.end();
|
||||
}
|
||||
};
|
||||
|
||||
export const cloneRawBitbucketRepository = async (entity: Compose) => {
|
||||
const { COMPOSE_PATH } = paths();
|
||||
const {
|
||||
appName,
|
||||
bitbucketRepository,
|
||||
bitbucketOwner,
|
||||
bitbucketBranch,
|
||||
bitbucketId,
|
||||
} = entity;
|
||||
|
||||
if (!bitbucketId) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Bitbucket Provider not found",
|
||||
});
|
||||
}
|
||||
|
||||
const bitbucketProvider = await findBitbucketById(bitbucketId);
|
||||
const basePath = COMPOSE_PATH;
|
||||
const outputPath = join(basePath, appName, "code");
|
||||
await recreateDirectory(outputPath);
|
||||
const repoclone = `bitbucket.org/${bitbucketOwner}/${bitbucketRepository}.git`;
|
||||
const cloneUrl = `https://${bitbucketProvider?.bitbucketUsername}:${bitbucketProvider?.appPassword}@${repoclone}`;
|
||||
|
||||
try {
|
||||
await spawnAsync("git", [
|
||||
"clone",
|
||||
"--branch",
|
||||
bitbucketBranch!,
|
||||
"--depth",
|
||||
"1",
|
||||
cloneUrl,
|
||||
outputPath,
|
||||
"--progress",
|
||||
]);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const cloneRawBitbucketRepositoryRemote = async (compose: Compose) => {
|
||||
const { COMPOSE_PATH } = paths(true);
|
||||
const {
|
||||
appName,
|
||||
bitbucketRepository,
|
||||
bitbucketOwner,
|
||||
bitbucketBranch,
|
||||
bitbucketId,
|
||||
serverId,
|
||||
} = compose;
|
||||
|
||||
if (!serverId) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Server not found",
|
||||
});
|
||||
}
|
||||
if (!bitbucketId) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Bitbucket Provider not found",
|
||||
});
|
||||
}
|
||||
|
||||
const bitbucketProvider = await findBitbucketById(bitbucketId);
|
||||
const basePath = COMPOSE_PATH;
|
||||
const outputPath = join(basePath, appName, "code");
|
||||
const repoclone = `bitbucket.org/${bitbucketOwner}/${bitbucketRepository}.git`;
|
||||
const cloneUrl = `https://${bitbucketProvider?.bitbucketUsername}:${bitbucketProvider?.appPassword}@${repoclone}`;
|
||||
|
||||
try {
|
||||
const command = `
|
||||
rm -rf ${outputPath};
|
||||
git clone --branch ${bitbucketBranch} --depth 1 ${cloneUrl} ${outputPath}
|
||||
`;
|
||||
await execAsyncRemote(serverId, command);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const getBitbucketCloneCommand = async (
|
||||
entity: ApplicationWithBitbucket | ComposeWithBitbucket,
|
||||
logPath: string,
|
||||
isCompose = false,
|
||||
) => {
|
||||
const { COMPOSE_PATH, APPLICATIONS_PATH } = paths(true);
|
||||
const {
|
||||
appName,
|
||||
bitbucketRepository,
|
||||
bitbucketOwner,
|
||||
bitbucketBranch,
|
||||
bitbucketId,
|
||||
serverId,
|
||||
bitbucket,
|
||||
} = entity;
|
||||
|
||||
if (!serverId) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Server not found",
|
||||
});
|
||||
}
|
||||
|
||||
if (!bitbucketId) {
|
||||
const command = `
|
||||
echo "Error: ❌ Bitbucket Provider not found" >> ${logPath};
|
||||
exit 1;
|
||||
`;
|
||||
await execAsyncRemote(serverId, command);
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Bitbucket Provider not found",
|
||||
});
|
||||
}
|
||||
|
||||
const bitbucketProvider = await findBitbucketById(bitbucketId);
|
||||
const basePath = isCompose ? COMPOSE_PATH : APPLICATIONS_PATH;
|
||||
const outputPath = join(basePath, appName, "code");
|
||||
await recreateDirectory(outputPath);
|
||||
const repoclone = `bitbucket.org/${bitbucketOwner}/${bitbucketRepository}.git`;
|
||||
const cloneUrl = `https://${bitbucketProvider?.bitbucketUsername}:${bitbucketProvider?.appPassword}@${repoclone}`;
|
||||
|
||||
const cloneCommand = `
|
||||
rm -rf ${outputPath};
|
||||
mkdir -p ${outputPath};
|
||||
if ! git clone --branch ${bitbucketBranch} --depth 1 --progress ${cloneUrl} ${outputPath} >> ${logPath} 2>&1; then
|
||||
echo "❌ [ERROR] Fail to clone the repository ${repoclone}" >> ${logPath};
|
||||
exit 1;
|
||||
fi
|
||||
echo "Cloned ${repoclone} to ${outputPath}: ✅" >> ${logPath};
|
||||
`;
|
||||
|
||||
return cloneCommand;
|
||||
};
|
||||
|
||||
export const getBitbucketRepositories = async (bitbucketId?: string) => {
|
||||
if (!bitbucketId) {
|
||||
return [];
|
||||
}
|
||||
const bitbucketProvider = await findBitbucketById(bitbucketId);
|
||||
|
||||
const username =
|
||||
bitbucketProvider.bitbucketWorkspaceName ||
|
||||
bitbucketProvider.bitbucketUsername;
|
||||
const url = `https://api.bitbucket.org/2.0/repositories/${username}?pagelen=100`;
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: `Basic ${Buffer.from(`${bitbucketProvider.bitbucketUsername}:${bitbucketProvider.appPassword}`).toString("base64")}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: `Failed to fetch repositories: ${response.statusText}`,
|
||||
});
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
const mappedData = data.values.map((repo: any) => {
|
||||
return {
|
||||
name: repo.name,
|
||||
url: repo.links.html.href,
|
||||
owner: {
|
||||
username: repo.workspace.slug,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
return mappedData as {
|
||||
name: string;
|
||||
url: string;
|
||||
owner: {
|
||||
username: string;
|
||||
};
|
||||
}[];
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const getBitbucketBranches = async (
|
||||
input: typeof apiFindBitbucketBranches._type,
|
||||
) => {
|
||||
if (!input.bitbucketId) {
|
||||
return [];
|
||||
}
|
||||
const bitbucketProvider = await findBitbucketById(input.bitbucketId);
|
||||
const { owner, repo } = input;
|
||||
const url = `https://api.bitbucket.org/2.0/repositories/${owner}/${repo}/refs/branches`;
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: `Basic ${Buffer.from(`${bitbucketProvider.bitbucketUsername}:${bitbucketProvider.appPassword}`).toString("base64")}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: `HTTP error! status: ${response.status}`,
|
||||
});
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
const mappedData = data.values.map((branch: any) => {
|
||||
return {
|
||||
name: branch.name,
|
||||
commit: {
|
||||
sha: branch.target.hash,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
return mappedData as {
|
||||
name: string;
|
||||
commit: {
|
||||
sha: string;
|
||||
};
|
||||
}[];
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const testBitbucketConnection = async (
|
||||
input: typeof apiBitbucketTestConnection._type,
|
||||
) => {
|
||||
const bitbucketProvider = await findBitbucketById(input.bitbucketId);
|
||||
|
||||
if (!bitbucketProvider) {
|
||||
throw new Error("Bitbucket provider not found");
|
||||
}
|
||||
|
||||
const { bitbucketUsername, workspaceName } = input;
|
||||
|
||||
const username = workspaceName || bitbucketUsername;
|
||||
|
||||
const url = `https://api.bitbucket.org/2.0/repositories/${username}`;
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: `Basic ${Buffer.from(`${bitbucketProvider.bitbucketUsername}:${bitbucketProvider.appPassword}`).toString("base64")}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: `Failed to fetch repositories: ${response.statusText}`,
|
||||
});
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
const mappedData = data.values.map((repo: any) => {
|
||||
return {
|
||||
name: repo.name,
|
||||
url: repo.links.html.href,
|
||||
owner: {
|
||||
username: repo.workspace.slug,
|
||||
},
|
||||
};
|
||||
}) as [];
|
||||
|
||||
return mappedData.length;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
86
packages/server/src/utils/providers/docker.ts
Normal file
86
packages/server/src/utils/providers/docker.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { createWriteStream } from "node:fs";
|
||||
import { type ApplicationNested, mechanizeDockerContainer } from "../builders";
|
||||
import { pullImage } from "../docker/utils";
|
||||
|
||||
interface RegistryAuth {
|
||||
username: string;
|
||||
password: string;
|
||||
serveraddress: string;
|
||||
}
|
||||
|
||||
export const buildDocker = async (
|
||||
application: ApplicationNested,
|
||||
logPath: string,
|
||||
): Promise<void> => {
|
||||
const { buildType, dockerImage, username, password } = application;
|
||||
const authConfig: Partial<RegistryAuth> = {
|
||||
username: username || "",
|
||||
password: password || "",
|
||||
};
|
||||
|
||||
const writeStream = createWriteStream(logPath, { flags: "a" });
|
||||
|
||||
writeStream.write(`\nBuild ${buildType}\n`);
|
||||
|
||||
writeStream.write(`Pulling ${dockerImage}: ✅\n`);
|
||||
|
||||
try {
|
||||
if (!dockerImage) {
|
||||
throw new Error("Docker image not found");
|
||||
}
|
||||
|
||||
await pullImage(
|
||||
dockerImage,
|
||||
(data) => {
|
||||
if (writeStream.writable) {
|
||||
writeStream.write(`${data.status}\n`);
|
||||
}
|
||||
},
|
||||
authConfig,
|
||||
);
|
||||
await mechanizeDockerContainer(application);
|
||||
writeStream.write("\nDocker Deployed: ✅\n");
|
||||
} catch (error) {
|
||||
writeStream.write(`ERROR: ${error}: ❌`);
|
||||
throw error;
|
||||
} finally {
|
||||
writeStream.end();
|
||||
}
|
||||
};
|
||||
|
||||
export const buildRemoteDocker = async (
|
||||
application: ApplicationNested,
|
||||
logPath: string,
|
||||
) => {
|
||||
const { sourceType, dockerImage, username, password } = application;
|
||||
|
||||
try {
|
||||
if (!dockerImage) {
|
||||
throw new Error("Docker image not found");
|
||||
}
|
||||
let command = `
|
||||
echo "Pulling ${dockerImage}" >> ${logPath};
|
||||
`;
|
||||
|
||||
if (username && password) {
|
||||
command += `
|
||||
if ! docker login --username ${username} --password ${password} https://index.docker.io/v1/ >> ${logPath} 2>&1; then
|
||||
echo "❌ Login failed" >> ${logPath};
|
||||
exit 1;
|
||||
fi
|
||||
`;
|
||||
}
|
||||
|
||||
command += `
|
||||
docker pull ${dockerImage} >> ${logPath} 2>> ${logPath} || {
|
||||
echo "❌ Pulling image failed" >> ${logPath};
|
||||
exit 1;
|
||||
}
|
||||
|
||||
echo "✅ Pulling image completed." >> ${logPath};
|
||||
`;
|
||||
return command;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
347
packages/server/src/utils/providers/git.ts
Normal file
347
packages/server/src/utils/providers/git.ts
Normal file
@@ -0,0 +1,347 @@
|
||||
import { createWriteStream } from "node:fs";
|
||||
import path, { join } from "node:path";
|
||||
import { paths } from "@/server/constants";
|
||||
import type { Compose } from "@/server/services/compose";
|
||||
import { updateSSHKeyById } from "@/server/services/ssh-key";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { recreateDirectory } from "../filesystem/directory";
|
||||
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
||||
import { spawnAsync } from "../process/spawnAsync";
|
||||
|
||||
export const cloneGitRepository = async (
|
||||
entity: {
|
||||
appName: string;
|
||||
customGitUrl?: string | null;
|
||||
customGitBranch?: string | null;
|
||||
customGitSSHKeyId?: string | null;
|
||||
},
|
||||
logPath: string,
|
||||
isCompose = false,
|
||||
) => {
|
||||
const { SSH_PATH, COMPOSE_PATH, APPLICATIONS_PATH } = paths();
|
||||
const { appName, customGitUrl, customGitBranch, customGitSSHKeyId } = entity;
|
||||
|
||||
if (!customGitUrl || !customGitBranch) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error: Repository not found",
|
||||
});
|
||||
}
|
||||
|
||||
const writeStream = createWriteStream(logPath, { flags: "a" });
|
||||
const keyPath = path.join(SSH_PATH, `${customGitSSHKeyId}_rsa`);
|
||||
const basePath = isCompose ? COMPOSE_PATH : APPLICATIONS_PATH;
|
||||
const outputPath = join(basePath, appName, "code");
|
||||
const knownHostsPath = path.join(SSH_PATH, "known_hosts");
|
||||
|
||||
try {
|
||||
if (!isHttpOrHttps(customGitUrl)) {
|
||||
await addHostToKnownHosts(customGitUrl);
|
||||
}
|
||||
await recreateDirectory(outputPath);
|
||||
// const command = `GIT_SSH_COMMAND="ssh -i ${keyPath} -o UserKnownHostsFile=${knownHostsPath}" git clone --branch ${customGitBranch} --depth 1 ${customGitUrl} ${gitCopyPath} --progress`;
|
||||
// const { stdout, stderr } = await execAsync(command);
|
||||
writeStream.write(
|
||||
`\nCloning Repo Custom ${customGitUrl} to ${outputPath}: ✅\n`,
|
||||
);
|
||||
|
||||
if (customGitSSHKeyId) {
|
||||
await updateSSHKeyById({
|
||||
sshKeyId: customGitSSHKeyId,
|
||||
lastUsedAt: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
await spawnAsync(
|
||||
"git",
|
||||
[
|
||||
"clone",
|
||||
"--branch",
|
||||
customGitBranch,
|
||||
"--depth",
|
||||
"1",
|
||||
"--recurse-submodules",
|
||||
customGitUrl,
|
||||
outputPath,
|
||||
"--progress",
|
||||
],
|
||||
(data) => {
|
||||
if (writeStream.writable) {
|
||||
writeStream.write(data);
|
||||
}
|
||||
},
|
||||
{
|
||||
env: {
|
||||
...process.env,
|
||||
...(customGitSSHKeyId && {
|
||||
GIT_SSH_COMMAND: `ssh -i ${keyPath} -o UserKnownHostsFile=${knownHostsPath}`,
|
||||
}),
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
writeStream.write(`\nCloned Custom Git ${customGitUrl}: ✅\n`);
|
||||
} catch (error) {
|
||||
writeStream.write(`\nERROR Cloning Custom Git: ${error}: ❌\n`);
|
||||
throw error;
|
||||
} finally {
|
||||
writeStream.end();
|
||||
}
|
||||
};
|
||||
|
||||
export const getCustomGitCloneCommand = async (
|
||||
entity: {
|
||||
appName: string;
|
||||
customGitUrl?: string | null;
|
||||
customGitBranch?: string | null;
|
||||
customGitSSHKeyId?: string | null;
|
||||
serverId: string | null;
|
||||
},
|
||||
logPath: string,
|
||||
isCompose = false,
|
||||
) => {
|
||||
const { SSH_PATH, COMPOSE_PATH, APPLICATIONS_PATH } = paths(true);
|
||||
const {
|
||||
appName,
|
||||
customGitUrl,
|
||||
customGitBranch,
|
||||
customGitSSHKeyId,
|
||||
serverId,
|
||||
} = entity;
|
||||
|
||||
if (!customGitUrl || !customGitBranch) {
|
||||
const command = `
|
||||
echo "Error: ❌ Repository not found" >> ${logPath};
|
||||
exit 1;
|
||||
`;
|
||||
|
||||
await execAsyncRemote(serverId, command);
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error: Repository not found",
|
||||
});
|
||||
}
|
||||
|
||||
const keyPath = path.join(SSH_PATH, `${customGitSSHKeyId}_rsa`);
|
||||
const basePath = isCompose ? COMPOSE_PATH : APPLICATIONS_PATH;
|
||||
const outputPath = join(basePath, appName, "code");
|
||||
const knownHostsPath = path.join(SSH_PATH, "known_hosts");
|
||||
|
||||
if (customGitSSHKeyId) {
|
||||
await updateSSHKeyById({
|
||||
sshKeyId: customGitSSHKeyId,
|
||||
lastUsedAt: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
try {
|
||||
const command = [];
|
||||
if (!isHttpOrHttps(customGitUrl)) {
|
||||
command.push(addHostToKnownHostsCommand(customGitUrl));
|
||||
}
|
||||
command.push(`rm -rf ${outputPath};`);
|
||||
command.push(`mkdir -p ${outputPath};`);
|
||||
command.push(
|
||||
`echo "Cloning Custom Git ${customGitUrl}" to ${outputPath}: ✅ >> ${logPath};`,
|
||||
);
|
||||
if (customGitSSHKeyId) {
|
||||
command.push(
|
||||
`GIT_SSH_COMMAND="ssh -i ${keyPath} -o UserKnownHostsFile=${knownHostsPath}"`,
|
||||
);
|
||||
}
|
||||
|
||||
command.push(
|
||||
`if ! git clone --branch ${customGitBranch} --depth 1 --progress ${customGitUrl} ${outputPath} >> ${logPath} 2>&1; then
|
||||
echo "❌ [ERROR] Fail to clone the repository ${customGitUrl}" >> ${logPath};
|
||||
exit 1;
|
||||
fi
|
||||
`,
|
||||
);
|
||||
command.push(`echo "Cloned Custom Git ${customGitUrl}: ✅" >> ${logPath};`);
|
||||
return command.join("\n");
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const isHttpOrHttps = (url: string): boolean => {
|
||||
const regex = /^https?:\/\//;
|
||||
return regex.test(url);
|
||||
};
|
||||
|
||||
const addHostToKnownHosts = async (repositoryURL: string) => {
|
||||
const { SSH_PATH } = paths();
|
||||
const { domain, port } = sanitizeRepoPathSSH(repositoryURL);
|
||||
const knownHostsPath = path.join(SSH_PATH, "known_hosts");
|
||||
|
||||
const command = `ssh-keyscan -p ${port} ${domain} >> ${knownHostsPath}`;
|
||||
try {
|
||||
await execAsync(command);
|
||||
} catch (error) {
|
||||
console.error(`Error adding host to known_hosts: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const addHostToKnownHostsCommand = (repositoryURL: string) => {
|
||||
const { SSH_PATH } = paths();
|
||||
const { domain, port } = sanitizeRepoPathSSH(repositoryURL);
|
||||
const knownHostsPath = path.join(SSH_PATH, "known_hosts");
|
||||
|
||||
return `ssh-keyscan -p ${port} ${domain} >> ${knownHostsPath};`;
|
||||
};
|
||||
const sanitizeRepoPathSSH = (input: string) => {
|
||||
const SSH_PATH_RE = new RegExp(
|
||||
[
|
||||
/^\s*/,
|
||||
/(?:(?<proto>[a-z]+):\/\/)?/,
|
||||
/(?:(?<user>[a-z_][a-z0-9_-]+)@)?/,
|
||||
/(?<domain>[^\s\/\?#:]+)/,
|
||||
/(?::(?<port>[0-9]{1,5}))?/,
|
||||
/(?:[\/:](?<owner>[^\s\/\?#:]+))?/,
|
||||
/(?:[\/:](?<repo>(?:[^\s\?#:.]|\.(?!git\/?\s*$))+))/,
|
||||
/(?:.git)?\/?\s*$/,
|
||||
]
|
||||
.map((r) => r.source)
|
||||
.join(""),
|
||||
"i",
|
||||
);
|
||||
|
||||
const found = input.match(SSH_PATH_RE);
|
||||
if (!found) {
|
||||
throw new Error(`Malformatted SSH path: ${input}`);
|
||||
}
|
||||
|
||||
return {
|
||||
user: found.groups?.user ?? "git",
|
||||
domain: found.groups?.domain,
|
||||
port: Number(found.groups?.port ?? 22),
|
||||
owner: found.groups?.owner ?? "",
|
||||
repo: found.groups?.repo,
|
||||
get repoPath() {
|
||||
return `ssh://${this.user}@${this.domain}:${this.port}/${this.owner}${
|
||||
this.owner && "/"
|
||||
}${this.repo}.git`;
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const cloneGitRawRepository = async (entity: {
|
||||
appName: string;
|
||||
customGitUrl?: string | null;
|
||||
customGitBranch?: string | null;
|
||||
customGitSSHKeyId?: string | null;
|
||||
}) => {
|
||||
const { appName, customGitUrl, customGitBranch, customGitSSHKeyId } = entity;
|
||||
|
||||
if (!customGitUrl || !customGitBranch) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error: Repository not found",
|
||||
});
|
||||
}
|
||||
|
||||
const { SSH_PATH, COMPOSE_PATH } = paths();
|
||||
const keyPath = path.join(SSH_PATH, `${customGitSSHKeyId}_rsa`);
|
||||
const basePath = COMPOSE_PATH;
|
||||
const outputPath = join(basePath, appName, "code");
|
||||
const knownHostsPath = path.join(SSH_PATH, "known_hosts");
|
||||
|
||||
try {
|
||||
await addHostToKnownHosts(customGitUrl);
|
||||
await recreateDirectory(outputPath);
|
||||
|
||||
if (customGitSSHKeyId) {
|
||||
await updateSSHKeyById({
|
||||
sshKeyId: customGitSSHKeyId,
|
||||
lastUsedAt: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
await spawnAsync(
|
||||
"git",
|
||||
[
|
||||
"clone",
|
||||
"--branch",
|
||||
customGitBranch,
|
||||
"--depth",
|
||||
"1",
|
||||
customGitUrl,
|
||||
outputPath,
|
||||
"--progress",
|
||||
],
|
||||
(data) => {},
|
||||
{
|
||||
env: {
|
||||
...process.env,
|
||||
...(customGitSSHKeyId && {
|
||||
GIT_SSH_COMMAND: `ssh -i ${keyPath} -o UserKnownHostsFile=${knownHostsPath}`,
|
||||
}),
|
||||
},
|
||||
},
|
||||
);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const cloneRawGitRepositoryRemote = async (compose: Compose) => {
|
||||
const {
|
||||
appName,
|
||||
customGitBranch,
|
||||
customGitUrl,
|
||||
customGitSSHKeyId,
|
||||
serverId,
|
||||
} = compose;
|
||||
|
||||
if (!serverId) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Server not found",
|
||||
});
|
||||
}
|
||||
if (!customGitUrl) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Git Provider not found",
|
||||
});
|
||||
}
|
||||
|
||||
const { SSH_PATH, COMPOSE_PATH } = paths(true);
|
||||
const keyPath = path.join(SSH_PATH, `${customGitSSHKeyId}_rsa`);
|
||||
const basePath = COMPOSE_PATH;
|
||||
const outputPath = join(basePath, appName, "code");
|
||||
const knownHostsPath = path.join(SSH_PATH, "known_hosts");
|
||||
|
||||
if (customGitSSHKeyId) {
|
||||
await updateSSHKeyById({
|
||||
sshKeyId: customGitSSHKeyId,
|
||||
lastUsedAt: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
try {
|
||||
const command = [];
|
||||
if (!isHttpOrHttps(customGitUrl)) {
|
||||
command.push(addHostToKnownHostsCommand(customGitUrl));
|
||||
}
|
||||
command.push(`rm -rf ${outputPath};`);
|
||||
command.push(`mkdir -p ${outputPath};`);
|
||||
if (customGitSSHKeyId) {
|
||||
command.push(
|
||||
`GIT_SSH_COMMAND="ssh -i ${keyPath} -o UserKnownHostsFile=${knownHostsPath}"`,
|
||||
);
|
||||
}
|
||||
|
||||
command.push(
|
||||
`if ! git clone --branch ${customGitBranch} --depth 1 --progress ${customGitUrl} ${outputPath} ; then
|
||||
echo "[ERROR] Fail to clone the repository ";
|
||||
exit 1;
|
||||
fi
|
||||
`,
|
||||
);
|
||||
|
||||
await execAsyncRemote(serverId, command.join("\n"));
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
336
packages/server/src/utils/providers/github.ts
Normal file
336
packages/server/src/utils/providers/github.ts
Normal file
@@ -0,0 +1,336 @@
|
||||
import { createWriteStream } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { paths } from "@/server/constants";
|
||||
import type { InferResultType } from "@/server/types/with";
|
||||
import { createAppAuth } from "@octokit/auth-app";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { Octokit } from "octokit";
|
||||
import { recreateDirectory } from "../filesystem/directory";
|
||||
import { spawnAsync } from "../process/spawnAsync";
|
||||
|
||||
import type { apiFindGithubBranches } from "@/server/db/schema";
|
||||
import type { Compose } from "@/server/services/compose";
|
||||
import { type Github, findGithubById } from "@/server/services/github";
|
||||
import { execAsyncRemote } from "../process/execAsync";
|
||||
|
||||
export const authGithub = (githubProvider: Github): Octokit => {
|
||||
if (!haveGithubRequirements(githubProvider)) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Github Account not configured correctly",
|
||||
});
|
||||
}
|
||||
|
||||
const octokit: Octokit = new Octokit({
|
||||
authStrategy: createAppAuth,
|
||||
auth: {
|
||||
appId: githubProvider?.githubAppId || 0,
|
||||
privateKey: githubProvider?.githubPrivateKey || "",
|
||||
installationId: githubProvider?.githubInstallationId,
|
||||
},
|
||||
});
|
||||
|
||||
return octokit;
|
||||
};
|
||||
|
||||
export const getGithubToken = async (
|
||||
octokit: ReturnType<typeof authGithub>,
|
||||
) => {
|
||||
const installation = (await octokit.auth({
|
||||
type: "installation",
|
||||
})) as {
|
||||
token: string;
|
||||
};
|
||||
|
||||
return installation.token;
|
||||
};
|
||||
|
||||
export const haveGithubRequirements = (githubProvider: Github) => {
|
||||
return !!(
|
||||
githubProvider?.githubAppId &&
|
||||
githubProvider?.githubPrivateKey &&
|
||||
githubProvider?.githubInstallationId
|
||||
);
|
||||
};
|
||||
|
||||
const getErrorCloneRequirements = (entity: {
|
||||
repository?: string | null;
|
||||
owner?: string | null;
|
||||
branch?: string | null;
|
||||
}) => {
|
||||
const reasons: string[] = [];
|
||||
const { repository, owner, branch } = entity;
|
||||
|
||||
if (!repository) reasons.push("1. Repository not assigned.");
|
||||
if (!owner) reasons.push("2. Owner not specified.");
|
||||
if (!branch) reasons.push("3. Branch not defined.");
|
||||
|
||||
return reasons;
|
||||
};
|
||||
|
||||
export type ApplicationWithGithub = InferResultType<
|
||||
"applications",
|
||||
{ github: true }
|
||||
>;
|
||||
|
||||
export type ComposeWithGithub = InferResultType<"compose", { github: true }>;
|
||||
export const cloneGithubRepository = async (
|
||||
entity: ApplicationWithGithub | ComposeWithGithub,
|
||||
logPath: string,
|
||||
isCompose = false,
|
||||
) => {
|
||||
const { APPLICATIONS_PATH, COMPOSE_PATH } = paths();
|
||||
const writeStream = createWriteStream(logPath, { flags: "a" });
|
||||
const { appName, repository, owner, branch, githubId } = entity;
|
||||
|
||||
if (!githubId) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "GitHub Provider not found",
|
||||
});
|
||||
}
|
||||
|
||||
const requirements = getErrorCloneRequirements(entity);
|
||||
|
||||
// Check if requirements are met
|
||||
if (requirements.length > 0) {
|
||||
writeStream.write(
|
||||
`\nGitHub Repository configuration failed for application: ${appName}\n`,
|
||||
);
|
||||
writeStream.write("Reasons:\n");
|
||||
writeStream.write(requirements.join("\n"));
|
||||
writeStream.end();
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error: GitHub repository information is incomplete.",
|
||||
});
|
||||
}
|
||||
|
||||
const githubProvider = await findGithubById(githubId);
|
||||
const basePath = isCompose ? COMPOSE_PATH : APPLICATIONS_PATH;
|
||||
const outputPath = join(basePath, appName, "code");
|
||||
const octokit = authGithub(githubProvider);
|
||||
const token = await getGithubToken(octokit);
|
||||
const repoclone = `github.com/${owner}/${repository}.git`;
|
||||
await recreateDirectory(outputPath);
|
||||
const cloneUrl = `https://oauth2:${token}@${repoclone}`;
|
||||
|
||||
try {
|
||||
writeStream.write(`\nClonning Repo ${repoclone} to ${outputPath}: ✅\n`);
|
||||
await spawnAsync(
|
||||
"git",
|
||||
[
|
||||
"clone",
|
||||
"--branch",
|
||||
branch!,
|
||||
"--depth",
|
||||
"1",
|
||||
cloneUrl,
|
||||
outputPath,
|
||||
"--progress",
|
||||
],
|
||||
(data) => {
|
||||
if (writeStream.writable) {
|
||||
writeStream.write(data);
|
||||
}
|
||||
},
|
||||
);
|
||||
writeStream.write(`\nCloned ${repoclone}: ✅\n`);
|
||||
} catch (error) {
|
||||
writeStream.write(`ERROR Clonning: ${error}: ❌`);
|
||||
throw error;
|
||||
} finally {
|
||||
writeStream.end();
|
||||
}
|
||||
};
|
||||
|
||||
export const getGithubCloneCommand = async (
|
||||
entity: ApplicationWithGithub | ComposeWithGithub,
|
||||
logPath: string,
|
||||
isCompose = false,
|
||||
) => {
|
||||
const { appName, repository, owner, branch, githubId, serverId } = entity;
|
||||
|
||||
if (!serverId) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Server not found",
|
||||
});
|
||||
}
|
||||
|
||||
if (!githubId) {
|
||||
const command = `
|
||||
echo "Error: ❌ Github Provider not found" >> ${logPath};
|
||||
exit 1;
|
||||
`;
|
||||
|
||||
await execAsyncRemote(serverId, command);
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "GitHub Provider not found",
|
||||
});
|
||||
}
|
||||
|
||||
const requirements = getErrorCloneRequirements(entity);
|
||||
|
||||
// Build log messages
|
||||
let logMessages = "";
|
||||
if (requirements.length > 0) {
|
||||
logMessages += `\nGitHub Repository configuration failed for application: ${appName}\n`;
|
||||
logMessages += "Reasons:\n";
|
||||
logMessages += requirements.join("\n");
|
||||
const escapedLogMessages = logMessages
|
||||
.replace(/\\/g, "\\\\")
|
||||
.replace(/"/g, '\\"')
|
||||
.replace(/\n/g, "\\n");
|
||||
|
||||
const bashCommand = `
|
||||
echo "${escapedLogMessages}" >> ${logPath};
|
||||
exit 1; # Exit with error code
|
||||
`;
|
||||
|
||||
await execAsyncRemote(serverId, bashCommand);
|
||||
return;
|
||||
}
|
||||
const { COMPOSE_PATH, APPLICATIONS_PATH } = paths(true);
|
||||
const githubProvider = await findGithubById(githubId);
|
||||
const basePath = isCompose ? COMPOSE_PATH : APPLICATIONS_PATH;
|
||||
const outputPath = join(basePath, appName, "code");
|
||||
const octokit = authGithub(githubProvider);
|
||||
const token = await getGithubToken(octokit);
|
||||
const repoclone = `github.com/${owner}/${repository}.git`;
|
||||
const cloneUrl = `https://oauth2:${token}@${repoclone}`;
|
||||
|
||||
const cloneCommand = `
|
||||
rm -rf ${outputPath};
|
||||
mkdir -p ${outputPath};
|
||||
if ! git clone --branch ${branch} --depth 1 --progress ${cloneUrl} ${outputPath} >> ${logPath} 2>&1; then
|
||||
echo "❌ [ERROR] Fallo al clonar el repositorio ${repoclone}" >> ${logPath};
|
||||
exit 1;
|
||||
fi
|
||||
echo "Cloned ${repoclone} to ${outputPath}: ✅" >> ${logPath};
|
||||
`;
|
||||
|
||||
return cloneCommand;
|
||||
};
|
||||
|
||||
export const cloneRawGithubRepository = async (entity: Compose) => {
|
||||
const { appName, repository, owner, branch, githubId } = entity;
|
||||
|
||||
if (!githubId) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "GitHub Provider not found",
|
||||
});
|
||||
}
|
||||
const { COMPOSE_PATH } = paths();
|
||||
const githubProvider = await findGithubById(githubId);
|
||||
const basePath = COMPOSE_PATH;
|
||||
const outputPath = join(basePath, appName, "code");
|
||||
const octokit = authGithub(githubProvider);
|
||||
const token = await getGithubToken(octokit);
|
||||
const repoclone = `github.com/${owner}/${repository}.git`;
|
||||
await recreateDirectory(outputPath);
|
||||
const cloneUrl = `https://oauth2:${token}@${repoclone}`;
|
||||
try {
|
||||
await spawnAsync("git", [
|
||||
"clone",
|
||||
"--branch",
|
||||
branch!,
|
||||
"--depth",
|
||||
"1",
|
||||
cloneUrl,
|
||||
outputPath,
|
||||
"--progress",
|
||||
]);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const cloneRawGithubRepositoryRemote = async (compose: Compose) => {
|
||||
const { appName, repository, owner, branch, githubId, serverId } = compose;
|
||||
|
||||
if (!serverId) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Server not found",
|
||||
});
|
||||
}
|
||||
if (!githubId) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "GitHub Provider not found",
|
||||
});
|
||||
}
|
||||
|
||||
const { COMPOSE_PATH } = paths(true);
|
||||
const githubProvider = await findGithubById(githubId);
|
||||
const basePath = COMPOSE_PATH;
|
||||
const outputPath = join(basePath, appName, "code");
|
||||
const octokit = authGithub(githubProvider);
|
||||
const token = await getGithubToken(octokit);
|
||||
const repoclone = `github.com/${owner}/${repository}.git`;
|
||||
const cloneUrl = `https://oauth2:${token}@${repoclone}`;
|
||||
try {
|
||||
const command = `
|
||||
rm -rf ${outputPath};
|
||||
git clone --branch ${branch} --depth 1 ${cloneUrl} ${outputPath}
|
||||
`;
|
||||
await execAsyncRemote(serverId, command);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const getGithubRepositories = async (githubId?: string) => {
|
||||
if (!githubId) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const githubProvider = await findGithubById(githubId);
|
||||
|
||||
const octokit = new Octokit({
|
||||
authStrategy: createAppAuth,
|
||||
auth: {
|
||||
appId: githubProvider.githubAppId,
|
||||
privateKey: githubProvider.githubPrivateKey,
|
||||
installationId: githubProvider.githubInstallationId,
|
||||
},
|
||||
});
|
||||
|
||||
const repositories = (await octokit.paginate(
|
||||
octokit.rest.apps.listReposAccessibleToInstallation,
|
||||
)) as unknown as Awaited<
|
||||
ReturnType<typeof octokit.rest.apps.listReposAccessibleToInstallation>
|
||||
>["data"]["repositories"];
|
||||
|
||||
return repositories;
|
||||
};
|
||||
|
||||
export const getGithubBranches = async (
|
||||
input: typeof apiFindGithubBranches._type,
|
||||
) => {
|
||||
if (!input.githubId) {
|
||||
return [];
|
||||
}
|
||||
const githubProvider = await findGithubById(input.githubId);
|
||||
|
||||
const octokit = new Octokit({
|
||||
authStrategy: createAppAuth,
|
||||
auth: {
|
||||
appId: githubProvider.githubAppId,
|
||||
privateKey: githubProvider.githubPrivateKey,
|
||||
installationId: githubProvider.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;
|
||||
};
|
||||
447
packages/server/src/utils/providers/gitlab.ts
Normal file
447
packages/server/src/utils/providers/gitlab.ts
Normal file
@@ -0,0 +1,447 @@
|
||||
import { createWriteStream } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { paths } from "@/server/constants";
|
||||
import type { apiGitlabTestConnection } from "@/server/db/schema";
|
||||
import type { Compose } from "@/server/services/compose";
|
||||
import {
|
||||
type Gitlab,
|
||||
findGitlabById,
|
||||
updateGitlab,
|
||||
} from "@/server/services/gitlab";
|
||||
import type { InferResultType } from "@/server/types/with";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { recreateDirectory } from "../filesystem/directory";
|
||||
import { execAsyncRemote } from "../process/execAsync";
|
||||
import { spawnAsync } from "../process/spawnAsync";
|
||||
|
||||
export const refreshGitlabToken = async (gitlabProviderId: string) => {
|
||||
const gitlabProvider = await findGitlabById(gitlabProviderId);
|
||||
const currentTime = Math.floor(Date.now() / 1000);
|
||||
|
||||
const safetyMargin = 60;
|
||||
if (
|
||||
gitlabProvider.expiresAt &&
|
||||
currentTime + safetyMargin < gitlabProvider.expiresAt
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await fetch("https://gitlab.com/oauth/token", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
grant_type: "refresh_token",
|
||||
refresh_token: gitlabProvider.refreshToken as string,
|
||||
client_id: gitlabProvider.applicationId as string,
|
||||
client_secret: gitlabProvider.secret as string,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to refresh token: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
const expiresAt = Math.floor(Date.now() / 1000) + data.expires_in;
|
||||
|
||||
console.log("Refreshed token");
|
||||
|
||||
await updateGitlab(gitlabProviderId, {
|
||||
accessToken: data.access_token,
|
||||
refreshToken: data.refresh_token,
|
||||
expiresAt,
|
||||
});
|
||||
return data;
|
||||
};
|
||||
|
||||
export const haveGitlabRequirements = (gitlabProvider: Gitlab) => {
|
||||
return !!(gitlabProvider?.accessToken && gitlabProvider?.refreshToken);
|
||||
};
|
||||
|
||||
const getErrorCloneRequirements = (entity: {
|
||||
gitlabRepository?: string | null;
|
||||
gitlabOwner?: string | null;
|
||||
gitlabBranch?: string | null;
|
||||
gitlabPathNamespace?: string | null;
|
||||
}) => {
|
||||
const reasons: string[] = [];
|
||||
const { gitlabBranch, gitlabOwner, gitlabRepository, gitlabPathNamespace } =
|
||||
entity;
|
||||
|
||||
if (!gitlabRepository) reasons.push("1. Repository not assigned.");
|
||||
if (!gitlabOwner) reasons.push("2. Owner not specified.");
|
||||
if (!gitlabBranch) reasons.push("3. Branch not defined.");
|
||||
if (!gitlabPathNamespace) reasons.push("4. Path namespace not defined.");
|
||||
|
||||
return reasons;
|
||||
};
|
||||
|
||||
export type ApplicationWithGitlab = InferResultType<
|
||||
"applications",
|
||||
{ gitlab: true }
|
||||
>;
|
||||
|
||||
export type ComposeWithGitlab = InferResultType<"compose", { gitlab: true }>;
|
||||
|
||||
export const cloneGitlabRepository = async (
|
||||
entity: ApplicationWithGitlab | ComposeWithGitlab,
|
||||
logPath: string,
|
||||
isCompose = false,
|
||||
) => {
|
||||
const writeStream = createWriteStream(logPath, { flags: "a" });
|
||||
const { appName, gitlabBranch, gitlabId, gitlab, gitlabPathNamespace } =
|
||||
entity;
|
||||
|
||||
if (!gitlabId) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Gitlab Provider not found",
|
||||
});
|
||||
}
|
||||
|
||||
await refreshGitlabToken(gitlabId);
|
||||
|
||||
const requirements = getErrorCloneRequirements(entity);
|
||||
|
||||
// Check if requirements are met
|
||||
if (requirements.length > 0) {
|
||||
writeStream.write(
|
||||
`\nGitLab Repository configuration failed for application: ${appName}\n`,
|
||||
);
|
||||
writeStream.write("Reasons:\n");
|
||||
writeStream.write(requirements.join("\n"));
|
||||
writeStream.end();
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error: GitLab repository information is incomplete.",
|
||||
});
|
||||
}
|
||||
|
||||
const { COMPOSE_PATH, APPLICATIONS_PATH } = paths();
|
||||
const basePath = isCompose ? COMPOSE_PATH : APPLICATIONS_PATH;
|
||||
const outputPath = join(basePath, appName, "code");
|
||||
await recreateDirectory(outputPath);
|
||||
const repoclone = `gitlab.com/${gitlabPathNamespace}.git`;
|
||||
const cloneUrl = `https://oauth2:${gitlab?.accessToken}@${repoclone}`;
|
||||
|
||||
try {
|
||||
writeStream.write(`\nClonning Repo ${repoclone} to ${outputPath}: ✅\n`);
|
||||
await spawnAsync(
|
||||
"git",
|
||||
[
|
||||
"clone",
|
||||
"--branch",
|
||||
gitlabBranch!,
|
||||
"--depth",
|
||||
"1",
|
||||
cloneUrl,
|
||||
outputPath,
|
||||
"--progress",
|
||||
],
|
||||
(data) => {
|
||||
if (writeStream.writable) {
|
||||
writeStream.write(data);
|
||||
}
|
||||
},
|
||||
);
|
||||
writeStream.write(`\nCloned ${repoclone}: ✅\n`);
|
||||
} catch (error) {
|
||||
writeStream.write(`ERROR Clonning: ${error}: ❌`);
|
||||
throw error;
|
||||
} finally {
|
||||
writeStream.end();
|
||||
}
|
||||
};
|
||||
|
||||
export const getGitlabCloneCommand = async (
|
||||
entity: ApplicationWithGitlab | ComposeWithGitlab,
|
||||
logPath: string,
|
||||
isCompose = false,
|
||||
) => {
|
||||
const {
|
||||
appName,
|
||||
gitlabRepository,
|
||||
gitlabOwner,
|
||||
gitlabPathNamespace,
|
||||
gitlabBranch,
|
||||
gitlabId,
|
||||
serverId,
|
||||
gitlab,
|
||||
} = entity;
|
||||
|
||||
if (!serverId) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Server not found",
|
||||
});
|
||||
}
|
||||
|
||||
if (!gitlabId) {
|
||||
const command = `
|
||||
echo "Error: ❌ Gitlab Provider not found" >> ${logPath};
|
||||
exit 1;
|
||||
`;
|
||||
|
||||
await execAsyncRemote(serverId, command);
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Gitlab Provider not found",
|
||||
});
|
||||
}
|
||||
|
||||
const requirements = getErrorCloneRequirements(entity);
|
||||
|
||||
// Build log messages
|
||||
let logMessages = "";
|
||||
if (requirements.length > 0) {
|
||||
logMessages += `\nGitLab Repository configuration failed for application: ${appName}\n`;
|
||||
logMessages += "Reasons:\n";
|
||||
logMessages += requirements.join("\n");
|
||||
const escapedLogMessages = logMessages
|
||||
.replace(/\\/g, "\\\\")
|
||||
.replace(/"/g, '\\"')
|
||||
.replace(/\n/g, "\\n");
|
||||
|
||||
const bashCommand = `
|
||||
echo "${escapedLogMessages}" >> ${logPath};
|
||||
exit 1; # Exit with error code
|
||||
`;
|
||||
|
||||
await execAsyncRemote(serverId, bashCommand);
|
||||
return;
|
||||
}
|
||||
|
||||
const { COMPOSE_PATH, APPLICATIONS_PATH } = paths(true);
|
||||
await refreshGitlabToken(gitlabId);
|
||||
const basePath = isCompose ? COMPOSE_PATH : APPLICATIONS_PATH;
|
||||
const outputPath = join(basePath, appName, "code");
|
||||
await recreateDirectory(outputPath);
|
||||
const repoclone = `gitlab.com/${gitlabPathNamespace}.git`;
|
||||
const cloneUrl = `https://oauth2:${gitlab?.accessToken}@${repoclone}`;
|
||||
|
||||
const cloneCommand = `
|
||||
rm -rf ${outputPath};
|
||||
mkdir -p ${outputPath};
|
||||
if ! git clone --branch ${gitlabBranch} --depth 1 --progress ${cloneUrl} ${outputPath} >> ${logPath} 2>&1; then
|
||||
echo "❌ [ERROR] Fail to clone the repository ${repoclone}" >> ${logPath};
|
||||
exit 1;
|
||||
fi
|
||||
echo "Cloned ${repoclone} to ${outputPath}: ✅" >> ${logPath};
|
||||
`;
|
||||
|
||||
return cloneCommand;
|
||||
};
|
||||
|
||||
export const getGitlabRepositories = async (gitlabId?: string) => {
|
||||
if (!gitlabId) {
|
||||
return [];
|
||||
}
|
||||
|
||||
await refreshGitlabToken(gitlabId);
|
||||
|
||||
const gitlabProvider = await findGitlabById(gitlabId);
|
||||
|
||||
const response = await fetch(
|
||||
`https://gitlab.com/api/v4/projects?membership=true&owned=true&page=${0}&per_page=${100}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${gitlabProvider.accessToken}`,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: `Failed to fetch repositories: ${response.statusText}`,
|
||||
});
|
||||
}
|
||||
|
||||
const repositories = await response.json();
|
||||
|
||||
const filteredRepos = repositories.filter((repo: any) => {
|
||||
const { full_path, kind } = repo.namespace;
|
||||
const groupName = gitlabProvider.groupName?.toLowerCase();
|
||||
|
||||
if (groupName) {
|
||||
return full_path.toLowerCase().includes(groupName) && kind === "group";
|
||||
}
|
||||
return kind === "user";
|
||||
});
|
||||
const mappedRepositories = filteredRepos.map((repo: any) => {
|
||||
return {
|
||||
id: repo.id,
|
||||
name: repo.name,
|
||||
url: repo.path_with_namespace,
|
||||
owner: {
|
||||
username: repo.namespace.path,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
return mappedRepositories as {
|
||||
id: number;
|
||||
name: string;
|
||||
url: string;
|
||||
owner: {
|
||||
username: string;
|
||||
};
|
||||
}[];
|
||||
};
|
||||
|
||||
export const getGitlabBranches = async (input: {
|
||||
id?: number;
|
||||
gitlabId?: string;
|
||||
owner: string;
|
||||
repo: string;
|
||||
}) => {
|
||||
if (!input.gitlabId || !input.id || input.id === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const gitlabProvider = await findGitlabById(input.gitlabId);
|
||||
|
||||
const branchesResponse = await fetch(
|
||||
`https://gitlab.com/api/v4/projects/${input.id}/repository/branches`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${gitlabProvider.accessToken}`,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (!branchesResponse.ok) {
|
||||
throw new Error(`Failed to fetch branches: ${branchesResponse.statusText}`);
|
||||
}
|
||||
|
||||
const branches = await branchesResponse.json();
|
||||
|
||||
return branches as {
|
||||
id: string;
|
||||
name: string;
|
||||
commit: {
|
||||
id: string;
|
||||
};
|
||||
}[];
|
||||
};
|
||||
|
||||
export const cloneRawGitlabRepository = async (entity: Compose) => {
|
||||
const {
|
||||
appName,
|
||||
gitlabRepository,
|
||||
gitlabOwner,
|
||||
gitlabBranch,
|
||||
gitlabId,
|
||||
gitlabPathNamespace,
|
||||
} = entity;
|
||||
|
||||
if (!gitlabId) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Gitlab Provider not found",
|
||||
});
|
||||
}
|
||||
|
||||
const gitlabProvider = await findGitlabById(gitlabId);
|
||||
const { COMPOSE_PATH } = paths();
|
||||
await refreshGitlabToken(gitlabId);
|
||||
const basePath = COMPOSE_PATH;
|
||||
const outputPath = join(basePath, appName, "code");
|
||||
await recreateDirectory(outputPath);
|
||||
const repoclone = `gitlab.com/${gitlabPathNamespace}.git`;
|
||||
const cloneUrl = `https://oauth2:${gitlabProvider?.accessToken}@${repoclone}`;
|
||||
|
||||
try {
|
||||
await spawnAsync("git", [
|
||||
"clone",
|
||||
"--branch",
|
||||
gitlabBranch!,
|
||||
"--depth",
|
||||
"1",
|
||||
cloneUrl,
|
||||
outputPath,
|
||||
"--progress",
|
||||
]);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const cloneRawGitlabRepositoryRemote = async (compose: Compose) => {
|
||||
const { appName, gitlabPathNamespace, branch, gitlabId, serverId } = compose;
|
||||
|
||||
if (!serverId) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Server not found",
|
||||
});
|
||||
}
|
||||
if (!gitlabId) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Gitlab Provider not found",
|
||||
});
|
||||
}
|
||||
const gitlabProvider = await findGitlabById(gitlabId);
|
||||
const { COMPOSE_PATH } = paths(true);
|
||||
await refreshGitlabToken(gitlabId);
|
||||
const basePath = COMPOSE_PATH;
|
||||
const outputPath = join(basePath, appName, "code");
|
||||
const repoclone = `gitlab.com/${gitlabPathNamespace}.git`;
|
||||
const cloneUrl = `https://oauth2:${gitlabProvider?.accessToken}@${repoclone}`;
|
||||
try {
|
||||
const command = `
|
||||
rm -rf ${outputPath};
|
||||
git clone --branch ${branch} --depth 1 ${cloneUrl} ${outputPath}
|
||||
`;
|
||||
await execAsyncRemote(serverId, command);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const testGitlabConnection = async (
|
||||
input: typeof apiGitlabTestConnection._type,
|
||||
) => {
|
||||
const { gitlabId, groupName } = input;
|
||||
|
||||
if (!gitlabId) {
|
||||
throw new Error("Gitlab provider not found");
|
||||
}
|
||||
|
||||
await refreshGitlabToken(gitlabId);
|
||||
|
||||
const gitlabProvider = await findGitlabById(gitlabId);
|
||||
|
||||
const response = await fetch(
|
||||
`https://gitlab.com/api/v4/projects?membership=true&owned=true&page=${0}&per_page=${100}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${gitlabProvider.accessToken}`,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: `Failed to fetch repositories: ${response.statusText}`,
|
||||
});
|
||||
}
|
||||
|
||||
const repositories = await response.json();
|
||||
|
||||
const filteredRepos = repositories.filter((repo: any) => {
|
||||
const { full_path, kind } = repo.namespace;
|
||||
|
||||
if (groupName) {
|
||||
return full_path.toLowerCase().includes(groupName) && kind === "group";
|
||||
}
|
||||
return kind === "user";
|
||||
});
|
||||
|
||||
return filteredRepos.length;
|
||||
};
|
||||
81
packages/server/src/utils/providers/raw.ts
Normal file
81
packages/server/src/utils/providers/raw.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import { createWriteStream } from "node:fs";
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { paths } from "@/server/constants";
|
||||
import type { Compose } from "@/server/services/compose";
|
||||
import { encodeBase64 } from "../docker/utils";
|
||||
import { recreateDirectory } from "../filesystem/directory";
|
||||
import { execAsyncRemote } from "../process/execAsync";
|
||||
|
||||
export const createComposeFile = async (compose: Compose, logPath: string) => {
|
||||
const { COMPOSE_PATH } = paths();
|
||||
const { appName, composeFile } = compose;
|
||||
const writeStream = createWriteStream(logPath, { flags: "a" });
|
||||
const outputPath = join(COMPOSE_PATH, appName, "code");
|
||||
|
||||
try {
|
||||
await recreateDirectory(outputPath);
|
||||
writeStream.write(
|
||||
`\nCreating File 'docker-compose.yml' to ${outputPath}: ✅\n`,
|
||||
);
|
||||
|
||||
await writeFile(join(outputPath, "docker-compose.yml"), composeFile);
|
||||
|
||||
writeStream.write(`\nFile 'docker-compose.yml' created: ✅\n`);
|
||||
} catch (error) {
|
||||
writeStream.write(`\nERROR Creating Compose File: ${error}: ❌\n`);
|
||||
throw error;
|
||||
} finally {
|
||||
writeStream.end();
|
||||
}
|
||||
};
|
||||
|
||||
export const getCreateComposeFileCommand = (
|
||||
compose: Compose,
|
||||
logPath: string,
|
||||
) => {
|
||||
const { COMPOSE_PATH } = paths(true);
|
||||
const { appName, composeFile } = compose;
|
||||
const outputPath = join(COMPOSE_PATH, appName, "code");
|
||||
const filePath = join(outputPath, "docker-compose.yml");
|
||||
const encodedContent = encodeBase64(composeFile);
|
||||
const bashCommand = `
|
||||
rm -rf ${outputPath};
|
||||
mkdir -p ${outputPath};
|
||||
echo "${encodedContent}" | base64 -d > "${filePath}";
|
||||
echo "File 'docker-compose.yml' created: ✅" >> ${logPath};
|
||||
`;
|
||||
return bashCommand;
|
||||
};
|
||||
|
||||
export const createComposeFileRaw = async (compose: Compose) => {
|
||||
const { COMPOSE_PATH } = paths();
|
||||
const { appName, composeFile } = compose;
|
||||
const outputPath = join(COMPOSE_PATH, appName, "code");
|
||||
const filePath = join(outputPath, "docker-compose.yml");
|
||||
try {
|
||||
await recreateDirectory(outputPath);
|
||||
await writeFile(filePath, composeFile);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const createComposeFileRawRemote = async (compose: Compose) => {
|
||||
const { COMPOSE_PATH } = paths(true);
|
||||
const { appName, composeFile, serverId } = compose;
|
||||
const outputPath = join(COMPOSE_PATH, appName, "code");
|
||||
const filePath = join(outputPath, "docker-compose.yml");
|
||||
|
||||
try {
|
||||
const encodedContent = encodeBase64(composeFile);
|
||||
const command = `
|
||||
rm -rf ${outputPath};
|
||||
mkdir -p ${outputPath};
|
||||
echo "${encodedContent}" | base64 -d > "${filePath}";
|
||||
`;
|
||||
await execAsyncRemote(serverId, command);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user