fix: application persistent storage things

This commit is contained in:
Andras Bacsai 2022-11-14 10:40:28 +01:00
parent bec6b961f3
commit b4f9d29129
10 changed files with 203 additions and 82 deletions

View File

@ -0,0 +1,45 @@
-- RedefineTables
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_Setting" (
"id" TEXT NOT NULL PRIMARY KEY,
"fqdn" TEXT,
"isAPIDebuggingEnabled" BOOLEAN DEFAULT false,
"isRegistrationEnabled" BOOLEAN NOT NULL DEFAULT false,
"dualCerts" BOOLEAN NOT NULL DEFAULT false,
"minPort" INTEGER NOT NULL DEFAULT 9000,
"maxPort" INTEGER NOT NULL DEFAULT 9100,
"proxyPassword" TEXT NOT NULL,
"proxyUser" TEXT NOT NULL,
"proxyHash" TEXT,
"proxyDefaultRedirect" TEXT,
"isAutoUpdateEnabled" BOOLEAN NOT NULL DEFAULT false,
"isDNSCheckEnabled" BOOLEAN NOT NULL DEFAULT true,
"DNSServers" TEXT,
"isTraefikUsed" BOOLEAN NOT NULL DEFAULT true,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
"ipv4" TEXT,
"ipv6" TEXT,
"arch" TEXT,
"concurrentBuilds" INTEGER NOT NULL DEFAULT 1,
"applicationStoragePathMigrationFinished" BOOLEAN NOT NULL DEFAULT false
);
INSERT INTO "new_Setting" ("DNSServers", "arch", "concurrentBuilds", "createdAt", "dualCerts", "fqdn", "id", "ipv4", "ipv6", "isAPIDebuggingEnabled", "isAutoUpdateEnabled", "isDNSCheckEnabled", "isRegistrationEnabled", "isTraefikUsed", "maxPort", "minPort", "proxyDefaultRedirect", "proxyHash", "proxyPassword", "proxyUser", "updatedAt") SELECT "DNSServers", "arch", "concurrentBuilds", "createdAt", "dualCerts", "fqdn", "id", "ipv4", "ipv6", "isAPIDebuggingEnabled", "isAutoUpdateEnabled", "isDNSCheckEnabled", "isRegistrationEnabled", "isTraefikUsed", "maxPort", "minPort", "proxyDefaultRedirect", "proxyHash", "proxyPassword", "proxyUser", "updatedAt" FROM "Setting";
DROP TABLE "Setting";
ALTER TABLE "new_Setting" RENAME TO "Setting";
CREATE UNIQUE INDEX "Setting_fqdn_key" ON "Setting"("fqdn");
CREATE TABLE "new_ApplicationPersistentStorage" (
"id" TEXT NOT NULL PRIMARY KEY,
"applicationId" TEXT NOT NULL,
"path" TEXT NOT NULL,
"oldPath" BOOLEAN NOT NULL DEFAULT false,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "ApplicationPersistentStorage_applicationId_fkey" FOREIGN KEY ("applicationId") REFERENCES "Application" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
INSERT INTO "new_ApplicationPersistentStorage" ("applicationId", "createdAt", "id", "path", "updatedAt") SELECT "applicationId", "createdAt", "id", "path", "updatedAt" FROM "ApplicationPersistentStorage";
DROP TABLE "ApplicationPersistentStorage";
ALTER TABLE "new_ApplicationPersistentStorage" RENAME TO "ApplicationPersistentStorage";
CREATE UNIQUE INDEX "ApplicationPersistentStorage_applicationId_path_key" ON "ApplicationPersistentStorage"("applicationId", "path");
PRAGMA foreign_key_check;
PRAGMA foreign_keys=ON;

View File

@ -40,6 +40,7 @@ model Setting {
ipv6 String? ipv6 String?
arch String? arch String?
concurrentBuilds Int @default(1) concurrentBuilds Int @default(1)
applicationStoragePathMigrationFinished Boolean @default(false)
} }
model User { model User {
@ -186,6 +187,7 @@ model ApplicationPersistentStorage {
id String @id @default(cuid()) id String @id @default(cuid())
applicationId String applicationId String
path String path String
oldPath Boolean @default(false)
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
application Application @relation(fields: [applicationId], references: [id]) application Application @relation(fields: [applicationId], references: [id])

View File

@ -17,7 +17,7 @@ import yaml from 'js-yaml'
import fs from 'fs/promises'; import fs from 'fs/promises';
import { verifyRemoteDockerEngineFn } from './routes/api/v1/destinations/handlers'; import { verifyRemoteDockerEngineFn } from './routes/api/v1/destinations/handlers';
import { checkContainer } from './lib/docker'; import { checkContainer } from './lib/docker';
import { migrateServicesToNewTemplate } from './lib'; import { migrateApplicationPersistentStorage, migrateServicesToNewTemplate } from './lib';
import { refreshTags, refreshTemplates } from './routes/api/v1/handlers'; import { refreshTags, refreshTemplates } from './routes/api/v1/handlers';
declare module 'fastify' { declare module 'fastify' {
@ -142,7 +142,8 @@ const host = '0.0.0.0';
await socketIOServer(fastify) await socketIOServer(fastify)
console.log(`Coolify's API is listening on ${host}:${port}`); console.log(`Coolify's API is listening on ${host}:${port}`);
migrateServicesToNewTemplate() migrateServicesToNewTemplate();
await migrateApplicationPersistentStorage();
await initServer(); await initServer();
const graceful = new Graceful({ brees: [scheduler] }); const graceful = new Graceful({ brees: [scheduler] });

View File

@ -117,8 +117,10 @@ import * as buildpacks from '../lib/buildPacks';
let domain = getDomain(fqdn); let domain = getDomain(fqdn);
const volumes = const volumes =
persistentStorage?.map((storage) => { persistentStorage?.map((storage) => {
return `${applicationId}${storage.path.replace(/\//gi, '-')}:${buildPack !== 'docker' ? '/app' : '' if (storage.oldPath) {
}${storage.path}`; return `${applicationId}${storage.path.replace(/\//gi, '-').replace('-app','')}:${storage.path}`;
}
return `${applicationId}${storage.path.replace(/\//gi, '-')}:${storage.path}`;
}) || []; }) || [];
// Previews, we need to get the source branch and set subdomain // Previews, we need to get the source branch and set subdomain
if (pullmergeRequestId) { if (pullmergeRequestId) {

View File

@ -2,6 +2,32 @@ import cuid from "cuid";
import { decrypt, encrypt, fixType, generatePassword, prisma } from "./lib/common"; import { decrypt, encrypt, fixType, generatePassword, prisma } from "./lib/common";
import { getTemplates } from "./lib/services"; import { getTemplates } from "./lib/services";
export async function migrateApplicationPersistentStorage() {
const settings = await prisma.setting.findFirst()
if (settings) {
const { id: settingsId, applicationStoragePathMigrationFinished } = settings
try {
if (!applicationStoragePathMigrationFinished) {
const applications = await prisma.application.findMany({ include: { persistentStorage: true } });
for (const application of applications) {
if (application.persistentStorage && application.persistentStorage.length > 0 && application?.buildPack !== 'docker') {
for (const storage of application.persistentStorage) {
let { id, path } = storage
if (!path.startsWith('/app')) {
path = `/app${path}`
await prisma.applicationPersistentStorage.update({ where: { id }, data: { path, oldPath: true } })
}
}
}
}
}
} catch (error) {
console.log(error)
} finally {
await prisma.setting.update({ where: { id: settingsId }, data: { applicationStoragePathMigrationFinished: true } })
}
}
}
export async function migrateServicesToNewTemplate() { export async function migrateServicesToNewTemplate() {
// This function migrates old hardcoded services to the new template based services // This function migrates old hardcoded services to the new template based services
try { try {

View File

@ -64,13 +64,14 @@ export default async function (data) {
} catch (error) { } catch (error) {
// //
} }
const composeVolumes = volumes.map((volume) => { const composeVolumes = [];
return { for (const volume of volumes) {
[`${volume.split(':')[0]}`]: { let [v, _] = volume.split(':');
name: volume.split(':')[0] 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}`
@ -79,16 +80,19 @@ 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] = volume.split(':'); let [v, path, permission] = volume.split(':');
v = `${applicationId}-${v}` v = `${applicationId}-${v}`
composeVolumes[v] = { composeVolumes[v] = {
name: v name: v
} }
return `${v}:${path}` return `${v}:${path}${permission ? ':' + permission : ''}`
}) })
} }
if (volumes.length > 0) { if (volumes.length > 0) {
value['volumes'] = [...value['volumes'] || '', volumes] for (const volume of volumes) {
value['volumes'].push(volume)
}
} }
if (dockerComposeConfiguration[key].port) { if (dockerComposeConfiguration[key].port) {
value['expose'] = [dockerComposeConfiguration[key].port] value['expose'] = [dockerComposeConfiguration[key].port]

View File

@ -159,7 +159,7 @@
"storage_saved": "Storage saved.", "storage_saved": "Storage saved.",
"storage_updated": "Storage updated.", "storage_updated": "Storage updated.",
"storage_deleted": "Storage deleted.", "storage_deleted": "Storage deleted.",
"persistent_storage_explainer": "You can specify any folder that you want to be persistent across deployments.<br><span class='text-settings '>/example</span> means it will preserve <span class='text-settings '>/app/example</span> in the container as <span class='text-settings '>/app</span> is <span class='text-settings '>the root directory</span> for your application.<br><br>This is useful for storing data such as a <span class='text-settings '>database (SQLite)</span> or a <span class='text-settings '>cache</span>." "persistent_storage_explainer": "You can specify any folder that you want to be persistent across deployments.<br><br><span class='text-settings '>/example</span> means it will preserve <span class='text-settings '>/example</span> between deployments.<br><br>Your application's data is copied to <span class='text-settings '>/app</span> inside the container, you can preserve data under it as well, like <span class='text-settings '>/app/db</span>.<br><br>This is useful for storing data such as a <span class='text-settings '>database (SQLite)</span> or a <span class='text-settings '>cache</span>."
}, },
"deployment_queued": "Deployment queued.", "deployment_queued": "Deployment queued.",
"confirm_to_delete": "Are you sure you would like to delete '{{name}}'?", "confirm_to_delete": "Are you sure you would like to delete '{{name}}'?",

View File

@ -60,49 +60,54 @@
</script> </script>
<div class="w-full lg:px-0 px-4"> <div class="w-full lg:px-0 px-4">
<div class="grid grid-col-1 lg:grid-cols-3 lg:space-x-4" class:pt-8={isNew}> {#if storage.predefined}
{#if storage.id} <div class="flex flex-col lg:flex-row gap-4 pb-2">
<div class="flex flex-col"> <input disabled readonly class="w-full" value={storage.id} />
<label for="name" class="pb-2 uppercase font-bold">Volume name</label> <input disabled readonly class="w-full" bind:value={storage.path} />
</div>
{:else}
<div class="flex gap-4 pb-2" class:pt-8={isNew}>
{#if storage.applicationId}
{#if storage.oldPath}
<input <input
disabled disabled
readonly readonly
class="w-full lg:w-64" class="w-full"
value="{storage.id}{storage.path.replace(/\//gi, '-')}" value="{storage.applicationId}{storage.path.replace(/\//gi, '-').replace('-app', '')}"
/> />
</div> {:else}
{/if}
<div class="flex flex-col">
<label for="name" class="pb-2 uppercase font-bold">{isNew ? 'New Path' : 'Path'}</label>
<input <input
class="w-full lg:w-64" disabled
readonly
class="w-full"
value="{storage.applicationId}{storage.path.replace(/\//gi, '-')}"
/>
{/if}
{/if}
<input
disabled={!isNew}
readonly={!isNew}
class="w-full"
bind:value={storage.path} bind:value={storage.path}
required required
placeholder="eg: /sqlite.db" placeholder="eg: /data"
/> />
</div>
<div class="pt-8"> <div class="flex items-center justify-center">
{#if isNew} {#if isNew}
<div class="flex items-center justify-center 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)}
>{$t('forms.add')}</button >{$t('forms.add')}</button
> >
</div> </div>
{:else} {:else}
<div class="flex flex-row items-center justify-center space-x-2 w-full lg:w-64">
<div class="flex items-center justify-center">
<button class="btn btn-sm btn-primary" on:click={() => saveStorage(false)}
>{$t('forms.set')}</button
>
</div>
<div class="flex justify-center"> <div class="flex justify-center">
<button class="btn btn-sm btn-error" on:click={removeStorage} <button class="btn btn-sm btn-error" on:click={removeStorage}
>{$t('forms.remove')}</button >{$t('forms.remove')}</button
> >
</div> </div>
</div>
{/if} {/if}
</div> </div>
</div> </div>
{/if}
</div> </div>

View File

@ -2,9 +2,11 @@
import type { Load } from '@sveltejs/kit'; import type { Load } from '@sveltejs/kit';
export const load: Load = async ({ params, stuff, url }) => { export const load: Load = async ({ params, stuff, url }) => {
try { try {
const { application } = stuff;
const response = await get(`/applications/${params.id}/storages`); const response = await get(`/applications/${params.id}/storages`);
return { return {
props: { props: {
application,
...response ...response
} }
}; };
@ -19,12 +21,26 @@
<script lang="ts"> <script lang="ts">
export let persistentStorages: any; export let persistentStorages: any;
export let application: any;
import { page } from '$app/stores'; import { page } from '$app/stores';
import Storage from './_Storage.svelte'; import Storage from './_Storage.svelte';
import { get } from '$lib/api'; import { get } from '$lib/api';
import { t } from '$lib/translations'; import { t } from '$lib/translations';
import Explainer from '$lib/components/Explainer.svelte'; import Explainer from '$lib/components/Explainer.svelte';
let composeJson = JSON.parse(application?.dockerComposeFile || '{}');
let predefinedVolumes: any[] = [];
if (composeJson?.services) {
for (const [_, service] of Object.entries(composeJson.services)) {
if (service?.volumes) {
for (const [_, volumeName] of Object.entries(service.volumes)) {
let [volume, target] = volumeName.split(':');
volume = `${application.id}-${volume}`;
predefinedVolumes.push({ id: volume, path: target, predefined: true });
}
}
}
}
const { id } = $page.params; const { id } = $page.params;
async function refreshStorage() { async function refreshStorage() {
const data = await get(`/applications/${id}/storages`); const data = await get(`/applications/${id}/storages`);
@ -35,19 +51,39 @@
<div class="w-full"> <div class="w-full">
<div class="mx-auto w-full"> <div class="mx-auto w-full">
<div class="flex flex-row border-b border-coolgray-500 mb-6 space-x-2"> <div class="flex flex-row border-b border-coolgray-500 mb-6 space-x-2">
<div class="title font-bold pb-3"> <div class="title font-bold pb-3">Persistent Volumes</div>
Persistent Volumes <Explainer </div>
position="dropdown-bottom" {#if predefinedVolumes.length > 0}
explanation={$t('application.storage.persistent_storage_explainer')} <div class="title">Predefined Volumes</div>
/> <div class="w-full lg:px-0 px-4">
<div class="grid grid-col-1 lg:grid-cols-2 py-2 gap-2">
<div class="font-bold uppercase">Volume Id</div>
<div class="font-bold uppercase">Mount Dir</div>
</div> </div>
</div> </div>
<div class="gap-4">
{#each predefinedVolumes as storage}
{#key storage.id}
<Storage on:refresh={refreshStorage} {storage} />
{/key}
{/each}
</div>
{/if}
{#if persistentStorages.length > 0}
<div class="title pt-10">Custom Volumes</div>
{/if}
{#each persistentStorages as storage} {#each persistentStorages as storage}
{#key storage.id} {#key storage.id}
<Storage on:refresh={refreshStorage} {storage} /> <Storage on:refresh={refreshStorage} {storage} />
{/key} {/key}
{/each} {/each}
<div class="title pt-10">
Add New Volume <Explainer
position="dropdown-bottom"
explanation={$t('application.storage.persistent_storage_explainer')}
/>
</div>
<Storage on:refresh={refreshStorage} isNew /> <Storage on:refresh={refreshStorage} isNew />
</div> </div>
</div> </div>