feat: Taiga

This commit is contained in:
Andras Bacsai 2022-09-02 14:11:36 +02:00
parent 8ad152e5fc
commit d098ea675f
15 changed files with 850 additions and 304 deletions

View File

@ -208,8 +208,7 @@ export const umami = [{
4. Add service deletion query to `removeService` function in [apps/api/src/lib/services/common.ts](apps/api/src/lib/services/common.ts) 4. Add service deletion query to `removeService` function in [apps/api/src/lib/services/common.ts](apps/api/src/lib/services/common.ts)
5. Add start process for the new service in [apps/api/src/lib/services/handlers.ts](apps/api/src/lib/services/handlers.ts)
5. Add start process for the new service in [apps/api/src/routes/api/v1/services/handlers.ts](apps/api/src/routes/api/v1/services/handlers.ts)
> See startUmamiService() function as example. > See startUmamiService() function as example.

View File

@ -0,0 +1,23 @@
-- CreateTable
CREATE TABLE "Taiga" (
"id" TEXT NOT NULL PRIMARY KEY,
"secretKey" TEXT NOT NULL,
"erlangSecret" TEXT NOT NULL,
"djangoAdminPassword" TEXT NOT NULL,
"djangoAdminUser" TEXT NOT NULL,
"rabbitMQUser" TEXT NOT NULL,
"rabbitMQPassword" TEXT NOT NULL,
"postgresqlHost" TEXT NOT NULL,
"postgresqlPort" INTEGER NOT NULL,
"postgresqlUser" TEXT NOT NULL,
"postgresqlPassword" TEXT NOT NULL,
"postgresqlDatabase" TEXT NOT NULL,
"postgresqlPublicPort" INTEGER,
"serviceId" TEXT NOT NULL,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "Taiga_serviceId_fkey" FOREIGN KEY ("serviceId") REFERENCES "Service" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
-- CreateIndex
CREATE UNIQUE INDEX "Taiga_serviceId_key" ON "Taiga"("serviceId");

View File

@ -349,6 +349,7 @@ model Service {
appwrite Appwrite? appwrite Appwrite?
searxng Searxng? searxng Searxng?
weblate Weblate? weblate Weblate?
taiga Taiga?
} }
model PlausibleAnalytics { model PlausibleAnalytics {
@ -575,3 +576,23 @@ model Weblate {
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
service Service @relation(fields: [serviceId], references: [id]) service Service @relation(fields: [serviceId], references: [id])
} }
model Taiga {
id String @id @default(cuid())
secretKey String
erlangSecret String
djangoAdminPassword String
djangoAdminUser String
rabbitMQUser String
rabbitMQPassword String
postgresqlHost String
postgresqlPort Int
postgresqlUser String
postgresqlPassword String
postgresqlDatabase String
postgresqlPublicPort Int?
serviceId String @unique
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
service Service @relation(fields: [serviceId], references: [id])
}

View File

@ -1353,9 +1353,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 }) {
@ -1475,14 +1475,25 @@ export async function cleanupDockerStorage(dockerId, lowDiskSpace, force) {
} }
export function persistentVolumes(id, persistentStorage, config) { export function persistentVolumes(id, persistentStorage, config) {
let volumeSet = new Set();
if (Object.keys(config).length > 0) {
for (const [key, value] of Object.entries(config)) {
if (value.volumes) {
for (const volume of value.volumes) {
volumeSet.add(volume);
}
}
}
}
const volumesArray = Array.from(volumeSet);
const persistentVolume = const persistentVolume =
persistentStorage?.map((storage) => { persistentStorage?.map((storage) => {
return `${id}${storage.path.replace(/\//gi, '-')}:${storage.path}`; return `${id}${storage.path.replace(/\//gi, '-')}:${storage.path}`;
}) || []; }) || [];
let volumes = [...persistentVolume] let volumes = [...persistentVolume]
if (config.volume) volumes = [config.volume, ...volumes] if (volumesArray) volumes = [...volumesArray, ...volumes]
const composeVolumes = volumes.length > 0 && volumes.map((volume) => { const composeVolumes = volumes.length > 0 && volumes.map((volume) => {
return { return {
[`${volume.split(':')[0]}`]: { [`${volume.split(':')[0]}`]: {
@ -1491,16 +1502,11 @@ export function persistentVolumes(id, persistentStorage, config) {
}; };
}) || [] }) || []
const volumeMounts = config.volume && Object.assign( const volumeMounts = Object.assign(
{}, {},
{
[config.volume.split(':')[0]]: {
name: config.volume.split(':')[0]
}
},
...composeVolumes ...composeVolumes
) || {} ) || {}
return { volumes, volumeMounts } return { volumeMounts }
} }
export function defaultComposeConfiguration(network: string): any { export function defaultComposeConfiguration(network: string): any {
return { return {

View File

@ -19,7 +19,8 @@ export const includeServices: any = {
appwrite: true, appwrite: true,
glitchTip: true, glitchTip: true,
searxng: true, searxng: true,
weblate: true weblate: true,
taiga: true
}; };
export async function configureServiceType({ export async function configureServiceType({
id, id,
@ -297,7 +298,7 @@ export async function configureServiceType({
} }
} }
}); });
}else if (type === 'weblate') { } else if (type === 'weblate') {
const adminPassword = encrypt(generatePassword({})) const adminPassword = encrypt(generatePassword({}))
const postgresqlUser = cuid(); const postgresqlUser = cuid();
const postgresqlPassword = encrypt(generatePassword({})); const postgresqlPassword = encrypt(generatePassword({}));
@ -318,6 +319,37 @@ export async function configureServiceType({
} }
} }
}); });
} else if (type === 'taiga') {
const secretKey = encrypt(generatePassword({}))
const erlangSecret = encrypt(generatePassword({}))
const rabbitMQUser = cuid();
const djangoAdminUser = cuid();
const djangoAdminPassword = encrypt(generatePassword({}))
const rabbitMQPassword = encrypt(generatePassword({}))
const postgresqlUser = cuid();
const postgresqlPassword = encrypt(generatePassword({}));
const postgresqlDatabase = 'taiga';
await prisma.service.update({
where: { id },
data: {
type,
taiga: {
create: {
secretKey,
erlangSecret,
djangoAdminUser,
djangoAdminPassword,
rabbitMQUser,
rabbitMQPassword,
postgresqlHost: `${id}-postgresql`,
postgresqlPort: 5432,
postgresqlUser,
postgresqlPassword,
postgresqlDatabase,
}
}
}
});
} else { } else {
await prisma.service.update({ await prisma.service.update({
where: { id }, where: { id },
@ -345,6 +377,7 @@ export async function removeService({ id }: { id: string }): Promise<void> {
await prisma.appwrite.deleteMany({ where: { serviceId: id } }); await prisma.appwrite.deleteMany({ where: { serviceId: id } });
await prisma.searxng.deleteMany({ where: { serviceId: id } }); await prisma.searxng.deleteMany({ where: { serviceId: id } });
await prisma.weblate.deleteMany({ where: { serviceId: id } }); await prisma.weblate.deleteMany({ where: { serviceId: id } });
await prisma.taiga.deleteMany({ where: { serviceId: id } });
await prisma.service.delete({ where: { id } }); await prisma.service.delete({ where: { id } });
} }

File diff suppressed because it is too large Load Diff

View File

@ -777,6 +777,86 @@ export const weblate = [{
isBoolean: false, isBoolean: false,
isEncrypted: true isEncrypted: true
}, },
{
name: 'postgresqlDatabase',
isEditable: false,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: false
}]
export const taiga = [{
name: 'secretKey',
isEditable: false,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: true
},
{
name: 'djangoAdminUser',
isEditable: false,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: false
},
{
name: 'djangoAdminPassword',
isEditable: false,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: true
},
{
name: 'rabbitMQUser',
isEditable: false,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: false
},
{
name: 'rabbitMQPassword',
isEditable: false,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: true
},
{
name: 'postgresqlHost',
isEditable: false,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: false
},
{
name: 'postgresqlPort',
isEditable: false,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: false
},
{
name: 'postgresqlUser',
isEditable: false,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: false
},
{
name: 'postgresqlPassword',
isEditable: false,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: true
},
{ {
name: 'postgresqlDatabase', name: 'postgresqlDatabase',
isEditable: false, isEditable: false,

View File

@ -194,11 +194,22 @@ export const supportedServiceTypesAndVersions = [
name: 'weblate', name: 'weblate',
fancyName: 'Weblate', fancyName: 'Weblate',
baseImage: 'weblate/weblate', baseImage: 'weblate/weblate',
images: ['postgres:14-alpine','redis:6-alpine'], images: ['postgres:14-alpine', 'redis:6-alpine'],
versions: ['latest'], versions: ['latest'],
recommendedVersion: 'latest', recommendedVersion: 'latest',
ports: { ports: {
main: 8080 main: 8080
} }
}, },
{
name: 'taiga',
fancyName: 'Taiga',
baseImage: 'taigaio/taiga-front',
images: ['postgres:12.3', 'rabbitmq:3.8-management-alpine', 'taigaio/taiga-back', 'taigaio/taiga-events', 'taigaio/taiga-protected'],
versions: ['latest'],
recommendedVersion: 'latest',
ports: {
main: 80
}
},
]; ];

View File

@ -1,15 +1,13 @@
import type { FastifyReply, FastifyRequest } from 'fastify'; import type { FastifyReply, FastifyRequest } from 'fastify';
import fs from 'fs/promises'; import fs from 'fs/promises';
import yaml from 'js-yaml'; import yaml from 'js-yaml';
import bcrypt from 'bcryptjs'; import { prisma, uniqueName, asyncExecShell, getServiceFromDB, getContainerUsage, isDomainConfigured, saveUpdateableFields, fixType, decrypt, encrypt, ComposeFile, getFreePublicPort, getDomain, errorHandler, generatePassword, isDev, stopTcpHttpProxy, executeDockerCmd, checkDomainsIsValidInDNS, checkExposedPort } from '../../../../lib/common';
import { prisma, uniqueName, asyncExecShell, getServiceImage, getServiceFromDB, getContainerUsage, isDomainConfigured, saveUpdateableFields, fixType, decrypt, encrypt, getServiceMainPort, createDirectories, ComposeFile, makeLabelForServices, getFreePublicPort, getDomain, errorHandler, generatePassword, isDev, stopTcpHttpProxy, executeDockerCmd, checkDomainsIsValidInDNS, persistentVolumes, asyncSleep, isARM, defaultComposeConfiguration, checkExposedPort } from '../../../../lib/common';
import { day } from '../../../../lib/dayjs'; import { day } from '../../../../lib/dayjs';
import { checkContainer, isContainerExited, removeContainer } from '../../../../lib/docker'; import { checkContainer, isContainerExited } from '../../../../lib/docker';
import cuid from 'cuid'; 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, SetGlitchTipSettings, SetWordpressSettings } from './types'; import type { ActivateWordpressFtp, CheckService, CheckServiceDomain, DeleteServiceSecret, DeleteServiceStorage, GetServiceLogs, SaveService, SaveServiceDestination, SaveServiceSecret, SaveServiceSettings, SaveServiceStorage, SaveServiceType, SaveServiceVersion, ServiceStartStop, SetGlitchTipSettings, SetWordpressSettings } from './types';
import { defaultServiceConfigurations } from '../../../../lib/services';
import { supportedServiceTypesAndVersions } from '../../../../lib/services/supportedVersions'; import { supportedServiceTypesAndVersions } from '../../../../lib/services/supportedVersions';
import { configureServiceType, removeService } from '../../../../lib/services/common'; import { configureServiceType, removeService } from '../../../../lib/services/common';

View File

@ -3,6 +3,7 @@ import { errorHandler, getDomain, isDev, prisma, executeDockerCmd } from "../../
import { supportedServiceTypesAndVersions } from "../../../lib/services/supportedVersions"; import { supportedServiceTypesAndVersions } from "../../../lib/services/supportedVersions";
import { includeServices } from "../../../lib/services/common"; import { includeServices } from "../../../lib/services/common";
import { TraefikOtherConfiguration } from "./types"; import { TraefikOtherConfiguration } from "./types";
import { OnlyId } from "../../../types";
function configureMiddleware( function configureMiddleware(
{ id, container, port, domain, nakedDomain, isHttps, isWWW, isDualCerts, scriptName, type }, { id, container, port, domain, nakedDomain, isHttps, isWWW, isDualCerts, scriptName, type },
@ -530,7 +531,7 @@ export async function traefikOtherConfiguration(request: FastifyRequest<TraefikO
} }
} }
export async function remoteTraefikConfiguration(request: FastifyRequest) { export async function remoteTraefikConfiguration(request: FastifyRequest<OnlyId>) {
const { id } = request.params const { id } = request.params
try { try {
const traefik = { const traefik = {

View File

@ -135,13 +135,13 @@
async function stopApplication() { async function stopApplication() {
try { try {
$status.application.initialLoading = true; $status.application.initialLoading = true;
$status.application.loading = true; // $status.application.loading = true;
await post(`/applications/${id}/stop`, {}); await post(`/applications/${id}/stop`, {});
} catch (error) { } catch (error) {
return errorNotification(error); return errorNotification(error);
} finally { } finally {
$status.application.initialLoading = false; $status.application.initialLoading = false;
$status.application.loading = false; // $status.application.loading = false;
await getStatus(); await getStatus();
} }
} }
@ -157,6 +157,9 @@
onDestroy(() => { onDestroy(() => {
$status.application.initialLoading = true; $status.application.initialLoading = true;
$status.application.isRunning = false;
$status.application.isExited = false;
$status.application.loading = false;
$location = null; $location = null;
clearInterval(statusInterval); clearInterval(statusInterval);
}); });

View File

@ -119,6 +119,9 @@
} }
onDestroy(() => { onDestroy(() => {
$status.database.initialLoading = true; $status.database.initialLoading = true;
$status.database.isRunning = false;
$status.database.isExited = false;
$status.database.loading = false;
clearInterval(statusInterval); clearInterval(statusInterval);
}); });
onMount(async () => { onMount(async () => {

View File

@ -31,6 +31,7 @@
import Searxng from './_Searxng.svelte'; import Searxng from './_Searxng.svelte';
import Weblate from './_Weblate.svelte'; import Weblate from './_Weblate.svelte';
import Explainer from '$lib/components/Explainer.svelte'; import Explainer from '$lib/components/Explainer.svelte';
import Taiga from './_Taiga.svelte';
const { id } = $page.params; const { id } = $page.params;
$: isDisabled = $: isDisabled =
@ -411,6 +412,8 @@
<Searxng bind:service /> <Searxng bind:service />
{:else if service.type === 'weblate'} {:else if service.type === 'weblate'}
<Weblate bind:service /> <Weblate bind:service />
{:else if service.type === 'taiga'}
<Taiga bind:service />
{/if} {/if}
</div> </div>
</form> </form>

View File

@ -0,0 +1,118 @@
<script lang="ts">
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import { t } from '$lib/translations';
export let service: any;
</script>
<div class="flex space-x-1 py-5 font-bold">
<div class="title">Taiga</div>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="secretKey">Secret Key</label>
<CopyPasswordField
name="secretKey"
id="secretKey"
isPasswordField
value={service.taiga.secretKey}
readonly
disabled
/>
</div>
<div class="flex space-x-1 py-5 font-bold">
<div class="title">Django</div>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="djangoAdminUser">Admin User</label>
<CopyPasswordField
name="djangoAdminUser"
id="djangoAdminUser"
value={service.taiga.djangoAdminUser}
readonly
disabled
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="djangoAdminPassword">Admin Password</label>
<CopyPasswordField
name="djangoAdminPassword"
id="djangoAdminPassword"
isPasswordField
value={service.taiga.djangoAdminPassword}
readonly
disabled
/>
</div>
<div class="flex space-x-1 py-5 font-bold">
<div class="title">RabbitMQ</div>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="rabbitMQUser">User</label>
<CopyPasswordField
name="rabbitMQUser"
id="rabbitMQUser"
value={service.taiga.rabbitMQUser}
readonly
disabled
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="rabbitMQPassword">Password</label>
<CopyPasswordField
name="rabbitMQPassword"
id="rabbitMQPassword"
isPasswordField
value={service.taiga.rabbitMQPassword}
readonly
disabled
/>
</div>
<div class="flex space-x-1 py-5 font-bold">
<div class="title">PostgreSQL</div>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="postgresqlHost">PostgreSQL Host</label>
<CopyPasswordField
name="postgresqlHost"
id="postgresqlHost"
value={service.taiga.postgresqlHost}
readonly
disabled
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="postgresqlPort">PostgreSQL Port</label>
<CopyPasswordField
name="postgresqlPort"
id="postgresqlPort"
value={service.taiga.postgresqlPort}
readonly
disabled
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="postgresqlUser">PostgreSQL User</label>
<CopyPasswordField
name="postgresqlUser"
id="postgresqlUser"
value={service.taiga.postgresqlUser}
readonly
disabled
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="postgresqlPassword">PostgreSQL Password</label>
<CopyPasswordField
name="postgresqlPassword"
id="postgresqlPassword"
isPasswordField
value={service.taiga.postgresqlPassword}
readonly
disabled
/>
</div>

View File

@ -96,14 +96,12 @@
const sure = confirm($t('database.confirm_stop', { name: service.name })); const sure = confirm($t('database.confirm_stop', { name: service.name }));
if (sure) { if (sure) {
$status.service.initialLoading = true; $status.service.initialLoading = true;
$status.service.loading = true;
try { try {
await post(`/services/${service.id}/${service.type}/stop`, {}); await post(`/services/${service.id}/${service.type}/stop`, {});
} catch (error) { } catch (error) {
return errorNotification(error); return errorNotification(error);
} finally { } finally {
$status.service.initialLoading = false; $status.service.initialLoading = false;
$status.service.loading = false;
} }
} }
} }
@ -131,6 +129,9 @@
} }
onDestroy(() => { onDestroy(() => {
$status.service.initialLoading = true; $status.service.initialLoading = true;
$status.service.isRunning = false;
$status.service.isExited = false;
$status.service.loading = false;
$location = null; $location = null;
clearInterval(statusInterval); clearInterval(statusInterval);
}); });
@ -150,7 +151,7 @@
</script> </script>
<nav class="nav-side"> <nav class="nav-side">
{#if service.type && service.destinationDockerId && service.version} {#if service.type && service.destinationDockerId && service.version && service.fqdn}
{#if $location} {#if $location}
<a <a
id="open" id="open"