mirror of
https://github.com/coollabsio/coolify.git
synced 2026-03-11 08:55:47 +00:00
194 lines
6.5 KiB
TypeScript
194 lines
6.5 KiB
TypeScript
import cuid from 'cuid';
|
|
import crypto from 'crypto';
|
|
import { uniqueNamesGenerator, adjectives, colors, animals } from 'unique-names-generator';
|
|
import { docker } from '$lib/api/docker';
|
|
import { baseServiceConfiguration } from './common';
|
|
import { execShellAsync } from '../common';
|
|
|
|
function getUniq() {
|
|
return uniqueNamesGenerator({ dictionaries: [adjectives, animals, colors], length: 2 });
|
|
}
|
|
|
|
export function setDefaultConfiguration(configuration) {
|
|
const nickname = configuration.general.nickname || getUniq();
|
|
const deployId = cuid();
|
|
const shaBase = JSON.stringify({ repository: configuration.repository });
|
|
const sha256 = crypto.createHash('sha256').update(shaBase).digest('hex');
|
|
|
|
configuration.build.container.name = sha256.slice(0, 15);
|
|
|
|
configuration.general.nickname = nickname;
|
|
configuration.general.deployId = deployId;
|
|
configuration.general.workdir = `/tmp/${deployId}`;
|
|
if (configuration.general.isPreviewDeploymentEnabled && configuration.general.pullRequest !== 0) {
|
|
configuration.build.container.name = `pr${configuration.general.pullRequest}-${sha256.slice(
|
|
0,
|
|
8
|
|
)}`;
|
|
configuration.publish.domain = `pr${configuration.general.pullRequest}.${configuration.publish.domain}`;
|
|
}
|
|
if (!configuration.publish.path) configuration.publish.path = '/';
|
|
if (!configuration.publish.port) {
|
|
if (
|
|
configuration.build.pack === 'nodejs' ||
|
|
configuration.build.pack === 'nuxtjs' ||
|
|
configuration.build.pack === 'rust' ||
|
|
configuration.build.pack === 'nextjs' ||
|
|
configuration.build.pack === 'nestjs'
|
|
) {
|
|
configuration.publish.port = 3000;
|
|
} else if (configuration.build.pack === 'python') {
|
|
configuration.publish.port = 4000;
|
|
} else {
|
|
configuration.publish.port = 80;
|
|
}
|
|
}
|
|
if (!configuration.build.directory) configuration.build.directory = '';
|
|
if (configuration.build.directory.startsWith('/'))
|
|
configuration.build.directory = configuration.build.directory.replace('/', '');
|
|
|
|
if (!configuration.publish.directory) configuration.publish.directory = '';
|
|
if (configuration.publish.directory.startsWith('/'))
|
|
configuration.publish.directory = configuration.publish.directory.replace('/', '');
|
|
|
|
if (configuration.build.pack === 'static' || configuration.build.pack === 'nodejs') {
|
|
if (!configuration.build.command.installation)
|
|
configuration.build.command.installation = 'yarn install';
|
|
}
|
|
if (
|
|
configuration.build.pack === 'nodejs' ||
|
|
configuration.build.pack === 'vuejs' ||
|
|
configuration.build.pack === 'nuxtjs' ||
|
|
configuration.build.pack === 'nextjs' ||
|
|
configuration.build.pack === 'nestjs'
|
|
) {
|
|
if (!configuration.build.command.start) configuration.build.command.start = 'yarn start';
|
|
}
|
|
if (configuration.build.pack === 'python') {
|
|
if (!configuration.build.command.python.module)
|
|
configuration.build.command.python.module = 'main';
|
|
if (!configuration.build.command.python.instance)
|
|
configuration.build.command.python.instance = 'app';
|
|
}
|
|
|
|
configuration.build.container.baseSHA = crypto
|
|
.createHash('sha256')
|
|
.update(JSON.stringify(baseServiceConfiguration))
|
|
.digest('hex');
|
|
configuration.baseServiceConfiguration = baseServiceConfiguration;
|
|
|
|
return configuration;
|
|
}
|
|
|
|
export async function precheckDeployment(configuration) {
|
|
const services = (await docker.engine.listServices()).filter(
|
|
(r) =>
|
|
r.Spec.Labels.managedBy === 'coolify' &&
|
|
r.Spec.Labels.type === 'application' &&
|
|
JSON.parse(r.Spec.Labels.configuration).publish.domain === configuration.publish.domain
|
|
);
|
|
let foundService = false;
|
|
let configChanged = false;
|
|
let imageChanged = false;
|
|
let forceUpdate = false;
|
|
for (const service of services) {
|
|
const running = JSON.parse(service.Spec.Labels.configuration);
|
|
if (running) {
|
|
if (
|
|
running.repository.id === configuration.repository.id &&
|
|
running.repository.branch === configuration.repository.branch
|
|
) {
|
|
foundService = true;
|
|
// Base service configuration changed
|
|
if (
|
|
!running.build.container.baseSHA ||
|
|
running.build.container.baseSHA !== configuration.build.container.baseSHA
|
|
) {
|
|
forceUpdate = true;
|
|
}
|
|
// If the deployment is in error state, forceUpdate
|
|
const state = await execShellAsync(
|
|
`docker stack ps ${running.build.container.name} --format '{{ json . }}'`
|
|
);
|
|
const isError = state
|
|
.split('\n')
|
|
.filter((n) => n)
|
|
.map((s) => JSON.parse(s))
|
|
.filter(
|
|
(n) =>
|
|
n.DesiredState !== 'Running' && n.Image.split(':')[1] === running.build.container.tag
|
|
);
|
|
if (isError.length > 0) {
|
|
forceUpdate = true;
|
|
}
|
|
|
|
const compareObjects = (a, b) => {
|
|
if (a === b) return true;
|
|
|
|
if (typeof a != 'object' || typeof b != 'object' || a == null || b == null) return false;
|
|
|
|
const keysA = Object.keys(a),
|
|
keysB = Object.keys(b);
|
|
|
|
if (keysA.length != keysB.length) return false;
|
|
|
|
for (const key of keysA) {
|
|
if (!keysB.includes(key)) return false;
|
|
|
|
if (typeof a[key] === 'function' || typeof b[key] === 'function') {
|
|
if (a[key].toString() != b[key].toString()) return false;
|
|
} else {
|
|
if (!compareObjects(a[key], b[key])) return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
};
|
|
|
|
const runningWithoutContainer = JSON.parse(JSON.stringify(running));
|
|
delete runningWithoutContainer.build.container;
|
|
|
|
const configurationWithoutContainer = JSON.parse(JSON.stringify(configuration));
|
|
delete configurationWithoutContainer.build.container;
|
|
|
|
// If only the configuration changed
|
|
if (
|
|
!compareObjects(runningWithoutContainer.build, configurationWithoutContainer.build) ||
|
|
!compareObjects(runningWithoutContainer.publish, configurationWithoutContainer.publish) ||
|
|
runningWithoutContainer.general.isPreviewDeploymentEnabled !==
|
|
configurationWithoutContainer.general.isPreviewDeploymentEnabled
|
|
) {
|
|
configChanged = true;
|
|
}
|
|
|
|
// If only the image changed
|
|
if (running.build.container.tag !== configuration.build.container.tag) imageChanged = true;
|
|
// If build pack changed, forceUpdate the service
|
|
if (running.build.pack !== configuration.build.pack) forceUpdate = true;
|
|
if (
|
|
configuration.general.isPreviewDeploymentEnabled &&
|
|
configuration.general.pullRequest !== 0
|
|
)
|
|
forceUpdate = true;
|
|
}
|
|
}
|
|
}
|
|
if (forceUpdate) {
|
|
imageChanged = false;
|
|
configChanged = false;
|
|
}
|
|
return {
|
|
foundService,
|
|
imageChanged,
|
|
configChanged,
|
|
forceUpdate
|
|
};
|
|
}
|
|
|
|
export async function updateServiceLabels(configuration) {
|
|
return await execShellAsync(
|
|
`docker service update --label-add configuration='${JSON.stringify(configuration)}' ${
|
|
configuration.build.container.name
|
|
}_${configuration.build.container.name}`
|
|
);
|
|
}
|