feat: add host path to any container

This commit is contained in:
Andras Bacsai 2023-03-07 11:15:05 +01:00
parent 3e81d7e9cb
commit 1c237affb4
8 changed files with 60 additions and 18 deletions

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "ApplicationPersistentStorage" ADD COLUMN "hostPath" TEXT;

View File

@ -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())

View File

@ -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]

View File

@ -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}`;
@ -78,6 +79,7 @@ export default async function (data) {
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(':'); let [v, path, permission] = volume.split(':');
console.log(v, path, permission)
if ( if (
v.startsWith('.') || v.startsWith('.') ||
v.startsWith('..') || v.startsWith('..') ||
@ -106,6 +108,7 @@ export default async function (data) {
value['volumes'].push(volume); value['volumes'].push(volume);
} }
} }
console.log({ volumes, composeVolumes })
if (dockerComposeConfiguration[key]?.port) { if (dockerComposeConfiguration[key]?.port) {
value['expose'] = [dockerComposeConfiguration[key].port]; value['expose'] = [dockerComposeConfiguration[key].port];
} }

View File

@ -1633,6 +1633,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);
} }

View File

@ -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();

View File

@ -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;

View File

@ -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)}