commit
470ff49a02
@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "ApplicationPersistentStorage" ADD COLUMN "hostPath" TEXT;
|
@ -195,6 +195,7 @@ model ApplicationSettings {
|
|||||||
model ApplicationPersistentStorage {
|
model ApplicationPersistentStorage {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
applicationId String
|
applicationId String
|
||||||
|
hostPath String?
|
||||||
path String
|
path String
|
||||||
oldPath Boolean @default(false)
|
oldPath Boolean @default(false)
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
|
@ -110,6 +110,9 @@ import * as buildpacks from '../lib/buildPacks';
|
|||||||
.replace(/\//gi, '-')
|
.replace(/\//gi, '-')
|
||||||
.replace('-app', '')}:${storage.path}`;
|
.replace('-app', '')}:${storage.path}`;
|
||||||
}
|
}
|
||||||
|
if (storage.hostPath) {
|
||||||
|
return `${storage.hostPath}:${storage.path}`
|
||||||
|
}
|
||||||
return `${applicationId}${storage.path.replace(/\//gi, '-')}:${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
|
port: exposePort ? `${exposePort}:${port}` : port
|
||||||
});
|
});
|
||||||
try {
|
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 {
|
return {
|
||||||
[`${volume.split(':')[0]}`]: {
|
[`${volume.split(':')[0]}`]: {
|
||||||
name: volume.split(':')[0]
|
name: volume.split(':')[0]
|
||||||
@ -381,6 +388,9 @@ import * as buildpacks from '../lib/buildPacks';
|
|||||||
.replace(/\//gi, '-')
|
.replace(/\//gi, '-')
|
||||||
.replace('-app', '')}:${storage.path}`;
|
.replace('-app', '')}:${storage.path}`;
|
||||||
}
|
}
|
||||||
|
if (storage.hostPath) {
|
||||||
|
return `${storage.hostPath}:${storage.path}`
|
||||||
|
}
|
||||||
return `${applicationId}${storage.path.replace(/\//gi, '-')}:${storage.path}`;
|
return `${applicationId}${storage.path.replace(/\//gi, '-')}:${storage.path}`;
|
||||||
}) || [];
|
}) || [];
|
||||||
|
|
||||||
@ -691,7 +701,11 @@ import * as buildpacks from '../lib/buildPacks';
|
|||||||
await saveDockerRegistryCredentials({ url, username, password, workdir });
|
await saveDockerRegistryCredentials({ url, username, password, workdir });
|
||||||
}
|
}
|
||||||
try {
|
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 {
|
return {
|
||||||
[`${volume.split(':')[0]}`]: {
|
[`${volume.split(':')[0]}`]: {
|
||||||
name: volume.split(':')[0]
|
name: volume.split(':')[0]
|
||||||
|
@ -36,12 +36,13 @@ export default async function (data) {
|
|||||||
if (volumes.length > 0) {
|
if (volumes.length > 0) {
|
||||||
for (const volume of volumes) {
|
for (const volume of volumes) {
|
||||||
let [v, path] = volume.split(':');
|
let [v, path] = volume.split(':');
|
||||||
composeVolumes[v] = {
|
if (!v.startsWith('.') && !v.startsWith('..') && !v.startsWith('/') && !v.startsWith('~')) {
|
||||||
name: v
|
composeVolumes[v] = {
|
||||||
};
|
name: v
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let networks = {};
|
let networks = {};
|
||||||
for (let [key, value] of Object.entries(dockerComposeYaml.services)) {
|
for (let [key, value] of Object.entries(dockerComposeYaml.services)) {
|
||||||
value['container_name'] = `${applicationId}-${key}`;
|
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
|
// TODO: If we support separated volume for each service, we need to add it here
|
||||||
if (value['volumes']?.length > 0) {
|
if (value['volumes']?.length > 0) {
|
||||||
value['volumes'] = value['volumes'].map((volume) => {
|
value['volumes'] = value['volumes'].map((volume) => {
|
||||||
let [v, path, permission] = volume.split(':');
|
if (typeof volume === 'string') {
|
||||||
if (!path) {
|
let [v, path, permission] = volume.split(':');
|
||||||
path = v;
|
if (
|
||||||
v = `${applicationId}${v.replace(/\//gi, '-').replace(/\./gi, '')}`;
|
v.startsWith('.') ||
|
||||||
} else {
|
v.startsWith('..') ||
|
||||||
v = `${applicationId}${v.replace(/\//gi, '-').replace(/\./gi, '')}`;
|
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] = {
|
if (typeof volume === 'object') {
|
||||||
name: v
|
let { source, target, mode } = volume;
|
||||||
};
|
if (
|
||||||
return `${v}:${path}${permission ? ':' + permission : ''}`;
|
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) {
|
if (volumes.length > 0) {
|
||||||
|
@ -11,7 +11,7 @@ import { promises as dns } from 'dns';
|
|||||||
import * as Sentry from '@sentry/node';
|
import * as Sentry from '@sentry/node';
|
||||||
import { PrismaClient } from '@prisma/client';
|
import { PrismaClient } from '@prisma/client';
|
||||||
import os from 'os';
|
import os from 'os';
|
||||||
import sshConfig from 'ssh-config';
|
import * as SSHConfig from 'ssh-config/src/ssh-config';
|
||||||
import jsonwebtoken from 'jsonwebtoken';
|
import jsonwebtoken from 'jsonwebtoken';
|
||||||
import { checkContainer, removeContainer } from './docker';
|
import { checkContainer, removeContainer } from './docker';
|
||||||
import { day } from './dayjs';
|
import { day } from './dayjs';
|
||||||
@ -19,7 +19,7 @@ import { saveBuildLog, saveDockerRegistryCredentials } from './buildPacks/common
|
|||||||
import { scheduler } from './scheduler';
|
import { scheduler } from './scheduler';
|
||||||
import type { ExecaChildProcess } from 'execa';
|
import type { ExecaChildProcess } from 'execa';
|
||||||
|
|
||||||
export const version = '3.12.25';
|
export const version = '3.12.26';
|
||||||
export const isDev = process.env.NODE_ENV === 'development';
|
export const isDev = process.env.NODE_ENV === 'development';
|
||||||
export const proxyPort = process.env.COOLIFY_PROXY_PORT;
|
export const proxyPort = process.env.COOLIFY_PROXY_PORT;
|
||||||
export const proxySecurePort = process.env.COOLIFY_PROXY_SECURE_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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the ssh config file with a host
|
||||||
|
*
|
||||||
|
* @param id Destination ID
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
export async function createRemoteEngineConfiguration(id: string) {
|
export async function createRemoteEngineConfiguration(id: string) {
|
||||||
const homedir = os.homedir();
|
|
||||||
const sshKeyFile = `/tmp/id_rsa-${id}`;
|
const sshKeyFile = `/tmp/id_rsa-${id}`;
|
||||||
const localPort = await getFreeSSHLocalPort(id);
|
const localPort = await getFreeSSHLocalPort(id);
|
||||||
const {
|
const {
|
||||||
sshKey: { privateKey },
|
sshKey: { privateKey },
|
||||||
network,
|
|
||||||
remoteIpAddress,
|
remoteIpAddress,
|
||||||
remotePort,
|
remotePort,
|
||||||
remoteUser
|
remoteUser
|
||||||
} = await prisma.destinationDocker.findFirst({ where: { id }, include: { sshKey: true } });
|
} = await prisma.destinationDocker.findFirst({ where: { id }, include: { sshKey: true } });
|
||||||
|
|
||||||
|
// Write new keyfile
|
||||||
await fs.writeFile(sshKeyFile, decrypt(privateKey) + '\n', { encoding: 'utf8', mode: 400 });
|
await fs.writeFile(sshKeyFile, decrypt(privateKey) + '\n', { encoding: 'utf8', mode: 400 });
|
||||||
const config = sshConfig.parse('');
|
|
||||||
const Host = `${remoteIpAddress}-remote`;
|
const Host = `${remoteIpAddress}-remote`;
|
||||||
|
|
||||||
|
// Removes previous ssh-keys
|
||||||
try {
|
try {
|
||||||
await executeCommand({ command: `ssh-keygen -R ${Host}` });
|
await executeCommand({ command: `ssh-keygen -R ${Host}` });
|
||||||
await executeCommand({ command: `ssh-keygen -R ${remoteIpAddress}` });
|
await executeCommand({ command: `ssh-keygen -R ${remoteIpAddress}` });
|
||||||
await executeCommand({ command: `ssh-keygen -R localhost:${localPort}` });
|
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 found = config.find({ Host });
|
||||||
const foundIp = config.find({ Host: remoteIpAddress });
|
const foundIp = config.find({ Host: remoteIpAddress });
|
||||||
|
|
||||||
if (found) config.remove({ Host });
|
if (found) config.remove({ Host });
|
||||||
if (foundIp) config.remove({ Host: remoteIpAddress });
|
if (foundIp) config.remove({ Host: remoteIpAddress });
|
||||||
|
|
||||||
|
// Create the new config
|
||||||
config.append({
|
config.append({
|
||||||
Host,
|
Host,
|
||||||
Hostname: remoteIpAddress,
|
Hostname: remoteIpAddress,
|
||||||
@ -537,13 +560,17 @@ export async function createRemoteEngineConfiguration(id: string) {
|
|||||||
ControlPersist: '10m'
|
ControlPersist: '10m'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Check if .ssh folder exists, and if not create one
|
||||||
try {
|
try {
|
||||||
await fs.stat(`${homedir}/.ssh/`);
|
await fs.stat(`${homedir}/.ssh/`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await fs.mkdir(`${homedir}/.ssh/`);
|
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({
|
export async function executeCommand({
|
||||||
command,
|
command,
|
||||||
dockerId = null,
|
dockerId = null,
|
||||||
@ -1633,6 +1660,9 @@ export function errorHandler({
|
|||||||
type?: string | null;
|
type?: string | null;
|
||||||
}) {
|
}) {
|
||||||
if (message.message) message = message.message;
|
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') {
|
if (type === 'normal') {
|
||||||
Sentry.captureException(message);
|
Sentry.captureException(message);
|
||||||
}
|
}
|
||||||
|
@ -1340,16 +1340,16 @@ export async function getStorages(request: FastifyRequest<OnlyId>) {
|
|||||||
export async function saveStorage(request: FastifyRequest<SaveStorage>, reply: FastifyReply) {
|
export async function saveStorage(request: FastifyRequest<SaveStorage>, reply: FastifyReply) {
|
||||||
try {
|
try {
|
||||||
const { id } = request.params;
|
const { id } = request.params;
|
||||||
const { path, newStorage, storageId } = request.body;
|
const { hostPath, path, newStorage, storageId } = request.body;
|
||||||
|
|
||||||
if (newStorage) {
|
if (newStorage) {
|
||||||
await prisma.applicationPersistentStorage.create({
|
await prisma.applicationPersistentStorage.create({
|
||||||
data: { path, application: { connect: { id } } }
|
data: { hostPath, path, application: { connect: { id } } }
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
await prisma.applicationPersistentStorage.update({
|
await prisma.applicationPersistentStorage.update({
|
||||||
where: { id: storageId },
|
where: { id: storageId },
|
||||||
data: { path }
|
data: { hostPath, path }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return reply.code(201).send();
|
return reply.code(201).send();
|
||||||
|
@ -96,6 +96,7 @@ export interface DeleteSecret extends OnlyId {
|
|||||||
}
|
}
|
||||||
export interface SaveStorage extends OnlyId {
|
export interface SaveStorage extends OnlyId {
|
||||||
Body: {
|
Body: {
|
||||||
|
hostPath?: string;
|
||||||
path: string;
|
path: string;
|
||||||
newStorage: boolean;
|
newStorage: boolean;
|
||||||
storageId: string;
|
storageId: string;
|
||||||
|
@ -12,6 +12,7 @@
|
|||||||
import { errorNotification } from '$lib/common';
|
import { errorNotification } from '$lib/common';
|
||||||
import { addToast } from '$lib/store';
|
import { addToast } from '$lib/store';
|
||||||
import CopyVolumeField from '$lib/components/CopyVolumeField.svelte';
|
import CopyVolumeField from '$lib/components/CopyVolumeField.svelte';
|
||||||
|
import SimpleExplainer from '$lib/components/SimpleExplainer.svelte';
|
||||||
const { id } = $page.params;
|
const { id } = $page.params;
|
||||||
let isHttps = browser && window.location.protocol === 'https:';
|
let isHttps = browser && window.location.protocol === 'https:';
|
||||||
export let value: string;
|
export let value: string;
|
||||||
@ -33,11 +34,13 @@
|
|||||||
storage.path.replace(/\/\//g, '/');
|
storage.path.replace(/\/\//g, '/');
|
||||||
await post(`/applications/${id}/storages`, {
|
await post(`/applications/${id}/storages`, {
|
||||||
path: storage.path,
|
path: storage.path,
|
||||||
|
hostPath: storage.hostPath,
|
||||||
storageId: storage.id,
|
storageId: storage.id,
|
||||||
newStorage
|
newStorage
|
||||||
});
|
});
|
||||||
dispatch('refresh');
|
dispatch('refresh');
|
||||||
if (isNew) {
|
if (isNew) {
|
||||||
|
storage.hostPath = null;
|
||||||
storage.path = null;
|
storage.path = null;
|
||||||
storage.id = null;
|
storage.id = null;
|
||||||
}
|
}
|
||||||
@ -80,27 +83,42 @@
|
|||||||
<div class="flex gap-4 pb-2" class:pt-8={isNew}>
|
<div class="flex gap-4 pb-2" class:pt-8={isNew}>
|
||||||
{#if storage.applicationId}
|
{#if storage.applicationId}
|
||||||
{#if storage.oldPath}
|
{#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', '')}"
|
value="{storage.applicationId}{storage.path.replace(/\//gi, '-').replace('-app', '')}"
|
||||||
/>
|
|
||||||
{:else}
|
|
||||||
|
|
||||||
<CopyVolumeField
|
|
||||||
value="{storage.applicationId}{storage.path.replace(/\//gi, '-').replace('-app', '')}"
|
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
{/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
|
<input
|
||||||
disabled={!isNew}
|
disabled={!isNew}
|
||||||
readonly={!isNew}
|
readonly={!isNew}
|
||||||
class="w-full"
|
class="w-full"
|
||||||
bind:value={storage.path}
|
bind:value={storage.path}
|
||||||
required
|
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}
|
{#if isNew}
|
||||||
<div class="w-full lg:w-64">
|
<div class="w-full lg:w-64">
|
||||||
<button class="btn btn-sm btn-primary w-full" on:click={() => saveStorage(true)}
|
<button class="btn btn-sm btn-primary w-full" on:click={() => saveStorage(true)}
|
||||||
|
@ -427,29 +427,6 @@
|
|||||||
</svg> Stop
|
</svg> Stop
|
||||||
</button>
|
</button>
|
||||||
{:else if $isDeploymentEnabled && !$page.url.pathname.startsWith(`/applications/${id}/configuration/`)}
|
{: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
|
<button
|
||||||
class="btn btn-sm gap-2"
|
class="btn btn-sm gap-2"
|
||||||
disabled={!$isDeploymentEnabled || !$appSession.isAdmin}
|
disabled={!$isDeploymentEnabled || !$appSession.isAdmin}
|
||||||
@ -493,6 +470,29 @@
|
|||||||
: 'Redeploy Stack'
|
: 'Redeploy Stack'
|
||||||
: 'Deploy'}
|
: 'Deploy'}
|
||||||
</button>
|
</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}
|
||||||
{#if $location && $status.application.overallStatus === 'healthy'}
|
{#if $location && $status.application.overallStatus === 'healthy'}
|
||||||
<a href={$location} target="_blank noreferrer" class="btn btn-sm gap-2 text-sm bg-primary"
|
<a href={$location} target="_blank noreferrer" class="btn btn-sm gap-2 text-sm bg-primary"
|
||||||
|
@ -35,17 +35,46 @@
|
|||||||
for (const [_, service] of Object.entries(composeJson.services)) {
|
for (const [_, service] of Object.entries(composeJson.services)) {
|
||||||
if (service?.volumes) {
|
if (service?.volumes) {
|
||||||
for (const [_, volumeName] of Object.entries(service.volumes)) {
|
for (const [_, volumeName] of Object.entries(service.volumes)) {
|
||||||
let [volume, target] = volumeName.split(':');
|
if (typeof volumeName === 'string') {
|
||||||
if (volume === '.') {
|
let [volume, target] = volumeName.split(':');
|
||||||
volume = target;
|
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) {
|
if (typeof volumeName === 'object') {
|
||||||
target = volume;
|
let { source, target } = volumeName;
|
||||||
volume = `${application.id}${volume.replace(/\//gi, '-').replace(/\./gi, '')}`;
|
if (
|
||||||
} else {
|
source.startsWith('.') ||
|
||||||
volume = `${application.id}${volume.replace(/\//gi, '-').replace(/\./gi, '')}`;
|
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}
|
{/key}
|
||||||
{/each}
|
{/each}
|
||||||
{#if $appSession.isAdmin}
|
{#if $appSession.isAdmin}
|
||||||
<div class:pt-10={predefinedVolumes.length > 0}>
|
<div class:pt-10={predefinedVolumes.length > 0}>
|
||||||
Add New Volume <Explainer
|
Add New Volume <Explainer
|
||||||
position="dropdown-bottom"
|
position="dropdown-bottom"
|
||||||
explanation={$t('application.storage.persistent_storage_explainer')}
|
explanation={$t('application.storage.persistent_storage_explainer')}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Storage on:refresh={refreshStorage} isNew />
|
<Storage on:refresh={refreshStorage} isNew />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "coolify",
|
"name": "coolify",
|
||||||
"description": "An open-source & self-hostable Heroku / Netlify alternative.",
|
"description": "An open-source & self-hostable Heroku / Netlify alternative.",
|
||||||
"version": "3.12.25",
|
"version": "3.12.26",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"repository": "github:coollabsio/coolify",
|
"repository": "github:coollabsio/coolify",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user