feat: add GlitchTip service

This commit is contained in:
Guillaume Bonnet
2022-08-15 09:56:34 +00:00
parent 0745a12e7d
commit 26d0ef9ac9
13 changed files with 745 additions and 5 deletions

View File

@@ -0,0 +1,30 @@
-- CreateTable
CREATE TABLE "GlitchTip" (
"id" TEXT NOT NULL PRIMARY KEY,
"postgresqlUser" TEXT NOT NULL,
"postgresqlPassword" TEXT NOT NULL,
"postgresqlDatabase" TEXT NOT NULL,
"postgresqlPublicPort" INTEGER,
"secretKeyBase" TEXT,
"defaultEmail" TEXT NOT NULL,
"defaultUsername" TEXT NOT NULL,
"defaultPassword" TEXT NOT NULL,
"defaultEmailFrom" TEXT NOT NULL DEFAULT 'glitchtip@domain.tdl',
"emailSmtpHost" TEXT DEFAULT 'domain.tdl',
"emailSmtpPort" INTEGER DEFAULT 25,
"emailSmtpUser" TEXT,
"emailSmtpPassword" TEXT,
"emailSmtpUseTls" BOOLEAN DEFAULT false,
"emailSmtpUseSsl" BOOLEAN DEFAULT false,
"emailBackend" TEXT,
"mailgunApiKey" TEXT,
"sendgridApiKey" TEXT,
"enableOpenUserRegistration" BOOLEAN NOT NULL DEFAULT true,
"serviceId" TEXT NOT NULL,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "GlitchTip_serviceId_fkey" FOREIGN KEY ("serviceId") REFERENCES "Service" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
-- CreateIndex
CREATE UNIQUE INDEX "GlitchTip_serviceId_key" ON "GlitchTip"("serviceId");

View File

@@ -325,6 +325,7 @@ model Service {
destinationDocker DestinationDocker? @relation(fields: [destinationDockerId], references: [id])
fider Fider?
ghost Ghost?
glitchTip GlitchTip?
hasura Hasura?
meiliSearch MeiliSearch?
minio Minio?
@@ -491,3 +492,30 @@ model Moodle {
updatedAt DateTime @updatedAt
service Service @relation(fields: [serviceId], references: [id])
}
model GlitchTip {
id String @id @default(cuid())
postgresqlUser String
postgresqlPassword String
postgresqlDatabase String
postgresqlPublicPort Int?
secretKeyBase String?
defaultEmail String
defaultUsername String
defaultPassword String
defaultEmailFrom String @default("glitchtip@domain.tdl")
emailSmtpHost String? @default("domain.tdl")
emailSmtpPort Int? @default(25)
emailSmtpUser String?
emailSmtpPassword String?
emailSmtpUseTls Boolean? @default(false)
emailSmtpUseSsl Boolean? @default(false)
emailBackend String?
mailgunApiKey String?
sendgridApiKey String?
enableOpenUserRegistration Boolean @default(true)
serviceId String @unique
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
service Service @relation(fields: [serviceId], references: [id])
}

View File

@@ -78,6 +78,7 @@ export const include: any = {
umami: true,
hasura: true,
fider: true,
glitchTip: true,
};
export const uniqueName = (): string => uniqueNamesGenerator(customConfig);
@@ -280,6 +281,17 @@ export const supportedServiceTypesAndVersions = [
// main: 8080
// }
// }
{
name: 'glitchTip',
fancyName: 'GlitchTip',
baseImage: 'glitchtip/glitchtip',
images: ['postgres:14-alpine', 'redis:7-alpine'],
versions: ['latest'],
recommendedVersion: 'latest',
ports: {
main: 8000
}
},
];
export async function checkDoubleBranch(branch: string, projectId: number): Promise<boolean> {
@@ -1517,7 +1529,33 @@ export async function configureServiceType({
}
}
});
} else {
} else if (type === 'glitchTip') {
const defaultUsername = cuid();
const defaultEmail = `${defaultUsername}@example.com`;
const defaultPassword = encrypt(generatePassword());
const postgresqlUser = cuid();
const postgresqlPassword = encrypt(generatePassword());
const postgresqlDatabase = 'glitchTip';
const secretKeyBase = encrypt(generatePassword(64));
await prisma.service.update({
where: { id },
data: {
type,
glitchTip: {
create: {
postgresqlDatabase,
postgresqlUser,
postgresqlPassword,
secretKeyBase,
defaultEmail,
defaultUsername,
defaultPassword,
}
}
}
});
} else {
await prisma.service.update({
where: { id },
data: {
@@ -1538,6 +1576,7 @@ export async function removeService({ id }: { id: string }): Promise<void> {
await prisma.minio.deleteMany({ where: { serviceId: id } });
await prisma.vscodeserver.deleteMany({ where: { serviceId: id } });
await prisma.wordpress.deleteMany({ where: { serviceId: id } });
await prisma.glitchTip.deleteMany({ where: { serviceId: id } });
await prisma.serviceSecret.deleteMany({ where: { serviceId: id } });
await prisma.service.delete({ where: { id } });

View File

@@ -476,4 +476,116 @@ export const moodle = [{
isNumber: false,
isBoolean: false,
isEncrypted: false
}]
export const glitchTip = [{
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
},
{
name: 'postgresqlPublicPort',
isEditable: false,
isLowerCase: false,
isNumber: true,
isBoolean: false,
isEncrypted: false
},
{
name: 'secretKeyBase',
isEditable: false,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: true
},
{
name: 'defaultEmail',
isEditable: false,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: false
},
{
name: 'defaultUsername',
isEditable: false,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: false
},
{
name: 'defaultPassword',
isEditable: false,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: true
},
{
name: 'defaultFromEmail',
isEditable: true,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: false
},
{
name: 'emailUrl',
isEditable: true,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: false
},
{
name: 'emailBackend',
isEditable: true,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: false
},
{
name: 'mailgunApiKey',
isEditable: true,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: true
},
{
name: 'sendgridApiKey',
isEditable: true,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: true
},
{
name: 'enableOpenUserRegistration',
isEditable: true,
isLowerCase: false,
isNumber: false,
isBoolean: true,
isEncrypted: false
}]

View File

@@ -590,6 +590,9 @@ export async function startService(request: FastifyRequest<ServiceStartStop>) {
if (type === 'moodle') {
return await startMoodleService(request)
}
if (type === 'glitchTip') {
return await startGlitchTipService(request)
}
throw `Service type ${type} not supported.`
} catch (error) {
throw { status: 500, message: error?.message || error }
@@ -643,6 +646,9 @@ export async function stopService(request: FastifyRequest<ServiceStartStop>) {
if (type === 'moodle') {
return await stopMoodleService(request)
}
if (type === 'glitchTip') {
return await stopGlitchTipService(request)
}
throw `Service type ${type} not supported.`
} catch (error) {
throw { status: 500, message: error?.message || error }
@@ -1247,7 +1253,7 @@ async function startWordpressService(request: FastifyRequest<ServiceStartStop>)
const { volumes, volumeMounts } = persistentVolumes(id, persistentStorage, config.wordpress)
let composeFile: ComposeFile = {
const composeFile: ComposeFile = {
version: '3.8',
services: {
[id]: {
@@ -2633,6 +2639,252 @@ async function stopMoodleService(request: FastifyRequest<ServiceStartStop>) {
}
}
async function startGlitchTipService(request: FastifyRequest<ServiceStartStop>) {
try {
const { id } = request.params;
const teamId = request.user.teamId;
const service = await getServiceFromDB({ id, teamId });
const {
type,
version,
fqdn,
destinationDockerId,
destinationDocker,
serviceSecret,
persistentStorage,
exposePort,
glitchTip: {
postgresqlDatabase,
postgresqlPassword,
postgresqlUser,
secretKeyBase,
defaultEmail,
defaultUsername,
defaultPassword,
defaultFromEmail,
emailSmtpHost,
emailSmtpPort,
emailSmtpUser,
emailSmtpPassword,
emailSmtpUseTls,
emailSmtpUseSsl,
emailBackend,
mailgunApiKey,
sendgridApiKey,
enableOpenUserRegistration,
}
} = service;
const network = destinationDockerId && destinationDocker.network;
const port = getServiceMainPort('glitchTip');
const { workdir } = await createDirectories({ repository: type, buildId: id });
const image = getServiceImage(type);
const config = {
glitchTip: {
image: `${image}:${version}`,
environmentVariables: {
PORT: port,
GLITCHTIP_DOMAIN: fqdn,
SECRET_KEY: secretKeyBase,
DATABASE_URL: `postgresql://${postgresqlUser}:${postgresqlPassword}@${id}-postgresql:5432/${postgresqlDatabase}`,
REDIS_URL: `redis://${id}-redis:6379/0`,
DEFAULT_FROM_EMAIL: defaultFromEmail,
EMAIL_HOST: emailSmtpHost,
EMAIL_PORT: emailSmtpPort,
EMAIL_HOST_USER: emailSmtpUser,
EMAIL_HOST_PASSWORD: emailSmtpPassword,
EMAIL_USE_TLS: emailSmtpUseTls,
EMAIL_USE_SSL: emailSmtpUseSsl,
EMAIL_BACKEND: emailBackend,
MAILGUN_API_KEY: mailgunApiKey,
SENDGRID_API_KEY: sendgridApiKey,
ENABLE_OPEN_USER_REGISTRATION: enableOpenUserRegistration,
DJANGO_SUPERUSER_EMAIL: defaultEmail,
DJANGO_SUPERUSER_USERNAME: defaultUsername,
DJANGO_SUPERUSER_PASSWORD: defaultPassword,
}
},
postgresql: {
image: 'postgres:14-alpine',
volume: `${id}-postgresql-data:/var/lib/postgresql/data`,
environmentVariables: {
POSTGRES_USER: postgresqlUser,
POSTGRES_PASSWORD: postgresqlPassword,
POSTGRES_DB: postgresqlDatabase
}
},
redis: {
image: 'redis:7-alpine',
volume: `${id}-redis-data:/data`,
}
};
if (serviceSecret.length > 0) {
serviceSecret.forEach((secret) => {
config.glitchTip.environmentVariables[secret.name] = secret.value;
});
}
const { volumes, volumeMounts } = persistentVolumes(id, persistentStorage, config.glitchTip)
const composeFile: ComposeFile = {
version: '3.8',
services: {
[id]: {
container_name: id,
image: config.glitchTip.image,
environment: config.glitchTip.environmentVariables,
networks: [network],
volumes,
restart: 'always',
labels: makeLabelForServices('glitchTip'),
...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}),
deploy: {
restart_policy: {
condition: 'on-failure',
delay: '5s',
max_attempts: 3,
window: '120s'
}
},
depends_on: [`${id}-postgresql`, `${id}-redis`]
},
[`${id}-worker`]: {
container_name: `${id}-worker`,
image: config.glitchTip.image,
command: './bin/run-celery-with-beat.sh',
environment: config.glitchTip.environmentVariables,
networks: [network],
restart: 'always',
deploy: {
restart_policy: {
condition: 'on-failure',
delay: '5s',
max_attempts: 3,
window: '120s'
}
},
depends_on: [`${id}-postgresql`, `${id}-redis`]
},
[`${id}-setup`]: {
container_name: `${id}-setup`,
image: config.glitchTip.image,
command: 'sh -c "(./manage.py migrate || true) && (./manage.py createsuperuser --noinput || true)"',
environment: config.glitchTip.environmentVariables,
networks: [network],
restart: "no",
depends_on: [`${id}-postgresql`, `${id}-redis`]
},
[`${id}-postgresql`]: {
image: config.postgresql.image,
container_name: `${id}-postgresql`,
environment: config.postgresql.environmentVariables,
networks: [network],
volumes: [config.postgresql.volume],
restart: 'always',
deploy: {
restart_policy: {
condition: 'on-failure',
delay: '5s',
max_attempts: 3,
window: '120s'
}
}
},
[`${id}-redis`]: {
image: config.redis.image,
container_name: `${id}-redis`,
networks: [network],
volumes: [config.redis.volume],
restart: 'always',
deploy: {
restart_policy: {
condition: 'on-failure',
delay: '5s',
max_attempts: 3,
window: '120s'
}
}
}
},
networks: {
[network]: {
external: true
}
},
volumes: {
...volumeMounts,
[config.postgresql.volume.split(':')[0]]: {
name: config.postgresql.volume.split(':')[0]
},
[config.redis.volume.split(':')[0]]: {
name: config.redis.volume.split(':')[0]
}
}
};
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 stopGlitchTipService(request: FastifyRequest<ServiceStartStop>) {
try {
const { id } = request.params;
const teamId = request.user.teamId;
const service = await getServiceFromDB({ id, teamId });
const { destinationDockerId, destinationDocker } = service;
if (destinationDockerId) {
try {
const found = await checkContainer({ dockerId: destinationDocker.id, container: id });
if (found) {
await removeContainer({ id, dockerId: destinationDocker.id });
}
} catch (error) {
console.error(error);
}
try {
const found = await checkContainer({ dockerId: destinationDocker.id, container: `${id}-worker` });
if (found) {
await removeContainer({ id: `${id}-worker`, dockerId: destinationDocker.id });
}
} catch (error) {
console.error(error);
}
try {
const found = await checkContainer({ dockerId: destinationDocker.id, container: `${id}-setup` });
if (found) {
await removeContainer({ id: `${id}-setup`, dockerId: destinationDocker.id });
}
} catch (error) {
console.error(error);
}
try {
const found = await checkContainer({ dockerId: destinationDocker.id, container: `${id}-postgresql` });
if (found) {
await removeContainer({ id: `${id}-postgresql`, dockerId: destinationDocker.id });
}
} catch (error) {
console.error(error);
}
try {
const found = await checkContainer({ dockerId: destinationDocker.id, container: `${id}-redis` });
if (found) {
await removeContainer({ id: `${id}-redis`, dockerId: destinationDocker.id });
}
} catch (error) {
console.error(error);
}
}
return {}
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function activatePlausibleUsers(request: FastifyRequest<OnlyId>, reply: FastifyReply) {
try {