commit
a67f633259
File diff suppressed because one or more lines are too long
@ -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
|
||||||
|
@ -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;
|
@ -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])
|
||||||
|
@ -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] });
|
||||||
|
@ -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) {
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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` })
|
||||||
|
@ -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';
|
||||||
|
@ -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,
|
||||||
|
@ -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
@ -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}
|
||||||
|
@ -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}}'?",
|
||||||
|
@ -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>
|
||||||
|
@ -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/`)}
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
BIN
apps/ui/static/icons/default.png
Normal file
BIN
apps/ui/static/icons/default.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 141 KiB |
BIN
apps/ui/static/icons/keycloak.png
Normal file
BIN
apps/ui/static/icons/keycloak.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 10 KiB |
@ -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": {
|
||||||
|
Loading…
Reference in New Issue
Block a user