diff --git a/.dockerignore b/.dockerignore index d61686b7e0..907d0f818a 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,3 +1,4 @@ node_modules dist -.routify \ No newline at end of file +.routify +.pnpm-store \ No newline at end of file diff --git a/.gitignore b/.gitignore index cb3de10263..7b5ba795e7 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,5 @@ dist-ssr yarn-error.log api/development/console.log .pnpm-debug.log -yarn.lock \ No newline at end of file +yarn.lock +.pnpm-store \ No newline at end of file diff --git a/api/app.js b/api/app.js index 9e094ae887..eac63297c9 100644 --- a/api/app.js +++ b/api/app.js @@ -12,6 +12,7 @@ module.exports = async function (fastify, opts) { server.register(require('./routes/v1/application/deploy'), { prefix: '/application/deploy' }) server.register(require('./routes/v1/application/deploy/logs'), { prefix: '/application/deploy/logs' }) server.register(require('./routes/v1/databases'), { prefix: '/databases' }) + server.register(require('./routes/v1/server'), { prefix: '/server' }) }) // Public routes fastify.register(require('./routes/v1/verify'), { prefix: '/verify' }) diff --git a/api/buildPacks/custom/index.js b/api/buildPacks/custom/index.js new file mode 100644 index 0000000000..79524da2ed --- /dev/null +++ b/api/buildPacks/custom/index.js @@ -0,0 +1,19 @@ +const fs = require('fs').promises +const { streamEvents, docker } = require('../../libs/docker') + +module.exports = async function (configuration) { + try { + 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 { error: 'No custom dockerfile found.', type: 'app' } + } + } catch (error) { + throw { error, type: 'server' } + } +} diff --git a/api/packs/helpers.js b/api/buildPacks/helpers.js similarity index 53% rename from api/packs/helpers.js rename to api/buildPacks/helpers.js index b46ed64780..1d90af8573 100644 --- a/api/packs/helpers.js +++ b/api/buildPacks/helpers.js @@ -10,12 +10,16 @@ const buildImageNodeDocker = (configuration) => { ].join('\n') } async function buildImage (configuration) { - 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}:${configuration.build.container.tag}` } - ) - await streamEvents(stream, configuration) + try { + 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}:${configuration.build.container.tag}` } + ) + await streamEvents(stream, configuration) + } catch (error) { + throw { error, type: 'server' } + } } module.exports = { diff --git a/api/packs/index.js b/api/buildPacks/index.js similarity index 61% rename from api/packs/index.js rename to api/buildPacks/index.js index 23fb82e347..a4a8876e20 100644 --- a/api/packs/index.js +++ b/api/buildPacks/index.js @@ -2,5 +2,6 @@ const static = require('./static') const nodejs = require('./nodejs') const php = require('./php') const custom = require('./custom') +const rust = require('./rust') -module.exports = { static, nodejs, php, custom } +module.exports = { static, nodejs, php, custom, rust } diff --git a/api/packs/nodejs/index.js b/api/buildPacks/nodejs/index.js similarity index 52% rename from api/packs/nodejs/index.js rename to api/buildPacks/nodejs/index.js index 97a24e9710..7b199d3303 100644 --- a/api/packs/nodejs/index.js +++ b/api/buildPacks/nodejs/index.js @@ -1,7 +1,7 @@ const fs = require('fs').promises const { buildImage } = require('../helpers') const { streamEvents, docker } = require('../../libs/docker') - +// `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', @@ -16,11 +16,15 @@ const publishNodejsDocker = (configuration) => { } module.exports = 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) + try { + 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) + } catch (error) { + throw { error, type: 'server' } + } } diff --git a/api/buildPacks/php/index.js b/api/buildPacks/php/index.js new file mode 100644 index 0000000000..b96700ee2e --- /dev/null +++ b/api/buildPacks/php/index.js @@ -0,0 +1,26 @@ +const fs = require('fs').promises +const { streamEvents, docker } = require('../../libs/docker') +// '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') +} + +module.exports = async function (configuration) { + try { + 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) + } catch (error) { + throw { error, type: 'server' } + } +} diff --git a/api/buildPacks/rust/index.js b/api/buildPacks/rust/index.js new file mode 100644 index 0000000000..6cbed387ac --- /dev/null +++ b/api/buildPacks/rust/index.js @@ -0,0 +1,64 @@ +const fs = require('fs').promises +const { streamEvents, docker } = require('../../libs/docker') +const { execShellAsync } = require('../../libs/common') +const TOML = require('@iarna/toml') + +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') +} + +module.exports = async function (configuration) { + try { + 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) + } catch (error) { + throw { error, type: 'server' } + } +} diff --git a/api/packs/static/index.js b/api/buildPacks/static/index.js similarity index 52% rename from api/packs/static/index.js rename to api/buildPacks/static/index.js index a2dc497a67..e6c4d56104 100644 --- a/api/packs/static/index.js +++ b/api/buildPacks/static/index.js @@ -2,6 +2,7 @@ const fs = require('fs').promises const { buildImage } = require('../helpers') const { streamEvents, docker } = require('../../libs/docker') +// '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', @@ -16,12 +17,16 @@ const publishStaticDocker = (configuration) => { } module.exports = async function (configuration) { - if (configuration.build.command.build) await buildImage(configuration) - await fs.writeFile(`${configuration.general.workdir}/Dockerfile`, publishStaticDocker(configuration)) + try { + if (configuration.build.command.build) await buildImage(configuration) + 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) + const stream = await docker.engine.buildImage( + { src: ['.'], context: configuration.general.workdir }, + { t: `${configuration.build.container.name}:${configuration.build.container.tag}` } + ) + await streamEvents(stream, configuration) + } catch (error) { + throw { error, type: 'server' } + } } diff --git a/api/libs/applications/build/container.js b/api/libs/applications/build/container.js index 54b50f0bb8..a8209ec618 100644 --- a/api/libs/applications/build/container.js +++ b/api/libs/applications/build/container.js @@ -1,4 +1,4 @@ -const packs = require('../../../packs') +const packs = require('../../../buildPacks') const { saveAppLog } = require('../../logging') const Deployment = require('../../../models/Deployment') @@ -26,9 +26,14 @@ module.exports = async function (configuration) { throw { error, type: 'app' } } } else { - await Deployment.findOneAndUpdate( - { repoId: id, branch, deployId, organization, name, domain }, - { repoId: id, branch, deployId, organization, name, domain, progress: 'failed' }) + try { + await Deployment.findOneAndUpdate( + { repoId: id, branch, deployId, organization, name, domain }, + { repoId: id, branch, deployId, organization, name, domain, progress: 'failed' }) + } catch (error) { + // Hmm. + } + throw { error: 'No buildpack found.', type: 'app' } } } diff --git a/api/libs/applications/cleanup/index.js b/api/libs/applications/cleanup/index.js index 8987eeb5b1..c5c3b9f663 100644 --- a/api/libs/applications/cleanup/index.js +++ b/api/libs/applications/cleanup/index.js @@ -2,17 +2,16 @@ const { docker } = require('../../docker') const { execShellAsync } = require('../../common') const Deployment = require('../../../models/Deployment') -async function purgeOldThings () { +async function purgeImagesContainers () { try { - // TODO: Tweak this, because it deletes coolify-base, so the upgrade will be slow - await docker.engine.pruneImages() - await docker.engine.pruneContainers() + await execShellAsync('docker container prune -f') + await execShellAsync('docker image prune -f --filter=label!=coolify-reserve=true') } catch (error) { throw { error, type: 'server' } } } -async function cleanup (configuration) { +async function cleanupStuckedDeploymentsInDB (configuration) { const { id } = configuration.repository const deployId = configuration.general.deployId try { @@ -39,4 +38,4 @@ async function deleteSameDeployments (configuration) { } } -module.exports = { cleanup, deleteSameDeployments, purgeOldThings } +module.exports = { cleanupStuckedDeploymentsInDB, deleteSameDeployments, purgeImagesContainers } diff --git a/api/libs/applications/configuration.js b/api/libs/applications/configuration.js index 5ed8e73f39..d8fce5c277 100644 --- a/api/libs/applications/configuration.js +++ b/api/libs/applications/configuration.js @@ -1,7 +1,7 @@ const { uniqueNamesGenerator, adjectives, colors, animals } = require('unique-names-generator') const cuid = require('cuid') const crypto = require('crypto') - +const { docker } = require('../docker') const { execShellAsync } = require('../common') function getUniq () { @@ -30,7 +30,8 @@ function setDefaultConfiguration (configuration) { rollback_config: { parallelism: 1, delay: '10s', - order: 'start-first' + order: 'start-first', + failure_action: 'rollback' } } @@ -48,11 +49,18 @@ function setDefaultConfiguration (configuration) { configuration.publish.port = 80 } else if (configuration.build.pack === 'nodejs') { configuration.publish.port = 3000 + } else if (configuration.build.pack === 'rust') { + configuration.publish.port = 3000 } } + if (!configuration.build.directory) { configuration.build.directory = '/' } + if (!configuration.publish.directory) { + configuration.publish.directory = '/' + } + if (configuration.build.pack === 'static' || configuration.build.pack === 'nodejs') { if (!configuration.build.command.installation) configuration.build.command.installation = 'yarn install' } @@ -66,8 +74,9 @@ function setDefaultConfiguration (configuration) { } } -async function updateServiceLabels (configuration, services) { +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) { @@ -79,10 +88,58 @@ async function updateServiceLabels (configuration, services) { const { ID } = found try { const Labels = { ...JSON.parse(found.Spec.Labels.configuration), ...configuration } - 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}`) + 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}`) } catch (error) { console.log(error) } } } -module.exports = { setDefaultConfiguration, updateServiceLabels } + +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 + } +} +module.exports = { setDefaultConfiguration, updateServiceLabels, precheckDeployment } diff --git a/api/libs/applications/deploy/copyFiles.js b/api/libs/applications/deploy/copyFiles.js index b6e71eb326..bb6defeaa3 100644 --- a/api/libs/applications/deploy/copyFiles.js +++ b/api/libs/applications/deploy/copyFiles.js @@ -1,52 +1,63 @@ const fs = require('fs').promises module.exports = async function (configuration) { try { - // TODO: Do it better. - await fs.writeFile(`${configuration.general.workdir}/.dockerignore`, 'node_modules') - 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; - } - - } - - } - ` - ) + // 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 (configuration.build.pack === 'static') { + 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) { throw { error, type: 'server' } } diff --git a/api/libs/applications/deploy/deploy.js b/api/libs/applications/deploy/deploy.js index 1f9619a062..0528320867 100644 --- a/api/libs/applications/deploy/deploy.js +++ b/api/libs/applications/deploy/deploy.js @@ -5,7 +5,7 @@ const { docker } = require('../../docker') const { saveAppLog } = require('../../logging') const { deleteSameDeployments } = require('../cleanup') -module.exports = async function (configuration, configChanged, imageChanged) { +module.exports = async function (configuration, imageChanged) { try { const generateEnvs = {} for (const secret of configuration.publish.secrets) { @@ -62,7 +62,6 @@ module.exports = async function (configuration, configChanged, imageChanged) { } await saveAppLog('### Publishing.', configuration) await fs.writeFile(`${configuration.general.workdir}/stack.yml`, yaml.dump(stack)) - // TODO: Compare stack.yml with the currently running one to upgrade if something changes, like restart_policy 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}`) diff --git a/api/libs/applications/index.js b/api/libs/applications/index.js index 24f7e07f11..38c1b8498f 100644 --- a/api/libs/applications/index.js +++ b/api/libs/applications/index.js @@ -8,10 +8,10 @@ const copyFiles = require('./deploy/copyFiles') const buildContainer = require('./build/container') const deploy = require('./deploy/deploy') const Deployment = require('../../models/Deployment') -const { cleanup, purgeOldThings } = require('./cleanup') +const { cleanupStuckedDeploymentsInDB, purgeImagesContainers } = require('./cleanup') const { updateServiceLabels } = require('./configuration') -async function queueAndBuild (configuration, services, configChanged, imageChanged) { +async function queueAndBuild (configuration, imageChanged) { const { id, organization, name, branch } = configuration.repository const { domain } = configuration.publish const { deployId, nickname, workdir } = configuration.general @@ -22,15 +22,15 @@ async function queueAndBuild (configuration, services, configChanged, imageChang await saveAppLog(`${dayjs().format('YYYY-MM-DD HH:mm:ss.SSS')} Queued.`, configuration) await copyFiles(configuration) await buildContainer(configuration) - await deploy(configuration, configChanged, imageChanged) + 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, services) + await updateServiceLabels(configuration) cleanupTmp(workdir) - await purgeOldThings() + await purgeImagesContainers() } catch (error) { - await cleanup(configuration) + await cleanupStuckedDeploymentsInDB(configuration) cleanupTmp(workdir) const { type } = error.error if (type === 'app') { diff --git a/api/libs/common.js b/api/libs/common.js index b26416ef6d..6026af14b5 100644 --- a/api/libs/common.js +++ b/api/libs/common.js @@ -15,12 +15,16 @@ function delay (t) { } async function verifyUserId (authorization) { - const token = authorization.split(' ')[1] - const verify = jsonwebtoken.verify(token, process.env.JWT_SIGN_KEY) - const found = await User.findOne({ uid: verify.jti }) - if (found) { - return true - } else { + try { + const token = authorization.split(' ')[1] + const verify = jsonwebtoken.verify(token, process.env.JWT_SIGN_KEY) + const found = await User.findOne({ uid: verify.jti }) + if (found) { + return true + } else { + return false + } + } catch (error) { return false } } diff --git a/api/libs/logging.js b/api/libs/logging.js index 6af682b998..9d94780929 100644 --- a/api/libs/logging.js +++ b/api/libs/logging.js @@ -40,13 +40,17 @@ async function saveAppLog (event, configuration, isError) { } async function saveServerLog ({ event, configuration, type }) { - if (configuration) { - const deployId = configuration.general.deployId - const repoId = configuration.repository.id - const branch = configuration.repository.branch - await new ApplicationLog({ repoId, branch, deployId, event: `[SERVER ERROR 😖]: ${event}` }).save() + try { + if (configuration) { + const deployId = configuration.general.deployId + const repoId = configuration.repository.id + const branch = configuration.repository.branch + await new ApplicationLog({ repoId, branch, deployId, event: `[SERVER ERROR 😖]: ${event}` }).save() + } + await new ServerLog({ event, type }).save() + } catch (error) { + // Hmm. } - await new ServerLog({ event, type }).save() } module.exports = { diff --git a/api/packs/custom/index.js b/api/packs/custom/index.js deleted file mode 100644 index 7c6f60065e..0000000000 --- a/api/packs/custom/index.js +++ /dev/null @@ -1,15 +0,0 @@ -const fs = require('fs').promises -const { streamEvents, docker } = require('../../libs/docker') - -module.exports = 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 { error: 'No custom dockerfile found.', type: 'app' } - } -} diff --git a/api/packs/php/index.js b/api/packs/php/index.js deleted file mode 100644 index 0361b89f7d..0000000000 --- a/api/packs/php/index.js +++ /dev/null @@ -1,21 +0,0 @@ -const fs = require('fs').promises -const { streamEvents, docker } = require('../../libs/docker') - -const publishPHPDocker = (configuration) => { - return [ - 'FROM php:apache', - 'WORKDIR /usr/src/app', - `COPY .${configuration.build.directory} /var/www/html`, - 'EXPOSE 80', - ' CMD ["apache2-foreground"]' - ].join('\n') -} - -module.exports = 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) -} diff --git a/api/routes/v1/application/check.js b/api/routes/v1/application/check.js index 4cab18915e..f53a91df9d 100644 --- a/api/routes/v1/application/check.js +++ b/api/routes/v1/application/check.js @@ -5,31 +5,36 @@ const { docker } = require('../../../libs/docker') module.exports = async function (fastify) { fastify.post('/', async (request, reply) => { - if (!await verifyUserId(request.headers.authorization)) { - reply.code(500).send({ error: 'Invalid request' }) - return - } - const configuration = setDefaultConfiguration(request.body) + try { + if (!await verifyUserId(request.headers.authorization)) { + reply.code(500).send({ error: 'Invalid request' }) + return + } + const configuration = setDefaultConfiguration(request.body) - const services = (await docker.engine.listServices()).filter(r => r.Spec.Labels.managedBy === 'coolify' && r.Spec.Labels.type === 'application') - let foundDomain = false + const services = (await docker.engine.listServices()).filter(r => r.Spec.Labels.managedBy === 'coolify' && r.Spec.Labels.type === 'application') + let foundDomain = false - for (const service of services) { - const running = JSON.parse(service.Spec.Labels.configuration) - if (running) { - if ( - running.publish.domain === configuration.publish.domain && - running.repository.id !== configuration.repository.id - ) { - foundDomain = true + for (const service of services) { + const running = JSON.parse(service.Spec.Labels.configuration) + if (running) { + if ( + running.publish.domain === configuration.publish.domain && + running.repository.id !== configuration.repository.id && + running.publish.path === configuration.publish.path + ) { + foundDomain = true + } } } + if (fastify.config.DOMAIN === configuration.publish.domain) foundDomain = true + if (foundDomain) { + reply.code(500).send({ message: 'Domain already in use.' }) + return + } + return { message: 'OK' } + } catch (error) { + throw { error, type: 'server' } } - if (fastify.config.DOMAIN === configuration.publish.domain) foundDomain = true - if (foundDomain) { - reply.code(500).send({ message: 'Domain already in use.' }) - return - } - return { message: 'OK' } }) } diff --git a/api/routes/v1/application/deploy/index.js b/api/routes/v1/application/deploy/index.js index 1322f3d6c4..2204aeb670 100644 --- a/api/routes/v1/application/deploy/index.js +++ b/api/routes/v1/application/deploy/index.js @@ -1,8 +1,8 @@ -const { verifyUserId, cleanupTmp, execShellAsync } = require('../../../../libs/common') +const { verifyUserId, cleanupTmp } = require('../../../../libs/common') const Deployment = require('../../../../models/Deployment') const { queueAndBuild } = require('../../../../libs/applications') -const { setDefaultConfiguration } = require('../../../../libs/applications/configuration') +const { setDefaultConfiguration, precheckDeployment } = require('../../../../libs/applications/configuration') const { docker } = require('../../../../libs/docker') const cloneRepository = require('../../../../libs/applications/github/cloneRepository') @@ -32,90 +32,43 @@ module.exports = async function (fastify) { // }, // }; fastify.post('/', async (request, reply) => { - if (!await verifyUserId(request.headers.authorization)) { + try { + await verifyUserId(request.headers.authorization) + } catch (error) { reply.code(500).send({ error: 'Invalid request' }) return } + try { + const services = (await docker.engine.listServices()).filter(r => r.Spec.Labels.managedBy === 'coolify' && r.Spec.Labels.type === 'application') + const configuration = setDefaultConfiguration(request.body) + await cloneRepository(configuration) + const { foundService, imageChanged, configChanged, forceUpdate } = await precheckDeployment({ services, configuration }) - const configuration = setDefaultConfiguration(request.body) - - const services = (await docker.engine.listServices()).filter(r => r.Spec.Labels.managedBy === 'coolify' && r.Spec.Labels.type === 'application') - - await cloneRepository(configuration) - - let foundService = false - let foundDomain = 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.publish.domain === configuration.publish.domain && - running.repository.id !== configuration.repository.id - ) { - foundDomain = true - } - 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) { - configChanged = true - } - 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') - 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 (foundDomain) { - cleanupTmp(configuration.general.workdir) - reply.code(500).send({ message: 'Domain already in use.' }) - return - } - if (forceUpdate) { - imageChanged = false - configChanged = false - } else { - if (foundService && !imageChanged && !configChanged) { + if (foundService && !forceUpdate && !imageChanged && !configChanged) { cleanupTmp(configuration.general.workdir) reply.code(500).send({ message: 'Nothing changed, no need to redeploy.' }) return } - } - const alreadyQueued = await Deployment.find({ - repoId: configuration.repository.id, - branch: configuration.repository.branch, - organization: configuration.repository.organization, - name: configuration.repository.name, - domain: configuration.publish.domain, - progress: { $in: ['queued', 'inprogress'] } - }) + const alreadyQueued = await Deployment.find({ + repoId: configuration.repository.id, + branch: configuration.repository.branch, + organization: configuration.repository.organization, + name: configuration.repository.name, + domain: configuration.publish.domain, + progress: { $in: ['queued', 'inprogress'] } + }) - if (alreadyQueued.length > 0) { - reply.code(200).send({ message: 'Already in the queue.' }) - return - } + if (alreadyQueued.length > 0) { + reply.code(200).send({ message: 'Already in the queue.' }) + return + } - queueAndBuild(configuration, services, configChanged, imageChanged) + queueAndBuild(configuration, imageChanged) - reply.code(201).send({ message: 'Deployment queued.', nickname: configuration.general.nickname, name: configuration.build.container.name }) + reply.code(201).send({ message: 'Deployment queued.', nickname: configuration.general.nickname, name: configuration.build.container.name }) + } catch (error) { + throw { error, type: 'server' } + } }) } diff --git a/api/routes/v1/application/deploy/logs.js b/api/routes/v1/application/deploy/logs.js index 322165f797..234015f6d5 100644 --- a/api/routes/v1/application/deploy/logs.js +++ b/api/routes/v1/application/deploy/logs.js @@ -18,25 +18,29 @@ module.exports = async function (fastify) { } } fastify.get('/', { schema: getLogSchema }, async (request, reply) => { - const { repoId, branch, page } = request.query - const onePage = 5 - const show = Number(page) * onePage || 5 - const deploy = await Deployment.find({ repoId, branch }) - .select('-_id -__v -repoId') - .sort({ createdAt: 'desc' }) - .limit(show) + try { + const { repoId, branch, page } = request.query + const onePage = 5 + const show = Number(page) * onePage || 5 + const deploy = await Deployment.find({ repoId, branch }) + .select('-_id -__v -repoId') + .sort({ createdAt: 'desc' }) + .limit(show) - const finalLogs = deploy.map(d => { - const finalLogs = { ...d._doc } + const finalLogs = deploy.map(d => { + const finalLogs = { ...d._doc } - const updatedAt = dayjs(d.updatedAt).utc() + const updatedAt = dayjs(d.updatedAt).utc() - finalLogs.took = updatedAt.diff(dayjs(d.createdAt)) / 1000 - finalLogs.since = updatedAt.fromNow() + finalLogs.took = updatedAt.diff(dayjs(d.createdAt)) / 1000 + finalLogs.since = updatedAt.fromNow() + return finalLogs + }) return finalLogs - }) - return finalLogs + } catch (error) { + throw { error, type: 'server' } + } }) fastify.get('/:deployId', async (request, reply) => { diff --git a/api/routes/v1/application/logs.js b/api/routes/v1/application/logs.js index e8221abac9..0396a22b32 100644 --- a/api/routes/v1/application/logs.js +++ b/api/routes/v1/application/logs.js @@ -2,9 +2,13 @@ const { docker } = require('../../../libs/docker') module.exports = async function (fastify) { fastify.get('/', async (request, reply) => { - const { name } = request.query - const service = await docker.engine.getService(`${name}_${name}`) - const logs = (await service.logs({ stdout: true, stderr: true, timestamps: true })).toString().split('\n').map(l => l.slice(8)).filter((a) => a) - return { logs } + try { + const { name } = request.query + const service = await docker.engine.getService(`${name}_${name}`) + const logs = (await service.logs({ stdout: true, stderr: true, timestamps: true })).toString().split('\n').map(l => l.slice(8)).filter((a) => a) + return { logs } + } catch (error) { + throw { error, type: 'server' } + } }) } diff --git a/api/routes/v1/config.js b/api/routes/v1/config.js index a8c66453c0..8a9543c062 100644 --- a/api/routes/v1/config.js +++ b/api/routes/v1/config.js @@ -1,60 +1,6 @@ const { docker } = require('../../libs/docker') module.exports = async function (fastify) { - // const getConfig = { - // querystring: { - // type: 'object', - // properties: { - // repoId: { type: 'number' }, - // branch: { type: 'string' } - // }, - // required: ['repoId', 'branch'] - // } - // } - - // const saveConfig = { - // body: { - // type: 'object', - // properties: { - // build: { - // type: 'object', - // properties: { - // baseDir: { type: 'string' }, - // installCmd: { type: 'string' }, - // buildCmd: { type: 'string' } - // }, - // required: ['baseDir', 'installCmd', 'buildCmd'] - // }, - // publish: { - // type: 'object', - // properties: { - // publishDir: { type: 'string' }, - // domain: { type: 'string' }, - // pathPrefix: { type: 'string' }, - // port: { type: 'number' } - // }, - // required: ['publishDir', 'domain', 'pathPrefix', 'port'] - // }, - // previewDeploy: { type: 'boolean' }, - // branch: { type: 'string' }, - // repoId: { type: 'number' }, - // buildPack: { type: 'string' }, - // fullName: { type: 'string' }, - // installationId: { type: 'number' } - // }, - // required: ['build', 'publish', 'previewDeploy', 'branch', 'repoId', 'buildPack', 'fullName', 'installationId'] - // } - // } - - // fastify.get("/all", async (request, reply) => { - // return await Config.find().select("-_id -__v"); - // }); - - // fastify.get("/", { schema: getConfig }, async (request, reply) => { - // const { repoId, branch } = request.query; - // return await Config.findOne({ repoId, branch }).select("-_id -__v"); - // }); - fastify.post('/', async (request, reply) => { const { name, organization, branch } = request.body const services = await docker.engine.listServices() @@ -79,25 +25,4 @@ module.exports = async function (fastify) { reply.code(500).send({ message: 'No configuration found.' }) } }) - - // fastify.delete("/", async (request, reply) => { - // const { repoId, branch } = request.body; - - // const deploys = await Deployment.find({ repoId, branch }) - // const found = deploys.filter(d => d.progress !== 'done' && d.progress !== 'failed') - // if (found.length > 0) { - // throw new Error('Deployment inprogress, cannot delete now.'); - // } - - // const config = await Config.findOneAndDelete({ repoId, branch }) - // for (const deploy of deploys) { - // await ApplicationLog.findOneAndRemove({ deployId: deploy.deployId }); - // } - // const secrets = await Secret.find({ repoId, branch }); - // for (const secret of secrets) { - // await Secret.findByIdAndRemove(secret._id); - // } - // await execShellAsync(`docker stack rm ${config.containerName}`); - // return { message: 'Deleted application and related configurations.' }; - // }); } diff --git a/api/routes/v1/dashboard/index.js b/api/routes/v1/dashboard/index.js index caec3441e3..f5e7c6c5ae 100644 --- a/api/routes/v1/dashboard/index.js +++ b/api/routes/v1/dashboard/index.js @@ -42,7 +42,7 @@ module.exports = async function (fastify) { r.Spec.Labels.configuration = configuration return r }) - applications = [...new Map(applications.map(item => [item.Spec.Labels.configuration.publish.domain, item])).values()] + applications = [...new Map(applications.map(item => [item.Spec.Labels.configuration.publish.domain + item.Spec.Labels.configuration.publish.path, item])).values()] return { serverLogs, applications: { @@ -55,6 +55,8 @@ module.exports = async function (fastify) { } catch (error) { if (error.code === 'ENOENT' && error.errno === -2) { throw new Error(`Docker service unavailable at ${error.address}.`) + } else { + throw { error, type: 'server' } } } }) diff --git a/api/routes/v1/databases/index.js b/api/routes/v1/databases/index.js index 17871aa038..7ca554c13a 100644 --- a/api/routes/v1/databases/index.js +++ b/api/routes/v1/databases/index.js @@ -45,124 +45,128 @@ module.exports = async function (fastify) { } fastify.post('/deploy', { schema: postSchema }, async (request, reply) => { - let { type, defaultDatabaseName } = request.body - const passwords = generator.generateMultiple(2, { - length: 24, - numbers: true, - strict: true - }) - const usernames = generator.generateMultiple(2, { - length: 10, - numbers: true, - strict: true - }) - // TODO: Query for existing db with the same name - const nickname = getUniq() + try { + let { type, defaultDatabaseName } = request.body + const passwords = generator.generateMultiple(2, { + length: 24, + numbers: true, + strict: true + }) + const usernames = generator.generateMultiple(2, { + length: 10, + numbers: true, + strict: true + }) + // TODO: Query for existing db with the same name + const nickname = getUniq() - if (!defaultDatabaseName) defaultDatabaseName = nickname + if (!defaultDatabaseName) defaultDatabaseName = nickname - reply.code(201).send({ message: 'Deploying.' }) - // TODO: Persistent volume, custom inputs - const deployId = cuid() - const configuration = { - general: { - workdir: `/tmp/${deployId}`, - deployId, - nickname, - type - }, - database: { - usernames, - passwords, - defaultDatabaseName - }, - deploy: { - name: nickname - } - } - let generateEnvs = {} - let image = null - let volume = null - if (type === 'mongodb') { - generateEnvs = { - MONGODB_ROOT_PASSWORD: passwords[0], - MONGODB_USERNAME: usernames[0], - MONGODB_PASSWORD: passwords[1], - MONGODB_DATABASE: defaultDatabaseName - } - image = 'bitnami/mongodb:4.4' - volume = `${configuration.general.deployId}-${type}-data:/bitnami/mongodb` - } else if (type === 'postgresql') { - generateEnvs = { - POSTGRESQL_PASSWORD: passwords[0], - POSTGRESQL_USERNAME: usernames[0], - POSTGRESQL_DATABASE: defaultDatabaseName - } - image = 'bitnami/postgresql:13.2.0' - volume = `${configuration.general.deployId}-${type}-data:/bitnami/postgresql` - } else if (type === 'couchdb') { - generateEnvs = { - COUCHDB_PASSWORD: passwords[0], - COUCHDB_USER: usernames[0] + reply.code(201).send({ message: 'Deploying.' }) + // TODO: Persistent volume, custom inputs + const deployId = cuid() + const configuration = { + general: { + workdir: `/tmp/${deployId}`, + deployId, + nickname, + type + }, + database: { + usernames, + passwords, + defaultDatabaseName + }, + deploy: { + name: nickname + } } - image = 'bitnami/couchdb:3' - volume = `${configuration.general.deployId}-${type}-data:/bitnami/couchdb` - } else if (type === 'mysql') { - generateEnvs = { - MYSQL_ROOT_PASSWORD: passwords[0], - MYSQL_ROOT_USER: usernames[0], - MYSQL_USER: usernames[1], - MYSQL_PASSWORD: passwords[1], - MYSQL_DATABASE: defaultDatabaseName + let generateEnvs = {} + let image = null + let volume = null + if (type === 'mongodb') { + generateEnvs = { + MONGODB_ROOT_PASSWORD: passwords[0], + MONGODB_USERNAME: usernames[0], + MONGODB_PASSWORD: passwords[1], + MONGODB_DATABASE: defaultDatabaseName + } + image = 'bitnami/mongodb:4.4' + volume = `${configuration.general.deployId}-${type}-data:/bitnami/mongodb` + } else if (type === 'postgresql') { + generateEnvs = { + POSTGRESQL_PASSWORD: passwords[0], + POSTGRESQL_USERNAME: usernames[0], + POSTGRESQL_DATABASE: defaultDatabaseName + } + image = 'bitnami/postgresql:13.2.0' + volume = `${configuration.general.deployId}-${type}-data:/bitnami/postgresql` + } else if (type === 'couchdb') { + generateEnvs = { + COUCHDB_PASSWORD: passwords[0], + COUCHDB_USER: usernames[0] + } + image = 'bitnami/couchdb:3' + volume = `${configuration.general.deployId}-${type}-data:/bitnami/couchdb` + } else if (type === 'mysql') { + generateEnvs = { + MYSQL_ROOT_PASSWORD: passwords[0], + MYSQL_ROOT_USER: usernames[0], + MYSQL_USER: usernames[1], + MYSQL_PASSWORD: passwords[1], + MYSQL_DATABASE: defaultDatabaseName + } + image = 'bitnami/mysql:8.0' + volume = `${configuration.general.deployId}-${type}-data:/bitnami/mysql/data` } - image = 'bitnami/mysql:8.0' - volume = `${configuration.general.deployId}-${type}-data:/bitnami/mysql/data` - } - const stack = { - version: '3.8', - services: { - [configuration.general.deployId]: { - image, - networks: [`${docker.network}`], - environment: generateEnvs, - volumes: [volume], - deploy: { - replicas: 1, - update_config: { - parallelism: 0, - delay: '10s', - order: 'start-first' - }, - rollback_config: { - parallelism: 0, - delay: '10s', - order: 'start-first' - }, - labels: [ - 'managedBy=coolify', - 'type=database', - 'configuration=' + JSON.stringify(configuration) - ] + const stack = { + version: '3.8', + services: { + [configuration.general.deployId]: { + image, + networks: [`${docker.network}`], + environment: generateEnvs, + volumes: [volume], + deploy: { + replicas: 1, + update_config: { + parallelism: 0, + delay: '10s', + order: 'start-first' + }, + rollback_config: { + parallelism: 0, + delay: '10s', + order: 'start-first' + }, + labels: [ + 'managedBy=coolify', + 'type=database', + 'configuration=' + JSON.stringify(configuration) + ] + } + } + }, + networks: { + [`${docker.network}`]: { + external: true + } + }, + volumes: { + [`${configuration.general.deployId}-${type}-data`]: { + external: true } - } - }, - networks: { - [`${docker.network}`]: { - external: true - } - }, - volumes: { - [`${configuration.general.deployId}-${type}-data`]: { - external: true } } + await execShellAsync(`mkdir -p ${configuration.general.workdir}`) + await fs.writeFile(`${configuration.general.workdir}/stack.yml`, yaml.dump(stack)) + await execShellAsync( + `cat ${configuration.general.workdir}/stack.yml | docker stack deploy -c - ${configuration.general.deployId}` + ) + } catch (error) { + throw { error, type: 'server' } } - await execShellAsync(`mkdir -p ${configuration.general.workdir}`) - await fs.writeFile(`${configuration.general.workdir}/stack.yml`, yaml.dump(stack)) - await execShellAsync( - `cat ${configuration.general.workdir}/stack.yml | docker stack deploy -c - ${configuration.general.deployId}` - ) }) fastify.delete('/:dbName', async (request, reply) => { diff --git a/api/routes/v1/server/index.js b/api/routes/v1/server/index.js new file mode 100644 index 0000000000..a43a6b9d49 --- /dev/null +++ b/api/routes/v1/server/index.js @@ -0,0 +1,14 @@ +const Server = require('../../../models/Logs/Server') +module.exports = async function (fastify) { + fastify.get('/', async (request, reply) => { + try { + const serverLogs = await Server.find().select('-_id -__v') + // TODO: Should do better + return { + serverLogs + } + } catch (error) { + throw { error, type: 'server' } + } + }) +} diff --git a/api/routes/v1/settings/index.js b/api/routes/v1/settings/index.js index 8ce3a35442..ac502e9d78 100644 --- a/api/routes/v1/settings/index.js +++ b/api/routes/v1/settings/index.js @@ -25,7 +25,7 @@ module.exports = async function (fastify) { settings } } catch (error) { - throw new Error(error) + throw { error, type: 'server' } } }) @@ -38,7 +38,7 @@ module.exports = async function (fastify) { ).select('-_id -__v') reply.code(201).send({ settings }) } catch (error) { - throw new Error(error) + throw { error, type: 'server' } } }) } diff --git a/api/routes/v1/upgrade/index.js b/api/routes/v1/upgrade/index.js index 7adfae051f..6ed1566804 100644 --- a/api/routes/v1/upgrade/index.js +++ b/api/routes/v1/upgrade/index.js @@ -3,10 +3,10 @@ const { saveServerLog } = require('../../../libs/logging') module.exports = async function (fastify) { fastify.get('/', async (request, reply) => { - const upgradeP1 = await execShellAsync('bash ./install.sh upgrade-phase-1') + const upgradeP1 = await execShellAsync('bash -c "$(curl -fsSL https://get.coollabs.io/coolify/upgrade-p1.sh)"') await saveServerLog({ event: upgradeP1, type: 'UPGRADE-P-1' }) reply.code(200).send('I\'m trying, okay?') - const upgradeP2 = await execShellAsync('bash ./install.sh upgrade-phase-2') + const upgradeP2 = await execShellAsync('docker run --rm -v /var/run/docker.sock:/var/run/docker.sock -u root coolify bash -c "$(curl -fsSL https://get.coollabs.io/coolify/upgrade-p2.sh)"') await saveServerLog({ event: upgradeP2, type: 'UPGRADE-P-2' }) }) } diff --git a/api/routes/v1/verify.js b/api/routes/v1/verify.js index 82e9331c07..1122d43ab2 100644 --- a/api/routes/v1/verify.js +++ b/api/routes/v1/verify.js @@ -3,14 +3,18 @@ const jwt = require('jsonwebtoken') module.exports = async function (fastify) { fastify.get('/', async (request, reply) => { - const { authorization } = request.headers - if (!authorization) { + try { + const { authorization } = request.headers + if (!authorization) { + reply.code(401).send({}) + return + } + const token = authorization.split(' ')[1] + const verify = jwt.verify(token, fastify.config.JWT_SIGN_KEY) + const found = await User.findOne({ uid: verify.jti }) + found ? reply.code(200).send({}) : reply.code(401).send({}) + } catch (error) { reply.code(401).send({}) - return } - const token = authorization.split(' ')[1] - const verify = jwt.verify(token, fastify.config.JWT_SIGN_KEY) - const found = await User.findOne({ uid: verify.jti }) - found ? reply.code(200).send({}) : reply.code(401).send({}) }) } diff --git a/api/routes/v1/webhooks/deploy.js b/api/routes/v1/webhooks/deploy.js index 51df9d8cbf..540bf17099 100644 --- a/api/routes/v1/webhooks/deploy.js +++ b/api/routes/v1/webhooks/deploy.js @@ -1,8 +1,8 @@ const crypto = require('crypto') -const { cleanupTmp, execShellAsync } = require('../../../libs/common') +const { cleanupTmp } = require('../../../libs/common') const Deployment = require('../../../models/Deployment') const { queueAndBuild } = require('../../../libs/applications') -const { setDefaultConfiguration } = require('../../../libs/applications/configuration') +const { setDefaultConfiguration, precheckDeployment } = require('../../../libs/applications/configuration') const { docker } = require('../../../libs/docker') const cloneRepository = require('../../../libs/applications/github/cloneRepository') @@ -45,98 +45,55 @@ module.exports = async function (fastify) { reply.code(500).send({ error: 'Not a push event.' }) return } - - const services = (await docker.engine.listServices()).filter(r => r.Spec.Labels.managedBy === 'coolify' && r.Spec.Labels.type === 'application') - - let configuration = services.find(r => { - if (request.body.ref.startsWith('refs')) { - const branch = request.body.ref.split('/')[2] - if ( - JSON.parse(r.Spec.Labels.configuration).repository.id === request.body.repository.id && - JSON.parse(r.Spec.Labels.configuration).repository.branch === branch - ) { - return r - } - } - - return null - }) - - if (!configuration) { - reply.code(500).send({ error: 'No configuration found.' }) - return - } - - configuration = setDefaultConfiguration(JSON.parse(configuration.Spec.Labels.configuration)) - - await cloneRepository(configuration) - - let foundService = false - let foundDomain = 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.publish.domain === configuration.publish.domain && - running.repository.id !== configuration.repository.id && - running.repository.branch !== configuration.repository.branch - ) { - foundDomain = true + try { + const services = (await docker.engine.listServices()).filter(r => r.Spec.Labels.managedBy === 'coolify' && r.Spec.Labels.type === 'application') + + let configuration = services.find(r => { + if (request.body.ref.startsWith('refs')) { + const branch = request.body.ref.split('/')[2] + if ( + JSON.parse(r.Spec.Labels.configuration).repository.id === request.body.repository.id && + JSON.parse(r.Spec.Labels.configuration).repository.branch === branch + ) { + return r + } } - if (running.repository.id === configuration.repository.id && running.repository.branch === configuration.repository.branch) { - 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') - if (isError.length > 0) forceUpdate = true - foundService = true - const runningWithoutContainer = JSON.parse(JSON.stringify(running)) - delete runningWithoutContainer.build.container + return null + }) + if (!configuration) { + reply.code(500).send({ error: 'No configuration found.' }) + return + } - const configurationWithoutContainer = JSON.parse(JSON.stringify(configuration)) - delete configurationWithoutContainer.build.container + configuration = setDefaultConfiguration(JSON.parse(configuration.Spec.Labels.configuration)) + await cloneRepository(configuration) + const { foundService, imageChanged, configChanged, forceUpdate } = await precheckDeployment({ services, configuration }) - if (JSON.stringify(runningWithoutContainer.build) !== JSON.stringify(configurationWithoutContainer.build) || JSON.stringify(runningWithoutContainer.publish) !== JSON.stringify(configurationWithoutContainer.publish)) configChanged = true - if (running.build.container.tag !== configuration.build.container.tag) imageChanged = true - } - } - } - if (foundDomain) { - cleanupTmp(configuration.general.workdir) - reply.code(500).send({ message: 'Domain already used.' }) - return - } - if (forceUpdate) { - imageChanged = false - configChanged = false - } else { - if (foundService && !imageChanged && !configChanged) { + if (foundService && !forceUpdate && !imageChanged && !configChanged) { cleanupTmp(configuration.general.workdir) reply.code(500).send({ message: 'Nothing changed, no need to redeploy.' }) return } - } + const alreadyQueued = await Deployment.find({ + repoId: configuration.repository.id, + branch: configuration.repository.branch, + organization: configuration.repository.organization, + name: configuration.repository.name, + domain: configuration.publish.domain, + progress: { $in: ['queued', 'inprogress'] } + }) + + if (alreadyQueued.length > 0) { + reply.code(200).send({ message: 'Already in the queue.' }) + return + } - const alreadyQueued = await Deployment.find({ - repoId: configuration.repository.id, - branch: configuration.repository.branch, - organization: configuration.repository.organization, - name: configuration.repository.name, - domain: configuration.publish.domain, - progress: { $in: ['queued', 'inprogress'] } - }) + queueAndBuild(configuration, imageChanged) - if (alreadyQueued.length > 0) { - reply.code(200).send({ message: 'Already in the queue.' }) - return + reply.code(201).send({ message: 'Deployment queued.', nickname: configuration.general.nickname, name: configuration.build.container.name }) + } catch (error) { + throw { error, type: 'server' } } - - queueAndBuild(configuration, services, configChanged, imageChanged) - - reply.code(201).send({ message: 'Deployment queued.' }) }) } diff --git a/api/server.js b/api/server.js index a84c45b40c..628cceb8c1 100644 --- a/api/server.js +++ b/api/server.js @@ -2,6 +2,8 @@ require('dotenv').config() const fs = require('fs') const util = require('util') const { saveServerLog } = require('./libs/logging') +const { execShellAsync } = require('./libs/common') +const { purgeImagesContainers, cleanupStuckedDeploymentsInDB } = require('./libs/applications/cleanup') const Deployment = require('./models/Deployment') const fastify = require('fastify')({ logger: { level: 'error' } @@ -10,6 +12,10 @@ const mongoose = require('mongoose') const path = require('path') const { schema } = require('./schema') +process.on('unhandledRejection', (reason, p) => { + console.log(reason) + console.log(p) +}) fastify.register(require('fastify-env'), { schema, dotenv: true @@ -31,13 +37,16 @@ if (process.env.NODE_ENV === 'production') { fastify.register(require('./app'), { prefix: '/api/v1' }) fastify.setErrorHandler(async (error, request, reply) => { - console.log({ error }) if (error.statusCode) { reply.status(error.statusCode).send({ message: error.message } || { message: 'Something is NOT okay. Are you okay?' }) } else { reply.status(500).send({ message: error.message } || { message: 'Something is NOT okay. Are you okay?' }) } - await saveServerLog({ event: error }) + try { + await saveServerLog({ event: error }) + } catch (error) { + // + } }) if (process.env.NODE_ENV === 'production') { @@ -83,8 +92,25 @@ mongoose.connection.once('open', async function () { console.log('Coolify API is up and running in development.') } // On start cleanup inprogress/queued deployments. - const deployments = await Deployment.find({ progress: { $in: ['queued', 'inprogress'] } }) - for (const deployment of deployments) { - await Deployment.findByIdAndUpdate(deployment._id, { $set: { progress: 'failed' } }) + try { + await cleanupStuckedDeploymentsInDB() + } catch (error) { + // Could not cleanup DB 🤔 + } + try { + // Doing because I do not want to prune these images. Prune skip coolify-reserve labeled images. + const basicImages = ['nginx:stable-alpine', 'node:lts', 'ubuntu:20.04'] + for (const image of basicImages) { + await execShellAsync(`echo "FROM ${image}" | docker build --label coolify-reserve=true -t ${image} -`) + } + } catch (error) { + console.log('Could not pull some basic images from Docker Hub.') + console.log(error) + } + try { + await purgeImagesContainers() + } catch (error) { + console.log('Could not purge containers/images.') + console.log(error) } }) diff --git a/install.sh b/install.sh index ce60f578a0..33889e7145 100644 --- a/install.sh +++ b/install.sh @@ -85,4 +85,4 @@ case "$1" in *) exit 1 ;; -esac +esac \ No newline at end of file diff --git a/install/Dockerfile-new b/install/Dockerfile-new new file mode 100644 index 0000000000..947123030f --- /dev/null +++ b/install/Dockerfile-new @@ -0,0 +1,24 @@ +FROM ubuntu:20.04 as binaries +LABEL coolify-preserve=true +RUN apt update && apt install -y curl gnupg2 ca-certificates +RUN curl -fsSL https://download.docker.com/linux/ubuntu/gpg | apt-key add - +RUN echo 'deb [arch=amd64] https://download.docker.com/linux/ubuntu focal stable' >> /etc/apt/sources.list +RUN curl -L https://github.com/a8m/envsubst/releases/download/v1.2.0/envsubst-`uname -s`-`uname -m` -o /usr/bin/envsubst +RUN curl -L https://github.com/stedolan/jq/releases/download/jq-1.6/jq-linux64 -o /usr/bin/jq +RUN chmod +x /usr/bin/envsubst /usr/bin/jq +RUN apt update && apt install -y docker-ce-cli && apt clean all + +FROM node:lts +WORKDIR /usr/src/app +LABEL coolify-preserve=true +COPY --from=binaries /usr/bin/docker /usr/bin/docker +COPY --from=binaries /usr/bin/envsubst /usr/bin/envsubst +COPY --from=binaries /usr/bin/jq /usr/bin/jq +COPY . . +RUN curl -f https://get.pnpm.io/v6.js | node - add --global pnpm@6 +RUN pnpm install +RUN pnpm build +RUN rm -fr node_modules .pnpm-store +RUN pnpm install -P +CMD ["pnpm", "start"] +EXPOSE 3000 \ No newline at end of file diff --git a/install/README.md b/install/README.md new file mode 100644 index 0000000000..d5f9c46a95 --- /dev/null +++ b/install/README.md @@ -0,0 +1,10 @@ +Some of the files are here for backwards compatibility. + +I will do things after 2 months: + +- rm ./install.js and ./update.js +- rm ../install.sh +- rm ./Dockerfile-base +- rm ./obs +- rm ./check.js "No need to check env file. During installation, it is checked by the installer. If you change it between to upgrades: 🤷♂️" +- Rename Dockerfile-new to Dockerfile diff --git a/install/check.js b/install/check.js new file mode 100644 index 0000000000..d21f8585e1 --- /dev/null +++ b/install/check.js @@ -0,0 +1,24 @@ +require('dotenv').config() +const fastify = require('fastify')() +const { schema } = require('../api/schema') + +checkConfig().then(() => { + console.log('Config: OK') +}).catch((err) => { + console.log('Config: NOT OK') + console.error(err) + process.exit(1) +}) + +function checkConfig () { + return new Promise((resolve, reject) => { + fastify.register(require('fastify-env'), { + schema, + dotenv: true + }) + .ready((err) => { + if (err) reject(err) + resolve() + }) + }) +} diff --git a/install/coolify-template.yml b/install/coolify-template.yml index 37e578adc6..574ade0e02 100644 --- a/install/coolify-template.yml +++ b/install/coolify-template.yml @@ -22,6 +22,7 @@ services: - --providers.docker.swarmMode=true - --providers.docker.exposedbydefault=false - --providers.docker.network=${DOCKER_NETWORK} + - --providers.docker.swarmModeRefreshSeconds=1s - --entrypoints.web.address=:80 - --entrypoints.websecure.address=:443 - --certificatesresolvers.letsencrypt.acme.httpchallenge=true diff --git a/install/obs/Dockerfile-base-new b/install/obs/Dockerfile-base-new new file mode 100644 index 0000000000..8c1ac91890 --- /dev/null +++ b/install/obs/Dockerfile-base-new @@ -0,0 +1,4 @@ +FROM coolify-base-nodejs +WORKDIR /usr/src/app +COPY . . +RUN pnpm install \ No newline at end of file diff --git a/install/obs/Dockerfile-base-nodejs b/install/obs/Dockerfile-base-nodejs new file mode 100644 index 0000000000..f35dbc7aea --- /dev/null +++ b/install/obs/Dockerfile-base-nodejs @@ -0,0 +1,6 @@ +FROM node:lts +LABEL coolify-preserve=true +COPY --from=coolify-binaries /usr/bin/docker /usr/bin/docker +COPY --from=coolify-binaries /usr/bin/envsubst /usr/bin/envsubst +COPY --from=coolify-binaries /usr/bin/jq /usr/bin/jq +RUN curl -f https://get.pnpm.io/v6.js | node - add --global pnpm@6 \ No newline at end of file diff --git a/install/obs/Dockerfile-binaries b/install/obs/Dockerfile-binaries new file mode 100644 index 0000000000..5dca5c2862 --- /dev/null +++ b/install/obs/Dockerfile-binaries @@ -0,0 +1,9 @@ +FROM ubuntu:20.04 +LABEL coolify-preserve=true +RUN apt update && apt install -y curl gnupg2 ca-certificates +RUN curl -fsSL https://download.docker.com/linux/ubuntu/gpg | apt-key add - +RUN echo 'deb [arch=amd64] https://download.docker.com/linux/ubuntu focal stable' >> /etc/apt/sources.list +RUN curl -L https://github.com/a8m/envsubst/releases/download/v1.2.0/envsubst-`uname -s`-`uname -m` -o /usr/bin/envsubst +RUN curl -L https://github.com/stedolan/jq/releases/download/jq-1.6/jq-linux64 -o /usr/bin/jq +RUN chmod +x /usr/bin/envsubst /usr/bin/jq +RUN apt update && apt install -y docker-ce-cli && apt clean all diff --git a/install/update.js b/install/update.js index 30f4cd4407..fb36d8a8ea 100644 --- a/install/update.js +++ b/install/update.js @@ -2,7 +2,6 @@ require('dotenv').config() const { program } = require('commander') const shell = require('shelljs') const user = shell.exec('whoami', { silent: true }).stdout.replace('\n', '') - program.version('0.0.1') program .option('-d, --debug', 'Debug outputs.') diff --git a/package.json b/package.json index a21497ca9e..7c7a802ee2 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "coolify", "description": "An open-source, hassle-free, self-hostable Heroku & Netlify alternative.", - "version": "1.0.5", + "version": "1.0.6", "license": "AGPL-3.0", "scripts": { "lint": "standard", @@ -16,8 +16,9 @@ "build:svite": "svite build" }, "dependencies": { + "@iarna/toml": "^2.2.5", "@roxi/routify": "^2.15.1", - "@zerodevx/svelte-toast": "^0.2.0", + "@zerodevx/svelte-toast": "^0.2.1", "axios": "^0.21.1", "commander": "^7.2.0", "compare-versions": "^3.6.0", @@ -26,7 +27,7 @@ "deepmerge": "^4.2.2", "dockerode": "^3.2.1", "dotenv": "^8.2.0", - "fastify": "^3.14.1", + "fastify": "^3.14.2", "fastify-env": "^2.1.0", "fastify-jwt": "^2.4.0", "fastify-plugin": "^3.0.0", @@ -52,7 +53,7 @@ "standard": "^16.0.3", "svelte": "^3.37.0", "svelte-hmr": "^0.14.0", - "svelte-preprocess": "^4.6.1", + "svelte-preprocess": "^4.7.0", "svite": "0.8.1", "tailwindcss": "2.1.1" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b6e57e9703..f09c6c74f6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,17 +1,18 @@ lockfileVersion: 5.3 specifiers: + '@iarna/toml': ^2.2.5 '@roxi/routify': ^2.15.1 - '@zerodevx/svelte-toast': ^0.2.0 + '@zerodevx/svelte-toast': ^0.2.1 axios: ^0.21.1 - commander: ^6.2.1 + commander: ^7.2.0 compare-versions: ^3.6.0 cuid: ^2.1.8 dayjs: ^1.10.4 deepmerge: ^4.2.2 dockerode: ^3.2.1 dotenv: ^8.2.0 - fastify: ^3.14.1 + fastify: ^3.14.2 fastify-env: ^2.1.0 fastify-jwt: ^2.4.0 fastify-plugin: ^3.0.0 @@ -33,24 +34,25 @@ specifiers: standard: ^16.0.3 svelte: ^3.37.0 svelte-hmr: ^0.14.0 - svelte-preprocess: ^4.6.1 + svelte-preprocess: ^4.7.0 svelte-select: ^3.17.0 svite: 0.8.1 tailwindcss: 2.1.1 unique-names-generator: ^4.4.0 dependencies: + '@iarna/toml': 2.2.5 '@roxi/routify': 2.15.1 - '@zerodevx/svelte-toast': 0.2.0 + '@zerodevx/svelte-toast': 0.2.1 axios: 0.21.1 - commander: 6.2.1 + commander: 7.2.0 compare-versions: 3.6.0 cuid: 2.1.8 dayjs: 1.10.4 deepmerge: 4.2.2 dockerode: 3.2.1 dotenv: 8.2.0 - fastify: 3.14.1 + fastify: 3.14.2 fastify-env: 2.1.0 fastify-jwt: 2.4.0 fastify-plugin: 3.0.0 @@ -76,7 +78,7 @@ devDependencies: standard: 16.0.3 svelte: 3.37.0 svelte-hmr: 0.14.0_svelte@3.37.0 - svelte-preprocess: 4.6.9_fa8d64e4f515eee295d8f0f45fceadd2 + svelte-preprocess: 4.7.0_fa8d64e4f515eee295d8f0f45fceadd2 svite: 0.8.1_d334b093211aa94b4e678204453b11ae tailwindcss: 2.1.1_postcss@8.2.9 @@ -155,6 +157,10 @@ packages: purgecss: 3.1.3 dev: true + /@iarna/toml/2.2.5: + resolution: {integrity: sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg==} + dev: false + /@koa/cors/3.1.0: resolution: {integrity: sha512-7ulRC1da/rBa6kj6P4g2aJfnET3z8Uf3SWu60cjbtxTA5g8lxRdX/Bd2P92EagGwwAhANeNw8T8if99rJliR6Q==} engines: {node: '>= 8.0.0'} @@ -553,8 +559,8 @@ packages: resolution: {integrity: sha512-dn5FyfSc4ky424jH4FntiHno7Ss5yLkqKNmM/NXwANRnlkmqu74pnGetexDFVG5phMk9/FhwovUZCWGxsotVKg==} dev: true - /@zerodevx/svelte-toast/0.2.0: - resolution: {integrity: sha512-zfnu02ZwAxpXfiqvAIZY97+Bv2hsBJ2fJlK/CVxliVu/+1I/R/z5Deo2BUtaLWmKAZX29FtFN9IBjF9hmPHQTA==} + /@zerodevx/svelte-toast/0.2.1: + resolution: {integrity: sha512-3yOusE+/xDaVNxkBJwbxDZea5ePQ77B15tbHv6ZlSYtlJu0u0PDhGMu8eoI+SmcCt4j+2sf0A1uS9+LcBIqUgg==} dev: false /abab/2.0.5: @@ -1167,6 +1173,7 @@ packages: /commander/6.2.1: resolution: {integrity: sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==} engines: {node: '>= 6'} + dev: true /commander/7.2.0: resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} @@ -2163,8 +2170,8 @@ packages: resolution: {integrity: sha512-s1EQguBw/9qtc1p/WTY4eq9WMRIACkj+HTcOIK1in4MV5aFaQC9ZCIt0dJ7pr5bIf4lPpHvAtP2ywpTNgs7hqw==} dev: false - /fastify/3.14.1: - resolution: {integrity: sha512-9hoK1vvopsUJnUJpge90t8PZIqNQhGM54yDrd2veCZLkxh8eipnaHrXe2+f7tIt6UScUZ92JZQavxFGB4HX7xA==} + /fastify/3.14.2: + resolution: {integrity: sha512-/PY//7gJnGxLQORaRHCEW148vpFKFpBIQNz1Yo/DxbHuk5EQqK2comzyE2ug8FSEldDX8nleapTshl0m78Px2w==} engines: {node: '>=10.16.0'} dependencies: '@fastify/proxy-addr': 3.0.0 @@ -5770,6 +5777,57 @@ packages: strip-indent: 3.0.0 svelte: 3.37.0 dev: true + optional: true + + /svelte-preprocess/4.7.0_fa8d64e4f515eee295d8f0f45fceadd2: + resolution: {integrity: sha512-iNrY4YGqi0LD2e6oT9YbdSzOKntxk8gmzfqso1z/lUJOZh4o6fyIqkirmiZ8/dDJFqtIE1spVgDFWgkfhLEYlw==} + engines: {node: '>= 9.11.2'} + requiresBuild: true + peerDependencies: + '@babel/core': ^7.10.2 + coffeescript: ^2.5.1 + less: ^3.11.3 + node-sass: '*' + postcss: ^7 || ^8 + postcss-load-config: ^2.1.0 || ^3.0.0 + pug: ^3.0.0 + sass: ^1.26.8 + stylus: ^0.54.7 + sugarss: ^2.0.0 + svelte: ^3.23.0 + typescript: ^3.9.5 || ^4.0.0 + peerDependenciesMeta: + '@babel/core': + optional: true + coffeescript: + optional: true + less: + optional: true + node-sass: + optional: true + postcss: + optional: true + postcss-load-config: + optional: true + pug: + optional: true + sass: + optional: true + stylus: + optional: true + sugarss: + optional: true + typescript: + optional: true + dependencies: + '@types/pug': 2.0.4 + '@types/sass': 1.16.0 + detect-indent: 6.0.0 + postcss: 8.2.9 + postcss-load-config: 3.0.1 + strip-indent: 3.0.0 + svelte: 3.37.0 + dev: true /svelte-select/3.17.0: resolution: {integrity: sha512-ITmX/XUiSdkaILmsTviKRkZPaXckM5/FA7Y8BhiUPoamaZG/ZDyOo6ydjFu9fDVFTbwoAUGUi6HBjs+ZdK2AwA==} diff --git a/src/App.svelte b/src/App.svelte index 3ce30e887d..8fe87ef357 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -3,8 +3,7 @@ import { Router } from "@roxi/routify"; import { routes } from "../.routify/routes"; const options = { - duration: 5000, - dismissable: true + duration: 2000 }; diff --git a/src/components/Application/Configuration/ActiveTab/General.svelte b/src/components/Application/Configuration/ActiveTab/General.svelte index e26c38bad4..915dc8b391 100644 --- a/src/components/Application/Configuration/ActiveTab/General.svelte +++ b/src/components/Application/Configuration/ActiveTab/General.svelte @@ -1,6 +1,7 @@
+ class="leading-4 text-left text-sm font-semibold tracking-tighter rounded-lg bg-black p-6 whitespace-pre-wrap"> {#if logs.length > 0} {#each logs as log} {log + '\n'} diff --git a/src/pages/application/[organization]/[name]/[branch]/logs/index.svelte b/src/pages/application/[organization]/[name]/[branch]/logs/index.svelte index c398c31044..42a91340e4 100644 --- a/src/pages/application/[organization]/[name]/[branch]/logs/index.svelte +++ b/src/pages/application/[organization]/[name]/[branch]/logs/index.svelte @@ -59,17 +59,17 @@{:then} diff --git a/tailwind.config.js b/tailwind.config.js index 33603f8213..ee9a9a72ac 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -16,7 +16,7 @@ module.exports = { ], preserveHtmlElements: true, options: { - safelist: [/svelte-/, 'border-green-500', 'border-yellow-300', 'border-red-500'], + safelist: [/svelte-/, 'border-green-500', 'border-yellow-300', 'border-red-500', 'hover:border-green-500', 'hover:border-red-200', 'hover:bg-red-200'], defaultExtractor: (content) => { // WARNING: tailwindExtractor is internal tailwind api // if this breaks after a tailwind update, report to svite repo diff --git a/vite.config.js b/vite.config.js index 190b57a073..42e8db2b97 100644 --- a/vite.config.js +++ b/vite.config.js @@ -26,7 +26,8 @@ module.exports = { '@zerodevx/svelte-toast', 'mongodb-memory-server-core', 'unique-names-generator', - 'generate-password' + 'generate-password', + '@iarna/toml' ] }, proxy: {Application logs{#if logs.length === 0} -Waiting for the logs...+Waiting for the logs...{:else} -+{#each logs as log} {log + '\n'} {/each} diff --git a/src/pages/application/new.svelte b/src/pages/application/new.svelte index 4c3dc3619d..9517ccadb0 100644 --- a/src/pages/application/new.svelte +++ b/src/pages/application/new.svelte @@ -2,12 +2,4 @@ import Configuration from "../../components/Application/Configuration/Configuration.svelte"; ---- New Application --diff --git a/src/pages/dashboard/applications.svelte b/src/pages/dashboard/applications.svelte index 8f2baa18d7..d19557f5cb 100644 --- a/src/pages/dashboard/applications.svelte +++ b/src/pages/dashboard/applications.svelte @@ -185,25 +185,37 @@ > + {:else if application.Spec.Labels.configuration.build.pack === "rust"} + {/if} -- {application.Spec.Labels.configuration.publish - .domain}{application.Spec.Labels.configuration.publish - .path !== "/" - ? application.Spec.Labels.configuration.publish.path - : ""} --- Last deployment+
- {new Intl.DateTimeFormat( - 'default', - $dateOptions, - ).format(new Date(application.UpdatedAt))} ++ {application.Spec.Labels.configuration.publish + .domain}{application.Spec.Labels.configuration.publish + .path !== "/" + ? application.Spec.Labels.configuration.publish.path + : ""}-+ Last deployment+
+ {new Intl.DateTimeFormat("default", $dateOptions).format( + new Date(application.UpdatedAt), + )} +