Merge branch 'next' into edge-db
This commit is contained in:
@@ -16,11 +16,11 @@
|
||||
"dependencies": {
|
||||
"@breejs/ts-worker": "2.0.0",
|
||||
"@fastify/autoload": "5.1.0",
|
||||
"@fastify/cookie": "7.1.0",
|
||||
"@fastify/cookie": "7.3.1",
|
||||
"@fastify/cors": "8.0.0",
|
||||
"@fastify/env": "4.0.0",
|
||||
"@fastify/jwt": "6.3.1",
|
||||
"@fastify/static": "6.4.0",
|
||||
"@fastify/static": "6.4.1",
|
||||
"@iarna/toml": "2.2.5",
|
||||
"@prisma/client": "3.15.2",
|
||||
"axios": "0.27.2",
|
||||
@@ -29,30 +29,33 @@
|
||||
"cabin": "9.1.2",
|
||||
"compare-versions": "4.1.3",
|
||||
"cuid": "2.1.8",
|
||||
"dayjs": "1.11.3",
|
||||
"dayjs": "1.11.4",
|
||||
"dockerode": "3.3.2",
|
||||
"dotenv-extended": "2.9.0",
|
||||
"fastify": "4.2.1",
|
||||
"fastify": "4.3.0",
|
||||
"fastify-plugin": "4.0.0",
|
||||
"generate-password": "1.7.0",
|
||||
"get-port": "6.1.2",
|
||||
"got": "12.1.0",
|
||||
"is-ip": "4.0.0",
|
||||
"got": "12.2.0",
|
||||
"is-ip": "5.0.0",
|
||||
"is-port-reachable": "4.0.0",
|
||||
"js-yaml": "4.1.0",
|
||||
"jsonwebtoken": "8.5.1",
|
||||
"node-forge": "1.3.1",
|
||||
"node-os-utils": "1.3.7",
|
||||
"p-queue": "7.2.0",
|
||||
"public-ip": "6.0.1",
|
||||
"ssh-config": "4.1.6",
|
||||
"strip-ansi": "7.0.1",
|
||||
"unique-names-generator": "4.7.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "18.0.4",
|
||||
"@types/node": "18.6.1",
|
||||
"@types/node-os-utils": "1.3.0",
|
||||
"@typescript-eslint/eslint-plugin": "5.30.6",
|
||||
"@typescript-eslint/parser": "5.30.6",
|
||||
"esbuild": "0.14.49",
|
||||
"eslint": "8.19.0",
|
||||
"@typescript-eslint/eslint-plugin": "5.31.0",
|
||||
"@typescript-eslint/parser": "5.31.0",
|
||||
"esbuild": "0.14.50",
|
||||
"eslint": "8.20.0",
|
||||
"eslint-config-prettier": "8.5.0",
|
||||
"eslint-plugin-prettier": "4.2.1",
|
||||
"nodemon": "2.0.19",
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "Moodle" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"serviceId" TEXT NOT NULL,
|
||||
"defaultUsername" TEXT NOT NULL,
|
||||
"defaultPassword" TEXT NOT NULL,
|
||||
"defaultEmail" TEXT NOT NULL,
|
||||
"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 "Moodle_serviceId_fkey" FOREIGN KEY ("serviceId") REFERENCES "Service" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Moodle_serviceId_key" ON "Moodle"("serviceId");
|
||||
@@ -0,0 +1,21 @@
|
||||
-- RedefineTables
|
||||
PRAGMA foreign_keys=OFF;
|
||||
CREATE TABLE "new_DestinationDocker" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"network" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"engine" TEXT,
|
||||
"remoteEngine" BOOLEAN NOT NULL DEFAULT false,
|
||||
"remoteIpAddress" TEXT,
|
||||
"remoteUser" TEXT,
|
||||
"remotePort" INTEGER,
|
||||
"isCoolifyProxyUsed" BOOLEAN DEFAULT false,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" DATETIME NOT NULL
|
||||
);
|
||||
INSERT INTO "new_DestinationDocker" ("createdAt", "engine", "id", "isCoolifyProxyUsed", "name", "network", "remoteEngine", "updatedAt") SELECT "createdAt", "engine", "id", "isCoolifyProxyUsed", "name", "network", "remoteEngine", "updatedAt" FROM "DestinationDocker";
|
||||
DROP TABLE "DestinationDocker";
|
||||
ALTER TABLE "new_DestinationDocker" RENAME TO "DestinationDocker";
|
||||
CREATE UNIQUE INDEX "DestinationDocker_network_key" ON "DestinationDocker"("network");
|
||||
PRAGMA foreign_key_check;
|
||||
PRAGMA foreign_keys=ON;
|
||||
@@ -0,0 +1,33 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "SshKey" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"name" TEXT NOT NULL,
|
||||
"privateKey" TEXT NOT NULL,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" DATETIME NOT NULL
|
||||
);
|
||||
|
||||
-- RedefineTables
|
||||
PRAGMA foreign_keys=OFF;
|
||||
CREATE TABLE "new_DestinationDocker" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"network" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"engine" TEXT,
|
||||
"remoteEngine" BOOLEAN NOT NULL DEFAULT false,
|
||||
"remoteIpAddress" TEXT,
|
||||
"remoteUser" TEXT,
|
||||
"remotePort" INTEGER,
|
||||
"remoteVerified" BOOLEAN NOT NULL DEFAULT false,
|
||||
"isCoolifyProxyUsed" BOOLEAN DEFAULT false,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" DATETIME NOT NULL,
|
||||
"sshKeyId" TEXT,
|
||||
CONSTRAINT "DestinationDocker_sshKeyId_fkey" FOREIGN KEY ("sshKeyId") REFERENCES "SshKey" ("id") ON DELETE SET NULL ON UPDATE CASCADE
|
||||
);
|
||||
INSERT INTO "new_DestinationDocker" ("createdAt", "engine", "id", "isCoolifyProxyUsed", "name", "network", "remoteEngine", "remoteIpAddress", "remotePort", "remoteUser", "updatedAt") SELECT "createdAt", "engine", "id", "isCoolifyProxyUsed", "name", "network", "remoteEngine", "remoteIpAddress", "remotePort", "remoteUser", "updatedAt" FROM "DestinationDocker";
|
||||
DROP TABLE "DestinationDocker";
|
||||
ALTER TABLE "new_DestinationDocker" RENAME TO "DestinationDocker";
|
||||
CREATE UNIQUE INDEX "DestinationDocker_network_key" ON "DestinationDocker"("network");
|
||||
PRAGMA foreign_key_check;
|
||||
PRAGMA foreign_keys=ON;
|
||||
@@ -0,0 +1,3 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Setting" ADD COLUMN "ipv4" TEXT;
|
||||
ALTER TABLE "Setting" ADD COLUMN "ipv6" TEXT;
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Setting" ADD COLUMN "arch" TEXT;
|
||||
@@ -0,0 +1,16 @@
|
||||
-- RedefineTables
|
||||
PRAGMA foreign_keys=OFF;
|
||||
CREATE TABLE "new_SshKey" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"name" TEXT NOT NULL,
|
||||
"privateKey" TEXT NOT NULL,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" DATETIME NOT NULL,
|
||||
"teamId" TEXT,
|
||||
CONSTRAINT "SshKey_teamId_fkey" FOREIGN KEY ("teamId") REFERENCES "Team" ("id") ON DELETE SET NULL ON UPDATE CASCADE
|
||||
);
|
||||
INSERT INTO "new_SshKey" ("createdAt", "id", "name", "privateKey", "updatedAt") SELECT "createdAt", "id", "name", "privateKey", "updatedAt" FROM "SshKey";
|
||||
DROP TABLE "SshKey";
|
||||
ALTER TABLE "new_SshKey" RENAME TO "SshKey";
|
||||
PRAGMA foreign_key_check;
|
||||
PRAGMA foreign_keys=ON;
|
||||
@@ -23,6 +23,9 @@ model Setting {
|
||||
isTraefikUsed Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
ipv4 String?
|
||||
ipv6 String?
|
||||
arch String?
|
||||
}
|
||||
|
||||
model User {
|
||||
@@ -30,39 +33,40 @@ model User {
|
||||
email String @unique
|
||||
type String
|
||||
password String?
|
||||
teams Team[]
|
||||
permission Permission[]
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
permission Permission[]
|
||||
teams Team[]
|
||||
}
|
||||
|
||||
model Permission {
|
||||
id String @id @default(cuid())
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
userId String
|
||||
team Team @relation(fields: [teamId], references: [id])
|
||||
teamId String
|
||||
permission String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
team Team @relation(fields: [teamId], references: [id])
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
}
|
||||
|
||||
model Team {
|
||||
id String @id @default(cuid())
|
||||
users User[]
|
||||
name String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
databaseId String?
|
||||
serviceId String?
|
||||
permissions Permission[]
|
||||
sshKey SshKey[]
|
||||
applications Application[]
|
||||
database Database[]
|
||||
destinationDocker DestinationDocker[]
|
||||
gitSources GitSource[]
|
||||
gitHubApps GithubApp[]
|
||||
gitLabApps GitlabApp[]
|
||||
destinationDocker DestinationDocker[]
|
||||
permissions Permission[]
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
database Database[] @relation(references: [id])
|
||||
databaseId String?
|
||||
service Service[] @relation(references: [id])
|
||||
serviceId String?
|
||||
service Service[]
|
||||
users User[]
|
||||
}
|
||||
|
||||
model TeamInvitation {
|
||||
@@ -101,21 +105,20 @@ model Application {
|
||||
denoOptions String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
settings ApplicationSettings?
|
||||
teams Team[]
|
||||
destinationDockerId String?
|
||||
destinationDocker DestinationDocker? @relation(fields: [destinationDockerId], references: [id])
|
||||
gitSourceId String?
|
||||
gitSource GitSource? @relation(fields: [gitSourceId], references: [id])
|
||||
secrets Secret[]
|
||||
persistentStorage ApplicationPersistentStorage[]
|
||||
baseImage String?
|
||||
baseBuildImage String?
|
||||
gitSource GitSource? @relation(fields: [gitSourceId], references: [id])
|
||||
destinationDocker DestinationDocker? @relation(fields: [destinationDockerId], references: [id])
|
||||
persistentStorage ApplicationPersistentStorage[]
|
||||
settings ApplicationSettings?
|
||||
secrets Secret[]
|
||||
teams Team[]
|
||||
}
|
||||
|
||||
model ApplicationSettings {
|
||||
id String @id @default(cuid())
|
||||
application Application @relation(fields: [applicationId], references: [id])
|
||||
applicationId String @unique
|
||||
dualCerts Boolean @default(false)
|
||||
debug Boolean @default(false)
|
||||
@@ -123,26 +126,27 @@ model ApplicationSettings {
|
||||
autodeploy Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
application Application @relation(fields: [applicationId], references: [id])
|
||||
}
|
||||
|
||||
model ApplicationPersistentStorage {
|
||||
id String @id @default(cuid())
|
||||
application Application @relation(fields: [applicationId], references: [id])
|
||||
applicationId String
|
||||
path String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
application Application @relation(fields: [applicationId], references: [id])
|
||||
|
||||
@@unique([applicationId, path])
|
||||
}
|
||||
|
||||
model ServicePersistentStorage {
|
||||
id String @id @default(cuid())
|
||||
service Service @relation(fields: [serviceId], references: [id])
|
||||
serviceId String
|
||||
path String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
service Service @relation(fields: [serviceId], references: [id])
|
||||
|
||||
@@unique([serviceId, path])
|
||||
}
|
||||
@@ -155,8 +159,8 @@ model Secret {
|
||||
isBuildSecret Boolean @default(false)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
application Application @relation(fields: [applicationId], references: [id])
|
||||
applicationId String
|
||||
application Application @relation(fields: [applicationId], references: [id])
|
||||
|
||||
@@unique([name, applicationId, isPRMRSecret])
|
||||
}
|
||||
@@ -167,8 +171,8 @@ model ServiceSecret {
|
||||
value String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
service Service @relation(fields: [serviceId], references: [id])
|
||||
serviceId String
|
||||
service Service @relation(fields: [serviceId], references: [id])
|
||||
|
||||
@@unique([name, serviceId])
|
||||
}
|
||||
@@ -200,21 +204,37 @@ model DestinationDocker {
|
||||
id String @id @default(cuid())
|
||||
network String @unique
|
||||
name String
|
||||
engine String
|
||||
engine String?
|
||||
remoteEngine Boolean @default(false)
|
||||
remoteIpAddress String?
|
||||
remoteUser String?
|
||||
remotePort Int?
|
||||
remoteVerified Boolean @default(false)
|
||||
isCoolifyProxyUsed Boolean? @default(false)
|
||||
teams Team[]
|
||||
application Application[]
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
sshKeyId String?
|
||||
sshKey SshKey? @relation(fields: [sshKeyId], references: [id])
|
||||
application Application[]
|
||||
database Database[]
|
||||
service Service[]
|
||||
teams Team[]
|
||||
}
|
||||
|
||||
model SshKey {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
privateKey String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
teamId String?
|
||||
team Team? @relation(fields: [teamId], references: [id])
|
||||
destinationDocker DestinationDocker[]
|
||||
}
|
||||
|
||||
model GitSource {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
teams Team[]
|
||||
type String?
|
||||
apiUrl String?
|
||||
htmlUrl String?
|
||||
@@ -223,16 +243,16 @@ model GitSource {
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
githubAppId String? @unique
|
||||
githubApp GithubApp? @relation(fields: [githubAppId], references: [id])
|
||||
application Application[]
|
||||
gitlabAppId String? @unique
|
||||
gitlabApp GitlabApp? @relation(fields: [gitlabAppId], references: [id])
|
||||
githubApp GithubApp? @relation(fields: [githubAppId], references: [id])
|
||||
application Application[]
|
||||
teams Team[]
|
||||
}
|
||||
|
||||
model GithubApp {
|
||||
id String @id @default(cuid())
|
||||
name String? @unique
|
||||
teams Team[]
|
||||
appId Int?
|
||||
installationId Int?
|
||||
clientId String?
|
||||
@@ -242,13 +262,13 @@ model GithubApp {
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
gitSource GitSource?
|
||||
teams Team[]
|
||||
}
|
||||
|
||||
model GitlabApp {
|
||||
id String @id @default(cuid())
|
||||
oauthId Int @unique
|
||||
groupName String? @unique
|
||||
teams Team[]
|
||||
deployKeyId Int?
|
||||
privateSshKey String?
|
||||
publicSshKey String?
|
||||
@@ -258,6 +278,7 @@ model GitlabApp {
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
gitSource GitSource?
|
||||
teams Team[]
|
||||
}
|
||||
|
||||
model Database {
|
||||
@@ -271,22 +292,22 @@ model Database {
|
||||
dbUserPassword String?
|
||||
rootUser String?
|
||||
rootUserPassword String?
|
||||
settings DatabaseSettings?
|
||||
destinationDocker DestinationDocker? @relation(fields: [destinationDockerId], references: [id])
|
||||
destinationDockerId String?
|
||||
teams Team[]
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
destinationDocker DestinationDocker? @relation(fields: [destinationDockerId], references: [id])
|
||||
settings DatabaseSettings?
|
||||
teams Team[]
|
||||
}
|
||||
|
||||
model DatabaseSettings {
|
||||
id String @id @default(cuid())
|
||||
database Database @relation(fields: [databaseId], references: [id])
|
||||
databaseId String @unique
|
||||
isPublic Boolean @default(false)
|
||||
appendOnly Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
database Database @relation(fields: [databaseId], references: [id])
|
||||
}
|
||||
|
||||
model Service {
|
||||
@@ -297,23 +318,23 @@ model Service {
|
||||
dualCerts Boolean @default(false)
|
||||
type String?
|
||||
version String?
|
||||
teams Team[]
|
||||
destinationDockerId String?
|
||||
destinationDocker DestinationDocker? @relation(fields: [destinationDockerId], references: [id])
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
plausibleAnalytics PlausibleAnalytics?
|
||||
destinationDocker DestinationDocker? @relation(fields: [destinationDockerId], references: [id])
|
||||
fider Fider?
|
||||
ghost Ghost?
|
||||
hasura Hasura?
|
||||
meiliSearch MeiliSearch?
|
||||
minio Minio?
|
||||
moodle Moodle?
|
||||
plausibleAnalytics PlausibleAnalytics?
|
||||
persistentStorage ServicePersistentStorage[]
|
||||
serviceSecret ServiceSecret[]
|
||||
umami Umami?
|
||||
vscodeserver Vscodeserver?
|
||||
wordpress Wordpress?
|
||||
ghost Ghost?
|
||||
serviceSecret ServiceSecret[]
|
||||
meiliSearch MeiliSearch?
|
||||
persistentStorage ServicePersistentStorage[]
|
||||
umami Umami?
|
||||
hasura Hasura?
|
||||
fider Fider?
|
||||
moodle Moodle?
|
||||
teams Team[]
|
||||
}
|
||||
|
||||
model PlausibleAnalytics {
|
||||
@@ -328,9 +349,9 @@ model PlausibleAnalytics {
|
||||
secretKeyBase String?
|
||||
scriptName String @default("plausible.js")
|
||||
serviceId String @unique
|
||||
service Service @relation(fields: [serviceId], references: [id])
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
service Service @relation(fields: [serviceId], references: [id])
|
||||
}
|
||||
|
||||
model Minio {
|
||||
@@ -340,18 +361,18 @@ model Minio {
|
||||
publicPort Int?
|
||||
apiFqdn String?
|
||||
serviceId String @unique
|
||||
service Service @relation(fields: [serviceId], references: [id])
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
service Service @relation(fields: [serviceId], references: [id])
|
||||
}
|
||||
|
||||
model Vscodeserver {
|
||||
id String @id @default(cuid())
|
||||
password String
|
||||
serviceId String @unique
|
||||
service Service @relation(fields: [serviceId], references: [id])
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
service Service @relation(fields: [serviceId], references: [id])
|
||||
}
|
||||
|
||||
model Wordpress {
|
||||
@@ -374,9 +395,9 @@ model Wordpress {
|
||||
ftpHostKey String?
|
||||
ftpHostKeyPrivate String?
|
||||
serviceId String @unique
|
||||
service Service @relation(fields: [serviceId], references: [id])
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
service Service @relation(fields: [serviceId], references: [id])
|
||||
}
|
||||
|
||||
model Ghost {
|
||||
@@ -390,18 +411,18 @@ model Ghost {
|
||||
mariadbDatabase String?
|
||||
mariadbPublicPort Int?
|
||||
serviceId String @unique
|
||||
service Service @relation(fields: [serviceId], references: [id])
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
service Service @relation(fields: [serviceId], references: [id])
|
||||
}
|
||||
|
||||
model MeiliSearch {
|
||||
id String @id @default(cuid())
|
||||
masterKey String
|
||||
serviceId String @unique
|
||||
service Service @relation(fields: [serviceId], references: [id])
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
service Service @relation(fields: [serviceId], references: [id])
|
||||
}
|
||||
|
||||
model Umami {
|
||||
@@ -413,9 +434,9 @@ model Umami {
|
||||
postgresqlPublicPort Int?
|
||||
umamiAdminPassword String
|
||||
hashSalt String
|
||||
service Service @relation(fields: [serviceId], references: [id])
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
service Service @relation(fields: [serviceId], references: [id])
|
||||
}
|
||||
|
||||
model Hasura {
|
||||
@@ -426,9 +447,9 @@ model Hasura {
|
||||
postgresqlDatabase String
|
||||
postgresqlPublicPort Int?
|
||||
graphQLAdminPassword String
|
||||
service Service @relation(fields: [serviceId], references: [id])
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
service Service @relation(fields: [serviceId], references: [id])
|
||||
}
|
||||
|
||||
model Fider {
|
||||
@@ -448,9 +469,9 @@ model Fider {
|
||||
emailSmtpUser String?
|
||||
emailSmtpPassword String?
|
||||
emailSmtpEnableStartTls Boolean @default(false)
|
||||
service Service @relation(fields: [serviceId], references: [id])
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
service Service @relation(fields: [serviceId], references: [id])
|
||||
}
|
||||
|
||||
model Moodle {
|
||||
@@ -465,7 +486,7 @@ model Moodle {
|
||||
mariadbRootUserPassword String
|
||||
mariadbDatabase String
|
||||
mariadbPublicPort Int?
|
||||
service Service @relation(fields: [serviceId], references: [id])
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
service Service @relation(fields: [serviceId], references: [id])
|
||||
}
|
||||
|
||||
@@ -24,7 +24,8 @@ async function main() {
|
||||
data: {
|
||||
isRegistrationEnabled: true,
|
||||
proxyPassword: encrypt(generatePassword()),
|
||||
proxyUser: cuid()
|
||||
proxyUser: cuid(),
|
||||
arch: process.arch
|
||||
}
|
||||
});
|
||||
} else {
|
||||
|
||||
@@ -5,7 +5,7 @@ import env from '@fastify/env';
|
||||
import cookie from '@fastify/cookie';
|
||||
import path, { join } from 'path';
|
||||
import autoLoad from '@fastify/autoload';
|
||||
import { asyncExecShell, isDev, prisma } from './lib/common';
|
||||
import { asyncExecShell, isDev, listSettings, prisma } from './lib/common';
|
||||
import { scheduler } from './lib/scheduler';
|
||||
|
||||
declare module 'fastify' {
|
||||
@@ -101,10 +101,10 @@ fastify.listen({ port, host }, async (err: any, address: any) => {
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(`Coolify's API is listening on ${host}:${port}`);
|
||||
await initServer()
|
||||
await initServer();
|
||||
await scheduler.start('deployApplication');
|
||||
await scheduler.start('cleanupStorage');
|
||||
await scheduler.start('checkProxies')
|
||||
await scheduler.start('checkProxies');
|
||||
|
||||
// Check if no build is running
|
||||
|
||||
@@ -130,12 +130,37 @@ fastify.listen({ port, host }, async (err: any, address: any) => {
|
||||
if (!scheduler.workers.has('deployApplication')) await scheduler.start('deployApplication');
|
||||
}
|
||||
});
|
||||
await getArch();
|
||||
await getIPAddress();
|
||||
});
|
||||
async function getIPAddress() {
|
||||
const { publicIpv4, publicIpv6 } = await import('public-ip')
|
||||
try {
|
||||
const settings = await listSettings();
|
||||
if (!settings.ipv4) {
|
||||
const ipv4 = await publicIpv4({ timeout: 2000 })
|
||||
await prisma.setting.update({ where: { id: settings.id }, data: { ipv4 } })
|
||||
}
|
||||
|
||||
if (!settings.ipv6) {
|
||||
const ipv6 = await publicIpv6({ timeout: 2000 })
|
||||
await prisma.setting.update({ where: { id: settings.id }, data: { ipv6 } })
|
||||
}
|
||||
|
||||
} catch (error) { }
|
||||
}
|
||||
async function initServer() {
|
||||
try {
|
||||
await asyncExecShell(`docker network create --attachable coolify`);
|
||||
} catch (error) { }
|
||||
}
|
||||
async function getArch() {
|
||||
try {
|
||||
const settings = await prisma.setting.findFirst({})
|
||||
if (settings && !settings.arch) {
|
||||
await prisma.setting.update({ where: { id: settings.id }, data: { arch: process.arch } })
|
||||
}
|
||||
} catch (error) { }
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
import { parentPort } from 'node:worker_threads';
|
||||
import { prisma, startTraefikTCPProxy, generateDatabaseConfiguration, startTraefikProxy, asyncExecShell } from '../lib/common';
|
||||
import { checkContainer, getEngine } from '../lib/docker';
|
||||
import { prisma, startTraefikTCPProxy, generateDatabaseConfiguration, startTraefikProxy, executeDockerCmd } from '../lib/common';
|
||||
import { checkContainer } from '../lib/docker';
|
||||
|
||||
(async () => {
|
||||
if (parentPort) {
|
||||
// Coolify Proxy
|
||||
// Coolify Proxy local
|
||||
const engine = '/var/run/docker.sock';
|
||||
const localDocker = await prisma.destinationDocker.findFirst({
|
||||
where: { engine, network: 'coolify' }
|
||||
});
|
||||
if (localDocker && localDocker.isCoolifyProxyUsed) {
|
||||
// Remove HAProxy
|
||||
const found = await checkContainer(engine, 'coolify-haproxy');
|
||||
const host = getEngine(engine);
|
||||
const found = await checkContainer({ dockerId: localDocker.id, container: 'coolify-haproxy' });
|
||||
if (found) {
|
||||
await asyncExecShell(
|
||||
`DOCKER_HOST="${host}" docker stop -t 0 coolify-haproxy && docker rm coolify-haproxy`
|
||||
);
|
||||
await executeDockerCmd({
|
||||
dockerId: localDocker.id,
|
||||
command: `docker stop -t 0 coolify-haproxy && docker rm coolify-haproxy`
|
||||
})
|
||||
}
|
||||
await startTraefikProxy(engine);
|
||||
await startTraefikProxy(localDocker.id);
|
||||
|
||||
}
|
||||
|
||||
@@ -32,12 +32,14 @@ import { checkContainer, getEngine } from '../lib/docker';
|
||||
if (destinationDockerId && destinationDocker.isCoolifyProxyUsed) {
|
||||
const { privatePort } = generateDatabaseConfiguration(database);
|
||||
// Remove HAProxy
|
||||
const found = await checkContainer(engine, `haproxy-for-${publicPort}`);
|
||||
const host = getEngine(engine);
|
||||
const found = await checkContainer({
|
||||
dockerId: localDocker.id, container: `haproxy-for-${publicPort}`
|
||||
});
|
||||
if (found) {
|
||||
await asyncExecShell(
|
||||
`DOCKER_HOST="${host}" docker stop -t 0 haproxy-for-${publicPort} && docker rm haproxy-for-${publicPort}`
|
||||
);
|
||||
await executeDockerCmd({
|
||||
dockerId: localDocker.id,
|
||||
command: `docker stop -t 0 haproxy-for-${publicPort} && docker rm haproxy-for-${publicPort}`
|
||||
})
|
||||
}
|
||||
await startTraefikTCPProxy(destinationDocker, id, publicPort, privatePort);
|
||||
|
||||
@@ -52,12 +54,12 @@ import { checkContainer, getEngine } from '../lib/docker';
|
||||
const { destinationDockerId, destinationDocker, id } = service;
|
||||
if (destinationDockerId && destinationDocker.isCoolifyProxyUsed) {
|
||||
// Remove HAProxy
|
||||
const found = await checkContainer(engine, `haproxy-for-${ftpPublicPort}`);
|
||||
const host = getEngine(engine);
|
||||
const found = await checkContainer({ dockerId: localDocker.id, container: `haproxy-for-${ftpPublicPort}` });
|
||||
if (found) {
|
||||
await asyncExecShell(
|
||||
`DOCKER_HOST="${host}" docker stop -t 0 haproxy-for-${ftpPublicPort} && docker rm haproxy-for-${ftpPublicPort} `
|
||||
);
|
||||
await executeDockerCmd({
|
||||
dockerId: localDocker.id,
|
||||
command: `docker stop -t 0 haproxy -for-${ftpPublicPort} && docker rm haproxy-for-${ftpPublicPort}`
|
||||
})
|
||||
}
|
||||
await startTraefikTCPProxy(destinationDocker, id, ftpPublicPort, 22, 'wordpressftp');
|
||||
}
|
||||
@@ -73,12 +75,12 @@ import { checkContainer, getEngine } from '../lib/docker';
|
||||
const { destinationDockerId, destinationDocker, id } = service;
|
||||
if (destinationDockerId && destinationDocker.isCoolifyProxyUsed) {
|
||||
// Remove HAProxy
|
||||
const found = await checkContainer(engine, `${id}-${publicPort}`);
|
||||
const host = getEngine(engine);
|
||||
const found = await checkContainer({ dockerId: localDocker.id, container: `${id}-${publicPort}` });
|
||||
if (found) {
|
||||
await asyncExecShell(
|
||||
`DOCKER_HOST="${host}" docker stop -t 0 ${id}-${publicPort} && docker rm ${id}-${publicPort}`
|
||||
);
|
||||
await executeDockerCmd({
|
||||
dockerId: localDocker.id,
|
||||
command: `docker stop -t 0 ${id}-${publicPort} && docker rm ${id}-${publicPort} `
|
||||
})
|
||||
}
|
||||
await startTraefikTCPProxy(destinationDocker, id, publicPort, 9000);
|
||||
}
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import { parentPort } from 'node:worker_threads';
|
||||
import { asyncExecShell, cleanupDockerStorage, isDev, prisma, version } from '../lib/common';
|
||||
import { getEngine } from '../lib/docker';
|
||||
import { asyncExecShell, cleanupDockerStorage, executeDockerCmd, isDev, prisma, version } from '../lib/common';
|
||||
|
||||
(async () => {
|
||||
if (parentPort) {
|
||||
const destinationDockers = await prisma.destinationDocker.findMany();
|
||||
const engines = [...new Set(destinationDockers.map(({ engine }) => engine))];
|
||||
for (const engine of engines) {
|
||||
let enginesDone = new Set()
|
||||
for (const destination of destinationDockers) {
|
||||
if (enginesDone.has(destination.engine) || enginesDone.has(destination.remoteIpAddress)) return
|
||||
if (destination.engine) enginesDone.add(destination.engine)
|
||||
if (destination.remoteIpAddress) enginesDone.add(destination.remoteIpAddress)
|
||||
|
||||
let lowDiskSpace = false;
|
||||
const host = getEngine(engine);
|
||||
try {
|
||||
let stdout = null
|
||||
if (!isDev) {
|
||||
const output = await asyncExecShell(
|
||||
`DOCKER_HOST=${host} docker exec coolify sh -c 'df -kPT /'`
|
||||
);
|
||||
const output = await executeDockerCmd({ dockerId: destination.id, command: `CONTAINER=$(docker ps -lq | head -1) && docker exec $CONTAINER sh -c 'df -kPT /'` })
|
||||
stdout = output.stdout;
|
||||
} else {
|
||||
const output = await asyncExecShell(
|
||||
@@ -53,7 +53,7 @@ import { getEngine } from '../lib/docker';
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
await cleanupDockerStorage(host, lowDiskSpace, false)
|
||||
await cleanupDockerStorage(destination.id, lowDiskSpace, false)
|
||||
}
|
||||
await prisma.$disconnect();
|
||||
} else process.exit(0);
|
||||
|
||||
@@ -4,8 +4,7 @@ import fs from 'fs/promises';
|
||||
import yaml from 'js-yaml';
|
||||
|
||||
import { copyBaseConfigurationFiles, makeLabelForStandaloneApplication, saveBuildLog, setDefaultConfiguration } from '../lib/buildPacks/common';
|
||||
import { asyncExecShell, createDirectories, decrypt, getDomain, prisma } from '../lib/common';
|
||||
import { dockerInstance, getEngine } from '../lib/docker';
|
||||
import { createDirectories, decrypt, executeDockerCmd, getDomain, prisma } from '../lib/common';
|
||||
import * as importers from '../lib/importers';
|
||||
import * as buildpacks from '../lib/buildPacks';
|
||||
|
||||
@@ -104,9 +103,6 @@ import * as buildpacks from '../lib/buildPacks';
|
||||
destinationType = 'docker';
|
||||
}
|
||||
if (destinationType === 'docker') {
|
||||
const docker = dockerInstance({ destinationDocker });
|
||||
const host = getEngine(destinationDocker.engine);
|
||||
|
||||
await prisma.build.update({ where: { id: buildId }, data: { status: 'running' } });
|
||||
const { workdir, repodir } = await createDirectories({ repository, buildId });
|
||||
const configuration = await setDefaultConfiguration(message);
|
||||
@@ -185,18 +181,23 @@ import * as buildpacks from '../lib/buildPacks';
|
||||
} else {
|
||||
deployNeeded = true;
|
||||
}
|
||||
const image = await docker.engine.getImage(`${applicationId}:${tag}`);
|
||||
|
||||
let imageFound = false;
|
||||
try {
|
||||
await image.inspect();
|
||||
await executeDockerCmd({
|
||||
dockerId: destinationDocker.id,
|
||||
command: `docker image inspect ${applicationId}:${tag}`
|
||||
})
|
||||
imageFound = true;
|
||||
} catch (error) {
|
||||
//
|
||||
}
|
||||
if (!imageFound || deployNeeded) {
|
||||
// if (!imageFound || deployNeeded) {
|
||||
if (true) {
|
||||
await copyBaseConfigurationFiles(buildPack, workdir, buildId, applicationId, baseImage);
|
||||
if (buildpacks[buildPack])
|
||||
await buildpacks[buildPack]({
|
||||
dockerId: destinationDocker.id,
|
||||
buildId,
|
||||
applicationId,
|
||||
domain,
|
||||
@@ -212,7 +213,6 @@ import * as buildpacks from '../lib/buildPacks';
|
||||
commit,
|
||||
tag,
|
||||
workdir,
|
||||
docker,
|
||||
port: exposePort ? `${exposePort}:${port}` : port,
|
||||
installCommand,
|
||||
buildCommand,
|
||||
@@ -238,8 +238,8 @@ import * as buildpacks from '../lib/buildPacks';
|
||||
await saveBuildLog({ line: 'Build image already available - no rebuild required.', buildId, applicationId });
|
||||
}
|
||||
try {
|
||||
await asyncExecShell(`DOCKER_HOST=${host} docker stop -t 0 ${imageId}`);
|
||||
await asyncExecShell(`DOCKER_HOST=${host} docker rm ${imageId}`);
|
||||
await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker stop -t 0 ${imageId}` })
|
||||
await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker rm ${imageId}` })
|
||||
} catch (error) {
|
||||
//
|
||||
}
|
||||
@@ -299,7 +299,7 @@ import * as buildpacks from '../lib/buildPacks';
|
||||
container_name: imageId,
|
||||
volumes,
|
||||
env_file: envFound ? [`${workdir}/.env`] : [],
|
||||
networks: [docker.network],
|
||||
networks: [destinationDocker.network],
|
||||
labels,
|
||||
depends_on: [],
|
||||
restart: 'always',
|
||||
@@ -318,16 +318,14 @@ import * as buildpacks from '../lib/buildPacks';
|
||||
}
|
||||
},
|
||||
networks: {
|
||||
[docker.network]: {
|
||||
[destinationDocker.network]: {
|
||||
external: true
|
||||
}
|
||||
},
|
||||
volumes: Object.assign({}, ...composeVolumes)
|
||||
};
|
||||
await fs.writeFile(`${workdir}/docker-compose.yml`, yaml.dump(composeFile));
|
||||
await asyncExecShell(
|
||||
`DOCKER_HOST=${host} docker compose --project-directory ${workdir} up -d`
|
||||
);
|
||||
await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose --project-directory ${workdir} up -d` })
|
||||
await saveBuildLog({ line: 'Deployment successful!', buildId, applicationId });
|
||||
} catch (error) {
|
||||
await saveBuildLog({ line: error, buildId, applicationId });
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { asyncExecShell, base64Encode, generateTimestamp, getDomain, isDev, prisma, version } from "../common";
|
||||
import { scheduler } from "../scheduler";
|
||||
import { base64Encode, executeDockerCmd, generateTimestamp, getDomain, isDev, prisma, version } from "../common";
|
||||
import { promises as fs } from 'fs';
|
||||
import { day } from "../dayjs";
|
||||
|
||||
const staticApps = ['static', 'react', 'vuejs', 'svelte', 'gatsby', 'astro', 'eleventy'];
|
||||
const nodeBased = [
|
||||
'react',
|
||||
@@ -511,8 +511,8 @@ export async function buildImage({
|
||||
applicationId,
|
||||
tag,
|
||||
workdir,
|
||||
docker,
|
||||
buildId,
|
||||
dockerId,
|
||||
isCache = false,
|
||||
debug = false,
|
||||
dockerFileLocation = '/Dockerfile'
|
||||
@@ -522,6 +522,9 @@ export async function buildImage({
|
||||
} else {
|
||||
await saveBuildLog({ line: `Building image started.`, buildId, applicationId });
|
||||
}
|
||||
if (debug) {
|
||||
await saveBuildLog({ line: `\n###############\nIMPORTANT: Due to some issues during implementing Remote Docker Engine, the builds logs are not streamed at the moment. You will see the full build log when the build is finished!\n###############`, buildId, applicationId });
|
||||
}
|
||||
if (!debug && isCache) {
|
||||
await saveBuildLog({
|
||||
line: `Debug turned off. To see more details, allow it in the configuration.`,
|
||||
@@ -529,16 +532,61 @@ export async function buildImage({
|
||||
applicationId
|
||||
});
|
||||
}
|
||||
|
||||
const stream = await docker.engine.buildImage(
|
||||
{ src: ['.'], context: workdir },
|
||||
{
|
||||
dockerfile: isCache ? `${dockerFileLocation}-cache` : dockerFileLocation,
|
||||
t: `${applicationId}:${tag}${isCache ? '-cache' : ''}`
|
||||
const dockerFile = isCache ? `${dockerFileLocation}-cache` : `${dockerFileLocation}`
|
||||
const cache = `${applicationId}:${tag}${isCache ? '-cache' : ''}`
|
||||
const { stderr } = await executeDockerCmd({ dockerId, command: `docker build --progress plain -f ${workdir}/${dockerFile} -t ${cache} ${workdir}` })
|
||||
if (debug) {
|
||||
const array = stderr.split('\n')
|
||||
for (const line of array) {
|
||||
if (line !== '\n') {
|
||||
await saveBuildLog({
|
||||
line: `${line.replace('\n', '')}`,
|
||||
buildId,
|
||||
applicationId
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
await streamEvents({ stream, docker, buildId, applicationId, debug });
|
||||
await saveBuildLog({ line: `Building image successful!`, buildId, applicationId });
|
||||
}
|
||||
|
||||
|
||||
// await new Promise((resolve, reject) => {
|
||||
// const command = spawn(`docker`, ['build', '-f', `${workdir}${dockerFile}`, '-t', `${cache}`,`${workdir}`], {
|
||||
// env: {
|
||||
// DOCKER_HOST: 'ssh://root@95.217.178.202',
|
||||
// DOCKER_BUILDKIT: '1'
|
||||
// }
|
||||
// });
|
||||
// command.stdout.on('data', function (data) {
|
||||
// console.log('stdout: ' + data);
|
||||
// });
|
||||
// command.stderr.on('data', function (data) {
|
||||
// console.log('stderr: ' + data);
|
||||
// });
|
||||
// command.on('error', function (error) {
|
||||
// console.log(error)
|
||||
// reject(error)
|
||||
// })
|
||||
// command.on('exit', function (code) {
|
||||
// console.log('exit code: ' + code);
|
||||
// resolve(code)
|
||||
// });
|
||||
// })
|
||||
|
||||
|
||||
// console.log({ stdout, stderr })
|
||||
// const stream = await docker.engine.buildImage(
|
||||
// { src: ['.'], context: workdir },
|
||||
// {
|
||||
// dockerfile: isCache ? `${dockerFileLocation}-cache` : dockerFileLocation,
|
||||
// t: `${applicationId}:${tag}${isCache ? '-cache' : ''}`
|
||||
// }
|
||||
// );
|
||||
// await streamEvents({ stream, docker, buildId, applicationId, debug });
|
||||
if (isCache) {
|
||||
await saveBuildLog({ line: `Building cache image successful.`, buildId, applicationId });
|
||||
} else {
|
||||
await saveBuildLog({ line: `Building image successful.`, buildId, applicationId });
|
||||
}
|
||||
}
|
||||
|
||||
export async function streamEvents({ stream, docker, buildId, applicationId, debug }) {
|
||||
@@ -617,18 +665,16 @@ export function makeLabelForStandaloneApplication({
|
||||
|
||||
export async function buildCacheImageWithNode(data, imageForBuild) {
|
||||
const {
|
||||
applicationId,
|
||||
tag,
|
||||
workdir,
|
||||
docker,
|
||||
buildId,
|
||||
baseDirectory,
|
||||
installCommand,
|
||||
buildCommand,
|
||||
debug,
|
||||
secrets,
|
||||
pullmergeRequestId
|
||||
} = data;
|
||||
|
||||
|
||||
const isPnpm = checkPnpm(installCommand, buildCommand);
|
||||
const Dockerfile: Array<string> = [];
|
||||
Dockerfile.push(`FROM ${imageForBuild}`);
|
||||
@@ -659,11 +705,12 @@ export async function buildCacheImageWithNode(data, imageForBuild) {
|
||||
Dockerfile.push(`COPY .${baseDirectory || ''} ./`);
|
||||
Dockerfile.push(`RUN ${buildCommand}`);
|
||||
await fs.writeFile(`${workdir}/Dockerfile-cache`, Dockerfile.join('\n'));
|
||||
await buildImage({ applicationId, tag, workdir, docker, buildId, isCache: true, debug });
|
||||
await buildImage({ ...data, isCache: true });
|
||||
}
|
||||
|
||||
export async function buildCacheImageForLaravel(data, imageForBuild) {
|
||||
const { applicationId, tag, workdir, docker, buildId, debug, secrets, pullmergeRequestId } = data;
|
||||
const { workdir, buildId, secrets, pullmergeRequestId } = data;
|
||||
|
||||
const Dockerfile: Array<string> = [];
|
||||
Dockerfile.push(`FROM ${imageForBuild}`);
|
||||
Dockerfile.push('WORKDIR /app');
|
||||
@@ -687,22 +734,16 @@ export async function buildCacheImageForLaravel(data, imageForBuild) {
|
||||
Dockerfile.push(`COPY resources /app/resources`);
|
||||
Dockerfile.push(`RUN yarn install && yarn production`);
|
||||
await fs.writeFile(`${workdir}/Dockerfile-cache`, Dockerfile.join('\n'));
|
||||
await buildImage({ applicationId, tag, workdir, docker, buildId, isCache: true, debug });
|
||||
await buildImage({ ...data, isCache: true });
|
||||
}
|
||||
|
||||
export async function buildCacheImageWithCargo(data, imageForBuild) {
|
||||
const {
|
||||
applicationId,
|
||||
tag,
|
||||
workdir,
|
||||
docker,
|
||||
buildId,
|
||||
baseDirectory,
|
||||
installCommand,
|
||||
buildCommand,
|
||||
debug,
|
||||
secrets
|
||||
} = data;
|
||||
|
||||
const Dockerfile: Array<string> = [];
|
||||
Dockerfile.push(`FROM ${imageForBuild} as planner-${applicationId}`);
|
||||
Dockerfile.push(`LABEL coolify.buildId=${buildId}`);
|
||||
@@ -717,5 +758,5 @@ export async function buildCacheImageWithCargo(data, imageForBuild) {
|
||||
Dockerfile.push(`COPY --from=planner-${applicationId} /app/recipe.json recipe.json`);
|
||||
Dockerfile.push('RUN cargo chef cook --release --recipe-path recipe.json');
|
||||
await fs.writeFile(`${workdir}/Dockerfile-cache`, Dockerfile.join('\n'));
|
||||
await buildImage({ applicationId, tag, workdir, docker, buildId, isCache: true, debug });
|
||||
await buildImage({ ...data, isCache: true });
|
||||
}
|
||||
@@ -1,18 +1,18 @@
|
||||
import { promises as fs } from 'fs';
|
||||
import { buildImage } from './common';
|
||||
|
||||
export default async function ({
|
||||
applicationId,
|
||||
debug,
|
||||
tag,
|
||||
workdir,
|
||||
docker,
|
||||
buildId,
|
||||
baseDirectory,
|
||||
secrets,
|
||||
pullmergeRequestId,
|
||||
dockerFileLocation
|
||||
}) {
|
||||
export default async function (data) {
|
||||
let {
|
||||
applicationId,
|
||||
debug,
|
||||
tag,
|
||||
workdir,
|
||||
buildId,
|
||||
baseDirectory,
|
||||
secrets,
|
||||
pullmergeRequestId,
|
||||
dockerFileLocation
|
||||
} = data
|
||||
try {
|
||||
const file = `${workdir}${dockerFileLocation}`;
|
||||
let dockerFileOut = `${workdir}`;
|
||||
@@ -45,7 +45,7 @@ export default async function ({
|
||||
}
|
||||
|
||||
await fs.writeFile(`${dockerFileOut}${dockerFileLocation}`, Dockerfile.join('\n'));
|
||||
await buildImage({ applicationId, tag, workdir, docker, buildId, debug, dockerFileLocation });
|
||||
await buildImage(data);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,33 +1,43 @@
|
||||
import { asyncExecShell } from './common';
|
||||
import Dockerode from 'dockerode';
|
||||
export function getEngine(engine: string): string {
|
||||
return engine === '/var/run/docker.sock' ? 'unix:///var/run/docker.sock' : engine;
|
||||
}
|
||||
export function dockerInstance({ destinationDocker }): { engine: Dockerode; network: string } {
|
||||
return {
|
||||
engine: new Dockerode({
|
||||
socketPath: destinationDocker.engine
|
||||
}),
|
||||
network: destinationDocker.network
|
||||
};
|
||||
}
|
||||
import { executeDockerCmd } from './common';
|
||||
|
||||
export async function checkContainer(engine: string, container: string, remove = false): Promise<boolean> {
|
||||
const host = getEngine(engine);
|
||||
export function formatLabelsOnDocker(data) {
|
||||
return data.trim().split('\n').map(a => JSON.parse(a)).map((container) => {
|
||||
const labels = container.Labels.split(',')
|
||||
let jsonLabels = {}
|
||||
labels.forEach(l => {
|
||||
const name = l.split('=')[0]
|
||||
const value = l.split('=')[1]
|
||||
jsonLabels = { ...jsonLabels, ...{ [name]: value } }
|
||||
})
|
||||
container.Labels = jsonLabels;
|
||||
return container
|
||||
})
|
||||
}
|
||||
export async function checkContainer({ dockerId, container, remove = false }: { dockerId: string, container: string, remove?: boolean }): Promise<boolean> {
|
||||
let containerFound = false;
|
||||
|
||||
try {
|
||||
const { stdout } = await asyncExecShell(
|
||||
`DOCKER_HOST="${host}" docker inspect --format '{{json .State}}' ${container}`
|
||||
);
|
||||
const { stdout } = await executeDockerCmd({
|
||||
dockerId,
|
||||
command:
|
||||
`docker inspect --format '{{json .State}}' ${container}`
|
||||
});
|
||||
|
||||
const parsedStdout = JSON.parse(stdout);
|
||||
const status = parsedStdout.Status;
|
||||
const isRunning = status === 'running';
|
||||
if (status === 'created') {
|
||||
await asyncExecShell(`DOCKER_HOST="${host}" docker rm ${container}`);
|
||||
await executeDockerCmd({
|
||||
dockerId,
|
||||
command:
|
||||
`docker rm ${container}`
|
||||
});
|
||||
}
|
||||
if (remove && status === 'exited') {
|
||||
await asyncExecShell(`DOCKER_HOST="${host}" docker rm ${container}`);
|
||||
await executeDockerCmd({
|
||||
dockerId,
|
||||
command:
|
||||
`docker rm ${container}`
|
||||
});
|
||||
}
|
||||
if (isRunning) {
|
||||
containerFound = true;
|
||||
@@ -38,13 +48,10 @@ export async function checkContainer(engine: string, container: string, remove =
|
||||
return containerFound;
|
||||
}
|
||||
|
||||
export async function isContainerExited(engine: string, containerName: string): Promise<boolean> {
|
||||
export async function isContainerExited(dockerId: string, containerName: string): Promise<boolean> {
|
||||
let isExited = false;
|
||||
const host = getEngine(engine);
|
||||
try {
|
||||
const { stdout } = await asyncExecShell(
|
||||
`DOCKER_HOST="${host}" docker inspect -f '{{.State.Status}}' ${containerName}`
|
||||
);
|
||||
const { stdout } = await executeDockerCmd({ dockerId, command: `docker inspect -f '{{.State.Status}}' ${containerName}` })
|
||||
if (stdout.trim() === 'exited') {
|
||||
isExited = true;
|
||||
}
|
||||
@@ -57,19 +64,17 @@ export async function isContainerExited(engine: string, containerName: string):
|
||||
|
||||
export async function removeContainer({
|
||||
id,
|
||||
engine
|
||||
dockerId
|
||||
}: {
|
||||
id: string;
|
||||
engine: string;
|
||||
dockerId: string;
|
||||
}): Promise<void> {
|
||||
const host = getEngine(engine);
|
||||
try {
|
||||
const { stdout } = await asyncExecShell(
|
||||
`DOCKER_HOST=${host} docker inspect --format '{{json .State}}' ${id}`
|
||||
);
|
||||
const { stdout } = await executeDockerCmd({ dockerId, command: `docker inspect --format '{{json .State}}' ${id}` })
|
||||
|
||||
if (JSON.parse(stdout).Running) {
|
||||
await asyncExecShell(`DOCKER_HOST=${host} docker stop -t 0 ${id}`);
|
||||
await asyncExecShell(`DOCKER_HOST=${host} docker rm ${id}`);
|
||||
await executeDockerCmd({ dockerId, command: `docker stop -t 0 ${id}` })
|
||||
await executeDockerCmd({ dockerId, command: `docker rm ${id}` })
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
|
||||
@@ -5,12 +5,12 @@ import axios from 'axios';
|
||||
import { FastifyReply } from 'fastify';
|
||||
import { day } from '../../../../lib/dayjs';
|
||||
import { setDefaultBaseImage, setDefaultConfiguration } from '../../../../lib/buildPacks/common';
|
||||
import { asyncExecShell, checkDomainsIsValidInDNS, checkDoubleBranch, decrypt, encrypt, errorHandler, generateSshKeyPair, getContainerUsage, getDomain, isDev, isDomainConfigured, prisma, stopBuild, uniqueName } from '../../../../lib/common';
|
||||
import { checkContainer, dockerInstance, getEngine, isContainerExited, removeContainer } from '../../../../lib/docker';
|
||||
import { checkDomainsIsValidInDNS, checkDoubleBranch, decrypt, encrypt, errorHandler, executeDockerCmd, generateSshKeyPair, getContainerUsage, getDomain, getFreeExposedPort, isDev, isDomainConfigured, prisma, stopBuild, uniqueName } from '../../../../lib/common';
|
||||
import { checkContainer, formatLabelsOnDocker, isContainerExited, removeContainer } from '../../../../lib/docker';
|
||||
import { scheduler } from '../../../../lib/scheduler';
|
||||
|
||||
import type { FastifyRequest } from 'fastify';
|
||||
import type { GetImages, CancelDeployment, CheckDNS, CheckRepository, DeleteApplication, DeleteSecret, DeleteStorage, GetApplicationLogs, GetBuildIdLogs, GetBuildLogs, SaveApplication, SaveApplicationSettings, SaveApplicationSource, SaveDeployKey, SaveDestination, SaveSecret, SaveStorage, DeployApplication } from './types';
|
||||
import type { GetImages, CancelDeployment, CheckDNS, CheckRepository, DeleteApplication, DeleteSecret, DeleteStorage, GetApplicationLogs, GetBuildIdLogs, GetBuildLogs, SaveApplication, SaveApplicationSettings, SaveApplicationSource, SaveDeployKey, SaveDestination, SaveSecret, SaveStorage, DeployApplication, CheckDomain, StopPreviewApplication } from './types';
|
||||
import { OnlyId } from '../../../../types';
|
||||
|
||||
export async function listApplications(request: FastifyRequest) {
|
||||
@@ -18,7 +18,7 @@ export async function listApplications(request: FastifyRequest) {
|
||||
const { teamId } = request.user
|
||||
const applications = await prisma.application.findMany({
|
||||
where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } },
|
||||
include: { teams: true }
|
||||
include: { teams: true, destinationDocker: true }
|
||||
});
|
||||
const settings = await prisma.setting.findFirst()
|
||||
return {
|
||||
@@ -57,7 +57,28 @@ export async function getImages(request: FastifyRequest<GetImages>) {
|
||||
}
|
||||
|
||||
|
||||
return { baseImage, baseBuildImage, baseBuildImages, baseImages, publishDirectory, port }
|
||||
return { baseBuildImage, baseBuildImages, publishDirectory, port }
|
||||
} catch ({ status, message }) {
|
||||
return errorHandler({ status, message })
|
||||
}
|
||||
}
|
||||
export async function getApplicationStatus(request: FastifyRequest<OnlyId>) {
|
||||
try {
|
||||
const { id } = request.params
|
||||
const { teamId } = request.user
|
||||
let isRunning = false;
|
||||
let isExited = false;
|
||||
|
||||
const application: any = await getApplicationFromDB(id, teamId);
|
||||
if (application?.destinationDockerId) {
|
||||
isRunning = await checkContainer({ dockerId: application.destinationDocker.id, container: id });
|
||||
isExited = await isContainerExited(application.destinationDocker.id, id);
|
||||
}
|
||||
return {
|
||||
isQueueActive: scheduler.workers.has('deployApplication'),
|
||||
isRunning,
|
||||
isExited,
|
||||
};
|
||||
} catch ({ status, message }) {
|
||||
return errorHandler({ status, message })
|
||||
}
|
||||
@@ -68,17 +89,9 @@ export async function getApplication(request: FastifyRequest<OnlyId>) {
|
||||
const { id } = request.params
|
||||
const { teamId } = request.user
|
||||
const appId = process.env['COOLIFY_APP_ID'];
|
||||
let isRunning = false;
|
||||
let isExited = false;
|
||||
const application: any = await getApplicationFromDB(id, teamId);
|
||||
if (application?.destinationDockerId && application.destinationDocker?.engine) {
|
||||
isRunning = await checkContainer(application.destinationDocker.engine, id);
|
||||
isExited = await isContainerExited(application.destinationDocker.engine, id);
|
||||
}
|
||||
|
||||
return {
|
||||
isQueueActive: scheduler.workers.has('deployApplication'),
|
||||
isRunning,
|
||||
isExited,
|
||||
application,
|
||||
appId
|
||||
};
|
||||
@@ -279,16 +292,35 @@ export async function saveApplicationSettings(request: FastifyRequest<SaveApplic
|
||||
}
|
||||
}
|
||||
|
||||
export async function stopPreviewApplication(request: FastifyRequest<StopPreviewApplication>, reply: FastifyReply) {
|
||||
try {
|
||||
const { id } = request.params
|
||||
const { pullmergeRequestId } = request.body
|
||||
const { teamId } = request.user
|
||||
const application: any = await getApplicationFromDB(id, teamId);
|
||||
if (application?.destinationDockerId) {
|
||||
const container = `${id}-${pullmergeRequestId}`
|
||||
const { id: dockerId } = application.destinationDocker;
|
||||
const found = await checkContainer({ dockerId, container });
|
||||
if (found) {
|
||||
await removeContainer({ id: container, dockerId: application.destinationDocker.id });
|
||||
}
|
||||
}
|
||||
return reply.code(201).send();
|
||||
} catch ({ status, message }) {
|
||||
return errorHandler({ status, message })
|
||||
}
|
||||
}
|
||||
export async function stopApplication(request: FastifyRequest<OnlyId>, reply: FastifyReply) {
|
||||
try {
|
||||
const { id } = request.params
|
||||
const { teamId } = request.user
|
||||
const application: any = await getApplicationFromDB(id, teamId);
|
||||
if (application?.destinationDockerId && application.destinationDocker?.engine) {
|
||||
const { engine } = application.destinationDocker;
|
||||
const found = await checkContainer(engine, id);
|
||||
if (application?.destinationDockerId) {
|
||||
const { id: dockerId } = application.destinationDocker;
|
||||
const found = await checkContainer({ dockerId, container: id });
|
||||
if (found) {
|
||||
await removeContainer({ id, engine });
|
||||
await removeContainer({ id, dockerId: application.destinationDocker.id });
|
||||
}
|
||||
}
|
||||
return reply.code(201).send();
|
||||
@@ -304,17 +336,17 @@ export async function deleteApplication(request: FastifyRequest<DeleteApplicatio
|
||||
where: { id },
|
||||
include: { destinationDocker: true }
|
||||
});
|
||||
if (application?.destinationDockerId && application.destinationDocker?.engine && application.destinationDocker?.network) {
|
||||
const host = getEngine(application.destinationDocker.engine);
|
||||
const { stdout: containers } = await asyncExecShell(
|
||||
`DOCKER_HOST=${host} docker ps -a --filter network=${application.destinationDocker.network} --filter name=${id} --format '{{json .}}'`
|
||||
);
|
||||
if (application?.destinationDockerId && application.destinationDocker?.network) {
|
||||
const { stdout: containers } = await executeDockerCmd({
|
||||
dockerId: application.destinationDocker.id,
|
||||
command: `docker ps -a --filter network=${application.destinationDocker.network} --filter name=${id} --format '{{json .}}'`
|
||||
})
|
||||
if (containers) {
|
||||
const containersArray = containers.trim().split('\n');
|
||||
for (const container of containersArray) {
|
||||
const containerObj = JSON.parse(container);
|
||||
const id = containerObj.ID;
|
||||
await removeContainer({ id, engine: application.destinationDocker.engine });
|
||||
await removeContainer({ id, dockerId: application.destinationDocker.id });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -333,33 +365,48 @@ export async function deleteApplication(request: FastifyRequest<DeleteApplicatio
|
||||
return errorHandler({ status, message })
|
||||
}
|
||||
}
|
||||
|
||||
export async function checkDomain(request: FastifyRequest<CheckDomain>) {
|
||||
try {
|
||||
const { id } = request.params
|
||||
const { domain } = request.query
|
||||
const { fqdn, settings: { dualCerts } } = await prisma.application.findUnique({ where: { id }, include: { settings: true } })
|
||||
return await checkDomainsIsValidInDNS({ hostname: domain, fqdn, dualCerts });
|
||||
} catch ({ status, message }) {
|
||||
return errorHandler({ status, message })
|
||||
}
|
||||
}
|
||||
export async function checkDNS(request: FastifyRequest<CheckDNS>) {
|
||||
try {
|
||||
const { id } = request.params
|
||||
|
||||
let { exposePort, fqdn, forceSave, dualCerts } = request.body
|
||||
fqdn = fqdn.toLowerCase();
|
||||
|
||||
if (fqdn) fqdn = fqdn.toLowerCase();
|
||||
if (exposePort) exposePort = Number(exposePort);
|
||||
|
||||
const { destinationDocker: { id: dockerId, remoteIpAddress, remoteEngine }, exposePort: configuredPort } = await prisma.application.findUnique({ where: { id }, include: { destinationDocker: true } })
|
||||
const { isDNSCheckEnabled } = await prisma.setting.findFirst({});
|
||||
const found = await isDomainConfigured({ id, fqdn });
|
||||
|
||||
const found = await isDomainConfigured({ id, fqdn, dockerId });
|
||||
if (found) {
|
||||
throw { status: 500, message: `Domain ${getDomain(fqdn).replace('www.', '')} is already in use!` }
|
||||
}
|
||||
if (exposePort) {
|
||||
exposePort = Number(exposePort);
|
||||
|
||||
if (exposePort < 1024 || exposePort > 65535) {
|
||||
throw { status: 500, message: `Exposed Port needs to be between 1024 and 65535.` }
|
||||
}
|
||||
const { default: getPort } = await import('get-port');
|
||||
const publicPort = await getPort({ port: exposePort });
|
||||
if (publicPort !== exposePort) {
|
||||
throw { status: 500, message: `Port ${exposePort} is already in use.` }
|
||||
|
||||
if (configuredPort !== exposePort) {
|
||||
const availablePort = await getFreeExposedPort(id, exposePort, dockerId, remoteIpAddress);
|
||||
if (availablePort.toString() !== exposePort.toString()) {
|
||||
throw { status: 500, message: `Port ${exposePort} is already in use.` }
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isDNSCheckEnabled && !isDev && !forceSave) {
|
||||
return await checkDomainsIsValidInDNS({ hostname: request.hostname.split(':')[0], fqdn, dualCerts });
|
||||
let hostname = request.hostname.split(':')[0];
|
||||
if (remoteEngine) hostname = remoteIpAddress;
|
||||
return await checkDomainsIsValidInDNS({ hostname, fqdn, dualCerts });
|
||||
}
|
||||
return {}
|
||||
} catch ({ status, message }) {
|
||||
@@ -375,7 +422,7 @@ export async function getUsage(request) {
|
||||
|
||||
const application: any = await getApplicationFromDB(id, teamId);
|
||||
if (application.destinationDockerId) {
|
||||
[usage] = await Promise.all([getContainerUsage(application.destinationDocker.engine, id)]);
|
||||
[usage] = await Promise.all([getContainerUsage(application.destinationDocker.id, id)]);
|
||||
}
|
||||
return {
|
||||
usage
|
||||
@@ -701,21 +748,20 @@ export async function getPreviews(request: FastifyRequest<OnlyId>) {
|
||||
secret.value = decrypt(secret.value);
|
||||
return secret;
|
||||
});
|
||||
|
||||
const applicationSecrets = secrets.filter((secret) => !secret.isPRMRSecret);
|
||||
const PRMRSecrets = secrets.filter((secret) => secret.isPRMRSecret);
|
||||
const destinationDocker = await prisma.destinationDocker.findFirst({
|
||||
where: { application: { some: { id } }, teams: { some: { id: teamId } } }
|
||||
});
|
||||
const docker = dockerInstance({ destinationDocker });
|
||||
const listContainers = await docker.engine.listContainers({
|
||||
filters: { network: [destinationDocker.network], name: [id] }
|
||||
});
|
||||
const containers = listContainers.filter((container) => {
|
||||
return (
|
||||
container.Labels['coolify.configuration'] &&
|
||||
container.Labels['coolify.type'] === 'standalone-application'
|
||||
);
|
||||
});
|
||||
const application = await prisma.application.findUnique({ where: { id }, include: { destinationDocker: true } });
|
||||
const { stdout } = await executeDockerCmd({ dockerId: application.destinationDocker.id, command: `docker container ls --filter 'name=${id}-' --format "{{json .}}"` })
|
||||
if (stdout === '') {
|
||||
return {
|
||||
containers: [],
|
||||
applicationSecrets: [],
|
||||
PRMRSecrets: []
|
||||
}
|
||||
}
|
||||
const containers = formatLabelsOnDocker(stdout).filter(container => container.Labels['coolify.configuration'] && container.Labels['coolify.type'] === 'standalone-application')
|
||||
|
||||
const jsonContainers = containers
|
||||
.map((container) =>
|
||||
JSON.parse(Buffer.from(container.Labels['coolify.configuration'], 'base64').toString())
|
||||
@@ -733,50 +779,46 @@ export async function getPreviews(request: FastifyRequest<OnlyId>) {
|
||||
})
|
||||
}
|
||||
} catch ({ status, message }) {
|
||||
console.log({ status, message })
|
||||
return errorHandler({ status, message })
|
||||
}
|
||||
}
|
||||
|
||||
export async function getApplicationLogs(request: FastifyRequest<GetApplicationLogs>) {
|
||||
try {
|
||||
const { id } = request.params
|
||||
const { id } = request.params;
|
||||
let { since = 0 } = request.query
|
||||
if (since !== 0) {
|
||||
since = day(since).unix();
|
||||
}
|
||||
const { destinationDockerId, destinationDocker } = await prisma.application.findUnique({
|
||||
const { destinationDockerId, destinationDocker: { id: dockerId } } = await prisma.application.findUnique({
|
||||
where: { id },
|
||||
include: { destinationDocker: true }
|
||||
});
|
||||
if (destinationDockerId) {
|
||||
const docker = dockerInstance({ destinationDocker });
|
||||
try {
|
||||
const container = await docker.engine.getContainer(id);
|
||||
if (container) {
|
||||
const { default: ansi } = await import('strip-ansi')
|
||||
const logs = (
|
||||
await container.logs({
|
||||
stdout: true,
|
||||
stderr: true,
|
||||
timestamps: true,
|
||||
since,
|
||||
tail: 5000
|
||||
})
|
||||
)
|
||||
.toString()
|
||||
.split('\n')
|
||||
.map((l) => ansi(l.slice(8)))
|
||||
.filter((a) => a);
|
||||
// const found = await checkContainer({ dockerId, container: id })
|
||||
// if (found) {
|
||||
const { default: ansi } = await import('strip-ansi')
|
||||
const { stdout, stderr } = await executeDockerCmd({ dockerId, command: `docker logs --since ${since} --tail 5000 --timestamps ${id}` })
|
||||
const stripLogsStdout = stdout.toString().split('\n').map((l) => ansi(l)).filter((a) => a);
|
||||
const stripLogsStderr = stderr.toString().split('\n').map((l) => ansi(l)).filter((a) => a);
|
||||
const logs = stripLogsStderr.concat(stripLogsStdout)
|
||||
const sortedLogs = logs.sort((a, b) => (day(a.split(' ')[0]).isAfter(day(b.split(' ')[0])) ? 1 : -1))
|
||||
return { logs: sortedLogs }
|
||||
// }
|
||||
} catch (error) {
|
||||
const { statusCode } = error;
|
||||
if (statusCode === 404) {
|
||||
return {
|
||||
logs
|
||||
logs: []
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
logs: []
|
||||
};
|
||||
}
|
||||
}
|
||||
return {
|
||||
message: 'No logs found.'
|
||||
}
|
||||
} catch ({ status, message }) {
|
||||
return errorHandler({ status, message })
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { FastifyPluginAsync } from 'fastify';
|
||||
import { OnlyId } from '../../../../types';
|
||||
import { cancelDeployment, checkDNS, checkRepository, deleteApplication, deleteSecret, deleteStorage, deployApplication, getApplication, getApplicationLogs, getBuildIdLogs, getBuildLogs, getBuildPack, getGitHubToken, getGitLabSSHKey, getImages, getPreviews, getSecrets, getStorages, getUsage, listApplications, newApplication, saveApplication, saveApplicationSettings, saveApplicationSource, saveBuildPack, saveDeployKey, saveDestination, saveGitLabSSHKey, saveRepository, saveSecret, saveStorage, stopApplication } from './handlers';
|
||||
import { cancelDeployment, checkDNS, checkDomain, checkRepository, deleteApplication, deleteSecret, deleteStorage, deployApplication, getApplication, getApplicationLogs, getApplicationStatus, getBuildIdLogs, getBuildLogs, getBuildPack, getGitHubToken, getGitLabSSHKey, getImages, getPreviews, getSecrets, getStorages, getUsage, listApplications, newApplication, saveApplication, saveApplicationSettings, saveApplicationSource, saveBuildPack, saveDeployKey, saveDestination, saveGitLabSSHKey, saveRepository, saveSecret, saveStorage, stopApplication, stopPreviewApplication } from './handlers';
|
||||
|
||||
import type { CancelDeployment, CheckDNS, CheckRepository, DeleteApplication, DeleteSecret, DeleteStorage, DeployApplication, GetApplicationLogs, GetBuildIdLogs, GetBuildLogs, GetImages, SaveApplication, SaveApplicationSettings, SaveApplicationSource, SaveDeployKey, SaveDestination, SaveSecret, SaveStorage } from './types';
|
||||
import type { CancelDeployment, CheckDNS, CheckDomain, CheckRepository, DeleteApplication, DeleteSecret, DeleteStorage, DeployApplication, GetApplicationLogs, GetBuildIdLogs, GetBuildLogs, GetImages, SaveApplication, SaveApplicationSettings, SaveApplicationSource, SaveDeployKey, SaveDestination, SaveSecret, SaveStorage, StopPreviewApplication } from './types';
|
||||
|
||||
const root: FastifyPluginAsync = async (fastify): Promise<void> => {
|
||||
fastify.addHook('onRequest', async (request) => {
|
||||
@@ -17,9 +17,14 @@ const root: FastifyPluginAsync = async (fastify): Promise<void> => {
|
||||
fastify.post<SaveApplication>('/:id', async (request, reply) => await saveApplication(request, reply));
|
||||
fastify.delete<DeleteApplication>('/:id', async (request, reply) => await deleteApplication(request, reply));
|
||||
|
||||
fastify.get<OnlyId>('/:id/status', async (request) => await getApplicationStatus(request));
|
||||
|
||||
fastify.post<OnlyId>('/:id/stop', async (request, reply) => await stopApplication(request, reply));
|
||||
fastify.post<StopPreviewApplication>('/:id/stop/preview', async (request, reply) => await stopPreviewApplication(request, reply));
|
||||
|
||||
fastify.post<SaveApplicationSettings>('/:id/settings', async (request, reply) => await saveApplicationSettings(request, reply));
|
||||
|
||||
fastify.get<CheckDomain>('/:id/check', async (request) => await checkDomain(request));
|
||||
fastify.post<CheckDNS>('/:id/check', async (request) => await checkDNS(request));
|
||||
|
||||
fastify.get<OnlyId>('/:id/secrets', async (request) => await getSecrets(request));
|
||||
|
||||
@@ -30,6 +30,9 @@ export interface SaveApplicationSettings extends OnlyId {
|
||||
export interface DeleteApplication extends OnlyId {
|
||||
Querystring: { domain: string; };
|
||||
}
|
||||
export interface CheckDomain extends OnlyId {
|
||||
Querystring: { domain: string; };
|
||||
}
|
||||
export interface CheckDNS extends OnlyId {
|
||||
Querystring: { domain: string; };
|
||||
Body: {
|
||||
@@ -115,3 +118,9 @@ export interface DeployApplication extends OnlyId {
|
||||
branch: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface StopPreviewApplication extends OnlyId {
|
||||
Body: {
|
||||
pullmergeRequestId: string | null,
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,13 @@
|
||||
import { FastifyPluginAsync } from 'fastify';
|
||||
import { errorHandler, version } from '../../../../lib/common';
|
||||
import { errorHandler, listSettings, version } from '../../../../lib/common';
|
||||
|
||||
const root: FastifyPluginAsync = async (fastify): Promise<void> => {
|
||||
fastify.get('/', async () => {
|
||||
const settings = await listSettings()
|
||||
try {
|
||||
return {
|
||||
ipv4: settings.ipv4,
|
||||
ipv6: settings.ipv6,
|
||||
version,
|
||||
whiteLabeled: process.env.COOLIFY_WHITE_LABELED === 'true',
|
||||
whiteLabeledIcon: process.env.COOLIFY_WHITE_LABELED_ICON,
|
||||
|
||||
@@ -3,24 +3,20 @@ import type { FastifyRequest } from 'fastify';
|
||||
import { FastifyReply } from 'fastify';
|
||||
import yaml from 'js-yaml';
|
||||
import fs from 'fs/promises';
|
||||
import { asyncExecShell, ComposeFile, createDirectories, decrypt, encrypt, errorHandler, generateDatabaseConfiguration, generatePassword, getContainerUsage, getDatabaseImage, getDatabaseVersions, getFreePort, listSettings, makeLabelForStandaloneDatabase, prisma, startTcpProxy, startTraefikTCPProxy, stopDatabaseContainer, stopTcpHttpProxy, supportedDatabaseTypesAndVersions, uniqueName, updatePasswordInDb } from '../../../../lib/common';
|
||||
import { dockerInstance, getEngine } from '../../../../lib/docker';
|
||||
import { ComposeFile, createDirectories, decrypt, encrypt, errorHandler, executeDockerCmd, generateDatabaseConfiguration, generatePassword, getContainerUsage, getDatabaseImage, getDatabaseVersions, getFreePublicPort, listSettings, makeLabelForStandaloneDatabase, prisma, startTraefikTCPProxy, stopDatabaseContainer, stopTcpHttpProxy, supportedDatabaseTypesAndVersions, uniqueName, updatePasswordInDb } from '../../../../lib/common';
|
||||
import { checkContainer } from '../../../../lib/docker';
|
||||
import { day } from '../../../../lib/dayjs';
|
||||
|
||||
import { GetDatabaseLogs, OnlyId, SaveDatabase, SaveDatabaseDestination, SaveDatabaseSettings, SaveVersion } from '../../../../types';
|
||||
import { SaveDatabaseType } from './types';
|
||||
|
||||
export async function listDatabases(request: FastifyRequest) {
|
||||
try {
|
||||
const teamId = request.user.teamId;
|
||||
let databases = []
|
||||
if (teamId === '0') {
|
||||
databases = await prisma.database.findMany({ include: { teams: true } });
|
||||
} else {
|
||||
databases = await prisma.database.findMany({
|
||||
where: { teams: { some: { id: teamId } } },
|
||||
include: { teams: true }
|
||||
});
|
||||
}
|
||||
const databases = await prisma.database.findMany({
|
||||
where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } },
|
||||
include: { teams: true, destinationDocker: true }
|
||||
});
|
||||
return {
|
||||
databases
|
||||
}
|
||||
@@ -56,6 +52,36 @@ export async function newDatabase(request: FastifyRequest, reply: FastifyReply)
|
||||
return errorHandler({ status, message })
|
||||
}
|
||||
}
|
||||
export async function getDatabaseStatus(request: FastifyRequest<OnlyId>) {
|
||||
try {
|
||||
const { id } = request.params;
|
||||
const teamId = request.user.teamId;
|
||||
let isRunning = false;
|
||||
|
||||
const database = await prisma.database.findFirst({
|
||||
where: { id, teams: { some: { id: teamId === '0' ? undefined : teamId } } },
|
||||
include: { destinationDocker: true, settings: true }
|
||||
});
|
||||
const { destinationDockerId, destinationDocker } = database;
|
||||
if (destinationDockerId) {
|
||||
try {
|
||||
const { stdout } = await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker inspect --format '{{json .State}}' ${id}` })
|
||||
|
||||
if (JSON.parse(stdout).Running) {
|
||||
isRunning = true;
|
||||
}
|
||||
} catch (error) {
|
||||
//
|
||||
}
|
||||
}
|
||||
return {
|
||||
isRunning
|
||||
}
|
||||
} catch ({ status, message }) {
|
||||
return errorHandler({ status, message })
|
||||
}
|
||||
}
|
||||
|
||||
export async function getDatabase(request: FastifyRequest<OnlyId>) {
|
||||
try {
|
||||
const { id } = request.params;
|
||||
@@ -69,29 +95,11 @@ export async function getDatabase(request: FastifyRequest<OnlyId>) {
|
||||
}
|
||||
if (database.dbUserPassword) database.dbUserPassword = decrypt(database.dbUserPassword);
|
||||
if (database.rootUserPassword) database.rootUserPassword = decrypt(database.rootUserPassword);
|
||||
const { destinationDockerId, destinationDocker } = database;
|
||||
let isRunning = false;
|
||||
if (destinationDockerId) {
|
||||
const host = getEngine(destinationDocker.engine);
|
||||
|
||||
try {
|
||||
const { stdout } = await asyncExecShell(
|
||||
`DOCKER_HOST=${host} docker inspect --format '{{json .State}}' ${id}`
|
||||
);
|
||||
|
||||
if (JSON.parse(stdout).Running) {
|
||||
isRunning = true;
|
||||
}
|
||||
} catch (error) {
|
||||
//
|
||||
}
|
||||
}
|
||||
const configuration = generateDatabaseConfiguration(database);
|
||||
const settings = await listSettings();
|
||||
return {
|
||||
privatePort: configuration?.privatePort,
|
||||
database,
|
||||
isRunning,
|
||||
versions: await getDatabaseVersions(database.type),
|
||||
settings
|
||||
};
|
||||
@@ -164,16 +172,15 @@ export async function saveDatabaseDestination(request: FastifyRequest<SaveDataba
|
||||
|
||||
const {
|
||||
destinationDockerId,
|
||||
destinationDocker: { engine },
|
||||
destinationDocker: { engine, id: dockerId },
|
||||
version,
|
||||
type
|
||||
} = await prisma.database.findUnique({ where: { id }, include: { destinationDocker: true } });
|
||||
|
||||
if (destinationDockerId) {
|
||||
const host = getEngine(engine);
|
||||
if (type && version) {
|
||||
const baseImage = getDatabaseImage(type);
|
||||
asyncExecShell(`DOCKER_HOST=${host} docker pull ${baseImage}:${version}`);
|
||||
executeDockerCmd({ dockerId, command: `docker pull ${baseImage}:${version}` })
|
||||
}
|
||||
}
|
||||
return reply.code(201).send({})
|
||||
@@ -194,7 +201,7 @@ export async function getDatabaseUsage(request: FastifyRequest<OnlyId>) {
|
||||
if (database.dbUserPassword) database.dbUserPassword = decrypt(database.dbUserPassword);
|
||||
if (database.rootUserPassword) database.rootUserPassword = decrypt(database.rootUserPassword);
|
||||
if (database.destinationDockerId) {
|
||||
[usage] = await Promise.all([getContainerUsage(database.destinationDocker.engine, id)]);
|
||||
[usage] = await Promise.all([getContainerUsage(database.destinationDocker.id, id)]);
|
||||
}
|
||||
return {
|
||||
usage
|
||||
@@ -225,7 +232,6 @@ export async function startDatabase(request: FastifyRequest<OnlyId>) {
|
||||
generateDatabaseConfiguration(database);
|
||||
|
||||
const network = destinationDockerId && destinationDocker.network;
|
||||
const host = getEngine(destinationDocker.engine);
|
||||
const volumeName = volume.split(':')[0];
|
||||
const labels = await makeLabelForStandaloneDatabase({ id, image, volume });
|
||||
|
||||
@@ -267,13 +273,13 @@ export async function startDatabase(request: FastifyRequest<OnlyId>) {
|
||||
const composeFileDestination = `${workdir}/docker-compose.yaml`;
|
||||
await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
|
||||
try {
|
||||
await asyncExecShell(`DOCKER_HOST=${host} docker volume create ${volumeName}`);
|
||||
await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker volume create ${volumeName}` })
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
try {
|
||||
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`);
|
||||
if (isPublic) await startTcpProxy(destinationDocker, id, publicPort, privatePort);
|
||||
await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} up -d` })
|
||||
if (isPublic) await startTraefikTCPProxy(destinationDocker, id, publicPort, privatePort);
|
||||
return {};
|
||||
} catch (error) {
|
||||
throw {
|
||||
@@ -311,39 +317,27 @@ export async function stopDatabase(request: FastifyRequest<OnlyId>) {
|
||||
}
|
||||
export async function getDatabaseLogs(request: FastifyRequest<GetDatabaseLogs>) {
|
||||
try {
|
||||
const teamId = request.user.teamId;
|
||||
const { id } = request.params;
|
||||
let { since = 0 } = request.query
|
||||
if (since !== 0) {
|
||||
since = day(since).unix();
|
||||
}
|
||||
const { destinationDockerId, destinationDocker } = await prisma.database.findUnique({
|
||||
const { destinationDockerId, destinationDocker: { id: dockerId } } = await prisma.database.findUnique({
|
||||
where: { id },
|
||||
include: { destinationDocker: true }
|
||||
});
|
||||
if (destinationDockerId) {
|
||||
const docker = dockerInstance({ destinationDocker });
|
||||
try {
|
||||
const container = await docker.engine.getContainer(id);
|
||||
if (container) {
|
||||
const { default: ansi } = await import('strip-ansi')
|
||||
const logs = (
|
||||
await container.logs({
|
||||
stdout: true,
|
||||
stderr: true,
|
||||
timestamps: true,
|
||||
since,
|
||||
tail: 5000
|
||||
})
|
||||
)
|
||||
.toString()
|
||||
.split('\n')
|
||||
.map((l) => ansi(l.slice(8)))
|
||||
.filter((a) => a);
|
||||
return {
|
||||
logs
|
||||
};
|
||||
}
|
||||
// const found = await checkContainer({ dockerId, container: id })
|
||||
// if (found) {
|
||||
const { default: ansi } = await import('strip-ansi')
|
||||
const { stdout, stderr } = await executeDockerCmd({ dockerId, command: `docker logs --since ${since} --tail 5000 --timestamps ${id}` })
|
||||
const stripLogsStdout = stdout.toString().split('\n').map((l) => ansi(l)).filter((a) => a);
|
||||
const stripLogsStderr = stderr.toString().split('\n').map((l) => ansi(l)).filter((a) => a);
|
||||
const logs = stripLogsStderr.concat(stripLogsStdout)
|
||||
const sortedLogs = logs.sort((a, b) => (day(a.split(' ')[0]).isAfter(day(b.split(' ')[0])) ? 1 : -1))
|
||||
return { logs: sortedLogs }
|
||||
// }
|
||||
} catch (error) {
|
||||
const { statusCode } = error;
|
||||
if (statusCode === 404) {
|
||||
@@ -432,8 +426,10 @@ export async function saveDatabaseSettings(request: FastifyRequest<SaveDatabaseS
|
||||
const teamId = request.user.teamId;
|
||||
const { id } = request.params;
|
||||
const { isPublic, appendOnly = true } = request.body;
|
||||
const publicPort = await getFreePort();
|
||||
const settings = await listSettings();
|
||||
|
||||
const { destinationDocker: { id: dockerId } } = await prisma.database.findUnique({ where: { id }, include: { destinationDocker: true } })
|
||||
const publicPort = await getFreePublicPort(id, dockerId);
|
||||
|
||||
await prisma.database.update({
|
||||
where: { id },
|
||||
data: {
|
||||
@@ -453,11 +449,7 @@ export async function saveDatabaseSettings(request: FastifyRequest<SaveDatabaseS
|
||||
if (destinationDockerId) {
|
||||
if (isPublic) {
|
||||
await prisma.database.update({ where: { id }, data: { publicPort } });
|
||||
if (settings.isTraefikUsed) {
|
||||
await startTraefikTCPProxy(destinationDocker, id, publicPort, privatePort);
|
||||
} else {
|
||||
await startTcpProxy(destinationDocker, id, publicPort, privatePort);
|
||||
}
|
||||
await startTraefikTCPProxy(destinationDocker, id, publicPort, privatePort);
|
||||
} else {
|
||||
await prisma.database.update({ where: { id }, data: { publicPort: null } });
|
||||
await stopTcpHttpProxy(id, destinationDocker, oldPublicPort);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { FastifyPluginAsync } from 'fastify';
|
||||
import { deleteDatabase, getDatabase, getDatabaseLogs, getDatabaseTypes, getDatabaseUsage, getVersions, listDatabases, newDatabase, saveDatabase, saveDatabaseDestination, saveDatabaseSettings, saveDatabaseType, saveVersion, startDatabase, stopDatabase } from './handlers';
|
||||
import { deleteDatabase, getDatabase, getDatabaseLogs, getDatabaseStatus, getDatabaseTypes, getDatabaseUsage, getVersions, listDatabases, newDatabase, saveDatabase, saveDatabaseDestination, saveDatabaseSettings, saveDatabaseType, saveVersion, startDatabase, stopDatabase } from './handlers';
|
||||
|
||||
import type { GetDatabaseLogs, OnlyId, SaveDatabase, SaveDatabaseDestination, SaveDatabaseSettings, SaveVersion } from '../../../../types';
|
||||
import type { SaveDatabaseType } from './types';
|
||||
@@ -15,6 +15,8 @@ const root: FastifyPluginAsync = async (fastify): Promise<void> => {
|
||||
fastify.post<SaveDatabase>('/:id', async (request, reply) => await saveDatabase(request, reply));
|
||||
fastify.delete<OnlyId>('/:id', async (request) => await deleteDatabase(request));
|
||||
|
||||
fastify.get<OnlyId>('/:id/status', async (request) => await getDatabaseStatus(request));
|
||||
|
||||
fastify.post<SaveDatabaseSettings>('/:id/settings', async (request) => await saveDatabaseSettings(request));
|
||||
|
||||
fastify.get('/:id/configuration/type', async (request) => await getDatabaseTypes(request));
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
import type { FastifyRequest } from 'fastify';
|
||||
import { FastifyReply } from 'fastify';
|
||||
import { asyncExecShell, errorHandler, listSettings, prisma, startCoolifyProxy, startTraefikProxy, stopTraefikProxy } from '../../../../lib/common';
|
||||
import { checkContainer, dockerInstance, getEngine } from '../../../../lib/docker';
|
||||
import sshConfig from 'ssh-config'
|
||||
import fs from 'fs/promises'
|
||||
import os from 'os';
|
||||
|
||||
import { asyncExecShell, decrypt, errorHandler, executeDockerCmd, listSettings, prisma, startTraefikProxy, stopTraefikProxy } from '../../../../lib/common';
|
||||
import { checkContainer } from '../../../../lib/docker';
|
||||
|
||||
import type { OnlyId } from '../../../../types';
|
||||
import type { CheckDestination, NewDestination, Proxy, SaveDestinationSettings } from './types';
|
||||
import type { CheckDestination, ListDestinations, NewDestination, Proxy, SaveDestinationSettings } from './types';
|
||||
|
||||
export async function listDestinations(request: FastifyRequest) {
|
||||
export async function listDestinations(request: FastifyRequest<ListDestinations>) {
|
||||
try {
|
||||
const teamId = request.user.teamId;
|
||||
const { onlyVerified = false } = request.query
|
||||
let destinations = []
|
||||
if (teamId === '0') {
|
||||
destinations = await prisma.destinationDocker.findMany({ include: { teams: true } });
|
||||
@@ -18,6 +23,9 @@ export async function listDestinations(request: FastifyRequest) {
|
||||
include: { teams: true }
|
||||
});
|
||||
}
|
||||
if (onlyVerified) {
|
||||
destinations = destinations.filter(destination => destination.engine || (destination.remoteEngine && destination.remoteVerified))
|
||||
}
|
||||
return {
|
||||
destinations
|
||||
}
|
||||
@@ -44,7 +52,8 @@ export async function getDestination(request: FastifyRequest<OnlyId>) {
|
||||
const { id } = request.params
|
||||
const teamId = request.user?.teamId;
|
||||
const destination = await prisma.destinationDocker.findFirst({
|
||||
where: { id, teams: { some: { id: teamId === '0' ? undefined : teamId } } }
|
||||
where: { id, teams: { some: { id: teamId === '0' ? undefined : teamId } } },
|
||||
include: { sshKey: true }
|
||||
});
|
||||
if (!destination && id !== 'new') {
|
||||
throw { status: 404, message: `Destination not found.` };
|
||||
@@ -52,23 +61,8 @@ export async function getDestination(request: FastifyRequest<OnlyId>) {
|
||||
const settings = await listSettings();
|
||||
let payload = {
|
||||
destination,
|
||||
settings,
|
||||
state: false
|
||||
settings
|
||||
};
|
||||
|
||||
if (destination?.remoteEngine) {
|
||||
// const { stdout } = await asyncExecShell(
|
||||
// `ssh -p ${destination.port} ${destination.user}@${destination.ipAddress} "docker ps -a"`
|
||||
// );
|
||||
// console.log(stdout)
|
||||
// const engine = await generateRemoteEngine(destination);
|
||||
// // await saveSshKey(destination);
|
||||
// payload.state = await checkContainer(engine, 'coolify-haproxy');
|
||||
} else {
|
||||
const containerName = 'coolify-proxy';
|
||||
payload.state =
|
||||
destination?.engine && (await checkContainer(destination.engine, containerName));
|
||||
}
|
||||
return {
|
||||
...payload
|
||||
};
|
||||
@@ -79,68 +73,68 @@ export async function getDestination(request: FastifyRequest<OnlyId>) {
|
||||
}
|
||||
export async function newDestination(request: FastifyRequest<NewDestination>, reply: FastifyReply) {
|
||||
try {
|
||||
const { id } = request.params
|
||||
let { name, network, engine, isCoolifyProxyUsed } = request.body
|
||||
const teamId = request.user.teamId;
|
||||
if (id === 'new') {
|
||||
const host = getEngine(engine);
|
||||
const docker = dockerInstance({ destinationDocker: { engine, network } });
|
||||
const found = await docker.engine.listNetworks({ filters: { name: [`^${network}$`] } });
|
||||
if (found.length === 0) {
|
||||
await asyncExecShell(`DOCKER_HOST=${host} docker network create --attachable ${network}`);
|
||||
}
|
||||
await prisma.destinationDocker.create({
|
||||
data: { name, teams: { connect: { id: teamId } }, engine, network, isCoolifyProxyUsed }
|
||||
});
|
||||
const destinations = await prisma.destinationDocker.findMany({ where: { engine } });
|
||||
const destination = destinations.find((destination) => destination.network === network);
|
||||
const { id } = request.params
|
||||
|
||||
if (destinations.length > 0) {
|
||||
const proxyConfigured = destinations.find(
|
||||
(destination) => destination.network !== network && destination.isCoolifyProxyUsed === true
|
||||
);
|
||||
if (proxyConfigured) {
|
||||
isCoolifyProxyUsed = !!proxyConfigured.isCoolifyProxyUsed;
|
||||
let { name, network, engine, isCoolifyProxyUsed, remoteIpAddress, remoteUser, remotePort } = request.body
|
||||
if (id === 'new') {
|
||||
console.log(engine)
|
||||
if (engine) {
|
||||
const { stdout } = await asyncExecShell(`DOCKER_HOST=unix:///var/run/docker.sock docker network ls --filter 'name=^${network}$' --format '{{json .}}'`);
|
||||
if (stdout === '') {
|
||||
await asyncExecShell(`DOCKER_HOST=unix:///var/run/docker.sock docker network create --attachable ${network}`);
|
||||
}
|
||||
await prisma.destinationDocker.updateMany({ where: { engine }, data: { isCoolifyProxyUsed } });
|
||||
}
|
||||
if (isCoolifyProxyUsed) {
|
||||
const settings = await prisma.setting.findFirst();
|
||||
if (settings?.isTraefikUsed) {
|
||||
await startTraefikProxy(engine);
|
||||
} else {
|
||||
await startCoolifyProxy(engine);
|
||||
await prisma.destinationDocker.create({
|
||||
data: { name, teams: { connect: { id: teamId } }, engine, network, isCoolifyProxyUsed }
|
||||
});
|
||||
const destinations = await prisma.destinationDocker.findMany({ where: { engine } });
|
||||
const destination = destinations.find((destination) => destination.network === network);
|
||||
if (destinations.length > 0) {
|
||||
const proxyConfigured = destinations.find(
|
||||
(destination) => destination.network !== network && destination.isCoolifyProxyUsed === true
|
||||
);
|
||||
if (proxyConfigured) {
|
||||
isCoolifyProxyUsed = !!proxyConfigured.isCoolifyProxyUsed;
|
||||
}
|
||||
await prisma.destinationDocker.updateMany({ where: { engine }, data: { isCoolifyProxyUsed } });
|
||||
}
|
||||
if (isCoolifyProxyUsed) {
|
||||
await startTraefikProxy(destination.id);
|
||||
}
|
||||
return reply.code(201).send({ id: destination.id });
|
||||
} else {
|
||||
const destination = await prisma.destinationDocker.create({
|
||||
data: { name, teams: { connect: { id: teamId } }, engine, network, isCoolifyProxyUsed, remoteEngine: true, remoteIpAddress, remoteUser, remotePort }
|
||||
});
|
||||
return reply.code(201).send({ id: destination.id })
|
||||
}
|
||||
return reply.code(201).send({ id: destination.id });
|
||||
} else {
|
||||
await prisma.destinationDocker.update({ where: { id }, data: { name, engine, network } });
|
||||
return reply.code(201).send();
|
||||
}
|
||||
|
||||
} catch ({ status, message }) {
|
||||
console.log({ status, message })
|
||||
return errorHandler({ status, message })
|
||||
}
|
||||
}
|
||||
export async function deleteDestination(request: FastifyRequest<OnlyId>) {
|
||||
try {
|
||||
const { id } = request.params
|
||||
const destination = await prisma.destinationDocker.delete({ where: { id } });
|
||||
if (destination.isCoolifyProxyUsed) {
|
||||
const host = getEngine(destination.engine);
|
||||
const { network } = destination;
|
||||
const settings = await prisma.setting.findFirst();
|
||||
const containerName = settings.isTraefikUsed ? 'coolify-proxy' : 'coolify-haproxy';
|
||||
const { stdout: found } = await asyncExecShell(
|
||||
`DOCKER_HOST=${host} docker ps -a --filter network=${network} --filter name=${containerName} --format '{{.}}'`
|
||||
);
|
||||
if (found) {
|
||||
await asyncExecShell(
|
||||
`DOCKER_HOST="${host}" docker network disconnect ${network} ${containerName}`
|
||||
);
|
||||
await asyncExecShell(`DOCKER_HOST="${host}" docker network rm ${network}`);
|
||||
const { network, remoteVerified, engine, isCoolifyProxyUsed } = await prisma.destinationDocker.findUnique({ where: { id } });
|
||||
if (isCoolifyProxyUsed) {
|
||||
if (engine || remoteVerified) {
|
||||
const { stdout: found } = await executeDockerCmd({
|
||||
dockerId: id,
|
||||
command: `docker ps -a --filter network=${network} --filter name=coolify-proxy --format '{{.}}'`
|
||||
})
|
||||
if (found) {
|
||||
await executeDockerCmd({ dockerId: id, command: `docker network disconnect ${network} coolify-proxy` })
|
||||
await executeDockerCmd({ dockerId: id, command: `docker network rm ${network}` })
|
||||
}
|
||||
}
|
||||
}
|
||||
await prisma.destinationDocker.delete({ where: { id } });
|
||||
return {}
|
||||
} catch ({ status, message }) {
|
||||
return errorHandler({ status, message })
|
||||
@@ -163,34 +157,105 @@ export async function saveDestinationSettings(request: FastifyRequest<SaveDestin
|
||||
}
|
||||
}
|
||||
export async function startProxy(request: FastifyRequest<Proxy>) {
|
||||
const { engine } = request.body;
|
||||
const { id } = request.params
|
||||
try {
|
||||
await startTraefikProxy(engine);
|
||||
await startTraefikProxy(id);
|
||||
return {}
|
||||
} catch ({ status, message }) {
|
||||
await stopTraefikProxy(engine);
|
||||
console.log({ status, message })
|
||||
await stopTraefikProxy(id);
|
||||
return errorHandler({ status, message })
|
||||
}
|
||||
}
|
||||
export async function stopProxy(request: FastifyRequest<Proxy>) {
|
||||
const { engine } = request.body;
|
||||
const { id } = request.params
|
||||
try {
|
||||
await stopTraefikProxy(engine);
|
||||
await stopTraefikProxy(id);
|
||||
return {}
|
||||
} catch ({ status, message }) {
|
||||
return errorHandler({ status, message })
|
||||
}
|
||||
}
|
||||
export async function restartProxy(request: FastifyRequest<Proxy>) {
|
||||
const { engine } = request.body;
|
||||
const { id } = request.params
|
||||
try {
|
||||
await stopTraefikProxy(engine);
|
||||
await startTraefikProxy(engine);
|
||||
await prisma.destinationDocker.updateMany({
|
||||
where: { engine },
|
||||
await stopTraefikProxy(id);
|
||||
await startTraefikProxy(id);
|
||||
await prisma.destinationDocker.update({
|
||||
where: { id },
|
||||
data: { isCoolifyProxyUsed: true }
|
||||
});
|
||||
return {}
|
||||
} catch ({ status, message }) {
|
||||
await prisma.destinationDocker.update({
|
||||
where: { id },
|
||||
data: { isCoolifyProxyUsed: false }
|
||||
});
|
||||
return errorHandler({ status, message })
|
||||
}
|
||||
}
|
||||
|
||||
export async function assignSSHKey(request: FastifyRequest) {
|
||||
try {
|
||||
const { id: sshKeyId } = request.body;
|
||||
const { id } = request.params;
|
||||
await prisma.destinationDocker.update({ where: { id }, data: { sshKey: { connect: { id: sshKeyId } } } })
|
||||
return {}
|
||||
} catch ({ status, message }) {
|
||||
return errorHandler({ status, message })
|
||||
}
|
||||
}
|
||||
export async function verifyRemoteDockerEngine(request: FastifyRequest, reply: FastifyReply) {
|
||||
try {
|
||||
const { id } = request.params;
|
||||
const homedir = os.homedir();
|
||||
|
||||
const { sshKey: { privateKey }, remoteIpAddress, remotePort, remoteUser, network } = await prisma.destinationDocker.findFirst({ where: { id }, include: { sshKey: true } })
|
||||
|
||||
await fs.writeFile(`/tmp/id_rsa_verification_${id}`, decrypt(privateKey) + '\n', { encoding: 'utf8', mode: 400 })
|
||||
|
||||
const host = `ssh://${remoteUser}@${remoteIpAddress}`
|
||||
|
||||
const config = sshConfig.parse('')
|
||||
const found = config.find({ Host: remoteIpAddress })
|
||||
if (!found) {
|
||||
config.append({
|
||||
Host: remoteIpAddress,
|
||||
Port: remotePort.toString(),
|
||||
User: remoteUser,
|
||||
IdentityFile: `/tmp/id_rsa_verification_${id}`,
|
||||
StrictHostKeyChecking: 'no'
|
||||
})
|
||||
}
|
||||
try {
|
||||
await fs.stat(`${homedir}/.ssh/`)
|
||||
} catch (error) {
|
||||
await fs.mkdir(`${homedir}/.ssh/`)
|
||||
}
|
||||
await fs.writeFile(`${homedir}/.ssh/config`, sshConfig.stringify(config))
|
||||
|
||||
const { stdout } = await asyncExecShell(`DOCKER_HOST=${host} docker network ls --filter 'name=${network}' --no-trunc --format "{{json .}}"`);
|
||||
|
||||
if (!stdout) {
|
||||
await asyncExecShell(`DOCKER_HOST=${host} docker network create --attachable ${network}`);
|
||||
}
|
||||
|
||||
await prisma.destinationDocker.update({ where: { id }, data: { remoteVerified: true } })
|
||||
return reply.code(201).send()
|
||||
|
||||
} catch ({ status, message }) {
|
||||
return errorHandler({ status, message })
|
||||
}
|
||||
}
|
||||
|
||||
export async function getDestinationStatus(request: FastifyRequest<OnlyId>) {
|
||||
try {
|
||||
const { id } = request.params
|
||||
const destination = await prisma.destinationDocker.findUnique({ where: { id } })
|
||||
const isRunning = await checkContainer({ dockerId: destination.id, container: 'coolify-proxy' })
|
||||
return {
|
||||
isRunning
|
||||
}
|
||||
} catch ({ status, message }) {
|
||||
return errorHandler({ status, message })
|
||||
}
|
||||
|
||||
@@ -1,24 +1,29 @@
|
||||
import { FastifyPluginAsync } from 'fastify';
|
||||
import { checkDestination, deleteDestination, getDestination, listDestinations, newDestination, restartProxy, saveDestinationSettings, startProxy, stopProxy } from './handlers';
|
||||
import { assignSSHKey, checkDestination, deleteDestination, getDestination, getDestinationStatus, listDestinations, newDestination, restartProxy, saveDestinationSettings, startProxy, stopProxy, verifyRemoteDockerEngine } from './handlers';
|
||||
|
||||
import type { OnlyId } from '../../../../types';
|
||||
import type { CheckDestination, NewDestination, Proxy, SaveDestinationSettings } from './types';
|
||||
import type { CheckDestination, ListDestinations, NewDestination, Proxy, SaveDestinationSettings } from './types';
|
||||
|
||||
const root: FastifyPluginAsync = async (fastify): Promise<void> => {
|
||||
fastify.addHook('onRequest', async (request) => {
|
||||
return await request.jwtVerify()
|
||||
})
|
||||
fastify.get('/', async (request) => await listDestinations(request));
|
||||
fastify.get<ListDestinations>('/', async (request) => await listDestinations(request));
|
||||
fastify.post<CheckDestination>('/check', async (request) => await checkDestination(request));
|
||||
|
||||
fastify.get<OnlyId>('/:id', async (request) => await getDestination(request));
|
||||
fastify.post<NewDestination>('/:id', async (request, reply) => await newDestination(request, reply));
|
||||
fastify.delete<OnlyId>('/:id', async (request) => await deleteDestination(request));
|
||||
fastify.get<OnlyId>('/:id/status', async (request) => await getDestinationStatus(request));
|
||||
|
||||
fastify.post<SaveDestinationSettings>('/:id/settings', async (request, reply) => await saveDestinationSettings(request));
|
||||
fastify.post<Proxy>('/:id/start', async (request, reply) => await startProxy(request));
|
||||
fastify.post<Proxy>('/:id/stop', async (request, reply) => await stopProxy(request));
|
||||
fastify.post<Proxy>('/:id/restart', async (request, reply) => await restartProxy(request));
|
||||
fastify.post<SaveDestinationSettings>('/:id/settings', async (request) => await saveDestinationSettings(request));
|
||||
fastify.post<Proxy>('/:id/start', async (request,) => await startProxy(request));
|
||||
fastify.post<Proxy>('/:id/stop', async (request) => await stopProxy(request));
|
||||
fastify.post<Proxy>('/:id/restart', async (request) => await restartProxy(request));
|
||||
|
||||
fastify.post('/:id/configuration/sshKey', async (request) => await assignSSHKey(request));
|
||||
|
||||
fastify.post('/:id/verify', async (request, reply) => await verifyRemoteDockerEngine(request, reply));
|
||||
};
|
||||
|
||||
export default root;
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { OnlyId } from "../../../../types"
|
||||
|
||||
export interface ListDestinations {
|
||||
Querystring: {
|
||||
onlyVerified: string
|
||||
}
|
||||
}
|
||||
export interface CheckDestination {
|
||||
Body: {
|
||||
network: string
|
||||
@@ -20,7 +25,5 @@ export interface SaveDestinationSettings extends OnlyId {
|
||||
}
|
||||
}
|
||||
export interface Proxy extends OnlyId {
|
||||
Body: {
|
||||
engine: string
|
||||
}
|
||||
|
||||
}
|
||||
@@ -17,7 +17,8 @@ export async function hashPassword(password: string): Promise<string> {
|
||||
|
||||
export async function cleanupManually() {
|
||||
try {
|
||||
await cleanupDockerStorage('unix:///var/run/docker.sock', true, true)
|
||||
const destination = await prisma.destinationDocker.findFirst({ where: { engine: '/var/run/docker.sock' } })
|
||||
await cleanupDockerStorage(destination.id, true, true)
|
||||
return {}
|
||||
} catch ({ status, message }) {
|
||||
return errorHandler({ status, message })
|
||||
@@ -154,7 +155,6 @@ export async function login(request: FastifyRequest<Login>, reply: FastifyReply)
|
||||
}
|
||||
if (userFound) {
|
||||
if (userFound.type === 'email') {
|
||||
// TODO: Review this one
|
||||
if (userFound.password === 'RESETME') {
|
||||
const hashedPassword = await hashPassword(password);
|
||||
if (userFound.updatedAt < new Date(Date.now() - 1000 * 60 * 10)) {
|
||||
|
||||
@@ -273,11 +273,15 @@ export async function inviteToTeam(request: FastifyRequest<InviteToTeam>, reply:
|
||||
const { email, permission, teamId, teamName } = request.body;
|
||||
const userFound = await prisma.user.findUnique({ where: { email } });
|
||||
if (!userFound) {
|
||||
throw `No user found with '${email}' email address.`
|
||||
throw {
|
||||
message: `No user found with '${email}' email address.`
|
||||
};
|
||||
}
|
||||
const uid = userFound.id;
|
||||
if (uid === userId) {
|
||||
throw `Invitation to yourself? Whaaaaat?`
|
||||
if (uid === userId) {
|
||||
throw {
|
||||
message: `Invitation to yourself? Whaaaaat?`
|
||||
};
|
||||
}
|
||||
const alreadyInTeam = await prisma.team.findFirst({
|
||||
where: { id: teamId, users: { some: { id: uid } } }
|
||||
|
||||
@@ -2,13 +2,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, getServiceImages, configureServiceType, getServiceFromDB, getContainerUsage, removeService, isDomainConfigured, saveUpdateableFields, fixType, decrypt, encrypt, getServiceMainPort, createDirectories, ComposeFile, makeLabelForServices, getFreePort, getDomain, errorHandler, generatePassword, isDev, stopTcpHttpProxy, supportedServiceTypesAndVersions } from '../../../../lib/common';
|
||||
import { prisma, uniqueName, asyncExecShell, getServiceImage, configureServiceType, getServiceFromDB, getContainerUsage, removeService, isDomainConfigured, saveUpdateableFields, fixType, decrypt, encrypt, getServiceMainPort, createDirectories, ComposeFile, makeLabelForServices, getFreePublicPort, getDomain, errorHandler, generatePassword, isDev, stopTcpHttpProxy, supportedServiceTypesAndVersions, executeDockerCmd, listSettings, getFreeExposedPort, checkDomainsIsValidInDNS } from '../../../../lib/common';
|
||||
import { day } from '../../../../lib/dayjs';
|
||||
import { checkContainer, dockerInstance, getEngine, removeContainer } from '../../../../lib/docker';
|
||||
import { checkContainer, isContainerExited, removeContainer } from '../../../../lib/docker';
|
||||
import cuid from 'cuid';
|
||||
|
||||
import type { OnlyId } from '../../../../types';
|
||||
import type { ActivateWordpressFtp, CheckService, 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';
|
||||
|
||||
// async function startServiceNew(request: FastifyRequest<OnlyId>) {
|
||||
// try {
|
||||
@@ -145,15 +145,10 @@ import type { ActivateWordpressFtp, CheckService, DeleteServiceSecret, DeleteSer
|
||||
export async function listServices(request: FastifyRequest) {
|
||||
try {
|
||||
const teamId = request.user.teamId;
|
||||
let services = []
|
||||
if (teamId === '0') {
|
||||
services = await prisma.service.findMany({ include: { teams: true } });
|
||||
} else {
|
||||
services = await prisma.service.findMany({
|
||||
where: { teams: { some: { id: teamId } } },
|
||||
include: { teams: true }
|
||||
});
|
||||
}
|
||||
const services = await prisma.service.findMany({
|
||||
where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } },
|
||||
include: { teams: true, destinationDocker: true }
|
||||
});
|
||||
return {
|
||||
services
|
||||
}
|
||||
@@ -172,43 +167,41 @@ export async function newService(request: FastifyRequest, reply: FastifyReply) {
|
||||
return errorHandler({ status, message })
|
||||
}
|
||||
}
|
||||
export async function getServiceStatus(request: FastifyRequest<OnlyId>) {
|
||||
try {
|
||||
const teamId = request.user.teamId;
|
||||
const { id } = request.params;
|
||||
|
||||
let isRunning = false;
|
||||
let isExited = false
|
||||
|
||||
const service = await getServiceFromDB({ id, teamId });
|
||||
const { destinationDockerId, settings } = service;
|
||||
|
||||
if (destinationDockerId) {
|
||||
isRunning = await checkContainer({ dockerId: service.destinationDocker.id, container: id });
|
||||
isExited = await isContainerExited(service.destinationDocker.id, id);
|
||||
}
|
||||
return {
|
||||
isRunning,
|
||||
isExited,
|
||||
settings
|
||||
}
|
||||
} catch ({ status, message }) {
|
||||
return errorHandler({ status, message })
|
||||
}
|
||||
}
|
||||
|
||||
export async function getService(request: FastifyRequest<OnlyId>) {
|
||||
try {
|
||||
const teamId = request.user.teamId;
|
||||
const { id } = request.params;
|
||||
const service = await getServiceFromDB({ id, teamId });
|
||||
|
||||
const settings = await listSettings()
|
||||
if (!service) {
|
||||
throw { status: 404, message: 'Service not found.' }
|
||||
}
|
||||
|
||||
const { destinationDockerId, destinationDocker, type, version, settings } = service;
|
||||
let isRunning = false;
|
||||
if (destinationDockerId) {
|
||||
const host = getEngine(destinationDocker.engine);
|
||||
const docker = dockerInstance({ destinationDocker });
|
||||
const baseImage = getServiceImage(type);
|
||||
const images = getServiceImages(type);
|
||||
docker.engine.pull(`${baseImage}:${version}`);
|
||||
if (images?.length > 0) {
|
||||
for (const image of images) {
|
||||
docker.engine.pull(`${image}:latest`);
|
||||
}
|
||||
}
|
||||
try {
|
||||
const { stdout } = await asyncExecShell(
|
||||
`DOCKER_HOST=${host} docker inspect --format '{{json .State}}' ${id}`
|
||||
);
|
||||
|
||||
if (JSON.parse(stdout).Running) {
|
||||
isRunning = true;
|
||||
}
|
||||
} catch (error) {
|
||||
//
|
||||
}
|
||||
}
|
||||
return {
|
||||
isRunning,
|
||||
service,
|
||||
settings
|
||||
}
|
||||
@@ -282,7 +275,7 @@ export async function getServiceUsage(request: FastifyRequest<OnlyId>) {
|
||||
|
||||
const service = await getServiceFromDB({ id, teamId });
|
||||
if (service.destinationDockerId) {
|
||||
[usage] = await Promise.all([getContainerUsage(service.destinationDocker.engine, id)]);
|
||||
[usage] = await Promise.all([getContainerUsage(service.destinationDocker.id, id)]);
|
||||
}
|
||||
return {
|
||||
usage
|
||||
@@ -299,33 +292,22 @@ export async function getServiceLogs(request: FastifyRequest<GetServiceLogs>) {
|
||||
if (since !== 0) {
|
||||
since = day(since).unix();
|
||||
}
|
||||
const { destinationDockerId, destinationDocker } = await prisma.service.findUnique({
|
||||
const { destinationDockerId, destinationDocker: { id: dockerId } } = await prisma.service.findUnique({
|
||||
where: { id },
|
||||
include: { destinationDocker: true }
|
||||
});
|
||||
if (destinationDockerId) {
|
||||
const docker = dockerInstance({ destinationDocker });
|
||||
try {
|
||||
const container = await docker.engine.getContainer(id);
|
||||
if (container) {
|
||||
const { default: ansi } = await import('strip-ansi')
|
||||
const logs = (
|
||||
await container.logs({
|
||||
stdout: true,
|
||||
stderr: true,
|
||||
timestamps: true,
|
||||
since,
|
||||
tail: 5000
|
||||
})
|
||||
)
|
||||
.toString()
|
||||
.split('\n')
|
||||
.map((l) => ansi(l.slice(8)))
|
||||
.filter((a) => a);
|
||||
return {
|
||||
logs
|
||||
};
|
||||
}
|
||||
// const found = await checkContainer({ dockerId, container: id })
|
||||
// if (found) {
|
||||
const { default: ansi } = await import('strip-ansi')
|
||||
const { stdout, stderr } = await executeDockerCmd({ dockerId, command: `docker logs --since ${since} --tail 5000 --timestamps ${id}` })
|
||||
const stripLogsStdout = stdout.toString().split('\n').map((l) => ansi(l)).filter((a) => a);
|
||||
const stripLogsStderr = stderr.toString().split('\n').map((l) => ansi(l)).filter((a) => a);
|
||||
const logs = stripLogsStderr.concat(stripLogsStdout)
|
||||
const sortedLogs = logs.sort((a, b) => (day(a.split(' ')[0]).isAfter(day(b.split(' ')[0])) ? 1 : -1))
|
||||
return { logs: sortedLogs }
|
||||
// }
|
||||
} catch (error) {
|
||||
const { statusCode } = error;
|
||||
if (statusCode === 404) {
|
||||
@@ -364,40 +346,57 @@ export async function saveServiceSettings(request: FastifyRequest<SaveServiceSet
|
||||
return errorHandler({ status, message })
|
||||
}
|
||||
}
|
||||
export async function checkServiceDomain(request: FastifyRequest<CheckServiceDomain>) {
|
||||
try {
|
||||
const { id } = request.params
|
||||
const { domain } = request.query
|
||||
const { fqdn, dualCerts } = await prisma.service.findUnique({ where: { id } })
|
||||
return await checkDomainsIsValidInDNS({ hostname: domain, fqdn, dualCerts });
|
||||
} catch ({ status, message }) {
|
||||
return errorHandler({ status, message })
|
||||
}
|
||||
}
|
||||
export async function checkService(request: FastifyRequest<CheckService>) {
|
||||
try {
|
||||
const { id } = request.params;
|
||||
let { fqdn, exposePort, otherFqdns } = request.body;
|
||||
let { fqdn, exposePort, forceSave, otherFqdns, dualCerts } = request.body;
|
||||
|
||||
if (fqdn) fqdn = fqdn.toLowerCase();
|
||||
if (otherFqdns && otherFqdns.length > 0) otherFqdns = otherFqdns.map((f) => f.toLowerCase());
|
||||
if (exposePort) exposePort = Number(exposePort);
|
||||
|
||||
let found = await isDomainConfigured({ id, fqdn });
|
||||
const { destinationDocker: { id: dockerId, remoteIpAddress, remoteEngine }, exposePort: configuredPort } = await prisma.service.findUnique({ where: { id }, include: { destinationDocker: true } })
|
||||
const { isDNSCheckEnabled } = await prisma.setting.findFirst({});
|
||||
|
||||
let found = await isDomainConfigured({ id, fqdn, dockerId });
|
||||
if (found) {
|
||||
throw { status: 500, message: `Domain ${getDomain(fqdn).replace('www.', '')} is already in use!` }
|
||||
}
|
||||
if (otherFqdns && otherFqdns.length > 0) {
|
||||
for (const ofqdn of otherFqdns) {
|
||||
found = await isDomainConfigured({ id, fqdn: ofqdn, checkOwn: true });
|
||||
found = await isDomainConfigured({ id, fqdn: ofqdn, dockerId });
|
||||
if (found) {
|
||||
throw { status: 500, message: `Domain ${getDomain(ofqdn).replace('www.', '')} is already in use!` }
|
||||
}
|
||||
}
|
||||
}
|
||||
if (exposePort) {
|
||||
const { default: getPort } = await import('get-port');
|
||||
exposePort = Number(exposePort);
|
||||
|
||||
if (exposePort < 1024 || exposePort > 65535) {
|
||||
throw { status: 500, message: `Exposed Port needs to be between 1024 and 65535.` }
|
||||
}
|
||||
|
||||
const publicPort = await getPort({ port: exposePort });
|
||||
if (publicPort !== exposePort) {
|
||||
throw { status: 500, message: `Port ${exposePort} is already in use.` }
|
||||
if (configuredPort !== exposePort) {
|
||||
const availablePort = await getFreeExposedPort(id, exposePort, dockerId, remoteIpAddress);
|
||||
if (availablePort.toString() !== exposePort.toString()) {
|
||||
throw { status: 500, message: `Port ${exposePort} is already in use.` }
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isDNSCheckEnabled && !isDev && !forceSave) {
|
||||
let hostname = request.hostname.split(':')[0];
|
||||
if (remoteEngine) hostname = remoteIpAddress;
|
||||
return await checkDomainsIsValidInDNS({ hostname, fqdn, dualCerts });
|
||||
}
|
||||
return {}
|
||||
} catch ({ status, message }) {
|
||||
return errorHandler({ status, message })
|
||||
@@ -742,7 +741,6 @@ async function startPlausibleAnalyticsService(request: FastifyRequest<ServiceSta
|
||||
});
|
||||
}
|
||||
const network = destinationDockerId && destinationDocker.network;
|
||||
const host = getEngine(destinationDocker.engine);
|
||||
const port = getServiceMainPort('plausibleanalytics');
|
||||
|
||||
const { workdir } = await createDirectories({ repository: type, buildId: id });
|
||||
@@ -859,10 +857,8 @@ COPY ./init-db.sh /docker-entrypoint-initdb.d/init-db.sh`;
|
||||
};
|
||||
const composeFileDestination = `${workdir}/docker-compose.yaml`;
|
||||
await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
|
||||
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`);
|
||||
await asyncExecShell(
|
||||
`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up --build -d`
|
||||
);
|
||||
//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 })
|
||||
@@ -877,17 +873,17 @@ async function stopPlausibleAnalyticsService(request: FastifyRequest<ServiceStar
|
||||
if (destinationDockerId) {
|
||||
const engine = destinationDocker.engine;
|
||||
|
||||
let found = await checkContainer(engine, id);
|
||||
let found = await checkContainer({ dockerId: destinationDocker.id, container: id });
|
||||
if (found) {
|
||||
await removeContainer({ id, engine });
|
||||
await removeContainer({ id, dockerId: destinationDocker.id });
|
||||
}
|
||||
found = await checkContainer(engine, `${id}-postgresql`);
|
||||
found = await checkContainer({ dockerId: destinationDocker.id, container: `${id}-postgresql` });
|
||||
if (found) {
|
||||
await removeContainer({ id: `${id}-postgresql`, engine });
|
||||
await removeContainer({ id: `${id}-postgresql`, dockerId: destinationDocker.id });
|
||||
}
|
||||
found = await checkContainer(engine, `${id}-clickhouse`);
|
||||
found = await checkContainer({ dockerId: destinationDocker.id, container: `${id}-clickhouse` });
|
||||
if (found) {
|
||||
await removeContainer({ id: `${id}-clickhouse`, engine });
|
||||
await removeContainer({ id: `${id}-clickhouse`, dockerId: destinationDocker.id });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -905,7 +901,6 @@ async function startNocodbService(request: FastifyRequest<ServiceStartStop>) {
|
||||
const { type, version, destinationDockerId, destinationDocker, serviceSecret, exposePort } =
|
||||
service;
|
||||
const network = destinationDockerId && destinationDocker.network;
|
||||
const host = getEngine(destinationDocker.engine);
|
||||
const port = getServiceMainPort('nocodb');
|
||||
|
||||
const { workdir } = await createDirectories({ repository: type, buildId: id });
|
||||
@@ -956,8 +951,8 @@ async function startNocodbService(request: FastifyRequest<ServiceStartStop>) {
|
||||
};
|
||||
const composeFileDestination = `${workdir}/docker-compose.yaml`;
|
||||
await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
|
||||
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`);
|
||||
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`);
|
||||
//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 })
|
||||
@@ -970,10 +965,9 @@ async function stopNocodbService(request: FastifyRequest<ServiceStartStop>) {
|
||||
const service = await getServiceFromDB({ id, teamId });
|
||||
const { destinationDockerId, destinationDocker, fqdn } = service;
|
||||
if (destinationDockerId) {
|
||||
const engine = destinationDocker.engine;
|
||||
const found = await checkContainer(engine, id);
|
||||
const found = await checkContainer({ dockerId: destinationDocker.id, container: id });
|
||||
if (found) {
|
||||
await removeContainer({ id, engine });
|
||||
await removeContainer({ id, dockerId: destinationDocker.id });
|
||||
}
|
||||
}
|
||||
return {}
|
||||
@@ -999,10 +993,10 @@ async function startMinioService(request: FastifyRequest<ServiceStartStop>) {
|
||||
} = service;
|
||||
|
||||
const network = destinationDockerId && destinationDocker.network;
|
||||
const host = getEngine(destinationDocker.engine);
|
||||
const port = getServiceMainPort('minio');
|
||||
|
||||
const publicPort = await getFreePort();
|
||||
const { service: { destinationDocker: { id: dockerId } } } = await prisma.minio.findUnique({ where: { serviceId: id }, include: { service: { include: { destinationDocker: true } } } })
|
||||
const publicPort = await getFreePublicPort(id, dockerId);
|
||||
|
||||
const consolePort = 9001;
|
||||
const { workdir } = await createDirectories({ repository: type, buildId: id });
|
||||
@@ -1058,8 +1052,8 @@ async function startMinioService(request: FastifyRequest<ServiceStartStop>) {
|
||||
};
|
||||
const composeFileDestination = `${workdir}/docker-compose.yaml`;
|
||||
await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
|
||||
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`);
|
||||
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`);
|
||||
//await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} pull` })
|
||||
await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} up --build -d` })
|
||||
await prisma.minio.update({ where: { serviceId: id }, data: { publicPort } });
|
||||
return {}
|
||||
} catch ({ status, message }) {
|
||||
@@ -1071,13 +1065,12 @@ async function stopMinioService(request: FastifyRequest<ServiceStartStop>) {
|
||||
const { id } = request.params;
|
||||
const teamId = request.user.teamId;
|
||||
const service = await getServiceFromDB({ id, teamId });
|
||||
const { destinationDockerId, destinationDocker, fqdn } = service;
|
||||
const { destinationDockerId, destinationDocker } = service;
|
||||
await prisma.minio.update({ where: { serviceId: id }, data: { publicPort: null } })
|
||||
if (destinationDockerId) {
|
||||
const engine = destinationDocker.engine;
|
||||
const found = await checkContainer(engine, id);
|
||||
const found = await checkContainer({ dockerId: destinationDocker.id, container: id });
|
||||
if (found) {
|
||||
await removeContainer({ id, engine });
|
||||
await removeContainer({ id, dockerId: destinationDocker.id });
|
||||
}
|
||||
}
|
||||
return {}
|
||||
@@ -1103,7 +1096,6 @@ async function startVscodeService(request: FastifyRequest<ServiceStartStop>) {
|
||||
} = service;
|
||||
|
||||
const network = destinationDockerId && destinationDocker.network;
|
||||
const host = getEngine(destinationDocker.engine);
|
||||
const port = getServiceMainPort('vscodeserver');
|
||||
|
||||
const { workdir } = await createDirectories({ repository: type, buildId: id });
|
||||
@@ -1175,16 +1167,16 @@ async function startVscodeService(request: FastifyRequest<ServiceStartStop>) {
|
||||
const composeFileDestination = `${workdir}/docker-compose.yaml`;
|
||||
await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
|
||||
|
||||
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`);
|
||||
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`);
|
||||
//await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} pull` })
|
||||
await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} up --build -d` })
|
||||
|
||||
const changePermissionOn = persistentStorage.map((p) => p.path);
|
||||
if (changePermissionOn.length > 0) {
|
||||
await asyncExecShell(
|
||||
`DOCKER_HOST=${host} docker exec -u root ${id} chown -R 1000:1000 ${changePermissionOn.join(
|
||||
await executeDockerCmd({
|
||||
dockerId: destinationDocker.id, command: `docker exec -u root ${id} chown -R 1000:1000 ${changePermissionOn.join(
|
||||
' '
|
||||
)}`
|
||||
);
|
||||
})
|
||||
}
|
||||
return {}
|
||||
} catch ({ status, message }) {
|
||||
@@ -1196,12 +1188,11 @@ async function stopVscodeService(request: FastifyRequest<ServiceStartStop>) {
|
||||
const { id } = request.params;
|
||||
const teamId = request.user.teamId;
|
||||
const service = await getServiceFromDB({ id, teamId });
|
||||
const { destinationDockerId, destinationDocker, fqdn } = service;
|
||||
const { destinationDockerId, destinationDocker } = service;
|
||||
if (destinationDockerId) {
|
||||
const engine = destinationDocker.engine;
|
||||
const found = await checkContainer(engine, id);
|
||||
const found = await checkContainer({ dockerId: destinationDocker.id, container: id });
|
||||
if (found) {
|
||||
await removeContainer({ id, engine });
|
||||
await removeContainer({ id, dockerId: destinationDocker.id });
|
||||
}
|
||||
}
|
||||
return {}
|
||||
@@ -1236,7 +1227,6 @@ async function startWordpressService(request: FastifyRequest<ServiceStartStop>)
|
||||
} = service;
|
||||
|
||||
const network = destinationDockerId && destinationDocker.network;
|
||||
const host = getEngine(destinationDocker.engine);
|
||||
const image = getServiceImage(type);
|
||||
const port = getServiceMainPort('wordpress');
|
||||
|
||||
@@ -1328,8 +1318,10 @@ async function startWordpressService(request: FastifyRequest<ServiceStartStop>)
|
||||
}
|
||||
const composeFileDestination = `${workdir}/docker-compose.yaml`;
|
||||
await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
|
||||
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`);
|
||||
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`);
|
||||
|
||||
//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 })
|
||||
@@ -1346,28 +1338,27 @@ async function stopWordpressService(request: FastifyRequest<ServiceStartStop>) {
|
||||
wordpress: { ftpEnabled }
|
||||
} = service;
|
||||
if (destinationDockerId) {
|
||||
const engine = destinationDocker.engine;
|
||||
try {
|
||||
const found = await checkContainer(engine, id);
|
||||
const found = await checkContainer({ dockerId: destinationDocker.id, container: id });
|
||||
if (found) {
|
||||
await removeContainer({ id, engine });
|
||||
await removeContainer({ id, dockerId: destinationDocker.id });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
try {
|
||||
const found = await checkContainer(engine, `${id}-mysql`);
|
||||
const found = await checkContainer({ dockerId: destinationDocker.id, container: `${id}-mysql` });
|
||||
if (found) {
|
||||
await removeContainer({ id: `${id}-mysql`, engine });
|
||||
await removeContainer({ id: `${id}-mysql`, dockerId: destinationDocker.id });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
try {
|
||||
if (ftpEnabled) {
|
||||
const found = await checkContainer(engine, `${id}-ftp`);
|
||||
const found = await checkContainer({ dockerId: destinationDocker.id, container: `${id}-ftp` });
|
||||
if (found) {
|
||||
await removeContainer({ id: `${id}-ftp`, engine });
|
||||
await removeContainer({ id: `${id}-ftp`, dockerId: destinationDocker.id });
|
||||
}
|
||||
await prisma.wordpress.update({
|
||||
where: { serviceId: id },
|
||||
@@ -1393,7 +1384,6 @@ async function startVaultwardenService(request: FastifyRequest<ServiceStartStop>
|
||||
service;
|
||||
|
||||
const network = destinationDockerId && destinationDocker.network;
|
||||
const host = getEngine(destinationDocker.engine);
|
||||
const port = getServiceMainPort('vaultwarden');
|
||||
|
||||
const { workdir } = await createDirectories({ repository: type, buildId: id });
|
||||
@@ -1444,8 +1434,10 @@ async function startVaultwardenService(request: FastifyRequest<ServiceStartStop>
|
||||
};
|
||||
const composeFileDestination = `${workdir}/docker-compose.yaml`;
|
||||
await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
|
||||
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`);
|
||||
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`);
|
||||
|
||||
//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 })
|
||||
@@ -1456,14 +1448,12 @@ async function stopVaultwardenService(request: FastifyRequest<ServiceStartStop>)
|
||||
const { id } = request.params;
|
||||
const teamId = request.user.teamId;
|
||||
const service = await getServiceFromDB({ id, teamId });
|
||||
const { destinationDockerId, destinationDocker, fqdn } = service;
|
||||
const { destinationDockerId, destinationDocker } = service;
|
||||
if (destinationDockerId) {
|
||||
const engine = destinationDocker.engine;
|
||||
|
||||
try {
|
||||
const found = await checkContainer(engine, id);
|
||||
const found = await checkContainer({ dockerId: destinationDocker.id, container: id });
|
||||
if (found) {
|
||||
await removeContainer({ id, engine });
|
||||
await removeContainer({ id, dockerId: destinationDocker.id });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
@@ -1483,7 +1473,6 @@ async function startLanguageToolService(request: FastifyRequest<ServiceStartStop
|
||||
const { type, version, destinationDockerId, destinationDocker, serviceSecret, exposePort } =
|
||||
service;
|
||||
const network = destinationDockerId && destinationDocker.network;
|
||||
const host = getEngine(destinationDocker.engine);
|
||||
const port = getServiceMainPort('languagetool');
|
||||
|
||||
const { workdir } = await createDirectories({ repository: type, buildId: id });
|
||||
@@ -1536,8 +1525,9 @@ async function startLanguageToolService(request: FastifyRequest<ServiceStartStop
|
||||
const composeFileDestination = `${workdir}/docker-compose.yaml`;
|
||||
await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
|
||||
|
||||
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`);
|
||||
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`);
|
||||
//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 })
|
||||
@@ -1548,14 +1538,12 @@ async function stopLanguageToolService(request: FastifyRequest<ServiceStartStop>
|
||||
const { id } = request.params;
|
||||
const teamId = request.user.teamId;
|
||||
const service = await getServiceFromDB({ id, teamId });
|
||||
const { destinationDockerId, destinationDocker, fqdn } = service;
|
||||
const { destinationDockerId, destinationDocker } = service;
|
||||
if (destinationDockerId) {
|
||||
const engine = destinationDocker.engine;
|
||||
|
||||
try {
|
||||
const found = await checkContainer(engine, id);
|
||||
const found = await checkContainer({ dockerId: destinationDocker.id, container: id });
|
||||
if (found) {
|
||||
await removeContainer({ id, engine });
|
||||
await removeContainer({ id, dockerId: destinationDocker.id });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
@@ -1575,7 +1563,6 @@ async function startN8nService(request: FastifyRequest<ServiceStartStop>) {
|
||||
const { type, version, destinationDockerId, destinationDocker, serviceSecret, exposePort } =
|
||||
service;
|
||||
const network = destinationDockerId && destinationDocker.network;
|
||||
const host = getEngine(destinationDocker.engine);
|
||||
const port = getServiceMainPort('n8n');
|
||||
|
||||
const { workdir } = await createDirectories({ repository: type, buildId: id });
|
||||
@@ -1628,8 +1615,10 @@ async function startN8nService(request: FastifyRequest<ServiceStartStop>) {
|
||||
};
|
||||
const composeFileDestination = `${workdir}/docker-compose.yaml`;
|
||||
await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
|
||||
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`);
|
||||
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`);
|
||||
|
||||
//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 })
|
||||
@@ -1640,14 +1629,12 @@ async function stopN8nService(request: FastifyRequest<ServiceStartStop>) {
|
||||
const { id } = request.params;
|
||||
const teamId = request.user.teamId;
|
||||
const service = await getServiceFromDB({ id, teamId });
|
||||
const { destinationDockerId, destinationDocker, fqdn } = service;
|
||||
const { destinationDockerId, destinationDocker } = service;
|
||||
if (destinationDockerId) {
|
||||
const engine = destinationDocker.engine;
|
||||
|
||||
try {
|
||||
const found = await checkContainer(engine, id);
|
||||
const found = await checkContainer({ dockerId: destinationDocker.id, container: id });
|
||||
if (found) {
|
||||
await removeContainer({ id, engine });
|
||||
await removeContainer({ id, dockerId: destinationDocker.id });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
@@ -1667,7 +1654,6 @@ async function startUptimekumaService(request: FastifyRequest<ServiceStartStop>)
|
||||
const { type, version, destinationDockerId, destinationDocker, serviceSecret, exposePort } =
|
||||
service;
|
||||
const network = destinationDockerId && destinationDocker.network;
|
||||
const host = getEngine(destinationDocker.engine);
|
||||
const port = getServiceMainPort('uptimekuma');
|
||||
|
||||
const { workdir } = await createDirectories({ repository: type, buildId: id });
|
||||
@@ -1719,8 +1705,9 @@ async function startUptimekumaService(request: FastifyRequest<ServiceStartStop>)
|
||||
const composeFileDestination = `${workdir}/docker-compose.yaml`;
|
||||
await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
|
||||
|
||||
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`);
|
||||
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`);
|
||||
//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 })
|
||||
@@ -1731,14 +1718,12 @@ async function stopUptimekumaService(request: FastifyRequest<ServiceStartStop>)
|
||||
const { id } = request.params;
|
||||
const teamId = request.user.teamId;
|
||||
const service = await getServiceFromDB({ id, teamId });
|
||||
const { destinationDockerId, destinationDocker, fqdn } = service;
|
||||
const { destinationDockerId, destinationDocker } = service;
|
||||
if (destinationDockerId) {
|
||||
const engine = destinationDocker.engine;
|
||||
|
||||
try {
|
||||
const found = await checkContainer(engine, id);
|
||||
const found = await checkContainer({ dockerId: destinationDocker.id, container: id });
|
||||
if (found) {
|
||||
await removeContainer({ id, engine });
|
||||
await removeContainer({ id, dockerId: destinationDocker.id });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
@@ -1774,7 +1759,6 @@ async function startGhostService(request: FastifyRequest<ServiceStartStop>) {
|
||||
}
|
||||
} = service;
|
||||
const network = destinationDockerId && destinationDocker.network;
|
||||
const host = getEngine(destinationDocker.engine);
|
||||
|
||||
const { workdir } = await createDirectories({ repository: type, buildId: id });
|
||||
const image = getServiceImage(type);
|
||||
@@ -1871,8 +1855,9 @@ async function startGhostService(request: FastifyRequest<ServiceStartStop>) {
|
||||
const composeFileDestination = `${workdir}/docker-compose.yaml`;
|
||||
await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
|
||||
|
||||
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`);
|
||||
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`);
|
||||
//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 })
|
||||
@@ -1883,18 +1868,16 @@ async function stopGhostService(request: FastifyRequest<ServiceStartStop>) {
|
||||
const { id } = request.params;
|
||||
const teamId = request.user.teamId;
|
||||
const service = await getServiceFromDB({ id, teamId });
|
||||
const { destinationDockerId, destinationDocker, fqdn } = service;
|
||||
const { destinationDockerId, destinationDocker } = service;
|
||||
if (destinationDockerId) {
|
||||
const engine = destinationDocker.engine;
|
||||
|
||||
try {
|
||||
let found = await checkContainer(engine, id);
|
||||
let found = await checkContainer({ dockerId: destinationDocker.id, container: id });
|
||||
if (found) {
|
||||
await removeContainer({ id, engine });
|
||||
await removeContainer({ id, dockerId: destinationDocker.id });
|
||||
}
|
||||
found = await checkContainer(engine, `${id}-mariadb`);
|
||||
found = await checkContainer({ dockerId: destinationDocker.id, container: `${id}-mariadb` });
|
||||
if (found) {
|
||||
await removeContainer({ id: `${id}-mariadb`, engine });
|
||||
await removeContainer({ id: `${id}-mariadb`, dockerId: destinationDocker.id });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
@@ -1917,7 +1900,6 @@ async function startMeilisearchService(request: FastifyRequest<ServiceStartStop>
|
||||
const { type, version, destinationDockerId, destinationDocker, serviceSecret, exposePort } =
|
||||
service;
|
||||
const network = destinationDockerId && destinationDocker.network;
|
||||
const host = getEngine(destinationDocker.engine);
|
||||
const port = getServiceMainPort('meilisearch');
|
||||
|
||||
const { workdir } = await createDirectories({ repository: type, buildId: id });
|
||||
@@ -1972,8 +1954,10 @@ async function startMeilisearchService(request: FastifyRequest<ServiceStartStop>
|
||||
const composeFileDestination = `${workdir}/docker-compose.yaml`;
|
||||
await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
|
||||
|
||||
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`);
|
||||
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`);
|
||||
|
||||
//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 })
|
||||
@@ -1984,14 +1968,12 @@ async function stopMeilisearchService(request: FastifyRequest<ServiceStartStop>)
|
||||
const { id } = request.params;
|
||||
const teamId = request.user.teamId;
|
||||
const service = await getServiceFromDB({ id, teamId });
|
||||
const { destinationDockerId, destinationDocker, fqdn } = service;
|
||||
const { destinationDockerId, destinationDocker } = service;
|
||||
if (destinationDockerId) {
|
||||
const engine = destinationDocker.engine;
|
||||
|
||||
try {
|
||||
const found = await checkContainer(engine, id);
|
||||
const found = await checkContainer({ dockerId: destinationDocker.id, container: id });
|
||||
if (found) {
|
||||
await removeContainer({ id, engine });
|
||||
await removeContainer({ id, dockerId: destinationDocker.id });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
@@ -2024,7 +2006,6 @@ async function startUmamiService(request: FastifyRequest<ServiceStartStop>) {
|
||||
}
|
||||
} = service;
|
||||
const network = destinationDockerId && destinationDocker.network;
|
||||
const host = getEngine(destinationDocker.engine);
|
||||
const port = getServiceMainPort('umami');
|
||||
|
||||
const { workdir } = await createDirectories({ repository: type, buildId: id });
|
||||
@@ -2191,8 +2172,10 @@ async function startUmamiService(request: FastifyRequest<ServiceStartStop>) {
|
||||
const composeFileDestination = `${workdir}/docker-compose.yaml`;
|
||||
await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
|
||||
|
||||
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`);
|
||||
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`);
|
||||
|
||||
//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 })
|
||||
@@ -2203,22 +2186,20 @@ async function stopUmamiService(request: FastifyRequest<ServiceStartStop>) {
|
||||
const { id } = request.params;
|
||||
const teamId = request.user.teamId;
|
||||
const service = await getServiceFromDB({ id, teamId });
|
||||
const { destinationDockerId, destinationDocker, fqdn } = service;
|
||||
const { destinationDockerId, destinationDocker } = service;
|
||||
if (destinationDockerId) {
|
||||
const engine = destinationDocker.engine;
|
||||
|
||||
try {
|
||||
const found = await checkContainer(engine, id);
|
||||
const found = await checkContainer({ dockerId: destinationDocker.id, container: id });
|
||||
if (found) {
|
||||
await removeContainer({ id, engine });
|
||||
await removeContainer({ id, dockerId: destinationDocker.id });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
try {
|
||||
const found = await checkContainer(engine, `${id}-postgresql`);
|
||||
const found = await checkContainer({ dockerId: destinationDocker.id, container: `${id}-postgresql` });
|
||||
if (found) {
|
||||
await removeContainer({ id: `${id}-postgresql`, engine });
|
||||
await removeContainer({ id: `${id}-postgresql`, dockerId: destinationDocker.id });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
@@ -2245,7 +2226,6 @@ async function startHasuraService(request: FastifyRequest<ServiceStartStop>) {
|
||||
hasura: { postgresqlUser, postgresqlPassword, postgresqlDatabase }
|
||||
} = service;
|
||||
const network = destinationDockerId && destinationDocker.network;
|
||||
const host = getEngine(destinationDocker.engine);
|
||||
const port = getServiceMainPort('hasura');
|
||||
|
||||
const { workdir } = await createDirectories({ repository: type, buildId: id });
|
||||
@@ -2327,8 +2307,9 @@ async function startHasuraService(request: FastifyRequest<ServiceStartStop>) {
|
||||
const composeFileDestination = `${workdir}/docker-compose.yaml`;
|
||||
await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
|
||||
|
||||
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`);
|
||||
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`);
|
||||
//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 })
|
||||
@@ -2339,22 +2320,20 @@ async function stopHasuraService(request: FastifyRequest<ServiceStartStop>) {
|
||||
const { id } = request.params;
|
||||
const teamId = request.user.teamId;
|
||||
const service = await getServiceFromDB({ id, teamId });
|
||||
const { destinationDockerId, destinationDocker, fqdn } = service;
|
||||
const { destinationDockerId, destinationDocker } = service;
|
||||
if (destinationDockerId) {
|
||||
const engine = destinationDocker.engine;
|
||||
|
||||
try {
|
||||
const found = await checkContainer(engine, id);
|
||||
const found = await checkContainer({ dockerId: destinationDocker.id, container: id });
|
||||
if (found) {
|
||||
await removeContainer({ id, engine });
|
||||
await removeContainer({ id, dockerId: destinationDocker.id });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
try {
|
||||
const found = await checkContainer(engine, `${id}-postgresql`);
|
||||
const found = await checkContainer({ dockerId: destinationDocker.id, container: `${id}-postgresql` });
|
||||
if (found) {
|
||||
await removeContainer({ id: `${id}-postgresql`, engine });
|
||||
await removeContainer({ id: `${id}-postgresql`, dockerId: destinationDocker.id });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
@@ -2396,7 +2375,6 @@ async function startFiderService(request: FastifyRequest<ServiceStartStop>) {
|
||||
}
|
||||
} = service;
|
||||
const network = destinationDockerId && destinationDocker.network;
|
||||
const host = getEngine(destinationDocker.engine);
|
||||
const port = getServiceMainPort('fider');
|
||||
|
||||
const { workdir } = await createDirectories({ repository: type, buildId: id });
|
||||
@@ -2489,8 +2467,8 @@ async function startFiderService(request: FastifyRequest<ServiceStartStop>) {
|
||||
const composeFileDestination = `${workdir}/docker-compose.yaml`;
|
||||
await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
|
||||
|
||||
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`);
|
||||
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`);
|
||||
//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 }) {
|
||||
@@ -2502,22 +2480,20 @@ async function stopFiderService(request: FastifyRequest<ServiceStartStop>) {
|
||||
const { id } = request.params;
|
||||
const teamId = request.user.teamId;
|
||||
const service = await getServiceFromDB({ id, teamId });
|
||||
const { destinationDockerId, destinationDocker, fqdn } = service;
|
||||
const { destinationDockerId, destinationDocker } = service;
|
||||
if (destinationDockerId) {
|
||||
const engine = destinationDocker.engine;
|
||||
|
||||
try {
|
||||
const found = await checkContainer(engine, id);
|
||||
const found = await checkContainer({ dockerId: destinationDocker.id, container: id });
|
||||
if (found) {
|
||||
await removeContainer({ id, engine });
|
||||
await removeContainer({ id, dockerId: destinationDocker.id });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
try {
|
||||
const found = await checkContainer(engine, `${id}-postgresql`);
|
||||
const found = await checkContainer({ dockerId: destinationDocker.id, container: `${id}-postgresql` });
|
||||
if (found) {
|
||||
await removeContainer({ id: `${id}-postgresql`, engine });
|
||||
await removeContainer({ id: `${id}-postgresql`, dockerId: destinationDocker.id });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
@@ -2554,12 +2530,10 @@ async function startMoodleService(request: FastifyRequest<ServiceStartStop>) {
|
||||
}
|
||||
} = service;
|
||||
const network = destinationDockerId && destinationDocker.network;
|
||||
const host = getEngine(destinationDocker.engine);
|
||||
const port = getServiceMainPort('moodle');
|
||||
|
||||
const { workdir } = await createDirectories({ repository: type, buildId: id });
|
||||
const image = getServiceImage(type);
|
||||
const domain = getDomain(fqdn);
|
||||
const config = {
|
||||
moodle: {
|
||||
image: `${image}:${version}`,
|
||||
@@ -2652,8 +2626,10 @@ async function startMoodleService(request: FastifyRequest<ServiceStartStop>) {
|
||||
const composeFileDestination = `${workdir}/docker-compose.yaml`;
|
||||
await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
|
||||
|
||||
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`);
|
||||
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`);
|
||||
|
||||
//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 }) {
|
||||
@@ -2665,22 +2641,20 @@ async function stopMoodleService(request: FastifyRequest<ServiceStartStop>) {
|
||||
const { id } = request.params;
|
||||
const teamId = request.user.teamId;
|
||||
const service = await getServiceFromDB({ id, teamId });
|
||||
const { destinationDockerId, destinationDocker, fqdn } = service;
|
||||
const { destinationDockerId, destinationDocker } = service;
|
||||
if (destinationDockerId) {
|
||||
const engine = destinationDocker.engine;
|
||||
|
||||
try {
|
||||
const found = await checkContainer(engine, id);
|
||||
const found = await checkContainer({ dockerId: destinationDocker.id, container: id });
|
||||
if (found) {
|
||||
await removeContainer({ id, engine });
|
||||
await removeContainer({ id, dockerId: destinationDocker.id });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
try {
|
||||
const found = await checkContainer(engine, `${id}-mariadb`);
|
||||
const found = await checkContainer({ dockerId: destinationDocker.id, container: `${id}-mariadb` });
|
||||
if (found) {
|
||||
await removeContainer({ id: `${id}-mariadb`, engine });
|
||||
await removeContainer({ id: `${id}-mariadb`, dockerId: destinationDocker.id });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
@@ -2703,14 +2677,10 @@ export async function activatePlausibleUsers(request: FastifyRequest<OnlyId>, re
|
||||
plausibleAnalytics: { postgresqlUser, postgresqlPassword, postgresqlDatabase }
|
||||
} = await getServiceFromDB({ id, teamId });
|
||||
if (destinationDockerId) {
|
||||
const docker = dockerInstance({ destinationDocker });
|
||||
const container = await docker.engine.getContainer(id);
|
||||
const command = await container.exec({
|
||||
Cmd: [
|
||||
`psql -H postgresql://${postgresqlUser}:${postgresqlPassword}@localhost:5432/${postgresqlDatabase} -c "UPDATE users SET email_verified = true;"`
|
||||
]
|
||||
});
|
||||
await command.start();
|
||||
await executeDockerCmd({
|
||||
dockerId: destinationDocker.id,
|
||||
command: `docker exec ${id} 'psql -H postgresql://${postgresqlUser}:${postgresqlPassword}@localhost:5432/${postgresqlDatabase} -c "UPDATE users SET email_verified = true;"'`
|
||||
})
|
||||
return await reply.code(201).send()
|
||||
}
|
||||
throw { status: 500, message: 'Could not activate users.' }
|
||||
@@ -2722,7 +2692,10 @@ export async function activateWordpressFtp(request: FastifyRequest<ActivateWordp
|
||||
const { id } = request.params
|
||||
const { ftpEnabled } = request.body;
|
||||
|
||||
const publicPort = await getFreePort();
|
||||
const { service: { destinationDocker: { id: dockerId } } } = await prisma.wordpress.findUnique({ where: { serviceId: id }, include: { service: { include: { destinationDocker: true } } } })
|
||||
|
||||
const publicPort = await getFreePublicPort(id, dockerId);
|
||||
|
||||
let ftpUser = cuid();
|
||||
let ftpPassword = generatePassword();
|
||||
|
||||
@@ -2742,7 +2715,6 @@ export async function activateWordpressFtp(request: FastifyRequest<ActivateWordp
|
||||
ftpHostKeyPrivate
|
||||
} = data;
|
||||
const { network, engine } = destinationDocker;
|
||||
const host = getEngine(engine);
|
||||
if (ftpEnabled) {
|
||||
if (user) ftpUser = user;
|
||||
if (savedPassword) ftpPassword = decrypt(savedPassword);
|
||||
@@ -2789,7 +2761,7 @@ export async function activateWordpressFtp(request: FastifyRequest<ActivateWordp
|
||||
});
|
||||
|
||||
try {
|
||||
const isRunning = await checkContainer(engine, `${id}-ftp`);
|
||||
const isRunning = await checkContainer({ dockerId: destinationDocker.id, container: `${id}-ftp` });
|
||||
if (isRunning) {
|
||||
await asyncExecShell(
|
||||
`DOCKER_HOST=${host} docker stop -t 0 ${id}-ftp && docker rm ${id}-ftp`
|
||||
@@ -2841,9 +2813,11 @@ export async function activateWordpressFtp(request: FastifyRequest<ActivateWordp
|
||||
);
|
||||
await asyncExecShell(`chmod +x ${hostkeyDir}/${id}.sh`);
|
||||
await fs.writeFile(`${hostkeyDir}/${id}-docker-compose.yml`, yaml.dump(compose));
|
||||
await asyncExecShell(
|
||||
`DOCKER_HOST=${host} docker compose -f ${hostkeyDir}/${id}-docker-compose.yml up -d`
|
||||
);
|
||||
await executeDockerCmd({
|
||||
dockerId: destinationDocker.id,
|
||||
command: `docker compose -f ${hostkeyDir}/${id}-docker-compose.yml up -d`
|
||||
})
|
||||
|
||||
}
|
||||
return reply.code(201).send({
|
||||
publicPort,
|
||||
@@ -2856,9 +2830,11 @@ export async function activateWordpressFtp(request: FastifyRequest<ActivateWordp
|
||||
data: { ftpPublicPort: null }
|
||||
});
|
||||
try {
|
||||
await asyncExecShell(
|
||||
`DOCKER_HOST=${host} docker stop -t 0 ${id}-ftp && docker rm ${id}-ftp`
|
||||
);
|
||||
await executeDockerCmd({
|
||||
dockerId: destinationDocker.id,
|
||||
command: `docker stop -t 0 ${id}-ftp && docker rm ${id}-ftp`
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
//
|
||||
}
|
||||
|
||||
@@ -3,12 +3,14 @@ import {
|
||||
activatePlausibleUsers,
|
||||
activateWordpressFtp,
|
||||
checkService,
|
||||
checkServiceDomain,
|
||||
deleteService,
|
||||
deleteServiceSecret,
|
||||
deleteServiceStorage,
|
||||
getService,
|
||||
getServiceLogs,
|
||||
getServiceSecrets,
|
||||
getServiceStatus,
|
||||
getServiceStorages,
|
||||
getServiceType,
|
||||
getServiceUsage,
|
||||
@@ -28,7 +30,7 @@ import {
|
||||
} from './handlers';
|
||||
|
||||
import type { OnlyId } from '../../../../types';
|
||||
import type { ActivateWordpressFtp, CheckService, 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';
|
||||
|
||||
const root: FastifyPluginAsync = async (fastify): Promise<void> => {
|
||||
fastify.addHook('onRequest', async (request) => {
|
||||
@@ -41,6 +43,9 @@ const root: FastifyPluginAsync = async (fastify): Promise<void> => {
|
||||
fastify.post<SaveService>('/:id', async (request, reply) => await saveService(request, reply));
|
||||
fastify.delete<OnlyId>('/:id', async (request) => await deleteService(request));
|
||||
|
||||
fastify.get<OnlyId>('/:id/status', async (request) => await getServiceStatus(request));
|
||||
|
||||
fastify.get<CheckServiceDomain>('/:id/check', async (request) => await checkServiceDomain(request));
|
||||
fastify.post<CheckService>('/:id/check', async (request) => await checkService(request));
|
||||
|
||||
fastify.post<SaveServiceSettings>('/:id/settings', async (request, reply) => await saveServiceSettings(request, reply));
|
||||
|
||||
@@ -25,9 +25,16 @@ export interface SaveServiceSettings extends OnlyId {
|
||||
dualCerts: boolean
|
||||
}
|
||||
}
|
||||
export interface CheckServiceDomain extends OnlyId {
|
||||
Querystring: {
|
||||
domain: string
|
||||
}
|
||||
}
|
||||
export interface CheckService extends OnlyId {
|
||||
Body: {
|
||||
fqdn: string,
|
||||
forceSave: boolean,
|
||||
dualCerts: boolean,
|
||||
exposePort: number,
|
||||
otherFqdns: Array<string>
|
||||
}
|
||||
|
||||
@@ -1,15 +1,24 @@
|
||||
import { promises as dns } from 'dns';
|
||||
|
||||
import type { FastifyReply, FastifyRequest } from 'fastify';
|
||||
import { checkDomainsIsValidInDNS, errorHandler, getDomain, isDNSValid, isDomainConfigured, listSettings, prisma } from '../../../../lib/common';
|
||||
import { CheckDNS, CheckDomain, DeleteDomain, SaveSettings } from './types';
|
||||
import { checkDomainsIsValidInDNS, decrypt, encrypt, errorHandler, getDomain, isDNSValid, isDomainConfigured, listSettings, prisma } from '../../../../lib/common';
|
||||
import { CheckDNS, CheckDomain, DeleteDomain, DeleteSSHKey, SaveSettings, SaveSSHKey } from './types';
|
||||
|
||||
|
||||
export async function listAllSettings(request: FastifyRequest) {
|
||||
try {
|
||||
const teamId = request.user.teamId;
|
||||
const settings = await listSettings();
|
||||
const sshKeys = await prisma.sshKey.findMany({ where: { team: { id: teamId } } })
|
||||
const unencryptedKeys = []
|
||||
if (sshKeys.length > 0) {
|
||||
for (const key of sshKeys) {
|
||||
unencryptedKeys.push({ id: key.id, name: key.name, privateKey: decrypt(key.privateKey), createdAt: key.createdAt })
|
||||
}
|
||||
}
|
||||
return {
|
||||
settings
|
||||
settings,
|
||||
sshKeys: unencryptedKeys
|
||||
}
|
||||
} catch ({ status, message }) {
|
||||
return errorHandler({ status, message })
|
||||
@@ -68,7 +77,8 @@ export async function checkDomain(request: FastifyRequest<CheckDomain>) {
|
||||
throw "Domain already configured";
|
||||
}
|
||||
if (isDNSCheckEnabled && !forceSave) {
|
||||
return await checkDomainsIsValidInDNS({ hostname: request.hostname.split(':')[0], fqdn, dualCerts });
|
||||
const hostname = request.hostname.split(':')[0]
|
||||
return await checkDomainsIsValidInDNS({ hostname, fqdn, dualCerts });
|
||||
}
|
||||
return {};
|
||||
} catch ({ status, message }) {
|
||||
@@ -83,4 +93,31 @@ export async function checkDNS(request: FastifyRequest<CheckDNS>) {
|
||||
} catch ({ status, message }) {
|
||||
return errorHandler({ status, message })
|
||||
}
|
||||
}
|
||||
|
||||
export async function saveSSHKey(request: FastifyRequest<SaveSSHKey>, reply: FastifyReply) {
|
||||
try {
|
||||
const teamId = request.user.teamId;
|
||||
const { privateKey, name } = request.body;
|
||||
const found = await prisma.sshKey.findMany({ where: { name } })
|
||||
if (found.length > 0) {
|
||||
throw {
|
||||
message: "Name already used. Choose another one please."
|
||||
}
|
||||
}
|
||||
const encryptedSSHKey = encrypt(privateKey)
|
||||
await prisma.sshKey.create({ data: { name, privateKey: encryptedSSHKey, team: { connect: { id: teamId } } } })
|
||||
return reply.code(201).send()
|
||||
} catch ({ status, message }) {
|
||||
return errorHandler({ status, message })
|
||||
}
|
||||
}
|
||||
export async function deleteSSHKey(request: FastifyRequest<DeleteSSHKey>, reply: FastifyReply) {
|
||||
try {
|
||||
const { id } = request.body;
|
||||
await prisma.sshKey.delete({ where: { id } })
|
||||
return reply.code(201).send()
|
||||
} catch ({ status, message }) {
|
||||
return errorHandler({ status, message })
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { FastifyPluginAsync } from 'fastify';
|
||||
import { checkDNS, checkDomain, deleteDomain, listAllSettings, saveSettings } from './handlers';
|
||||
import { CheckDNS, CheckDomain, DeleteDomain, SaveSettings } from './types';
|
||||
import { checkDNS, checkDomain, deleteDomain, deleteSSHKey, listAllSettings, saveSettings, saveSSHKey } from './handlers';
|
||||
import { CheckDNS, CheckDomain, DeleteDomain, DeleteSSHKey, SaveSettings, SaveSSHKey } from './types';
|
||||
|
||||
|
||||
const root: FastifyPluginAsync = async (fastify): Promise<void> => {
|
||||
@@ -13,6 +13,9 @@ const root: FastifyPluginAsync = async (fastify): Promise<void> => {
|
||||
|
||||
fastify.get<CheckDNS>('/check', async (request) => await checkDNS(request));
|
||||
fastify.post<CheckDomain>('/check', async (request) => await checkDomain(request));
|
||||
|
||||
fastify.post<SaveSSHKey>('/sshKey', async (request, reply) => await saveSSHKey(request, reply));
|
||||
fastify.delete<DeleteSSHKey>('/sshKey', async (request, reply) => await deleteSSHKey(request, reply));
|
||||
};
|
||||
|
||||
export default root;
|
||||
|
||||
@@ -28,4 +28,15 @@ export interface CheckDNS {
|
||||
Params: {
|
||||
domain: string,
|
||||
}
|
||||
}
|
||||
export interface SaveSSHKey {
|
||||
Body: {
|
||||
privateKey: string,
|
||||
name: string
|
||||
}
|
||||
}
|
||||
export interface DeleteSSHKey {
|
||||
Body: {
|
||||
id: string
|
||||
}
|
||||
}
|
||||
@@ -75,18 +75,18 @@ export async function gitHubEvents(request: FastifyRequest<GitHubEvents>): Promi
|
||||
if (!allowedGithubEvents.includes(githubEvent)) {
|
||||
throw { status: 500, message: 'Event not allowed.' }
|
||||
}
|
||||
let repository, projectId, branch;
|
||||
let projectId, branch;
|
||||
const body = request.body
|
||||
if (githubEvent === 'push') {
|
||||
repository = body.repository;
|
||||
projectId = repository.id;
|
||||
branch = body.ref.split('/')[2];
|
||||
projectId = body.repository.id;
|
||||
branch = body.ref.includes('/') ? body.ref.split('/')[2] : body.ref;
|
||||
} else if (githubEvent === 'pull_request') {
|
||||
repository = body.pull_request.head.repo;
|
||||
projectId = repository.id;
|
||||
branch = body.pull_request.head.ref.split('/')[2];
|
||||
projectId = body.pull_request.base.repo.id;
|
||||
branch = body.pull_request.base.ref.includes('/') ? body.pull_request.base.ref.split('/')[2] : body.pull_request.base.ref;
|
||||
}
|
||||
if (!projectId || !branch) {
|
||||
throw { status: 500, message: 'Cannot parse projectId or branch from the webhook?!' }
|
||||
}
|
||||
|
||||
const applicationFound = await getApplicationFromDBWebhook(projectId, branch);
|
||||
if (applicationFound) {
|
||||
const webhookSecret = applicationFound.gitSource.githubApp.webhookSecret || null;
|
||||
@@ -154,7 +154,7 @@ export async function gitHubEvents(request: FastifyRequest<GitHubEvents>): Promi
|
||||
} else if (githubEvent === 'pull_request') {
|
||||
const pullmergeRequestId = body.number;
|
||||
const pullmergeRequestAction = body.action;
|
||||
const sourceBranch = body.pull_request.head.ref;
|
||||
const sourceBranch = body.pull_request.head.ref.includes('/') ? body.pull_request.head.ref.split('/')[2] : body.pull_request.head.ref;
|
||||
if (!allowedActions.includes(pullmergeRequestAction)) {
|
||||
throw { status: 500, message: 'Action not allowed.' }
|
||||
}
|
||||
@@ -162,8 +162,10 @@ export async function gitHubEvents(request: FastifyRequest<GitHubEvents>): Promi
|
||||
if (applicationFound.settings.previews) {
|
||||
if (applicationFound.destinationDockerId) {
|
||||
const isRunning = await checkContainer(
|
||||
applicationFound.destinationDocker.engine,
|
||||
applicationFound.id
|
||||
{
|
||||
dockerId: applicationFound.destinationDocker.id,
|
||||
container: applicationFound.id
|
||||
}
|
||||
);
|
||||
if (!isRunning) {
|
||||
throw { status: 500, message: 'Application not running.' }
|
||||
@@ -204,8 +206,7 @@ export async function gitHubEvents(request: FastifyRequest<GitHubEvents>): Promi
|
||||
} else if (pullmergeRequestAction === 'closed') {
|
||||
if (applicationFound.destinationDockerId) {
|
||||
const id = `${applicationFound.id}-${pullmergeRequestId}`;
|
||||
const engine = applicationFound.destinationDocker.engine;
|
||||
await removeContainer({ id, engine });
|
||||
await removeContainer({ id, dockerId: applicationFound.destinationDocker.id });
|
||||
}
|
||||
return {
|
||||
message: 'Removed preview. Thank you!'
|
||||
|
||||
@@ -8,12 +8,22 @@ export interface GitHubEvents {
|
||||
Body: {
|
||||
number: string,
|
||||
action: string,
|
||||
repository: string,
|
||||
repository: {
|
||||
id: string,
|
||||
},
|
||||
ref: string,
|
||||
pull_request: {
|
||||
base: {
|
||||
ref: string,
|
||||
repo: {
|
||||
id: string,
|
||||
}
|
||||
},
|
||||
head: {
|
||||
ref: string,
|
||||
repo: string
|
||||
repo: {
|
||||
id: string,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,6 @@ export async function configureGitLabApp(request: FastifyRequest<ConfigureGitLab
|
||||
}
|
||||
return reply.redirect(`/webhooks/success?token=${data.access_token}`)
|
||||
} catch ({ status, message, ...other }) {
|
||||
console.log(other)
|
||||
return errorHandler({ status, message })
|
||||
}
|
||||
}
|
||||
@@ -117,8 +116,10 @@ export async function gitLabEvents(request: FastifyRequest<GitLabEvents>) {
|
||||
if (applicationFound.settings.previews) {
|
||||
if (applicationFound.destinationDockerId) {
|
||||
const isRunning = await checkContainer(
|
||||
applicationFound.destinationDocker.engine,
|
||||
applicationFound.id
|
||||
{
|
||||
dockerId: applicationFound.destinationDocker.id,
|
||||
container: applicationFound.id
|
||||
}
|
||||
);
|
||||
if (!isRunning) {
|
||||
throw { status: 500, message: 'Application not running.' }
|
||||
@@ -164,7 +165,7 @@ export async function gitLabEvents(request: FastifyRequest<GitLabEvents>) {
|
||||
if (applicationFound.destinationDockerId) {
|
||||
const id = `${applicationFound.id}-${pullmergeRequestId}`;
|
||||
const engine = applicationFound.destinationDocker.engine;
|
||||
await removeContainer({ id, engine });
|
||||
await removeContainer({ id, dockerId: applicationFound.destinationDocker.id });
|
||||
}
|
||||
return {
|
||||
message: 'Removed preview. Thank you!'
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { FastifyRequest } from "fastify";
|
||||
import { asyncExecShell, errorHandler, getDomain, isDev, listServicesWithIncludes, prisma, supportedServiceTypesAndVersions } from "../../../lib/common";
|
||||
import { getEngine } from "../../../lib/docker";
|
||||
import { errorHandler, getDomain, isDev, prisma, supportedServiceTypesAndVersions, include, executeDockerCmd } from "../../../lib/common";
|
||||
import { TraefikOtherConfiguration } from "./types";
|
||||
|
||||
function configureMiddleware(
|
||||
@@ -167,6 +166,7 @@ export async function traefikConfiguration(request, reply) {
|
||||
}
|
||||
};
|
||||
const applications = await prisma.application.findMany({
|
||||
where: { destinationDocker: { remoteEngine: false } },
|
||||
include: { destinationDocker: true, settings: true }
|
||||
});
|
||||
const data = {
|
||||
@@ -184,7 +184,7 @@ export async function traefikConfiguration(request, reply) {
|
||||
settings: { previews, dualCerts }
|
||||
} = application;
|
||||
if (destinationDockerId) {
|
||||
const { engine, network } = destinationDocker;
|
||||
const { network, id: dockerId } = destinationDocker;
|
||||
const isRunning = true;
|
||||
if (fqdn) {
|
||||
const domain = getDomain(fqdn);
|
||||
@@ -205,10 +205,7 @@ export async function traefikConfiguration(request, reply) {
|
||||
});
|
||||
}
|
||||
if (previews) {
|
||||
const host = getEngine(engine);
|
||||
const { stdout } = await asyncExecShell(
|
||||
`DOCKER_HOST=${host} docker container ls --filter="status=running" --filter="network=${network}" --filter="name=${id}-" --format="{{json .Names}}"`
|
||||
);
|
||||
const { stdout } = await executeDockerCmd({ dockerId, command: `docker container ls --filter="status=running" --filter="network=${network}" --filter="name=${id}-" --format="{{json .Names}}"` })
|
||||
const containers = stdout
|
||||
.trim()
|
||||
.split('\n')
|
||||
@@ -235,7 +232,11 @@ export async function traefikConfiguration(request, reply) {
|
||||
}
|
||||
}
|
||||
}
|
||||
const services = await listServicesWithIncludes();
|
||||
const services: any = await prisma.service.findMany({
|
||||
where: { destinationDocker: { remoteEngine: false } },
|
||||
include,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
|
||||
for (const service of services) {
|
||||
const {
|
||||
@@ -248,7 +249,6 @@ export async function traefikConfiguration(request, reply) {
|
||||
plausibleAnalytics
|
||||
} = service;
|
||||
if (destinationDockerId) {
|
||||
const { engine } = destinationDocker;
|
||||
const found = supportedServiceTypesAndVersions.find((a) => a.name === type);
|
||||
if (found) {
|
||||
const port = found.ports.main;
|
||||
@@ -487,4 +487,219 @@ export async function traefikOtherConfiguration(request: FastifyRequest<TraefikO
|
||||
console.log(status, message);
|
||||
return errorHandler({ status, message })
|
||||
}
|
||||
}
|
||||
|
||||
export async function remoteTraefikConfiguration(request: FastifyRequest) {
|
||||
const { id } = request.params
|
||||
try {
|
||||
const traefik = {
|
||||
http: {
|
||||
routers: {},
|
||||
services: {},
|
||||
middlewares: {
|
||||
'redirect-to-https': {
|
||||
redirectscheme: {
|
||||
scheme: 'https'
|
||||
}
|
||||
},
|
||||
'redirect-to-http': {
|
||||
redirectscheme: {
|
||||
scheme: 'http'
|
||||
}
|
||||
},
|
||||
'redirect-to-non-www': {
|
||||
redirectregex: {
|
||||
regex: '^https?://www\\.(.+)',
|
||||
replacement: 'http://${1}'
|
||||
}
|
||||
},
|
||||
'redirect-to-www': {
|
||||
redirectregex: {
|
||||
regex: '^https?://(?:www\\.)?(.+)',
|
||||
replacement: 'http://www.${1}'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
const applications = await prisma.application.findMany({
|
||||
where: { destinationDocker: { id } },
|
||||
include: { destinationDocker: true, settings: true }
|
||||
});
|
||||
const data = {
|
||||
applications: [],
|
||||
services: [],
|
||||
coolify: []
|
||||
};
|
||||
for (const application of applications) {
|
||||
const {
|
||||
fqdn,
|
||||
id,
|
||||
port,
|
||||
destinationDocker,
|
||||
destinationDockerId,
|
||||
settings: { previews, dualCerts }
|
||||
} = application;
|
||||
if (destinationDockerId) {
|
||||
const { id: dockerId, network } = destinationDocker;
|
||||
const isRunning = true;
|
||||
if (fqdn) {
|
||||
const domain = getDomain(fqdn);
|
||||
const nakedDomain = domain.replace(/^www\./, '');
|
||||
const isHttps = fqdn.startsWith('https://');
|
||||
const isWWW = fqdn.includes('www.');
|
||||
if (isRunning) {
|
||||
data.applications.push({
|
||||
id,
|
||||
container: id,
|
||||
port: port || 3000,
|
||||
domain,
|
||||
nakedDomain,
|
||||
isRunning,
|
||||
isHttps,
|
||||
isWWW,
|
||||
isDualCerts: dualCerts
|
||||
});
|
||||
}
|
||||
if (previews) {
|
||||
const { stdout } = await executeDockerCmd({ dockerId, command: `docker container ls --filter="status=running" --filter="network=${network}" --filter="name=${id}-" --format="{{json .Names}}"` })
|
||||
const containers = stdout
|
||||
.trim()
|
||||
.split('\n')
|
||||
.filter((a) => a)
|
||||
.map((c) => c.replace(/"/g, ''));
|
||||
if (containers.length > 0) {
|
||||
for (const container of containers) {
|
||||
const previewDomain = `${container.split('-')[1]}.${domain}`;
|
||||
const nakedDomain = previewDomain.replace(/^www\./, '');
|
||||
data.applications.push({
|
||||
id: container,
|
||||
container,
|
||||
port: port || 3000,
|
||||
domain: previewDomain,
|
||||
isRunning,
|
||||
nakedDomain,
|
||||
isHttps,
|
||||
isWWW,
|
||||
isDualCerts: dualCerts
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const services: any = await prisma.service.findMany({
|
||||
where: { destinationDocker: { id } },
|
||||
include,
|
||||
orderBy: { createdAt: 'desc' }
|
||||
});
|
||||
|
||||
for (const service of services) {
|
||||
const {
|
||||
fqdn,
|
||||
id,
|
||||
type,
|
||||
destinationDocker,
|
||||
destinationDockerId,
|
||||
dualCerts,
|
||||
plausibleAnalytics
|
||||
} = service;
|
||||
if (destinationDockerId) {
|
||||
const found = supportedServiceTypesAndVersions.find((a) => a.name === type);
|
||||
if (found) {
|
||||
const port = found.ports.main;
|
||||
const publicPort = service[type]?.publicPort;
|
||||
const isRunning = true;
|
||||
if (fqdn) {
|
||||
const domain = getDomain(fqdn);
|
||||
const nakedDomain = domain.replace(/^www\./, '');
|
||||
const isHttps = fqdn.startsWith('https://');
|
||||
const isWWW = fqdn.includes('www.');
|
||||
if (isRunning) {
|
||||
// Plausible Analytics custom script
|
||||
let scriptName = false;
|
||||
if (type === 'plausibleanalytics' && plausibleAnalytics.scriptName !== 'plausible.js') {
|
||||
scriptName = plausibleAnalytics.scriptName;
|
||||
}
|
||||
|
||||
let container = id;
|
||||
let otherDomain = null;
|
||||
let otherNakedDomain = null;
|
||||
let otherIsHttps = null;
|
||||
let otherIsWWW = null;
|
||||
|
||||
if (type === 'minio' && service.minio.apiFqdn) {
|
||||
otherDomain = getDomain(service.minio.apiFqdn);
|
||||
otherNakedDomain = otherDomain.replace(/^www\./, '');
|
||||
otherIsHttps = service.minio.apiFqdn.startsWith('https://');
|
||||
otherIsWWW = service.minio.apiFqdn.includes('www.');
|
||||
}
|
||||
data.services.push({
|
||||
id,
|
||||
container,
|
||||
type,
|
||||
otherDomain,
|
||||
otherNakedDomain,
|
||||
otherIsHttps,
|
||||
otherIsWWW,
|
||||
port,
|
||||
publicPort,
|
||||
domain,
|
||||
nakedDomain,
|
||||
isRunning,
|
||||
isHttps,
|
||||
isWWW,
|
||||
isDualCerts: dualCerts,
|
||||
scriptName
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
for (const application of data.applications) {
|
||||
configureMiddleware(application, traefik);
|
||||
}
|
||||
for (const service of data.services) {
|
||||
const { id, scriptName } = service;
|
||||
|
||||
configureMiddleware(service, traefik);
|
||||
if (service.type === 'minio') {
|
||||
service.id = id + '-minio';
|
||||
service.container = id;
|
||||
service.domain = service.otherDomain;
|
||||
service.nakedDomain = service.otherNakedDomain;
|
||||
service.isHttps = service.otherIsHttps;
|
||||
service.isWWW = service.otherIsWWW;
|
||||
service.port = 9000;
|
||||
configureMiddleware(service, traefik);
|
||||
}
|
||||
|
||||
if (scriptName) {
|
||||
traefik.http.middlewares[`${id}-redir`] = {
|
||||
replacepathregex: {
|
||||
regex: `/js/${scriptName}`,
|
||||
replacement: '/js/plausible.js'
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
for (const coolify of data.coolify) {
|
||||
configureMiddleware(coolify, traefik);
|
||||
}
|
||||
if (Object.keys(traefik.http.routers).length === 0) {
|
||||
traefik.http.routers = null;
|
||||
}
|
||||
if (Object.keys(traefik.http.services).length === 0) {
|
||||
traefik.http.services = null;
|
||||
}
|
||||
return {
|
||||
...traefik
|
||||
}
|
||||
} catch ({ status, message }) {
|
||||
return errorHandler({ status, message })
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
import { FastifyPluginAsync } from 'fastify';
|
||||
import { traefikConfiguration, traefikOtherConfiguration } from './handlers';
|
||||
import { remoteTraefikConfiguration, traefikConfiguration, traefikOtherConfiguration } from './handlers';
|
||||
import { TraefikOtherConfiguration } from './types';
|
||||
|
||||
const root: FastifyPluginAsync = async (fastify): Promise<void> => {
|
||||
fastify.get('/main.json', async (request, reply) => traefikConfiguration(request, reply));
|
||||
fastify.get<TraefikOtherConfiguration>('/other.json', async (request, reply) => traefikOtherConfiguration(request));
|
||||
|
||||
fastify.get('/remote/:id', async (request) => remoteTraefikConfiguration(request));
|
||||
};
|
||||
|
||||
export default root;
|
||||
|
||||
@@ -36,3 +36,4 @@ export interface SaveDatabaseSettings extends OnlyId {
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -15,13 +15,13 @@
|
||||
"format": "prettier --write --plugin-search-dir=. ."
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "1.23.3",
|
||||
"@sveltejs/kit": "1.0.0-next.375",
|
||||
"@playwright/test": "1.23.4",
|
||||
"@sveltejs/kit": "1.0.0-next.377",
|
||||
"@types/js-cookie": "3.0.2",
|
||||
"@typescript-eslint/eslint-plugin": "5.30.6",
|
||||
"@typescript-eslint/parser": "5.30.6",
|
||||
"autoprefixer": "10.4.7",
|
||||
"eslint": "8.19.0",
|
||||
"eslint": "8.20.0",
|
||||
"eslint-config-prettier": "8.5.0",
|
||||
"eslint-plugin-svelte3": "4.0.0",
|
||||
"postcss": "8.4.14",
|
||||
@@ -34,11 +34,11 @@
|
||||
"tailwindcss-scrollbar": "0.1.0",
|
||||
"tslib": "2.4.0",
|
||||
"typescript": "4.7.4",
|
||||
"vite": "^3.0.0"
|
||||
"vite": "3.0.1"
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@sveltejs/adapter-static": "1.0.0-next.36",
|
||||
"@sveltejs/adapter-static": "1.0.0-next.37",
|
||||
"@zerodevx/svelte-toast": "0.7.2",
|
||||
"cuid": "2.1.8",
|
||||
"js-cookie": "3.0.1",
|
||||
|
||||
@@ -250,7 +250,7 @@
|
||||
"no_destination_found": "No destination found",
|
||||
"new_error_network_already_exists": "Network {{network}} already configured for another team!",
|
||||
"new": {
|
||||
"saving_and_configuring_proxy": "Saving and configuring proxy...",
|
||||
"saving_and_configuring_proxy": "Saving...",
|
||||
"install_proxy": "This will install a proxy on the destination to allow you to access your applications and services without any manual configuration (recommended for Docker).<br><br>Databases will have their own proxy.",
|
||||
"add_new_destination": "Add New Destination",
|
||||
"predefined_destinations": "Predefined destinations"
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { writable, readable, type Writable, type Readable } from 'svelte/store';
|
||||
|
||||
interface AppSession {
|
||||
ipv4: string | null,
|
||||
ipv6: string | null,
|
||||
version: string | null,
|
||||
userId: string | null,
|
||||
teamId: string | null,
|
||||
@@ -17,6 +19,8 @@ interface AppSession {
|
||||
}
|
||||
export const loginEmail: Writable<string | undefined> = writable()
|
||||
export const appSession: Writable<AppSession> = writable({
|
||||
ipv4: null,
|
||||
ipv6: null,
|
||||
version: null,
|
||||
userId: null,
|
||||
teamId: null,
|
||||
@@ -31,7 +35,6 @@ export const appSession: Writable<AppSession> = writable({
|
||||
gitlab: null
|
||||
}
|
||||
});
|
||||
export const isTraefikUsed: Writable<boolean> = writable(false);
|
||||
export const disabledButton: Writable<boolean> = writable(false);
|
||||
export const status: Writable<any> = writable({
|
||||
application: {
|
||||
@@ -41,14 +44,16 @@ export const status: Writable<any> = writable({
|
||||
initialLoading: true
|
||||
},
|
||||
service: {
|
||||
initialLoading: true,
|
||||
isRunning: false,
|
||||
isExited: false,
|
||||
loading: false,
|
||||
isRunning: false
|
||||
initialLoading: true
|
||||
},
|
||||
database: {
|
||||
initialLoading: true,
|
||||
isRunning: false,
|
||||
isExited: false,
|
||||
loading: false,
|
||||
isRunning: false
|
||||
initialLoading: true
|
||||
}
|
||||
|
||||
});
|
||||
@@ -60,7 +65,6 @@ export const features = readable({
|
||||
|
||||
export const location: Writable<null | string> = writable(null)
|
||||
export const setLocation = (resource: any) => {
|
||||
console.log(GITPOD_WORKSPACE_URL)
|
||||
if (GITPOD_WORKSPACE_URL && resource.exposePort) {
|
||||
const { href } = new URL(GITPOD_WORKSPACE_URL);
|
||||
const newURL = href
|
||||
|
||||
@@ -65,11 +65,12 @@
|
||||
|
||||
<script lang="ts">
|
||||
export let baseSettings: any;
|
||||
$appSession.ipv4 = baseSettings.ipv4;
|
||||
$appSession.ipv6 = baseSettings.ipv6;
|
||||
$appSession.version = baseSettings.version;
|
||||
$appSession.whiteLabeled = baseSettings.whiteLabeled;
|
||||
$appSession.whiteLabeledDetails.icon = baseSettings.whiteLabeledIcon;
|
||||
|
||||
export let settings: any;
|
||||
export let userId: string;
|
||||
export let teamId: string;
|
||||
export let permission: string;
|
||||
@@ -84,15 +85,13 @@
|
||||
import UpdateAvailable from '$lib/components/UpdateAvailable.svelte';
|
||||
import PageLoader from '$lib/components/PageLoader.svelte';
|
||||
import { errorNotification } from '$lib/common';
|
||||
import { appSession, isTraefikUsed } from '$lib/store';
|
||||
import { appSession } from '$lib/store';
|
||||
|
||||
if (userId) $appSession.userId = userId;
|
||||
if (teamId) $appSession.teamId = teamId;
|
||||
if (permission) $appSession.permission = permission;
|
||||
if (isAdmin) $appSession.isAdmin = isAdmin;
|
||||
|
||||
$isTraefikUsed = settings?.isTraefikUsed || false;
|
||||
|
||||
async function logout() {
|
||||
try {
|
||||
Cookies.remove('token');
|
||||
@@ -322,33 +321,31 @@
|
||||
<path d="M21 21v-2a4 4 0 0 0 -3 -3.85" />
|
||||
</svg>
|
||||
</a>
|
||||
{#if $appSession.teamId === '0'}
|
||||
<a
|
||||
sveltekit:prefetch
|
||||
href="/settings"
|
||||
class="icons tooltip-yellow-500 tooltip-right bg-coolgray-200 hover:text-yellow-500"
|
||||
class:text-yellow-500={$page.url.pathname.startsWith('/settings')}
|
||||
class:bg-coolgray-500={$page.url.pathname.startsWith('/settings')}
|
||||
data-tooltip="Settings"
|
||||
<a
|
||||
sveltekit:prefetch
|
||||
href={$appSession.teamId === '0' ? '/settings/global' : '/settings/ssh-keys'}
|
||||
class="icons tooltip-yellow-500 tooltip-right bg-coolgray-200 hover:text-yellow-500"
|
||||
class:text-yellow-500={$page.url.pathname.startsWith('/settings')}
|
||||
class:bg-coolgray-500={$page.url.pathname.startsWith('/settings')}
|
||||
data-tooltip="Settings"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-8 w-8"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-8 w-8"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path
|
||||
d="M10.325 4.317c.426 -1.756 2.924 -1.756 3.35 0a1.724 1.724 0 0 0 2.573 1.066c1.543 -.94 3.31 .826 2.37 2.37a1.724 1.724 0 0 0 1.065 2.572c1.756 .426 1.756 2.924 0 3.35a1.724 1.724 0 0 0 -1.066 2.573c.94 1.543 -.826 3.31 -2.37 2.37a1.724 1.724 0 0 0 -2.572 1.065c-.426 1.756 -2.924 1.756 -3.35 0a1.724 1.724 0 0 0 -2.573 -1.066c-1.543 .94 -3.31 -.826 -2.37 -2.37a1.724 1.724 0 0 0 -1.065 -2.572c-1.756 -.426 -1.756 -2.924 0 -3.35a1.724 1.724 0 0 0 1.066 -2.573c-.94 -1.543 .826 -3.31 2.37 -2.37c1 .608 2.296 .07 2.572 -1.065z"
|
||||
/>
|
||||
<circle cx="12" cy="12" r="3" />
|
||||
</svg>
|
||||
</a>
|
||||
{/if}
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path
|
||||
d="M10.325 4.317c.426 -1.756 2.924 -1.756 3.35 0a1.724 1.724 0 0 0 2.573 1.066c1.543 -.94 3.31 .826 2.37 2.37a1.724 1.724 0 0 0 1.065 2.572c1.756 .426 1.756 2.924 0 3.35a1.724 1.724 0 0 0 -1.066 2.573c.94 1.543 -.826 3.31 -2.37 2.37a1.724 1.724 0 0 0 -2.572 1.065c-.426 1.756 -2.924 1.756 -3.35 0a1.724 1.724 0 0 0 -2.573 -1.066c-1.543 .94 -3.31 -.826 -2.37 -2.37a1.724 1.724 0 0 0 -1.065 -2.572c-1.756 -.426 -1.756 -2.924 0 -3.35a1.724 1.724 0 0 0 1.066 -2.573c-.94 -1.543 .826 -3.31 2.37 -2.37c1 .608 2.296 .07 2.572 -1.065z"
|
||||
/>
|
||||
<circle cx="12" cy="12" r="3" />
|
||||
</svg>
|
||||
</a>
|
||||
|
||||
<div
|
||||
class="icons tooltip-red-500 tooltip-right bg-coolgray-200 hover:text-red-500"
|
||||
|
||||
@@ -36,7 +36,6 @@
|
||||
|
||||
return {
|
||||
props: {
|
||||
isQueueActive,
|
||||
application
|
||||
},
|
||||
stuff: {
|
||||
@@ -53,7 +52,6 @@
|
||||
|
||||
<script lang="ts">
|
||||
export let application: any;
|
||||
export let isQueueActive: any;
|
||||
|
||||
import { page } from '$app/stores';
|
||||
import DeleteIcon from '$lib/components/DeleteIcon.svelte';
|
||||
@@ -68,7 +66,7 @@
|
||||
|
||||
let loading = false;
|
||||
let statusInterval: any;
|
||||
|
||||
let isQueueActive= false;
|
||||
$disabledButton =
|
||||
!$appSession.isAdmin ||
|
||||
!application.fqdn ||
|
||||
@@ -114,12 +112,12 @@
|
||||
return window.location.reload();
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
async function getStatus() {
|
||||
if ($status.application.loading) return;
|
||||
$status.application.loading = true;
|
||||
const data = await get(`/applications/${id}`);
|
||||
const data = await get(`/applications/${id}/status`);
|
||||
isQueueActive = data.isQueueActive;
|
||||
$status.application.isRunning = data.isRunning;
|
||||
$status.application.isExited = data.isExited;
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
redirect: `/applications/${params.id}`
|
||||
};
|
||||
}
|
||||
const response = await get(`/destinations`);
|
||||
const response = await get(`/destinations?onlyVerified=true`);
|
||||
return {
|
||||
props: {
|
||||
...response
|
||||
@@ -65,7 +65,7 @@
|
||||
<div class="flex flex-col justify-center">
|
||||
{#if !destinations || ownDestinations.length === 0}
|
||||
<div class="flex-col">
|
||||
<div class="pb-2">{$t('application.configuration.no_configurable_destination')}</div>
|
||||
<div class="pb-2 text-center font-bold">{$t('application.configuration.no_configurable_destination')}</div>
|
||||
<div class="flex justify-center">
|
||||
<a href="/new/destination" sveltekit:prefetch class="add-icon bg-sky-600 hover:bg-sky-500">
|
||||
<svg
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
import Setting from './_Setting.svelte';
|
||||
const { id } = $page.params;
|
||||
|
||||
$: isDisabled = !$appSession.isAdmin || $status.application.isRunning;
|
||||
$: isDisabled = !$appSession.isAdmin || $status.application.isRunning || $status.application.initialLoading;
|
||||
|
||||
let domainEl: HTMLInputElement;
|
||||
|
||||
@@ -308,7 +308,7 @@
|
||||
<div class="mx-auto max-w-4xl px-6">
|
||||
<!-- svelte-ignore missing-declaration -->
|
||||
<form on:submit|preventDefault={handleSubmit} class="py-4">
|
||||
<div class="flex space-x-1 pb-5 font-bold">
|
||||
<div class="flex space-x-1 pb-5">
|
||||
<div class="title">{$t('general')}</div>
|
||||
{#if $appSession.isAdmin}
|
||||
<button
|
||||
@@ -536,7 +536,7 @@
|
||||
<div class="grid grid-cols-2 items-center pb-8">
|
||||
<Setting
|
||||
dataTooltip={$t('forms.must_be_stopped_to_modify')}
|
||||
disabled={isDisabled}
|
||||
disabled={$status.application.isRunning}
|
||||
isCenter={false}
|
||||
bind:setting={dualCerts}
|
||||
title={$t('application.ssl_www_and_non_www')}
|
||||
|
||||
@@ -106,7 +106,7 @@
|
||||
{#if currentStatus === 'queued'}
|
||||
<div class="text-center font-bold text-xl">{$t('application.build.queued_waiting_exec')}</div>
|
||||
{:else}
|
||||
<div class="flex justify-end sticky top-0 p-2">
|
||||
<div class="flex justify-end sticky top-0 p-1 mx-1">
|
||||
<button
|
||||
on:click={followBuild}
|
||||
class="bg-transparent hover:text-green-500 hover:bg-coolgray-500"
|
||||
|
||||
@@ -1,32 +1,13 @@
|
||||
<script context="module" lang="ts">
|
||||
import type { Load } from '@sveltejs/kit';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
export const load: Load = async ({ fetch, params, url, stuff }) => {
|
||||
try {
|
||||
const response = await get(`/applications/${params.id}/logs`);
|
||||
return {
|
||||
props: {
|
||||
application: stuff.application,
|
||||
...response
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
status: 500,
|
||||
error: new Error(`Could not load ${url}`)
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export let application: any;
|
||||
import { page } from '$app/stores';
|
||||
import { get } from '$lib/api';
|
||||
import { t } from '$lib/translations';
|
||||
import { errorNotification } from '$lib/common';
|
||||
import LoadingLogs from '$lib/components/LoadingLogs.svelte';
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
|
||||
let application: any = {};
|
||||
let logsLoading = false;
|
||||
let loadLogsInterval: any = null;
|
||||
let logs: any = [];
|
||||
let lastLog: any = null;
|
||||
@@ -37,6 +18,8 @@
|
||||
|
||||
const { id } = $page.params;
|
||||
onMount(async () => {
|
||||
const response = await get(`/applications/${id}`);
|
||||
application = response.application;
|
||||
loadAllLogs();
|
||||
loadLogsInterval = setInterval(() => {
|
||||
loadLogs();
|
||||
@@ -48,6 +31,7 @@
|
||||
});
|
||||
async function loadAllLogs() {
|
||||
try {
|
||||
logsLoading = true;
|
||||
const data: any = await get(`/applications/${id}/logs`);
|
||||
if (data?.logs) {
|
||||
lastLog = data.logs[data.logs.length - 1];
|
||||
@@ -56,9 +40,12 @@
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return errorNotification(error);
|
||||
} finally {
|
||||
logsLoading = false;
|
||||
}
|
||||
}
|
||||
async function loadLogs() {
|
||||
if (logsLoading) return;
|
||||
try {
|
||||
const newLogs: any = await get(
|
||||
`/applications/${id}/logs?since=${lastLog?.split(' ')[0] || 0}`
|
||||
@@ -157,7 +144,7 @@
|
||||
{#if loadLogsInterval}
|
||||
<LoadingLogs />
|
||||
{/if}
|
||||
<div class="flex justify-end sticky top-0 p-2 mx-1">
|
||||
<div class="flex justify-end sticky top-0 p-1 mx-1">
|
||||
<button
|
||||
on:click={followBuild}
|
||||
class="bg-transparent"
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
<script context="module" lang="ts">
|
||||
import type { Load } from '@sveltejs/kit';
|
||||
export const load: Load = async ({ fetch, params, stuff, url }) => {
|
||||
export const load: Load = async ({ stuff, url }) => {
|
||||
try {
|
||||
const response = await get(`/applications/${params.id}/previews`);
|
||||
return {
|
||||
props: {
|
||||
application: stuff.application,
|
||||
...response
|
||||
application: stuff.application
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
@@ -19,10 +17,7 @@
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export let containers: any;
|
||||
export let application: any;
|
||||
export let PRMRSecrets: any;
|
||||
export let applicationSecrets: any;
|
||||
import Secret from './_Secret.svelte';
|
||||
import { get, post } from '$lib/api';
|
||||
import { page } from '$app/stores';
|
||||
@@ -31,12 +26,33 @@
|
||||
import { t } from '$lib/translations';
|
||||
import { goto } from '$app/navigation';
|
||||
import { errorNotification, getDomain } from '$lib/common';
|
||||
import { onMount } from 'svelte';
|
||||
import Loading from '$lib/components/Loading.svelte';
|
||||
|
||||
const { id } = $page.params;
|
||||
|
||||
let containers: any;
|
||||
let PRMRSecrets: any;
|
||||
let applicationSecrets: any;
|
||||
let loading = {
|
||||
init: true,
|
||||
removing: false
|
||||
};
|
||||
async function refreshSecrets() {
|
||||
const data = await get(`/applications/${id}/secrets`);
|
||||
PRMRSecrets = [...data.secrets];
|
||||
}
|
||||
async function removeApplication(container: any) {
|
||||
try {
|
||||
loading.removing = true;
|
||||
await post(`/applications/${id}/stop/preview`, {
|
||||
pullmergeRequestId: container.pullmergeRequestId
|
||||
});
|
||||
return window.location.reload();
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
async function redeploy(container: any) {
|
||||
try {
|
||||
const { buildId } = await post(`/applications/${id}/deploy`, {
|
||||
@@ -55,6 +71,19 @@
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
onMount(async () => {
|
||||
try {
|
||||
loading.init = true;
|
||||
const response = await get(`/applications/${id}/previews`);
|
||||
containers = response.containers;
|
||||
PRMRSecrets = response.PRMRSecrets;
|
||||
applicationSecrets = response.applicationSecrets;
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
} finally {
|
||||
loading.init = false;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="flex items-center space-x-2 p-5 px-6 font-bold">
|
||||
@@ -62,7 +91,7 @@
|
||||
<div class="md:max-w-64 truncate text-base tracking-tight md:text-2xl lg:block">
|
||||
Preview Deployments
|
||||
</div>
|
||||
<span class="text-xs">{application.name} </span>
|
||||
<span class="text-xs">{application?.name}</span>
|
||||
</div>
|
||||
{#if application.gitSource?.htmlUrl && application.repository && application.branch}
|
||||
<a
|
||||
@@ -108,68 +137,81 @@
|
||||
</a>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="mx-auto max-w-6xl px-6 pt-4">
|
||||
<div class="flex justify-center py-4 text-center">
|
||||
<Explainer
|
||||
customClass="w-full"
|
||||
text={applicationSecrets.length === 0
|
||||
? "You can add secrets to PR/MR deployments. Please add secrets to the application first. <br>Useful for creating <span class='text-green-500 font-bold'>staging</span> environments."
|
||||
: "These values overwrite application secrets in PR/MR deployments. <br>Useful for creating <span class='text-green-500 font-bold'>staging</span> environments."}
|
||||
/>
|
||||
</div>
|
||||
{#if applicationSecrets.length !== 0}
|
||||
<table class="mx-auto border-separate text-left">
|
||||
<thead>
|
||||
<tr class="h-12">
|
||||
<th scope="col">{$t('forms.name')}</th>
|
||||
<th scope="col">{$t('forms.value')}</th>
|
||||
<th scope="col" class="w-64 text-center"
|
||||
>{$t('application.preview.need_during_buildtime')}</th
|
||||
>
|
||||
<th scope="col" class="w-96 text-center">{$t('forms.action')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each applicationSecrets as secret}
|
||||
{#key secret.id}
|
||||
<tr>
|
||||
<Secret
|
||||
PRMRSecret={PRMRSecrets.find((s) => s.name === secret.name)}
|
||||
isPRMRSecret
|
||||
name={secret.name}
|
||||
value={secret.value}
|
||||
isBuildSecret={secret.isBuildSecret}
|
||||
on:refresh={refreshSecrets}
|
||||
/>
|
||||
</tr>
|
||||
{/key}
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="mx-auto max-w-4xl py-10">
|
||||
<div class="flex flex-wrap justify-center space-x-2">
|
||||
{#if containers.length > 0}
|
||||
{#each containers as container}
|
||||
<a href={container.fqdn} class="p-2 no-underline" target="_blank">
|
||||
<div class="box-selection text-center hover:border-transparent hover:bg-coolgray-200">
|
||||
<div class="truncate text-center text-xl font-bold">{getDomain(container.fqdn)}</div>
|
||||
</div>
|
||||
</a>
|
||||
<div class="flex items-center justify-center">
|
||||
<button class="bg-coollabs hover:bg-coollabs-100" on:click={() => redeploy(container)}
|
||||
>{$t('application.preview.redeploy')}</button
|
||||
>
|
||||
</div>
|
||||
{/each}
|
||||
{:else}
|
||||
<div class="flex-col">
|
||||
<div class="text-center font-bold text-xl">
|
||||
{$t('application.preview.no_previews_available')}
|
||||
</div>
|
||||
</div>
|
||||
{#if loading.init}
|
||||
<Loading />
|
||||
{:else}
|
||||
<div class="mx-auto max-w-6xl px-6 pt-4">
|
||||
<div class="flex justify-center py-4 text-center">
|
||||
<Explainer
|
||||
customClass="w-full"
|
||||
text={applicationSecrets.length === 0
|
||||
? "You can add secrets to PR/MR deployments. Please add secrets to the application first. <br>Useful for creating <span class='text-green-500 font-bold'>staging</span> environments."
|
||||
: "These values overwrite application secrets in PR/MR deployments. <br>Useful for creating <span class='text-green-500 font-bold'>staging</span> environments."}
|
||||
/>
|
||||
</div>
|
||||
{#if applicationSecrets.length !== 0}
|
||||
<table class="mx-auto border-separate text-left">
|
||||
<thead>
|
||||
<tr class="h-12">
|
||||
<th scope="col">{$t('forms.name')}</th>
|
||||
<th scope="col">{$t('forms.value')}</th>
|
||||
<th scope="col" class="w-64 text-center"
|
||||
>{$t('application.preview.need_during_buildtime')}</th
|
||||
>
|
||||
<th scope="col" class="w-96 text-center">{$t('forms.action')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each applicationSecrets as secret}
|
||||
{#key secret.id}
|
||||
<tr>
|
||||
<Secret
|
||||
PRMRSecret={PRMRSecrets.find((s) => s.name === secret.name)}
|
||||
isPRMRSecret
|
||||
name={secret.name}
|
||||
value={secret.value}
|
||||
isBuildSecret={secret.isBuildSecret}
|
||||
on:refresh={refreshSecrets}
|
||||
/>
|
||||
</tr>
|
||||
{/key}
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mx-auto max-w-4xl py-10">
|
||||
<div class="flex flex-wrap justify-center space-x-2">
|
||||
{#if containers.length > 0}
|
||||
{#each containers as container}
|
||||
<a href={container.fqdn} class="p-2 no-underline" target="_blank">
|
||||
<div class="box-selection text-center hover:border-transparent hover:bg-green-600">
|
||||
<div class="truncate text-center text-xl font-bold">{getDomain(container.fqdn)}</div>
|
||||
</div>
|
||||
</a>
|
||||
<div class="flex items-center justify-center">
|
||||
<button class="bg-coollabs hover:bg-coollabs-100" on:click={() => redeploy(container)}
|
||||
>{$t('application.preview.redeploy')}</button
|
||||
>
|
||||
</div>
|
||||
<div class="flex items-center justify-center">
|
||||
<button
|
||||
class:bg-red-600={!loading.removing}
|
||||
class:hover:bg-red-500={!loading.removing}
|
||||
disabled={loading.removing}
|
||||
on:click={() => removeApplication(container)}
|
||||
>{loading.removing ? 'Removing...' : 'Remove Application'}
|
||||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
{:else}
|
||||
<div class="flex-col">
|
||||
<div class="text-center font-bold text-xl">
|
||||
{$t('application.preview.no_previews_available')}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -93,7 +93,7 @@
|
||||
<div class="flex flex-col">
|
||||
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row">
|
||||
{#each ownApplications as application}
|
||||
<a href="/applications/{application.id}" class="w-96 p-2 no-underline">
|
||||
<a href="/applications/{application.id}" class="p-2 no-underline">
|
||||
<div class="box-selection group relative hover:bg-green-600">
|
||||
{#if application.buildPack}
|
||||
{#if application.buildPack.toLowerCase() === 'rust'}
|
||||
@@ -140,15 +140,18 @@
|
||||
{#if application.fqdn}
|
||||
<div class="truncate text-center">{getDomain(application.fqdn) || ''}</div>
|
||||
{/if}
|
||||
{#if application.destinationDocker?.name}
|
||||
<div class="truncate text-center">{application.destinationDocker.name}</div>
|
||||
{/if}
|
||||
{#if !application.gitSourceId || !application.repository || !application.branch}
|
||||
<div class="truncate text-center font-bold text-red-500 group-hover:text-white">
|
||||
Git Source Missing
|
||||
</div>
|
||||
{:else if !application.destinationDockerId}
|
||||
{:else if !application.destinationDockerId}
|
||||
<div class="truncate text-center font-bold text-red-500 group-hover:text-white">
|
||||
Destination Missing
|
||||
</div>
|
||||
{:else if !application.fqdn}
|
||||
{:else if !application.fqdn}
|
||||
<div class="truncate text-center font-bold text-red-500 group-hover:text-white">
|
||||
URL Missing
|
||||
</div>
|
||||
@@ -158,10 +161,10 @@
|
||||
{/each}
|
||||
</div>
|
||||
{#if otherApplications.length > 0 && $appSession.teamId === '0'}
|
||||
<div class="px-6 pb-5 pt-10 text-xl font-bold">Other Applications</div>
|
||||
<div class="px-6 pb-5 pt-10 text-2xl font-bold text-center">Other Applications</div>
|
||||
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row">
|
||||
{#each otherApplications as application}
|
||||
<a href="/applications/{application.id}" class="w-96 p-2 no-underline">
|
||||
<a href="/applications/{application.id}" class="p-2 no-underline">
|
||||
<div class="box-selection group relative hover:bg-green-600">
|
||||
{#if application.buildPack}
|
||||
{#if application.buildPack.toLowerCase() === 'rust'}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<script lang="ts">
|
||||
export let database: any;
|
||||
export let privatePort: any;
|
||||
export let settings: any;
|
||||
|
||||
import { page } from '$app/stores';
|
||||
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
|
||||
@@ -17,8 +16,9 @@
|
||||
import { post } from '$lib/api';
|
||||
import { toast } from '@zerodevx/svelte-toast';
|
||||
import { t } from '$lib/translations';
|
||||
import { errorNotification, getDomain } from '$lib/common';
|
||||
import { errorNotification } from '$lib/common';
|
||||
import { appSession, status } from '$lib/store';
|
||||
import Explainer from '$lib/components/Explainer.svelte';
|
||||
|
||||
const { id } = $page.params;
|
||||
|
||||
@@ -49,12 +49,15 @@
|
||||
databaseDbUser = '';
|
||||
}
|
||||
}
|
||||
|
||||
function generateUrl(): string {
|
||||
return `${database.type}://${
|
||||
databaseDbUser ? databaseDbUser + ':' : ''
|
||||
}${databaseDbUserPassword}@${
|
||||
isPublic ? (settings.fqdn ? getDomain(settings.fqdn) : window.location.hostname) : database.id
|
||||
isPublic
|
||||
? database.destinationDocker.remoteEngine
|
||||
? database.destinationDocker.remoteIpAddress
|
||||
: $appSession.ipv4
|
||||
: database.id
|
||||
}:${isPublic ? database.publicPort : privatePort}/${databaseDefault}`;
|
||||
}
|
||||
|
||||
@@ -103,7 +106,7 @@
|
||||
|
||||
<div class="mx-auto max-w-4xl px-6">
|
||||
<form on:submit|preventDefault={handleSubmit} class="py-4">
|
||||
<div class="flex space-x-1 pb-5 font-bold">
|
||||
<div class="flex space-x-1 pb-5">
|
||||
<div class="title">{$t('general')}</div>
|
||||
{#if $appSession.isAdmin}
|
||||
<button
|
||||
@@ -153,7 +156,8 @@
|
||||
>
|
||||
<input
|
||||
value={database.version}
|
||||
disabled={$status.database.isRunning}
|
||||
readonly
|
||||
disabled={$status.database.isRunning || $status.database.initialLoading}
|
||||
class:cursor-pointer={!$status.database.isRunning}
|
||||
/></a
|
||||
>
|
||||
@@ -203,9 +207,16 @@
|
||||
<EdgeDB {database} />
|
||||
{/if}
|
||||
<div class="grid grid-cols-2 items-center px-10 pb-8">
|
||||
<label for="url" class="text-base font-bold text-stone-100"
|
||||
>{$t('database.connection_string')}</label
|
||||
>
|
||||
<div>
|
||||
<label for="url" class="text-base font-bold text-stone-100"
|
||||
>{$t('database.connection_string')}</label
|
||||
>
|
||||
{#if !isPublic && database.destinationDocker.remoteEngine}
|
||||
<Explainer
|
||||
text="You can only access the database with this URL if your application is deployed to the same Destination."
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
<CopyPasswordField
|
||||
textarea={true}
|
||||
placeholder={$t('forms.generated_automatically_after_start')}
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { errorNotification, handlerNotFoundLoad } from '$lib/common';
|
||||
import { appSession, status } from '$lib/store';
|
||||
import { appSession, status, disabledButton } from '$lib/store';
|
||||
import DeleteIcon from '$lib/components/DeleteIcon.svelte';
|
||||
import Loading from '$lib/components/Loading.svelte';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
@@ -66,6 +66,9 @@
|
||||
|
||||
let loading = false;
|
||||
let statusInterval: any = false;
|
||||
|
||||
$disabledButton = !$appSession.isAdmin;
|
||||
|
||||
async function deleteDatabase() {
|
||||
const sure = confirm(`Are you sure you would like to delete '${database.name}'?`);
|
||||
if (sure) {
|
||||
@@ -104,7 +107,7 @@
|
||||
async function getStatus() {
|
||||
if ($status.database.loading) return;
|
||||
$status.database.loading = true;
|
||||
const data = await get(`/databases/${id}`);
|
||||
const data = await get(`/databases/${id}/status`);
|
||||
$status.database.isRunning = data.isRunning;
|
||||
$status.database.initialLoading = false;
|
||||
$status.database.loading = false;
|
||||
@@ -138,6 +141,32 @@
|
||||
<Loading fullscreen cover />
|
||||
{:else}
|
||||
{#if database.type && database.destinationDockerId && database.version && database.defaultDatabase}
|
||||
{#if $status.database.isExited}
|
||||
<a
|
||||
href={!$disabledButton ? `/databases/${id}/logs` : null}
|
||||
class=" icons bg-transparent tooltip-bottom text-sm flex items-center text-red-500 tooltip-red-500"
|
||||
data-tooltip="Service exited with an error!"
|
||||
sveltekit:prefetch
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="w-6 h-6"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentcolor"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path
|
||||
d="M8.7 3h6.6c.3 0 .5 .1 .7 .3l4.7 4.7c.2 .2 .3 .4 .3 .7v6.6c0 .3 -.1 .5 -.3 .7l-4.7 4.7c-.2 .2 -.4 .3 -.7 .3h-6.6c-.3 0 -.5 -.1 -.7 -.3l-4.7 -4.7c-.2 -.2 -.3 -.4 -.3 -.7v-6.6c0 -.3 .1 -.5 .3 -.7l4.7 -4.7c.2 -.2 .4 -.3 .7 -.3z"
|
||||
/>
|
||||
<line x1="12" y1="8" x2="12" y2="12" />
|
||||
<line x1="12" y1="16" x2="12.01" y2="16" />
|
||||
</svg>
|
||||
</a>
|
||||
{/if}
|
||||
{#if $status.database.initialLoading}
|
||||
<button
|
||||
class="icons tooltip-bottom flex animate-spin items-center space-x-2 bg-transparent text-sm duration-500 ease-in-out"
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
redirect: `/database/${params.id}`
|
||||
};
|
||||
}
|
||||
const response = await get(`/destinations`);
|
||||
const response = await get(`/destinations?onlyVerified=true`);
|
||||
return {
|
||||
props: {
|
||||
...response
|
||||
@@ -55,7 +55,7 @@
|
||||
<div class="flex justify-center">
|
||||
{#if !destinations || destinations.length === 0}
|
||||
<div class="flex-col">
|
||||
<div class="pb-2">{$t('application.configuration.no_configurable_destination')}</div>
|
||||
<div class="pb-2 text-center font-bold">{$t('application.configuration.no_configurable_destination')}</div>
|
||||
<div class="flex justify-center">
|
||||
<a href="/new/destination" sveltekit:prefetch class="add-icon bg-sky-600 hover:bg-sky-500">
|
||||
<svg
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
import Databases from './_Databases/_Databases.svelte';
|
||||
import { status } from '$lib/store';
|
||||
export let database: any;
|
||||
export let settings: any;
|
||||
export let privatePort: any;
|
||||
|
||||
const { id } = $page.params;
|
||||
@@ -86,4 +85,4 @@
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
<Databases bind:database {privatePort} {settings} />
|
||||
<Databases bind:database {privatePort}/>
|
||||
|
||||
@@ -1,31 +1,13 @@
|
||||
<script context="module" lang="ts">
|
||||
import type { Load } from '@sveltejs/kit';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
export const load: Load = async ({ fetch, params, url, stuff }) => {
|
||||
try {
|
||||
const response = await get(`/databases/${params.id}/logs`);
|
||||
return {
|
||||
props: {
|
||||
database: stuff.database,
|
||||
...response
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
status: 500,
|
||||
error: new Error(`Could not load ${url}`)
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export let database: any;
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
|
||||
import { page } from '$app/stores';
|
||||
import LoadingLogs from './_Loading.svelte';
|
||||
import { get } from '$lib/api';
|
||||
import { t } from '$lib/translations';
|
||||
import { errorNotification } from '$lib/common';
|
||||
import { errorNotification } from '$lib/common';
|
||||
|
||||
const { id } = $page.params;
|
||||
|
||||
let loadLogsInterval: any = null;
|
||||
let logs: any = [];
|
||||
@@ -34,9 +16,12 @@ import { errorNotification } from '$lib/common';
|
||||
let followingLogs: any;
|
||||
let logsEl: any;
|
||||
let position = 0;
|
||||
let loadingLogs = false;
|
||||
let database: any = {};
|
||||
|
||||
const { id } = $page.params;
|
||||
onMount(async () => {
|
||||
const { logs: firstLogs } = await get(`/databases/${id}/logs`);
|
||||
logs = firstLogs;
|
||||
loadAllLogs();
|
||||
loadLogsInterval = setInterval(() => {
|
||||
loadLogs();
|
||||
@@ -48,6 +33,7 @@ import { errorNotification } from '$lib/common';
|
||||
});
|
||||
async function loadAllLogs() {
|
||||
try {
|
||||
loadingLogs = true;
|
||||
const data: any = await get(`/databases/${id}/logs`);
|
||||
if (data?.logs) {
|
||||
lastLog = data.logs[data.logs.length - 1];
|
||||
@@ -56,13 +42,15 @@ import { errorNotification } from '$lib/common';
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return errorNotification(error);
|
||||
} finally {
|
||||
loadingLogs = false;
|
||||
}
|
||||
}
|
||||
async function loadLogs() {
|
||||
if (loadingLogs) return;
|
||||
try {
|
||||
const newLogs: any = await get(
|
||||
`/databases/${id}/logs?since=${lastLog?.split(' ')[0] || 0}`
|
||||
);
|
||||
loadingLogs = true;
|
||||
const newLogs: any = await get(`/databases/${id}/logs?since=${lastLog?.split(' ')[0] || 0}`);
|
||||
|
||||
if (newLogs?.logs && newLogs.logs[newLogs.logs.length - 1] !== logs[logs.length - 1]) {
|
||||
logs = logs.concat(newLogs.logs);
|
||||
@@ -70,6 +58,8 @@ import { errorNotification } from '$lib/common';
|
||||
}
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
} finally {
|
||||
loadingLogs = false;
|
||||
}
|
||||
}
|
||||
function detect() {
|
||||
@@ -137,7 +127,7 @@ import { errorNotification } from '$lib/common';
|
||||
{#if loadLogsInterval}
|
||||
<LoadingLogs />
|
||||
{/if}
|
||||
<div class="flex justify-end sticky top-0 p-2 mx-1">
|
||||
<div class="flex justify-end sticky top-0 p-1 mx-1">
|
||||
<button
|
||||
on:click={followBuild}
|
||||
class="bg-transparent"
|
||||
|
||||
@@ -78,7 +78,7 @@
|
||||
<div class="flex flex-col">
|
||||
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row">
|
||||
{#each ownDatabases as database}
|
||||
<a href="/databases/{database.id}" class="w-96 p-2 no-underline">
|
||||
<a href="/databases/{database.id}" class="p-2 no-underline">
|
||||
<div class="box-selection group relative hover:bg-purple-600">
|
||||
{#if database.type === 'clickhouse'}
|
||||
<Clickhouse isAbsolute />
|
||||
@@ -103,6 +103,9 @@
|
||||
{#if $appSession.teamId === '0' && otherDatabases.length > 0}
|
||||
<div class="truncate text-center">{database.teams[0].name}</div>
|
||||
{/if}
|
||||
{#if database.destinationDocker?.name}
|
||||
<div class="truncate text-center">{database.destinationDocker.name}</div>
|
||||
{/if}
|
||||
{#if !database.type}
|
||||
<div class="truncate text-center font-bold text-red-500 group-hover:text-white">
|
||||
{$t('application.configuration.configuration_missing')}
|
||||
@@ -113,10 +116,10 @@
|
||||
{/each}
|
||||
</div>
|
||||
{#if otherDatabases.length > 0 && $appSession.teamId === '0'}
|
||||
<div class="px-6 pb-5 pt-10 text-xl font-bold">Other Databases</div>
|
||||
<div class="px-6 pb-5 pt-10 text-2xl font-bold text-center">Other Databases</div>
|
||||
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row">
|
||||
{#each otherDatabases as database}
|
||||
<a href="/databases/{database.id}" class="w-96 p-2 no-underline">
|
||||
<a href="/databases/{database.id}" class="p-2 no-underline">
|
||||
<div class="box-selection group relative hover:bg-purple-600">
|
||||
{#if database.type === 'clickhouse'}
|
||||
<Clickhouse isAbsolute />
|
||||
|
||||
@@ -1,56 +1,65 @@
|
||||
<script lang="ts">
|
||||
export let destination: any;
|
||||
export let settings: any;
|
||||
export let state: any;
|
||||
|
||||
import { toast } from '@zerodevx/svelte-toast';
|
||||
import { page } from '$app/stores';
|
||||
import { post } from '$lib/api';
|
||||
import { get, post } from '$lib/api';
|
||||
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import { t } from '$lib/translations';
|
||||
import { errorNotification } from '$lib/common';
|
||||
import { appSession } from '$lib/store';
|
||||
import Setting from '$lib/components/Setting.svelte';
|
||||
|
||||
const { id } = $page.params;
|
||||
let cannotDisable = settings.fqdn && destination.engine === '/var/run/docker.sock';
|
||||
let loading = false;
|
||||
let loadingProxy = false;
|
||||
let restarting = false;
|
||||
let loading = {
|
||||
restart: false,
|
||||
proxy: false,
|
||||
save: false
|
||||
};
|
||||
|
||||
async function handleSubmit() {
|
||||
loading = true;
|
||||
loading.save = true;
|
||||
try {
|
||||
return await post(`/destinations/${id}`, { ...destination });
|
||||
await post(`/destinations/${id}`, { ...destination });
|
||||
toast.push('Configuration saved.');
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
} finally {
|
||||
loading = false;
|
||||
loading.save = false;
|
||||
}
|
||||
}
|
||||
onMount(async () => {
|
||||
if (state === false && destination.isCoolifyProxyUsed === true) {
|
||||
destination.isCoolifyProxyUsed = !destination.isCoolifyProxyUsed;
|
||||
loading.proxy = true;
|
||||
const { isRunning } = await get(`/destinations/${id}/status`);
|
||||
let proxyUsed = !destination.isCoolifyProxyUsed;
|
||||
if (isRunning === false && destination.isCoolifyProxyUsed === true) {
|
||||
try {
|
||||
await post(`/destinations/${id}/settings`, {
|
||||
isCoolifyProxyUsed: destination.isCoolifyProxyUsed,
|
||||
isCoolifyProxyUsed: proxyUsed,
|
||||
engine: destination.engine
|
||||
});
|
||||
await stopProxy();
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
} else if (state === true && destination.isCoolifyProxyUsed === false) {
|
||||
destination.isCoolifyProxyUsed = !destination.isCoolifyProxyUsed;
|
||||
} else if (isRunning === true && destination.isCoolifyProxyUsed === false) {
|
||||
try {
|
||||
await post(`/destinations/${id}/settings`, {
|
||||
isCoolifyProxyUsed: destination.isCoolifyProxyUsed,
|
||||
isCoolifyProxyUsed: proxyUsed,
|
||||
engine: destination.engine
|
||||
});
|
||||
await startProxy();
|
||||
destination.isCoolifyProxyUsed = proxyUsed;
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
} finally {
|
||||
loading.proxy = false;
|
||||
}
|
||||
}
|
||||
loading.proxy = false;
|
||||
});
|
||||
async function changeProxySetting() {
|
||||
if (!cannotDisable) {
|
||||
@@ -67,7 +76,7 @@
|
||||
}
|
||||
destination.isCoolifyProxyUsed = !destination.isCoolifyProxyUsed;
|
||||
try {
|
||||
loadingProxy = true;
|
||||
loading.proxy = true;
|
||||
await post(`/destinations/${id}/settings`, {
|
||||
isCoolifyProxyUsed: destination.isCoolifyProxyUsed,
|
||||
engine: destination.engine
|
||||
@@ -80,7 +89,7 @@
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
} finally {
|
||||
loadingProxy = false;
|
||||
loading.proxy = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -104,7 +113,7 @@
|
||||
const sure = confirm($t('destination.confirm_restart_proxy'));
|
||||
if (sure) {
|
||||
try {
|
||||
restarting = true;
|
||||
loading.restart = true;
|
||||
toast.push($t('destination.coolify_proxy_restarting'));
|
||||
await post(`/destinations/${id}/restart`, {
|
||||
engine: destination.engine,
|
||||
@@ -115,7 +124,7 @@
|
||||
window.location.reload();
|
||||
}, 5000);
|
||||
} finally {
|
||||
restarting = false;
|
||||
loading.restart = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -128,23 +137,20 @@
|
||||
<button
|
||||
type="submit"
|
||||
class="bg-sky-600 hover:bg-sky-500"
|
||||
class:bg-sky-600={!loading}
|
||||
class:hover:bg-sky-500={!loading}
|
||||
disabled={loading}
|
||||
>{loading ? $t('forms.saving') : $t('forms.save')}
|
||||
class:bg-sky-600={!loading.save}
|
||||
class:hover:bg-sky-500={!loading.save}
|
||||
disabled={loading.save}
|
||||
>{loading.save ? $t('forms.saving') : $t('forms.save')}
|
||||
</button>
|
||||
<button
|
||||
class={restarting ? '' : 'bg-red-600 hover:bg-red-500'}
|
||||
disabled={restarting}
|
||||
class={loading.restart ? '' : 'bg-red-600 hover:bg-red-500'}
|
||||
disabled={loading.restart}
|
||||
on:click|preventDefault={forceRestartProxy}
|
||||
>{restarting
|
||||
>{loading.restart
|
||||
? $t('destination.restarting_please_wait')
|
||||
: $t('destination.force_restart_proxy')}</button
|
||||
>
|
||||
{/if}
|
||||
<!-- <button type="button" class="bg-coollabs hover:bg-coollabs-100" on:click={scanApps}
|
||||
>Scan for applications</button
|
||||
> -->
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10 ">
|
||||
<label for="name" class="text-base font-bold text-stone-100">{$t('forms.name')}</label>
|
||||
@@ -168,10 +174,6 @@
|
||||
value={destination.engine}
|
||||
/>
|
||||
</div>
|
||||
<!-- <div class="flex items-center">
|
||||
<label for="remoteEngine">Remote Engine?</label>
|
||||
<input name="remoteEngine" type="checkbox" bind:checked={payload.remoteEngine} />
|
||||
</div> -->
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="network" class="text-base font-bold text-stone-100">{$t('forms.network')}</label>
|
||||
<CopyPasswordField
|
||||
@@ -186,7 +188,7 @@
|
||||
{#if $appSession.teamId === '0'}
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<Setting
|
||||
loading={loadingProxy}
|
||||
loading={loading.proxy}
|
||||
disabled={cannotDisable}
|
||||
bind:setting={destination.isCoolifyProxyUsed}
|
||||
on:click={changeProxySetting}
|
||||
|
||||
@@ -21,10 +21,9 @@
|
||||
payload = {
|
||||
name: $t('sources.remote_docker'),
|
||||
remoteEngine: true,
|
||||
ipAddress: null,
|
||||
user: 'root',
|
||||
port: 22,
|
||||
sshPrivateKey: null,
|
||||
remoteIpAddress: null,
|
||||
remoteUser: 'root',
|
||||
remotePort: 22,
|
||||
network: cuid(),
|
||||
isCoolifyProxyUsed: true
|
||||
};
|
||||
@@ -44,7 +43,7 @@
|
||||
<button class="w-32" on:click={() => setPredefined('localDocker')}
|
||||
>{$t('sources.local_docker')}</button
|
||||
>
|
||||
<!-- <button class="w-32" on:click={() => setPredefined('remoteDocker')}>Remote Docker</button> -->
|
||||
<button class="w-32" on:click={() => setPredefined('remoteDocker')}>Remote Docker</button>
|
||||
<!-- <button class="w-32" on:click={() => setPredefined('kubernetes')}>Kubernetes</button> -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,23 +5,35 @@
|
||||
|
||||
import { post } from '$lib/api';
|
||||
import { errorNotification } from '$lib/common';
|
||||
import Explainer from '$lib/components/Explainer.svelte';
|
||||
import Setting from '$lib/components/Setting.svelte';
|
||||
import { t } from '$lib/translations';
|
||||
|
||||
let loading = false;
|
||||
|
||||
async function handleSubmit() {
|
||||
if (loading) return;
|
||||
try {
|
||||
const { id } = await post('/new/destination/docker', {
|
||||
loading = true;
|
||||
await post(`/destinations/check`, { network: payload.network });
|
||||
const { id } = await post(`/destinations/new`, {
|
||||
...payload
|
||||
});
|
||||
return await goto(`/destinations/${id}`);
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="text-center flex justify-center">
|
||||
<Explainer
|
||||
customClass="max-w-[32rem]"
|
||||
text="Remote Docker Engines are using <span class='text-white font-bold'>SSH connection</span> to initiate connection. You need to setup an <span class='text-white font-bold'>SSH key</span> in advance on the server and install Docker. <br>See <a class='text-white' href='https://docs.coollabs.io'>docs</a> for more details."
|
||||
/>
|
||||
</div>
|
||||
<div class="flex justify-center px-6 pb-8">
|
||||
<form on:submit|preventDefault={handleSubmit} class="grid grid-flow-row gap-2 py-4">
|
||||
<div class="flex items-center space-x-2 pb-5">
|
||||
@@ -44,40 +56,36 @@
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="ipAddress" class="text-base font-bold text-stone-100"
|
||||
<label for="remoteIpAddress" class="text-base font-bold text-stone-100"
|
||||
>{$t('forms.ip_address')}</label
|
||||
>
|
||||
<input
|
||||
required
|
||||
name="ipAddress"
|
||||
name="remoteIpAddress"
|
||||
placeholder="{$t('forms.eg')}: 192.168..."
|
||||
bind:value={payload.ipAddress}
|
||||
bind:value={payload.remoteIpAddress}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="user" class="text-base font-bold text-stone-100">{$t('forms.user')}</label>
|
||||
<input required name="user" placeholder="{$t('forms.eg')}: root" bind:value={payload.user} />
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="port" class="text-base font-bold text-stone-100">{$t('forms.port')}</label>
|
||||
<input required name="port" placeholder="{$t('forms.eg')}: 22" bind:value={payload.port} />
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="sshPrivateKey" class="text-base font-bold text-stone-100"
|
||||
>{$t('forms.ssh_private_key')}</label
|
||||
>
|
||||
<textarea
|
||||
rows="10"
|
||||
class="resize-none"
|
||||
<label for="remoteUser" class="text-base font-bold text-stone-100">{$t('forms.user')}</label>
|
||||
<input
|
||||
required
|
||||
name="sshPrivateKey"
|
||||
placeholder="{$t('forms.eg')}: -----BEGIN...."
|
||||
bind:value={payload.sshPrivateKey}
|
||||
name="remoteUser"
|
||||
placeholder="{$t('forms.eg')}: root"
|
||||
bind:value={payload.remoteUser}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="remotePort" class="text-base font-bold text-stone-100">{$t('forms.port')}</label>
|
||||
<input
|
||||
required
|
||||
name="remotePort"
|
||||
placeholder="{$t('forms.eg')}: 22"
|
||||
bind:value={payload.remotePort}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="network" class="text-base font-bold text-stone-100">{$t('forms.network')}</label>
|
||||
<input
|
||||
|
||||
@@ -1,80 +1,90 @@
|
||||
<script lang="ts">
|
||||
export let destination: any;
|
||||
export let settings: any
|
||||
export let state: any
|
||||
export let settings: any;
|
||||
|
||||
import { toast } from '@zerodevx/svelte-toast';
|
||||
import { page, session } from '$app/stores';
|
||||
import Setting from '$lib/components/Setting.svelte';
|
||||
import { post } from '$lib/api';
|
||||
import { get, post } from '$lib/api';
|
||||
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import { t } from '$lib/translations';
|
||||
import { errorNotification, generateRemoteEngine } from '$lib/common';
|
||||
import { appSession } from '$lib/store';
|
||||
import { errorNotification } from '$lib/common';
|
||||
import { appSession } from '$lib/store';
|
||||
|
||||
const { id } = $page.params;
|
||||
|
||||
let cannotDisable = settings.fqdn && destination.engine === '/var/run/docker.sock';
|
||||
// let scannedApps = [];
|
||||
let loading = false;
|
||||
let restarting = false;
|
||||
let loading = {
|
||||
restart: false,
|
||||
proxy: true,
|
||||
save: false,
|
||||
verify: false
|
||||
};
|
||||
|
||||
$: isDisabled = !$appSession.isAdmin;
|
||||
|
||||
async function handleSubmit() {
|
||||
loading = true;
|
||||
loading.save = true;
|
||||
try {
|
||||
return await post(`/destinations/${id}.json`, { ...destination });
|
||||
} catch (error ) {
|
||||
await post(`/destinations/${id}`, { ...destination });
|
||||
toast.push('Configuration saved.');
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
} finally {
|
||||
loading = false;
|
||||
loading.save = false;
|
||||
}
|
||||
}
|
||||
// async function scanApps() {
|
||||
// scannedApps = [];
|
||||
// const data = await fetch(`/destinations/${id}/scan.json`);
|
||||
// const { containers } = await data.json();
|
||||
// scannedApps = containers;
|
||||
// }
|
||||
onMount(async () => {
|
||||
if (state === false && destination.isCoolifyProxyUsed === true) {
|
||||
destination.isCoolifyProxyUsed = !destination.isCoolifyProxyUsed;
|
||||
try {
|
||||
await post(`/destinations/${id}/settings.json`, {
|
||||
isCoolifyProxyUsed: destination.isCoolifyProxyUsed,
|
||||
engine: destination.engine
|
||||
});
|
||||
await stopProxy();
|
||||
} catch (error ) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
} else if (state === true && destination.isCoolifyProxyUsed === false) {
|
||||
destination.isCoolifyProxyUsed = !destination.isCoolifyProxyUsed;
|
||||
try {
|
||||
await post(`/destinations/${id}/settings.json`, {
|
||||
isCoolifyProxyUsed: destination.isCoolifyProxyUsed,
|
||||
engine: destination.engine
|
||||
});
|
||||
await startProxy();
|
||||
} catch ( error ) {
|
||||
return errorNotification(error);
|
||||
loading.proxy = true;
|
||||
if (destination.remoteEngine && destination.remoteVerified) {
|
||||
const { isRunning } = await get(`/destinations/${id}/status`);
|
||||
if (isRunning === false && destination.isCoolifyProxyUsed === true) {
|
||||
destination.isCoolifyProxyUsed = !destination.isCoolifyProxyUsed;
|
||||
try {
|
||||
await post(`/destinations/${id}/settings`, {
|
||||
isCoolifyProxyUsed: destination.isCoolifyProxyUsed,
|
||||
engine: destination.engine
|
||||
});
|
||||
await stopProxy();
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
} else if (isRunning === true && destination.isCoolifyProxyUsed === false) {
|
||||
destination.isCoolifyProxyUsed = !destination.isCoolifyProxyUsed;
|
||||
try {
|
||||
await post(`/destinations/${id}/settings`, {
|
||||
isCoolifyProxyUsed: destination.isCoolifyProxyUsed,
|
||||
engine: destination.engine
|
||||
});
|
||||
await startProxy();
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
loading.proxy = false;
|
||||
});
|
||||
async function changeProxySetting() {
|
||||
loading.proxy = true;
|
||||
if (!cannotDisable) {
|
||||
const isProxyActivated = destination.isCoolifyProxyUsed;
|
||||
if (isProxyActivated) {
|
||||
const sure = confirm(
|
||||
`Are you sure you want to ${
|
||||
destination.isCoolifyProxyUsed ? 'disable' : 'enable'
|
||||
} Coolify proxy? It will remove the proxy for all configured networks and all deployments on '${
|
||||
destination.engine
|
||||
}'! Nothing will be reachable if you do it!`
|
||||
} Coolify proxy? It will remove the proxy for all configured networks and all deployments! Nothing will be reachable if you do it!`
|
||||
);
|
||||
if (!sure) return;
|
||||
if (!sure) {
|
||||
loading.proxy = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
destination.isCoolifyProxyUsed = !destination.isCoolifyProxyUsed;
|
||||
let proxyUsed = !destination.isCoolifyProxyUsed;
|
||||
try {
|
||||
await post(`/destinations/${id}/settings.json`, {
|
||||
isCoolifyProxyUsed: destination.isCoolifyProxyUsed,
|
||||
await post(`/destinations/${id}/settings`, {
|
||||
isCoolifyProxyUsed: proxyUsed,
|
||||
engine: destination.engine
|
||||
});
|
||||
if (isProxyActivated) {
|
||||
@@ -82,15 +92,17 @@ import { appSession } from '$lib/store';
|
||||
} else {
|
||||
await startProxy();
|
||||
}
|
||||
destination.isCoolifyProxyUsed = proxyUsed;
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
} finally {
|
||||
loading.proxy = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
async function stopProxy() {
|
||||
try {
|
||||
const engine = generateRemoteEngine(destination);
|
||||
await post(`/destinations/${id}/stop.json`, { engine });
|
||||
await post(`/destinations/${id}/stop`, { engine: destination.engine });
|
||||
return toast.push($t('destination.coolify_proxy_stopped'));
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
@@ -98,8 +110,7 @@ import { appSession } from '$lib/store';
|
||||
}
|
||||
async function startProxy() {
|
||||
try {
|
||||
const engine = generateRemoteEngine(destination);
|
||||
await post(`/destinations/${id}/start.json`, { engine });
|
||||
await post(`/destinations/${id}/start`, { engine: destination.engine });
|
||||
return toast.push($t('destination.coolify_proxy_started'));
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
@@ -109,9 +120,9 @@ import { appSession } from '$lib/store';
|
||||
const sure = confirm($t('destination.confirm_restart_proxy'));
|
||||
if (sure) {
|
||||
try {
|
||||
restarting = true;
|
||||
loading.restart = true;
|
||||
toast.push($t('destination.coolify_proxy_restarting'));
|
||||
await post(`/destinations/${id}/restart.json`, {
|
||||
await post(`/destinations/${id}/restart`, {
|
||||
engine: destination.engine,
|
||||
fqdn: settings.fqdn
|
||||
});
|
||||
@@ -119,9 +130,24 @@ import { appSession } from '$lib/store';
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 5000);
|
||||
} finally {
|
||||
loading.restart = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
async function verifyRemoteDocker() {
|
||||
try {
|
||||
loading.verify = true;
|
||||
await post(`/destinations/${id}/verify`, {});
|
||||
destination.remoteVerified = true;
|
||||
toast.push('Remote Docker Engine verified!');
|
||||
return;
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
} finally {
|
||||
loading.verify = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<form on:submit|preventDefault={handleSubmit} class="grid grid-flow-row gap-2 py-4">
|
||||
@@ -131,23 +157,28 @@ import { appSession } from '$lib/store';
|
||||
<button
|
||||
type="submit"
|
||||
class="bg-sky-600 hover:bg-sky-500"
|
||||
class:bg-sky-600={!loading}
|
||||
class:hover:bg-sky-500={!loading}
|
||||
disabled={loading}
|
||||
>{loading ? $t('forms.saving') : $t('forms.save')}
|
||||
class:bg-sky-600={!loading.save}
|
||||
class:hover:bg-sky-500={!loading.save}
|
||||
disabled={loading.save}
|
||||
>{loading.save ? $t('forms.saving') : $t('forms.save')}
|
||||
</button>
|
||||
<button
|
||||
class={restarting ? '' : 'bg-red-600 hover:bg-red-500'}
|
||||
disabled={restarting}
|
||||
on:click|preventDefault={forceRestartProxy}
|
||||
>{restarting
|
||||
? $t('destination.restarting_please_wait')
|
||||
: $t('destination.force_restart_proxy')}</button
|
||||
>
|
||||
{#if !destination.remoteVerified}
|
||||
<button
|
||||
disabled={loading.verify}
|
||||
on:click|preventDefault|stopPropagation={verifyRemoteDocker}
|
||||
>{loading.verify ? 'Verifying...' : 'Verify Remote Docker Engine'}</button
|
||||
>
|
||||
{:else}
|
||||
<button
|
||||
class={loading.restart ? '' : 'bg-red-600 hover:bg-red-500'}
|
||||
disabled={loading.restart}
|
||||
on:click|preventDefault={forceRestartProxy}
|
||||
>{loading.restart
|
||||
? $t('destination.restarting_please_wait')
|
||||
: $t('destination.force_restart_proxy')}</button
|
||||
>
|
||||
{/if}
|
||||
{/if}
|
||||
<!-- <button type="button" class="bg-coollabs hover:bg-coollabs-100" on:click={scanApps}
|
||||
>Scan for applications</button
|
||||
> -->
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10 ">
|
||||
<label for="name" class="text-base font-bold text-stone-100">{$t('forms.name')}</label>
|
||||
@@ -159,22 +190,6 @@ import { appSession } from '$lib/store';
|
||||
bind:value={destination.name}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="engine" class="text-base font-bold text-stone-100">{$t('forms.engine')}</label>
|
||||
<CopyPasswordField
|
||||
id="engine"
|
||||
readonly
|
||||
disabled
|
||||
name="engine"
|
||||
placeholder="{$t('forms.eg')}: /var/run/docker.sock"
|
||||
value={destination.engine}
|
||||
/>
|
||||
</div>
|
||||
<!-- <div class="flex items-center">
|
||||
<label for="remoteEngine">Remote Engine?</label>
|
||||
<input name="remoteEngine" type="checkbox" bind:checked={payload.remoteEngine} />
|
||||
</div> -->
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="network" class="text-base font-bold text-stone-100">{$t('forms.network')}</label>
|
||||
<CopyPasswordField
|
||||
@@ -186,9 +201,53 @@ import { appSession } from '$lib/store';
|
||||
value={destination.network}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="remoteIpAddress" class="text-base font-bold text-stone-100">IP Address</label>
|
||||
<CopyPasswordField
|
||||
id="remoteIpAddress"
|
||||
readonly
|
||||
disabled
|
||||
name="remoteIpAddress"
|
||||
value={destination.remoteIpAddress}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="remoteUser" class="text-base font-bold text-stone-100">User</label>
|
||||
<CopyPasswordField
|
||||
id="remoteUser"
|
||||
readonly
|
||||
disabled
|
||||
name="remoteUser"
|
||||
value={destination.remoteUser}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="remotePort" class="text-base font-bold text-stone-100">Port</label>
|
||||
<CopyPasswordField
|
||||
id="remotePort"
|
||||
readonly
|
||||
disabled
|
||||
name="remotePort"
|
||||
value={destination.remotePort}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="sshKey" class="text-base font-bold text-stone-100">SSH Key</label>
|
||||
<a
|
||||
href={!isDisabled ? `/destinations/${id}/configuration/sshkey?from=/destinations/${id}` : ''}
|
||||
class="no-underline"
|
||||
><input
|
||||
value={destination.sshKey.name}
|
||||
readonly
|
||||
id="sshKey"
|
||||
class="cursor-pointer hover:bg-coolgray-500"
|
||||
/></a
|
||||
>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<Setting
|
||||
disabled={cannotDisable}
|
||||
loading={loading.proxy}
|
||||
bind:setting={destination.isCoolifyProxyUsed}
|
||||
on:click={changeProxySetting}
|
||||
title={$t('destination.use_coolify_proxy')}
|
||||
@@ -200,27 +259,3 @@ import { appSession } from '$lib/store';
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
<!-- <div class="flex justify-center">
|
||||
{#if payload.isCoolifyProxyUsed}
|
||||
{#if state}
|
||||
<button on:click={stopProxy}>Stop proxy</button>
|
||||
{:else}
|
||||
<button on:click={startProxy}>Start proxy</button>
|
||||
{/if}
|
||||
{/if}
|
||||
</div> -->
|
||||
|
||||
<!-- {#if scannedApps.length > 0}
|
||||
<div class="flex justify-center px-6 pb-10">
|
||||
<div class="flex space-x-2 h-8 items-center">
|
||||
<div class="font-bold text-xl text-white">Found applications</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="max-w-4xl mx-auto px-6">
|
||||
<div class="flex space-x-2 justify-center">
|
||||
{#each scannedApps as app}
|
||||
<FoundApp {app} />
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if} -->
|
||||
|
||||
@@ -1,27 +1,46 @@
|
||||
<script context="module" lang="ts">
|
||||
import type { Load } from '@sveltejs/kit';
|
||||
export const load: Load = async ({ fetch, url, params }) => {
|
||||
function checkConfiguration(destination: any): string | null {
|
||||
let configurationPhase = null;
|
||||
if (!destination?.remoteEngine) return configurationPhase;
|
||||
if (!destination?.sshKey) {
|
||||
configurationPhase = 'sshkey';
|
||||
}
|
||||
return configurationPhase;
|
||||
}
|
||||
export const load: Load = async ({ url, params }) => {
|
||||
try {
|
||||
const { id } = params;
|
||||
const response = await get(`/destinations/${id}`);
|
||||
const { destination, settings, state } = response;
|
||||
const { destination, settings } = response;
|
||||
if (id !== 'new' && (!destination || Object.entries(destination).length === 0)) {
|
||||
return {
|
||||
status: 302,
|
||||
redirect: '/destinations'
|
||||
};
|
||||
}
|
||||
const configurationPhase = checkConfiguration(destination);
|
||||
if (
|
||||
configurationPhase &&
|
||||
url.pathname !== `/destinations/${params.id}/configuration/${configurationPhase}`
|
||||
) {
|
||||
return {
|
||||
status: 302,
|
||||
redirect: `/destinations/${params.id}/configuration/${configurationPhase}`
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
props: {
|
||||
destination
|
||||
},
|
||||
stuff: {
|
||||
destination,
|
||||
settings,
|
||||
state
|
||||
settings
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
return handlerNotFoundLoad(error, url);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
<script context="module" lang="ts">
|
||||
import type { Load } from '@sveltejs/kit';
|
||||
export const load: Load = async ({ fetch, params, url, stuff }) => {
|
||||
try {
|
||||
const response = await get(`/settings`);
|
||||
return {
|
||||
props: {
|
||||
...response
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
status: 500,
|
||||
error: new Error(`Could not load ${url}`)
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { goto } from '$app/navigation';
|
||||
import { get, post } from '$lib/api';
|
||||
import { errorNotification } from '$lib/common';
|
||||
|
||||
const { id } = $page.params;
|
||||
const from = $page.url.searchParams.get('from');
|
||||
|
||||
export let sshKeys: any;
|
||||
|
||||
async function handleSubmit(sshKeyId: string) {
|
||||
try {
|
||||
await post(`/destinations/${id}/configuration/sshKey`, { id: sshKeyId });
|
||||
return await goto(from || `/destinations/${id}`);
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex space-x-1 p-6 font-bold">
|
||||
<div class="mr-4 text-2xl tracking-tight">Select a SSH Keys</div>
|
||||
</div>
|
||||
<div class="flex flex-col justify-center">
|
||||
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row ">
|
||||
{#if sshKeys.length > 0}
|
||||
{#each sshKeys as sshKey}
|
||||
<div class="p-2 relative">
|
||||
<form on:submit|preventDefault={() => handleSubmit(sshKey.id)}>
|
||||
<button
|
||||
type="submit"
|
||||
class="disabled:opacity-95 bg-coolgray-200 disabled:text-white box-selection hover:bg-orange-700 group"
|
||||
>
|
||||
<div class="font-bold text-xl text-center truncate">{sshKey.name}</div>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
{/each}
|
||||
{:else}
|
||||
<div class="flex-col">
|
||||
<div class="pb-2 text-center font-bold">No SSH key found</div>
|
||||
<div class="flex justify-center">
|
||||
<a
|
||||
href="/settings/ssh-keys"
|
||||
sveltekit:prefetch
|
||||
class="add-icon bg-sky-600 hover:bg-sky-500"
|
||||
>
|
||||
<svg
|
||||
class="w-6"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
><path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 6v6m0 0v6m0-6h6m-6 0H6"
|
||||
/></svg
|
||||
>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
@@ -39,7 +39,7 @@
|
||||
<div class="flex space-x-1 p-6 font-bold">
|
||||
<div class="mr-4 text-2xl tracking-tight">{$t('index.destinations')}</div>
|
||||
{#if $appSession.isAdmin}
|
||||
<a href="/destinations/new" class="add-icon bg-sky-600 hover:bg-sky-500">
|
||||
<a href="/destinations/new" class="add-icon bg-sky-600 hover:bg-sky-500">
|
||||
<svg
|
||||
class="w-6"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@@ -66,24 +66,112 @@
|
||||
<div class="flex flex-col">
|
||||
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row">
|
||||
{#each ownDestinations as destination}
|
||||
<a href="/destinations/{destination.id}" class="w-96 p-2 no-underline">
|
||||
<div class="box-selection hover:bg-sky-600">
|
||||
<a href="/destinations/{destination.id}" class="p-2 no-underline relative">
|
||||
<div class="box-selection hover:bg-sky-600 ">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="absolute top-0 left-0 -m-4 h-12 w-12 text-sky-500"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path
|
||||
d="M22 12.54c-1.804 -.345 -2.701 -1.08 -3.523 -2.94c-.487 .696 -1.102 1.568 -.92 2.4c.028 .238 -.32 1.002 -.557 1h-14c0 5.208 3.164 7 6.196 7c4.124 .022 7.828 -1.376 9.854 -5c1.146 -.101 2.296 -1.505 2.95 -2.46z"
|
||||
/>
|
||||
<path d="M5 10h3v3h-3z" />
|
||||
<path d="M8 10h3v3h-3z" />
|
||||
<path d="M11 10h3v3h-3z" />
|
||||
<path d="M8 7h3v3h-3z" />
|
||||
<path d="M11 7h3v3h-3z" />
|
||||
<path d="M11 4h3v3h-3z" />
|
||||
<path d="M4.571 18c1.5 0 2.047 -.074 2.958 -.78" />
|
||||
<line x1="10" y1="16" x2="10" y2="16.01" />
|
||||
</svg>
|
||||
{#if destination.remoteEngine}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="absolute top-0 left-9 -m-4 h-6 w-6 text-sky-500 rotate-45"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="3"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<line x1="12" y1="18" x2="12.01" y2="18" />
|
||||
<path d="M9.172 15.172a4 4 0 0 1 5.656 0" />
|
||||
<path d="M6.343 12.343a8 8 0 0 1 11.314 0" />
|
||||
<path d="M3.515 9.515c4.686 -4.687 12.284 -4.687 17 0" />
|
||||
</svg>
|
||||
{/if}
|
||||
<div class="truncate text-center text-xl font-bold">{destination.name}</div>
|
||||
{#if $appSession.teamId === '0' && otherDestinations.length > 0}
|
||||
<div class="truncate text-center">{destination.teams[0].name}</div>
|
||||
{/if}
|
||||
<div class="truncate text-center">{destination.network}</div>
|
||||
{#if $appSession.teamId === '0' && destination.remoteVerified === false && destination.remoteEngine}
|
||||
<div class="truncate text-center text-sm text-red-500">Not verified yet</div>
|
||||
{/if}
|
||||
{#if destination.remoteEngine && !destination.sshKeyId}
|
||||
<div class="truncate text-center text-sm text-red-500">SSH Key missing!</div>
|
||||
{/if}
|
||||
</div>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
{#if otherDestinations.length > 0 && $appSession.teamId === '0'}
|
||||
<div class="px-6 pb-5 pt-10 text-xl font-bold">Other Destinations</div>
|
||||
<div class="px-6 pb-5 pt-10 text-2xl font-bold text-center">Other Destinations</div>
|
||||
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row">
|
||||
{#each otherDestinations as destination}
|
||||
<a href="/destinations/{destination.id}" class="w-96 p-2 no-underline">
|
||||
<a href="/destinations/{destination.id}" class="p-2 no-underline relative">
|
||||
<div class="box-selection hover:bg-sky-600">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="absolute top-0 left-0 -m-4 h-12 w-12 text-sky-500"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path
|
||||
d="M22 12.54c-1.804 -.345 -2.701 -1.08 -3.523 -2.94c-.487 .696 -1.102 1.568 -.92 2.4c.028 .238 -.32 1.002 -.557 1h-14c0 5.208 3.164 7 6.196 7c4.124 .022 7.828 -1.376 9.854 -5c1.146 -.101 2.296 -1.505 2.95 -2.46z"
|
||||
/>
|
||||
<path d="M5 10h3v3h-3z" />
|
||||
<path d="M8 10h3v3h-3z" />
|
||||
<path d="M11 10h3v3h-3z" />
|
||||
<path d="M8 7h3v3h-3z" />
|
||||
<path d="M11 7h3v3h-3z" />
|
||||
<path d="M11 4h3v3h-3z" />
|
||||
<path d="M4.571 18c1.5 0 2.047 -.074 2.958 -.78" />
|
||||
<line x1="10" y1="16" x2="10" y2="16.01" />
|
||||
</svg>
|
||||
{#if destination.remoteEngine}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="absolute top-0 left-9 -m-4 h-6 w-6 text-sky-500 rotate-45"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="3"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<line x1="12" y1="18" x2="12.01" y2="18" />
|
||||
<path d="M9.172 15.172a4 4 0 0 1 5.656 0" />
|
||||
<path d="M6.343 12.343a8 8 0 0 1 11.314 0" />
|
||||
<path d="M3.515 9.515c4.686 -4.687 12.284 -4.687 17 0" />
|
||||
</svg>
|
||||
{/if}
|
||||
<div class="truncate text-center text-xl font-bold">{destination.name}</div>
|
||||
{#if $appSession.teamId === '0'}
|
||||
<div class="truncate text-center">{destination.teams[0].name}</div>
|
||||
|
||||
@@ -189,54 +189,52 @@
|
||||
|
||||
<div class="mx-auto max-w-4xl px-6">
|
||||
<div class="title font-bold">Teams</div>
|
||||
<div class="flex items-center justify-center pt-10">
|
||||
<div class="flex flex-col">
|
||||
<div class="flex flex-row flex-wrap justify-center px-2 pb-10 md:flex-row">
|
||||
{#each ownTeams as team}
|
||||
<a href="/iam/team/{team.id}" class="w-96 p-2 no-underline">
|
||||
<div class="box-selection relative">
|
||||
<div>
|
||||
<div class="truncate text-center text-xl font-bold">
|
||||
{team.name}
|
||||
</div>
|
||||
<div class="mt-1 text-center text-xs">
|
||||
{team.permissions?.length} member(s)
|
||||
</div>
|
||||
<div class="flex-col items-center justify-center pt-10">
|
||||
<div class="flex flex-row flex-wrap justify-center px-2 pb-10 md:flex-row">
|
||||
{#each ownTeams as team}
|
||||
<a href="/iam/team/{team.id}" class="p-2 no-underline">
|
||||
<div class="box-selection relative">
|
||||
<div>
|
||||
<div class="truncate text-center text-xl font-bold">
|
||||
{team.name}
|
||||
</div>
|
||||
<div class="flex items-center justify-center pt-3">
|
||||
<button
|
||||
on:click|preventDefault={() => switchTeam(team.id)}
|
||||
class:bg-fuchsia-600={$appSession.teamId !== team.id}
|
||||
class:hover:bg-fuchsia-500={$appSession.teamId !== team.id}
|
||||
class:bg-transparent={$appSession.teamId === team.id}
|
||||
disabled={$appSession.teamId === team.id}
|
||||
>{$appSession.teamId === team.id ? 'Current Team' : 'Switch Team'}</button
|
||||
>
|
||||
<div class="mt-1 text-center text-xs">
|
||||
{team.permissions?.length} member(s)
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-center pt-3">
|
||||
<button
|
||||
on:click|preventDefault={() => switchTeam(team.id)}
|
||||
class:bg-fuchsia-600={$appSession.teamId !== team.id}
|
||||
class:hover:bg-fuchsia-500={$appSession.teamId !== team.id}
|
||||
class:bg-transparent={$appSession.teamId === team.id}
|
||||
disabled={$appSession.teamId === team.id}
|
||||
>{$appSession.teamId === team.id ? 'Current Team' : 'Switch Team'}</button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
{#if $appSession.teamId === '0' && allTeams.length > 0}
|
||||
<div class="pb-5 pt-10 text-xl font-bold">Other Teams</div>
|
||||
<div class="flex flex-row flex-wrap justify-center px-2 md:flex-row">
|
||||
{#each allTeams as team}
|
||||
<a href="/iam/team/{team.id}" class="p-2 no-underline">
|
||||
<div
|
||||
class="box-selection relative"
|
||||
class:hover:bg-fuchsia-600={team.id !== '0'}
|
||||
class:hover:bg-red-500={team.id === '0'}
|
||||
>
|
||||
<div class="truncate text-center text-xl font-bold">
|
||||
{team.name}
|
||||
</div>
|
||||
|
||||
<div class="mt-1 text-center">{team.permissions?.length} member(s)</div>
|
||||
</div>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
{#if $appSession.teamId === '0' && allTeams.length > 0}
|
||||
<div class="pb-5 pt-10 text-xl font-bold">Other Teams</div>
|
||||
<div class="flex flex-row flex-wrap justify-center px-2 md:flex-row">
|
||||
{#each allTeams as team}
|
||||
<a href="/iam/team/{team.id}" class="w-96 p-2 no-underline">
|
||||
<div
|
||||
class="box-selection relative"
|
||||
class:hover:bg-fuchsia-600={team.id !== '0'}
|
||||
class:hover:bg-red-500={team.id === '0'}
|
||||
>
|
||||
<div class="truncate text-center text-xl font-bold">
|
||||
{team.name}
|
||||
</div>
|
||||
|
||||
<div class="mt-1 text-center">{team.permissions?.length} member(s)</div>
|
||||
</div>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -121,10 +121,10 @@
|
||||
<div class="flex space-x-2 h-8 items-center justify-center pt-8">
|
||||
<button
|
||||
type="submit"
|
||||
class="hover:bg-coollabs-100 text-white"
|
||||
class="text-white"
|
||||
disabled={loading}
|
||||
class:hover:bg-coollabs-100={!loading}
|
||||
class:bg-transparent={loading}
|
||||
class:text-stone-600={loading}
|
||||
class:bg-coollabs={!loading}
|
||||
>{loading ? $t('register.registering') : $t('register.register')}</button
|
||||
>
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
import { toast } from '@zerodevx/svelte-toast';
|
||||
|
||||
import { get, post } from '$lib/api';
|
||||
import { errorNotification } from '$lib/common';
|
||||
import { errorNotification, getDomain } from '$lib/common';
|
||||
import { t } from '$lib/translations';
|
||||
import { appSession, disabledButton, status, location, setLocation } from '$lib/store';
|
||||
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
|
||||
@@ -30,25 +30,63 @@
|
||||
import Moodle from './_Moodle.svelte';
|
||||
|
||||
const { id } = $page.params;
|
||||
$: isDisabled =
|
||||
!$appSession.isAdmin || $status.service.isRunning || $status.service.initialLoading;
|
||||
|
||||
let forceSave = false;
|
||||
let loading = false;
|
||||
let loadingVerification = false;
|
||||
let dualCerts = service.dualCerts;
|
||||
|
||||
let nonWWWDomain = service.fqdn && getDomain(service.fqdn).replace(/^www\./, '');
|
||||
let isNonWWWDomainOK = false;
|
||||
let isWWWDomainOK = false;
|
||||
|
||||
async function isDNSValid(domain: any, isWWW: any) {
|
||||
try {
|
||||
await get(`/services/${id}/check?domain=${domain}`);
|
||||
toast.push('DNS configuration is valid.');
|
||||
isWWW ? (isWWWDomainOK = true) : (isNonWWWDomainOK = true);
|
||||
return true;
|
||||
} catch (error) {
|
||||
errorNotification(error);
|
||||
isWWW ? (isWWWDomainOK = false) : (isNonWWWDomainOK = false);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
if (loading) return;
|
||||
loading = true;
|
||||
try {
|
||||
await post(`/services/${id}/check`, {
|
||||
fqdn: service.fqdn,
|
||||
forceSave,
|
||||
dualCerts,
|
||||
otherFqdns: service.minio?.apiFqdn ? [service.minio?.apiFqdn] : [],
|
||||
exposePort: service.exposePort
|
||||
});
|
||||
await post(`/services/${id}`, { ...service });
|
||||
setLocation(service)
|
||||
setLocation(service);
|
||||
$disabledButton = false;
|
||||
forceSave = false;
|
||||
toast.push('Configuration saved.');
|
||||
} catch (error) {
|
||||
//@ts-ignore
|
||||
if (error?.message.startsWith($t('application.dns_not_set_partial_error'))) {
|
||||
forceSave = true;
|
||||
if (dualCerts) {
|
||||
isNonWWWDomainOK = await isDNSValid(getDomain(nonWWWDomain), false);
|
||||
isWWWDomainOK = await isDNSValid(getDomain(`www.${nonWWWDomain}`), true);
|
||||
} else {
|
||||
const isWWW = getDomain(service.fqdn).includes('www.');
|
||||
if (isWWW) {
|
||||
isWWWDomainOK = await isDNSValid(getDomain(`www.${nonWWWDomain}`), true);
|
||||
} else {
|
||||
isNonWWWDomainOK = await isDNSValid(getDomain(nonWWWDomain), false);
|
||||
}
|
||||
}
|
||||
}
|
||||
return errorNotification(error);
|
||||
} finally {
|
||||
loading = false;
|
||||
@@ -102,14 +140,21 @@
|
||||
|
||||
<div class="mx-auto max-w-4xl px-6 pb-12">
|
||||
<form on:submit|preventDefault={handleSubmit} class="py-4">
|
||||
<div class="flex space-x-1 pb-5 font-bold">
|
||||
<div class="flex space-x-1 pb-5">
|
||||
<div class="title">{$t('general')}</div>
|
||||
{#if $appSession.isAdmin}
|
||||
<button
|
||||
type="submit"
|
||||
class:bg-pink-600={!loading}
|
||||
class:bg-orange-600={forceSave}
|
||||
class:hover:bg-pink-500={!loading}
|
||||
disabled={loading}>{loading ? $t('forms.saving') : $t('forms.save')}</button
|
||||
class:hover:bg-orange-400={forceSave}
|
||||
disabled={loading}
|
||||
>{loading
|
||||
? $t('forms.saving')
|
||||
: forceSave
|
||||
? $t('forms.confirm_continue')
|
||||
: $t('forms.save')}</button
|
||||
>
|
||||
{/if}
|
||||
{#if service.type === 'plausibleanalytics' && $status.service.isRunning}
|
||||
@@ -145,7 +190,7 @@
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="version" class="text-base font-bold text-stone-100">Version / Tag</label>
|
||||
<a
|
||||
href={$appSession.isAdmin && !$status.service.isRunning
|
||||
href={$appSession.isAdmin && !$status.service.isRunning && !$status.service.initialLoading
|
||||
? `/services/${id}/configuration/version?from=/services/${id}`
|
||||
: ''}
|
||||
class="no-underline"
|
||||
@@ -153,7 +198,8 @@
|
||||
<input
|
||||
value={service.version}
|
||||
id="service"
|
||||
disabled={$status.service.isRunning}
|
||||
readonly
|
||||
disabled={$status.service.isRunning || $status.service.initialLoading}
|
||||
class:cursor-pointer={!$status.service.isRunning}
|
||||
/></a
|
||||
>
|
||||
@@ -183,8 +229,8 @@
|
||||
|
||||
<CopyPasswordField
|
||||
placeholder="eg: https://console.min.io"
|
||||
readonly={!$appSession.isAdmin && !$status.service.isRunning}
|
||||
disabled={!$appSession.isAdmin || $status.service.isRunning}
|
||||
readonly={isDisabled}
|
||||
disabled={isDisabled}
|
||||
name="fqdn"
|
||||
id="fqdn"
|
||||
pattern="^https?://([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{'{'}2,{'}'}$"
|
||||
@@ -201,7 +247,7 @@
|
||||
<CopyPasswordField
|
||||
placeholder="eg: https://min.io"
|
||||
readonly={!$appSession.isAdmin && !$status.service.isRunning}
|
||||
disabled={!$appSession.isAdmin || $status.service.isRunning}
|
||||
disabled={isDisabled}
|
||||
name="apiFqdn"
|
||||
id="apiFqdn"
|
||||
pattern="^https?://([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{'{'}2,{'}'}$"
|
||||
@@ -221,7 +267,9 @@
|
||||
<CopyPasswordField
|
||||
placeholder="eg: https://analytics.coollabs.io"
|
||||
readonly={!$appSession.isAdmin && !$status.service.isRunning}
|
||||
disabled={!$appSession.isAdmin || $status.service.isRunning}
|
||||
disabled={!$appSession.isAdmin ||
|
||||
$status.service.isRunning ||
|
||||
$status.service.initialLoading}
|
||||
name="fqdn"
|
||||
id="fqdn"
|
||||
pattern="^https?://([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{'{'}2,{'}'}$"
|
||||
@@ -230,7 +278,38 @@
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if forceSave}
|
||||
<div class="flex-col space-y-2 pt-4 text-center">
|
||||
{#if isNonWWWDomainOK}
|
||||
<button
|
||||
class="bg-green-600 hover:bg-green-500"
|
||||
on:click|preventDefault={() => isDNSValid(getDomain(nonWWWDomain), false)}
|
||||
>DNS settings for {nonWWWDomain} is OK, click to recheck.</button
|
||||
>
|
||||
{:else}
|
||||
<button
|
||||
class="bg-red-600 hover:bg-red-500"
|
||||
on:click|preventDefault={() => isDNSValid(getDomain(nonWWWDomain), false)}
|
||||
>DNS settings for {nonWWWDomain} is invalid, click to recheck.</button
|
||||
>
|
||||
{/if}
|
||||
{#if dualCerts}
|
||||
{#if isWWWDomainOK}
|
||||
<button
|
||||
class="bg-green-600 hover:bg-green-500"
|
||||
on:click|preventDefault={() => isDNSValid(getDomain(`www.${nonWWWDomain}`), true)}
|
||||
>DNS settings for www.{nonWWWDomain} is OK, click to recheck.</button
|
||||
>
|
||||
{:else}
|
||||
<button
|
||||
class="bg-red-600 hover:bg-red-500"
|
||||
on:click|preventDefault={() => isDNSValid(getDomain(`www.${nonWWWDomain}`), true)}
|
||||
>DNS settings for www.{nonWWWDomain} is invalid, click to recheck.</button
|
||||
>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<Setting
|
||||
disabled={$status.service.isRunning}
|
||||
@@ -245,7 +324,9 @@
|
||||
<label for="exposePort" class="text-base font-bold text-stone-100">Exposed Port</label>
|
||||
<input
|
||||
readonly={!$appSession.isAdmin && !$status.service.isRunning}
|
||||
disabled={!$appSession.isAdmin || $status.service.isRunning}
|
||||
disabled={!$appSession.isAdmin ||
|
||||
$status.service.isRunning ||
|
||||
$status.service.initialLoading}
|
||||
name="exposePort"
|
||||
id="exposePort"
|
||||
bind:value={service.exposePort}
|
||||
|
||||
@@ -76,13 +76,13 @@
|
||||
<label for="extraConfig">{$t('forms.extra_config')}</label>
|
||||
<textarea
|
||||
bind:value={service.wordpress.extraConfig}
|
||||
disabled={$status.service.isRunning}
|
||||
disabled={$status.service.isRunning || $status.service.initialLoading}
|
||||
readonly={$status.service.isRunning}
|
||||
class:resize-none={$status.service.isRunning}
|
||||
rows="5"
|
||||
name="extraConfig"
|
||||
id="extraConfig"
|
||||
placeholder={!$status.service.isRunning
|
||||
placeholder={!$status.service.isRunning && !$status.service.initialLoading
|
||||
? `${$t('forms.eg')}:
|
||||
|
||||
define('WP_ALLOW_MULTISITE', true);
|
||||
@@ -112,7 +112,14 @@ define('SUBDOMAIN_INSTALL', false);`
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center px-10">
|
||||
<label for="ftpPassword">Password</label>
|
||||
<CopyPasswordField id="ftpPassword" isPasswordField readonly disabled name="ftpPassword" value={ftpPassword} />
|
||||
<CopyPasswordField
|
||||
id="ftpPassword"
|
||||
isPasswordField
|
||||
readonly
|
||||
disabled
|
||||
name="ftpPassword"
|
||||
value={ftpPassword}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex space-x-1 py-5 font-bold">
|
||||
|
||||
@@ -120,8 +120,9 @@
|
||||
async function getStatus() {
|
||||
if ($status.service.loading) return;
|
||||
$status.service.loading = true;
|
||||
const data = await get(`/services/${id}`);
|
||||
const data = await get(`/services/${id}/status`);
|
||||
$status.service.isRunning = data.isRunning;
|
||||
$status.service.isExited = data.isExited;
|
||||
$status.service.initialLoading = false;
|
||||
$status.service.loading = false;
|
||||
}
|
||||
@@ -173,6 +174,32 @@
|
||||
>
|
||||
<div class="border border-stone-700 h-8" />
|
||||
{/if}
|
||||
{#if $status.service.isExited}
|
||||
<a
|
||||
href={!$disabledButton ? `/services/${id}/logs` : null}
|
||||
class=" icons bg-transparent tooltip-bottom text-sm flex items-center text-red-500 tooltip-red-500"
|
||||
data-tooltip="Service exited with an error!"
|
||||
sveltekit:prefetch
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="w-6 h-6"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentcolor"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path
|
||||
d="M8.7 3h6.6c.3 0 .5 .1 .7 .3l4.7 4.7c.2 .2 .3 .4 .3 .7v6.6c0 .3 -.1 .5 -.3 .7l-4.7 4.7c-.2 .2 -.4 .3 -.7 .3h-6.6c-.3 0 -.5 -.1 -.7 -.3l-4.7 -4.7c-.2 -.2 -.3 -.4 -.3 -.7v-6.6c0 -.3 .1 -.5 .3 -.7l4.7 -4.7c.2 -.2 .4 -.3 .7 -.3z"
|
||||
/>
|
||||
<line x1="12" y1="8" x2="12" y2="12" />
|
||||
<line x1="12" y1="16" x2="12.01" y2="16" />
|
||||
</svg>
|
||||
</a>
|
||||
{/if}
|
||||
{#if $status.service.initialLoading}
|
||||
<button
|
||||
class="icons tooltip-bottom flex animate-spin items-center space-x-2 bg-transparent text-sm duration-500 ease-in-out"
|
||||
@@ -347,7 +374,7 @@
|
||||
>
|
||||
<div class="border border-stone-700 h-8" />
|
||||
<a
|
||||
href={$status.service.isRunning ? `/services/${id}/logs` : null}
|
||||
href={!$disabledButton && $status.service.isRunning ? `/services/${id}/logs` : null}
|
||||
sveltekit:prefetch
|
||||
class="hover:text-pink-500 rounded"
|
||||
class:text-pink-500={$page.url.pathname === `/services/${id}/logs`}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
redirect: `/services/${params.id}`
|
||||
};
|
||||
}
|
||||
const response = await get(`/destinations`);
|
||||
const response = await get(`/destinations?onlyVerified=true`);
|
||||
return {
|
||||
props: {
|
||||
...response
|
||||
@@ -54,7 +54,7 @@
|
||||
<div class="flex justify-center">
|
||||
{#if !destinations || destinations.length === 0}
|
||||
<div class="flex-col">
|
||||
<div class="pb-2">{$t('application.configuration.no_configurable_destination')}</div>
|
||||
<div class="pb-2 text-center font-bold">{$t('application.configuration.no_configurable_destination')}</div>
|
||||
<div class="flex justify-center">
|
||||
<a href="/new/destination" sveltekit:prefetch class="add-icon bg-sky-600 hover:bg-sky-500">
|
||||
<svg
|
||||
|
||||
@@ -1,32 +1,13 @@
|
||||
<script context="module" lang="ts">
|
||||
import type { Load } from '@sveltejs/kit';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
export const load: Load = async ({ fetch, params, url, stuff }) => {
|
||||
try {
|
||||
const response = await get(`/services/${params.id}/logs`);
|
||||
return {
|
||||
props: {
|
||||
service: stuff.service,
|
||||
...response
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
status: 500,
|
||||
error: new Error(`Could not load ${url}`)
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export let service: any;
|
||||
import { page } from '$app/stores';
|
||||
import LoadingLogs from './_Loading.svelte';
|
||||
import { get } from '$lib/api';
|
||||
import { t } from '$lib/translations';
|
||||
import { errorNotification } from '$lib/common';
|
||||
import { errorNotification } from '$lib/common';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
|
||||
let service: any = {};
|
||||
let logsLoading = false;
|
||||
let loadLogsInterval: any = null;
|
||||
let logs: any = [];
|
||||
let lastLog: any = null;
|
||||
@@ -36,18 +17,23 @@ import { errorNotification } from '$lib/common';
|
||||
let position = 0;
|
||||
|
||||
const { id } = $page.params;
|
||||
|
||||
onMount(async () => {
|
||||
const response = await get(`/services/${id}`);
|
||||
service = response.service;
|
||||
loadAllLogs();
|
||||
loadLogsInterval = setInterval(() => {
|
||||
loadLogs();
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
clearInterval(loadLogsInterval);
|
||||
clearInterval(followingInterval);
|
||||
});
|
||||
async function loadAllLogs() {
|
||||
try {
|
||||
logsLoading = true;
|
||||
const data: any = await get(`/services/${id}/logs`);
|
||||
if (data?.logs) {
|
||||
lastLog = data.logs[data.logs.length - 1];
|
||||
@@ -56,13 +42,14 @@ import { errorNotification } from '$lib/common';
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return errorNotification(error);
|
||||
} finally {
|
||||
logsLoading = false;
|
||||
}
|
||||
}
|
||||
async function loadLogs() {
|
||||
if (logsLoading) return;
|
||||
try {
|
||||
const newLogs: any = await get(
|
||||
`/services/${id}/logs?since=${lastLog?.split(' ')[0] || 0}`
|
||||
);
|
||||
const newLogs: any = await get(`/services/${id}/logs?since=${lastLog?.split(' ')[0] || 0}`);
|
||||
|
||||
if (newLogs?.logs && newLogs.logs[newLogs.logs.length - 1] !== logs[logs.length - 1]) {
|
||||
logs = logs.concat(newLogs.logs);
|
||||
@@ -137,7 +124,7 @@ import { errorNotification } from '$lib/common';
|
||||
{#if loadLogsInterval}
|
||||
<LoadingLogs />
|
||||
{/if}
|
||||
<div class="flex justify-end sticky top-0 p-2 mx-1">
|
||||
<div class="flex justify-end sticky top-0 p-1 mx-1">
|
||||
<button
|
||||
on:click={followBuild}
|
||||
class="bg-transparent"
|
||||
|
||||
@@ -73,7 +73,7 @@
|
||||
<div class="flex flex-col">
|
||||
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row">
|
||||
{#each ownServices as service}
|
||||
<a href="/services/{service.id}" class="w-96 p-2 no-underline">
|
||||
<a href="/services/{service.id}" class=" p-2 no-underline">
|
||||
<div class="box-selection group relative hover:bg-pink-600">
|
||||
<Services type={service.type} />
|
||||
<div class="truncate text-center text-xl font-bold">
|
||||
@@ -85,6 +85,9 @@
|
||||
{#if service.fqdn}
|
||||
<div class="truncate text-center">{getDomain(service.fqdn) || ''}</div>
|
||||
{/if}
|
||||
{#if service.destinationDocker?.name}
|
||||
<div class="truncate text-center">{service.destinationDocker.name}</div>
|
||||
{/if}
|
||||
{#if !service.type || !service.fqdn}
|
||||
<div class="truncate text-center font-bold text-red-500 group-hover:text-white">
|
||||
{$t('application.configuration.configuration_missing')}
|
||||
@@ -95,10 +98,10 @@
|
||||
{/each}
|
||||
</div>
|
||||
{#if otherServices.length > 0 && $appSession.teamId === '0'}
|
||||
<div class="px-6 pb-5 pt-10 text-xl font-bold">Other Services</div>
|
||||
<div class="px-6 pb-5 pt-10 text-2xl font-bold text-center">Other Services</div>
|
||||
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row">
|
||||
{#each otherServices as service}
|
||||
<a href="/services/{service.id}" class="w-96 p-2 no-underline">
|
||||
<a href="/services/{service.id}" class="p-2 no-underline">
|
||||
<div class="box-selection group relative hover:bg-pink-600">
|
||||
<Services type={service.type} />
|
||||
<div class="truncate text-center text-xl font-bold">
|
||||
|
||||
21
apps/ui/src/routes/settings/_Menu.svelte
Normal file
21
apps/ui/src/routes/settings/_Menu.svelte
Normal file
@@ -0,0 +1,21 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { appSession } from '$lib/store';
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col pt-4 space-y-6 w-96 px-20">
|
||||
{#if $appSession.teamId === '0'}
|
||||
<a
|
||||
href="/settings/global"
|
||||
class="sub-menu no-underline"
|
||||
class:sub-menu-active={$page.routeId === 'settings/global'}
|
||||
>
|
||||
Global Settings
|
||||
</a>
|
||||
{/if}
|
||||
<a
|
||||
href="/settings/ssh-keys"
|
||||
class="sub-menu no-underline"
|
||||
class:sub-menu-active={$page.routeId === 'settings/ssh-keys'}>SSH Keys</a
|
||||
>
|
||||
</div>
|
||||
23
apps/ui/src/routes/settings/__layout.svelte
Normal file
23
apps/ui/src/routes/settings/__layout.svelte
Normal file
@@ -0,0 +1,23 @@
|
||||
<script context="module" lang="ts">
|
||||
import { get } from '$lib/api';
|
||||
|
||||
import type { Load } from '@sveltejs/kit';
|
||||
export const load: Load = async () => {
|
||||
try {
|
||||
const response = await get(`/settings`);
|
||||
return {
|
||||
stuff: {
|
||||
...response
|
||||
}
|
||||
};
|
||||
} catch (error: any) {
|
||||
return {
|
||||
status: 500,
|
||||
error: new Error(error)
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
<slot />
|
||||
302
apps/ui/src/routes/settings/global.svelte
Normal file
302
apps/ui/src/routes/settings/global.svelte
Normal file
@@ -0,0 +1,302 @@
|
||||
<script context="module" lang="ts">
|
||||
import type { Load } from '@sveltejs/kit';
|
||||
export const load: Load = async ({ stuff }) => {
|
||||
try {
|
||||
return {
|
||||
props: {
|
||||
...stuff
|
||||
}
|
||||
};
|
||||
} catch (error: any) {
|
||||
return {
|
||||
status: 500,
|
||||
error: new Error(error)
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export let settings: any;
|
||||
import Setting from '$lib/components/Setting.svelte';
|
||||
import Explainer from '$lib/components/Explainer.svelte';
|
||||
import { del, get, post } from '$lib/api';
|
||||
import { browser } from '$app/env';
|
||||
import { toast } from '@zerodevx/svelte-toast';
|
||||
import { t } from '$lib/translations';
|
||||
import { appSession, features } from '$lib/store';
|
||||
import { errorNotification, getDomain } from '$lib/common';
|
||||
import Menu from './_Menu.svelte';
|
||||
|
||||
let isRegistrationEnabled = settings.isRegistrationEnabled;
|
||||
let dualCerts = settings.dualCerts;
|
||||
let isAutoUpdateEnabled = settings.isAutoUpdateEnabled;
|
||||
let isDNSCheckEnabled = settings.isDNSCheckEnabled;
|
||||
|
||||
let minPort = settings.minPort;
|
||||
let maxPort = settings.maxPort;
|
||||
|
||||
let forceSave = false;
|
||||
let fqdn = settings.fqdn;
|
||||
let nonWWWDomain = fqdn && getDomain(fqdn).replace(/^www\./, '');
|
||||
let isNonWWWDomainOK = false;
|
||||
let isWWWDomainOK = false;
|
||||
let isFqdnSet = !!settings.fqdn;
|
||||
let loading = {
|
||||
save: false,
|
||||
remove: false,
|
||||
proxyMigration: false
|
||||
};
|
||||
|
||||
async function removeFqdn() {
|
||||
if (fqdn) {
|
||||
loading.remove = true;
|
||||
try {
|
||||
const { redirect } = await del(`/settings`, { fqdn });
|
||||
return redirect ? window.location.replace(redirect) : window.location.reload();
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
} finally {
|
||||
loading.remove = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
async function changeSettings(name: any) {
|
||||
try {
|
||||
resetView();
|
||||
if (name === 'isRegistrationEnabled') {
|
||||
isRegistrationEnabled = !isRegistrationEnabled;
|
||||
}
|
||||
if (name === 'dualCerts') {
|
||||
dualCerts = !dualCerts;
|
||||
}
|
||||
if (name === 'isAutoUpdateEnabled') {
|
||||
isAutoUpdateEnabled = !isAutoUpdateEnabled;
|
||||
}
|
||||
if (name === 'isDNSCheckEnabled') {
|
||||
isDNSCheckEnabled = !isDNSCheckEnabled;
|
||||
}
|
||||
|
||||
await post(`/settings`, {
|
||||
isRegistrationEnabled,
|
||||
dualCerts,
|
||||
isAutoUpdateEnabled,
|
||||
isDNSCheckEnabled
|
||||
});
|
||||
return toast.push(t.get('application.settings_saved'));
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
loading.save = true;
|
||||
nonWWWDomain = fqdn && getDomain(fqdn).replace(/^www\./, '');
|
||||
|
||||
if (fqdn !== settings.fqdn) {
|
||||
await post(`/settings/check`, { fqdn, forceSave, dualCerts, isDNSCheckEnabled });
|
||||
await post(`/settings`, { fqdn });
|
||||
return window.location.reload();
|
||||
}
|
||||
if (minPort !== settings.minPort || maxPort !== settings.maxPort) {
|
||||
await post(`/settings`, { minPort, maxPort });
|
||||
settings.minPort = minPort;
|
||||
settings.maxPort = maxPort;
|
||||
}
|
||||
forceSave = false;
|
||||
toast.push('Configuration saved.');
|
||||
} catch (error) {
|
||||
if (error.message?.startsWith($t('application.dns_not_set_partial_error'))) {
|
||||
forceSave = true;
|
||||
if (dualCerts) {
|
||||
isNonWWWDomainOK = await isDNSValid(getDomain(nonWWWDomain), false);
|
||||
isWWWDomainOK = await isDNSValid(getDomain(`www.${nonWWWDomain}`), true);
|
||||
} else {
|
||||
const isWWW = getDomain(settings.fqdn).includes('www.');
|
||||
if (isWWW) {
|
||||
isWWWDomainOK = await isDNSValid(getDomain(`www.${nonWWWDomain}`), true);
|
||||
} else {
|
||||
isNonWWWDomainOK = await isDNSValid(getDomain(nonWWWDomain), false);
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log(error);
|
||||
return errorNotification(error);
|
||||
} finally {
|
||||
loading.save = false;
|
||||
}
|
||||
}
|
||||
async function isDNSValid(domain: any, isWWW: any) {
|
||||
try {
|
||||
await get(`/settings/check?domain=${domain}`);
|
||||
toast.push('DNS configuration is valid.');
|
||||
isWWW ? (isWWWDomainOK = true) : (isNonWWWDomainOK = true);
|
||||
return true;
|
||||
} catch (error) {
|
||||
errorNotification(error);
|
||||
isWWW ? (isWWWDomainOK = false) : (isNonWWWDomainOK = false);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
function resetView() {
|
||||
forceSave = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex space-x-1 p-6 font-bold">
|
||||
<div class="mr-4 text-2xl tracking-tight">{$t('index.settings')}</div>
|
||||
</div>
|
||||
<div class="mx-auto w-full">
|
||||
<div class="flex flex-row">
|
||||
<Menu />
|
||||
<div>
|
||||
<form on:submit|preventDefault={handleSubmit} class="grid grid-flow-row gap-2 py-4">
|
||||
<div class="flex space-x-1 pb-6">
|
||||
<div class="title font-bold">{$t('index.global_settings')}</div>
|
||||
<button
|
||||
type="submit"
|
||||
class:bg-yellow-500={!loading.save}
|
||||
class:bg-orange-600={forceSave}
|
||||
class:hover:bg-yellow-500={!loading.save}
|
||||
class:hover:bg-orange-400={forceSave}
|
||||
disabled={loading.save}
|
||||
>{loading.save
|
||||
? $t('forms.saving')
|
||||
: forceSave
|
||||
? $t('forms.confirm_continue')
|
||||
: $t('forms.save')}</button
|
||||
>
|
||||
|
||||
{#if isFqdnSet}
|
||||
<button
|
||||
on:click|preventDefault={removeFqdn}
|
||||
disabled={loading.remove}
|
||||
class:bg-red-600={!loading.remove}
|
||||
class:hover:bg-red-500={!loading.remove}
|
||||
>{loading.remove ? $t('forms.removing') : $t('forms.remove_domain')}</button
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="grid grid-flow-row gap-2 px-10">
|
||||
<!-- <Language /> -->
|
||||
<div class="grid grid-cols-2 items-start">
|
||||
<div class="flex-col">
|
||||
<div class="pt-2 text-base font-bold text-stone-100">
|
||||
{$t('application.url_fqdn')}
|
||||
</div>
|
||||
<Explainer text={$t('setting.ssl_explainer')} />
|
||||
</div>
|
||||
<div class="justify-start text-left">
|
||||
<input
|
||||
bind:value={fqdn}
|
||||
readonly={!$appSession.isAdmin || isFqdnSet}
|
||||
disabled={!$appSession.isAdmin || isFqdnSet}
|
||||
on:input={resetView}
|
||||
name="fqdn"
|
||||
id="fqdn"
|
||||
pattern="^https?://([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{'{'}2,{'}'}$"
|
||||
placeholder="{$t('forms.eg')}: https://coolify.io"
|
||||
/>
|
||||
|
||||
{#if forceSave}
|
||||
<div class="flex-col space-y-2 pt-4 text-center">
|
||||
{#if isNonWWWDomainOK}
|
||||
<button
|
||||
class="bg-green-600 hover:bg-green-500"
|
||||
on:click|preventDefault={() => isDNSValid(getDomain(nonWWWDomain), false)}
|
||||
>DNS settings for {nonWWWDomain} is OK, click to recheck.</button
|
||||
>
|
||||
{:else}
|
||||
<button
|
||||
class="bg-red-600 hover:bg-red-500"
|
||||
on:click|preventDefault={() => isDNSValid(getDomain(nonWWWDomain), false)}
|
||||
>DNS settings for {nonWWWDomain} is invalid, click to recheck.</button
|
||||
>
|
||||
{/if}
|
||||
{#if dualCerts}
|
||||
{#if isWWWDomainOK}
|
||||
<button
|
||||
class="bg-green-600 hover:bg-green-500"
|
||||
on:click|preventDefault={() =>
|
||||
isDNSValid(getDomain(`www.${nonWWWDomain}`), true)}
|
||||
>DNS settings for www.{nonWWWDomain} is OK, click to recheck.</button
|
||||
>
|
||||
{:else}
|
||||
<button
|
||||
class="bg-red-600 hover:bg-red-500"
|
||||
on:click|preventDefault={() =>
|
||||
isDNSValid(getDomain(`www.${nonWWWDomain}`), true)}
|
||||
>DNS settings for www.{nonWWWDomain} is invalid, click to recheck.</button
|
||||
>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-start py-6">
|
||||
<div class="flex-col">
|
||||
<div class="pt-2 text-base font-bold text-stone-100">
|
||||
{$t('forms.public_port_range')}
|
||||
</div>
|
||||
<Explainer text={$t('forms.public_port_range_explainer')} />
|
||||
</div>
|
||||
<div class="mx-auto flex-row items-center justify-center space-y-2">
|
||||
<input
|
||||
class="h-8 w-20 px-2"
|
||||
type="number"
|
||||
bind:value={minPort}
|
||||
min="1024"
|
||||
max={maxPort}
|
||||
/>
|
||||
-
|
||||
<input
|
||||
class="h-8 w-20 px-2"
|
||||
type="number"
|
||||
bind:value={maxPort}
|
||||
min={minPort}
|
||||
max="65543"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<Setting
|
||||
bind:setting={isDNSCheckEnabled}
|
||||
title={$t('setting.is_dns_check_enabled')}
|
||||
description={$t('setting.is_dns_check_enabled_explainer')}
|
||||
on:click={() => changeSettings('isDNSCheckEnabled')}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<Setting
|
||||
dataTooltip={$t('setting.must_remove_domain_before_changing')}
|
||||
disabled={isFqdnSet}
|
||||
bind:setting={dualCerts}
|
||||
title={$t('application.ssl_www_and_non_www')}
|
||||
description={$t('setting.generate_www_non_www_ssl')}
|
||||
on:click={() => !isFqdnSet && changeSettings('dualCerts')}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<Setting
|
||||
bind:setting={isRegistrationEnabled}
|
||||
title={$t('setting.registration_allowed')}
|
||||
description={$t('setting.registration_allowed_explainer')}
|
||||
on:click={() => changeSettings('isRegistrationEnabled')}
|
||||
/>
|
||||
</div>
|
||||
{#if browser && $features.beta}
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<Setting
|
||||
bind:setting={isAutoUpdateEnabled}
|
||||
title={$t('setting.auto_update_enabled')}
|
||||
description={$t('setting.auto_update_enabled_explainer')}
|
||||
on:click={() => changeSettings('isAutoUpdateEnabled')}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,357 +1,8 @@
|
||||
<script context="module" lang="ts">
|
||||
import type { Load } from '@sveltejs/kit';
|
||||
export const load: Load = async () => {
|
||||
try {
|
||||
const response = await get(`/settings`);
|
||||
return {
|
||||
props: {
|
||||
...response
|
||||
}
|
||||
};
|
||||
} catch (error: any) {
|
||||
return {
|
||||
status: 500,
|
||||
error: new Error(error)
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export let settings: any;
|
||||
|
||||
import Setting from '$lib/components/Setting.svelte';
|
||||
import Explainer from '$lib/components/Explainer.svelte';
|
||||
import { del, get, post } from '$lib/api';
|
||||
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
|
||||
import { browser } from '$app/env';
|
||||
import { toast } from '@zerodevx/svelte-toast';
|
||||
import { t } from '$lib/translations';
|
||||
import { appSession, features, isTraefikUsed } from '$lib/store';
|
||||
import { errorNotification, getDomain } from '$lib/common';
|
||||
|
||||
let isRegistrationEnabled = settings.isRegistrationEnabled;
|
||||
let dualCerts = settings.dualCerts;
|
||||
let isAutoUpdateEnabled = settings.isAutoUpdateEnabled;
|
||||
let isDNSCheckEnabled = settings.isDNSCheckEnabled;
|
||||
$isTraefikUsed = settings.isTraefikUsed;
|
||||
|
||||
let minPort = settings.minPort;
|
||||
let maxPort = settings.maxPort;
|
||||
|
||||
let forceSave = false;
|
||||
let fqdn = settings.fqdn;
|
||||
let nonWWWDomain = fqdn && getDomain(fqdn).replace(/^www\./, '');
|
||||
let isNonWWWDomainOK = false;
|
||||
let isWWWDomainOK = false;
|
||||
let isFqdnSet = !!settings.fqdn;
|
||||
let loading = {
|
||||
save: false,
|
||||
remove: false,
|
||||
proxyMigration: false
|
||||
};
|
||||
|
||||
async function removeFqdn() {
|
||||
if (fqdn) {
|
||||
loading.remove = true;
|
||||
try {
|
||||
const { redirect } = await del(`/settings`, { fqdn });
|
||||
return redirect ? window.location.replace(redirect) : window.location.reload();
|
||||
} catch (error ) {
|
||||
return errorNotification(error);
|
||||
} finally {
|
||||
loading.remove = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
async function changeSettings(name: any) {
|
||||
try {
|
||||
resetView();
|
||||
if (name === 'isRegistrationEnabled') {
|
||||
isRegistrationEnabled = !isRegistrationEnabled;
|
||||
}
|
||||
if (name === 'dualCerts') {
|
||||
dualCerts = !dualCerts;
|
||||
}
|
||||
if (name === 'isAutoUpdateEnabled') {
|
||||
isAutoUpdateEnabled = !isAutoUpdateEnabled;
|
||||
}
|
||||
if (name === 'isDNSCheckEnabled') {
|
||||
isDNSCheckEnabled = !isDNSCheckEnabled;
|
||||
}
|
||||
|
||||
await post(`/settings`, {
|
||||
isRegistrationEnabled,
|
||||
dualCerts,
|
||||
isAutoUpdateEnabled,
|
||||
isDNSCheckEnabled
|
||||
});
|
||||
return toast.push(t.get('application.settings_saved'));
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
loading.save = true;
|
||||
nonWWWDomain = fqdn && getDomain(fqdn).replace(/^www\./, '');
|
||||
|
||||
if (fqdn !== settings.fqdn) {
|
||||
await post(`/settings/check`, { fqdn, forceSave, dualCerts, isDNSCheckEnabled });
|
||||
await post(`/settings`, { fqdn });
|
||||
return window.location.reload();
|
||||
}
|
||||
if (minPort !== settings.minPort || maxPort !== settings.maxPort) {
|
||||
await post(`/settings`, { minPort, maxPort });
|
||||
settings.minPort = minPort;
|
||||
settings.maxPort = maxPort;
|
||||
}
|
||||
forceSave = false;
|
||||
} catch (error ) {
|
||||
if (error.message?.startsWith($t('application.dns_not_set_partial_error'))) {
|
||||
forceSave = true;
|
||||
if (dualCerts) {
|
||||
isNonWWWDomainOK = await isDNSValid(getDomain(nonWWWDomain), false);
|
||||
isWWWDomainOK = await isDNSValid(getDomain(`www.${nonWWWDomain}`), true);
|
||||
} else {
|
||||
const isWWW = getDomain(settings.fqdn).includes('www.');
|
||||
if (isWWW) {
|
||||
isWWWDomainOK = await isDNSValid(getDomain(`www.${nonWWWDomain}`), true);
|
||||
} else {
|
||||
isNonWWWDomainOK = await isDNSValid(getDomain(nonWWWDomain), false);
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log(error)
|
||||
return errorNotification(error);
|
||||
} finally {
|
||||
loading.save = false;
|
||||
}
|
||||
}
|
||||
async function isDNSValid(domain: any, isWWW: any) {
|
||||
try {
|
||||
await get(`/settings/check?domain=${domain}`);
|
||||
toast.push('DNS configuration is valid.');
|
||||
isWWW ? (isWWWDomainOK = true) : (isNonWWWDomainOK = true);
|
||||
return true;
|
||||
} catch (error) {
|
||||
errorNotification(error);
|
||||
isWWW ? (isWWWDomainOK = false) : (isNonWWWDomainOK = false);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
function resetView() {
|
||||
forceSave = false;
|
||||
}
|
||||
async function migrateProxy(to: any) {
|
||||
if (loading.proxyMigration) return;
|
||||
try {
|
||||
loading.proxyMigration = true;
|
||||
await post(`/update`, { type: to });
|
||||
const data = await get(`/settings`);
|
||||
$isTraefikUsed = data.settings.isTraefikUsed;
|
||||
return toast.push('Proxy migration started, it takes a few seconds.');
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
} finally {
|
||||
loading.proxyMigration = false;
|
||||
}
|
||||
import { goto } from '$app/navigation';
|
||||
import { appSession } from '$lib/store';
|
||||
if ($appSession.teamId !== '0') {
|
||||
goto('/settings/ssh-keys');
|
||||
}
|
||||
goto('/settings/global');
|
||||
</script>
|
||||
|
||||
<div class="flex space-x-1 p-6 font-bold">
|
||||
<div class="mr-4 text-2xl tracking-tight">{$t('index.settings')}</div>
|
||||
</div>
|
||||
{#if $appSession.teamId === '0'}
|
||||
<div class="mx-auto max-w-4xl px-6">
|
||||
<form on:submit|preventDefault={handleSubmit} class="grid grid-flow-row gap-2 py-4">
|
||||
<div class="flex space-x-1 pb-6">
|
||||
<div class="title font-bold">{$t('index.global_settings')}</div>
|
||||
<button
|
||||
type="submit"
|
||||
class:bg-green-600={!loading.save}
|
||||
class:bg-orange-600={forceSave}
|
||||
class:hover:bg-green-500={!loading.save}
|
||||
class:hover:bg-orange-400={forceSave}
|
||||
disabled={loading.save}
|
||||
>{loading.save
|
||||
? $t('forms.saving')
|
||||
: forceSave
|
||||
? $t('forms.confirm_continue')
|
||||
: $t('forms.save')}</button
|
||||
>
|
||||
|
||||
{#if isFqdnSet}
|
||||
<button
|
||||
on:click|preventDefault={removeFqdn}
|
||||
disabled={loading.remove}
|
||||
class:bg-red-600={!loading.remove}
|
||||
class:hover:bg-red-500={!loading.remove}
|
||||
>{loading.remove ? $t('forms.removing') : $t('forms.remove_domain')}</button
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="grid grid-flow-row gap-2 px-10">
|
||||
<!-- <Language /> -->
|
||||
<div class="grid grid-cols-2 items-start">
|
||||
<div class="flex-col">
|
||||
<div class="pt-2 text-base font-bold text-stone-100">
|
||||
{$t('application.url_fqdn')}
|
||||
</div>
|
||||
<Explainer text={$t('setting.ssl_explainer')} />
|
||||
</div>
|
||||
<div class="justify-start text-left">
|
||||
<input
|
||||
bind:value={fqdn}
|
||||
readonly={!$appSession.isAdmin || isFqdnSet}
|
||||
disabled={!$appSession.isAdmin || isFqdnSet}
|
||||
on:input={resetView}
|
||||
name="fqdn"
|
||||
id="fqdn"
|
||||
pattern="^https?://([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{'{'}2,{'}'}$"
|
||||
placeholder="{$t('forms.eg')}: https://coolify.io"
|
||||
/>
|
||||
|
||||
{#if forceSave}
|
||||
<div class="flex-col space-y-2 pt-4 text-center">
|
||||
{#if isNonWWWDomainOK}
|
||||
<button
|
||||
class="bg-green-600 hover:bg-green-500"
|
||||
on:click|preventDefault={() => isDNSValid(getDomain(nonWWWDomain), false)}
|
||||
>DNS settings for {nonWWWDomain} is OK, click to recheck.</button
|
||||
>
|
||||
{:else}
|
||||
<button
|
||||
class="bg-red-600 hover:bg-red-500"
|
||||
on:click|preventDefault={() => isDNSValid(getDomain(nonWWWDomain), false)}
|
||||
>DNS settings for {nonWWWDomain} is invalid, click to recheck.</button
|
||||
>
|
||||
{/if}
|
||||
{#if dualCerts}
|
||||
{#if isWWWDomainOK}
|
||||
<button
|
||||
class="bg-green-600 hover:bg-green-500"
|
||||
on:click|preventDefault={() =>
|
||||
isDNSValid(getDomain(`www.${nonWWWDomain}`), true)}
|
||||
>DNS settings for www.{nonWWWDomain} is OK, click to recheck.</button
|
||||
>
|
||||
{:else}
|
||||
<button
|
||||
class="bg-red-600 hover:bg-red-500"
|
||||
on:click|preventDefault={() =>
|
||||
isDNSValid(getDomain(`www.${nonWWWDomain}`), true)}
|
||||
>DNS settings for www.{nonWWWDomain} is invalid, click to recheck.</button
|
||||
>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-start py-6">
|
||||
<div class="flex-col">
|
||||
<div class="pt-2 text-base font-bold text-stone-100">
|
||||
{$t('forms.public_port_range')}
|
||||
</div>
|
||||
<Explainer text={$t('forms.public_port_range_explainer')} />
|
||||
</div>
|
||||
<div class="mx-auto flex-row items-center justify-center space-y-2">
|
||||
<input
|
||||
class="h-8 w-20 px-2"
|
||||
type="number"
|
||||
bind:value={minPort}
|
||||
min="1024"
|
||||
max={maxPort}
|
||||
/>
|
||||
-
|
||||
<input
|
||||
class="h-8 w-20 px-2"
|
||||
type="number"
|
||||
bind:value={maxPort}
|
||||
min={minPort}
|
||||
max="65543"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<Setting
|
||||
bind:setting={isDNSCheckEnabled}
|
||||
title={$t('setting.is_dns_check_enabled')}
|
||||
description={$t('setting.is_dns_check_enabled_explainer')}
|
||||
on:click={() => changeSettings('isDNSCheckEnabled')}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<Setting
|
||||
dataTooltip={$t('setting.must_remove_domain_before_changing')}
|
||||
disabled={isFqdnSet}
|
||||
bind:setting={dualCerts}
|
||||
title={$t('application.ssl_www_and_non_www')}
|
||||
description={$t('setting.generate_www_non_www_ssl')}
|
||||
on:click={() => !isFqdnSet && changeSettings('dualCerts')}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<Setting
|
||||
bind:setting={isRegistrationEnabled}
|
||||
title={$t('setting.registration_allowed')}
|
||||
description={$t('setting.registration_allowed_explainer')}
|
||||
on:click={() => changeSettings('isRegistrationEnabled')}
|
||||
/>
|
||||
</div>
|
||||
{#if browser && $features.beta}
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<Setting
|
||||
bind:setting={isAutoUpdateEnabled}
|
||||
title={$t('setting.auto_update_enabled')}
|
||||
description={$t('setting.auto_update_enabled_explainer')}
|
||||
on:click={() => changeSettings('isAutoUpdateEnabled')}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</form>
|
||||
{#if !settings.isTraefikUsed}
|
||||
<div class="flex space-x-1 pt-6 font-bold">
|
||||
<div class="title">{$t('setting.coolify_proxy_settings')}</div>
|
||||
</div>
|
||||
<Explainer
|
||||
text={$t('setting.credential_stat_explainer', {
|
||||
link: fqdn
|
||||
? `http://${settings.proxyUser}:${settings.proxyPassword}@` + getDomain(fqdn) + ':8404'
|
||||
: browser &&
|
||||
`http://${settings.proxyUser}:${settings.proxyPassword}@` +
|
||||
window.location.hostname +
|
||||
':8404'
|
||||
})}
|
||||
/>
|
||||
<div class="space-y-2 px-10 py-5">
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<label for="proxyUser">{$t('forms.user')}</label>
|
||||
<CopyPasswordField
|
||||
readonly
|
||||
disabled
|
||||
id="proxyUser"
|
||||
name="proxyUser"
|
||||
value={settings.proxyUser}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<label for="proxyPassword">{$t('forms.password')}</label>
|
||||
<CopyPasswordField
|
||||
readonly
|
||||
disabled
|
||||
id="proxyPassword"
|
||||
name="proxyPassword"
|
||||
isPasswordField
|
||||
value={settings.proxyPassword}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="mx-auto max-w-4xl px-6">
|
||||
<!-- <Language /> -->
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
150
apps/ui/src/routes/settings/ssh-keys.svelte
Normal file
150
apps/ui/src/routes/settings/ssh-keys.svelte
Normal file
@@ -0,0 +1,150 @@
|
||||
<script context="module" lang="ts">
|
||||
import type { Load } from '@sveltejs/kit';
|
||||
export const load: Load = async ({ stuff }) => {
|
||||
try {
|
||||
return {
|
||||
props: {
|
||||
...stuff
|
||||
}
|
||||
};
|
||||
} catch (error: any) {
|
||||
return {
|
||||
status: 500,
|
||||
error: new Error(error)
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export let sshKeys: any;
|
||||
import { del, post } from '$lib/api';
|
||||
import { toast } from '@zerodevx/svelte-toast';
|
||||
import { t } from '$lib/translations';
|
||||
import { errorNotification } from '$lib/common';
|
||||
import Menu from './_Menu.svelte';
|
||||
|
||||
let loading = {
|
||||
save: false
|
||||
};
|
||||
let isModalActive = false;
|
||||
|
||||
let newSSHKey = {
|
||||
name: null,
|
||||
privateKey: null
|
||||
};
|
||||
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
await post(`/settings/sshKey`, { ...newSSHKey });
|
||||
return window.location.reload();
|
||||
} catch (error) {
|
||||
errorNotification(error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
async function deleteSSHKey(id: string) {
|
||||
const sure = confirm('Are you sure you would like to delete this SSH key?');
|
||||
if (sure) {
|
||||
try {
|
||||
if (!id) return;
|
||||
await del(`/settings/sshKey`, { id });
|
||||
return window.location.reload();
|
||||
} catch (error) {
|
||||
errorNotification(error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex space-x-1 p-6 font-bold">
|
||||
<div class="mr-4 text-2xl tracking-tight">{$t('index.settings')}</div>
|
||||
</div>
|
||||
<div class="mx-auto w-full">
|
||||
<div class="flex flex-row">
|
||||
<Menu />
|
||||
<div class="grid grid-flow-row gap-2 py-4">
|
||||
<div class="flex space-x-1 pb-6">
|
||||
<div class="title font-bold">SSH Keys</div>
|
||||
<button
|
||||
on:click={() => (isModalActive = true)}
|
||||
class:bg-yellow-500={!loading.save}
|
||||
class:hover:bg-yellow-400={!loading.save}
|
||||
disabled={loading.save}>New SSH Key</button
|
||||
>
|
||||
</div>
|
||||
<div class="grid grid-flow-col gap-2 px-10">
|
||||
{#if sshKeys.length === 0}
|
||||
<div class="text-sm ">No SSH keys found</div>
|
||||
{:else}
|
||||
{#each sshKeys as key}
|
||||
<div class="box-selection group relative">
|
||||
<div class="text-xl font-bold">{key.name}</div>
|
||||
<div class="py-3 text-stone-600">Added on {key.createdAt}</div>
|
||||
<button on:click={() => deleteSSHKey(key.id)} class="bg-red-500">Delete</button>
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{#if isModalActive}
|
||||
<div class="relative z-10" aria-labelledby="modal-title" role="dialog" aria-modal="true">
|
||||
<div class="fixed inset-0 bg-coolgray-500 bg-opacity-75 transition-opacity" />
|
||||
<div class="fixed z-10 inset-0 overflow-y-auto text-white">
|
||||
<div class="flex items-end sm:items-center justify-center min-h-full p-4 text-center sm:p-0">
|
||||
<form
|
||||
on:submit|preventDefault={handleSubmit}
|
||||
class="relative bg-coolblack rounded-lg px-4 pt-5 pb-4 text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:max-w-lg sm:w-full sm:p-6 border border-coolgray-500"
|
||||
>
|
||||
<div class="hidden sm:block absolute top-0 right-0 pt-4 pr-4">
|
||||
<button
|
||||
on:click={() => (isModalActive = false)}
|
||||
type="button"
|
||||
class=" rounded-md text-white focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
||||
>
|
||||
<span class="sr-only">Close</span>
|
||||
<svg
|
||||
class="h-6 w-6"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
stroke="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="sm:flex sm:items-start">
|
||||
<div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
|
||||
<h3 class="text-lg leading-6 font-medium pb-4" id="modal-title">New SSH Key</h3>
|
||||
<div class="text-xs text-stone-400">Add an SSH key to your Coolify instance.</div>
|
||||
<div class="mt-2">
|
||||
<label for="privateKey" class="pb-2">Key</label>
|
||||
<textarea
|
||||
id="privateKey"
|
||||
required
|
||||
bind:value={newSSHKey.privateKey}
|
||||
class="w-full"
|
||||
rows={15}
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<label for="name" class="pb-2">Name</label>
|
||||
<input id="name" required bind:value={newSSHKey.name} class="w-full" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-5 flex space-x-4 justify-end">
|
||||
<button type="submit" class="bg-green-600 hover:bg-green-500">Save</button>
|
||||
<button on:click={() => (isModalActive = false)} type="button" class="">Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -66,11 +66,7 @@
|
||||
});
|
||||
const { organization, htmlUrl } = source;
|
||||
const { fqdn } = settings;
|
||||
const host = dev
|
||||
? getAPIUrl()
|
||||
: fqdn
|
||||
? fqdn
|
||||
: `http://${window.location.host}` || '';
|
||||
const host = dev ? getAPIUrl() : fqdn ? fqdn : `http://${window.location.host}` || '';
|
||||
const domain = getDomain(fqdn);
|
||||
|
||||
let url = 'settings/apps/new';
|
||||
@@ -80,9 +76,7 @@
|
||||
name: `coolify-${name}`,
|
||||
url: host,
|
||||
hook_attributes: {
|
||||
url: dev
|
||||
? getWebhookUrl('github')
|
||||
: `${host}/webhooks/github/events`
|
||||
url: dev ? getWebhookUrl('github') : `${host}/webhooks/github/events`
|
||||
},
|
||||
redirect_url: `${host}/webhooks/github`,
|
||||
callback_urls: [`${host}/login/github/app`],
|
||||
@@ -118,10 +112,10 @@
|
||||
<div class="mx-auto max-w-4xl px-6">
|
||||
{#if !source.githubAppId}
|
||||
<form on:submit|preventDefault={newGithubApp} class="py-4">
|
||||
<div class="flex space-x-1 pb-7 font-bold">
|
||||
<div class="flex space-x-1 pb-7">
|
||||
<div class="title">General</div>
|
||||
{#if !source.githubAppId}
|
||||
<button class="bg-orange-600" type="submit">Save</button>
|
||||
<button class="bg-orange-600 font-normal" type="submit">Save</button>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="grid grid-flow-row gap-2 px-10">
|
||||
@@ -173,7 +167,7 @@
|
||||
</form>
|
||||
{:else if source.githubApp?.installationId}
|
||||
<form on:submit|preventDefault={handleSubmit} class="py-4">
|
||||
<div class="flex space-x-1 pb-5 font-bold">
|
||||
<div class="flex space-x-1 pb-5 ">
|
||||
<div class="title">{$t('general')}</div>
|
||||
{#if $appSession.isAdmin}
|
||||
<button
|
||||
@@ -218,20 +212,24 @@
|
||||
bind:value={source.apiUrl}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<label for="customPort" class="text-base font-bold text-stone-100">Custom SSH Port</label>
|
||||
<input
|
||||
name="customPort"
|
||||
id="customPort"
|
||||
disabled={!selfHosted}
|
||||
readonly={!selfHosted}
|
||||
required
|
||||
value={source.customPort}
|
||||
/>
|
||||
<Explainer
|
||||
text="If you use a self-hosted version of Git, you can provide custom port for all the Git related actions."
|
||||
/>
|
||||
</div>
|
||||
{#if selfHosted}
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<label for="customPort" class="text-base font-bold text-stone-100"
|
||||
>Custom SSH Port</label
|
||||
>
|
||||
<input
|
||||
name="customPort"
|
||||
id="customPort"
|
||||
disabled={!selfHosted}
|
||||
readonly={!selfHosted}
|
||||
required
|
||||
value={source.customPort}
|
||||
/>
|
||||
<Explainer
|
||||
text="If you use a self-hosted version of Git, you can provide custom port for all the Git related actions."
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="grid grid-cols-2">
|
||||
<div class="flex flex-col">
|
||||
<label for="organization" class="pt-2 text-base font-bold text-stone-100"
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
appSecret: null
|
||||
};
|
||||
}
|
||||
$: selfHosted = source.htmlUrl !== 'https://gitlab.com' ;
|
||||
$: selfHosted = source.htmlUrl !== 'https://gitlab.com';
|
||||
|
||||
onMount(() => {
|
||||
oauthIdEl && oauthIdEl.focus();
|
||||
@@ -141,7 +141,7 @@
|
||||
|
||||
<div class="mx-auto max-w-4xl px-6">
|
||||
<form on:submit|preventDefault={handleSubmit} class="py-4">
|
||||
<div class="flex space-x-1 pb-7 font-bold">
|
||||
<div class="flex space-x-1 pb-7">
|
||||
<div class="title">General</div>
|
||||
{#if $appSession.isAdmin}
|
||||
<button
|
||||
@@ -228,20 +228,22 @@
|
||||
bind:value={source.apiUrl}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<label for="customPort" class="text-base font-bold text-stone-100">Custom SSH Port</label>
|
||||
<input
|
||||
name="customPort"
|
||||
id="customPort"
|
||||
disabled={!selfHosted}
|
||||
readonly={!selfHosted}
|
||||
required
|
||||
bind:value={source.customPort}
|
||||
/>
|
||||
<Explainer
|
||||
text="If you use a self-hosted version of Git, you can provide custom port for all the Git related actions."
|
||||
/>
|
||||
</div>
|
||||
{#if selfHosted}
|
||||
<div class="grid grid-cols-2 items-center">
|
||||
<label for="customPort" class="text-base font-bold text-stone-100">Custom SSH Port</label>
|
||||
<input
|
||||
name="customPort"
|
||||
id="customPort"
|
||||
disabled={!selfHosted}
|
||||
readonly={!selfHosted}
|
||||
required
|
||||
bind:value={source.customPort}
|
||||
/>
|
||||
<Explainer
|
||||
text="If you use a self-hosted version of Git, you can provide custom port for all the Git related actions."
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="grid grid-cols-2 items-start">
|
||||
<div class="flex-col">
|
||||
<label for="oauthId" class="pt-2 text-base font-bold text-stone-100"
|
||||
|
||||
@@ -66,7 +66,7 @@
|
||||
<div class="flex flex-col">
|
||||
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row">
|
||||
{#each ownSources as source}
|
||||
<a href="/sources/{source.id}" class="w-96 p-2 no-underline">
|
||||
<a href="/sources/{source.id}" class="p-2 no-underline">
|
||||
<div
|
||||
class="box-selection group relative hover:bg-orange-600"
|
||||
class:border-red-500={source.gitlabApp && !source.gitlabAppId}
|
||||
@@ -128,10 +128,10 @@
|
||||
</div>
|
||||
|
||||
{#if otherSources.length > 0 && $appSession.teamId === '0'}
|
||||
<div class="px-6 pb-5 pt-10 text-xl font-bold">Other Sources</div>
|
||||
<div class="px-6 pb-5 pt-10 text-2xl font-bold text-center">Other Sources</div>
|
||||
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row">
|
||||
{#each otherSources as source}
|
||||
<a href="/sources/{source.id}" class="w-96 p-2 no-underline">
|
||||
<a href="/sources/{source.id}" class="p-2 no-underline">
|
||||
<div
|
||||
class="box-selection group hover:bg-orange-600"
|
||||
class:border-red-500={source.gitlabApp && !source.gitlabAppId}
|
||||
|
||||
@@ -92,7 +92,7 @@ label {
|
||||
}
|
||||
|
||||
button, .button {
|
||||
@apply rounded bg-coolgray-200 p-1 px-2 py-1 text-xs font-bold outline-none transition-all duration-100 hover:bg-coolgray-500 disabled:cursor-not-allowed disabled:bg-coolblack disabled:text-stone-600;
|
||||
@apply rounded bg-coolgray-200 p-2 px-3 text-sm outline-none transition-all duration-100 hover:bg-coolgray-500 disabled:cursor-not-allowed disabled:bg-coolblack disabled:text-stone-600;
|
||||
}
|
||||
|
||||
a {
|
||||
@@ -104,7 +104,7 @@ a {
|
||||
}
|
||||
|
||||
.title {
|
||||
@apply mr-4 text-base tracking-tight md:text-2xl;
|
||||
@apply mr-4 text-base tracking-tight md:text-2xl font-bold;
|
||||
}
|
||||
.nav-main {
|
||||
@apply fixed top-0 left-0 min-h-screen w-16 min-w-[4rem] overflow-auto border-r border-stone-800 bg-coolgray-200 scrollbar-w-1 scrollbar-thumb-coollabs scrollbar-track-coolgray-200 xl:overflow-visible;
|
||||
@@ -395,3 +395,11 @@ a {
|
||||
transform: scale(0.9);
|
||||
}
|
||||
}
|
||||
|
||||
.sub-menu {
|
||||
@apply text-xl font-bold hover:bg-coolgray-500 rounded p-2 hover:text-white text-stone-200 cursor-pointer;
|
||||
}
|
||||
|
||||
.sub-menu-active {
|
||||
@apply bg-coolgray-500 text-white;
|
||||
}
|
||||
Reference in New Issue
Block a user