diff --git a/apps/api/src/jobs/deployApplication.ts b/apps/api/src/jobs/deployApplication.ts index 5722dbcfb..23adbf608 100644 --- a/apps/api/src/jobs/deployApplication.ts +++ b/apps/api/src/jobs/deployApplication.ts @@ -357,7 +357,9 @@ import * as buildpacks from '../lib/buildPacks'; where: { id: buildId, status: { in: ['queued', 'running'] } }, data: { status: 'failed' } }); - await saveBuildLog({ line: error, buildId, applicationId: application.id }); + if (error !== 1) { + await saveBuildLog({ line: error, buildId, applicationId: application.id }); + } } }); } diff --git a/apps/api/src/lib/common.ts b/apps/api/src/lib/common.ts index 55bfb08ab..9378d7266 100644 --- a/apps/api/src/lib/common.ts +++ b/apps/api/src/lib/common.ts @@ -21,7 +21,7 @@ import { scheduler } from './scheduler'; import { supportedServiceTypesAndVersions } from './services/supportedVersions'; import { includeServices } from './services/common'; -export const version = '3.10.1'; +export const version = '3.10.2'; export const isDev = process.env.NODE_ENV === 'development'; const algorithm = 'aes-256-ctr'; @@ -75,13 +75,15 @@ export const asyncExecShellStream = async ({ debug, buildId, applicationId, comm return await new Promise(async (resolve, reject) => { const { execaCommand } = await import('execa') const subprocess = execaCommand(command, { env: { DOCKER_BUILDKIT: "1", DOCKER_HOST: engine } }) - if (debug) { + const errorLogs = [] + const logs = [] subprocess.stdout.on('data', async (data) => { const stdout = data.toString(); const array = stdout.split('\n') for (const line of array) { if (line !== '\n' && line !== '') { - await saveBuildLog({ + logs.push(line.replace('\n', '')) + debug && await saveBuildLog({ line: `${line.replace('\n', '')}`, buildId, applicationId @@ -94,6 +96,22 @@ export const asyncExecShellStream = async ({ debug, buildId, applicationId, comm const array = stderr.split('\n') for (const line of array) { if (line !== '\n' && line !== '') { + errorLogs.push(line.replace('\n', '')) + debug && await saveBuildLog({ + line: `${line.replace('\n', '')}`, + buildId, + applicationId + }); + } + } + }) + subprocess.on('exit', async (code) => { + await asyncSleep(1000); + if (code === 0) { + resolve(code) + } else { + if (!debug) { + for (const line of errorLogs) { await saveBuildLog({ line: `${line.replace('\n', '')}`, buildId, @@ -101,13 +119,6 @@ export const asyncExecShellStream = async ({ debug, buildId, applicationId, comm }); } } - }) - } - subprocess.on('exit', async (code) => { - await asyncSleep(1000); - if (code === 0) { - resolve(code) - } else { reject(code) } }) diff --git a/apps/api/src/lib/importers/gitlab.ts b/apps/api/src/lib/importers/gitlab.ts index 69f204155..35fa3d8ce 100644 --- a/apps/api/src/lib/importers/gitlab.ts +++ b/apps/api/src/lib/importers/gitlab.ts @@ -40,7 +40,7 @@ export default async function ({ if (forPublic) { await asyncExecShell( - `git clone -q -b ${branch} git@${url}:${repository}.git --config core.sshCommand="ssh -p ${customPort} -q -o StrictHostKeyChecking=no" ${workdir}/ && cd ${workdir}/ && git submodule update --init --recursive && git lfs pull && cd .. ` + `git clone -q -b ${branch} https://${url}/${repository}.git ${workdir}/ && cd ${workdir}/ && git submodule update --init --recursive && git lfs pull && cd .. ` ); } else { await asyncExecShell( diff --git a/apps/api/src/lib/services/supportedVersions.ts b/apps/api/src/lib/services/supportedVersions.ts index 145d7395b..f4a08e5a2 100644 --- a/apps/api/src/lib/services/supportedVersions.ts +++ b/apps/api/src/lib/services/supportedVersions.ts @@ -116,7 +116,7 @@ export const supportedServiceTypesAndVersions = [ { name: 'umami', fancyName: 'Umami', - baseImage: 'ghcr.io/mikecao/umami', + baseImage: 'ghcr.io/umami-software/umami', images: ['postgres:12-alpine'], versions: ['postgresql-latest'], recommendedVersion: 'postgresql-latest', diff --git a/apps/api/src/routes/api/v1/handlers.ts b/apps/api/src/routes/api/v1/handlers.ts index 4e877f13c..a0217c58a 100644 --- a/apps/api/src/routes/api/v1/handlers.ts +++ b/apps/api/src/routes/api/v1/handlers.ts @@ -1,13 +1,23 @@ - -import axios from 'axios'; -import { compareVersions } from 'compare-versions'; -import cuid from 'cuid'; -import bcrypt from 'bcryptjs'; -import { asyncExecShell, asyncSleep, cleanupDockerStorage, errorHandler, isDev, listSettings, prisma, uniqueName, version } from '../../../lib/common'; -import { supportedServiceTypesAndVersions } from '../../../lib/services/supportedVersions'; -import type { FastifyReply, FastifyRequest } from 'fastify'; -import type { Login, Update } from '.'; -import type { GetCurrentUser } from './types'; +import axios from "axios"; +import { compareVersions } from "compare-versions"; +import cuid from "cuid"; +import bcrypt from "bcryptjs"; +import { + asyncExecShell, + asyncSleep, + cleanupDockerStorage, + errorHandler, + isDev, + listSettings, + prisma, + uniqueName, + version, +} from "../../../lib/common"; +import { supportedServiceTypesAndVersions } from "../../../lib/services/supportedVersions"; +import { scheduler } from "../../../lib/scheduler"; +import type { FastifyReply, FastifyRequest } from "fastify"; +import type { Login, Update } from "."; +import type { GetCurrentUser } from "./types"; export async function hashPassword(password: string): Promise { const saltRounds = 15; @@ -17,34 +27,38 @@ export async function hashPassword(password: string): Promise { export async function cleanupManually(request: FastifyRequest) { try { const { serverId } = request.body; - const destination = await prisma.destinationDocker.findUnique({ where: { id: serverId } }) - await cleanupDockerStorage(destination.id, true, true) - return {} + const destination = await prisma.destinationDocker.findUnique({ + where: { id: serverId }, + }); + await cleanupDockerStorage(destination.id, true, true); + return {}; } catch ({ status, message }) { - return errorHandler({ status, message }) + return errorHandler({ status, message }); } } export async function checkUpdate(request: FastifyRequest) { try { - const isStaging = request.hostname === 'staging.coolify.io' || request.hostname === 'arm.coolify.io' + const isStaging = + request.hostname === "staging.coolify.io" || + request.hostname === "arm.coolify.io"; const currentVersion = version; const { data: versions } = await axios.get( - `https://get.coollabs.io/versions.json?appId=${process.env['COOLIFY_APP_ID']}&version=${currentVersion}` + `https://get.coollabs.io/versions.json?appId=${process.env["COOLIFY_APP_ID"]}&version=${currentVersion}` ); - const latestVersion = versions['coolify'].main.version + const latestVersion = versions["coolify"].main.version; const isUpdateAvailable = compareVersions(latestVersion, currentVersion); if (isStaging) { return { isUpdateAvailable: true, - latestVersion: 'next' - } + latestVersion: "next", + }; } return { isUpdateAvailable: isStaging ? true : isUpdateAvailable === 1, - latestVersion + latestVersion, }; } catch ({ status, message }) { - return errorHandler({ status, message }) + return errorHandler({ status, message }); } } @@ -67,13 +81,27 @@ export async function update(request: FastifyRequest) { return {}; } } catch ({ status, message }) { - return errorHandler({ status, message }) + return errorHandler({ status, message }); + } +} +export async function resetQueue(request: FastifyRequest) { + try { + const teamId = request.user.teamId; + if (teamId === "0") { + await prisma.build.updateMany({ + where: { status: { in: ["queued", "running"] } }, + data: { status: "canceled" }, + }); + scheduler.workers.get("deployApplication").postMessage("cancel"); + } + } catch ({ status, message }) { + return errorHandler({ status, message }); } } export async function restartCoolify(request: FastifyRequest) { try { const teamId = request.user.teamId; - if (teamId === '0') { + if (teamId === "0") { if (!isDev) { asyncExecShell(`docker restart coolify`); return {}; @@ -81,9 +109,12 @@ export async function restartCoolify(request: FastifyRequest) { return {}; } } - throw { status: 500, message: 'You are not authorized to restart Coolify.' }; + throw { + status: 500, + message: "You are not authorized to restart Coolify.", + }; } catch ({ status, message }) { - return errorHandler({ status, message }) + return errorHandler({ status, message }); } } @@ -92,24 +123,24 @@ export async function showDashboard(request: FastifyRequest) { const userId = request.user.userId; const teamId = request.user.teamId; const applications = await prisma.application.findMany({ - where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } }, - include: { settings: true, destinationDocker: true, teams: true } + where: { teams: { some: { id: teamId === "0" ? undefined : teamId } } }, + include: { settings: true, destinationDocker: true, teams: true }, }); const databases = await prisma.database.findMany({ - where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } }, - include: { settings: true, destinationDocker: true, teams: true } + where: { teams: { some: { id: teamId === "0" ? undefined : teamId } } }, + include: { settings: true, destinationDocker: true, teams: true }, }); const services = await prisma.service.findMany({ - where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } }, - include: { destinationDocker: true, teams: true } + where: { teams: { some: { id: teamId === "0" ? undefined : teamId } } }, + include: { destinationDocker: true, teams: true }, }); const gitSources = await prisma.gitSource.findMany({ - where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } }, - include: { teams: true } + where: { teams: { some: { id: teamId === "0" ? undefined : teamId } } }, + include: { teams: true }, }); const destinations = await prisma.destinationDocker.findMany({ - where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } }, - include: { teams: true } + where: { teams: { some: { id: teamId === "0" ? undefined : teamId } } }, + include: { teams: true }, }); const settings = await listSettings(); return { @@ -121,88 +152,98 @@ export async function showDashboard(request: FastifyRequest) { settings, }; } catch ({ status, message }) { - return errorHandler({ status, message }) + return errorHandler({ status, message }); } } -export async function login(request: FastifyRequest, reply: FastifyReply) { +export async function login( + request: FastifyRequest, + reply: FastifyReply +) { if (request.user) { - return reply.redirect('/dashboard'); + return reply.redirect("/dashboard"); } else { const { email, password, isLogin } = request.body || {}; if (!email || !password) { - throw { status: 500, message: 'Email and password are required.' }; + throw { status: 500, message: "Email and password are required." }; } const users = await prisma.user.count(); const userFound = await prisma.user.findUnique({ where: { email }, include: { teams: true, permission: true }, - rejectOnNotFound: false + rejectOnNotFound: false, }); if (!userFound && isLogin) { - throw { status: 500, message: 'User not found.' }; + throw { status: 500, message: "User not found." }; } - const { isRegistrationEnabled, id } = await prisma.setting.findFirst() + const { isRegistrationEnabled, id } = await prisma.setting.findFirst(); let uid = cuid(); - let permission = 'read'; + let permission = "read"; let isAdmin = false; if (users === 0) { - await prisma.setting.update({ where: { id }, data: { isRegistrationEnabled: false } }); - uid = '0'; + await prisma.setting.update({ + where: { id }, + data: { isRegistrationEnabled: false }, + }); + uid = "0"; } if (userFound) { - if (userFound.type === 'email') { - if (userFound.password === 'RESETME') { + if (userFound.type === "email") { + if (userFound.password === "RESETME") { const hashedPassword = await hashPassword(password); if (userFound.updatedAt < new Date(Date.now() - 1000 * 60 * 10)) { - if (userFound.id === '0') { + if (userFound.id === "0") { await prisma.user.update({ where: { email: userFound.email }, - data: { password: 'RESETME' } + data: { password: "RESETME" }, }); } else { await prisma.user.update({ where: { email: userFound.email }, - data: { password: 'RESETTIMEOUT' } + data: { password: "RESETTIMEOUT" }, }); } throw { status: 500, - message: 'Password reset link has expired. Please request a new one.' + message: + "Password reset link has expired. Please request a new one.", }; } else { await prisma.user.update({ where: { email: userFound.email }, - data: { password: hashedPassword } + data: { password: hashedPassword }, }); return { userId: userFound.id, teamId: userFound.id, permission: userFound.permission, - isAdmin: true + isAdmin: true, }; } } - const passwordMatch = await bcrypt.compare(password, userFound.password); + const passwordMatch = await bcrypt.compare( + password, + userFound.password + ); if (!passwordMatch) { throw { status: 500, - message: 'Wrong password or email address.' + message: "Wrong password or email address.", }; } uid = userFound.id; isAdmin = true; } } else { - permission = 'owner'; + permission = "owner"; isAdmin = true; if (!isRegistrationEnabled) { throw { status: 404, - message: 'Registration disabled by administrator.' + message: "Registration disabled by administrator.", }; } const hashedPassword = await hashPassword(password); @@ -212,17 +253,17 @@ export async function login(request: FastifyRequest, reply: FastifyReply) id: uid, email, password: hashedPassword, - type: 'email', + type: "email", teams: { create: { id: uid, name: uniqueName(), - destinationDocker: { connect: { network: 'coolify' } } - } + destinationDocker: { connect: { network: "coolify" } }, + }, }, - permission: { create: { teamId: uid, permission: 'owner' } } + permission: { create: { teamId: uid, permission: "owner" } }, }, - include: { teams: true } + include: { teams: true }, }); } else { await prisma.user.create({ @@ -230,16 +271,16 @@ export async function login(request: FastifyRequest, reply: FastifyReply) id: uid, email, password: hashedPassword, - type: 'email', + type: "email", teams: { create: { id: uid, - name: uniqueName() - } + name: uniqueName(), + }, }, - permission: { create: { teamId: uid, permission: 'owner' } } + permission: { create: { teamId: uid, permission: "owner" } }, }, - include: { teams: true } + include: { teams: true }, }); } } @@ -247,18 +288,21 @@ export async function login(request: FastifyRequest, reply: FastifyReply) userId: uid, teamId: uid, permission, - isAdmin + isAdmin, }; } } -export async function getCurrentUser(request: FastifyRequest, fastify) { - let token = null - const { teamId } = request.query +export async function getCurrentUser( + request: FastifyRequest, + fastify +) { + let token = null; + const { teamId } = request.query; try { const user = await prisma.user.findUnique({ - where: { id: request.user.userId } - }) + where: { id: request.user.userId }, + }); if (!user) { throw "User not found"; } @@ -269,20 +313,20 @@ export async function getCurrentUser(request: FastifyRequest, fa try { const user = await prisma.user.findFirst({ where: { id: request.user.userId, teams: { some: { id: teamId } } }, - include: { teams: true, permission: true } - }) + include: { teams: true, permission: true }, + }); if (user) { - const permission = user.permission.find(p => p.teamId === teamId).permission + const permission = user.permission.find( + (p) => p.teamId === teamId + ).permission; const payload = { ...request.user, teamId, permission: permission || null, - isAdmin: permission === 'owner' || permission === 'admin' - - } - token = fastify.jwt.sign(payload) + isAdmin: permission === "owner" || permission === "admin", + }; + token = fastify.jwt.sign(payload); } - } catch (error) { // No new token -> not switching teams } @@ -291,6 +335,6 @@ export async function getCurrentUser(request: FastifyRequest, fa settings: await prisma.setting.findFirst(), supportedServiceTypesAndVersions, token, - ...request.user - } + ...request.user, + }; } diff --git a/apps/api/src/routes/api/v1/index.ts b/apps/api/src/routes/api/v1/index.ts index 52310998d..bab30236b 100644 --- a/apps/api/src/routes/api/v1/index.ts +++ b/apps/api/src/routes/api/v1/index.ts @@ -1,5 +1,5 @@ import { FastifyPluginAsync } from 'fastify'; -import { checkUpdate, login, showDashboard, update, showUsage, getCurrentUser, cleanupManually, restartCoolify } from './handlers'; +import { checkUpdate, login, showDashboard, update, resetQueue, getCurrentUser, cleanupManually, restartCoolify } from './handlers'; import { GetCurrentUser } from './types'; export interface Update { @@ -47,6 +47,10 @@ const root: FastifyPluginAsync = async (fastify): Promise => { onRequest: [fastify.authenticate] }, async (request) => await restartCoolify(request)); + fastify.post('/internal/resetQueue', { + onRequest: [fastify.authenticate] + }, async (request) => await resetQueue(request)); + fastify.post('/internal/cleanup', { onRequest: [fastify.authenticate] }, async (request) => await cleanupManually(request)); diff --git a/apps/ui/src/routes/applications/[id]/logs/build.svelte b/apps/ui/src/routes/applications/[id]/logs/build.svelte index faeec72c0..de5d60c35 100644 --- a/apps/ui/src/routes/applications/[id]/logs/build.svelte +++ b/apps/ui/src/routes/applications/[id]/logs/build.svelte @@ -24,10 +24,11 @@ export let buildCount: any; import { page } from '$app/stores'; +import {addToast} from '$lib/store'; import BuildLog from './_BuildLog.svelte'; - import { get } from '$lib/api'; + import { get, post } from '$lib/api'; import { t } from '$lib/translations'; - import { changeQueryParams, dateOptions, errorNotification } from '$lib/common'; + import { changeQueryParams, dateOptions, errorNotification, asyncSleep } from '$lib/common'; import Tooltip from '$lib/components/Tooltip.svelte'; let buildId: any; @@ -83,6 +84,23 @@ buildId = build; return changeQueryParams(buildId); } + async function resetQueue() { + const sure = confirm('It will reset all build queues for all applications. If something is queued, it will be canceled automatically. Are you sure? '); + if (sure) { + + try { + await post(`/internal/resetQueue`, {}); + addToast({ + message: 'Queue reset done.', + type: 'success' + }); + await asyncSleep(500) + return window.location.reload() + } catch (error) { + return errorNotification(error); + } + } + }
@@ -138,6 +156,7 @@
+
{#each builds as build, index (build.id)}
loadBuild(build.id)} class:rounded-tr={index === 0} class:rounded-br={index === builds.length - 1} - class="flex cursor-pointer items-center justify-center border-l-2 py-4 no-underline transition-all duration-100 hover:bg-coolgray-400 hover:shadow-xl" + class="flex cursor-pointer items-center justify-center py-4 no-underline transition-all duration-100 hover:bg-coolgray-400 hover:shadow-xl" class:bg-coolgray-400={buildId === build.id} - class:border-red-500={build.status === 'failed'} - class:border-orange-500={build.status === 'canceled'} - class:border-green-500={build.status === 'success'} - class:border-yellow-500={build.status === 'running'} >
@@ -159,6 +174,11 @@
{build.type}
+
{build.status}
@@ -187,7 +207,7 @@ {#if !noMoreBuilds} {#if buildCount > 5}
-
diff --git a/apps/ui/src/routes/index.svelte b/apps/ui/src/routes/index.svelte index c0620a42e..673390ffd 100644 --- a/apps/ui/src/routes/index.svelte +++ b/apps/ui/src/routes/index.svelte @@ -32,7 +32,7 @@ import Usage from '$lib/components/Usage.svelte'; import { t } from '$lib/translations'; import { asyncSleep } from '$lib/common'; - import { appSession, search } from '$lib/store'; + import { appSession, search, addToast} from '$lib/store'; import ApplicationsIcons from '$lib/components/svg/applications/ApplicationIcons.svelte'; import DatabaseIcons from '$lib/components/svg/databases/DatabaseIcons.svelte'; @@ -273,6 +273,7 @@ filtered = setInitials(); } } +
@@ -280,6 +281,7 @@ {#if $appSession.isAdmin && (applications.length !== 0 || destinations.length !== 0 || databases.length !== 0 || services.length !== 0 || gitSources.length !== 0 || destinations.length !== 0)} {/if} +