v1.0.23 (#68)
# Features - Build environment variables for NodeJS builds - Initial monorepo support (more tests needed!) # Fixes - Fix wrong redirects - Logout fix for the session manager
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
import { docker } from '$lib/api/docker';
|
||||
import Deployment from '$models/Deployment';
|
||||
import { execShellAsync } from '../common';
|
||||
|
||||
export async function deleteSameDeployments(configuration) {
|
||||
import crypto from 'crypto';
|
||||
export async function deleteSameDeployments(configuration, originalDomain = null) {
|
||||
await (
|
||||
await docker.engine.listServices()
|
||||
)
|
||||
@@ -12,7 +12,7 @@ export async function deleteSameDeployments(configuration) {
|
||||
if (
|
||||
running.repository.id === configuration.repository.id &&
|
||||
running.repository.branch === configuration.repository.branch &&
|
||||
running.publish.domain === configuration.publish.domain
|
||||
running.publish.domain === originalDomain || configuration.publish.domain
|
||||
) {
|
||||
await execShellAsync(`docker stack rm ${s.Spec.Labels['com.docker.stack.namespace']}`);
|
||||
}
|
||||
|
@@ -4,7 +4,8 @@ import { uniqueNamesGenerator, adjectives, colors, animals } from 'unique-names-
|
||||
import { docker } from '$lib/api/docker';
|
||||
import { baseServiceConfiguration } from './common';
|
||||
import { execShellAsync } from '../common';
|
||||
|
||||
import { promises as fs } from 'fs';
|
||||
import Configuration from '$models/Configuration';
|
||||
function getUniq() {
|
||||
return uniqueNamesGenerator({ dictionaries: [adjectives, animals, colors], length: 2 });
|
||||
}
|
||||
@@ -12,7 +13,7 @@ function getUniq() {
|
||||
export function setDefaultConfiguration(configuration) {
|
||||
const nickname = configuration.general.nickname || getUniq();
|
||||
const deployId = cuid();
|
||||
const shaBase = JSON.stringify({ repository: configuration.repository });
|
||||
const shaBase = JSON.stringify({ path: configuration.publish.path, domain: configuration.publish.domain });
|
||||
const sha256 = crypto.createHash('sha256').update(shaBase).digest('hex');
|
||||
|
||||
configuration.build.container.name = sha256.slice(0, 15);
|
||||
@@ -51,7 +52,7 @@ export function setDefaultConfiguration(configuration) {
|
||||
if (configuration.publish.directory.startsWith('/'))
|
||||
configuration.publish.directory = configuration.publish.directory.replace('/', '');
|
||||
|
||||
if (configuration.build.pack === 'static' || configuration.build.pack === 'nodejs') {
|
||||
if (configuration.build.pack === 'nodejs') {
|
||||
if (!configuration.build.command.installation)
|
||||
configuration.build.command.installation = 'yarn install';
|
||||
}
|
||||
@@ -81,34 +82,37 @@ export function setDefaultConfiguration(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
|
||||
);
|
||||
const services = await Configuration.find({
|
||||
'publish.domain': configuration.publish.domain,
|
||||
'publish.path': configuration.publish.path
|
||||
})
|
||||
// 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) {
|
||||
// const running = JSON.parse(service.Spec.Labels.configuration);
|
||||
if (
|
||||
running.repository.id === configuration.repository.id &&
|
||||
running.repository.branch === configuration.repository.branch
|
||||
service.repository.id === configuration.repository.id &&
|
||||
service.repository.branch === configuration.repository.branch
|
||||
) {
|
||||
foundService = true;
|
||||
// Base service configuration changed
|
||||
if (
|
||||
!running.build.container.baseSHA ||
|
||||
running.build.container.baseSHA !== configuration.build.container.baseSHA
|
||||
!service.build.container.baseSHA ||
|
||||
service.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 . }}'`
|
||||
`docker stack ps ${service.build.container.name} --format '{{ json . }}'`
|
||||
);
|
||||
const isError = state
|
||||
.split('\n')
|
||||
@@ -116,7 +120,7 @@ export async function precheckDeployment(configuration) {
|
||||
.map((s) => JSON.parse(s))
|
||||
.filter(
|
||||
(n) =>
|
||||
n.DesiredState !== 'Running' && n.Image.split(':')[1] === running.build.container.tag
|
||||
n.DesiredState !== 'Running' && n.Image.split(':')[1] === service.build.container.tag
|
||||
);
|
||||
if (isError.length > 0) {
|
||||
forceUpdate = true;
|
||||
@@ -145,7 +149,7 @@ export async function precheckDeployment(configuration) {
|
||||
return true;
|
||||
};
|
||||
|
||||
const runningWithoutContainer = JSON.parse(JSON.stringify(running));
|
||||
const runningWithoutContainer = JSON.parse(JSON.stringify(service));
|
||||
delete runningWithoutContainer.build.container;
|
||||
|
||||
const configurationWithoutContainer = JSON.parse(JSON.stringify(configuration));
|
||||
@@ -162,16 +166,16 @@ export async function precheckDeployment(configuration) {
|
||||
}
|
||||
|
||||
// If only the image changed
|
||||
if (running.build.container.tag !== configuration.build.container.tag) imageChanged = true;
|
||||
if (service.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 (service.build.pack !== configuration.build.pack) forceUpdate = true;
|
||||
if (
|
||||
configuration.general.isPreviewDeploymentEnabled &&
|
||||
configuration.general.pullRequest !== 0
|
||||
)
|
||||
forceUpdate = true;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
if (forceUpdate) {
|
||||
imageChanged = false;
|
||||
|
@@ -5,7 +5,7 @@ import { deleteSameDeployments, purgeImagesContainers } from './cleanup';
|
||||
import yaml from 'js-yaml';
|
||||
import { delay, execShellAsync } from '../common';
|
||||
|
||||
export default async function (configuration, imageChanged) {
|
||||
export default async function (configuration, nextStep) {
|
||||
const generateEnvs = {};
|
||||
for (const secret of configuration.publish.secrets) {
|
||||
generateEnvs[secret.name] = secret.value;
|
||||
@@ -56,23 +56,25 @@ export default async function (configuration, imageChanged) {
|
||||
};
|
||||
await saveAppLog('### Publishing.', configuration);
|
||||
await fs.writeFile(`${configuration.general.workdir}/stack.yml`, yaml.dump(stack));
|
||||
if (imageChanged) {
|
||||
if (nextStep === 2) {
|
||||
// console.log('image changed')
|
||||
await execShellAsync(
|
||||
`docker service update --image ${containerName}:${containerTag} ${containerName}_${containerName}`
|
||||
);
|
||||
} else {
|
||||
// console.log('new deployment or force deployment or config changed')
|
||||
await deleteSameDeployments(configuration);
|
||||
// if (originalDomain !== configuration.publish.domain) {
|
||||
// await deleteSameDeployments(configuration, originalDomain);
|
||||
// } else {
|
||||
// await deleteSameDeployments(configuration);
|
||||
// }
|
||||
// await deleteSameDeployments(configuration);
|
||||
await execShellAsync(
|
||||
`cat ${configuration.general.workdir}/stack.yml | docker stack deploy --prune -c - ${containerName}`
|
||||
);
|
||||
}
|
||||
async function purgeImagesAsync(found) {
|
||||
await delay(10000);
|
||||
await purgeImagesContainers(found);
|
||||
}
|
||||
//purgeImagesAsync(configuration);
|
||||
|
||||
|
||||
|
||||
await saveAppLog('### Published done!', configuration);
|
||||
}
|
||||
|
@@ -1,9 +1,10 @@
|
||||
import { docker, streamEvents } from '$lib/api/docker';
|
||||
import { promises as fs } from 'fs';
|
||||
|
||||
const buildImageNodeDocker = (configuration, prodBuild) => {
|
||||
const buildImageNodeDocker = (configuration, prodBuild, generateEnvs) => {
|
||||
return [
|
||||
'FROM node:lts',
|
||||
...generateEnvs,
|
||||
'WORKDIR /usr/src/app',
|
||||
`COPY ${configuration.build.directory}/package*.json ./`,
|
||||
configuration.build.command.installation && `RUN ${configuration.build.command.installation}`,
|
||||
@@ -13,18 +14,29 @@ const buildImageNodeDocker = (configuration, prodBuild) => {
|
||||
].join('\n');
|
||||
};
|
||||
export async function buildImage(configuration, cacheBuild?: boolean, prodBuild?: boolean) {
|
||||
// TODO: Edit secrets
|
||||
// TODO: Add secret from .env file / json
|
||||
const generateEnvs = [];
|
||||
const dotEnv = []
|
||||
for (const secret of configuration.publish.secrets) {
|
||||
dotEnv.push(`${secret.name}=${secret.value}`)
|
||||
if (secret.isBuild) generateEnvs.push(`ENV ${secret.name}=${secret.value}`)
|
||||
}
|
||||
await fs.writeFile(
|
||||
`${configuration.general.workdir}/.env`,
|
||||
dotEnv.join('\n')
|
||||
)
|
||||
await fs.writeFile(
|
||||
`${configuration.general.workdir}/Dockerfile`,
|
||||
buildImageNodeDocker(configuration, prodBuild)
|
||||
buildImageNodeDocker(configuration, prodBuild, generateEnvs)
|
||||
);
|
||||
const stream = await docker.engine.buildImage(
|
||||
{ src: ['.'], context: configuration.general.workdir },
|
||||
{
|
||||
t: `${configuration.build.container.name}:${
|
||||
cacheBuild
|
||||
? `${configuration.build.container.tag}-cache`
|
||||
: configuration.build.container.tag
|
||||
}`
|
||||
t: `${configuration.build.container.name}:${cacheBuild
|
||||
? `${configuration.build.container.tag}-cache`
|
||||
: configuration.build.container.tag
|
||||
}`
|
||||
}
|
||||
);
|
||||
await streamEvents(stream, configuration);
|
||||
|
76
src/lib/api/applications/preChecks.ts
Normal file
76
src/lib/api/applications/preChecks.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import Configuration from "$models/Configuration";
|
||||
import { compareObjects, execShellAsync } from "../common";
|
||||
|
||||
export default async function (configuration) {
|
||||
/*
|
||||
0 => nothing changed, no need to redeploy
|
||||
1 => force update
|
||||
2 => configuration changed
|
||||
3 => continue normally
|
||||
*/
|
||||
const currentConfiguration = await Configuration.findOne({
|
||||
'general.nickname': configuration.general.nickname
|
||||
})
|
||||
if (currentConfiguration) {
|
||||
// Base service configuration changed
|
||||
if (
|
||||
!currentConfiguration.build.container.baseSHA ||
|
||||
currentConfiguration.build.container.baseSHA !== configuration.build.container.baseSHA
|
||||
) {
|
||||
return 1
|
||||
}
|
||||
|
||||
// If the deployment is in error state, forceUpdate
|
||||
try {
|
||||
const state = await execShellAsync(
|
||||
`docker stack ps ${currentConfiguration.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] === currentConfiguration.build.container.tag
|
||||
);
|
||||
if (isError.length > 0) {
|
||||
return 1
|
||||
}
|
||||
} catch(error) {
|
||||
console.log(error)
|
||||
}
|
||||
|
||||
|
||||
// If previewDeployments enabled
|
||||
if (
|
||||
currentConfiguration.general.isPreviewDeploymentEnabled &&
|
||||
currentConfiguration.general.pullRequest !== 0
|
||||
) {
|
||||
return 1
|
||||
}
|
||||
// If build pack changed, forceUpdate the service
|
||||
if (currentConfiguration.build.pack !== configuration.build.pack) {
|
||||
return 1
|
||||
}
|
||||
|
||||
const currentConfigurationCompare = JSON.parse(JSON.stringify(currentConfiguration));
|
||||
const configurationCompare = JSON.parse(JSON.stringify(configuration));
|
||||
delete currentConfigurationCompare.build.container;
|
||||
delete configurationCompare.build.container;
|
||||
|
||||
if (
|
||||
!compareObjects(currentConfigurationCompare.build, configurationCompare.build) ||
|
||||
!compareObjects(currentConfigurationCompare.publish, configurationCompare.publish) ||
|
||||
currentConfigurationCompare.general.isPreviewDeploymentEnabled !==
|
||||
configurationCompare.general.isPreviewDeploymentEnabled
|
||||
) {
|
||||
return 1
|
||||
}
|
||||
|
||||
if (currentConfiguration.build.container.tag !== configuration.build.container.tag) {
|
||||
return 2
|
||||
}
|
||||
return 0
|
||||
}
|
||||
return 3
|
||||
}
|
44
src/lib/api/applications/preTasks.ts
Normal file
44
src/lib/api/applications/preTasks.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import Configuration from "$models/Configuration";
|
||||
import Deployment from "$models/Deployment";
|
||||
|
||||
export default async function (configuration) {
|
||||
// Check if deployment is already queued
|
||||
const alreadyQueued = await Deployment.find({
|
||||
path: configuration.publish.path,
|
||||
domain: configuration.publish.domain,
|
||||
progress: { $in: ['queued', 'inprogress'] }
|
||||
});
|
||||
if (alreadyQueued.length > 0) {
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
success: false,
|
||||
message: 'Deployment already queued.'
|
||||
}
|
||||
};
|
||||
}
|
||||
const { id, organization, name, branch } = configuration.repository;
|
||||
const { domain, path } = configuration.publish;
|
||||
const { deployId, nickname } = configuration.general;
|
||||
// Save new deployment
|
||||
await new Deployment({
|
||||
repoId: id,
|
||||
branch,
|
||||
deployId,
|
||||
domain,
|
||||
organization,
|
||||
name,
|
||||
nickname
|
||||
}).save();
|
||||
|
||||
await Configuration.findOneAndUpdate(
|
||||
{
|
||||
'publish.domain': domain,
|
||||
'publish.path': path,
|
||||
'general.pullRequest': { $in: [null, 0] }
|
||||
},
|
||||
{ ...configuration },
|
||||
{ upsert: true, new: true }
|
||||
);
|
||||
return
|
||||
}
|
@@ -1,26 +1,28 @@
|
||||
import Deployment from '$models/Deployment';
|
||||
import dayjs from 'dayjs';
|
||||
import buildContainer from './buildContainer';
|
||||
import { purgeImagesContainers } from './cleanup';
|
||||
import { updateServiceLabels } from './configuration';
|
||||
import copyFiles from './copyFiles';
|
||||
import deploy from './deploy';
|
||||
import { saveAppLog } from './logging';
|
||||
|
||||
export default async function (configuration, imageChanged) {
|
||||
export default async function (configuration, nextStep) {
|
||||
const { id, organization, name, branch } = configuration.repository;
|
||||
const { domain } = configuration.publish;
|
||||
const { deployId } = configuration.general;
|
||||
try {
|
||||
await saveAppLog(`${dayjs().format('YYYY-MM-DD HH:mm:ss.SSS')} Queued.`, configuration);
|
||||
await saveAppLog(`### Successfully queued.`, configuration);
|
||||
await copyFiles(configuration);
|
||||
await buildContainer(configuration);
|
||||
await deploy(configuration, imageChanged);
|
||||
await deploy(configuration, nextStep);
|
||||
await Deployment.findOneAndUpdate(
|
||||
{ repoId: id, branch, deployId, organization, name, domain },
|
||||
{ repoId: id, branch, deployId, organization, name, domain, progress: 'done' }
|
||||
);
|
||||
|
||||
await updateServiceLabels(configuration);
|
||||
await purgeImagesContainers(configuration);
|
||||
} catch (error) {
|
||||
await Deployment.findOneAndUpdate(
|
||||
{ repoId: id, branch, deployId, organization, name, domain },
|
||||
|
@@ -46,3 +46,27 @@ export function delay(t) {
|
||||
}, t);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
export function 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;
|
||||
};
|
Reference in New Issue
Block a user