Merge pull request #726 from coollabsio/next

v3.11.8
This commit is contained in:
Andras Bacsai 2022-11-14 14:24:52 +01:00 committed by GitHub
commit a67f633259
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 351 additions and 98 deletions

File diff suppressed because one or more lines are too long

View File

@ -1,3 +1,88 @@
- templateVersion: 1.0.0
defaultVersion: '20.0'
documentation: https://www.keycloak.org/documentation
type: keycloak
name: Keycloak
description: "Keycloak provides user federation, strong authentication, user management, fine-grained authorization, and more."
labels:
- authentication
- authorization
- oidconnect
- saml2
services:
$$id:
name: Keycloak
command: start --db=postgres
depends_on:
- $$id-postgresql
image: "quay.io/keycloak/keycloak:$$core_version"
volumes:
- $$id-import:/opt/keycloak/data/import
environment:
- KC_HEALTH_ENABLED=true
- KC_PROXY=edge
- KC_DB=postgres
- KC_HOSTNAME=$$config_keycloak_domain
- KEYCLOAK_ADMIN=$$config_admin_user
- KEYCLOAK_ADMIN_PASSWORD=$$secret_keycloak_admin_password
- KC_DB_PASSWORD=$$secret_postgres_password
- KC_DB_USERNAME=$$config_postgres_user
- KC_DB_URL=$$secret_keycloak_database_url
ports:
- '8080'
$$id-postgresql:
name: PostgreSQL
depends_on: []
image: "postgres:14-alpine"
volumes:
- "$$id-postgresql-data:/var/lib/postgresql/data"
environment:
- POSTGRES_USER=$$config_postgres_user
- POSTGRES_PASSWORD=$$secret_postgres_password
- POSTGRES_DB=$$config_postgres_db
ports: []
variables:
- id: $$config_keycloak_domain
name: KEYCLOAK_DOMAIN
label: Keycloak Domain
defaultValue: $$generate_domain
description: ""
- id: $$secret_keycloak_database_url
name: KEYCLOAK_DATABASE_URL
label: Keycloak Database Url
defaultValue: >-
jdbc:postgresql://$$id-postgresql:5432/$$config_postgres_db
description: ""
- id: $$config_admin_user
name: KEYCLOAK_ADMIN
label: Keycloak Admin User
defaultValue: $$generate_username
description: ""
- id: $$secret_keycloak_admin_password
name: KEYCLOAK_ADMIN_PASSWORD
label: Keycloak Admin Password
defaultValue: $$generate_password
description: ""
showOnConfiguration: true
- id: $$config_postgres_user
main: $$id-postgresql
name: POSTGRES_USER
label: PostgreSQL User
defaultValue: $$generate_username
description: ""
- id: $$secret_postgres_password
main: $$id-postgresql
name: POSTGRES_PASSWORD
label: PostgreSQL Password
defaultValue: $$generate_password
description: ""
showOnConfiguration: true
- id: $$config_postgres_db
main: $$id-postgresql
name: POSTGRES_DB
label: PostgreSQL Database
defaultValue: keycloak
description: ""
- templateVersion: 1.0.0 - templateVersion: 1.0.0
defaultVersion: v3.6 defaultVersion: v3.6
documentation: https://github.com/freyacodes/Lavalink documentation: https://github.com/freyacodes/Lavalink

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 {
@ -458,7 +484,7 @@ async function migrateSettings(settings: any[], service: any, template: any) {
// console.log('Migrating setting', name, value, 'for service', service.id, ', service name:', service.name, 'variableName: ', variableName) // console.log('Migrating setting', name, value, 'for service', service.id, ', service name:', service.name, 'variableName: ', variableName)
await prisma.serviceSetting.findFirst({ where: { name: minio, serviceId: service.id } }) || await prisma.serviceSetting.create({ data: { name: minio, value, variableName, service: { connect: { id: service.id } } } }) await prisma.serviceSetting.findFirst({ where: { name: minio, serviceId: service.id } }) || await prisma.serviceSetting.create({ data: { name: minio, value, variableName, service: { connect: { id: service.id } } } })
} catch(error) { } catch (error) {
console.log(error) console.log(error)
} }
} }
@ -473,7 +499,7 @@ async function migrateSecrets(secrets: any[], service: any) {
} }
// console.log('Migrating secret', name, value, 'for service', service.id, ', service name:', service.name) // console.log('Migrating secret', name, value, 'for service', service.id, ', service name:', service.name)
await prisma.serviceSecret.findFirst({ where: { name, serviceId: service.id } }) || await prisma.serviceSecret.create({ data: { name, value, service: { connect: { id: service.id } } } }) await prisma.serviceSecret.findFirst({ where: { name, serviceId: service.id } }) || await prisma.serviceSecret.create({ data: { name, value, service: { connect: { id: service.id } } } })
} catch(error) { } catch (error) {
console.log(error) console.log(error)
} }
} }

View File

