feat: Taiga
This commit is contained in:
parent
8ad152e5fc
commit
d098ea675f
@ -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.
|
||||
|
||||
|
@ -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");
|
@ -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])
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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
@ -785,3 +785,83 @@ export const weblate = [{
|
||||
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,
|
||||
isLowerCase: false,
|
||||
isNumber: false,
|
||||
isBoolean: false,
|
||||
isEncrypted: false
|
||||
}]
|
@ -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
|
||||
}
|
||||
},
|
||||
];
|
@ -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';
|
||||
|
||||
|
@ -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 = {
|
||||
|
@ -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);
|
||||
});
|
||||
|
@ -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 () => {
|
||||
|
@ -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>
|
||||
|
118
apps/ui/src/routes/services/[id]/_Services/_Taiga.svelte
Normal file
118
apps/ui/src/routes/services/[id]/_Services/_Taiga.svelte
Normal 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>
|
@ -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"
|
||||
|
Loading…
Reference in New Issue
Block a user