v1.0.12 - Sveltekit migration (#44)

Changed the whole tech stack to SvelteKit which means:
- Typescript 
- SSR
- No fastify :(
- Beta, but it's fine!

Other changes:
- Tailwind -> Tailwind JIT
- A lot more
This commit is contained in:
Andras Bacsai
2021-05-14 21:51:14 +02:00
committed by GitHub
parent cccb9a5fec
commit 23a4ebb74a
229 changed files with 7781 additions and 11333 deletions

View File

@@ -0,0 +1,29 @@
import Deployment from '$models/Logs/Deployment';
import { saveAppLog } from './logging';
import * as packs from './packs';
export default async function (configuration) {
const { id, organization, name, branch } = configuration.repository;
const { domain } = configuration.publish;
const deployId = configuration.general.deployId;
const execute = packs[configuration.build.pack];
if (execute) {
await Deployment.findOneAndUpdate(
{ repoId: id, branch, deployId, organization, name, domain },
{ repoId: id, branch, deployId, organization, name, domain, progress: 'inprogress' }
);
await saveAppLog('### Building application.', configuration);
await execute(configuration);
await saveAppLog('### Building done.', configuration);
} else {
try {
await Deployment.findOneAndUpdate(
{ repoId: id, branch, deployId, organization, name, domain },
{ repoId: id, branch, deployId, organization, name, domain, progress: 'failed' }
);
} catch (error) {
// Hmm.
}
throw new Error('No buildpack found.');
}
}

View File

@@ -0,0 +1,45 @@
import { docker } from '$lib/api/docker';
import { execShellAsync } from '../common';
export async function deleteSameDeployments(configuration) {
await (
await docker.engine.listServices()
)
.filter((r) => r.Spec.Labels.managedBy === 'coolify' && r.Spec.Labels.type === 'application')
.map(async (s) => {
const running = JSON.parse(s.Spec.Labels.configuration);
if (
running.repository.id === configuration.repository.id &&
running.repository.branch === configuration.repository.branch
) {
await execShellAsync(`docker stack rm ${s.Spec.Labels['com.docker.stack.namespace']}`);
}
});
}
export async function purgeImagesContainers(configuration, deleteAll = false) {
const { name, tag } = configuration.build.container;
await execShellAsync('docker container prune -f');
if (deleteAll) {
const IDsToDelete = (
await execShellAsync(`docker images ls --filter=reference='${name}' --format '{{json .ID }}'`)
)
.trim()
.replace(/"/g, '')
.split('\n');
if (IDsToDelete.length > 0)
await execShellAsync(`docker rmi -f ${IDsToDelete.toString().replace(',', ' ')}`);
} else {
const IDsToDelete = (
await execShellAsync(
`docker images ls --filter=reference='${name}' --filter=before='${name}:${tag}' --format '{{json .ID }}'`
)
)
.trim()
.replace(/"/g, '')
.split('\n');
if (IDsToDelete.length > 1)
await execShellAsync(`docker rmi -f ${IDsToDelete.toString().replace(',', ' ')}`);
}
await execShellAsync('docker image prune -f');
}

View File

@@ -0,0 +1,48 @@
import jsonwebtoken from 'jsonwebtoken';
import { execShellAsync } from '../common';
export default async function (configuration) {
try {
const { GITHUB_APP_PRIVATE_KEY } = process.env;
const { workdir } = configuration.general;
const { organization, name, branch } = configuration.repository;
const github = configuration.github;
if (!github.installation.id || !github.app.id) {
throw new Error('Github installation ID is invalid.');
}
const githubPrivateKey = GITHUB_APP_PRIVATE_KEY.replace(/\\n/g, '\n').replace(/"/g, '');
const payload = {
iat: Math.round(new Date().getTime() / 1000),
exp: Math.round(new Date().getTime() / 1000 + 60),
iss: parseInt(github.app.id)
};
const jwtToken = jsonwebtoken.sign(payload, githubPrivateKey, {
algorithm: 'RS256'
});
const { token } = await (
await fetch(
`https://api.github.com/app/installations/${github.installation.id}/access_tokens`,
{
method: 'POST',
headers: {
Authorization: 'Bearer ' + jwtToken,
Accept: 'application/vnd.github.machine-man-preview+json'
}
}
)
).json();
await execShellAsync(
`mkdir -p ${workdir} && git clone -q -b ${branch} https://x-access-token:${token}@github.com/${organization}/${name}.git ${workdir}/`
);
configuration.build.container.tag = (
await execShellAsync(`cd ${configuration.general.workdir}/ && git rev-parse HEAD`)
)
.replace('\n', '')
.slice(0, 7);
} catch (error) {
console.log(error);
}
}

View File

@@ -0,0 +1,18 @@
export const baseServiceConfiguration = {
replicas: 1,
restart_policy: {
condition: 'any',
max_attempts: 6
},
update_config: {
parallelism: 1,
delay: '10s',
order: 'start-first'
},
rollback_config: {
parallelism: 1,
delay: '10s',
order: 'start-first',
failure_action: 'rollback'
}
};

View File

@@ -0,0 +1,156 @@
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 = 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.publish.path) configuration.publish.path = '/';
if (!configuration.publish.port) {
if (
configuration.build.pack === 'nodejs' ||
configuration.build.pack === 'vuejs' ||
configuration.build.pack === 'nuxtjs' ||
configuration.build.pack === 'rust' ||
configuration.build.pack === 'nextjs'
) {
configuration.publish.port = 3000;
} 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';
}
configuration.build.container.baseSHA = crypto
.createHash('sha256')
.update(JSON.stringify(baseServiceConfiguration))
.digest('hex');
configuration.baseServiceConfiguration = baseServiceConfiguration;
return configuration;
}
export async function precheckDeployment({ services, configuration }) {
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
) {
// 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;
foundService = 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 (
JSON.stringify(runningWithoutContainer.build) !==
JSON.stringify(configurationWithoutContainer.build) ||
JSON.stringify(runningWithoutContainer.publish) !==
JSON.stringify(configurationWithoutContainer.publish)
)
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 (forceUpdate) {
imageChanged = false;
configChanged = false;
}
return {
foundService,
imageChanged,
configChanged,
forceUpdate
};
}
export async function updateServiceLabels(configuration) {
// In case of any failure during deployment, still update the current configuration.
const services = (await docker.engine.listServices()).filter(
(r) => r.Spec.Labels.managedBy === 'coolify' && r.Spec.Labels.type === 'application'
);
const found = services.find((s) => {
const config = JSON.parse(s.Spec.Labels.configuration);
if (
config.repository.id === configuration.repository.id &&
config.repository.branch === configuration.repository.branch
) {
return config;
}
return null;
});
if (found) {
const { ID } = found;
const Labels = { ...JSON.parse(found.Spec.Labels.configuration), ...configuration };
await execShellAsync(
`docker service update --label-add configuration='${JSON.stringify(
Labels
)}' --label-add com.docker.stack.image='${configuration.build.container.name}:${
configuration.build.container.tag
}' ${ID}`
);
}
}

View File

@@ -0,0 +1,69 @@
import { promises as fs } from 'fs';
export default async function (configuration) {
const staticDeployments = ['react', 'vuejs', 'static', 'svelte', 'gatsby'];
try {
// TODO: Write full .dockerignore for all deployments!!
if (configuration.build.pack === 'php') {
await fs.writeFile(
`${configuration.general.workdir}/.htaccess`,
`
RewriteEngine On
RewriteBase /
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.+)$ index.php [QSA,L]
`
);
}
// await fs.writeFile(`${configuration.general.workdir}/.dockerignore`, 'node_modules')
if (staticDeployments.includes(configuration.build.pack)) {
await fs.writeFile(
`${configuration.general.workdir}/nginx.conf`,
`user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
access_log off;
sendfile on;
#tcp_nopush on;
keepalive_timeout 65;
server {
listen 80;
server_name localhost;
location / {
root /usr/share/nginx/html;
index index.html;
try_files $uri $uri/index.html $uri/ /index.html =404;
}
error_page 404 /50x.html;
# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}
}
`
);
}
} catch (error) {
console.log(error);
throw new Error(error);
}
}

View File

@@ -0,0 +1,76 @@
import { docker } from '$lib/api/docker';
import { saveAppLog } from './logging';
import { promises as fs } from 'fs';
import { deleteSameDeployments } from './cleanup';
import yaml from 'js-yaml';
import { execShellAsync } from '../common';
export default async function (configuration, imageChanged) {
const generateEnvs = {};
for (const secret of configuration.publish.secrets) {
generateEnvs[secret.name] = secret.value;
}
const containerName = configuration.build.container.name;
// Only save SHA256 of it in the configuration label
const baseServiceConfiguration = configuration.baseServiceConfiguration;
delete configuration.baseServiceConfiguration;
const stack = {
version: '3.8',
services: {
[containerName]: {
image: `${configuration.build.container.name}:${configuration.build.container.tag}`,
networks: [`${docker.network}`],
environment: generateEnvs,
deploy: {
...baseServiceConfiguration,
labels: [
'managedBy=coolify',
'type=application',
'configuration=' + JSON.stringify(configuration),
'traefik.enable=true',
'traefik.http.services.' +
configuration.build.container.name +
`.loadbalancer.server.port=${configuration.publish.port}`,
'traefik.http.routers.' + configuration.build.container.name + '.entrypoints=websecure',
'traefik.http.routers.' +
configuration.build.container.name +
'.rule=Host(`' +
configuration.publish.domain +
'`) && PathPrefix(`' +
configuration.publish.path +
'`)',
'traefik.http.routers.' +
configuration.build.container.name +
'.tls.certresolver=letsencrypt',
'traefik.http.routers.' +
configuration.build.container.name +
'.middlewares=global-compress'
]
}
}
},
networks: {
[`${docker.network}`]: {
external: true
}
}
};
await saveAppLog('### Publishing.', configuration);
await fs.writeFile(`${configuration.general.workdir}/stack.yml`, yaml.dump(stack));
if (imageChanged) {
// console.log('image changed')
await execShellAsync(
`docker service update --image ${configuration.build.container.name}:${configuration.build.container.tag} ${configuration.build.container.name}_${configuration.build.container.name}`
);
} else {
// console.log('new deployment or force deployment or config changed')
await deleteSameDeployments(configuration);
await execShellAsync(
`cat ${configuration.general.workdir}/stack.yml | docker stack deploy --prune -c - ${containerName}`
);
}
await saveAppLog('### Published done!', configuration);
}

View File

@@ -0,0 +1,58 @@
import Settings from '$models/Settings';
import ServerLog from '$models/Logs/Server';
import ApplicationLog from '$models/Logs/Application';
import dayjs from 'dayjs';
import { version } from '../../../../package.json';
function generateTimestamp() {
return `${dayjs().format('YYYY-MM-DD HH:mm:ss.SSS')} `;
}
const patterns = [
'[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)',
'(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~]))'
].join('|');
export async function saveAppLog(event, configuration, isError?: boolean) {
try {
const deployId = configuration.general.deployId;
const repoId = configuration.repository.id;
const branch = configuration.repository.branch;
if (isError) {
const clearedEvent =
'[ERROR 😱] ' +
generateTimestamp() +
event.replace(new RegExp(patterns, 'g'), '').replace(/(\r\n|\n|\r)/gm, '');
await new ApplicationLog({ repoId, branch, deployId, event: clearedEvent }).save();
} else {
if (event && event !== '\n') {
const clearedEvent =
'[INFO] ' +
generateTimestamp() +
event.replace(new RegExp(patterns, 'g'), '').replace(/(\r\n|\n|\r)/gm, '');
await new ApplicationLog({ repoId, branch, deployId, event: clearedEvent }).save();
}
}
} catch (error) {
console.log(error);
return error;
}
}
export async function saveServerLog(error) {
const settings = await Settings.findOne({ applicationName: 'coolify' });
const payload = {
message: error.message,
stack: error.stack,
type: error.type || 'spaghetticode',
version
};
const found = await ServerLog.find(payload);
if (found.length === 0 && error.message) await new ServerLog(payload).save();
if (settings && settings.sendErrors && process.env.NODE_ENV === 'production') {
await fetch('https://errors.coollabs.io/api/error', {
method: 'POST',
body: JSON.stringify({ ...payload })
});
}
}

View File

@@ -0,0 +1,17 @@
import { docker, streamEvents } from '$lib/api/docker';
import { promises as fs } from 'fs';
export default async function (configuration) {
const path = `${configuration.general.workdir}/${
configuration.build.directory ? configuration.build.directory : ''
}`;
if (fs.stat(`${path}/Dockerfile`)) {
const stream = await docker.engine.buildImage(
{ src: ['.'], context: path },
{ t: `${configuration.build.container.name}:${configuration.build.container.tag}` }
);
await streamEvents(stream, configuration);
} else {
throw new Error('No custom dockerfile found.');
}
}

View File

@@ -0,0 +1,28 @@
import { docker, streamEvents } from '$lib/api/docker';
import { promises as fs } from 'fs';
import { buildImage } from '../helpers';
// 'HEALTHCHECK --timeout=10s --start-period=10s --interval=5s CMD curl -I -s -f http://localhost/ || exit 1',
const publishStaticDocker = (configuration) => {
return [
'FROM nginx:stable-alpine',
'COPY nginx.conf /etc/nginx/nginx.conf',
'WORKDIR /usr/share/nginx/html',
`COPY --from=${configuration.build.container.name}:${configuration.build.container.tag}-cache /usr/src/app/${configuration.publish.directory} ./`,
'EXPOSE 80',
'CMD ["nginx", "-g", "daemon off;"]'
].join('\n');
};
export default async function (configuration) {
await buildImage(configuration, true);
await fs.writeFile(
`${configuration.general.workdir}/Dockerfile`,
publishStaticDocker(configuration)
);
const stream = await docker.engine.buildImage(
{ src: ['.'], context: configuration.general.workdir },
{ t: `${configuration.build.container.name}:${configuration.build.container.tag}` }
);
await streamEvents(stream, configuration);
}

View File

@@ -0,0 +1,30 @@
import { docker, streamEvents } from '$lib/api/docker';
import { promises as fs } from 'fs';
const buildImageNodeDocker = (configuration) => {
return [
'FROM node:lts',
'WORKDIR /usr/src/app',
`COPY ${configuration.build.directory}/package*.json ./`,
configuration.build.command.installation && `RUN ${configuration.build.command.installation}`,
`COPY ./${configuration.build.directory} ./`,
`RUN ${configuration.build.command.build}`
].join('\n');
};
export async function buildImage(configuration, cacheBuild?: boolean) {
await fs.writeFile(
`${configuration.general.workdir}/Dockerfile`,
buildImageNodeDocker(configuration)
);
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
}`
}
);
await streamEvents(stream, configuration);
}

View File

@@ -0,0 +1,25 @@
import vuejs from './vuejs';
import svelte from './svelte';
import Static from './static';
import rust from './rust';
import react from './react';
import php from './php';
import nuxtjs from './nuxtjs';
import nodejs from './nodejs';
import nextjs from './nextjs';
import gatsby from './gatsby';
import docker from './docker';
export {
vuejs,
svelte,
Static as static,
rust,
react,
php,
nuxtjs,
nodejs,
nextjs,
gatsby,
docker
};

View File

@@ -0,0 +1,30 @@
import { docker, streamEvents } from '$lib/api/docker';
import { promises as fs } from 'fs';
import { buildImage } from '../helpers';
// `HEALTHCHECK --timeout=10s --start-period=10s --interval=5s CMD curl -I -s -f http://localhost:${configuration.publish.port}${configuration.publish.path} || exit 1`,
const publishNodejsDocker = (configuration) => {
return [
'FROM node:lts',
'WORKDIR /usr/src/app',
configuration.build.command.build
? `COPY --from=${configuration.build.container.name}:${configuration.build.container.tag} /usr/src/app/${configuration.publish.directory} ./`
: `
COPY ${configuration.build.directory}/package*.json ./
RUN ${configuration.build.command.installation}
COPY ./${configuration.build.directory} ./`,
`EXPOSE ${configuration.publish.port}`,
'CMD [ "yarn", "start" ]'
].join('\n');
};
export default async function (configuration) {
await buildImage(configuration);
await fs.writeFile(
`${configuration.general.workdir}/Dockerfile`,
publishNodejsDocker(configuration)
);
const stream = await docker.engine.buildImage(
{ src: ['.'], context: configuration.general.workdir },
{ t: `${configuration.build.container.name}:${configuration.build.container.tag}` }
);
await streamEvents(stream, configuration);
}

View File

@@ -0,0 +1,31 @@
import { docker, streamEvents } from '$lib/api/docker';
import { promises as fs } from 'fs';
import { buildImage } from '../helpers';
// `HEALTHCHECK --timeout=10s --start-period=10s --interval=5s CMD curl -I -s -f http://localhost:${configuration.publish.port}${configuration.publish.path} || exit 1`,
const publishNodejsDocker = (configuration) => {
return [
'FROM node:lts',
'WORKDIR /usr/src/app',
configuration.build.command.build
? `COPY --from=${configuration.build.container.name}:${configuration.build.container.tag} /usr/src/app/${configuration.publish.directory} ./`
: `
COPY ${configuration.build.directory}/package*.json ./
RUN ${configuration.build.command.installation}
COPY ./${configuration.build.directory} ./`,
`EXPOSE ${configuration.publish.port}`,
'CMD [ "yarn", "start" ]'
].join('\n');
};
export default async function (configuration) {
if (configuration.build.command.build) await buildImage(configuration);
await fs.writeFile(
`${configuration.general.workdir}/Dockerfile`,
publishNodejsDocker(configuration)
);
const stream = await docker.engine.buildImage(
{ src: ['.'], context: configuration.general.workdir },
{ t: `${configuration.build.container.name}:${configuration.build.container.tag}` }
);
await streamEvents(stream, configuration);
}

View File

@@ -0,0 +1,31 @@
import { docker, streamEvents } from '$lib/api/docker';
import { promises as fs } from 'fs';
import { buildImage } from '../helpers';
// `HEALTHCHECK --timeout=10s --start-period=10s --interval=5s CMD curl -I -s -f http://localhost:${configuration.publish.port}${configuration.publish.path} || exit 1`,
const publishNodejsDocker = (configuration) => {
return [
'FROM node:lts',
'WORKDIR /usr/src/app',
configuration.build.command.build
? `COPY --from=${configuration.build.container.name}:${configuration.build.container.tag} /usr/src/app/${configuration.publish.directory} ./`
: `
COPY ${configuration.build.directory}/package*.json ./
RUN ${configuration.build.command.installation}
COPY ./${configuration.build.directory} ./`,
`EXPOSE ${configuration.publish.port}`,
'CMD [ "yarn", "start" ]'
].join('\n');
};
export default async function (configuration) {
await buildImage(configuration);
await fs.writeFile(
`${configuration.general.workdir}/Dockerfile`,
publishNodejsDocker(configuration)
);
const stream = await docker.engine.buildImage(
{ src: ['.'], context: configuration.general.workdir },
{ t: `${configuration.build.container.name}:${configuration.build.container.tag}` }
);
await streamEvents(stream, configuration);
}

View File

@@ -0,0 +1,25 @@
import { docker, streamEvents } from '$lib/api/docker';
import { promises as fs } from 'fs';
// 'HEALTHCHECK --timeout=10s --start-period=10s --interval=5s CMD curl -I -s -f http://localhost/ || exit 1',
const publishPHPDocker = (configuration) => {
return [
'FROM php:apache',
'RUN a2enmod rewrite',
'WORKDIR /usr/src/app',
`COPY ./${configuration.build.directory} /var/www/html`,
'EXPOSE 80',
' CMD ["apache2-foreground"]'
].join('\n');
};
export default async function (configuration) {
await fs.writeFile(
`${configuration.general.workdir}/Dockerfile`,
publishPHPDocker(configuration)
);
const stream = await docker.engine.buildImage(
{ src: ['.'], context: configuration.general.workdir },
{ t: `${configuration.build.container.name}:${configuration.build.container.tag}` }
);
await streamEvents(stream, configuration);
}

View File

@@ -0,0 +1,28 @@
import { docker, streamEvents } from '$lib/api/docker';
import { promises as fs } from 'fs';
import { buildImage } from '../helpers';
// 'HEALTHCHECK --timeout=10s --start-period=10s --interval=5s CMD curl -I -s -f http://localhost/ || exit 1',
const publishStaticDocker = (configuration) => {
return [
'FROM nginx:stable-alpine',
'COPY nginx.conf /etc/nginx/nginx.conf',
'WORKDIR /usr/share/nginx/html',
`COPY --from=${configuration.build.container.name}:${configuration.build.container.tag}-cache /usr/src/app/${configuration.publish.directory} ./`,
'EXPOSE 80',
'CMD ["nginx", "-g", "daemon off;"]'
].join('\n');
};
export default async function (configuration) {
await buildImage(configuration, true);
await fs.writeFile(
`${configuration.general.workdir}/Dockerfile`,
publishStaticDocker(configuration)
);
const stream = await docker.engine.buildImage(
{ src: ['.'], context: configuration.general.workdir },
{ t: `${configuration.build.container.name}:${configuration.build.container.tag}` }
);
await streamEvents(stream, configuration);
}

View File

@@ -0,0 +1,66 @@
import { docker, streamEvents } from '$lib/api/docker';
import { promises as fs } from 'fs';
import TOML from '@iarna/toml';
import { execShellAsync } from '$lib/api/common';
const publishRustDocker = (configuration, custom) => {
return [
'FROM rust:latest',
'WORKDIR /app',
`COPY --from=${configuration.build.container.name}:cache /app/target target`,
`COPY --from=${configuration.build.container.name}:cache /usr/local/cargo /usr/local/cargo`,
'COPY . .',
`RUN cargo build --release --bin ${custom.name}`,
'FROM debian:buster-slim',
'WORKDIR /app',
'RUN apt-get update -y && apt-get install -y --no-install-recommends openssl libcurl4 ca-certificates && apt-get autoremove -y && apt-get clean -y && rm -rf /var/lib/apt/lists/*',
'RUN update-ca-certificates',
`COPY --from=${configuration.build.container.name}:cache /app/target/release/${custom.name} ${custom.name}`,
`EXPOSE ${configuration.publish.port}`,
`CMD ["/app/${custom.name}"]`
].join('\n');
};
const cacheRustDocker = (configuration, custom) => {
return [
`FROM rust:latest AS planner-${configuration.build.container.name}`,
'WORKDIR /app',
'RUN cargo install cargo-chef',
'COPY . .',
'RUN cargo chef prepare --recipe-path recipe.json',
'FROM rust:latest',
'WORKDIR /app',
'RUN cargo install cargo-chef',
`COPY --from=planner-${configuration.build.container.name} /app/recipe.json recipe.json`,
'RUN cargo chef cook --release --recipe-path recipe.json'
].join('\n');
};
export default async function (configuration) {
const cargoToml = await execShellAsync(`cat ${configuration.general.workdir}/Cargo.toml`);
const parsedToml = TOML.parse(cargoToml);
const custom = {
name: parsedToml.package.name
};
await fs.writeFile(
`${configuration.general.workdir}/Dockerfile`,
cacheRustDocker(configuration, custom)
);
let stream = await docker.engine.buildImage(
{ src: ['.'], context: configuration.general.workdir },
{ t: `${configuration.build.container.name}:cache` }
);
await streamEvents(stream, configuration);
await fs.writeFile(
`${configuration.general.workdir}/Dockerfile`,
publishRustDocker(configuration, custom)
);
stream = await docker.engine.buildImage(
{ src: ['.'], context: configuration.general.workdir },
{ t: `${configuration.build.container.name}:${configuration.build.container.tag}` }
);
await streamEvents(stream, configuration);
}

View File

@@ -0,0 +1,30 @@
import { docker, streamEvents } from '$lib/api/docker';
import { promises as fs } from 'fs';
import { buildImage } from '../helpers';
// 'HEALTHCHECK --timeout=10s --start-period=10s --interval=5s CMD curl -I -s -f http://localhost/ || exit 1',
const publishStaticDocker = (configuration) => {
return [
'FROM nginx:stable-alpine',
'COPY nginx.conf /etc/nginx/nginx.conf',
'WORKDIR /usr/share/nginx/html',
configuration.build.command.build
? `COPY --from=${configuration.build.container.name}:${configuration.build.container.tag}-cache /usr/src/app/${configuration.publish.directory} ./`
: `COPY ./${configuration.build.directory} ./`,
'EXPOSE 80',
'CMD ["nginx", "-g", "daemon off;"]'
].join('\n');
};
export default async function (configuration) {
if (configuration.build.command.build) await buildImage(configuration, true);
await fs.writeFile(
`${configuration.general.workdir}/Dockerfile`,
publishStaticDocker(configuration)
);
const stream = await docker.engine.buildImage(
{ src: ['.'], context: configuration.general.workdir },
{ t: `${configuration.build.container.name}:${configuration.build.container.tag}` }
);
await streamEvents(stream, configuration);
}

View File

@@ -0,0 +1,28 @@
import { docker, streamEvents } from '$lib/api/docker';
import { promises as fs } from 'fs';
import { buildImage } from '../helpers';
// 'HEALTHCHECK --timeout=10s --start-period=10s --interval=5s CMD curl -I -s -f http://localhost/ || exit 1',
const publishStaticDocker = (configuration) => {
return [
'FROM nginx:stable-alpine',
'COPY nginx.conf /etc/nginx/nginx.conf',
'WORKDIR /usr/share/nginx/html',
`COPY --from=${configuration.build.container.name}:${configuration.build.container.tag}-cache /usr/src/app/${configuration.publish.directory} ./`,
'EXPOSE 80',
'CMD ["nginx", "-g", "daemon off;"]'
].join('\n');
};
export default async function (configuration) {
await buildImage(configuration, true);
await fs.writeFile(
`${configuration.general.workdir}/Dockerfile`,
publishStaticDocker(configuration)
);
const stream = await docker.engine.buildImage(
{ src: ['.'], context: configuration.general.workdir },
{ t: `${configuration.build.container.name}:${configuration.build.container.tag}` }
);
await streamEvents(stream, configuration);
}

View File

@@ -0,0 +1,28 @@
import { docker, streamEvents } from '$lib/api/docker';
import { promises as fs } from 'fs';
import { buildImage } from '../helpers';
// 'HEALTHCHECK --timeout=10s --start-period=10s --interval=5s CMD curl -I -s -f http://localhost/ || exit 1',
const publishStaticDocker = (configuration) => {
return [
'FROM nginx:stable-alpine',
'COPY nginx.conf /etc/nginx/nginx.conf',
'WORKDIR /usr/share/nginx/html',
`COPY --from=${configuration.build.container.name}:${configuration.build.container.tag}-cache /usr/src/app/${configuration.publish.directory} ./`,
'EXPOSE 80',
'CMD ["nginx", "-g", "daemon off;"]'
].join('\n');
};
export default async function (configuration) {
await buildImage(configuration, true);
await fs.writeFile(
`${configuration.general.workdir}/Dockerfile`,
publishStaticDocker(configuration)
);
const stream = await docker.engine.buildImage(
{ src: ['.'], context: configuration.general.workdir },
{ t: `${configuration.build.container.name}:${configuration.build.container.tag}` }
);
await streamEvents(stream, configuration);
}

View File

@@ -0,0 +1,31 @@
import Deployment from '$models/Logs/Deployment';
import dayjs from 'dayjs';
import buildContainer from './buildContainer';
import { updateServiceLabels } from './configuration';
import copyFiles from './copyFiles';
import deploy from './deploy';
import { saveAppLog } from './logging';
export default async function (configuration, imageChanged) {
const { id, organization, name, branch } = configuration.repository;
const { domain } = configuration.publish;
const { deployId, nickname } = configuration.general;
await new Deployment({
repoId: id,
branch,
deployId,
domain,
organization,
name,
nickname
}).save();
await saveAppLog(`${dayjs().format('YYYY-MM-DD HH:mm:ss.SSS')} Queued.`, configuration);
await copyFiles(configuration);
await buildContainer(configuration);
await deploy(configuration, imageChanged);
await Deployment.findOneAndUpdate(
{ repoId: id, branch, deployId, organization, name, domain },
{ repoId: id, branch, deployId, organization, name, domain, progress: 'done' }
);
await updateServiceLabels(configuration);
}

View File

@@ -0,0 +1,57 @@
const defaultBuildAndDeploy = {
installation: 'yarn install',
build: 'yarn build'
};
const templates = {
svelte: {
pack: 'svelte',
...defaultBuildAndDeploy,
directory: 'public',
name: 'Svelte'
},
next: {
pack: 'nextjs',
...defaultBuildAndDeploy,
port: 3000,
name: 'NextJS'
},
nuxt: {
pack: 'nuxtjs',
...defaultBuildAndDeploy,
port: 3000,
name: 'NuxtJS'
},
'react-scripts': {
pack: 'react',
...defaultBuildAndDeploy,
directory: 'build',
name: 'React'
},
'parcel-bundler': {
pack: 'static',
...defaultBuildAndDeploy,
directory: 'dist',
name: 'Parcel'
},
'@vue/cli-service': {
pack: 'vuejs',
...defaultBuildAndDeploy,
directory: 'dist',
name: 'Vue'
},
gatsby: {
pack: 'gatsby',
...defaultBuildAndDeploy,
directory: 'public',
name: 'Gatsby'
},
'preact-cli': {
pack: 'react',
...defaultBuildAndDeploy,
directory: 'build',
name: 'Preact'
}
};
export default templates;

44
src/lib/api/common.ts Normal file
View File

@@ -0,0 +1,44 @@
import shell from 'shelljs';
import User from '$models/User';
import jsonwebtoken from 'jsonwebtoken';
export function execShellAsync(cmd, opts = {}) {
try {
return new Promise(function (resolve, reject) {
shell.config.silent = true;
shell.exec(cmd, opts, function (code, stdout, stderr) {
if (code !== 0) return reject(new Error(stderr));
return resolve(stdout);
});
});
} catch (error) {
return new Error('Oops');
}
}
export function cleanupTmp(dir) {
if (dir !== '/') shell.rm('-fr', dir);
}
export async function verifyUserId(token) {
const { JWT_SIGN_KEY } = process.env;
try {
const verify = jsonwebtoken.verify(token, JWT_SIGN_KEY);
const found = await User.findOne({ uid: verify.jti });
if (found) {
return Promise.resolve(true);
} else {
return Promise.reject(false);
}
} catch (error) {
console.log(error);
return Promise.reject(false);
}
}
export function delay(t) {
return new Promise(function (resolve) {
setTimeout(function () {
resolve('OK');
}, t);
});
}

27
src/lib/api/docker.ts Normal file
View File

@@ -0,0 +1,27 @@
import Dockerode from 'dockerode';
import { saveAppLog } from './applications/logging';
const { DOCKER_ENGINE, DOCKER_NETWORK } = process.env;
export const docker = {
engine: new Dockerode({
socketPath: DOCKER_ENGINE
}),
network: DOCKER_NETWORK
};
export async function streamEvents(stream, configuration) {
await new Promise((resolve, reject) => {
docker.engine.modem.followProgress(stream, onFinished, onProgress);
function onFinished(err, res) {
if (err) reject(err);
resolve(res);
}
function onProgress(event) {
if (event.error) {
saveAppLog(event.error, configuration, true);
reject(event.error);
} else if (event.stream) {
saveAppLog(event.stream, configuration);
}
}
});
}

102
src/lib/api/request.ts Normal file
View File

@@ -0,0 +1,102 @@
import { toast } from '@zerodevx/svelte-toast';
import { browser } from '$app/env';
export async function request(
url,
session,
{
method,
body,
customHeaders
}: {
url?: string;
session?: any;
fetch?: any;
method?: string;
body?: any;
customHeaders?: Record<string, unknown>;
} = {}
) {
let fetch;
if (browser) {
fetch = window.fetch;
} else {
fetch = session.fetch;
}
let headers = { 'content-type': 'application/json; charset=UTF-8' };
if (method === 'DELETE') {
delete headers['content-type'];
}
const isGithub = url.match(/api.github.com/);
if (isGithub) {
headers = Object.assign(headers, {
Authorization: `token ${session.ghToken}`
});
}
const config: any = {
method: method || (body ? 'POST' : 'GET'),
cache: isGithub ? 'no-cache' : 'default',
headers: {
...headers,
...customHeaders
}
};
if (body) {
config.body = JSON.stringify(body);
}
const response = await fetch(url, config);
if (response.status >= 200 && response.status <= 299) {
if (response.headers.get('content-type').match(/application\/json/)) {
const json = await response.json();
if (json?.success === false) {
browser && json.showToast !== false && toast.push(json.message);
return Promise.reject({
status: response.status,
error: json.message
});
}
return json;
} else if (response.headers.get('content-type').match(/text\/plain/)) {
return await response.text();
} else if (response.headers.get('content-type').match(/multipart\/form-data/)) {
return await response.formData();
} else {
console.log(response);
if (response.headers.get('content-disposition')) {
const blob = await response.blob();
console.log(blob);
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = response.headers.get('content-disposition').split('=')[1] || 'backup.gz';
link.target = '_blank';
link.setAttribute('type', 'hidden');
document.body.appendChild(link);
link.click();
link.remove();
return;
}
return await response.blob();
}
} else {
if (response.status === 401) {
browser && toast.push('Unauthorized');
return Promise.reject({
status: response.status,
error: 'Unauthorized'
});
} else if (response.status >= 500) {
const error = (await response.json()).error;
browser && toast.push(error);
return Promise.reject({
status: response.status,
error: error || 'Oops, something is not okay. Are you okay?'
});
} else {
browser && toast.push(response.statusText);
return Promise.reject({
status: response.status,
error: response.statusText
});
}
}
}