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 @@ ### Add required functions/properties
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/routes/api/v1/services/handlers.ts](apps/api/src/routes/api/v1/services/handlers.ts)
5. Add start process for the new service in [apps/api/src/lib/services/handlers.ts](apps/api/src/lib/services/handlers.ts)
> 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?
searxng Searxng?
weblate Weblate?
taiga Taiga?
}
model PlausibleAnalytics {
@ -575,3 +576,23 @@ model Weblate {
updatedAt DateTime @updatedAt
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) {
return [
'coolify.managed=true',
`coolify.version = ${version}`,
`coolify.type = service`,
`coolify.service.type = ${type}`
`coolify.version=${version}`,
`coolify.type=service`,
`coolify.service.type=${type}`
];
}
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) {
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 =
persistentStorage?.map((storage) => {
return `${id}${storage.path.replace(/\//gi, '-')}:${storage.path}`;
}) || [];
let volumes = [...persistentVolume]
if (config.volume) volumes = [config.volume, ...volumes]
if (volumesArray) volumes = [...volumesArray, ...volumes]
const composeVolumes = volumes.length > 0 && volumes.map((volume) => {
return {
[`${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
) || {}
return { volumes, volumeMounts }
return { volumeMounts }
}
export function defaultComposeConfiguration(network: string): any {
return {

View File

@ -19,7 +19,8 @@ export const includeServices: any = {
appwrite: true,
glitchTip: true,
searxng: true,
weblate: true
weblate: true,
taiga: true
};
export async function configureServiceType({
id,
@ -297,7 +298,7 @@ export async function configureServiceType({
}
}
});
}else if (type === 'weblate') {
} else if (type === 'weblate') {
const adminPassword = encrypt(generatePassword({}))
const postgresqlUser = cuid();
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 {
await prisma.service.update({
where: { id },
@ -345,6 +377,7 @@ export async function removeService({ id }: { id: string }): Promise<void> {
await prisma.appwrite.deleteMany({ where: { serviceId: id } });
await prisma.searxng.deleteMany({ where: { serviceId: id } });
await prisma.weblate.deleteMany({ where: { serviceId: id } });
await prisma.taiga.deleteMany({ where: { serviceId: 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,
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',
isEditable: false,

View File

@ -194,11 +194,22 @@ export const supportedServiceTypesAndVersions = [
name: 'weblate',
fancyName: 'Weblate',
baseImage: 'weblate/weblate',
images: ['postgres:14-alpine','redis:6-alpine'],
images: ['postgres:14-alpine', 'redis:6-alpine'],
versions: ['latest'],
recommendedVersion: 'latest',
ports: {
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 fs from 'fs/promises';
import yaml from 'js-yaml';
import bcrypt from 'bcryptjs';
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 { prisma, uniqueName, asyncExecShell, getServiceFromDB, getContainerUsage, isDomainConfigured, saveUpdateableFields, fixType, decrypt, encrypt, ComposeFile, getFreePublicPort, getDomain, errorHandler, generatePassword, isDev, stopTcpHttpProxy, executeDockerCmd, checkDomainsIsValidInDNS, checkExposedPort } from '../../../../lib/common';
import { day } from '../../../../lib/dayjs';
import { checkContainer, isContainerExited, removeContainer } from '../../../../lib/docker';
import { checkContainer, isContainerExited } from '../../../../lib/docker';
import cuid from 'cuid';
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 { defaultServiceConfigurations } from '../../../../lib/services';
import { supportedServiceTypesAndVersions } from '../../../../lib/services/supportedVersions';
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 { includeServices } from "../../../lib/services/common";
import { TraefikOtherConfiguration } from "./types";
import { OnlyId } from "../../../types";
function configureMiddleware(
{ 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
try {
const traefik = {

View File

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

View File

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

View File

@ -31,6 +31,7 @@
import Searxng from './_Searxng.svelte';
import Weblate from './_Weblate.svelte';
import Explainer from '$lib/components/Explainer.svelte';
import Taiga from './_Taiga.svelte';
const { id } = $page.params;
$: isDisabled =
@ -411,6 +412,8 @@
<Searxng bind:service />
{:else if service.type === 'weblate'}
<Weblate bind:service />
{:else if service.type === 'taiga'}
<Taiga bind:service />
{/if}
</div>
</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 }));
if (sure) {
$status.service.initialLoading = true;
$status.service.loading = true;
try {
await post(`/services/${service.id}/${service.type}/stop`, {});
} catch (error) {
return errorNotification(error);
} finally {
$status.service.initialLoading = false;
$status.service.loading = false;
}
}
}
@ -131,6 +129,9 @@
}
onDestroy(() => {
$status.service.initialLoading = true;
$status.service.isRunning = false;
$status.service.isExited = false;
$status.service.loading = false;
$location = null;
clearInterval(statusInterval);
});
@ -150,7 +151,7 @@
</script>
<nav class="nav-side">
{#if service.type && service.destinationDockerId && service.version}
{#if service.type && service.destinationDockerId && service.version && service.fqdn}
{#if $location}
<a
id="open"