Merge remote-tracking branch 'upstream/next' into feature/glitchtip-service

# Conflicts:
#	apps/api/prisma/schema.prisma
#	apps/api/src/lib/common.ts
#	apps/api/src/lib/serviceFields.ts
#	apps/api/src/routes/api/v1/services/handlers.ts
This commit is contained in:
Guillaume Bonnet 2022-08-15 21:30:53 +02:00
commit ce2c887469
21 changed files with 933 additions and 38 deletions

2
.gitpod.Dockerfile vendored Normal file
View File

@ -0,0 +1,2 @@
FROM gitpod/workspace-node:2022-06-20-19-54-55
RUN (curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.27.0/pack-v0.27.0-linux.tgz" | tar -C /usr/local/bin/ --no-same-owner -xzv pack)

View File

@ -1,7 +1,8 @@
# This configuration file was automatically generated by Gitpod. # This configuration file was automatically generated by Gitpod.
# Please adjust to your needs (see https://www.gitpod.io/docs/config-gitpod-file) # Please adjust to your needs (see https://www.gitpod.io/docs/config-gitpod-file)
# and commit this file to your remote git repository to share the goodness with others. # and commit this file to your remote git repository to share the goodness with others.
image: gitpod/workspace-node:2022-06-20-19-54-55 image:
file: .gitpod.Dockerfile
tasks: tasks:
- init: pnpm install && pnpm db:push && pnpm db:seed - init: pnpm install && pnpm db:push && pnpm db:seed
command: pnpm dev command: pnpm dev

View File

@ -0,0 +1,22 @@
-- CreateTable
CREATE TABLE "Appwrite" (
"id" TEXT NOT NULL PRIMARY KEY,
"serviceId" TEXT NOT NULL,
"opensslKeyV1" TEXT NOT NULL,
"executorSecret" TEXT NOT NULL,
"redisPassword" TEXT NOT NULL,
"mariadbHost" TEXT,
"mariadbPort" INTEGER NOT NULL DEFAULT 3306,
"mariadbUser" TEXT NOT NULL,
"mariadbPassword" TEXT NOT NULL,
"mariadbRootUser" TEXT NOT NULL,
"mariadbRootUserPassword" TEXT NOT NULL,
"mariadbDatabase" TEXT NOT NULL,
"mariadbPublicPort" INTEGER,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "Appwrite_serviceId_fkey" FOREIGN KEY ("serviceId") REFERENCES "Service" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
-- CreateIndex
CREATE UNIQUE INDEX "Appwrite_serviceId_key" ON "Appwrite"("serviceId");

View File

@ -323,6 +323,7 @@ model Service {
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
destinationDocker DestinationDocker? @relation(fields: [destinationDockerId], references: [id]) destinationDocker DestinationDocker? @relation(fields: [destinationDockerId], references: [id])
fider Fider? fider Fider?
ghost Ghost? ghost Ghost?
glitchTip GlitchTip? glitchTip GlitchTip?
@ -336,7 +337,9 @@ model Service {
umami Umami? umami Umami?
vscodeserver Vscodeserver? vscodeserver Vscodeserver?
wordpress Wordpress? wordpress Wordpress?
teams Team[] appwrite Appwrite?
teams Team[]
} }
model PlausibleAnalytics { model PlausibleAnalytics {
@ -493,6 +496,25 @@ model Moodle {
service Service @relation(fields: [serviceId], references: [id]) service Service @relation(fields: [serviceId], references: [id])
} }
model Appwrite {
id String @id @default(cuid())
serviceId String @unique
opensslKeyV1 String
executorSecret String
redisPassword String
mariadbHost String?
mariadbPort Int @default(3306)
mariadbUser String
mariadbPassword String
mariadbRootUser String
mariadbRootUserPassword String
mariadbDatabase String
mariadbPublicPort Int?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
service Service @relation(fields: [serviceId], references: [id])
}
model GlitchTip { model GlitchTip {
id String @id @default(cuid()) id String @id @default(cuid())
postgresqlUser String postgresqlUser String

View File

@ -2,14 +2,14 @@ import { executeDockerCmd, prisma } from "../common"
import { saveBuildLog } from "./common"; import { saveBuildLog } from "./common";
export default async function (data: any): Promise<void> { export default async function (data: any): Promise<void> {
const { buildId, applicationId, tag, dockerId, debug, workdir } = data
try { try {
const { buildId, applicationId, tag, dockerId, debug, workdir } = data
await saveBuildLog({ line: `Building image started.`, buildId, applicationId }); await saveBuildLog({ line: `Building image started.`, buildId, applicationId });
const { stdout } = await executeDockerCmd({ const { stdout } = await executeDockerCmd({
dockerId, dockerId,
command: `pack build -p ${workdir} ${applicationId}:${tag} --builder heroku/buildpacks:20` command: `pack build -p ${workdir} ${applicationId}:${tag} --builder heroku/buildpacks:20`
}) })
if (debug) { if (debug) {
const array = stdout.split('\n') const array = stdout.split('\n')
for (const line of array) { for (const line of array) {
@ -24,6 +24,16 @@ export default async function (data: any): Promise<void> {
} }
await saveBuildLog({ line: `Building image successful.`, buildId, applicationId }); await saveBuildLog({ line: `Building image successful.`, buildId, applicationId });
} catch (error) { } catch (error) {
const array = error.stdout.split('\n')
for (const line of array) {
if (line !== '\n') {
await saveBuildLog({
line: `${line.replace('\n', '')}`,
buildId,
applicationId
});
}
}
throw error; throw error;
} }
} }

View File

@ -17,7 +17,7 @@ import { checkContainer, removeContainer } from './docker';
import { day } from './dayjs'; import { day } from './dayjs';
import * as serviceFields from './serviceFields' import * as serviceFields from './serviceFields'
export const version = '3.3.2'; export const version = '3.4.0';
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';
@ -78,6 +78,8 @@ export const include: any = {
umami: true, umami: true,
hasura: true, hasura: true,
fider: true, fider: true,
moodle: true,
appwrite: true,
glitchTip: true, glitchTip: true,
}; };
@ -97,17 +99,23 @@ export const base64Decode = (text: string): string => {
}; };
export const decrypt = (hashString: string) => { export const decrypt = (hashString: string) => {
if (hashString) { if (hashString) {
const hash = JSON.parse(hashString); try {
const decipher = crypto.createDecipheriv( const hash = JSON.parse(hashString);
algorithm, const decipher = crypto.createDecipheriv(
process.env['COOLIFY_SECRET_KEY'], algorithm,
Buffer.from(hash.iv, 'hex') process.env['COOLIFY_SECRET_KEY'],
); Buffer.from(hash.iv, 'hex')
const decrpyted = Buffer.concat([ );
decipher.update(Buffer.from(hash.content, 'hex')), const decrpyted = Buffer.concat([
decipher.final() decipher.update(Buffer.from(hash.content, 'hex')),
]); decipher.final()
return decrpyted.toString(); ]);
return decrpyted.toString();
} catch (error) {
console.log({ decryptionError: error.message })
return hashString
}
} }
}; };
export const encrypt = (text: string) => { export const encrypt = (text: string) => {
@ -270,6 +278,17 @@ export const supportedServiceTypesAndVersions = [
main: 3000 main: 3000
} }
}, },
{
name: 'appwrite',
fancyName: 'Appwrite',
baseImage: 'appwrite/appwrite',
images: ['mariadb:10.7', 'redis:6.2-alpine', 'appwrite/telegraf:1.4.0'],
versions: ['latest', '0.15.3'],
recommendedVersion: '0.15.3',
ports: {
main: 80
}
}
// { // {
// name: 'moodle', // name: 'moodle',
// fancyName: 'Moodle', // fancyName: 'Moodle',
@ -585,6 +604,11 @@ export async function executeDockerCmd({ dockerId, command }: { dockerId: string
} else { } else {
engine = 'unix:///var/run/docker.sock' engine = 'unix:///var/run/docker.sock'
} }
if (process.env.CODESANDBOX_HOST) {
if (command.startsWith('docker compose')) {
command = command.replace(/docker compose/gi, 'docker-compose')
}
}
return await asyncExecShell( return await asyncExecShell(
`DOCKER_BUILDKIT=1 DOCKER_HOST="${engine}" ${command}` `DOCKER_BUILDKIT=1 DOCKER_HOST="${engine}" ${command}`
); );
@ -596,6 +620,11 @@ export async function startTraefikProxy(id: string): Promise<void> {
const { id: settingsId, ipv4, ipv6 } = await listSettings(); const { id: settingsId, ipv4, ipv6 } = await listSettings();
if (!found) { if (!found) {
const { stdout: coolifyNetwork } = await executeDockerCmd({ dockerId: id, command: `docker network ls --filter 'name=coolify-infra' --no-trunc --format "{{json .}}"` })
if (!coolifyNetwork) {
await executeDockerCmd({ dockerId: id, command: `docker network create --attachable coolify-infra` })
}
const { stdout: Config } = await executeDockerCmd({ dockerId: id, command: `docker network inspect ${network} --format '{{json .IPAM.Config }}'` }) const { stdout: Config } = await executeDockerCmd({ dockerId: id, command: `docker network inspect ${network} --format '{{json .IPAM.Config }}'` })
const ip = JSON.parse(Config)[0].Gateway; const ip = JSON.parse(Config)[0].Gateway;
let traefikUrl = mainTraefikEndpoint let traefikUrl = mainTraefikEndpoint
@ -879,6 +908,11 @@ export function generateDatabaseConfiguration(database: any, arch: string):
} }
if (isARM(arch)) { if (isARM(arch)) {
configuration.volume = `${id}-${type}-data:/var/lib/postgresql`; configuration.volume = `${id}-${type}-data:/var/lib/postgresql`;
configuration.environmentVariables = {
POSTGRES_PASSWORD: dbUserPassword,
POSTGRES_USER: dbUser,
POSTGRES_DB: defaultDatabase
}
} }
return configuration return configuration
} else if (type === 'redis') { } else if (type === 'redis') {
@ -915,7 +949,7 @@ export function generateDatabaseConfiguration(database: any, arch: string):
return configuration return configuration
} }
} }
export function isARM(arch) { export function isARM(arch: string) {
if (arch === 'arm' || arch === 'arm64') { if (arch === 'arm' || arch === 'arm64') {
return true return true
} }
@ -1238,7 +1272,6 @@ export async function startTraefikTCPProxy(
} }
traefikUrl = `${ip}/webhooks/traefik/other.json` traefikUrl = `${ip}/webhooks/traefik/other.json`
} }
console.log(traefikUrl)
const tcpProxy = { const tcpProxy = {
version: '3.8', version: '3.8',
services: { services: {
@ -1303,6 +1336,7 @@ export async function getServiceFromDB({ id, teamId }: { id: string; teamId: str
return s; return s;
}); });
} }
body[type] = { ...body[type], ...getUpdateableFields(type, body[type]) } body[type] = { ...body[type], ...getUpdateableFields(type, body[type]) }
return { ...body, settings }; return { ...body, settings };
} }
@ -1529,6 +1563,35 @@ export async function configureServiceType({
} }
} }
}); });
} else if (type === 'appwrite') {
const opensslKeyV1 = encrypt(generatePassword());
const executorSecret = encrypt(generatePassword());
const redisPassword = encrypt(generatePassword());
const mariadbHost = `${id}-mariadb`
const mariadbUser = cuid();
const mariadbPassword = encrypt(generatePassword());
const mariadbDatabase = 'appwrite';
const mariadbRootUser = cuid();
const mariadbRootUserPassword = encrypt(generatePassword());
await prisma.service.update({
where: { id },
data: {
type,
appwrite: {
create: {
opensslKeyV1,
executorSecret,
redisPassword,
mariadbHost,
mariadbUser,
mariadbPassword,
mariadbDatabase,
mariadbRootUser,
mariadbRootUserPassword
}
}
}
});
} else if (type === 'glitchTip') { } else if (type === 'glitchTip') {
const defaultUsername = cuid(); const defaultUsername = cuid();
const defaultEmail = `${defaultUsername}@example.com`; const defaultEmail = `${defaultUsername}@example.com`;
@ -1566,6 +1629,7 @@ export async function configureServiceType({
} }
export async function removeService({ id }: { id: string }): Promise<void> { export async function removeService({ id }: { id: string }): Promise<void> {
await prisma.serviceSecret.deleteMany({ where: { serviceId: id } });
await prisma.servicePersistentStorage.deleteMany({ where: { serviceId: id } }); await prisma.servicePersistentStorage.deleteMany({ where: { serviceId: id } });
await prisma.meiliSearch.deleteMany({ where: { serviceId: id } }); await prisma.meiliSearch.deleteMany({ where: { serviceId: id } });
await prisma.fider.deleteMany({ where: { serviceId: id } }); await prisma.fider.deleteMany({ where: { serviceId: id } });
@ -1577,8 +1641,8 @@ export async function removeService({ id }: { id: string }): Promise<void> {
await prisma.vscodeserver.deleteMany({ where: { serviceId: id } }); await prisma.vscodeserver.deleteMany({ where: { serviceId: id } });
await prisma.wordpress.deleteMany({ where: { serviceId: id } }); await prisma.wordpress.deleteMany({ where: { serviceId: id } });
await prisma.glitchTip.deleteMany({ where: { serviceId: id } }); await prisma.glitchTip.deleteMany({ where: { serviceId: id } });
await prisma.serviceSecret.deleteMany({ where: { serviceId: id } }); await prisma.moodle.deleteMany({ where: { serviceId: id } });
await prisma.appwrite.deleteMany({ where: { serviceId: id } });
await prisma.service.delete({ where: { id } }); await prisma.service.delete({ where: { id } });
} }
@ -1643,9 +1707,9 @@ export const getServiceMainPort = (service: string) => {
export function makeLabelForServices(type) { export function makeLabelForServices(type) {
return [ return [
'coolify.managed=true', 'coolify.managed=true',
`coolify.version = ${version} `, `coolify.version = ${version}`,
`coolify.type = service`, `coolify.type = service`,
`coolify.service.type = ${type} ` `coolify.service.type = ${type}`
]; ];
} }
export function errorHandler({ status = 500, message = 'Unknown error.' }: { status: number, message: string | any }) { export function errorHandler({ status = 500, message = 'Unknown error.' }: { status: number, message: string | any }) {
@ -1717,7 +1781,7 @@ export function convertTolOldVolumeNames(type) {
// export async function getAvailableServices(): Promise<any> { // export async function getAvailableServices(): Promise<any> {
// const { data } = await axios.get(`https://gist.githubusercontent.com/andrasbacsai/4aac36d8d6214dbfc34fa78110554a50/raw/5b27e6c37d78aaeedc1148d797112c827a2f43cf/availableServices.json`) // const { data } = await axios.get(`https://gist.githubusercontent.com/andrasbacsai/4aac36d8d6214dbfc34fa78110554a50/raw/5b27e6c37d78aaeedc1148d797112c827a2f43cf/availableServices.json`)
// return data // return data
// //
export async function cleanupDockerStorage(dockerId, lowDiskSpace, force) { export async function cleanupDockerStorage(dockerId, lowDiskSpace, force) {
// Cleanup old coolify images // Cleanup old coolify images
try { try {
@ -1780,4 +1844,4 @@ export function persistentVolumes(id, persistentStorage, config) {
...composeVolumes ...composeVolumes
) || {} ) || {}
return { volumes, volumeMounts } return { volumes, volumeMounts }
} }

View File

@ -16,6 +16,7 @@ export function formatLabelsOnDocker(data) {
export async function checkContainer({ dockerId, container, remove = false }: { dockerId: string, container: string, remove?: boolean }): Promise<boolean> { export async function checkContainer({ dockerId, container, remove = false }: { dockerId: string, container: string, remove?: boolean }): Promise<boolean> {
let containerFound = false; let containerFound = false;
try { try {
console.log('checking ', container)
const { stdout } = await executeDockerCmd({ const { stdout } = await executeDockerCmd({
dockerId, dockerId,
command: command:
@ -71,7 +72,7 @@ export async function removeContainer({
}): Promise<void> { }): Promise<void> {
try { try {
const { stdout } = await executeDockerCmd({ dockerId, command: `docker inspect --format '{{json .State}}' ${id}` }) const { stdout } = await executeDockerCmd({ dockerId, command: `docker inspect --format '{{json .State}}' ${id}` })
console.log(id)
if (JSON.parse(stdout).Running) { if (JSON.parse(stdout).Running) {
await executeDockerCmd({ dockerId, command: `docker stop -t 0 ${id}` }) await executeDockerCmd({ dockerId, command: `docker stop -t 0 ${id}` })
await executeDockerCmd({ dockerId, command: `docker rm ${id}` }) await executeDockerCmd({ dockerId, command: `docker rm ${id}` })

View File

@ -326,7 +326,7 @@ export const fider = [{
isBoolean: false, isBoolean: false,
isEncrypted: true isEncrypted: true
}, { }, {
name: 'postgreslUser', name: 'postgresqlUser',
isEditable: false, isEditable: false,
isLowerCase: false, isLowerCase: false,
isNumber: false, isNumber: false,
@ -477,6 +477,88 @@ export const moodle = [{
isBoolean: false, isBoolean: false,
isEncrypted: false isEncrypted: false
}] }]
export const appwrite = [{
name: 'opensslKeyV1',
isEditable: false,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: true
},
{
name: 'executorSecret',
isEditable: false,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: true
},
{
name: 'redisPassword',
isEditable: false,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: true
},
{
name: 'mariadbHost',
isEditable: false,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: false
},
{
name: 'mariadbPort',
isEditable: false,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: false
},
{
name: 'mariadbUser',
isEditable: false,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: false
},
{
name: 'mariadbPassword',
isEditable: false,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: true
},
{
name: 'mariadbRootUser',
isEditable: false,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: false
},
{
name: 'mariadbRootUserPassword',
isEditable: false,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: true
},
{
name: 'mariadbDatabase',
isEditable: true,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: false
}]
export const glitchTip = [{ export const glitchTip = [{
name: 'postgresqlUser', name: 'postgresqlUser',
isEditable: false, isEditable: false,
@ -588,4 +670,4 @@ export const glitchTip = [{
isNumber: false, isNumber: false,
isBoolean: true, isBoolean: true,
isEncrypted: false isEncrypted: false
}] }]

View File

@ -0,0 +1,35 @@
import { createDirectories, getServiceFromDB, getServiceImage, getServiceMainPort, makeLabelForServices } from "./common";
export async function defaultServiceConfigurations({ id, teamId }) {
const service = await getServiceFromDB({ id, teamId });
const { destinationDockerId, destinationDocker, type, serviceSecret } = service;
const network = destinationDockerId && destinationDocker.network;
const port = getServiceMainPort(type);
const { workdir } = await createDirectories({ repository: type, buildId: id });
const image = getServiceImage(type);
let secrets = [];
if (serviceSecret.length > 0) {
serviceSecret.forEach((secret) => {
secrets.push([secret.name]=secret.value);
});
}
return { ...service, network, port, workdir, image, secrets }
}
export function defaultServiceComposeConfiguration(network: string) {
return {
networks: [network],
restart: 'always',
deploy: {
restart_policy: {
condition: 'on-failure',
delay: '10s',
max_attempts: 10,
window: '120s'
}
}
}
}

View File

@ -9,6 +9,7 @@ import cuid from 'cuid';
import type { OnlyId } from '../../../../types'; import type { OnlyId } from '../../../../types';
import type { ActivateWordpressFtp, CheckService, CheckServiceDomain, DeleteServiceSecret, DeleteServiceStorage, GetServiceLogs, SaveService, SaveServiceDestination, SaveServiceSecret, SaveServiceSettings, SaveServiceStorage, SaveServiceType, SaveServiceVersion, ServiceStartStop, SetWordpressSettings } from './types'; import type { ActivateWordpressFtp, CheckService, CheckServiceDomain, DeleteServiceSecret, DeleteServiceStorage, GetServiceLogs, SaveService, SaveServiceDestination, SaveServiceSecret, SaveServiceSettings, SaveServiceStorage, SaveServiceType, SaveServiceVersion, ServiceStartStop, SetWordpressSettings } from './types';
import { defaultServiceComposeConfiguration, defaultServiceConfigurations } from '../../../../lib/services';
// async function startServiceNew(request: FastifyRequest<OnlyId>) { // async function startServiceNew(request: FastifyRequest<OnlyId>) {
// try { // try {
@ -29,7 +30,7 @@ import type { ActivateWordpressFtp, CheckService, CheckServiceDomain, DeleteServ
// serviceSecret.forEach((secret) => { // serviceSecret.forEach((secret) => {
// environmentVariables[secret.name] = secret.value; // environmentVariables[secret.name] = secret.value;
// }); // });
// } // }
// config.newVolumes = {} // config.newVolumes = {}
// for (const service of Object.entries(config.services)) { // for (const service of Object.entries(config.services)) {
// const name = service[0] // const name = service[0]
@ -97,7 +98,7 @@ import type { ActivateWordpressFtp, CheckService, CheckServiceDomain, DeleteServ
// } // }
// console.log(config.services) // console.log(config.services)
// console.log(config.volumes) // console.log(config.volumes)
// // config.services[id] = JSON.parse(JSON.stringify(config.services[type])) // // config.services[id] = JSON.parse(JSON.stringify(config.services[type]))
// // config.services[id].container_name = id // // config.services[id].container_name = id
@ -197,13 +198,11 @@ export async function getService(request: FastifyRequest<OnlyId>) {
const teamId = request.user.teamId; const teamId = request.user.teamId;
const { id } = request.params; const { id } = request.params;
const service = await getServiceFromDB({ id, teamId }); const service = await getServiceFromDB({ id, teamId });
const settings = await listSettings()
if (!service) { if (!service) {
throw { status: 404, message: 'Service not found.' } throw { status: 404, message: 'Service not found.' }
} }
return { return {
service, service
settings
} }
} catch ({ status, message }) { } catch ({ status, message }) {
return errorHandler({ status, message }) return errorHandler({ status, message })
@ -590,6 +589,9 @@ export async function startService(request: FastifyRequest<ServiceStartStop>) {
if (type === 'moodle') { if (type === 'moodle') {
return await startMoodleService(request) return await startMoodleService(request)
} }
if (type === 'appwrite') {
return await startAppWriteService(request)
}
if (type === 'glitchTip') { if (type === 'glitchTip') {
return await startGlitchTipService(request) return await startGlitchTipService(request)
} }
@ -643,6 +645,9 @@ export async function stopService(request: FastifyRequest<ServiceStartStop>) {
if (type === 'fider') { if (type === 'fider') {
return await stopFiderService(request) return await stopFiderService(request)
} }
if (type === 'appwrite') {
return await stopAppWriteService(request)
}
if (type === 'moodle') { if (type === 'moodle') {
return await stopMoodleService(request) return await stopMoodleService(request)
} }
@ -2480,7 +2485,511 @@ async function stopFiderService(request: FastifyRequest<ServiceStartStop>) {
return errorHandler({ status, message }) return errorHandler({ status, message })
} }
} }
async function startAppWriteService(request: FastifyRequest<ServiceStartStop>) {
try {
const { id } = request.params;
const teamId = request.user.teamId;
const { version, fqdn, destinationDocker, secrets, exposePort, network, port, workdir, image, appwrite } = await defaultServiceConfigurations({ id, teamId })
let isStatsEnabled = false
if (secrets._APP_USAGE_STATS) {
isStatsEnabled = true
}
const {
opensslKeyV1,
executorSecret,
mariadbHost,
mariadbPort,
mariadbUser,
mariadbPassword,
mariadbRootUser,
mariadbRootUserPassword,
mariadbDatabase
} = appwrite;
const dockerCompose = {
[id]: {
...defaultServiceComposeConfiguration(network),
image: `${image}:${version}`,
container_name: id,
labels: makeLabelForServices('appwrite'),
...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}),
"volumes": [
`${id}-uploads:/storage/uploads:rw`,
`${id}-cache:/storage/cache:rw`,
`${id}-config:/storage/config:rw`,
`${id}-certificates:/storage/certificates:rw`,
`${id}-functions:/storage/functions:rw`
],
"depends_on": [
`${id}-mariadb`,
`${id}-redis`,
],
"environment": [
"_APP_ENV=production",
"_APP_LOCALE=en",
`_APP_OPENSSL_KEY_V1=${opensslKeyV1}`,
`_APP_DOMAIN=${fqdn}`,
`_APP_DOMAIN_TARGET=${fqdn}`,
`_APP_REDIS_HOST=${id}-redis`,
"_APP_REDIS_PORT=6379",
`_APP_DB_HOST=${mariadbHost}`,
`_APP_DB_PORT=${mariadbPort}`,
`_APP_DB_SCHEMA=${mariadbDatabase}`,
`_APP_DB_USER=${mariadbUser}`,
`_APP_DB_PASS=${mariadbPassword}`,
`_APP_INFLUXDB_HOST=${id}-influxdb`,
"_APP_INFLUXDB_PORT=8806",
`_APP_EXECUTOR_SECRET=${executorSecret}`,
`_APP_EXECUTOR_HOST=http://${id}-executor/v1`,
`_APP_STATSD_HOST=${id}-telegraf`,
"_APP_STATSD_PORT=8125",
...secrets
]
},
[`${id}-realtime`]: {
...defaultServiceComposeConfiguration(network),
image: `${image}:${version}`,
container_name: `${id}-realtime`,
entrypoint: "realtime",
labels: makeLabelForServices('appwrite'),
"depends_on": [
`${id}-mariadb`,
`${id}-redis`,
],
"environment": [
"_APP_ENV=production",
`_APP_OPENSSL_KEY_V1=${opensslKeyV1}`,
`_APP_REDIS_HOST=${id}-redis`,
"_APP_REDIS_PORT=6379",
`_APP_DB_HOST=${mariadbHost}`,
`_APP_DB_PORT=${mariadbPort}`,
`_APP_DB_SCHEMA=${mariadbDatabase}`,
`_APP_DB_USER=${mariadbUser}`,
`_APP_DB_PASS=${mariadbPassword}`,
...secrets
]
},
[`${id}-worker-audits`]: {
...defaultServiceComposeConfiguration(network),
image: `${image}:${version}`,
container_name: `${id}-worker-audits`,
labels: makeLabelForServices('appwrite'),
"entrypoint": "worker-audits",
"depends_on": [
`${id}-mariadb`,
`${id}-redis`,
],
"environment": [
"_APP_ENV=production",
`_APP_OPENSSL_KEY_V1=${opensslKeyV1}`,
`_APP_REDIS_HOST=${id}-redis`,
"_APP_REDIS_PORT=6379",
`_APP_DB_HOST=${mariadbHost}`,
`_APP_DB_PORT=${mariadbPort}`,
`_APP_DB_SCHEMA=${mariadbDatabase}`,
`_APP_DB_USER=${mariadbUser}`,
`_APP_DB_PASS=${mariadbPassword}`,
...secrets
]
},
[`${id}-worker-webhooks`]: {
...defaultServiceComposeConfiguration(network),
image: `${image}:${version}`,
container_name: `${id}-worker-webhooks`,
labels: makeLabelForServices('appwrite'),
"entrypoint": "worker-webhooks",
"depends_on": [
`${id}-mariadb`,
`${id}-redis`,
],
"environment": [
"_APP_ENV=production",
`_APP_OPENSSL_KEY_V1=${opensslKeyV1}`,
`_APP_REDIS_HOST=${id}-redis`,
"_APP_REDIS_PORT=6379",
...secrets
]
},
[`${id}-worker-deletes`]: {
...defaultServiceComposeConfiguration(network),
image: `${image}:${version}`,
container_name: `${id}-worker-deletes`,
labels: makeLabelForServices('appwrite'),
"entrypoint": "worker-deletes",
"depends_on": [
`${id}-mariadb`,
`${id}-redis`,
],
"volumes": [
`${id}-uploads:/storage/uploads:rw`,
`${id}-cache:/storage/cache:rw`,
`${id}-config:/storage/config:rw`,
`${id}-certificates:/storage/certificates:rw`,
`${id}-functions:/storage/functions:rw`,
`${id}-builds:/storage/builds:rw`,
],
"environment": [
"_APP_ENV=production",
`_APP_OPENSSL_KEY_V1=${opensslKeyV1}`,
`_APP_REDIS_HOST=${id}-redis`,
"_APP_REDIS_PORT=6379",
`_APP_DB_HOST=${mariadbHost}`,
`_APP_DB_PORT=${mariadbPort}`,
`_APP_DB_SCHEMA=${mariadbDatabase}`,
`_APP_DB_USER=${mariadbUser}`,
`_APP_DB_PASS=${mariadbPassword}`,
`_APP_EXECUTOR_SECRET=${executorSecret}`,
`_APP_EXECUTOR_HOST=http://${id}-executor/v1`,
...secrets
]
},
[`${id}-worker-databases`]: {
...defaultServiceComposeConfiguration(network),
image: `${image}:${version}`,
container_name: `${id}-worker-databases`,
labels: makeLabelForServices('appwrite'),
"entrypoint": "worker-databases",
"depends_on": [
`${id}-mariadb`,
`${id}-redis`,
],
"environment": [
"_APP_ENV=production",
`_APP_OPENSSL_KEY_V1=${opensslKeyV1}`,
`_APP_REDIS_HOST=${id}-redis`,
"_APP_REDIS_PORT=6379",
`_APP_DB_HOST=${mariadbHost}`,
`_APP_DB_PORT=${mariadbPort}`,
`_APP_DB_SCHEMA=${mariadbDatabase}`,
`_APP_DB_USER=${mariadbUser}`,
`_APP_DB_PASS=${mariadbPassword}`,
...secrets
]
},
[`${id}-worker-builds`]: {
...defaultServiceComposeConfiguration(network),
image: `${image}:${version}`,
container_name: `${id}-worker-builds`,
labels: makeLabelForServices('appwrite'),
"entrypoint": "worker-builds",
"depends_on": [
`${id}-mariadb`,
`${id}-redis`,
],
"environment": [
"_APP_ENV=production",
`_APP_OPENSSL_KEY_V1=${opensslKeyV1}`,
`_APP_EXECUTOR_SECRET=${executorSecret}`,
`_APP_EXECUTOR_HOST=http://${id}-executor/v1`,
`_APP_REDIS_HOST=${id}-redis`,
"_APP_REDIS_PORT=6379",
`_APP_DB_HOST=${mariadbHost}`,
`_APP_DB_PORT=${mariadbPort}`,
`_APP_DB_SCHEMA=${mariadbDatabase}`,
`_APP_DB_USER=${mariadbUser}`,
`_APP_DB_PASS=${mariadbPassword}`,
...secrets
]
},
[`${id}-worker-certificates`]: {
...defaultServiceComposeConfiguration(network),
image: `${image}:${version}`,
container_name: `${id}-worker-certificates`,
labels: makeLabelForServices('appwrite'),
"entrypoint": "worker-certificates",
"depends_on": [
`${id}-mariadb`,
`${id}-redis`,
],
"volumes": [
`${id}-config:/storage/config:rw`,
`${id}-certificates:/storage/certificates:rw`,
],
"environment": [
"_APP_ENV=production",
`_APP_OPENSSL_KEY_V1=${opensslKeyV1}`,
`_APP_DOMAIN=${fqdn}`,
`_APP_DOMAIN_TARGET=${fqdn}`,
`_APP_REDIS_HOST=${id}-redis`,
"_APP_REDIS_PORT=6379",
`_APP_DB_HOST=${mariadbHost}`,
`_APP_DB_PORT=${mariadbPort}`,
`_APP_DB_SCHEMA=${mariadbDatabase}`,
`_APP_DB_USER=${mariadbUser}`,
`_APP_DB_PASS=${mariadbPassword}`,
...secrets
]
},
[`${id}-worker-functions`]: {
...defaultServiceComposeConfiguration(network),
image: `${image}:${version}`,
container_name: `${id}-worker-functions`,
labels: makeLabelForServices('appwrite'),
"entrypoint": "worker-functions",
"depends_on": [
`${id}-mariadb`,
`${id}-redis`,
`${id}-executor`
],
"environment": [
"_APP_ENV=production",
`_APP_OPENSSL_KEY_V1=${opensslKeyV1}`,
`_APP_REDIS_HOST=${id}-redis`,
"_APP_REDIS_PORT=6379",
`_APP_DB_HOST=${mariadbHost}`,
`_APP_DB_PORT=${mariadbPort}`,
`_APP_DB_SCHEMA=${mariadbDatabase}`,
`_APP_DB_USER=${mariadbUser}`,
`_APP_DB_PASS=${mariadbPassword}`,
`_APP_EXECUTOR_SECRET=${executorSecret}`,
`_APP_EXECUTOR_HOST=http://${id}-executor/v1`,
...secrets
]
},
[`${id}-executor`]: {
...defaultServiceComposeConfiguration(network),
image: `${image}:${version}`,
container_name: `${id}-executor`,
labels: makeLabelForServices('appwrite'),
"entrypoint": "executor",
"stop_signal": "SIGINT",
"volumes": [
`${id}-functions:/storage/functions:rw`,
`${id}-builds:/storage/builds:rw`,
"/var/run/docker.sock:/var/run/docker.sock",
"/tmp:/tmp:rw"
],
"depends_on": [
`${id}-mariadb`,
`${id}-redis`,
`${id}`
],
"environment": [
"_APP_ENV=production",
`_APP_EXECUTOR_SECRET=${executorSecret}`,
...secrets
]
},
[`${id}-worker-mails`]: {
...defaultServiceComposeConfiguration(network),
image: `${image}:${version}`,
container_name: `${id}-worker-mails`,
labels: makeLabelForServices('appwrite'),
"entrypoint": "worker-mails",
"depends_on": [
`${id}-redis`,
],
"environment": [
"_APP_ENV=production",
`_APP_OPENSSL_KEY_V1=${opensslKeyV1}`,
`_APP_REDIS_HOST=${id}-redis`,
"_APP_REDIS_PORT=6379",
...secrets
]
},
[`${id}-worker-messaging`]: {
...defaultServiceComposeConfiguration(network),
image: `${image}:${version}`,
container_name: `${id}-worker-messaging`,
labels: makeLabelForServices('appwrite'),
"entrypoint": "worker-messaging",
"depends_on": [
`${id}-redis`,
],
"environment": [
"_APP_ENV=production",
`_APP_REDIS_HOST=${id}-redis`,
"_APP_REDIS_PORT=6379",
...secrets
]
},
[`${id}-maintenance`]: {
...defaultServiceComposeConfiguration(network),
image: `${image}:${version}`,
container_name: `${id}-maintenance`,
labels: makeLabelForServices('appwrite'),
"entrypoint": "maintenance",
"depends_on": [
`${id}-redis`,
],
"environment": [
"_APP_ENV=production",
`_APP_OPENSSL_KEY_V1=${opensslKeyV1}`,
`_APP_DOMAIN=${fqdn}`,
`_APP_DOMAIN_TARGET=${fqdn}`,
`_APP_REDIS_HOST=${id}-redis`,
"_APP_REDIS_PORT=6379",
`_APP_DB_HOST=${mariadbHost}`,
`_APP_DB_PORT=${mariadbPort}`,
`_APP_DB_SCHEMA=${mariadbDatabase}`,
`_APP_DB_USER=${mariadbUser}`,
`_APP_DB_PASS=${mariadbPassword}`,
...secrets
]
},
[`${id}-schedule`]: {
...defaultServiceComposeConfiguration(network),
image: `${image}:${version}`,
container_name: `${id}-schedule`,
labels: makeLabelForServices('appwrite'),
"entrypoint": "schedule",
"depends_on": [
`${id}-redis`,
],
"environment": [
"_APP_ENV=production",
`_APP_REDIS_HOST=${id}-redis`,
"_APP_REDIS_PORT=6379",
...secrets
]
},
[`${id}-mariadb`]: {
...defaultServiceComposeConfiguration(network),
"image": "mariadb:10.7",
container_name: `${id}-mariadb`,
labels: makeLabelForServices('appwrite'),
"volumes": [
`${id}-mariadb:/var/lib/mysql:rw`
],
"environment": [
`MYSQL_ROOT_USER=${mariadbRootUser}`,
`MYSQL_ROOT_PASSWORD=${mariadbRootUserPassword}`,
`MYSQL_USER=${mariadbUser}`,
`MYSQL_PASSWORD=${mariadbPassword}`,
`MYSQL_DATABASE=${mariadbDatabase}`
],
"command": "mysqld --innodb-flush-method=fsync"
},
[`${id}-redis`]: {
...defaultServiceComposeConfiguration(network),
"image": "redis:6.2-alpine",
container_name: `${id}-redis`,
"command": `redis-server --maxmemory 512mb --maxmemory-policy allkeys-lru --maxmemory-samples 5\n`,
"volumes": [
`${id}-redis:/data:rw`
]
},
};
if (isStatsEnabled) {
dockerCompose.id.depends_on.push(`${id}-influxdb`);
dockerCompose[`${id}-usage`] = {
...defaultServiceComposeConfiguration(network),
image: `${image}:${version}`,
container_name: `${id}-usage`,
labels: makeLabelForServices('appwrite'),
"entrypoint": "usage",
"depends_on": [
`${id}-mariadb`,
`${id}-influxdb`,
],
"environment": [
"_APP_ENV=production",
`_APP_OPENSSL_KEY_V1=${opensslKeyV1}`,
`_APP_DB_HOST=${mariadbHost}`,
`_APP_DB_PORT=${mariadbPort}`,
`_APP_DB_SCHEMA=${mariadbDatabase}`,
`_APP_DB_USER=${mariadbUser}`,
`_APP_DB_PASS=${mariadbPassword}`,
`_APP_INFLUXDB_HOST=${id}-influxdb`,
"_APP_INFLUXDB_PORT=8806",
`_APP_REDIS_HOST=${id}-redis`,
"_APP_REDIS_PORT=6379",
...secrets
]
}
dockerCompose[`${id}-influxdb`] = {
...defaultServiceComposeConfiguration(network),
"image": "appwrite/influxdb:1.5.0",
container_name: `${id}-influxdb`,
"volumes": [
`${id}-influxdb:/var/lib/influxdb:rw`
]
}
dockerCompose[`${id}-telegraf`] = {
...defaultServiceComposeConfiguration(network),
"image": "appwrite/telegraf:1.4.0",
container_name: `${id}-telegraf`,
"environment": [
`_APP_INFLUXDB_HOST=${id}-influxdb`,
"_APP_INFLUXDB_PORT=8806",
]
}
}
const composeFile: any = {
version: '3.8',
services: dockerCompose,
networks: {
[network]: {
external: true
}
},
volumes: {
[`${id}-uploads`]: {
name: `${id}-uploads`
},
[`${id}-cache`]: {
name: `${id}-cache`
},
[`${id}-config`]: {
name: `${id}-config`
},
[`${id}-certificates`]: {
name: `${id}-certificates`
},
[`${id}-functions`]: {
name: `${id}-functions`
},
[`${id}-builds`]: {
name: `${id}-builds`
},
[`${id}-mariadb`]: {
name: `${id}-mariadb`
},
[`${id}-redis`]: {
name: `${id}-redis`
},
[`${id}-influxdb`]: {
name: `${id}-influxdb`
}
}
};
const composeFileDestination = `${workdir}/docker-compose.yaml`;
await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} pull` })
await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} up --build -d` })
return {}
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
async function stopAppWriteService(request: FastifyRequest<ServiceStartStop>) {
try {
// TODO: Fix async for of
const { id } = request.params;
const teamId = request.user.teamId;
const service = await getServiceFromDB({ id, teamId });
const { destinationDockerId, destinationDocker } = service;
const containers = [`${id}-mariadb`, `${id}-redis`, `${id}-influxdb`, `${id}-telegraf`, id, `${id}-realtime`, `${id}-worker-audits`, `${id}worker-webhooks`, `${id}-worker-deletes`, `${id}-worker-databases`, `${id}-worker-builds`, `${id}-worker-certificates`, `${id}-worker-functions`, `${id}-worker-mails`, `${id}-worker-messaging`, `${id}-maintenance`, `${id}-schedule`, `${id}-executor`, `${id}-usage`]
if (destinationDockerId) {
for (const container of containers) {
const found = await checkContainer({ dockerId: destinationDocker.id, container });
if (found) {
await removeContainer({ id, dockerId: destinationDocker.id });
}
}
}
return {}
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
async function startMoodleService(request: FastifyRequest<ServiceStartStop>) { async function startMoodleService(request: FastifyRequest<ServiceStartStop>) {
try { try {
const { id } = request.params; const { id } = request.params;

View File

@ -148,6 +148,17 @@ export const supportedServiceTypesAndVersions = [
main: 3000 main: 3000
} }
}, },
{
name: 'appwrite',
fancyName: 'Appwrite',
baseImage: 'appwrite/appwrite',
images: ['mariadb:10.7', 'redis:6.2-alpine', 'appwrite/telegraf:1.4.0'],
versions: ['latest', '0.15.3'],
recommendedVersion: '0.15.3',
ports: {
main: 80
}
}
// { // {
// name: 'moodle', // name: 'moodle',
// fancyName: 'Moodle', // fancyName: 'Moodle',

View File

@ -13,5 +13,6 @@ export { default as MeiliSearch } from './MeiliSearch.svelte';
export { default as Umami } from './Umami.svelte'; export { default as Umami } from './Umami.svelte';
export { default as Hasura } from './Hasura.svelte'; export { default as Hasura } from './Hasura.svelte';
export { default as Fider } from './Fider.svelte'; export { default as Fider } from './Fider.svelte';
export { default as Appwrite } from './Moodle.svelte';
export { default as Moodle } from './Moodle.svelte'; export { default as Moodle } from './Moodle.svelte';
export { default as GlitchTip } from './GlitchTip.svelte'; export { default as GlitchTip } from './GlitchTip.svelte';

View File

@ -292,8 +292,7 @@ export const buildPacks = [
fancyName: 'Deno', fancyName: 'Deno',
hoverColor: 'hover:bg-green-700', hoverColor: 'hover:bg-green-700',
color: 'bg-green-700' color: 'bg-green-700'
} },
// },
// { // {
// name: 'heroku', // name: 'heroku',
// fancyName: 'Heroku Buildpack', // fancyName: 'Heroku Buildpack',

View File

@ -59,7 +59,7 @@
async function changeSettings(name: any) { async function changeSettings(name: any) {
if (name !== 'appendOnly') { if (name !== 'appendOnly') {
if (publicLoading || !$status.database.isRunning || name !== 'appendOnly') return; if (publicLoading || !$status.database.isRunning) return;
} }
publicLoading = true; publicLoading = true;
let data = { let data = {
@ -247,6 +247,7 @@
{#if database.type === 'redis'} {#if database.type === 'redis'}
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<Setting <Setting
loading={publicLoading}
bind:setting={appendOnly} bind:setting={appendOnly}
on:click={() => changeSettings('appendOnly')} on:click={() => changeSettings('appendOnly')}
title={$t('database.change_append_only_mode')} title={$t('database.change_append_only_mode')}

View File

@ -97,6 +97,7 @@
<div class="mt-10 pb-12 tracking-tight sm:pb-16"> <div class="mt-10 pb-12 tracking-tight sm:pb-16">
<div class="mx-auto px-10"> <div class="mx-auto px-10">
<div class="flex flex-col justify-center xl:flex-row"> <div class="flex flex-col justify-center xl:flex-row">
{#if applications.length > 0}
<div> <div>
<div class="title">Resources</div> <div class="title">Resources</div>
<div class="flex items-start justify-center p-8"> <div class="flex items-start justify-center p-8">
@ -308,6 +309,7 @@
</table> </table>
</div> </div>
</div> </div>
{/if}
{#if $appSession.teamId === '0'} {#if $appSession.teamId === '0'}
<Usage /> <Usage />
{/if} {/if}

View File

@ -55,6 +55,10 @@
<a href="https://fider.io" target="_blank"> <a href="https://fider.io" target="_blank">
<Icons.Fider /> <Icons.Fider />
</a> </a>
{:else if service.type === 'appwrote'}
<a href="https://appwrite.io" target="_blank">
<Icons.Appwrite/>
</a>
{:else if service.type === 'moodle'} {:else if service.type === 'moodle'}
<a href="https://moodle.org" target="_blank"> <a href="https://moodle.org" target="_blank">
<Icons.Moodle /> <Icons.Moodle />

View File

@ -0,0 +1,126 @@
<script lang="ts">
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import { t } from '$lib/translations';
import Select from 'svelte-select';
export let service: any;
export let readOnly: any;
</script>
<div class="flex space-x-1 py-5 font-bold">
<div class="title">Appwrite</div>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="opensslKeyV1">Encryption Key</label>
<CopyPasswordField
name="opensslKeyV1"
id="opensslKeyV1"
isPasswordField
value={service.appwrite.opensslKeyV1}
readonly
disabled
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="executorSecret">Executor Secret</label>
<CopyPasswordField
name="executorSecret"
id="executorSecret"
isPasswordField
value={service.appwrite.executorSecret}
readonly
disabled
/>
</div>
<!-- <div class="flex space-x-1 py-5 font-bold">
<div class="title">Redis</div>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="redisPassword">Password</label>
<CopyPasswordField
name="redisPassword"
id="redisPassword"
isPasswordField
value={service.appwrite.redisPassword}
readonly
disabled
/>
</div> -->
<div class="flex space-x-1 py-5 font-bold">
<div class="title">MariaDB</div>
</div>
<!-- <div class="grid grid-cols-2 items-center px-10">
<label for="mariadbHost">MariaDB Host</label>
<CopyPasswordField
name="mariadbHost"
id="mariadbHost"
value={service.appwrite.mariadbHost}
readonly
disabled
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="mariadbPort">MariaDB Port</label>
<CopyPasswordField
name="mariadbPort"
id="mariadbPort"
value={service.appwrite.mariadbPort}
readonly
disabled
/>
</div> -->
<div class="grid grid-cols-2 items-center px-10">
<label for="mariadbUser">{$t('forms.username')}</label>
<CopyPasswordField
name="mariadbUser"
id="mariadbUser"
value={service.appwrite.mariadbUser}
readonly
disabled
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="mariadbPassword">{$t('forms.password')}</label>
<CopyPasswordField
id="mariadbPassword"
isPasswordField
readonly
disabled
name="mariadbPassword"
value={service.appwrite.mariadbPassword}
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="mariadbRootUser">Root User</label>
<CopyPasswordField
name="mariadbRootUser"
id="mariadbRootUser"
value={service.appwrite.mariadbRootUser}
readonly
disabled
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="mariadbRootUserPassword">Root Password</label>
<CopyPasswordField
id="mariadbRootUserPassword"
isPasswordField
readonly
disabled
name="mariadbRootUserPassword"
value={service.appwrite.mariadbRootUserPassword}
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="mariadbDatabase">{$t('index.database')}</label>
<CopyPasswordField
name="mariadbDatabase"
id="mariadbDatabase"
value={service.appwrite.mariadbDatabase}
readonly
disabled
/>
</div>

View File

@ -27,6 +27,7 @@
import Umami from './_Umami.svelte'; import Umami from './_Umami.svelte';
import VsCodeServer from './_VSCodeServer.svelte'; import VsCodeServer from './_VSCodeServer.svelte';
import Wordpress from './_Wordpress.svelte'; import Wordpress from './_Wordpress.svelte';
import Appwrite from './_Appwrite.svelte';
import Moodle from './_Moodle.svelte'; import Moodle from './_Moodle.svelte';
const { id } = $page.params; const { id } = $page.params;
@ -395,6 +396,8 @@
<Hasura bind:service /> <Hasura bind:service />
{:else if service.type === 'fider'} {:else if service.type === 'fider'}
<Fider bind:service {readOnly} /> <Fider bind:service {readOnly} />
{:else if service.type === 'appwrite'}
<Appwrite bind:service {readOnly} />
{:else if service.type === 'moodle'} {:else if service.type === 'moodle'}
<Moodle bind:service {readOnly} /> <Moodle bind:service {readOnly} />
{:else if service.type === 'glitchTip'} {:else if service.type === 'glitchTip'}

View File

@ -35,7 +35,6 @@
const { id } = $page.params; const { id } = $page.params;
const from = $page.url.searchParams.get('from'); const from = $page.url.searchParams.get('from');
let recommendedVersion = supportedServiceTypesAndVersions.find( let recommendedVersion = supportedServiceTypesAndVersions.find(
({ name }) => name === type ({ name }) => name === type
)?.recommendedVersion; )?.recommendedVersion;

View File

@ -1,9 +1,10 @@
with import <nixpkgs> {}; with import <nixpkgs> {};
stdenv.mkDerivation { stdenv.mkDerivation {
name = "git"; name = "environment";
buildInputs = [ buildInputs = [
git git
git-lfs git-lfs
docker-compose
]; ];
} }

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