@ -38,9 +38,10 @@ export default async function (data) {
if (!dockerComposeYaml.services) { if (!dockerComposeYaml.services) {
throw 'No Services found in docker-compose file.' throw 'No Services found in docker-compose file.'
} }
const envs = [ const envs = [];
`PORT=${port}` if (Object.entries(dockerComposeYaml.services).length === 1) {
]; envs.push(`PORT=${port}`)
}
if (secrets.length > 0) { if (secrets.length > 0) {
secrets.forEach((secret) => { secrets.forEach((secret) => {
if (pullmergeRequestId) { if (pullmergeRequestId) {
@ -64,19 +65,42 @@ export default async function (data) {
} catch (error) { } catch (error) {
// //
} }
const composeVolumes = volumes.map((volume) => { const composeVolumes = [];
return { if (volumes.length > 0) {
[`${volume.split(':')[0]}`]: { for (const volume of volumes) {
name: volume.split(':')[0] let [v, path] = volume.split(':');
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}`
value['env_file'] = envFound ? [`${workdir}/.env`] : [] value['env_file'] = envFound ? [`${workdir}/.env`] : []
value['labels'] = labels value['labels'] = labels
value['volumes'] = volumes // 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, '-')}`
} else {
v = `${applicationId}-${v}`
}
composeVolumes[v] = {
name: v
}
return `${v}:${path}${permission ? ':' + permission : ''}`
})
}
if (volumes.length > 0) {
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]
} }
@ -89,8 +113,11 @@ export default async function (data) {
} }
value['networks'] = [...value['networks'] || '', network] value['networks'] = [...value['networks'] || '', network]
dockerComposeYaml.services[key] = { ...dockerComposeYaml.services[key], restart: defaultComposeConfiguration(network).restart, deploy: defaultComposeConfiguration(network).deploy } dockerComposeYaml.services[key] = { ...dockerComposeYaml.services[key], restart: defaultComposeConfiguration(network).restart, deploy: defaultComposeConfiguration(network).deploy }
}
if (Object.keys(composeVolumes).length > 0) {
dockerComposeYaml['volumes'] = { ...composeVolumes }
} }
dockerComposeYaml['volumes'] = Object.assign({ ...dockerComposeYaml['volumes'] }, ...composeVolumes)
dockerComposeYaml['networks'] = Object.assign({ ...networks }, { [network]: { external: true } }) dockerComposeYaml['networks'] = Object.assign({ ...networks }, { [network]: { external: true } })
await fs.writeFile(`${workdir}/docker-compose.${isYml ? 'yml' : 'yaml'}`, yaml.dump(dockerComposeYaml)); await fs.writeFile(`${workdir}/docker-compose.${isYml ? 'yml' : 'yaml'}`, yaml.dump(dockerComposeYaml));
await executeDockerCmd({ debug, buildId, applicationId, dockerId, command: `docker compose --project-directory ${workdir} pull` }) await executeDockerCmd({ debug, buildId, applicationId, dockerId, command: `docker compose --project-directory ${workdir} pull` })

View File

@ -17,7 +17,7 @@ import { day } from './dayjs';
import { saveBuildLog } from './buildPacks/common'; import { saveBuildLog } from './buildPacks/common';
import { scheduler } from './scheduler'; import { scheduler } from './scheduler';
export const version = '3.11.7'; export const version = '3.11.8';
export const isDev = process.env.NODE_ENV === 'development'; export const isDev = process.env.NODE_ENV === 'development';
const algorithm = 'aes-256-ctr'; const algorithm = 'aes-256-ctr';

View File

@ -118,7 +118,7 @@ export async function startService(request: FastifyRequest<ServiceStartStop>, fa
entrypoint: template.services[s]?.entrypoint, entrypoint: template.services[s]?.entrypoint,
image, image,
expose: template.services[s].ports, expose: template.services[s].ports,
...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), ...(exposePort && port ? { ports: [`${exposePort}:${port}`] } : {}),
volumes: Array.from(volumes), volumes: Array.from(volumes),
environment: newEnvironments, environment: newEnvironments,
depends_on: template.services[s]?.depends_on, depends_on: template.services[s]?.depends_on,

View File

@ -242,6 +242,9 @@ export async function parseAndFindServiceTemplates(service: any, workdir?: strin
if (value) { if (value) {
strParsedTemplate = strParsedTemplate.replaceAll(regexHashed, bcrypt.hashSync(value.replaceAll("\"", "\\\""), 10)) strParsedTemplate = strParsedTemplate.replaceAll(regexHashed, bcrypt.hashSync(value.replaceAll("\"", "\\\""), 10))
strParsedTemplate = strParsedTemplate.replaceAll(regex, value.replaceAll("\"", "\\\"")) strParsedTemplate = strParsedTemplate.replaceAll(regex, value.replaceAll("\"", "\\\""))
} else {
strParsedTemplate = strParsedTemplate.replaceAll(regexHashed, '')
strParsedTemplate = strParsedTemplate.replaceAll(regex, '')
} }
} }
} }

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,8 @@
<script lang="ts"> <script lang="ts">
export let type: string; export let type: string;
export let isAbsolute = false; export let isAbsolute = false;
let fallback = '/icons/default.png';
const handleError = (ev: { target: { src: string } }) => (ev.target.src = fallback);
let extension = 'png'; let extension = 'png';
let svgs = [ let svgs = [
'languagetool', 'languagetool',
@ -46,5 +48,10 @@
</script> </script>
{#if name} {#if name}
<img class={generateClass()} src={`/icons/${name}.${extension}`} alt={`Icon of ${name}`} /> <img
class={generateClass()}
src={`/icons/${name}.${extension}`}
on:error={handleError}
alt={`Icon of ${name}`}
/>
{/if} {/if}

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

@ -501,7 +501,7 @@
</div> </div>
</div> </div>
<div <div
class="mx-auto max-w-screen-2xl px-0 lg:px-2 grid grid-cols-1" class="mx-auto max-w-screen-2xl px-0 lg:px-10 grid grid-cols-1"
class:lg:grid-cols-4={!$page.url.pathname.startsWith(`/applications/${id}/configuration/`)} class:lg:grid-cols-4={!$page.url.pathname.startsWith(`/applications/${id}/configuration/`)}
> >
{#if !$page.url.pathname.startsWith(`/applications/${id}/configuration/`)} {#if !$page.url.pathname.startsWith(`/applications/${id}/configuration/`)}

View File

@ -61,7 +61,10 @@
$isDeploymentEnabled = checkIfDeploymentEnabledApplications($appSession.isAdmin, application); $isDeploymentEnabled = checkIfDeploymentEnabledApplications($appSession.isAdmin, application);
let statues: any = {}; let statues: any = {};
let loading = false; let loading = {
save: false,
reloadCompose: false
};
let fqdnEl: any = null; let fqdnEl: any = null;
let forceSave = false; let forceSave = false;
let isPublicRepository = application.settings.isPublicRepository; let isPublicRepository = application.settings.isPublicRepository;
@ -102,7 +105,6 @@
label: 'Uvicorn' label: 'Uvicorn'
} }
]; ];
function normalizeDockerServices(services: any[]) { function normalizeDockerServices(services: any[]) {
const tempdockerComposeServices = []; const tempdockerComposeServices = [];
for (const [name, data] of Object.entries(services)) { for (const [name, data] of Object.entries(services)) {
@ -237,8 +239,8 @@
} }
} }
async function handleSubmit(toast: boolean = true) { async function handleSubmit(toast: boolean = true) {
if (loading) return; if (loading.save) return;
if (toast) loading = true; if (toast) loading.save = true;
try { try {
nonWWWDomain = application.fqdn && getDomain(application.fqdn).replace(/^www\./, ''); nonWWWDomain = application.fqdn && getDomain(application.fqdn).replace(/^www\./, '');
if (application.deploymentType) if (application.deploymentType)
@ -299,7 +301,7 @@
} }
return errorNotification(error); return errorNotification(error);
} finally { } finally {
loading = false; loading.save = false;
} }
} }
async function selectWSGI(event: any) { async function selectWSGI(event: any) {
@ -361,6 +363,8 @@
}); });
} }
async function reloadCompose() { async function reloadCompose() {
if (loading.reloadCompose) return;
loading.reloadCompose = true;
try { try {
if (application.gitSource.type === 'github') { if (application.gitSource.type === 'github') {
const headers = isPublicRepository const headers = isPublicRepository
@ -427,6 +431,8 @@
}); });
} catch (error) { } catch (error) {
errorNotification(error); errorNotification(error);
} finally {
loading.reloadCompose = false;
} }
} }
$: if ($status.application.statuses) { $: if ($status.application.statuses) {
@ -464,10 +470,10 @@
<button <button
class="btn btn-sm btn-primary" class="btn btn-sm btn-primary"
type="submit" type="submit"
class:loading class:loading={loading.save}
class:bg-orange-600={forceSave} class:bg-orange-600={forceSave}
class:hover:bg-orange-400={forceSave} class:hover:bg-orange-400={forceSave}
disabled={loading}>{$t('forms.save')}</button disabled={loading.save}>{$t('forms.save')}</button
> >
{/if} {/if}
</div> </div>
@ -993,8 +999,11 @@
<div class="title font-bold pb-3 pt-10 border-b border-coolgray-500 mb-6"> <div class="title font-bold pb-3 pt-10 border-b border-coolgray-500 mb-6">
Stack <Beta /> Stack <Beta />
{#if $appSession.isAdmin} {#if $appSession.isAdmin}
<button class="btn btn-sm btn-primary" on:click|preventDefault={reloadCompose} <button
>Reload Docker Compose File</button class="btn btn-sm btn-primary"
class:loading={loading.reloadCompose}
disabled={loading.reloadCompose}
on:click|preventDefault={reloadCompose}>Reload Docker Compose File</button
> >
{/if} {/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,31 @@
<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(':');
if (!target) {
target = volume;
volume = `${application.id}${volume.replace(/\//gi, '-')}`;
} else {
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 +56,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" class:pt-10={predefinedVolumes.length > 0}>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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -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.11.7", "version": "3.11.8",
"license": "Apache-2.0", "license": "Apache-2.0",
"repository": "github:coollabsio/coolify", "repository": "github:coollabsio/coolify",
"scripts": { "scripts": {