mirror of
https://github.com/ershisan99/coolify.git
synced 2026-01-04 12:33:47 +00:00
Compare commits
38 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
470ff49a02 | ||
|
|
04d741581d | ||
|
|
038f210148 | ||
|
|
2adad3a7bd | ||
|
|
05fb26a49b | ||
|
|
1c237affb4 | ||
|
|
3e81d7e9cb | ||
|
|
edb66620c1 | ||
|
|
04f7e8e777 | ||
|
|
eee201013c | ||
|
|
1190cb4ea1 | ||
|
|
507100ea0b | ||
|
|
9b13912b6d | ||
|
|
ee65deebfd | ||
|
|
ba9fa442d1 | ||
|
|
87da27f9bf | ||
|
|
b5bc5fe2c6 | ||
|
|
d2329360d0 | ||
|
|
7ece0ae10a | ||
|
|
f931b47eb8 | ||
|
|
7f7eb12ded | ||
|
|
c0940f7a19 | ||
|
|
9dfde11e35 | ||
|
|
6f15cc2dbc | ||
|
|
120308638f | ||
|
|
1d04ef99bb | ||
|
|
9b00d177ef | ||
|
|
884524c448 | ||
|
|
3ae1e7e87d | ||
|
|
81f885311d | ||
|
|
d9362f09d8 | ||
|
|
906d181d1b | ||
|
|
44b8812a7b | ||
|
|
3308c45e88 | ||
|
|
e530ecf9f9 | ||
|
|
51b5edb04f | ||
|
|
f0d89f850e | ||
|
|
b777e08542 |
@@ -22,7 +22,7 @@ ARG DOCKER_VERSION=20.10.18
|
||||
# Reverted to 2.6.1 because of this https://github.com/docker/compose/issues/9704. 2.9.0 still has a bug.
|
||||
ARG DOCKER_COMPOSE_VERSION=2.6.1
|
||||
# https://github.com/buildpacks/pack/releases
|
||||
ARG PACK_VERSION=v0.27.0
|
||||
ARG PACK_VERSION=0.27.0
|
||||
|
||||
RUN apt update && apt -y install --no-install-recommends ca-certificates git git-lfs openssh-client curl jq cmake sqlite3 openssl psmisc python3
|
||||
RUN apt-get clean autoclean && apt-get autoremove --yes && rm -rf /var/lib/{apt,dpkg,cache,log}/
|
||||
@@ -50,4 +50,4 @@ RUN pnpm install -p
|
||||
|
||||
EXPOSE 3000
|
||||
ENV CHECKPOINT_DISABLE=1
|
||||
CMD pnpm start
|
||||
CMD pnpm start
|
||||
|
||||
@@ -9,7 +9,7 @@ ARG DOCKER_VERSION=20.10.18
|
||||
# Reverted to 2.6.1 because of this https://github.com/docker/compose/issues/9704. 2.9.0 still has a bug.
|
||||
ARG DOCKER_COMPOSE_VERSION=2.6.1
|
||||
# https://github.com/buildpacks/pack/releases
|
||||
ARG PACK_VERSION=v0.27.0
|
||||
ARG PACK_VERSION=0.27.0
|
||||
WORKDIR /app
|
||||
|
||||
RUN npm --no-update-notifier --no-fund --global install pnpm@${PNPM_VERSION}
|
||||
@@ -28,4 +28,4 @@ RUN curl -SL https://cdn.coollabs.io/bin/$TARGETPLATFORM/pack-$PACK_VERSION -o /
|
||||
RUN chmod +x ~/.docker/cli-plugins/docker-compose /usr/bin/docker /usr/local/bin/pack
|
||||
|
||||
EXPOSE 3000
|
||||
ENV CHECKPOINT_DISABLE=1
|
||||
ENV CHECKPOINT_DISABLE=1
|
||||
|
||||
@@ -153,3 +153,6 @@ Support this project with your organization. Your logo will show up here with a
|
||||
|
||||
<a href="https://opencollective.com/coollabsio"><img src="https://opencollective.com/coollabsio/individuals.svg?width=890"></a>
|
||||
|
||||
## Star History
|
||||
|
||||
[](https://star-history.com/#coollabsio/coolify&Date)
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "ApplicationPersistentStorage" ADD COLUMN "hostPath" TEXT;
|
||||
@@ -195,6 +195,7 @@ model ApplicationSettings {
|
||||
model ApplicationPersistentStorage {
|
||||
id String @id @default(cuid())
|
||||
applicationId String
|
||||
hostPath String?
|
||||
path String
|
||||
oldPath Boolean @default(false)
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@ -110,6 +110,9 @@ import * as buildpacks from '../lib/buildPacks';
|
||||
.replace(/\//gi, '-')
|
||||
.replace('-app', '')}:${storage.path}`;
|
||||
}
|
||||
if (storage.hostPath) {
|
||||
return `${storage.hostPath}:${storage.path}`
|
||||
}
|
||||
return `${applicationId}${storage.path.replace(/\//gi, '-')}:${storage.path}`;
|
||||
}) || [];
|
||||
|
||||
@@ -160,7 +163,11 @@ import * as buildpacks from '../lib/buildPacks';
|
||||
port: exposePort ? `${exposePort}:${port}` : port
|
||||
});
|
||||
try {
|
||||
const composeVolumes = volumes.map((volume) => {
|
||||
const composeVolumes = volumes.filter(v => {
|
||||
if (!v.startsWith('.') && !v.startsWith('..') && !v.startsWith('/') && !v.startsWith('~')) {
|
||||
return v;
|
||||
}
|
||||
}).map((volume) => {
|
||||
return {
|
||||
[`${volume.split(':')[0]}`]: {
|
||||
name: volume.split(':')[0]
|
||||
@@ -381,12 +388,15 @@ import * as buildpacks from '../lib/buildPacks';
|
||||
.replace(/\//gi, '-')
|
||||
.replace('-app', '')}:${storage.path}`;
|
||||
}
|
||||
if (storage.hostPath) {
|
||||
return `${storage.hostPath}:${storage.path}`
|
||||
}
|
||||
return `${applicationId}${storage.path.replace(/\//gi, '-')}:${storage.path}`;
|
||||
}) || [];
|
||||
|
||||
try {
|
||||
dockerComposeConfiguration = JSON.parse(dockerComposeConfiguration);
|
||||
} catch (error) {}
|
||||
} catch (error) { }
|
||||
let deployNeeded = true;
|
||||
let destinationType;
|
||||
|
||||
@@ -406,7 +416,7 @@ import * as buildpacks from '../lib/buildPacks';
|
||||
installCommand = configuration.installCommand;
|
||||
startCommand = configuration.startCommand;
|
||||
buildCommand = configuration.buildCommand;
|
||||
publishDirectory = configuration.publishDirectory;
|
||||
publishDirectory = configuration.publishDirectory || '';
|
||||
baseDirectory = configuration.baseDirectory || '';
|
||||
dockerFileLocation = configuration.dockerFileLocation;
|
||||
dockerComposeFileLocation = configuration.dockerComposeFileLocation;
|
||||
@@ -453,7 +463,7 @@ import * as buildpacks from '../lib/buildPacks';
|
||||
|
||||
try {
|
||||
await prisma.build.update({ where: { id: buildId }, data: { commit } });
|
||||
} catch (err) {}
|
||||
} catch (err) { }
|
||||
|
||||
if (!pullmergeRequestId) {
|
||||
if (configHash !== currentHash) {
|
||||
@@ -494,9 +504,8 @@ import * as buildpacks from '../lib/buildPacks';
|
||||
try {
|
||||
await executeCommand({
|
||||
dockerId: destinationDocker.id,
|
||||
command: `docker ${
|
||||
location ? `--config ${location}` : ''
|
||||
} pull ${imageName}:${customTag}`
|
||||
command: `docker ${location ? `--config ${location}` : ''
|
||||
} pull ${imageName}:${customTag}`
|
||||
});
|
||||
imageFoundRemotely = true;
|
||||
} catch (error) {
|
||||
@@ -659,9 +668,8 @@ import * as buildpacks from '../lib/buildPacks';
|
||||
try {
|
||||
const { stdout: containers } = await executeCommand({
|
||||
dockerId: destinationDockerId,
|
||||
command: `docker ps -a --filter 'label=com.docker.compose.service=${
|
||||
pullmergeRequestId ? imageId : applicationId
|
||||
}' --format {{.ID}}`
|
||||
command: `docker ps -a --filter 'label=com.docker.compose.service=${pullmergeRequestId ? imageId : applicationId
|
||||
}' --format {{.ID}}`
|
||||
});
|
||||
if (containers) {
|
||||
const containerArray = containers.split('\n');
|
||||
@@ -693,7 +701,11 @@ import * as buildpacks from '../lib/buildPacks';
|
||||
await saveDockerRegistryCredentials({ url, username, password, workdir });
|
||||
}
|
||||
try {
|
||||
const composeVolumes = volumes.map((volume) => {
|
||||
const composeVolumes = volumes.filter(v => {
|
||||
if (!v.startsWith('.') && !v.startsWith('..') && !v.startsWith('/') && !v.startsWith('~')) {
|
||||
return v;
|
||||
}
|
||||
}).map((volume) => {
|
||||
return {
|
||||
[`${volume.split(':')[0]}`]: {
|
||||
name: volume.split(':')[0]
|
||||
|
||||
@@ -429,7 +429,12 @@ export const setDefaultConfiguration = async (data: any) => {
|
||||
startCommand = template?.startCommand || 'yarn start';
|
||||
if (!buildCommand && buildPack !== 'static' && buildPack !== 'laravel')
|
||||
buildCommand = template?.buildCommand || null;
|
||||
if (!publishDirectory) publishDirectory = template?.publishDirectory || null;
|
||||
if (!publishDirectory) {
|
||||
publishDirectory = template?.publishDirectory || null;
|
||||
} else {
|
||||
if (!publishDirectory.startsWith('/')) publishDirectory = `/${publishDirectory}`;
|
||||
if (publishDirectory.endsWith('/')) publishDirectory = publishDirectory.slice(0, -1);
|
||||
}
|
||||
if (baseDirectory) {
|
||||
if (!baseDirectory.startsWith('/')) baseDirectory = `/${baseDirectory}`;
|
||||
if (baseDirectory.endsWith('/') && baseDirectory !== '/')
|
||||
@@ -702,9 +707,8 @@ export async function buildImage({
|
||||
buildId,
|
||||
applicationId,
|
||||
dockerId,
|
||||
command: `docker ${location ? `--config ${location}` : ''} build ${
|
||||
forceRebuild ? '--no-cache' : ''
|
||||
} --progress plain -f ${workdir}/${dockerFile} -t ${cache} --build-arg SOURCE_COMMIT=${commit} ${workdir}`
|
||||
command: `docker ${location ? `--config ${location}` : ''} build ${forceRebuild ? '--no-cache' : ''
|
||||
} --progress plain -f ${workdir}/${dockerFile} -t ${cache} --build-arg SOURCE_COMMIT=${commit} ${workdir}`
|
||||
});
|
||||
|
||||
const { status } = await prisma.build.findUnique({ where: { id: buildId } });
|
||||
|
||||
@@ -36,12 +36,13 @@ export default async function (data) {
|
||||
if (volumes.length > 0) {
|
||||
for (const volume of volumes) {
|
||||
let [v, path] = volume.split(':');
|
||||
composeVolumes[v] = {
|
||||
name: v
|
||||
};
|
||||
if (!v.startsWith('.') && !v.startsWith('..') && !v.startsWith('/') && !v.startsWith('~')) {
|
||||
composeVolumes[v] = {
|
||||
name: v
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let networks = {};
|
||||
for (let [key, value] of Object.entries(dockerComposeYaml.services)) {
|
||||
value['container_name'] = `${applicationId}-${key}`;
|
||||
@@ -77,17 +78,54 @@ export default async function (data) {
|
||||
// TODO: If we support separated volume for each service, we need to add it here
|
||||
if (value['volumes']?.length > 0) {
|
||||
value['volumes'] = value['volumes'].map((volume) => {
|
||||
let [v, path, permission] = volume.split(':');
|
||||
if (!path) {
|
||||
path = v;
|
||||
v = `${applicationId}${v.replace(/\//gi, '-').replace(/\./gi, '')}`;
|
||||
} else {
|
||||
v = `${applicationId}${v.replace(/\//gi, '-').replace(/\./gi, '')}`;
|
||||
if (typeof volume === 'string') {
|
||||
let [v, path, permission] = volume.split(':');
|
||||
if (
|
||||
v.startsWith('.') ||
|
||||
v.startsWith('..') ||
|
||||
v.startsWith('/') ||
|
||||
v.startsWith('~') ||
|
||||
v.startsWith('$PWD')
|
||||
) {
|
||||
v = v.replace(/^\./, `~`).replace(/^\.\./, '~').replace(/^\$PWD/, '~');
|
||||
} else {
|
||||
if (!path) {
|
||||
path = v;
|
||||
v = `${applicationId}${v.replace(/\//gi, '-').replace(/\./gi, '')}`;
|
||||
} else {
|
||||
v = `${applicationId}${v.replace(/\//gi, '-').replace(/\./gi, '')}`;
|
||||
}
|
||||
composeVolumes[v] = {
|
||||
name: v
|
||||
};
|
||||
}
|
||||
return `${v}:${path}${permission ? ':' + permission : ''}`;
|
||||
}
|
||||
composeVolumes[v] = {
|
||||
name: v
|
||||
};
|
||||
return `${v}:${path}${permission ? ':' + permission : ''}`;
|
||||
if (typeof volume === 'object') {
|
||||
let { source, target, mode } = volume;
|
||||
if (
|
||||
source.startsWith('.') ||
|
||||
source.startsWith('..') ||
|
||||
source.startsWith('/') ||
|
||||
source.startsWith('~') ||
|
||||
source.startsWith('$PWD')
|
||||
) {
|
||||
|
||||
source = source.replace(/^\./, `~`).replace(/^\.\./, '~').replace(/^\$PWD/, '~');
|
||||
console.log({source})
|
||||
|
||||
} else {
|
||||
if (!target) {
|
||||
target = source;
|
||||
source = `${applicationId}${source.replace(/\//gi, '-').replace(/\./gi, '')}`;
|
||||
} else {
|
||||
source = `${applicationId}${source.replace(/\//gi, '-').replace(/\./gi, '')}`;
|
||||
}
|
||||
}
|
||||
|
||||
return `${source}:${target}${mode ? ':' + mode : ''}`;
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
if (volumes.length > 0) {
|
||||
@@ -95,7 +133,7 @@ export default async function (data) {
|
||||
value['volumes'].push(volume);
|
||||
}
|
||||
}
|
||||
if (dockerComposeConfiguration[key].port) {
|
||||
if (dockerComposeConfiguration[key]?.port) {
|
||||
value['expose'] = [dockerComposeConfiguration[key].port];
|
||||
}
|
||||
if (value['networks']?.length > 0) {
|
||||
|
||||
@@ -8,7 +8,7 @@ const createDockerfile = async (data, imageforBuild): Promise<void> => {
|
||||
Dockerfile.push(`FROM ${imageforBuild}`);
|
||||
Dockerfile.push('WORKDIR /app');
|
||||
Dockerfile.push(`LABEL coolify.buildId=${buildId}`);
|
||||
Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app/${publishDirectory} ./`);
|
||||
Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app${publishDirectory} ./`);
|
||||
if (baseImage?.includes('nginx')) {
|
||||
Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { promises as fs } from 'fs';
|
||||
import { buildCacheImageWithNode, buildImage } from './common';
|
||||
|
||||
const createDockerfile = async (data, image): Promise<void> => {
|
||||
const { buildId, applicationId, tag, port, startCommand, workdir, baseDirectory } = data;
|
||||
const { buildId, applicationId, tag, port, startCommand, workdir, publishDirectory } = data;
|
||||
const Dockerfile: Array<string> = [];
|
||||
const isPnpm = startCommand.includes('pnpm');
|
||||
|
||||
@@ -12,7 +12,7 @@ const createDockerfile = async (data, image): Promise<void> => {
|
||||
if (isPnpm) {
|
||||
Dockerfile.push('RUN curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm@7');
|
||||
}
|
||||
Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app/${baseDirectory || ''} ./`);
|
||||
Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app${publishDirectory} ./`);
|
||||
|
||||
Dockerfile.push(`EXPOSE ${port}`);
|
||||
Dockerfile.push(`CMD ${startCommand}`);
|
||||
|
||||
@@ -42,7 +42,7 @@ const createDockerfile = async (data, image): Promise<void> => {
|
||||
if (baseImage?.includes('nginx')) {
|
||||
Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`);
|
||||
}
|
||||
Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app/${publishDirectory} ./`);
|
||||
Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app${publishDirectory} ./`);
|
||||
Dockerfile.push(`EXPOSE 80`);
|
||||
}
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ const createDockerfile = async (data, image): Promise<void> => {
|
||||
if (baseImage?.includes('nginx')) {
|
||||
Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`);
|
||||
}
|
||||
Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app/${publishDirectory} ./`);
|
||||
Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app${publishDirectory} ./`);
|
||||
Dockerfile.push(`EXPOSE 80`);
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ const createDockerfile = async (data, image): Promise<void> => {
|
||||
Dockerfile.push(`FROM ${image}`);
|
||||
Dockerfile.push(`LABEL coolify.buildId=${buildId}`);
|
||||
Dockerfile.push('WORKDIR /app');
|
||||
Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app/${publishDirectory} ./`);
|
||||
Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app${publishDirectory} ./`);
|
||||
if (baseImage?.includes('nginx')) {
|
||||
Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`);
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ const createDockerfile = async (data, image): Promise<void> => {
|
||||
});
|
||||
}
|
||||
if (buildCommand) {
|
||||
Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app/${publishDirectory} ./`);
|
||||
Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app${publishDirectory} ./`);
|
||||
} else {
|
||||
Dockerfile.push(`COPY .${baseDirectory || ''} ./`);
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ const createDockerfile = async (data, image): Promise<void> => {
|
||||
Dockerfile.push(`FROM ${image}`);
|
||||
Dockerfile.push('WORKDIR /app');
|
||||
Dockerfile.push(`LABEL coolify.buildId=${buildId}`);
|
||||
Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app/${publishDirectory} ./`);
|
||||
Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app${publishDirectory} ./`);
|
||||
if (baseImage?.includes('nginx')) {
|
||||
Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`);
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ const createDockerfile = async (data, image): Promise<void> => {
|
||||
Dockerfile.push(`FROM ${image}`);
|
||||
Dockerfile.push('WORKDIR /app');
|
||||
Dockerfile.push(`LABEL coolify.buildId=${buildId}`);
|
||||
Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app/${publishDirectory} ./`);
|
||||
Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app${publishDirectory} ./`);
|
||||
if (baseImage?.includes('nginx')) {
|
||||
Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`);
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import { promises as dns } from 'dns';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import os from 'os';
|
||||
import sshConfig from 'ssh-config';
|
||||
import * as SSHConfig from 'ssh-config/src/ssh-config';
|
||||
import jsonwebtoken from 'jsonwebtoken';
|
||||
import { checkContainer, removeContainer } from './docker';
|
||||
import { day } from './dayjs';
|
||||
@@ -19,7 +19,7 @@ import { saveBuildLog, saveDockerRegistryCredentials } from './buildPacks/common
|
||||
import { scheduler } from './scheduler';
|
||||
import type { ExecaChildProcess } from 'execa';
|
||||
|
||||
export const version = '3.12.20';
|
||||
export const version = '3.12.26';
|
||||
export const isDev = process.env.NODE_ENV === 'development';
|
||||
export const proxyPort = process.env.COOLIFY_PROXY_PORT;
|
||||
export const proxySecurePort = process.env.COOLIFY_PROXY_SECURE_PORT;
|
||||
@@ -498,33 +498,56 @@ export async function getFreeSSHLocalPort(id: string): Promise<number | boolean>
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the ssh config file with a host
|
||||
*
|
||||
* @param id Destination ID
|
||||
* @returns
|
||||
*/
|
||||
export async function createRemoteEngineConfiguration(id: string) {
|
||||
const homedir = os.homedir();
|
||||
const sshKeyFile = `/tmp/id_rsa-${id}`;
|
||||
const localPort = await getFreeSSHLocalPort(id);
|
||||
const {
|
||||
sshKey: { privateKey },
|
||||
network,
|
||||
remoteIpAddress,
|
||||
remotePort,
|
||||
remoteUser
|
||||
} = await prisma.destinationDocker.findFirst({ where: { id }, include: { sshKey: true } });
|
||||
|
||||
// Write new keyfile
|
||||
await fs.writeFile(sshKeyFile, decrypt(privateKey) + '\n', { encoding: 'utf8', mode: 400 });
|
||||
const config = sshConfig.parse('');
|
||||
|
||||
const Host = `${remoteIpAddress}-remote`;
|
||||
|
||||
// Removes previous ssh-keys
|
||||
try {
|
||||
await executeCommand({ command: `ssh-keygen -R ${Host}` });
|
||||
await executeCommand({ command: `ssh-keygen -R ${remoteIpAddress}` });
|
||||
await executeCommand({ command: `ssh-keygen -R localhost:${localPort}` });
|
||||
} catch (error) { }
|
||||
} catch (error) {
|
||||
//
|
||||
}
|
||||
|
||||
const homedir = os.homedir();
|
||||
let currentConfigFileContent = '';
|
||||
try {
|
||||
// Read the current config file
|
||||
currentConfigFileContent = (await fs.readFile(`${homedir}/.ssh/config`)).toString();
|
||||
} catch (error) {
|
||||
// File doesn't exist, so we do nothing, a new one is going to be created
|
||||
}
|
||||
|
||||
// Parse the config file
|
||||
const config = SSHConfig.parse(currentConfigFileContent);
|
||||
|
||||
// Remove current config for the given host
|
||||
const found = config.find({ Host });
|
||||
const foundIp = config.find({ Host: remoteIpAddress });
|
||||
|
||||
if (found) config.remove({ Host });
|
||||
if (foundIp) config.remove({ Host: remoteIpAddress });
|
||||
|
||||
// Create the new config
|
||||
config.append({
|
||||
Host,
|
||||
Hostname: remoteIpAddress,
|
||||
@@ -537,13 +560,17 @@ export async function createRemoteEngineConfiguration(id: string) {
|
||||
ControlPersist: '10m'
|
||||
});
|
||||
|
||||
// Check if .ssh folder exists, and if not create one
|
||||
try {
|
||||
await fs.stat(`${homedir}/.ssh/`);
|
||||
} catch (error) {
|
||||
await fs.mkdir(`${homedir}/.ssh/`);
|
||||
}
|
||||
return await fs.writeFile(`${homedir}/.ssh/config`, sshConfig.stringify(config));
|
||||
|
||||
// Write the config
|
||||
return await fs.writeFile(`${homedir}/.ssh/config`, SSHConfig.stringify(config));
|
||||
}
|
||||
|
||||
export async function executeCommand({
|
||||
command,
|
||||
dockerId = null,
|
||||
@@ -1048,7 +1075,7 @@ export function generateDatabaseConfiguration(database: any): DatabaseConfigurat
|
||||
}
|
||||
export function isARM() {
|
||||
const arch = process.arch;
|
||||
if (arch === 'arm' || arch === 'arm64') {
|
||||
if (arch === 'arm' || arch === 'arm64' || arch === 'aarch' || arch === 'aarch64') {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -1633,6 +1660,9 @@ export function errorHandler({
|
||||
type?: string | null;
|
||||
}) {
|
||||
if (message.message) message = message.message;
|
||||
if (message.includes('Unique constraint failed')) {
|
||||
message = 'This data is unique and already exists. Please try again with a different value.';
|
||||
}
|
||||
if (type === 'normal') {
|
||||
Sentry.captureException(message);
|
||||
}
|
||||
|
||||
@@ -50,24 +50,12 @@ export async function startService(request: FastifyRequest<ServiceStartStop>, fa
|
||||
const config = {};
|
||||
for (const s in template.services) {
|
||||
let newEnvironments = []
|
||||
if (arm) {
|
||||
if (template.services[s]?.environmentArm?.length > 0) {
|
||||
for (const environment of template.services[s].environmentArm) {
|
||||
let [env, ...value] = environment.split("=");
|
||||
value = value.join("=")
|
||||
if (!value.startsWith('$$secret') && value !== '') {
|
||||
newEnvironments.push(`${env}=${value}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (template.services[s]?.environment?.length > 0) {
|
||||
for (const environment of template.services[s].environment) {
|
||||
let [env, ...value] = environment.split("=");
|
||||
value = value.join("=")
|
||||
if (!value.startsWith('$$secret') && value !== '') {
|
||||
newEnvironments.push(`${env}=${value}`)
|
||||
}
|
||||
if (template.services[s]?.environment?.length > 0) {
|
||||
for (const environment of template.services[s].environment) {
|
||||
let [env, ...value] = environment.split("=");
|
||||
value = value.join("=")
|
||||
if (!value.startsWith('$$secret') && value !== '') {
|
||||
newEnvironments.push(`${env}=${value}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -87,12 +75,13 @@ export async function startService(request: FastifyRequest<ServiceStartStop>, fa
|
||||
}
|
||||
const customVolumes = await prisma.servicePersistentStorage.findMany({ where: { serviceId: id } })
|
||||
let volumes = new Set()
|
||||
if (arm) {
|
||||
template.services[s]?.volumesArm && template.services[s].volumesArm.length > 0 && template.services[s].volumesArm.forEach(v => volumes.add(v))
|
||||
if (arm && template.services[s]?.volumesArm?.length > 0) {
|
||||
template.services[s].volumesArm.forEach(v => volumes.add(v))
|
||||
} else {
|
||||
template.services[s]?.volumes && template.services[s].volumes.length > 0 && template.services[s].volumes.forEach(v => volumes.add(v))
|
||||
if (template.services[s]?.volumes?.length > 0) {
|
||||
template.services[s].volumes.forEach(v => volumes.add(v))
|
||||
}
|
||||
}
|
||||
|
||||
// Workaround: old plausible analytics service wrong volume id name
|
||||
if (service.type === 'plausibleanalytics' && service.plausibleAnalytics?.id) {
|
||||
let temp = Array.from(volumes)
|
||||
|
||||
@@ -736,7 +736,7 @@ export async function deleteApplication(
|
||||
where: { id },
|
||||
include: { destinationDocker: true, teams: true }
|
||||
});
|
||||
if (teamId !== '0' || !application.teams.some((team) => team.id === teamId)) {
|
||||
if (teamId !== '0' && !application.teams.some((team) => team.id === teamId)) {
|
||||
throw { status: 403, message: 'You are not allowed to delete this application.' };
|
||||
}
|
||||
if (application?.destinationDocker?.id && application.destinationDocker?.network) {
|
||||
@@ -1340,16 +1340,16 @@ export async function getStorages(request: FastifyRequest<OnlyId>) {
|
||||
export async function saveStorage(request: FastifyRequest<SaveStorage>, reply: FastifyReply) {
|
||||
try {
|
||||
const { id } = request.params;
|
||||
const { path, newStorage, storageId } = request.body;
|
||||
const { hostPath, path, newStorage, storageId } = request.body;
|
||||
|
||||
if (newStorage) {
|
||||
await prisma.applicationPersistentStorage.create({
|
||||
data: { path, application: { connect: { id } } }
|
||||
data: { hostPath, path, application: { connect: { id } } }
|
||||
});
|
||||
} else {
|
||||
await prisma.applicationPersistentStorage.update({
|
||||
where: { id: storageId },
|
||||
data: { path }
|
||||
data: { hostPath, path }
|
||||
});
|
||||
}
|
||||
return reply.code(201).send();
|
||||
|
||||
@@ -96,6 +96,7 @@ export interface DeleteSecret extends OnlyId {
|
||||
}
|
||||
export interface SaveStorage extends OnlyId {
|
||||
Body: {
|
||||
hostPath?: string;
|
||||
path: string;
|
||||
newStorage: boolean;
|
||||
storageId: string;
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
import { errorNotification } from '$lib/common';
|
||||
import { addToast } from '$lib/store';
|
||||
import CopyVolumeField from '$lib/components/CopyVolumeField.svelte';
|
||||
import SimpleExplainer from '$lib/components/SimpleExplainer.svelte';
|
||||
const { id } = $page.params;
|
||||
let isHttps = browser && window.location.protocol === 'https:';
|
||||
export let value: string;
|
||||
@@ -33,11 +34,13 @@
|
||||
storage.path.replace(/\/\//g, '/');
|
||||
await post(`/applications/${id}/storages`, {
|
||||
path: storage.path,
|
||||
hostPath: storage.hostPath,
|
||||
storageId: storage.id,
|
||||
newStorage
|
||||
});
|
||||
dispatch('refresh');
|
||||
if (isNew) {
|
||||
storage.hostPath = null;
|
||||
storage.path = null;
|
||||
storage.id = null;
|
||||
}
|
||||
@@ -80,27 +83,42 @@
|
||||
<div class="flex gap-4 pb-2" class:pt-8={isNew}>
|
||||
{#if storage.applicationId}
|
||||
{#if storage.oldPath}
|
||||
|
||||
<CopyVolumeField
|
||||
<CopyVolumeField
|
||||
value="{storage.applicationId}{storage.path.replace(/\//gi, '-').replace('-app', '')}"
|
||||
/>
|
||||
{:else if !storage.hostPath}
|
||||
<CopyVolumeField
|
||||
value="{storage.applicationId}{storage.path.replace(/\//gi, '-').replace('-app', '')}"
|
||||
/>
|
||||
{:else}
|
||||
|
||||
<CopyVolumeField
|
||||
value="{storage.applicationId}{storage.path.replace(/\//gi, '-').replace('-app', '')}"
|
||||
/>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
{#if isNew}
|
||||
<div class="w-full">
|
||||
<input
|
||||
disabled={!isNew}
|
||||
readonly={!isNew}
|
||||
bind:value={storage.hostPath}
|
||||
placeholder="Host path, example: ~/.directory"
|
||||
/>
|
||||
|
||||
<SimpleExplainer
|
||||
text="You can mount <span class='text-yellow-400 font-bold'>host paths</span> from the operating system.<br>Leave it empty to define a volume based volume."
|
||||
/>
|
||||
</div>
|
||||
{:else if storage.hostPath}
|
||||
<input disabled readonly value={storage.hostPath} />
|
||||
{/if}
|
||||
<input
|
||||
disabled={!isNew}
|
||||
readonly={!isNew}
|
||||
class="w-full"
|
||||
bind:value={storage.path}
|
||||
required
|
||||
placeholder="eg: /data"
|
||||
placeholder="Mount point inside the container, example: /data"
|
||||
/>
|
||||
|
||||
<div class="flex items-center justify-center">
|
||||
<div class="flex items-start justify-center">
|
||||
{#if isNew}
|
||||
<div class="w-full lg:w-64">
|
||||
<button class="btn btn-sm btn-primary w-full" on:click={() => saveStorage(true)}
|
||||
|
||||
@@ -427,29 +427,6 @@
|
||||
</svg> Stop
|
||||
</button>
|
||||
{:else if $isDeploymentEnabled && !$page.url.pathname.startsWith(`/applications/${id}/configuration/`)}
|
||||
{#if $status.application.overallStatus === 'degraded'}
|
||||
<button
|
||||
on:click={stopApplication}
|
||||
type="submit"
|
||||
disabled={!$isDeploymentEnabled || !$appSession.isAdmin}
|
||||
class="btn btn-sm gap-2"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="w-6 h-6 text-error"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<rect x="6" y="5" width="4" height="14" rx="1" />
|
||||
<rect x="14" y="5" width="4" height="14" rx="1" />
|
||||
</svg> Stop
|
||||
</button>
|
||||
{/if}
|
||||
<button
|
||||
class="btn btn-sm gap-2"
|
||||
disabled={!$isDeploymentEnabled || !$appSession.isAdmin}
|
||||
@@ -493,6 +470,29 @@
|
||||
: 'Redeploy Stack'
|
||||
: 'Deploy'}
|
||||
</button>
|
||||
{#if $status.application.overallStatus === 'degraded'}
|
||||
<button
|
||||
on:click={stopApplication}
|
||||
type="submit"
|
||||
disabled={!$isDeploymentEnabled || !$appSession.isAdmin}
|
||||
class="btn btn-sm gap-2"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="w-6 h-6 text-error"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<rect x="6" y="5" width="4" height="14" rx="1" />
|
||||
<rect x="14" y="5" width="4" height="14" rx="1" />
|
||||
</svg> Stop
|
||||
</button>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if $location && $status.application.overallStatus === 'healthy'}
|
||||
<a href={$location} target="_blank noreferrer" class="btn btn-sm gap-2 text-sm bg-primary"
|
||||
|
||||
@@ -406,7 +406,7 @@
|
||||
>
|
||||
{#if tryAgain}
|
||||
<div class="p-5">
|
||||
An error occured during authenticating with GitLab. Please check your GitLab Source
|
||||
An error occurred during authenticating with GitLab. Please check your GitLab Source
|
||||
configuration <a href={`/sources/${application.gitSource.id}`}>here.</a>
|
||||
</div>
|
||||
<button
|
||||
|
||||
@@ -35,17 +35,46 @@
|
||||
for (const [_, service] of Object.entries(composeJson.services)) {
|
||||
if (service?.volumes) {
|
||||
for (const [_, volumeName] of Object.entries(service.volumes)) {
|
||||
let [volume, target] = volumeName.split(':');
|
||||
if (volume === '.') {
|
||||
volume = target;
|
||||
if (typeof volumeName === 'string') {
|
||||
let [volume, target] = volumeName.split(':');
|
||||
if (
|
||||
volume.startsWith('.') ||
|
||||
volume.startsWith('..') ||
|
||||
volume.startsWith('/') ||
|
||||
volume.startsWith('~') ||
|
||||
volume.startsWith('$PWD')
|
||||
) {
|
||||
volume = volume.replace(/^\./, `~`).replace(/^\.\./, '~').replace(/^\$PWD/, '~');
|
||||
} else {
|
||||
if (!target) {
|
||||
target = volume;
|
||||
volume = `${application.id}${volume.replace(/\//gi, '-').replace(/\./gi, '')}`;
|
||||
} else {
|
||||
volume = `${application.id}${volume.replace(/\//gi, '-').replace(/\./gi, '')}`;
|
||||
}
|
||||
}
|
||||
predefinedVolumes.push({ id: volume, path: target, predefined: true });
|
||||
}
|
||||
if (!target) {
|
||||
target = volume;
|
||||
volume = `${application.id}${volume.replace(/\//gi, '-').replace(/\./gi, '')}`;
|
||||
} else {
|
||||
volume = `${application.id}${volume.replace(/\//gi, '-').replace(/\./gi, '')}`;
|
||||
if (typeof volumeName === 'object') {
|
||||
let { source, target } = volumeName;
|
||||
if (
|
||||
source.startsWith('.') ||
|
||||
source.startsWith('..') ||
|
||||
source.startsWith('/') ||
|
||||
source.startsWith('~') ||
|
||||
source.startsWith('$PWD')
|
||||
) {
|
||||
source = source.replace(/^\./, `~`).replace(/^\.\./, '~').replace(/^\$PWD/, '~');
|
||||
} else {
|
||||
if (!target) {
|
||||
target = source;
|
||||
source = `${application.id}${source.replace(/\//gi, '-').replace(/\./gi, '')}`;
|
||||
} else {
|
||||
source = `${application.id}${source.replace(/\//gi, '-').replace(/\./gi, '')}`;
|
||||
}
|
||||
}
|
||||
predefinedVolumes.push({ id: source, path: target, predefined: true });
|
||||
}
|
||||
predefinedVolumes.push({ id: volume, path: target, predefined: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -88,14 +117,14 @@
|
||||
{/key}
|
||||
{/each}
|
||||
{#if $appSession.isAdmin}
|
||||
<div class:pt-10={predefinedVolumes.length > 0}>
|
||||
Add New Volume <Explainer
|
||||
position="dropdown-bottom"
|
||||
explanation={$t('application.storage.persistent_storage_explainer')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Storage on:refresh={refreshStorage} isNew />
|
||||
<div class:pt-10={predefinedVolumes.length > 0}>
|
||||
Add New Volume <Explainer
|
||||
position="dropdown-bottom"
|
||||
explanation={$t('application.storage.persistent_storage_explainer')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Storage on:refresh={refreshStorage} isNew />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -86,7 +86,7 @@
|
||||
readonly
|
||||
class="w-full"
|
||||
value={`${
|
||||
services.find((s) => s.id === storage.containerId).name || storage.containerId
|
||||
services.find((s) => s.id === storage.containerId)?.name || storage.containerId
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
@@ -111,19 +111,18 @@
|
||||
name="containerId"
|
||||
class="w-full lg:w-64"
|
||||
disabled={storage.predefined}
|
||||
readonly={storage.predefined}
|
||||
bind:value={storage.containerId}
|
||||
>
|
||||
{#if services.length === 1}
|
||||
{#if services[0].name}
|
||||
<option selected value={services[0].id}>{services[0].name}</option>
|
||||
<option selected value={services[0].id}>{services[0]?.name}</option>
|
||||
{:else}
|
||||
<option selected value={services[0]}>{services[0]}</option>
|
||||
{/if}
|
||||
{:else}
|
||||
{#each services as service}
|
||||
{#if service.name}
|
||||
<option value={service.id}>{service.name}</option>
|
||||
<option value={service.id}>{service?.name}</option>
|
||||
{:else}
|
||||
<option value={service}>{service}</option>
|
||||
{/if}
|
||||
@@ -157,7 +156,7 @@
|
||||
disabled
|
||||
readonly
|
||||
class="w-full"
|
||||
value={`${services.find((s) => s.id === storage.containerId).name || storage.containerId}`}
|
||||
value={`${services.find((s) => s.id === storage.containerId)?.name || storage.containerId}`}
|
||||
/>
|
||||
<input disabled readonly class="w-full" value={`${storage.volumeName}:${storage.path}`} />
|
||||
<button
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "coolify",
|
||||
"description": "An open-source & self-hostable Heroku / Netlify alternative.",
|
||||
"version": "3.12.20",
|
||||
"version": "3.12.26",
|
||||
"license": "Apache-2.0",
|
||||
"repository": "github:coollabsio/coolify",
|
||||
"scripts": {
|
||||
|
||||
Reference in New Issue
Block a user