commit
cc2f83c4d9
2
.gitignore
vendored
2
.gitignore
vendored
@ -14,4 +14,4 @@ local-serve
|
|||||||
apps/api/db/migration.db-journal
|
apps/api/db/migration.db-journal
|
||||||
apps/api/core*
|
apps/api/core*
|
||||||
logs
|
logs
|
||||||
others/certificates
|
others/certificates
|
||||||
|
@ -53,71 +53,5 @@ Optional:
|
|||||||
- **Database ORM**: [Prisma.io](https://www.prisma.io/)
|
- **Database ORM**: [Prisma.io](https://www.prisma.io/)
|
||||||
- **Docker Engine API**
|
- **Docker Engine API**
|
||||||
|
|
||||||
## Add a new service
|
## How to add a new service?
|
||||||
### Which service is eligable to add to Coolify?
|
You can find all details [here](https://github.com/coollabsio/coolify-community-templates)
|
||||||
The following statements needs to be true:
|
|
||||||
|
|
||||||
- Self-hostable
|
|
||||||
- Open-source
|
|
||||||
- Maintained (I do not want to add software full of bugs)
|
|
||||||
|
|
||||||
### Create Prisma / Database schema for the new service.
|
|
||||||
All data that needs to be persist for a service should be saved to the database in `cleartext` or `encrypted`.
|
|
||||||
|
|
||||||
very password/api key/passphrase needs to be encrypted. If you are not sure, whether it should be encrypted or not, just encrypt it.
|
|
||||||
|
|
||||||
Update Prisma schema in [src/apps/api/prisma/schema.prisma](https://github.com/coollabsio/coolify/blob/main/apps/api/prisma/schema.prisma).
|
|
||||||
|
|
||||||
- Add new model with the new service name.
|
|
||||||
- Make a relationship with `Service` model.
|
|
||||||
- In the `Service` model, the name of the new field should be with low-capital.
|
|
||||||
- If the service needs a database, define a `publicPort` field to be able to make it's database public, example field name in case of PostgreSQL: `postgresqlPublicPort`. It should be a optional field.
|
|
||||||
|
|
||||||
Once done, create Prisma schema with `pnpm db:push`.
|
|
||||||
> You may also need to restart `Typescript Language Server` in your IDE to get the new types.
|
|
||||||
|
|
||||||
### Add available versions
|
|
||||||
|
|
||||||
Versions are hardcoded into Coolify at the moment and based on Docker image tags.
|
|
||||||
- Update `supportedServiceTypesAndVersions` function [here](apps/api/src/lib/services/supportedVersions.ts)
|
|
||||||
|
|
||||||
### Include the new service in queries
|
|
||||||
|
|
||||||
At [here](apps/api/src/lib/services/common.ts) in `includeServices` function add the new table name, so it will be included in all places in the database queries where it is required.
|
|
||||||
|
|
||||||
### Define auto-generated fields
|
|
||||||
|
|
||||||
At [here](apps/api/src/lib/services/common.ts) in `configureServiceType` function add the initial auto-generated details such as password, users etc, and the encryption process of secrets (if applicable).
|
|
||||||
|
|
||||||
### Define input field details
|
|
||||||
|
|
||||||
At [here](apps/api/src/lib/services/serviceFields.ts) add details about the input fields shown in the UI, so every component (API/UI) will know what to do with the values (decrypt/show it by default/readonly/etc).
|
|
||||||
|
|
||||||
### Define the start process
|
|
||||||
|
|
||||||
- At [here](apps/api/src/lib/services/handlers.ts), define how the service should start. It could be complex and based on `docker-compose` definitions.
|
|
||||||
|
|
||||||
> See `startUmamiService()` function as example.
|
|
||||||
|
|
||||||
- At [here](apps/api/src/routes/api/v1/services/handlers.ts), add the new start service process to `startService` function.
|
|
||||||
|
|
||||||
### Define the deletion process
|
|
||||||
|
|
||||||
[Here](apps/api/src/lib/services/common.ts) in `removeService` add the database deletion process.
|
|
||||||
|
|
||||||
### Custom logo
|
|
||||||
|
|
||||||
- At [here](apps/ui/src/lib/components/svg/services) add the service custom log as a Svelte component and export it [here](apps/ui/src/lib/components/svg/services/index.ts).
|
|
||||||
|
|
||||||
> SVG is recommended, but you can use PNG as well. It should have the `isAbsolute` variable with the suitable CSS classes, primarily for sizing and positioning.
|
|
||||||
|
|
||||||
- At [here](apps/ui/src/lib/components/svg/services/ServiceIcons.svelte) include the new logo with `isAbsolute` property.
|
|
||||||
|
|
||||||
- At [here](apps/ui/src/routes/services/[id]/_ServiceLinks.svelte) add links to the documentation of the service.
|
|
||||||
|
|
||||||
### Custom fields on the UI
|
|
||||||
By default the URL and name are shown on the UI. Everything else needs to be added [here](apps/ui/src/routes/services/[id]/_Services/_Services.svelte)
|
|
||||||
|
|
||||||
> If you need to show more details on the frontend, such as users/passwords, you need to add Svelte component [here](apps/ui/src/routes/services/[id]/_Services) with an underscore. For example, see other [here](apps/ui/src/routes/services/[id]/_Services/_Umami.svelte).
|
|
||||||
|
|
||||||
Good job! 👏
|
|
@ -43,6 +43,8 @@ COPY --from=build /app/apps/ui/build/ ./public
|
|||||||
COPY --from=build /app/apps/api/prisma/ ./prisma
|
COPY --from=build /app/apps/api/prisma/ ./prisma
|
||||||
COPY --from=build /app/apps/api/package.json .
|
COPY --from=build /app/apps/api/package.json .
|
||||||
COPY --from=build /app/docker-compose.yaml .
|
COPY --from=build /app/docker-compose.yaml .
|
||||||
|
COPY --from=build /app/apps/api/tags.json .
|
||||||
|
COPY --from=build /app/apps/api/templates.json .
|
||||||
|
|
||||||
RUN pnpm install -p
|
RUN pnpm install -p
|
||||||
|
|
||||||
|
1
apps/api/devTags.json
Normal file
1
apps/api/devTags.json
Normal file
File diff suppressed because one or more lines are too long
2785
apps/api/devTemplates.yaml
Normal file
2785
apps/api/devTemplates.yaml
Normal file
File diff suppressed because it is too large
Load Diff
@ -3,7 +3,7 @@
|
|||||||
"description": "Coolify's Fastify API",
|
"description": "Coolify's Fastify API",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"db:generate":"prisma generate",
|
"db:generate": "prisma generate",
|
||||||
"db:push": "prisma db push && prisma generate",
|
"db:push": "prisma db push && prisma generate",
|
||||||
"db:seed": "prisma db seed",
|
"db:seed": "prisma db seed",
|
||||||
"db:studio": "prisma studio",
|
"db:studio": "prisma studio",
|
||||||
@ -16,18 +16,16 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@breejs/ts-worker": "2.0.0",
|
"@breejs/ts-worker": "2.0.0",
|
||||||
"@fastify/autoload": "5.4.0",
|
"@fastify/autoload": "5.4.1",
|
||||||
"@fastify/cookie": "8.3.0",
|
"@fastify/cookie": "8.3.0",
|
||||||
"@fastify/cors": "8.1.0",
|
"@fastify/cors": "8.1.1",
|
||||||
"@fastify/env": "4.1.0",
|
"@fastify/env": "4.1.0",
|
||||||
"@fastify/jwt": "6.3.2",
|
"@fastify/jwt": "6.3.2",
|
||||||
"@fastify/multipart": "7.2.0",
|
"@fastify/multipart": "7.3.0",
|
||||||
"@fastify/static": "6.5.0",
|
"@fastify/static": "6.5.0",
|
||||||
"@iarna/toml": "2.2.5",
|
"@iarna/toml": "2.2.5",
|
||||||
"@ladjs/graceful": "3.0.2",
|
"@ladjs/graceful": "3.0.2",
|
||||||
"@prisma/client": "4.4.0",
|
"@prisma/client": "4.5.0",
|
||||||
"prisma": "4.4.0",
|
|
||||||
"axios": "0.27.2",
|
|
||||||
"bcryptjs": "2.4.3",
|
"bcryptjs": "2.4.3",
|
||||||
"bree": "9.1.2",
|
"bree": "9.1.2",
|
||||||
"cabin": "9.1.2",
|
"cabin": "9.1.2",
|
||||||
@ -35,12 +33,13 @@
|
|||||||
"csv-parse": "5.3.1",
|
"csv-parse": "5.3.1",
|
||||||
"csvtojson": "2.0.10",
|
"csvtojson": "2.0.10",
|
||||||
"cuid": "2.1.8",
|
"cuid": "2.1.8",
|
||||||
"dayjs": "1.11.5",
|
"dayjs": "1.11.6",
|
||||||
"dockerode": "3.3.4",
|
"dockerode": "3.3.4",
|
||||||
"dotenv-extended": "2.9.0",
|
"dotenv-extended": "2.9.0",
|
||||||
"execa": "6.1.0",
|
"execa": "6.1.0",
|
||||||
"fastify": "4.8.1",
|
"fastify": "4.9.2",
|
||||||
"fastify-plugin": "4.2.1",
|
"fastify-plugin": "4.3.0",
|
||||||
|
"fastify-socket.io": "4.0.0",
|
||||||
"generate-password": "1.7.0",
|
"generate-password": "1.7.0",
|
||||||
"got": "12.5.2",
|
"got": "12.5.2",
|
||||||
"is-ip": "5.0.0",
|
"is-ip": "5.0.0",
|
||||||
@ -51,26 +50,28 @@
|
|||||||
"node-os-utils": "1.3.7",
|
"node-os-utils": "1.3.7",
|
||||||
"p-all": "4.0.0",
|
"p-all": "4.0.0",
|
||||||
"p-throttle": "5.0.0",
|
"p-throttle": "5.0.0",
|
||||||
|
"prisma": "4.5.0",
|
||||||
"public-ip": "6.0.1",
|
"public-ip": "6.0.1",
|
||||||
"pump": "^3.0.0",
|
"pump": "3.0.0",
|
||||||
|
"socket.io": "4.5.3",
|
||||||
"ssh-config": "4.1.6",
|
"ssh-config": "4.1.6",
|
||||||
"strip-ansi": "7.0.1",
|
"strip-ansi": "7.0.1",
|
||||||
"unique-names-generator": "4.7.1"
|
"unique-names-generator": "4.7.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "18.8.5",
|
"@types/node": "18.11.6",
|
||||||
"@types/node-os-utils": "1.3.0",
|
"@types/node-os-utils": "1.3.0",
|
||||||
"@typescript-eslint/eslint-plugin": "5.38.1",
|
"@typescript-eslint/eslint-plugin": "5.41.0",
|
||||||
"@typescript-eslint/parser": "5.38.1",
|
"@typescript-eslint/parser": "5.41.0",
|
||||||
"esbuild": "0.15.10",
|
"esbuild": "0.15.12",
|
||||||
"eslint": "8.25.0",
|
"eslint": "8.26.0",
|
||||||
"eslint-config-prettier": "8.5.0",
|
"eslint-config-prettier": "8.5.0",
|
||||||
"eslint-plugin-prettier": "4.2.1",
|
"eslint-plugin-prettier": "4.2.1",
|
||||||
"nodemon": "2.0.20",
|
"nodemon": "2.0.20",
|
||||||
"prettier": "2.7.1",
|
"prettier": "2.7.1",
|
||||||
|
|
||||||
"rimraf": "3.0.2",
|
"rimraf": "3.0.2",
|
||||||
"tsconfig-paths": "4.1.0",
|
"tsconfig-paths": "4.1.0",
|
||||||
|
"types-fastify-socket.io": "0.0.1",
|
||||||
"typescript": "4.8.4"
|
"typescript": "4.8.4"
|
||||||
},
|
},
|
||||||
"prisma": {
|
"prisma": {
|
||||||
|
@ -0,0 +1,13 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "ServiceSetting" (
|
||||||
|
"id" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"serviceId" TEXT NOT NULL,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"value" TEXT NOT NULL,
|
||||||
|
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" DATETIME NOT NULL,
|
||||||
|
CONSTRAINT "ServiceSetting_serviceId_fkey" FOREIGN KEY ("serviceId") REFERENCES "Service" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "ServiceSetting_serviceId_name_key" ON "ServiceSetting"("serviceId", "name");
|
@ -0,0 +1,19 @@
|
|||||||
|
-- RedefineTables
|
||||||
|
PRAGMA foreign_keys=OFF;
|
||||||
|
CREATE TABLE "new_ServicePersistentStorage" (
|
||||||
|
"id" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"serviceId" TEXT NOT NULL,
|
||||||
|
"path" TEXT NOT NULL,
|
||||||
|
"volumeName" TEXT,
|
||||||
|
"predefined" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"containerId" TEXT,
|
||||||
|
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" DATETIME NOT NULL,
|
||||||
|
CONSTRAINT "ServicePersistentStorage_serviceId_fkey" FOREIGN KEY ("serviceId") REFERENCES "Service" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
INSERT INTO "new_ServicePersistentStorage" ("createdAt", "id", "path", "serviceId", "updatedAt") SELECT "createdAt", "id", "path", "serviceId", "updatedAt" FROM "ServicePersistentStorage";
|
||||||
|
DROP TABLE "ServicePersistentStorage";
|
||||||
|
ALTER TABLE "new_ServicePersistentStorage" RENAME TO "ServicePersistentStorage";
|
||||||
|
CREATE UNIQUE INDEX "ServicePersistentStorage_serviceId_path_key" ON "ServicePersistentStorage"("serviceId", "path");
|
||||||
|
PRAGMA foreign_key_check;
|
||||||
|
PRAGMA foreign_keys=ON;
|
@ -0,0 +1,24 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- Added the required column `variableName` to the `ServiceSetting` table without a default value. This is not possible if the table is not empty.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- RedefineTables
|
||||||
|
PRAGMA foreign_keys=OFF;
|
||||||
|
CREATE TABLE "new_ServiceSetting" (
|
||||||
|
"id" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"serviceId" TEXT NOT NULL,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"value" TEXT NOT NULL,
|
||||||
|
"variableName" TEXT NOT NULL,
|
||||||
|
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" DATETIME NOT NULL,
|
||||||
|
CONSTRAINT "ServiceSetting_serviceId_fkey" FOREIGN KEY ("serviceId") REFERENCES "Service" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
INSERT INTO "new_ServiceSetting" ("createdAt", "id", "name", "serviceId", "updatedAt", "value") SELECT "createdAt", "id", "name", "serviceId", "updatedAt", "value" FROM "ServiceSetting";
|
||||||
|
DROP TABLE "ServiceSetting";
|
||||||
|
ALTER TABLE "new_ServiceSetting" RENAME TO "ServiceSetting";
|
||||||
|
CREATE UNIQUE INDEX "ServiceSetting_serviceId_name_key" ON "ServiceSetting"("serviceId", "name");
|
||||||
|
PRAGMA foreign_key_check;
|
||||||
|
PRAGMA foreign_keys=ON;
|
@ -0,0 +1,21 @@
|
|||||||
|
-- RedefineTables
|
||||||
|
PRAGMA foreign_keys=OFF;
|
||||||
|
CREATE TABLE "new_Service" (
|
||||||
|
"id" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"fqdn" TEXT,
|
||||||
|
"exposePort" INTEGER,
|
||||||
|
"dualCerts" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"type" TEXT,
|
||||||
|
"version" TEXT,
|
||||||
|
"templateVersion" TEXT NOT NULL DEFAULT '0.0.0',
|
||||||
|
"destinationDockerId" TEXT,
|
||||||
|
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" DATETIME NOT NULL,
|
||||||
|
CONSTRAINT "Service_destinationDockerId_fkey" FOREIGN KEY ("destinationDockerId") REFERENCES "DestinationDocker" ("id") ON DELETE SET NULL ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
INSERT INTO "new_Service" ("createdAt", "destinationDockerId", "dualCerts", "exposePort", "fqdn", "id", "name", "type", "updatedAt", "version") SELECT "createdAt", "destinationDockerId", "dualCerts", "exposePort", "fqdn", "id", "name", "type", "updatedAt", "version" FROM "Service";
|
||||||
|
DROP TABLE "Service";
|
||||||
|
ALTER TABLE "new_Service" RENAME TO "Service";
|
||||||
|
PRAGMA foreign_key_check;
|
||||||
|
PRAGMA foreign_keys=ON;
|
@ -0,0 +1,11 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- A unique constraint covering the columns `[serviceId,containerId,path]` on the table `ServicePersistentStorage` will be added. If there are existing duplicate values, this will fail.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- DropIndex
|
||||||
|
DROP INDEX "ServicePersistentStorage_serviceId_path_key";
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "ServicePersistentStorage_serviceId_containerId_path_key" ON "ServicePersistentStorage"("serviceId", "containerId", "path");
|
@ -0,0 +1,32 @@
|
|||||||
|
-- RedefineTables
|
||||||
|
PRAGMA foreign_keys=OFF;
|
||||||
|
CREATE TABLE "new_Wordpress" (
|
||||||
|
"id" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"extraConfig" TEXT,
|
||||||
|
"tablePrefix" TEXT,
|
||||||
|
"ownMysql" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"mysqlHost" TEXT,
|
||||||
|
"mysqlPort" INTEGER,
|
||||||
|
"mysqlUser" TEXT,
|
||||||
|
"mysqlPassword" TEXT,
|
||||||
|
"mysqlRootUser" TEXT,
|
||||||
|
"mysqlRootUserPassword" TEXT,
|
||||||
|
"mysqlDatabase" TEXT,
|
||||||
|
"mysqlPublicPort" INTEGER,
|
||||||
|
"ftpEnabled" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"ftpUser" TEXT,
|
||||||
|
"ftpPassword" TEXT,
|
||||||
|
"ftpPublicPort" INTEGER,
|
||||||
|
"ftpHostKey" TEXT,
|
||||||
|
"ftpHostKeyPrivate" TEXT,
|
||||||
|
"serviceId" TEXT NOT NULL,
|
||||||
|
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" DATETIME NOT NULL,
|
||||||
|
CONSTRAINT "Wordpress_serviceId_fkey" FOREIGN KEY ("serviceId") REFERENCES "Service" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
INSERT INTO "new_Wordpress" ("createdAt", "extraConfig", "ftpEnabled", "ftpHostKey", "ftpHostKeyPrivate", "ftpPassword", "ftpPublicPort", "ftpUser", "id", "mysqlDatabase", "mysqlHost", "mysqlPassword", "mysqlPort", "mysqlPublicPort", "mysqlRootUser", "mysqlRootUserPassword", "mysqlUser", "ownMysql", "serviceId", "tablePrefix", "updatedAt") SELECT "createdAt", "extraConfig", "ftpEnabled", "ftpHostKey", "ftpHostKeyPrivate", "ftpPassword", "ftpPublicPort", "ftpUser", "id", "mysqlDatabase", "mysqlHost", "mysqlPassword", "mysqlPort", "mysqlPublicPort", "mysqlRootUser", "mysqlRootUserPassword", "mysqlUser", "ownMysql", "serviceId", "tablePrefix", "updatedAt" FROM "Wordpress";
|
||||||
|
DROP TABLE "Wordpress";
|
||||||
|
ALTER TABLE "new_Wordpress" RENAME TO "Wordpress";
|
||||||
|
CREATE UNIQUE INDEX "Wordpress_serviceId_key" ON "Wordpress"("serviceId");
|
||||||
|
PRAGMA foreign_key_check;
|
||||||
|
PRAGMA foreign_keys=ON;
|
@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Setting" ADD COLUMN "proxyDefaultRedirect" TEXT;
|
@ -29,6 +29,7 @@ model Setting {
|
|||||||
proxyPassword String
|
proxyPassword String
|
||||||
proxyUser String
|
proxyUser String
|
||||||
proxyHash String?
|
proxyHash String?
|
||||||
|
proxyDefaultRedirect String?
|
||||||
isAutoUpdateEnabled Boolean @default(false)
|
isAutoUpdateEnabled Boolean @default(false)
|
||||||
isDNSCheckEnabled Boolean @default(true)
|
isDNSCheckEnabled Boolean @default(true)
|
||||||
DNSServers String?
|
DNSServers String?
|
||||||
@ -193,14 +194,17 @@ model ApplicationPersistentStorage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model ServicePersistentStorage {
|
model ServicePersistentStorage {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
serviceId String
|
serviceId String
|
||||||
path String
|
path String
|
||||||
createdAt DateTime @default(now())
|
volumeName String?
|
||||||
updatedAt DateTime @updatedAt
|
predefined Boolean @default(false)
|
||||||
service Service @relation(fields: [serviceId], references: [id])
|
containerId String?
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
service Service @relation(fields: [serviceId], references: [id])
|
||||||
|
|
||||||
@@unique([serviceId, path])
|
@@unique([serviceId, containerId, path])
|
||||||
}
|
}
|
||||||
|
|
||||||
model Secret {
|
model Secret {
|
||||||
@ -392,12 +396,14 @@ model Service {
|
|||||||
dualCerts Boolean @default(false)
|
dualCerts Boolean @default(false)
|
||||||
type String?
|
type String?
|
||||||
version String?
|
version String?
|
||||||
|
templateVersion String @default("0.0.0")
|
||||||
destinationDockerId String?
|
destinationDockerId String?
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
destinationDocker DestinationDocker? @relation(fields: [destinationDockerId], references: [id])
|
destinationDocker DestinationDocker? @relation(fields: [destinationDockerId], references: [id])
|
||||||
persistentStorage ServicePersistentStorage[]
|
persistentStorage ServicePersistentStorage[]
|
||||||
serviceSecret ServiceSecret[]
|
serviceSecret ServiceSecret[]
|
||||||
|
serviceSetting ServiceSetting[]
|
||||||
teams Team[]
|
teams Team[]
|
||||||
|
|
||||||
fider Fider?
|
fider Fider?
|
||||||
@ -417,6 +423,19 @@ model Service {
|
|||||||
taiga Taiga?
|
taiga Taiga?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model ServiceSetting {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
serviceId String
|
||||||
|
name String
|
||||||
|
value String
|
||||||
|
variableName String
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
service Service @relation(fields: [serviceId], references: [id])
|
||||||
|
|
||||||
|
@@unique([serviceId, name])
|
||||||
|
}
|
||||||
|
|
||||||
model PlausibleAnalytics {
|
model PlausibleAnalytics {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
email String?
|
email String?
|
||||||
@ -462,10 +481,10 @@ model Wordpress {
|
|||||||
ownMysql Boolean @default(false)
|
ownMysql Boolean @default(false)
|
||||||
mysqlHost String?
|
mysqlHost String?
|
||||||
mysqlPort Int?
|
mysqlPort Int?
|
||||||
mysqlUser String
|
mysqlUser String?
|
||||||
mysqlPassword String
|
mysqlPassword String?
|
||||||
mysqlRootUser String
|
mysqlRootUser String?
|
||||||
mysqlRootUserPassword String
|
mysqlRootUserPassword String?
|
||||||
mysqlDatabase String?
|
mysqlDatabase String?
|
||||||
mysqlPublicPort Int?
|
mysqlPublicPort Int?
|
||||||
ftpEnabled Boolean @default(false)
|
ftpEnabled Boolean @default(false)
|
||||||
|
67
apps/api/scripts/generateTags.mjs
Normal file
67
apps/api/scripts/generateTags.mjs
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import fs from 'fs/promises';
|
||||||
|
import yaml from 'js-yaml';
|
||||||
|
import got from 'got';
|
||||||
|
|
||||||
|
const repositories = [];
|
||||||
|
const templates = await fs.readFile('./apps/api/devTemplates.yaml', 'utf8');
|
||||||
|
const devTemplates = yaml.load(templates);
|
||||||
|
for (const template of devTemplates) {
|
||||||
|
let image = template.services['$$id'].image.replaceAll(':$$core_version', '');
|
||||||
|
if (!image.includes('/')) {
|
||||||
|
image = `library/${image}`;
|
||||||
|
}
|
||||||
|
repositories.push({ image, name: template.type });
|
||||||
|
}
|
||||||
|
const services = []
|
||||||
|
const numberOfTags = 30;
|
||||||
|
// const semverRegex = new RegExp(/^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/g)
|
||||||
|
for (const repository of repositories) {
|
||||||
|
console.log('Querying', repository.name, 'at', repository.image);
|
||||||
|
let semverRegex = new RegExp(/^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/g)
|
||||||
|
if (repository.name.startsWith('wordpress')) {
|
||||||
|
semverRegex = new RegExp(/^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)-php(0|[1-9]\d*)$/g)
|
||||||
|
}
|
||||||
|
if (repository.name.startsWith('minio')) {
|
||||||
|
semverRegex = new RegExp(/^RELEASE.*$/g)
|
||||||
|
}
|
||||||
|
if (repository.name.startsWith('fider')) {
|
||||||
|
semverRegex = new RegExp(/^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)-([0-9]+)$/g)
|
||||||
|
}
|
||||||
|
if (repository.name.startsWith('searxng')) {
|
||||||
|
semverRegex = new RegExp(/^\d{4}[\.\-](0?[1-9]|[12][0-9]|3[01])[\.\-](0?[1-9]|1[012]).*$/)
|
||||||
|
}
|
||||||
|
if (repository.name.startsWith('umami')) {
|
||||||
|
semverRegex = new RegExp(/^postgresql-v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)-([0-9]+)$/g)
|
||||||
|
}
|
||||||
|
if (repository.image.includes('ghcr.io')) {
|
||||||
|
const { execaCommand } = await import('execa');
|
||||||
|
const { stdout } = await execaCommand(`docker run --rm quay.io/skopeo/stable list-tags docker://${repository.image}`);
|
||||||
|
if (stdout) {
|
||||||
|
const json = JSON.parse(stdout);
|
||||||
|
const semverTags = json.Tags.filter((tag) => semverRegex.test(tag))
|
||||||
|
let tags = semverTags.length > 10 ? semverTags.sort().reverse().slice(0, numberOfTags) : json.Tags.sort().reverse().slice(0, numberOfTags)
|
||||||
|
if (!tags.includes('latest')) {
|
||||||
|
tags.push('latest')
|
||||||
|
}
|
||||||
|
services.push({ name: repository.name, image: repository.image, tags })
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const { token } = await got.get(`https://auth.docker.io/token?service=registry.docker.io&scope=repository:${repository.image}:pull`).json()
|
||||||
|
let data = await got.get(`https://registry-1.docker.io/v2/${repository.image}/tags/list`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`
|
||||||
|
}
|
||||||
|
}).json()
|
||||||
|
const semverTags = data.tags.filter((tag) => semverRegex.test(tag))
|
||||||
|
let tags = semverTags.length > 10 ? semverTags.sort().reverse().slice(0, numberOfTags) : data.tags.sort().reverse().slice(0, numberOfTags)
|
||||||
|
if (!tags.includes('latest')) {
|
||||||
|
tags.push('latest')
|
||||||
|
}
|
||||||
|
services.push({
|
||||||
|
name: repository.name,
|
||||||
|
image: repository.image,
|
||||||
|
tags
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await fs.writeFile('./apps/api/devTags.json', JSON.stringify(services));
|
@ -6,14 +6,20 @@ import cookie from '@fastify/cookie';
|
|||||||
import multipart from '@fastify/multipart';
|
import multipart from '@fastify/multipart';
|
||||||
import path, { join } from 'path';
|
import path, { join } from 'path';
|
||||||
import autoLoad from '@fastify/autoload';
|
import autoLoad from '@fastify/autoload';
|
||||||
import { asyncExecShell, cleanupDockerStorage, createRemoteEngineConfiguration, decrypt, executeDockerCmd, executeSSHCmd, generateDatabaseConfiguration, getDomain, isDev, listSettings, prisma, startTraefikProxy, startTraefikTCPProxy, version } from './lib/common';
|
import socketIO from 'fastify-socket.io'
|
||||||
|
import socketIOServer from './realtime'
|
||||||
|
|
||||||
|
import { asyncExecShell, cleanupDockerStorage, createRemoteEngineConfiguration, decrypt, encrypt, executeDockerCmd, executeSSHCmd, generateDatabaseConfiguration, isDev, listSettings, prisma, startTraefikProxy, startTraefikTCPProxy, version } from './lib/common';
|
||||||
import { scheduler } from './lib/scheduler';
|
import { scheduler } from './lib/scheduler';
|
||||||
import { compareVersions } from 'compare-versions';
|
import { compareVersions } from 'compare-versions';
|
||||||
import Graceful from '@ladjs/graceful'
|
import Graceful from '@ladjs/graceful'
|
||||||
import axios from 'axios';
|
import yaml from 'js-yaml'
|
||||||
import fs from 'fs/promises';
|
import fs from 'fs/promises';
|
||||||
import { verifyRemoteDockerEngineFn } from './routes/api/v1/destinations/handlers';
|
import { verifyRemoteDockerEngineFn } from './routes/api/v1/destinations/handlers';
|
||||||
import { checkContainer } from './lib/docker';
|
import { checkContainer } from './lib/docker';
|
||||||
|
import { migrateServicesToNewTemplate } from './lib';
|
||||||
|
import { refreshTags, refreshTemplates } from './routes/api/v1/handlers';
|
||||||
|
|
||||||
declare module 'fastify' {
|
declare module 'fastify' {
|
||||||
interface FastifyInstance {
|
interface FastifyInstance {
|
||||||
config: {
|
config: {
|
||||||
@ -103,27 +109,40 @@ const host = '0.0.0.0';
|
|||||||
});
|
});
|
||||||
fastify.register(cookie)
|
fastify.register(cookie)
|
||||||
fastify.register(cors);
|
fastify.register(cors);
|
||||||
fastify.addHook('onRequest', async (request, reply) => {
|
fastify.register(socketIO, {
|
||||||
let allowedList = ['coolify:3000'];
|
cors: {
|
||||||
const { ipv4, ipv6, fqdn } = await prisma.setting.findFirst({})
|
origin: isDev ? "*" : ''
|
||||||
|
|
||||||
ipv4 && allowedList.push(`${ipv4}:3000`);
|
|
||||||
ipv6 && allowedList.push(ipv6);
|
|
||||||
fqdn && allowedList.push(getDomain(fqdn));
|
|
||||||
isDev && allowedList.push('localhost:3000') && allowedList.push('localhost:3001') && allowedList.push('host.docker.internal:3001');
|
|
||||||
const remotes = await prisma.destinationDocker.findMany({ where: { remoteEngine: true, remoteVerified: true } })
|
|
||||||
if (remotes.length > 0) {
|
|
||||||
remotes.forEach(remote => {
|
|
||||||
allowedList.push(`${remote.remoteIpAddress}:3000`);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if (!allowedList.includes(request.headers.host)) {
|
|
||||||
// console.log('not allowed', request.headers.host)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// To detect allowed origins
|
||||||
|
// fastify.addHook('onRequest', async (request, reply) => {
|
||||||
|
// console.log(request.headers.host)
|
||||||
|
// let allowedList = ['coolify:3000'];
|
||||||
|
// const { ipv4, ipv6, fqdn } = await prisma.setting.findFirst({})
|
||||||
|
|
||||||
|
// ipv4 && allowedList.push(`${ipv4}:3000`);
|
||||||
|
// ipv6 && allowedList.push(ipv6);
|
||||||
|
// fqdn && allowedList.push(getDomain(fqdn));
|
||||||
|
// isDev && allowedList.push('localhost:3000') && allowedList.push('localhost:3001') && allowedList.push('host.docker.internal:3001');
|
||||||
|
// const remotes = await prisma.destinationDocker.findMany({ where: { remoteEngine: true, remoteVerified: true } })
|
||||||
|
// if (remotes.length > 0) {
|
||||||
|
// remotes.forEach(remote => {
|
||||||
|
// allowedList.push(`${remote.remoteIpAddress}:3000`);
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
// if (!allowedList.includes(request.headers.host)) {
|
||||||
|
// // console.log('not allowed', request.headers.host)
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await fastify.listen({ port, host })
|
await fastify.listen({ port, host })
|
||||||
|
await socketIOServer(fastify)
|
||||||
console.log(`Coolify's API is listening on ${host}:${port}`);
|
console.log(`Coolify's API is listening on ${host}:${port}`);
|
||||||
|
|
||||||
|
migrateServicesToNewTemplate()
|
||||||
await initServer();
|
await initServer();
|
||||||
|
|
||||||
const graceful = new Graceful({ brees: [scheduler] });
|
const graceful = new Graceful({ brees: [scheduler] });
|
||||||
@ -145,21 +164,30 @@ const host = '0.0.0.0';
|
|||||||
await cleanupStorage()
|
await cleanupStorage()
|
||||||
}, 60000 * 10)
|
}, 60000 * 10)
|
||||||
|
|
||||||
// checkProxies and checkFluentBit
|
// checkProxies, checkFluentBit & refresh templates
|
||||||
setInterval(async () => {
|
setInterval(async () => {
|
||||||
await checkProxies();
|
await checkProxies();
|
||||||
await checkFluentBit();
|
await checkFluentBit();
|
||||||
}, 10000)
|
}, 60000)
|
||||||
|
|
||||||
|
// Refresh and check templates
|
||||||
|
setInterval(async () => {
|
||||||
|
await refreshTemplates()
|
||||||
|
await refreshTags()
|
||||||
|
await migrateServicesToNewTemplate()
|
||||||
|
}, 60000)
|
||||||
|
|
||||||
setInterval(async () => {
|
setInterval(async () => {
|
||||||
await copySSLCertificates();
|
await copySSLCertificates();
|
||||||
}, 2000)
|
}, 10000)
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
|
getTagsTemplates(),
|
||||||
getArch(),
|
getArch(),
|
||||||
getIPAddress(),
|
getIPAddress(),
|
||||||
configureRemoteDockers(),
|
configureRemoteDockers(),
|
||||||
])
|
])
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
@ -185,6 +213,28 @@ async function getIPAddress() {
|
|||||||
|
|
||||||
} catch (error) { }
|
} catch (error) { }
|
||||||
}
|
}
|
||||||
|
async function getTagsTemplates() {
|
||||||
|
const { default: got } = await import('got')
|
||||||
|
try {
|
||||||
|
if (isDev) {
|
||||||
|
const templates = await fs.readFile('./devTemplates.yaml', 'utf8')
|
||||||
|
const tags = await fs.readFile('./devTags.json', 'utf8')
|
||||||
|
await fs.writeFile('./templates.json', JSON.stringify(yaml.load(templates)))
|
||||||
|
await fs.writeFile('./tags.json', tags)
|
||||||
|
console.log('Tags and templates loaded in dev mode...')
|
||||||
|
} else {
|
||||||
|
const tags = await got.get('https://get.coollabs.io/coolify/service-tags.json').text()
|
||||||
|
const response = await got.get('https://get.coollabs.io/coolify/service-templates.yaml').text()
|
||||||
|
await fs.writeFile('/app/templates.json', JSON.stringify(yaml.load(response)))
|
||||||
|
await fs.writeFile('/app/tags.json', tags)
|
||||||
|
console.log('Tags and templates loaded...')
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Couldn't get latest templates.")
|
||||||
|
console.log(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
async function initServer() {
|
async function initServer() {
|
||||||
try {
|
try {
|
||||||
console.log(`Initializing server...`);
|
console.log(`Initializing server...`);
|
||||||
@ -197,6 +247,7 @@ async function initServer() {
|
|||||||
}
|
}
|
||||||
} catch (error) { }
|
} catch (error) { }
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getArch() {
|
async function getArch() {
|
||||||
try {
|
try {
|
||||||
const settings = await prisma.setting.findFirst({})
|
const settings = await prisma.setting.findFirst({})
|
||||||
@ -226,17 +277,15 @@ async function configureRemoteDockers() {
|
|||||||
|
|
||||||
async function autoUpdater() {
|
async function autoUpdater() {
|
||||||
try {
|
try {
|
||||||
|
const { default: got } = await import('got')
|
||||||
const currentVersion = version;
|
const currentVersion = version;
|
||||||
const { data: versions } = await axios
|
const { coolify } = await got.get('https://get.coollabs.io/versions.json', {
|
||||||
.get(
|
searchParams: {
|
||||||
`https://get.coollabs.io/versions.json`
|
appId: process.env['COOLIFY_APP_ID'] || undefined,
|
||||||
, {
|
version: currentVersion
|
||||||
params: {
|
}
|
||||||
appId: process.env['COOLIFY_APP_ID'] || undefined,
|
}).json()
|
||||||
version: currentVersion
|
const latestVersion = coolify.main.version;
|
||||||
}
|
|
||||||
})
|
|
||||||
const latestVersion = versions['coolify'].main.version;
|
|
||||||
const isUpdateAvailable = compareVersions(latestVersion, currentVersion);
|
const isUpdateAvailable = compareVersions(latestVersion, currentVersion);
|
||||||
if (isUpdateAvailable === 1) {
|
if (isUpdateAvailable === 1) {
|
||||||
const activeCount = 0
|
const activeCount = 0
|
||||||
@ -258,7 +307,9 @@ async function autoUpdater() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) { }
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function checkFluentBit() {
|
async function checkFluentBit() {
|
||||||
@ -338,17 +389,17 @@ async function checkProxies() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// HTTP Proxies
|
// HTTP Proxies
|
||||||
const minioInstances = await prisma.minio.findMany({
|
// const minioInstances = await prisma.minio.findMany({
|
||||||
where: { publicPort: { not: null } },
|
// where: { publicPort: { not: null } },
|
||||||
include: { service: { include: { destinationDocker: true } } }
|
// include: { service: { include: { destinationDocker: true } } }
|
||||||
});
|
// });
|
||||||
for (const minio of minioInstances) {
|
// for (const minio of minioInstances) {
|
||||||
const { service, publicPort } = minio;
|
// const { service, publicPort } = minio;
|
||||||
const { destinationDockerId, destinationDocker, id } = service;
|
// const { destinationDockerId, destinationDocker, id } = service;
|
||||||
if (destinationDockerId && destinationDocker.isCoolifyProxyUsed) {
|
// if (destinationDockerId && destinationDocker.isCoolifyProxyUsed) {
|
||||||
await startTraefikTCPProxy(destinationDocker, id, publicPort, 9000);
|
// await startTraefikTCPProxy(destinationDocker, id, publicPort, 9000);
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -131,7 +131,6 @@ import * as buildpacks from '../lib/buildPacks';
|
|||||||
try {
|
try {
|
||||||
dockerComposeConfiguration = JSON.parse(dockerComposeConfiguration)
|
dockerComposeConfiguration = JSON.parse(dockerComposeConfiguration)
|
||||||
} catch (error) { }
|
} catch (error) { }
|
||||||
|
|
||||||
let deployNeeded = true;
|
let deployNeeded = true;
|
||||||
let destinationType;
|
let destinationType;
|
||||||
|
|
||||||
@ -313,11 +312,11 @@ import * as buildpacks from '../lib/buildPacks';
|
|||||||
try {
|
try {
|
||||||
await executeDockerCmd({
|
await executeDockerCmd({
|
||||||
dockerId: destinationDockerId,
|
dockerId: destinationDockerId,
|
||||||
command: `docker ps -a --filter 'label=com.docker.compose.service=${applicationId}' --format {{.ID}}|xargs -r -n 1 docker stop -t 0`
|
command: `docker ps -a --filter 'label=com.docker.compose.service=${pullmergeRequestId ? imageId : applicationId}' --format {{.ID}}|xargs -r -n 1 docker stop -t 0`
|
||||||
})
|
})
|
||||||
await executeDockerCmd({
|
await executeDockerCmd({
|
||||||
dockerId: destinationDockerId,
|
dockerId: destinationDockerId,
|
||||||
command: `docker ps -a --filter 'label=com.docker.compose.service=${applicationId}' --format {{.ID}}|xargs -r -n 1 docker rm --force`
|
command: `docker ps -a --filter 'label=com.docker.compose.service=${pullmergeRequestId ? imageId : applicationId}' --format {{.ID}}|xargs -r -n 1 docker rm --force`
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
//
|
//
|
||||||
|
479
apps/api/src/lib.ts
Normal file
479
apps/api/src/lib.ts
Normal file
@ -0,0 +1,479 @@
|
|||||||
|
import cuid from "cuid";
|
||||||
|
import { decrypt, encrypt, fixType, generatePassword, getDomain, prisma } from "./lib/common";
|
||||||
|
import { getTemplates } from "./lib/services";
|
||||||
|
|
||||||
|
export async function migrateServicesToNewTemplate() {
|
||||||
|
// This function migrates old hardcoded services to the new template based services
|
||||||
|
try {
|
||||||
|
let templates = await getTemplates()
|
||||||
|
const services: any = await prisma.service.findMany({
|
||||||
|
include: {
|
||||||
|
destinationDocker: true,
|
||||||
|
persistentStorage: true,
|
||||||
|
serviceSecret: true,
|
||||||
|
serviceSetting: true,
|
||||||
|
minio: true,
|
||||||
|
plausibleAnalytics: true,
|
||||||
|
vscodeserver: true,
|
||||||
|
wordpress: true,
|
||||||
|
ghost: true,
|
||||||
|
meiliSearch: true,
|
||||||
|
umami: true,
|
||||||
|
hasura: true,
|
||||||
|
fider: true,
|
||||||
|
moodle: true,
|
||||||
|
appwrite: true,
|
||||||
|
glitchTip: true,
|
||||||
|
searxng: true,
|
||||||
|
weblate: true,
|
||||||
|
taiga: true,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
for (const service of services) {
|
||||||
|
const { id } = service
|
||||||
|
if (!service.type) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let template = templates.find(t => fixType(t.type) === fixType(service.type));
|
||||||
|
if (template) {
|
||||||
|
template = JSON.parse(JSON.stringify(template).replaceAll('$$id', service.id))
|
||||||
|
if (service.type === 'plausibleanalytics' && service.plausibleAnalytics) await plausibleAnalytics(service, template)
|
||||||
|
if (service.type === 'fider' && service.fider) await fider(service, template)
|
||||||
|
if (service.type === 'minio' && service.minio) await minio(service, template)
|
||||||
|
if (service.type === 'vscodeserver' && service.vscodeserver) await vscodeserver(service, template)
|
||||||
|
if (service.type === 'wordpress' && service.wordpress) await wordpress(service, template)
|
||||||
|
if (service.type === 'ghost' && service.ghost) await ghost(service, template)
|
||||||
|
if (service.type === 'meilisearch' && service.meiliSearch) await meilisearch(service, template)
|
||||||
|
if (service.type === 'umami' && service.umami) await umami(service, template)
|
||||||
|
if (service.type === 'hasura' && service.hasura) await hasura(service, template)
|
||||||
|
if (service.type === 'glitchTip' && service.glitchTip) await glitchtip(service, template)
|
||||||
|
if (service.type === 'searxng' && service.searxng) await searxng(service, template)
|
||||||
|
if (service.type === 'weblate' && service.weblate) await weblate(service, template)
|
||||||
|
if (service.type === 'appwrite' && service.appwrite) await appwrite(service, template)
|
||||||
|
|
||||||
|
await createVolumes(service, template);
|
||||||
|
|
||||||
|
if (template.variables.length > 0) {
|
||||||
|
for (const variable of template.variables) {
|
||||||
|
const { defaultValue } = variable;
|
||||||
|
const regex = /^\$\$.*\((\d+)\)$/g;
|
||||||
|
const length = Number(regex.exec(defaultValue)?.[1]) || undefined
|
||||||
|
if (variable.defaultValue.startsWith('$$generate_password')) {
|
||||||
|
variable.value = generatePassword({ length });
|
||||||
|
} else if (variable.defaultValue.startsWith('$$generate_hex')) {
|
||||||
|
variable.value = generatePassword({ length, isHex: true });
|
||||||
|
} else if (variable.defaultValue.startsWith('$$generate_username')) {
|
||||||
|
variable.value = cuid();
|
||||||
|
} else {
|
||||||
|
variable.value = variable.defaultValue || '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const variable of template.variables) {
|
||||||
|
if (variable.id.startsWith('$$secret_')) {
|
||||||
|
const found = await prisma.serviceSecret.findFirst({ where: { name: variable.name, serviceId: id } })
|
||||||
|
if (!found) {
|
||||||
|
await prisma.serviceSecret.create({
|
||||||
|
data: { name: variable.name, value: encrypt(variable.value) || '', service: { connect: { id } } }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
if (variable.id.startsWith('$$config_')) {
|
||||||
|
const found = await prisma.serviceSetting.findFirst({ where: { name: variable.name, serviceId: id } })
|
||||||
|
if (!found) {
|
||||||
|
await prisma.serviceSetting.create({
|
||||||
|
data: { name: variable.name, value: variable.value.toString(), variableName: variable.id, service: { connect: { id } } }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const service of Object.keys(template.services)) {
|
||||||
|
if (template.services[service].volumes) {
|
||||||
|
for (const volume of template.services[service].volumes) {
|
||||||
|
const [volumeName, path] = volume.split(':')
|
||||||
|
if (!volumeName.startsWith('/')) {
|
||||||
|
const found = await prisma.servicePersistentStorage.findFirst({ where: { volumeName, serviceId: id } })
|
||||||
|
if (!found) {
|
||||||
|
await prisma.servicePersistentStorage.create({
|
||||||
|
data: { volumeName, path, containerId: service, predefined: true, service: { connect: { id } } }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await prisma.service.update({ where: { id }, data: { templateVersion: template.templateVersion } })
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function appwrite(service: any, template: any) {
|
||||||
|
const { opensslKeyV1, executorSecret, mariadbHost, mariadbPort, mariadbUser, mariadbPassword, mariadbRootUserPassword, mariadbDatabase } = service.appwrite
|
||||||
|
|
||||||
|
const secrets = [
|
||||||
|
`_APP_EXECUTOR_SECRET@@@${executorSecret}`,
|
||||||
|
`_APP_OPENSSL_KEY_V1@@@${opensslKeyV1}`,
|
||||||
|
`_APP_DB_PASS@@@${mariadbPassword}`,
|
||||||
|
`_APP_DB_ROOT_PASS@@@${mariadbRootUserPassword}`,
|
||||||
|
]
|
||||||
|
|
||||||
|
const settings = [
|
||||||
|
`_APP_DB_HOST@@@${mariadbHost}`,
|
||||||
|
`_APP_DB_PORT@@@${mariadbPort}`,
|
||||||
|
`_APP_DB_USER@@@${mariadbUser}`,
|
||||||
|
`_APP_DB_SCHEMA@@@${mariadbDatabase}`,
|
||||||
|
]
|
||||||
|
await migrateSecrets(secrets, service);
|
||||||
|
await migrateSettings(settings, service, template);
|
||||||
|
|
||||||
|
// Disconnect old service data
|
||||||
|
// await prisma.service.update({ where: { id: service.id }, data: { appwrite: { disconnect: true } } })
|
||||||
|
}
|
||||||
|
async function weblate(service: any, template: any) {
|
||||||
|
const { adminPassword, postgresqlUser, postgresqlPassword, postgresqlDatabase } = service.weblate
|
||||||
|
|
||||||
|
const secrets = [
|
||||||
|
`WEBLATE_ADMIN_PASSWORD@@@${adminPassword}`,
|
||||||
|
`POSTGRES_PASSWORD@@@${postgresqlPassword}`,
|
||||||
|
]
|
||||||
|
|
||||||
|
const settings = [
|
||||||
|
`WEBLATE_SITE_DOMAIN@@@$$generate_domain`,
|
||||||
|
`POSTGRES_USER@@@${postgresqlUser}`,
|
||||||
|
`POSTGRES_DATABASE@@@${postgresqlDatabase}`,
|
||||||
|
`POSTGRES_DB@@@${postgresqlDatabase}`,
|
||||||
|
`POSTGRES_HOST@@@$$id-postgres`,
|
||||||
|
`POSTGRES_PORT@@@5432`,
|
||||||
|
`REDIS_HOST@@@$$id-redis`,
|
||||||
|
]
|
||||||
|
await migrateSettings(settings, service, template);
|
||||||
|
await migrateSecrets(secrets, service);
|
||||||
|
|
||||||
|
// Disconnect old service data
|
||||||
|
// await prisma.service.update({ where: { id: service.id }, data: { weblate: { disconnect: true } } })
|
||||||
|
}
|
||||||
|
async function searxng(service: any, template: any) {
|
||||||
|
const { secretKey, redisPassword } = service.searxng
|
||||||
|
|
||||||
|
const secrets = [
|
||||||
|
`SECRET_KEY@@@${secretKey}`,
|
||||||
|
`REDIS_PASSWORD@@@${redisPassword}`,
|
||||||
|
]
|
||||||
|
|
||||||
|
const settings = [
|
||||||
|
`SEARXNG_BASE_URL@@@$$generate_fqdn`
|
||||||
|
]
|
||||||
|
await migrateSettings(settings, service, template);
|
||||||
|
await migrateSecrets(secrets, service);
|
||||||
|
|
||||||
|
// Disconnect old service data
|
||||||
|
// await prisma.service.update({ where: { id: service.id }, data: { searxng: { disconnect: true } } })
|
||||||
|
}
|
||||||
|
async function glitchtip(service: any, template: any) {
|
||||||
|
const { postgresqlUser, postgresqlPassword, postgresqlDatabase, secretKeyBase, defaultEmail, defaultUsername, defaultPassword, defaultEmailFrom, emailSmtpHost, emailSmtpPort, emailSmtpUser, emailSmtpPassword, emailSmtpUseTls, emailSmtpUseSsl, emailBackend, mailgunApiKey, sendgridApiKey, enableOpenUserRegistration } = service.glitchTip
|
||||||
|
const { id } = service
|
||||||
|
|
||||||
|
const secrets = [
|
||||||
|
`POSTGRES_PASSWORD@@@${postgresqlPassword}`,
|
||||||
|
`SECRET_KEY@@@${secretKeyBase}`,
|
||||||
|
`MAILGUN_API_KEY@@@${mailgunApiKey}`,
|
||||||
|
`SENDGRID_API_KEY@@@${sendgridApiKey}`,
|
||||||
|
`DJANGO_SUPERUSER_PASSWORD@@@${defaultPassword}`,
|
||||||
|
emailSmtpUser && emailSmtpPassword && emailSmtpHost && emailSmtpPort && `EMAIL_URL@@@${encrypt(`smtp://${emailSmtpUser}:${decrypt(emailSmtpPassword)}@${emailSmtpHost}:${emailSmtpPort}`)} || ''`,
|
||||||
|
`DATABASE_URL@@@${encrypt(`postgres://${postgresqlUser}:${decrypt(postgresqlPassword)}@${id}-postgresql:5432/${postgresqlDatabase}`)}`,
|
||||||
|
`REDIS_URL@@@${encrypt(`redis://${id}-redis:6379`)}`
|
||||||
|
]
|
||||||
|
const settings = [
|
||||||
|
`POSTGRES_USER@@@${postgresqlUser}`,
|
||||||
|
`POSTGRES_DB@@@${postgresqlDatabase}`,
|
||||||
|
`DEFAULT_FROM_EMAIL@@@${defaultEmailFrom}`,
|
||||||
|
`EMAIL_USE_TLS@@@${emailSmtpUseTls}`,
|
||||||
|
`EMAIL_USE_SSL@@@${emailSmtpUseSsl}`,
|
||||||
|
`EMAIL_BACKEND@@@${emailBackend}`,
|
||||||
|
`ENABLE_OPEN_USER_REGISTRATION@@@${enableOpenUserRegistration}`,
|
||||||
|
`DJANGO_SUPERUSER_EMAIL@@@${defaultEmail}`,
|
||||||
|
`DJANGO_SUPERUSER_USERNAME@@@${defaultUsername}`,
|
||||||
|
]
|
||||||
|
await migrateSettings(settings, service, template);
|
||||||
|
await migrateSecrets(secrets, service);
|
||||||
|
|
||||||
|
await prisma.service.update({ where: { id: service.id }, data: { type: 'glitchtip' } })
|
||||||
|
// Disconnect old service data
|
||||||
|
// await prisma.service.update({ where: { id: service.id }, data: { glitchTip: { disconnect: true } } })
|
||||||
|
}
|
||||||
|
async function hasura(service: any, template: any) {
|
||||||
|
const { postgresqlUser, postgresqlPassword, postgresqlDatabase, graphQLAdminPassword } = service.hasura
|
||||||
|
const { id } = service
|
||||||
|
|
||||||
|
const secrets = [
|
||||||
|
`HASURA_GRAPHQL_ADMIN_PASSWORD@@@${graphQLAdminPassword}`,
|
||||||
|
`HASURA_GRAPHQL_METADATA_DATABASE_URL@@@${encrypt(`postgres://${postgresqlUser}:${decrypt(postgresqlPassword)}@${id}-postgresql:5432/${postgresqlDatabase}`)}`,
|
||||||
|
`POSTGRES_PASSWORD@@@${postgresqlPassword}`,
|
||||||
|
]
|
||||||
|
const settings = [
|
||||||
|
`POSTGRES_USER@@@${postgresqlUser}`,
|
||||||
|
`POSTGRES_DB@@@${postgresqlDatabase}`,
|
||||||
|
]
|
||||||
|
await migrateSettings(settings, service, template);
|
||||||
|
await migrateSecrets(secrets, service);
|
||||||
|
|
||||||
|
// Disconnect old service data
|
||||||
|
// await prisma.service.update({ where: { id: service.id }, data: { hasura: { disconnect: true } } })
|
||||||
|
}
|
||||||
|
async function umami(service: any, template: any) {
|
||||||
|
const { postgresqlUser, postgresqlPassword, postgresqlDatabase, umamiAdminPassword, hashSalt } = service.umami
|
||||||
|
const { id } = service
|
||||||
|
|
||||||
|
const secrets = [
|
||||||
|
`HASH_SALT@@@${hashSalt}`,
|
||||||
|
`POSTGRES_PASSWORD@@@${postgresqlPassword}`,
|
||||||
|
`ADMIN_PASSWORD@@@${umamiAdminPassword}`,
|
||||||
|
`DATABASE_URL@@@${encrypt(`postgres://${postgresqlUser}:${decrypt(postgresqlPassword)}@${id}-postgresql:5432/${postgresqlDatabase}`)}`,
|
||||||
|
]
|
||||||
|
const settings = [
|
||||||
|
`DATABASE_TYPE@@@postgresql`,
|
||||||
|
`POSTGRES_USER@@@${postgresqlUser}`,
|
||||||
|
`POSTGRES_DB@@@${postgresqlDatabase}`,
|
||||||
|
]
|
||||||
|
await migrateSettings(settings, service, template);
|
||||||
|
await migrateSecrets(secrets, service);
|
||||||
|
|
||||||
|
// Disconnect old service data
|
||||||
|
// await prisma.service.update({ where: { id: service.id }, data: { umami: { disconnect: true } } })
|
||||||
|
}
|
||||||
|
async function meilisearch(service: any, template: any) {
|
||||||
|
const { masterKey } = service.meiliSearch
|
||||||
|
|
||||||
|
const secrets = [
|
||||||
|
`MEILI_MASTER_KEY@@@${masterKey}`,
|
||||||
|
]
|
||||||
|
|
||||||
|
// await migrateSettings(settings, service, template);
|
||||||
|
await migrateSecrets(secrets, service);
|
||||||
|
|
||||||
|
// Disconnect old service data
|
||||||
|
// await prisma.service.update({ where: { id: service.id }, data: { meiliSearch: { disconnect: true } } })
|
||||||
|
}
|
||||||
|
async function ghost(service: any, template: any) {
|
||||||
|
const { defaultEmail, defaultPassword, mariadbUser, mariadbPassword, mariadbRootUser, mariadbRootUserPassword, mariadbDatabase } = service.ghost
|
||||||
|
const { fqdn } = service
|
||||||
|
|
||||||
|
const isHttps = fqdn.startsWith('https://');
|
||||||
|
|
||||||
|
const secrets = [
|
||||||
|
`GHOST_PASSWORD@@@${defaultPassword}`,
|
||||||
|
`MARIADB_PASSWORD@@@${mariadbPassword}`,
|
||||||
|
`MARIADB_ROOT_PASSWORD@@@${mariadbRootUserPassword}`,
|
||||||
|
`GHOST_DATABASE_PASSWORD@@@${mariadbPassword}`,
|
||||||
|
]
|
||||||
|
const settings = [
|
||||||
|
`GHOST_EMAIL@@@${defaultEmail}`,
|
||||||
|
`GHOST_DATABASE_HOST@@@${service.id}-mariadb`,
|
||||||
|
`GHOST_DATABASE_USER@@@${mariadbUser}`,
|
||||||
|
`GHOST_DATABASE_NAME@@@${mariadbDatabase}`,
|
||||||
|
`GHOST_DATABASE_PORT_NUMBER@@@3306`,
|
||||||
|
`MARIADB_USER@@@${mariadbUser}`,
|
||||||
|
`MARIADB_DATABASE@@@${mariadbDatabase}`,
|
||||||
|
`MARIADB_ROOT_USER@@@${mariadbRootUser}`,
|
||||||
|
`GHOST_HOST@@@$$generate_domain`,
|
||||||
|
`url@@@$$generate_fqdn`,
|
||||||
|
`GHOST_ENABLE_HTTPS@@@${isHttps ? 'yes' : 'no'}`
|
||||||
|
]
|
||||||
|
await migrateSettings(settings, service, template);
|
||||||
|
await migrateSecrets(secrets, service);
|
||||||
|
|
||||||
|
await prisma.service.update({ where: { id: service.id }, data: { type: "ghost-mariadb" } })
|
||||||
|
|
||||||
|
// Disconnect old service data
|
||||||
|
// await prisma.service.update({ where: { id: service.id }, data: { ghost: { disconnect: true } } })
|
||||||
|
}
|
||||||
|
async function wordpress(service: any, template: any) {
|
||||||
|
const { extraConfig, tablePrefix, ownMysql, mysqlHost, mysqlPort, mysqlUser, mysqlPassword, mysqlRootUser, mysqlRootUserPassword, mysqlDatabase, ftpEnabled, ftpUser, ftpPassword, ftpPublicPort, ftpHostKey, ftpHostKeyPrivate } = service.wordpress
|
||||||
|
|
||||||
|
let settings = []
|
||||||
|
let secrets = []
|
||||||
|
if (ownMysql) {
|
||||||
|
secrets = [
|
||||||
|
`WORDPRESS_DB_PASSWORD@@@${mysqlPassword}`,
|
||||||
|
ftpPassword && `COOLIFY_FTP_PASSWORD@@@${ftpPassword}`,
|
||||||
|
ftpHostKeyPrivate && `COOLIFY_FTP_HOST_KEY_PRIVATE@@@${ftpHostKeyPrivate}`,
|
||||||
|
ftpHostKey && `COOLIFY_FTP_HOST_KEY@@@${ftpHostKey}`,
|
||||||
|
]
|
||||||
|
settings = [
|
||||||
|
`WORDPRESS_CONFIG_EXTRA@@@${extraConfig}`,
|
||||||
|
`WORDPRESS_DB_HOST@@@${mysqlHost}`,
|
||||||
|
`WORDPRESS_DB_PORT@@@${mysqlPort}`,
|
||||||
|
`WORDPRESS_DB_USER@@@${mysqlUser}`,
|
||||||
|
`WORDPRESS_DB_NAME@@@${mysqlDatabase}`,
|
||||||
|
]
|
||||||
|
} else {
|
||||||
|
secrets = [
|
||||||
|
`MYSQL_ROOT_PASSWORD@@@${mysqlRootUserPassword}`,
|
||||||
|
`MYSQL_PASSWORD@@@${mysqlPassword}`,
|
||||||
|
ftpPassword && `COOLIFY_FTP_PASSWORD@@@${ftpPassword}`,
|
||||||
|
ftpHostKeyPrivate && `COOLIFY_FTP_HOST_KEY_PRIVATE@@@${ftpHostKeyPrivate}`,
|
||||||
|
ftpHostKey && `COOLIFY_FTP_HOST_KEY@@@${ftpHostKey}`,
|
||||||
|
]
|
||||||
|
settings = [
|
||||||
|
`MYSQL_ROOT_USER@@@${mysqlRootUser}`,
|
||||||
|
`MYSQL_USER@@@${mysqlUser}`,
|
||||||
|
`MYSQL_DATABASE@@@${mysqlDatabase}`,
|
||||||
|
`MYSQL_HOST@@@${service.id}-mysql`,
|
||||||
|
`MYSQL_PORT@@@${mysqlPort}`,
|
||||||
|
`WORDPRESS_CONFIG_EXTRA@@@${extraConfig}`,
|
||||||
|
`WORDPRESS_TABLE_PREFIX@@@${tablePrefix}`,
|
||||||
|
`WORDPRESS_DB_HOST@@@${service.id}-mysql`,
|
||||||
|
`COOLIFY_OWN_DB@@@${ownMysql}`,
|
||||||
|
`COOLIFY_FTP_ENABLED@@@${ftpEnabled}`,
|
||||||
|
`COOLIFY_FTP_USER@@@${ftpUser}`,
|
||||||
|
`COOLIFY_FTP_PUBLIC_PORT@@@${ftpPublicPort}`,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
await migrateSettings(settings, service, template);
|
||||||
|
await migrateSecrets(secrets, service);
|
||||||
|
if (ownMysql) {
|
||||||
|
await prisma.service.update({ where: { id: service.id }, data: { type: "wordpress-only" } })
|
||||||
|
}
|
||||||
|
// Disconnect old service data
|
||||||
|
// await prisma.service.update({ where: { id: service.id }, data: { wordpress: { disconnect: true } } })
|
||||||
|
}
|
||||||
|
async function vscodeserver(service: any, template: any) {
|
||||||
|
const { password } = service.vscodeserver
|
||||||
|
|
||||||
|
const secrets = [
|
||||||
|
`PASSWORD@@@${password}`,
|
||||||
|
]
|
||||||
|
await migrateSecrets(secrets, service);
|
||||||
|
|
||||||
|
// Disconnect old service data
|
||||||
|
// await prisma.service.update({ where: { id: service.id }, data: { vscodeserver: { disconnect: true } } })
|
||||||
|
}
|
||||||
|
async function minio(service: any, template: any) {
|
||||||
|
const { rootUser, rootUserPassword, apiFqdn } = service.minio
|
||||||
|
const secrets = [
|
||||||
|
`MINIO_ROOT_PASSWORD@@@${rootUserPassword}`,
|
||||||
|
]
|
||||||
|
const settings = [
|
||||||
|
`MINIO_ROOT_USER@@@${rootUser}`,
|
||||||
|
`MINIO_SERVER_URL@@@${apiFqdn}`,
|
||||||
|
`MINIO_BROWSER_REDIRECT_URL@@@$$generate_fqdn`,
|
||||||
|
`MINIO_DOMAIN@@@$$generate_domain`,
|
||||||
|
]
|
||||||
|
await migrateSettings(settings, service, template);
|
||||||
|
await migrateSecrets(secrets, service);
|
||||||
|
|
||||||
|
// Disconnect old service data
|
||||||
|
// await prisma.service.update({ where: { id: service.id }, data: { minio: { disconnect: true } } })
|
||||||
|
}
|
||||||
|
async function fider(service: any, template: any) {
|
||||||
|
const { postgresqlUser, postgresqlPassword, postgresqlDatabase, jwtSecret, emailNoreply, emailMailgunApiKey, emailMailgunDomain, emailMailgunRegion, emailSmtpHost, emailSmtpPort, emailSmtpUser, emailSmtpPassword, emailSmtpEnableStartTls } = service.fider
|
||||||
|
const { id } = service
|
||||||
|
const secrets = [
|
||||||
|
`JWT_SECRET@@@${jwtSecret}`,
|
||||||
|
emailMailgunApiKey && `EMAIL_MAILGUN_API@@@${emailMailgunApiKey}`,
|
||||||
|
emailSmtpPassword && `EMAIL_SMTP_PASSWORD@@@${emailSmtpPassword}`,
|
||||||
|
`POSTGRES_PASSWORD@@@${postgresqlPassword}`,
|
||||||
|
`DATABASE_URL@@@${encrypt(`postgresql://${postgresqlUser}:${decrypt(postgresqlPassword)}@${id}-postgresql:5432/${postgresqlDatabase}?sslmode=disable`)}`
|
||||||
|
]
|
||||||
|
const settings = [
|
||||||
|
`BASE_URL@@@$$generate_fqdn`,
|
||||||
|
`EMAIL_NOREPLY@@@${emailNoreply || 'noreply@example.com'}`,
|
||||||
|
`EMAIL_MAILGUN_DOMAIN@@@${emailMailgunDomain || ''}`,
|
||||||
|
`EMAIL_MAILGUN_REGION@@@${emailMailgunRegion || ''}`,
|
||||||
|
`EMAIL_SMTP_HOST@@@${emailSmtpHost || ''}`,
|
||||||
|
`EMAIL_SMTP_PORT@@@${emailSmtpPort || 587}`,
|
||||||
|
`EMAIL_SMTP_USER@@@${emailSmtpUser || ''}`,
|
||||||
|
`EMAIL_SMTP_PASSWORD@@@${emailSmtpPassword || ''}`,
|
||||||
|
`EMAIL_SMTP_ENABLE_STARTTLS@@@${emailSmtpEnableStartTls || 'false'}`,
|
||||||
|
`POSTGRES_USER@@@${postgresqlUser}`,
|
||||||
|
`POSTGRES_DB@@@${postgresqlDatabase}`,
|
||||||
|
]
|
||||||
|
await migrateSettings(settings, service, template);
|
||||||
|
await migrateSecrets(secrets, service);
|
||||||
|
|
||||||
|
// Disconnect old service data
|
||||||
|
// await prisma.service.update({ where: { id: service.id }, data: { fider: { disconnect: true } } })
|
||||||
|
|
||||||
|
}
|
||||||
|
async function plausibleAnalytics(service: any, template: any) {
|
||||||
|
const { email, username, password, postgresqlUser, postgresqlPassword, postgresqlDatabase, secretKeyBase, scriptName } = service.plausibleAnalytics;
|
||||||
|
const { id } = service
|
||||||
|
|
||||||
|
const settings = [
|
||||||
|
`BASE_URL@@@$$generate_fqdn`,
|
||||||
|
`ADMIN_USER_EMAIL@@@${email}`,
|
||||||
|
`ADMIN_USER_NAME@@@${username}`,
|
||||||
|
`DISABLE_AUTH@@@false`,
|
||||||
|
`DISABLE_REGISTRATION@@@true`,
|
||||||
|
`POSTGRESQL_USERNAME@@@${postgresqlUser}`,
|
||||||
|
`POSTGRESQL_DATABASE@@@${postgresqlDatabase}`,
|
||||||
|
`SCRIPT_NAME@@@${scriptName}`,
|
||||||
|
]
|
||||||
|
const secrets = [
|
||||||
|
`ADMIN_USER_PWD@@@${password}`,
|
||||||
|
`SECRET_KEY_BASE@@@${secretKeyBase}`,
|
||||||
|
`POSTGRESQL_PASSWORD@@@${postgresqlPassword}`,
|
||||||
|
`DATABASE_URL@@@${encrypt(`postgres://${postgresqlUser}:${decrypt(postgresqlPassword)}@${id}-postgresql:5432/${postgresqlDatabase}`)}`,
|
||||||
|
]
|
||||||
|
await migrateSettings(settings, service, template);
|
||||||
|
await migrateSecrets(secrets, service);
|
||||||
|
|
||||||
|
// Disconnect old service data
|
||||||
|
// await prisma.service.update({ where: { id: service.id }, data: { plausibleAnalytics: { disconnect: true } } })
|
||||||
|
}
|
||||||
|
|
||||||
|
async function migrateSettings(settings: any[], service: any, template: any) {
|
||||||
|
for (const setting of settings) {
|
||||||
|
if (!setting) continue;
|
||||||
|
let [name, value] = setting.split('@@@')
|
||||||
|
let minio = name
|
||||||
|
if (name === 'MINIO_SERVER_URL') {
|
||||||
|
name = 'coolify_fqdn_minio_console'
|
||||||
|
}
|
||||||
|
if (!value || value === 'null') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let variableName = template.variables.find((v: any) => v.name === name)?.id
|
||||||
|
if (!variableName) {
|
||||||
|
variableName = `$$config_${name.toLowerCase()}`
|
||||||
|
}
|
||||||
|
// console.log('Migrating setting', name, value, 'for service', service.id, ', service name:', service.name, 'variableName: ', variableName)
|
||||||
|
|
||||||
|
await prisma.serviceSetting.findFirst({ where: { name: minio, serviceId: service.id } }) || await prisma.serviceSetting.create({ data: { name: minio, value, variableName, service: { connect: { id: service.id } } } })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function migrateSecrets(secrets: any[], service: any) {
|
||||||
|
for (const secret of secrets) {
|
||||||
|
if (!secret) continue;
|
||||||
|
let [name, value] = secret.split('@@@')
|
||||||
|
if (!value || value === 'null') {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// console.log('Migrating secret', name, value, 'for service', service.id, ', service name:', service.name)
|
||||||
|
await prisma.serviceSecret.findFirst({ where: { name, serviceId: service.id } }) || await prisma.serviceSecret.create({ data: { name, value, service: { connect: { id: service.id } } } })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function createVolumes(service: any, template: any) {
|
||||||
|
const volumes = [];
|
||||||
|
for (const s of Object.keys(template.services)) {
|
||||||
|
if (template.services[s].volumes && template.services[s].volumes.length > 0) {
|
||||||
|
for (const volume of template.services[s].volumes) {
|
||||||
|
const volumeName = volume.split(':')[0]
|
||||||
|
const volumePath = volume.split(':')[1]
|
||||||
|
const volumeService = s
|
||||||
|
volumes.push(`${volumeName}@@@${volumePath}@@@${volumeService}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const volume of volumes) {
|
||||||
|
const [volumeName, path, containerId] = volume.split('@@@')
|
||||||
|
// console.log('Creating volume', volumeName, path, containerId, 'for service', service.id, ', service name:', service.name)
|
||||||
|
await prisma.servicePersistentStorage.findFirst({ where: { volumeName, serviceId: service.id } }) || await prisma.servicePersistentStorage.create({ data: { volumeName, path, containerId, predefined: true, service: { connect: { id: service.id } } } })
|
||||||
|
}
|
||||||
|
}
|
@ -480,7 +480,6 @@ export const saveBuildLog = async ({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (isDev) return
|
|
||||||
return await prisma.buildLog.create({
|
return await prisma.buildLog.create({
|
||||||
data: {
|
data: {
|
||||||
line: addTimestamp, buildId, time: Number(day().valueOf()), applicationId
|
line: addTimestamp, buildId, time: Number(day().valueOf()), applicationId
|
||||||
|
@ -2,13 +2,14 @@ import { executeDockerCmd, prisma } from "../common"
|
|||||||
import { saveBuildLog } from "./common";
|
import { saveBuildLog } from "./common";
|
||||||
|
|
||||||
export default async function (data: any): Promise<void> {
|
export default async function (data: any): Promise<void> {
|
||||||
const { buildId, applicationId, tag, dockerId, debug, workdir, baseDirectory } = data
|
const { buildId, applicationId, tag, dockerId, debug, workdir, baseDirectory, baseImage } = data
|
||||||
try {
|
try {
|
||||||
await saveBuildLog({ line: `Building image started.`, buildId, applicationId });
|
await saveBuildLog({ line: `Building image started.`, buildId, applicationId });
|
||||||
await executeDockerCmd({
|
await executeDockerCmd({
|
||||||
|
buildId,
|
||||||
debug,
|
debug,
|
||||||
dockerId,
|
dockerId,
|
||||||
command: `pack build -p ${workdir}${baseDirectory} ${applicationId}:${tag} --builder heroku/buildpacks:20`
|
command: `pack build -p ${workdir}${baseDirectory} ${applicationId}:${tag} --builder ${baseImage}`
|
||||||
})
|
})
|
||||||
await saveBuildLog({ line: `Building image successful.`, buildId, applicationId });
|
await saveBuildLog({ line: `Building image successful.`, buildId, applicationId });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -14,13 +14,10 @@ import sshConfig from 'ssh-config';
|
|||||||
|
|
||||||
import { checkContainer, removeContainer } from './docker';
|
import { checkContainer, removeContainer } from './docker';
|
||||||
import { day } from './dayjs';
|
import { day } from './dayjs';
|
||||||
import * as serviceFields from './services/serviceFields';
|
|
||||||
import { saveBuildLog } from './buildPacks/common';
|
import { saveBuildLog } from './buildPacks/common';
|
||||||
import { scheduler } from './scheduler';
|
import { scheduler } from './scheduler';
|
||||||
import { supportedServiceTypesAndVersions } from './services/supportedVersions';
|
|
||||||
import { includeServices } from './services/common';
|
|
||||||
|
|
||||||
export const version = '3.10.16';
|
export const version = '3.11.0';
|
||||||
export const isDev = process.env.NODE_ENV === 'development';
|
export const isDev = process.env.NODE_ENV === 'development';
|
||||||
|
|
||||||
const algorithm = 'aes-256-ctr';
|
const algorithm = 'aes-256-ctr';
|
||||||
@ -44,7 +41,7 @@ export function getAPIUrl() {
|
|||||||
if (process.env.CODESANDBOX_HOST) {
|
if (process.env.CODESANDBOX_HOST) {
|
||||||
return `https://${process.env.CODESANDBOX_HOST.replace(/\$PORT/, '3001')}`;
|
return `https://${process.env.CODESANDBOX_HOST.replace(/\$PORT/, '3001')}`;
|
||||||
}
|
}
|
||||||
return isDev ? 'http://localhost:3001' : 'http://localhost:3000';
|
return isDev ? 'http://host.docker.internal:3001' : 'http://localhost:3000';
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getUIUrl() {
|
export function getUIUrl() {
|
||||||
@ -198,7 +195,7 @@ export const encrypt = (text: string) => {
|
|||||||
if (text) {
|
if (text) {
|
||||||
const iv = crypto.randomBytes(16);
|
const iv = crypto.randomBytes(16);
|
||||||
const cipher = crypto.createCipheriv(algorithm, process.env['COOLIFY_SECRET_KEY'], iv);
|
const cipher = crypto.createCipheriv(algorithm, process.env['COOLIFY_SECRET_KEY'], iv);
|
||||||
const encrypted = Buffer.concat([cipher.update(text), cipher.final()]);
|
const encrypted = Buffer.concat([cipher.update(text.trim()), cipher.final()]);
|
||||||
return JSON.stringify({
|
return JSON.stringify({
|
||||||
iv: iv.toString('hex'),
|
iv: iv.toString('hex'),
|
||||||
content: encrypted.toString('hex')
|
content: encrypted.toString('hex')
|
||||||
@ -244,7 +241,11 @@ export async function isDNSValid(hostname: any, domain: string): Promise<any> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getDomain(domain: string): string {
|
export function getDomain(domain: string): string {
|
||||||
return domain?.replace('https://', '').replace('http://', '');
|
if (domain) {
|
||||||
|
return domain?.replace('https://', '').replace('http://', '');
|
||||||
|
} else {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function isDomainConfigured({
|
export async function isDomainConfigured({
|
||||||
@ -279,9 +280,7 @@ export async function isDomainConfigured({
|
|||||||
where: {
|
where: {
|
||||||
OR: [
|
OR: [
|
||||||
{ fqdn: { endsWith: `//${nakedDomain}` } },
|
{ fqdn: { endsWith: `//${nakedDomain}` } },
|
||||||
{ fqdn: { endsWith: `//www.${nakedDomain}` } },
|
{ fqdn: { endsWith: `//www.${nakedDomain}` } }
|
||||||
{ minio: { apiFqdn: { endsWith: `//${nakedDomain}` } } },
|
|
||||||
{ minio: { apiFqdn: { endsWith: `//www.${nakedDomain}` } } }
|
|
||||||
],
|
],
|
||||||
id: { not: checkOwn ? undefined : id },
|
id: { not: checkOwn ? undefined : id },
|
||||||
destinationDocker: {
|
destinationDocker: {
|
||||||
@ -396,12 +395,6 @@ export function generateTimestamp(): string {
|
|||||||
return `${day().format('HH:mm:ss.SSS')}`;
|
return `${day().format('HH:mm:ss.SSS')}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function listServicesWithIncludes(): Promise<any> {
|
|
||||||
return await prisma.service.findMany({
|
|
||||||
include: includeServices,
|
|
||||||
orderBy: { createdAt: 'desc' }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export const supportedDatabaseTypesAndVersions = [
|
export const supportedDatabaseTypesAndVersions = [
|
||||||
{
|
{
|
||||||
@ -511,56 +504,62 @@ export async function createRemoteEngineConfiguration(id: string) {
|
|||||||
const localPort = await getFreeSSHLocalPort(id);
|
const localPort = await getFreeSSHLocalPort(id);
|
||||||
const {
|
const {
|
||||||
sshKey: { privateKey },
|
sshKey: { privateKey },
|
||||||
|
network,
|
||||||
remoteIpAddress,
|
remoteIpAddress,
|
||||||
remotePort,
|
remotePort,
|
||||||
remoteUser
|
remoteUser
|
||||||
} = await prisma.destinationDocker.findFirst({ where: { id }, include: { sshKey: true } });
|
} = await prisma.destinationDocker.findFirst({ where: { id }, include: { sshKey: true } });
|
||||||
await fs.writeFile(sshKeyFile, decrypt(privateKey) + '\n', { encoding: 'utf8', mode: 400 });
|
await fs.writeFile(sshKeyFile, decrypt(privateKey) + '\n', { encoding: 'utf8', mode: 400 });
|
||||||
// Needed for remote docker compose
|
// Needed for remote docker compose
|
||||||
const { stdout: numberOfSSHAgentsRunning } = await asyncExecShell(
|
// const { stdout: numberOfSSHAgentsRunning } = await asyncExecShell(
|
||||||
`ps ax | grep [s]sh-agent | grep coolify-ssh-agent.pid | grep -v grep | wc -l`
|
// `ps ax | grep [s]sh-agent | grep coolify-ssh-agent.pid | grep -v grep | wc -l`
|
||||||
);
|
// );
|
||||||
if (numberOfSSHAgentsRunning !== '' && Number(numberOfSSHAgentsRunning.trim()) == 0) {
|
// if (numberOfSSHAgentsRunning !== '' && Number(numberOfSSHAgentsRunning.trim()) == 0) {
|
||||||
try {
|
// try {
|
||||||
await fs.stat(`/tmp/coolify-ssh-agent.pid`);
|
// await fs.stat(`/tmp/coolify-ssh-agent.pid`);
|
||||||
await fs.rm(`/tmp/coolify-ssh-agent.pid`);
|
// await fs.rm(`/tmp/coolify-ssh-agent.pid`);
|
||||||
} catch (error) { }
|
// } catch (error) { }
|
||||||
await asyncExecShell(`eval $(ssh-agent -sa /tmp/coolify-ssh-agent.pid)`);
|
// await asyncExecShell(`eval $(ssh-agent -sa /tmp/coolify-ssh-agent.pid)`);
|
||||||
}
|
// }
|
||||||
await asyncExecShell(`SSH_AUTH_SOCK=/tmp/coolify-ssh-agent.pid ssh-add -q ${sshKeyFile}`);
|
// await asyncExecShell(`SSH_AUTH_SOCK=/tmp/coolify-ssh-agent.pid ssh-add -q ${sshKeyFile}`);
|
||||||
|
|
||||||
const { stdout: numberOfSSHTunnelsRunning } = await asyncExecShell(
|
// const { stdout: numberOfSSHTunnelsRunning } = await asyncExecShell(
|
||||||
`ps ax | grep 'ssh -F /dev/null -o StrictHostKeyChecking no -fNL ${localPort}:localhost:${remotePort}' | grep -v grep | wc -l`
|
// `ps ax | grep 'ssh -F /dev/null -o StrictHostKeyChecking no -fNL ${localPort}:localhost:${remotePort}' | grep -v grep | wc -l`
|
||||||
);
|
// );
|
||||||
if (numberOfSSHTunnelsRunning !== '' && Number(numberOfSSHTunnelsRunning.trim()) == 0) {
|
// if (numberOfSSHTunnelsRunning !== '' && Number(numberOfSSHTunnelsRunning.trim()) == 0) {
|
||||||
try {
|
// try {
|
||||||
await asyncExecShell(
|
// await asyncExecShell(
|
||||||
`SSH_AUTH_SOCK=/tmp/coolify-ssh-agent.pid ssh -F /dev/null -o "StrictHostKeyChecking no" -fNL ${localPort}:localhost:${remotePort} ${remoteUser}@${remoteIpAddress}`
|
// `SSH_AUTH_SOCK=/tmp/coolify-ssh-agent.pid ssh -F /dev/null -o "StrictHostKeyChecking no" -fNL ${localPort}:localhost:${remotePort} ${remoteUser}@${remoteIpAddress}`
|
||||||
);
|
// );
|
||||||
} catch (error) { }
|
// } catch (error) { }
|
||||||
}
|
// }
|
||||||
const config = sshConfig.parse('');
|
const config = sshConfig.parse('');
|
||||||
const foundWildcard = config.find({ Host: '*' });
|
const Host = `${remoteIpAddress}-remote`
|
||||||
if (!foundWildcard) {
|
|
||||||
config.append({
|
try {
|
||||||
Host: '*',
|
await asyncExecShell(`ssh-keygen -R ${Host}`);
|
||||||
StrictHostKeyChecking: 'no',
|
await asyncExecShell(`ssh-keygen -R ${remoteIpAddress}`);
|
||||||
ControlMaster: 'auto',
|
await asyncExecShell(`ssh-keygen -R localhost:${localPort}`);
|
||||||
ControlPath: `${homedir}/.ssh/coolify-%r@%h:%p`,
|
} catch (error) { }
|
||||||
ControlPersist: '10m'
|
|
||||||
})
|
|
||||||
}
|
const found = config.find({ Host });
|
||||||
const found = config.find({ Host: remoteIpAddress });
|
const foundIp = config.find({ Host: remoteIpAddress });
|
||||||
if (!found) {
|
|
||||||
config.append({
|
if (found) config.remove({ Host })
|
||||||
Host: remoteIpAddress,
|
if (foundIp) config.remove({ Host: remoteIpAddress })
|
||||||
Hostname: 'localhost',
|
|
||||||
Port: localPort.toString(),
|
config.append({
|
||||||
User: remoteUser,
|
Host,
|
||||||
IdentityFile: sshKeyFile,
|
Hostname: remoteIpAddress,
|
||||||
StrictHostKeyChecking: 'no'
|
Port: remotePort.toString(),
|
||||||
});
|
User: remoteUser,
|
||||||
}
|
StrictHostKeyChecking: 'no',
|
||||||
|
IdentityFile: sshKeyFile,
|
||||||
|
ControlMaster: 'auto',
|
||||||
|
ControlPath: `${homedir}/.ssh/coolify-${remoteIpAddress}-%r@%h:%p`,
|
||||||
|
ControlPersist: '10m'
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await fs.stat(`${homedir}/.ssh/`);
|
await fs.stat(`${homedir}/.ssh/`);
|
||||||
@ -571,27 +570,23 @@ export async function createRemoteEngineConfiguration(id: string) {
|
|||||||
}
|
}
|
||||||
export async function executeSSHCmd({ dockerId, command }) {
|
export async function executeSSHCmd({ dockerId, command }) {
|
||||||
const { execaCommand } = await import('execa')
|
const { execaCommand } = await import('execa')
|
||||||
let { remoteEngine, remoteIpAddress, engine, remoteUser } = await prisma.destinationDocker.findUnique({ where: { id: dockerId } })
|
let { remoteEngine, remoteIpAddress } = await prisma.destinationDocker.findUnique({ where: { id: dockerId } })
|
||||||
if (remoteEngine) {
|
if (remoteEngine) {
|
||||||
await createRemoteEngineConfiguration(dockerId)
|
await createRemoteEngineConfiguration(dockerId)
|
||||||
engine = `ssh://${remoteIpAddress}`
|
|
||||||
} else {
|
|
||||||
engine = 'unix:///var/run/docker.sock'
|
|
||||||
}
|
}
|
||||||
if (process.env.CODESANDBOX_HOST) {
|
if (process.env.CODESANDBOX_HOST) {
|
||||||
if (command.startsWith('docker compose')) {
|
if (command.startsWith('docker compose')) {
|
||||||
command = command.replace(/docker compose/gi, 'docker-compose')
|
command = command.replace(/docker compose/gi, 'docker-compose')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
command = `ssh ${remoteIpAddress} ${command}`
|
return await execaCommand(`ssh ${remoteIpAddress}-remote ${command}`)
|
||||||
return await execaCommand(command)
|
|
||||||
}
|
}
|
||||||
export async function executeDockerCmd({ debug, buildId, applicationId, dockerId, command }: { debug?: boolean, buildId?: string, applicationId?: string, dockerId: string, command: string }): Promise<any> {
|
export async function executeDockerCmd({ debug, buildId, applicationId, dockerId, command }: { debug?: boolean, buildId?: string, applicationId?: string, dockerId: string, command: string }): Promise<any> {
|
||||||
const { execaCommand } = await import('execa')
|
const { execaCommand } = await import('execa')
|
||||||
let { remoteEngine, remoteIpAddress, engine, remoteUser } = await prisma.destinationDocker.findUnique({ where: { id: dockerId } })
|
let { remoteEngine, remoteIpAddress, engine } = await prisma.destinationDocker.findUnique({ where: { id: dockerId } })
|
||||||
if (remoteEngine) {
|
if (remoteEngine) {
|
||||||
await createRemoteEngineConfiguration(dockerId);
|
await createRemoteEngineConfiguration(dockerId);
|
||||||
engine = `ssh://${remoteIpAddress}`;
|
engine = `ssh://${remoteIpAddress}-remote`;
|
||||||
} else {
|
} else {
|
||||||
engine = 'unix:///var/run/docker.sock';
|
engine = 'unix:///var/run/docker.sock';
|
||||||
}
|
}
|
||||||
@ -1095,6 +1090,7 @@ export const createDirectories = async ({
|
|||||||
repository: string;
|
repository: string;
|
||||||
buildId: string;
|
buildId: string;
|
||||||
}): Promise<{ workdir: string; repodir: string }> => {
|
}): Promise<{ workdir: string; repodir: string }> => {
|
||||||
|
repository = repository.replaceAll(' ', '')
|
||||||
const repodir = `/tmp/build-sources/${repository}/`;
|
const repodir = `/tmp/build-sources/${repository}/`;
|
||||||
const workdir = `/tmp/build-sources/${repository}/${buildId}`;
|
const workdir = `/tmp/build-sources/${repository}/${buildId}`;
|
||||||
let workdirFound = false;
|
let workdirFound = false;
|
||||||
@ -1399,7 +1395,7 @@ export async function startTraefikTCPProxy(
|
|||||||
`--entrypoints.tcp.address=:${publicPort}`,
|
`--entrypoints.tcp.address=:${publicPort}`,
|
||||||
`--entryPoints.tcp.forwardedHeaders.insecure=true`,
|
`--entryPoints.tcp.forwardedHeaders.insecure=true`,
|
||||||
`--providers.http.endpoint=${traefikUrl}?id=${id}&privatePort=${privatePort}&publicPort=${publicPort}&type=tcp&address=${dependentId}`,
|
`--providers.http.endpoint=${traefikUrl}?id=${id}&privatePort=${privatePort}&publicPort=${publicPort}&type=tcp&address=${dependentId}`,
|
||||||
'--providers.http.pollTimeout=2s',
|
'--providers.http.pollTimeout=10s',
|
||||||
'--log.level=error'
|
'--log.level=error'
|
||||||
],
|
],
|
||||||
ports: [`${publicPort}:${publicPort}`],
|
ports: [`${publicPort}:${publicPort}`],
|
||||||
@ -1447,13 +1443,18 @@ export async function getServiceFromDB({
|
|||||||
const settings = await prisma.setting.findFirst();
|
const settings = await prisma.setting.findFirst();
|
||||||
const body = await prisma.service.findFirst({
|
const body = await prisma.service.findFirst({
|
||||||
where: { id, teams: { some: { id: teamId === '0' ? undefined : teamId } } },
|
where: { id, teams: { some: { id: teamId === '0' ? undefined : teamId } } },
|
||||||
include: includeServices
|
include: {
|
||||||
|
destinationDocker: true,
|
||||||
|
persistentStorage: true,
|
||||||
|
serviceSecret: true,
|
||||||
|
serviceSetting: true,
|
||||||
|
wordpress: true
|
||||||
|
}
|
||||||
});
|
});
|
||||||
if (!body) {
|
if (!body) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
let { type } = body;
|
// body.type = fixType(body.type);
|
||||||
type = fixType(type);
|
|
||||||
|
|
||||||
if (body?.serviceSecret.length > 0) {
|
if (body?.serviceSecret.length > 0) {
|
||||||
body.serviceSecret = body.serviceSecret.map((s) => {
|
body.serviceSecret = body.serviceSecret.map((s) => {
|
||||||
@ -1461,87 +1462,18 @@ export async function getServiceFromDB({
|
|||||||
return s;
|
return s;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (body.wordpress) {
|
||||||
|
body.wordpress.ftpPassword = decrypt(body.wordpress.ftpPassword);
|
||||||
|
}
|
||||||
|
|
||||||
body[type] = { ...body[type], ...getUpdateableFields(type, body[type]) };
|
|
||||||
return { ...body, settings };
|
return { ...body, settings };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getServiceImage(type: string): string {
|
|
||||||
const found = supportedServiceTypesAndVersions.find((t) => t.name === type);
|
|
||||||
if (found) {
|
|
||||||
return found.baseImage;
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getServiceImages(type: string): string[] {
|
|
||||||
const found = supportedServiceTypesAndVersions.find((t) => t.name === type);
|
|
||||||
if (found) {
|
|
||||||
return found.images;
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function saveUpdateableFields(type: string, data: any) {
|
|
||||||
const update = {};
|
|
||||||
if (type && serviceFields[type]) {
|
|
||||||
serviceFields[type].map((k) => {
|
|
||||||
let temp = data[k.name];
|
|
||||||
if (temp) {
|
|
||||||
if (k.isEncrypted) {
|
|
||||||
temp = encrypt(temp);
|
|
||||||
}
|
|
||||||
if (k.isLowerCase) {
|
|
||||||
temp = temp.toLowerCase();
|
|
||||||
}
|
|
||||||
if (k.isNumber) {
|
|
||||||
temp = Number(temp);
|
|
||||||
}
|
|
||||||
if (k.isBoolean) {
|
|
||||||
temp = Boolean(temp);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (k.isNumber && temp === '') {
|
|
||||||
temp = null;
|
|
||||||
}
|
|
||||||
update[k.name] = temp;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return update;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getUpdateableFields(type: string, data: any) {
|
|
||||||
const update = {};
|
|
||||||
if (type && serviceFields[type]) {
|
|
||||||
serviceFields[type].map((k) => {
|
|
||||||
let temp = data[k.name];
|
|
||||||
if (temp) {
|
|
||||||
if (k.isEncrypted) {
|
|
||||||
temp = decrypt(temp);
|
|
||||||
}
|
|
||||||
update[k.name] = temp;
|
|
||||||
}
|
|
||||||
update[k.name] = temp;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return update;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function fixType(type) {
|
export function fixType(type) {
|
||||||
// Hack to fix the type case sensitivity...
|
return type?.replaceAll(' ', '').toLowerCase() || null;
|
||||||
if (type === 'plausibleanalytics') type = 'plausibleAnalytics';
|
|
||||||
if (type === 'meilisearch') type = 'meiliSearch';
|
|
||||||
return type;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getServiceMainPort = (service: string) => {
|
|
||||||
const serviceType = supportedServiceTypesAndVersions.find((s) => s.name === service);
|
|
||||||
if (serviceType) {
|
|
||||||
return serviceType.ports.main;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function makeLabelForServices(type) {
|
export function makeLabelForServices(type) {
|
||||||
return [
|
return [
|
||||||
'coolify.managed=true',
|
'coolify.managed=true',
|
||||||
@ -1681,7 +1613,9 @@ export function persistentVolumes(id, persistentStorage, config) {
|
|||||||
for (const [key, value] of Object.entries(config)) {
|
for (const [key, value] of Object.entries(config)) {
|
||||||
if (value.volumes) {
|
if (value.volumes) {
|
||||||
for (const volume of value.volumes) {
|
for (const volume of value.volumes) {
|
||||||
volumeSet.add(volume);
|
if (!volume.startsWith('/var/run/docker.sock')) {
|
||||||
|
volumeSet.add(volume);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,20 +1,170 @@
|
|||||||
import { createDirectories, getServiceFromDB, getServiceImage, getServiceMainPort, makeLabelForServices } from "./common";
|
import { isDev } from "./common";
|
||||||
|
import fs from 'fs/promises';
|
||||||
export async function defaultServiceConfigurations({ id, teamId }) {
|
export async function getTemplates() {
|
||||||
const service = await getServiceFromDB({ id, teamId });
|
let templates: any = [];
|
||||||
const { destinationDockerId, destinationDocker, type, serviceSecret } = service;
|
if (isDev) {
|
||||||
|
templates = JSON.parse(await (await fs.readFile('./templates.json')).toString())
|
||||||
const network = destinationDockerId && destinationDocker.network;
|
} else {
|
||||||
const port = getServiceMainPort(type);
|
templates = JSON.parse(await (await fs.readFile('/app/templates.json')).toString())
|
||||||
|
|
||||||
const { workdir } = await createDirectories({ repository: type, buildId: id });
|
|
||||||
|
|
||||||
const image = getServiceImage(type);
|
|
||||||
let secrets = [];
|
|
||||||
if (serviceSecret.length > 0) {
|
|
||||||
serviceSecret.forEach((secret) => {
|
|
||||||
secrets.push(`${secret.name}=${secret.value}`);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
return { ...service, network, port, workdir, image, secrets }
|
// if (!isDev) {
|
||||||
}
|
// templates.push({
|
||||||
|
// "templateVersion": "1.0.0",
|
||||||
|
// "defaultVersion": "latest",
|
||||||
|
// "name": "Test-Fake-Service",
|
||||||
|
// "description": "",
|
||||||
|
// "services": {
|
||||||
|
// "$$id": {
|
||||||
|
// "name": "Test-Fake-Service",
|
||||||
|
// "depends_on": [
|
||||||
|
// "$$id-postgresql",
|
||||||
|
// "$$id-redis"
|
||||||
|
// ],
|
||||||
|
// "image": "weblate/weblate:$$core_version",
|
||||||
|
// "volumes": [
|
||||||
|
// "$$id-data:/app/data",
|
||||||
|
// ],
|
||||||
|
// "environment": [
|
||||||
|
// `POSTGRES_SECRET=$$secret_postgres_secret`,
|
||||||
|
// `WEBLATE_SITE_DOMAIN=$$config_weblate_site_domain`,
|
||||||
|
// `WEBLATE_ADMIN_PASSWORD=$$secret_weblate_admin_password`,
|
||||||
|
// `POSTGRES_PASSWORD=$$secret_postgres_password`,
|
||||||
|
// `POSTGRES_USER=$$config_postgres_user`,
|
||||||
|
// `POSTGRES_DATABASE=$$config_postgres_db`,
|
||||||
|
// `POSTGRES_HOST=$$id-postgresql`,
|
||||||
|
// `POSTGRES_PORT=5432`,
|
||||||
|
// `REDIS_HOST=$$id-redis`,
|
||||||
|
// ],
|
||||||
|
// "ports": [
|
||||||
|
// "8080"
|
||||||
|
// ]
|
||||||
|
// },
|
||||||
|
// "$$id-postgresql": {
|
||||||
|
// "name": "PostgreSQL",
|
||||||
|
// "depends_on": [],
|
||||||
|
// "image": "postgres:14-alpine",
|
||||||
|
// "volumes": [
|
||||||
|
// "$$id-postgresql-data:/var/lib/postgresql/data",
|
||||||
|
// ],
|
||||||
|
// "environment": [
|
||||||
|
// "POSTGRES_USER=$$config_postgres_user",
|
||||||
|
// "POSTGRES_PASSWORD=$$secret_postgres_password",
|
||||||
|
// "POSTGRES_DB=$$config_postgres_db",
|
||||||
|
// ],
|
||||||
|
// "ports": []
|
||||||
|
// },
|
||||||
|
// "$$id-redis": {
|
||||||
|
// "name": "Redis",
|
||||||
|
// "depends_on": [],
|
||||||
|
// "image": "redis:7-alpine",
|
||||||
|
// "volumes": [
|
||||||
|
// "$$id-redis-data:/data",
|
||||||
|
// ],
|
||||||
|
// "environment": [],
|
||||||
|
// "ports": [],
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// "variables": [
|
||||||
|
// {
|
||||||
|
// "id": "$$config_weblate_site_domain",
|
||||||
|
// "main": "$$id",
|
||||||
|
// "name": "WEBLATE_SITE_DOMAIN",
|
||||||
|
// "label": "Weblate Domain",
|
||||||
|
// "defaultValue": "$$generate_domain",
|
||||||
|
// "description": "",
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// "id": "$$secret_weblate_admin_password",
|
||||||
|
// "main": "$$id",
|
||||||
|
// "name": "WEBLATE_ADMIN_PASSWORD",
|
||||||
|
// "label": "Weblate Admin Password",
|
||||||
|
// "defaultValue": "$$generate_password",
|
||||||
|
// "description": "",
|
||||||
|
// "extras": {
|
||||||
|
// "isVisibleOnUI": true,
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// "id": "$$secret_weblate_admin_password2",
|
||||||
|
// "name": "WEBLATE_ADMIN_PASSWORD2",
|
||||||
|
// "label": "Weblate Admin Password2",
|
||||||
|
// "defaultValue": "$$generate_password",
|
||||||
|
// "description": "",
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// "id": "$$config_postgres_user",
|
||||||
|
// "main": "$$id-postgresql",
|
||||||
|
// "name": "POSTGRES_USER",
|
||||||
|
// "label": "PostgreSQL User",
|
||||||
|
// "defaultValue": "$$generate_username",
|
||||||
|
// "description": "",
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// "id": "$$secret_postgres_password",
|
||||||
|
// "main": "$$id-postgresql",
|
||||||
|
// "name": "POSTGRES_PASSWORD",
|
||||||
|
// "label": "PostgreSQL Password",
|
||||||
|
// "defaultValue": "$$generate_password(32)",
|
||||||
|
// "description": "",
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// "id": "$$secret_postgres_password_hex32",
|
||||||
|
// "name": "POSTGRES_PASSWORD_hex32",
|
||||||
|
// "label": "PostgreSQL Password hex32",
|
||||||
|
// "defaultValue": "$$generate_hex(32)",
|
||||||
|
// "description": "",
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// "id": "$$config_postgres_something_hex32",
|
||||||
|
// "name": "POSTGRES_SOMETHING_HEX32",
|
||||||
|
// "label": "PostgreSQL Something hex32",
|
||||||
|
// "defaultValue": "$$generate_hex(32)",
|
||||||
|
// "description": "",
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// "id": "$$config_postgres_db",
|
||||||
|
// "main": "$$id-postgresql",
|
||||||
|
// "name": "POSTGRES_DB",
|
||||||
|
// "label": "PostgreSQL Database",
|
||||||
|
// "defaultValue": "weblate",
|
||||||
|
// "description": "",
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// "id": "$$secret_postgres_secret",
|
||||||
|
// "name": "POSTGRES_SECRET",
|
||||||
|
// "label": "PostgreSQL Secret",
|
||||||
|
// "defaultValue": "",
|
||||||
|
// "description": "",
|
||||||
|
// },
|
||||||
|
// ]
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
return templates
|
||||||
|
}
|
||||||
|
const compareSemanticVersions = (a: string, b: string) => {
|
||||||
|
const a1 = a.split('.');
|
||||||
|
const b1 = b.split('.');
|
||||||
|
const len = Math.min(a1.length, b1.length);
|
||||||
|
for (let i = 0; i < len; i++) {
|
||||||
|
const a2 = +a1[i] || 0;
|
||||||
|
const b2 = +b1[i] || 0;
|
||||||
|
if (a2 !== b2) {
|
||||||
|
return a2 > b2 ? 1 : -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return b1.length - a1.length;
|
||||||
|
};
|
||||||
|
export async function getTags(type: string) {
|
||||||
|
if (type) {
|
||||||
|
let tags: any = [];
|
||||||
|
if (isDev) {
|
||||||
|
tags = JSON.parse(await (await fs.readFile('./tags.json')).toString())
|
||||||
|
} else {
|
||||||
|
tags = JSON.parse(await (await fs.readFile('/app/tags.json')).toString())
|
||||||
|
}
|
||||||
|
tags = tags.find((tag: any) => tag.name.includes(type))
|
||||||
|
tags.tags = tags.tags.sort(compareSemanticVersions).reverse();
|
||||||
|
return tags
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
@ -1,367 +1,9 @@
|
|||||||
|
|
||||||
import cuid from 'cuid';
|
import { prisma } from '../common';
|
||||||
import { encrypt, generatePassword, prisma } from '../common';
|
|
||||||
|
|
||||||
export const includeServices: any = {
|
|
||||||
destinationDocker: true,
|
|
||||||
persistentStorage: true,
|
|
||||||
serviceSecret: true,
|
|
||||||
minio: true,
|
|
||||||
plausibleAnalytics: true,
|
|
||||||
vscodeserver: true,
|
|
||||||
wordpress: true,
|
|
||||||
ghost: true,
|
|
||||||
meiliSearch: true,
|
|
||||||
umami: true,
|
|
||||||
hasura: true,
|
|
||||||
fider: true,
|
|
||||||
moodle: true,
|
|
||||||
appwrite: true,
|
|
||||||
glitchTip: true,
|
|
||||||
searxng: true,
|
|
||||||
weblate: true,
|
|
||||||
taiga: true,
|
|
||||||
};
|
|
||||||
export async function configureServiceType({
|
|
||||||
id,
|
|
||||||
type
|
|
||||||
}: {
|
|
||||||
id: string;
|
|
||||||
type: string;
|
|
||||||
}): Promise<void> {
|
|
||||||
if (type === 'plausibleanalytics') {
|
|
||||||
const password = encrypt(generatePassword({}));
|
|
||||||
const postgresqlUser = cuid();
|
|
||||||
const postgresqlPassword = encrypt(generatePassword({}));
|
|
||||||
const postgresqlDatabase = 'plausibleanalytics';
|
|
||||||
const secretKeyBase = encrypt(generatePassword({ length: 64 }));
|
|
||||||
|
|
||||||
await prisma.service.update({
|
|
||||||
where: { id },
|
|
||||||
data: {
|
|
||||||
type,
|
|
||||||
plausibleAnalytics: {
|
|
||||||
create: {
|
|
||||||
postgresqlDatabase,
|
|
||||||
postgresqlUser,
|
|
||||||
postgresqlPassword,
|
|
||||||
password,
|
|
||||||
secretKeyBase
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else if (type === 'nocodb') {
|
|
||||||
await prisma.service.update({
|
|
||||||
where: { id },
|
|
||||||
data: { type }
|
|
||||||
});
|
|
||||||
} else if (type === 'minio') {
|
|
||||||
const rootUser = cuid();
|
|
||||||
const rootUserPassword = encrypt(generatePassword({}));
|
|
||||||
await prisma.service.update({
|
|
||||||
where: { id },
|
|
||||||
data: { type, minio: { create: { rootUser, rootUserPassword } } }
|
|
||||||
});
|
|
||||||
} else if (type === 'vscodeserver') {
|
|
||||||
const password = encrypt(generatePassword({}));
|
|
||||||
await prisma.service.update({
|
|
||||||
where: { id },
|
|
||||||
data: { type, vscodeserver: { create: { password } } }
|
|
||||||
});
|
|
||||||
} else if (type === 'wordpress') {
|
|
||||||
const mysqlUser = cuid();
|
|
||||||
const mysqlPassword = encrypt(generatePassword({}));
|
|
||||||
const mysqlRootUser = cuid();
|
|
||||||
const mysqlRootUserPassword = encrypt(generatePassword({}));
|
|
||||||
await prisma.service.update({
|
|
||||||
where: { id },
|
|
||||||
data: {
|
|
||||||
type,
|
|
||||||
wordpress: { create: { mysqlPassword, mysqlRootUserPassword, mysqlRootUser, mysqlUser } }
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else if (type === 'vaultwarden') {
|
|
||||||
await prisma.service.update({
|
|
||||||
where: { id },
|
|
||||||
data: {
|
|
||||||
type
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else if (type === 'languagetool') {
|
|
||||||
await prisma.service.update({
|
|
||||||
where: { id },
|
|
||||||
data: {
|
|
||||||
type
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else if (type === 'n8n') {
|
|
||||||
await prisma.service.update({
|
|
||||||
where: { id },
|
|
||||||
data: {
|
|
||||||
type
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else if (type === 'uptimekuma') {
|
|
||||||
await prisma.service.update({
|
|
||||||
where: { id },
|
|
||||||
data: {
|
|
||||||
type
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else if (type === 'ghost') {
|
|
||||||
const defaultEmail = `${cuid()}@example.com`;
|
|
||||||
const defaultPassword = encrypt(generatePassword({}));
|
|
||||||
const mariadbUser = cuid();
|
|
||||||
const mariadbPassword = encrypt(generatePassword({}));
|
|
||||||
const mariadbRootUser = cuid();
|
|
||||||
const mariadbRootUserPassword = encrypt(generatePassword({}));
|
|
||||||
|
|
||||||
await prisma.service.update({
|
|
||||||
where: { id },
|
|
||||||
data: {
|
|
||||||
type,
|
|
||||||
ghost: {
|
|
||||||
create: {
|
|
||||||
defaultEmail,
|
|
||||||
defaultPassword,
|
|
||||||
mariadbUser,
|
|
||||||
mariadbPassword,
|
|
||||||
mariadbRootUser,
|
|
||||||
mariadbRootUserPassword
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else if (type === 'meilisearch') {
|
|
||||||
const masterKey = encrypt(generatePassword({ length: 32 }));
|
|
||||||
await prisma.service.update({
|
|
||||||
where: { id },
|
|
||||||
data: {
|
|
||||||
type,
|
|
||||||
meiliSearch: { create: { masterKey } }
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else if (type === 'umami') {
|
|
||||||
const umamiAdminPassword = encrypt(generatePassword({}));
|
|
||||||
const postgresqlUser = cuid();
|
|
||||||
const postgresqlPassword = encrypt(generatePassword({}));
|
|
||||||
const postgresqlDatabase = 'umami';
|
|
||||||
const hashSalt = encrypt(generatePassword({ length: 64 }));
|
|
||||||
await prisma.service.update({
|
|
||||||
where: { id },
|
|
||||||
data: {
|
|
||||||
type,
|
|
||||||
umami: {
|
|
||||||
create: {
|
|
||||||
umamiAdminPassword,
|
|
||||||
postgresqlDatabase,
|
|
||||||
postgresqlPassword,
|
|
||||||
postgresqlUser,
|
|
||||||
hashSalt
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else if (type === 'hasura') {
|
|
||||||
const postgresqlUser = cuid();
|
|
||||||
const postgresqlPassword = encrypt(generatePassword({}));
|
|
||||||
const postgresqlDatabase = 'hasura';
|
|
||||||
const graphQLAdminPassword = encrypt(generatePassword({}));
|
|
||||||
await prisma.service.update({
|
|
||||||
where: { id },
|
|
||||||
data: {
|
|
||||||
type,
|
|
||||||
hasura: {
|
|
||||||
create: {
|
|
||||||
postgresqlDatabase,
|
|
||||||
postgresqlPassword,
|
|
||||||
postgresqlUser,
|
|
||||||
graphQLAdminPassword
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else if (type === 'fider') {
|
|
||||||
const postgresqlUser = cuid();
|
|
||||||
const postgresqlPassword = encrypt(generatePassword({}));
|
|
||||||
const postgresqlDatabase = 'fider';
|
|
||||||
const jwtSecret = encrypt(generatePassword({ length: 64, symbols: true }));
|
|
||||||
await prisma.service.update({
|
|
||||||
where: { id },
|
|
||||||
data: {
|
|
||||||
type,
|
|
||||||
fider: {
|
|
||||||
create: {
|
|
||||||
postgresqlDatabase,
|
|
||||||
postgresqlPassword,
|
|
||||||
postgresqlUser,
|
|
||||||
jwtSecret
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else if (type === 'moodle') {
|
|
||||||
const defaultUsername = cuid();
|
|
||||||
const defaultPassword = encrypt(generatePassword({}));
|
|
||||||
const defaultEmail = `${cuid()} @example.com`;
|
|
||||||
const mariadbUser = cuid();
|
|
||||||
const mariadbPassword = encrypt(generatePassword({}));
|
|
||||||
const mariadbDatabase = 'moodle_db';
|
|
||||||
const mariadbRootUser = cuid();
|
|
||||||
const mariadbRootUserPassword = encrypt(generatePassword({}));
|
|
||||||
await prisma.service.update({
|
|
||||||
where: { id },
|
|
||||||
data: {
|
|
||||||
type,
|
|
||||||
moodle: {
|
|
||||||
create: {
|
|
||||||
defaultUsername,
|
|
||||||
defaultPassword,
|
|
||||||
defaultEmail,
|
|
||||||
mariadbUser,
|
|
||||||
mariadbPassword,
|
|
||||||
mariadbDatabase,
|
|
||||||
mariadbRootUser,
|
|
||||||
mariadbRootUserPassword
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else if (type === 'appwrite') {
|
|
||||||
const opensslKeyV1 = encrypt(generatePassword({}));
|
|
||||||
const executorSecret = encrypt(generatePassword({}));
|
|
||||||
const redisPassword = encrypt(generatePassword({}));
|
|
||||||
const mariadbHost = `${id}-mariadb`
|
|
||||||
const mariadbUser = cuid();
|
|
||||||
const mariadbPassword = encrypt(generatePassword({}));
|
|
||||||
const mariadbDatabase = 'appwrite';
|
|
||||||
const mariadbRootUser = cuid();
|
|
||||||
const mariadbRootUserPassword = encrypt(generatePassword({}));
|
|
||||||
await prisma.service.update({
|
|
||||||
where: { id },
|
|
||||||
data: {
|
|
||||||
type,
|
|
||||||
appwrite: {
|
|
||||||
create: {
|
|
||||||
opensslKeyV1,
|
|
||||||
executorSecret,
|
|
||||||
redisPassword,
|
|
||||||
mariadbHost,
|
|
||||||
mariadbUser,
|
|
||||||
mariadbPassword,
|
|
||||||
mariadbDatabase,
|
|
||||||
mariadbRootUser,
|
|
||||||
mariadbRootUserPassword
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else if (type === 'glitchTip') {
|
|
||||||
const defaultUsername = cuid();
|
|
||||||
const defaultEmail = `${defaultUsername}@example.com`;
|
|
||||||
const defaultPassword = encrypt(generatePassword({}));
|
|
||||||
const postgresqlUser = cuid();
|
|
||||||
const postgresqlPassword = encrypt(generatePassword({}));
|
|
||||||
const postgresqlDatabase = 'glitchTip';
|
|
||||||
const secretKeyBase = encrypt(generatePassword({ length: 64 }));
|
|
||||||
|
|
||||||
await prisma.service.update({
|
|
||||||
where: { id },
|
|
||||||
data: {
|
|
||||||
type,
|
|
||||||
glitchTip: {
|
|
||||||
create: {
|
|
||||||
postgresqlDatabase,
|
|
||||||
postgresqlUser,
|
|
||||||
postgresqlPassword,
|
|
||||||
secretKeyBase,
|
|
||||||
defaultEmail,
|
|
||||||
defaultUsername,
|
|
||||||
defaultPassword,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else if (type === 'searxng') {
|
|
||||||
const secretKey = encrypt(generatePassword({ length: 32, isHex: true }))
|
|
||||||
const redisPassword = encrypt(generatePassword({}));
|
|
||||||
await prisma.service.update({
|
|
||||||
where: { id },
|
|
||||||
data: {
|
|
||||||
type,
|
|
||||||
searxng: {
|
|
||||||
create: {
|
|
||||||
secretKey,
|
|
||||||
redisPassword,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else if (type === 'weblate') {
|
|
||||||
const adminPassword = encrypt(generatePassword({}))
|
|
||||||
const postgresqlUser = cuid();
|
|
||||||
const postgresqlPassword = encrypt(generatePassword({}));
|
|
||||||
const postgresqlDatabase = 'weblate';
|
|
||||||
await prisma.service.update({
|
|
||||||
where: { id },
|
|
||||||
data: {
|
|
||||||
type,
|
|
||||||
weblate: {
|
|
||||||
create: {
|
|
||||||
adminPassword,
|
|
||||||
postgresqlHost: `${id}-postgresql`,
|
|
||||||
postgresqlPort: 5432,
|
|
||||||
postgresqlUser,
|
|
||||||
postgresqlPassword,
|
|
||||||
postgresqlDatabase,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else if (type === 'taiga') {
|
|
||||||
const secretKey = encrypt(generatePassword({}))
|
|
||||||
const erlangSecret = encrypt(generatePassword({}))
|
|
||||||
const rabbitMQUser = cuid();
|
|
||||||
const djangoAdminUser = cuid();
|
|
||||||
const djangoAdminPassword = encrypt(generatePassword({}))
|
|
||||||
const rabbitMQPassword = encrypt(generatePassword({}))
|
|
||||||
const postgresqlUser = cuid();
|
|
||||||
const postgresqlPassword = encrypt(generatePassword({}));
|
|
||||||
const postgresqlDatabase = 'taiga';
|
|
||||||
await prisma.service.update({
|
|
||||||
where: { id },
|
|
||||||
data: {
|
|
||||||
type,
|
|
||||||
taiga: {
|
|
||||||
create: {
|
|
||||||
secretKey,
|
|
||||||
erlangSecret,
|
|
||||||
djangoAdminUser,
|
|
||||||
djangoAdminPassword,
|
|
||||||
rabbitMQUser,
|
|
||||||
rabbitMQPassword,
|
|
||||||
postgresqlHost: `${id}-postgresql`,
|
|
||||||
postgresqlPort: 5432,
|
|
||||||
postgresqlUser,
|
|
||||||
postgresqlPassword,
|
|
||||||
postgresqlDatabase,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
await prisma.service.update({
|
|
||||||
where: { id },
|
|
||||||
data: {
|
|
||||||
type
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function removeService({ id }: { id: string }): Promise<void> {
|
export async function removeService({ id }: { id: string }): Promise<void> {
|
||||||
await prisma.serviceSecret.deleteMany({ where: { serviceId: id } });
|
await prisma.serviceSecret.deleteMany({ where: { serviceId: id } });
|
||||||
|
await prisma.serviceSetting.deleteMany({ where: { serviceId: id } });
|
||||||
await prisma.servicePersistentStorage.deleteMany({ where: { serviceId: id } });
|
await prisma.servicePersistentStorage.deleteMany({ where: { serviceId: id } });
|
||||||
await prisma.meiliSearch.deleteMany({ where: { serviceId: id } });
|
await prisma.meiliSearch.deleteMany({ where: { serviceId: id } });
|
||||||
await prisma.fider.deleteMany({ where: { serviceId: id } });
|
await prisma.fider.deleteMany({ where: { serviceId: id } });
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -1,278 +0,0 @@
|
|||||||
/*
|
|
||||||
Example of a supported version:
|
|
||||||
{
|
|
||||||
// Name used to identify the service internally
|
|
||||||
name: 'umami',
|
|
||||||
// Fancier name to show to the user
|
|
||||||
fancyName: 'Umami',
|
|
||||||
// Docker base image for the service
|
|
||||||
baseImage: 'ghcr.io/mikecao/umami',
|
|
||||||
// Optional: If there is any dependent image, you should list it here
|
|
||||||
images: [],
|
|
||||||
// Usable tags
|
|
||||||
versions: ['postgresql-latest'],
|
|
||||||
// Which tag is the recommended
|
|
||||||
recommendedVersion: 'postgresql-latest',
|
|
||||||
// Application's default port, Umami listens on 3000
|
|
||||||
ports: {
|
|
||||||
main: 3000
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
export const supportedServiceTypesAndVersions = [
|
|
||||||
{
|
|
||||||
name: 'plausibleanalytics',
|
|
||||||
fancyName: 'Plausible Analytics',
|
|
||||||
baseImage: 'plausible/analytics',
|
|
||||||
images: ['bitnami/postgresql:13.2.0', 'yandex/clickhouse-server:21.3.2.5'],
|
|
||||||
versions: ['latest', 'stable'],
|
|
||||||
recommendedVersion: 'stable',
|
|
||||||
ports: {
|
|
||||||
main: 8000
|
|
||||||
},
|
|
||||||
labels: ['analytics', 'plausible', 'plausible-analytics', 'gdpr', 'no-cookie']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'nocodb',
|
|
||||||
fancyName: 'NocoDB',
|
|
||||||
baseImage: 'nocodb/nocodb',
|
|
||||||
versions: ['latest'],
|
|
||||||
recommendedVersion: 'latest',
|
|
||||||
ports: {
|
|
||||||
main: 8080
|
|
||||||
},
|
|
||||||
labels: ['nocodb', 'airtable', 'database']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'minio',
|
|
||||||
fancyName: 'MinIO',
|
|
||||||
baseImage: 'minio/minio',
|
|
||||||
versions: ['latest'],
|
|
||||||
recommendedVersion: 'latest',
|
|
||||||
ports: {
|
|
||||||
main: 9001
|
|
||||||
},
|
|
||||||
labels: ['minio', 's3', 'storage']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'vscodeserver',
|
|
||||||
fancyName: 'VSCode Server',
|
|
||||||
baseImage: 'codercom/code-server',
|
|
||||||
versions: ['latest'],
|
|
||||||
recommendedVersion: 'latest',
|
|
||||||
ports: {
|
|
||||||
main: 8080
|
|
||||||
},
|
|
||||||
labels: ['vscodeserver', 'vscode', 'code-server', 'ide']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'wordpress',
|
|
||||||
fancyName: 'WordPress',
|
|
||||||
baseImage: 'wordpress',
|
|
||||||
images: ['bitnami/mysql:5.7'],
|
|
||||||
versions: ['latest', 'php8.1', 'php8.0', 'php7.4', 'php7.3'],
|
|
||||||
recommendedVersion: 'latest',
|
|
||||||
ports: {
|
|
||||||
main: 80
|
|
||||||
},
|
|
||||||
labels: ['wordpress', 'blog', 'cms']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'vaultwarden',
|
|
||||||
fancyName: 'Vaultwarden',
|
|
||||||
baseImage: 'vaultwarden/server',
|
|
||||||
versions: ['latest'],
|
|
||||||
recommendedVersion: 'latest',
|
|
||||||
ports: {
|
|
||||||
main: 80
|
|
||||||
},
|
|
||||||
labels: ['vaultwarden', 'password-manager', 'passwords']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'languagetool',
|
|
||||||
fancyName: 'LanguageTool',
|
|
||||||
baseImage: 'silviof/docker-languagetool',
|
|
||||||
versions: ['latest'],
|
|
||||||
recommendedVersion: 'latest',
|
|
||||||
ports: {
|
|
||||||
main: 8010
|
|
||||||
},
|
|
||||||
labels: ['languagetool', 'grammar', 'spell-checker']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'n8n',
|
|
||||||
fancyName: 'n8n',
|
|
||||||
baseImage: 'n8nio/n8n',
|
|
||||||
versions: ['latest'],
|
|
||||||
recommendedVersion: 'latest',
|
|
||||||
ports: {
|
|
||||||
main: 5678
|
|
||||||
},
|
|
||||||
labels: ['n8n', 'workflow', 'automation', 'ifttt', 'zapier', 'nodered']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'uptimekuma',
|
|
||||||
fancyName: 'Uptime Kuma',
|
|
||||||
baseImage: 'louislam/uptime-kuma',
|
|
||||||
versions: ['latest'],
|
|
||||||
recommendedVersion: 'latest',
|
|
||||||
ports: {
|
|
||||||
main: 3001
|
|
||||||
},
|
|
||||||
labels: ['uptimekuma', 'uptime', 'monitoring']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'ghost',
|
|
||||||
fancyName: 'Ghost',
|
|
||||||
baseImage: 'bitnami/ghost',
|
|
||||||
images: ['bitnami/mariadb'],
|
|
||||||
versions: ['latest'],
|
|
||||||
recommendedVersion: 'latest',
|
|
||||||
ports: {
|
|
||||||
main: 2368
|
|
||||||
},
|
|
||||||
labels: ['ghost', 'blog', 'cms']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'meilisearch',
|
|
||||||
fancyName: 'Meilisearch',
|
|
||||||
baseImage: 'getmeili/meilisearch',
|
|
||||||
images: [],
|
|
||||||
versions: ['latest'],
|
|
||||||
recommendedVersion: 'latest',
|
|
||||||
ports: {
|
|
||||||
main: 7700
|
|
||||||
},
|
|
||||||
labels: ['meilisearch', 'search', 'search-engine']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'umami',
|
|
||||||
fancyName: 'Umami',
|
|
||||||
baseImage: 'ghcr.io/umami-software/umami',
|
|
||||||
images: ['postgres:12-alpine'],
|
|
||||||
versions: ['postgresql-latest'],
|
|
||||||
recommendedVersion: 'postgresql-latest',
|
|
||||||
ports: {
|
|
||||||
main: 3000
|
|
||||||
},
|
|
||||||
labels: ['umami', 'analytics', 'gdpr', 'no-cookie']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'hasura',
|
|
||||||
fancyName: 'Hasura',
|
|
||||||
baseImage: 'hasura/graphql-engine',
|
|
||||||
images: ['postgres:12-alpine'],
|
|
||||||
versions: ['latest', 'v2.10.0', 'v2.5.1'],
|
|
||||||
recommendedVersion: 'v2.10.0',
|
|
||||||
ports: {
|
|
||||||
main: 8080
|
|
||||||
},
|
|
||||||
labels: ['hasura', 'graphql', 'database']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'fider',
|
|
||||||
fancyName: 'Fider',
|
|
||||||
baseImage: 'getfider/fider',
|
|
||||||
images: ['postgres:12-alpine'],
|
|
||||||
versions: ['stable'],
|
|
||||||
recommendedVersion: 'stable',
|
|
||||||
ports: {
|
|
||||||
main: 3000
|
|
||||||
},
|
|
||||||
labels: ['fider', 'feedback', 'suggestions']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'appwrite',
|
|
||||||
fancyName: 'Appwrite',
|
|
||||||
baseImage: 'appwrite/appwrite',
|
|
||||||
images: ['mariadb:10.7', 'redis:6.2-alpine', 'appwrite/telegraf:1.4.0'],
|
|
||||||
versions: ['latest', '1.0', '0.15.3'],
|
|
||||||
recommendedVersion: '1.0',
|
|
||||||
ports: {
|
|
||||||
main: 80
|
|
||||||
},
|
|
||||||
labels: ['appwrite', 'database', 'storage', 'api', 'serverless']
|
|
||||||
},
|
|
||||||
// {
|
|
||||||
// name: 'moodle',
|
|
||||||
// fancyName: 'Moodle',
|
|
||||||
// baseImage: 'bitnami/moodle',
|
|
||||||
// images: [],
|
|
||||||
// versions: ['latest', 'v4.0.2'],
|
|
||||||
// recommendedVersion: 'latest',
|
|
||||||
// ports: {
|
|
||||||
// main: 8080
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
{
|
|
||||||
name: 'glitchTip',
|
|
||||||
fancyName: 'GlitchTip',
|
|
||||||
baseImage: 'glitchtip/glitchtip',
|
|
||||||
images: ['postgres:14-alpine', 'redis:7-alpine'],
|
|
||||||
versions: ['latest'],
|
|
||||||
recommendedVersion: 'latest',
|
|
||||||
ports: {
|
|
||||||
main: 8000
|
|
||||||
},
|
|
||||||
labels: ['glitchtip', 'error-reporting', 'error', 'sentry', 'bugsnag']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'searxng',
|
|
||||||
fancyName: 'SearXNG',
|
|
||||||
baseImage: 'searxng/searxng',
|
|
||||||
images: [],
|
|
||||||
versions: ['latest'],
|
|
||||||
recommendedVersion: 'latest',
|
|
||||||
ports: {
|
|
||||||
main: 8080
|
|
||||||
},
|
|
||||||
labels: ['searxng', 'search', 'search-engine']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'weblate',
|
|
||||||
fancyName: 'Weblate',
|
|
||||||
baseImage: 'weblate/weblate',
|
|
||||||
images: ['postgres:14-alpine', 'redis:6-alpine'],
|
|
||||||
versions: ['latest'],
|
|
||||||
recommendedVersion: 'latest',
|
|
||||||
ports: {
|
|
||||||
main: 8080
|
|
||||||
},
|
|
||||||
labels: ['weblate', 'translation', 'localization']
|
|
||||||
},
|
|
||||||
// {
|
|
||||||
// name: 'taiga',
|
|
||||||
// fancyName: 'Taiga',
|
|
||||||
// baseImage: 'taigaio/taiga-front',
|
|
||||||
// images: ['postgres:12.3', 'rabbitmq:3.8-management-alpine', 'taigaio/taiga-back', 'taigaio/taiga-events', 'taigaio/taiga-protected'],
|
|
||||||
// versions: ['latest'],
|
|
||||||
// recommendedVersion: 'latest',
|
|
||||||
// ports: {
|
|
||||||
// main: 80
|
|
||||||
// }
|
|
||||||
// },
|
|
||||||
{
|
|
||||||
name: 'grafana',
|
|
||||||
fancyName: 'Grafana',
|
|
||||||
baseImage: 'grafana/grafana',
|
|
||||||
images: [],
|
|
||||||
versions: ['latest', '9.1.3', '9.1.2', '9.0.8', '8.3.11', '8.4.11', '8.5.11'],
|
|
||||||
recommendedVersion: 'latest',
|
|
||||||
ports: {
|
|
||||||
main: 3000
|
|
||||||
},
|
|
||||||
labels: ['grafana', 'monitoring', 'metrics', 'dashboard']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'trilium',
|
|
||||||
fancyName: 'Trilium Notes',
|
|
||||||
baseImage: 'zadam/trilium',
|
|
||||||
images: [],
|
|
||||||
versions: ['latest'],
|
|
||||||
recommendedVersion: 'latest',
|
|
||||||
ports: {
|
|
||||||
main: 8080
|
|
||||||
},
|
|
||||||
labels: ['trilium', 'notes', 'note-taking', 'wiki']
|
|
||||||
},
|
|
||||||
];
|
|
29
apps/api/src/realtime/index.ts
Normal file
29
apps/api/src/realtime/index.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
|
||||||
|
export default async (fastify) => {
|
||||||
|
fastify.io.use((socket, next) => {
|
||||||
|
const { token } = socket.handshake.auth;
|
||||||
|
if (token && fastify.jwt.verify(token)) {
|
||||||
|
next();
|
||||||
|
} else {
|
||||||
|
return next(new Error("unauthorized event"));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
fastify.io.on('connection', (socket: any) => {
|
||||||
|
const { token } = socket.handshake.auth;
|
||||||
|
const { teamId } = fastify.jwt.decode(token);
|
||||||
|
socket.join(teamId);
|
||||||
|
// console.info('Socket connected!', socket.id)
|
||||||
|
// console.info('Socket joined team!', teamId)
|
||||||
|
// socket.on('message', (message) => {
|
||||||
|
// console.log(message)
|
||||||
|
// })
|
||||||
|
// socket.on('error', (err) => {
|
||||||
|
// console.log(err)
|
||||||
|
// })
|
||||||
|
})
|
||||||
|
// fastify.io.on("error", (err) => {
|
||||||
|
// if (err && err.message === "unauthorized event") {
|
||||||
|
// fastify.io.disconnect();
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
}
|
@ -1,16 +1,15 @@
|
|||||||
import cuid from 'cuid';
|
import cuid from 'cuid';
|
||||||
import crypto from 'node:crypto'
|
import crypto from 'node:crypto'
|
||||||
import jsonwebtoken from 'jsonwebtoken';
|
import jsonwebtoken from 'jsonwebtoken';
|
||||||
import axios from 'axios';
|
|
||||||
import { FastifyReply } from 'fastify';
|
import { FastifyReply } from 'fastify';
|
||||||
import fs from 'fs/promises';
|
import fs from 'fs/promises';
|
||||||
import yaml from 'js-yaml';
|
import yaml from 'js-yaml';
|
||||||
import csv from 'csvtojson';
|
import csv from 'csvtojson';
|
||||||
|
|
||||||
import { day } from '../../../../lib/dayjs';
|
import { day } from '../../../../lib/dayjs';
|
||||||
import { makeLabelForStandaloneApplication, setDefaultBaseImage, setDefaultConfiguration } from '../../../../lib/buildPacks/common';
|
import { setDefaultBaseImage, setDefaultConfiguration } from '../../../../lib/buildPacks/common';
|
||||||
import { checkDomainsIsValidInDNS, checkDoubleBranch, checkExposedPort, createDirectories, decrypt, defaultComposeConfiguration, encrypt, errorHandler, executeDockerCmd, generateSshKeyPair, getContainerUsage, getDomain, isDev, isDomainConfigured, listSettings, prisma, stopBuild, uniqueName } from '../../../../lib/common';
|
import { checkDomainsIsValidInDNS, checkExposedPort, createDirectories, decrypt, defaultComposeConfiguration, encrypt, errorHandler, executeDockerCmd, generateSshKeyPair, getContainerUsage, getDomain, isDev, isDomainConfigured, listSettings, prisma, stopBuild, uniqueName } from '../../../../lib/common';
|
||||||
import { checkContainer, formatLabelsOnDocker, isContainerExited, removeContainer } from '../../../../lib/docker';
|
import { checkContainer, formatLabelsOnDocker, removeContainer } from '../../../../lib/docker';
|
||||||
|
|
||||||
import type { FastifyRequest } from 'fastify';
|
import type { FastifyRequest } from 'fastify';
|
||||||
import type { GetImages, CancelDeployment, CheckDNS, CheckRepository, DeleteApplication, DeleteSecret, DeleteStorage, GetApplicationLogs, GetBuildIdLogs, SaveApplication, SaveApplicationSettings, SaveApplicationSource, SaveDeployKey, SaveDestination, SaveSecret, SaveStorage, DeployApplication, CheckDomain, StopPreviewApplication, RestartPreviewApplication, GetBuilds } from './types';
|
import type { GetImages, CancelDeployment, CheckDNS, CheckRepository, DeleteApplication, DeleteSecret, DeleteStorage, GetApplicationLogs, GetBuildIdLogs, SaveApplication, SaveApplicationSettings, SaveApplicationSource, SaveDeployKey, SaveDestination, SaveSecret, SaveStorage, DeployApplication, CheckDomain, StopPreviewApplication, RestartPreviewApplication, GetBuilds } from './types';
|
||||||
@ -771,6 +770,7 @@ export async function saveApplicationSource(request: FastifyRequest<SaveApplicat
|
|||||||
|
|
||||||
export async function getGitHubToken(request: FastifyRequest<OnlyId>, reply: FastifyReply) {
|
export async function getGitHubToken(request: FastifyRequest<OnlyId>, reply: FastifyReply) {
|
||||||
try {
|
try {
|
||||||
|
const { default: got } = await import('got')
|
||||||
const { id } = request.params
|
const { id } = request.params
|
||||||
const { teamId } = request.user
|
const { teamId } = request.user
|
||||||
const application: any = await getApplicationFromDB(id, teamId);
|
const application: any = await getApplicationFromDB(id, teamId);
|
||||||
@ -782,13 +782,13 @@ export async function getGitHubToken(request: FastifyRequest<OnlyId>, reply: Fas
|
|||||||
const githubToken = jsonwebtoken.sign(payload, application.gitSource.githubApp.privateKey, {
|
const githubToken = jsonwebtoken.sign(payload, application.gitSource.githubApp.privateKey, {
|
||||||
algorithm: 'RS256'
|
algorithm: 'RS256'
|
||||||
});
|
});
|
||||||
const { data } = await axios.post(`${application.gitSource.apiUrl}/app/installations/${application.gitSource.githubApp.installationId}/access_tokens`, {}, {
|
const { token } = await got.post(`${application.gitSource.apiUrl}/app/installations/${application.gitSource.githubApp.installationId}/access_tokens`, {
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${githubToken}`
|
'Authorization': `Bearer ${githubToken}`,
|
||||||
}
|
}
|
||||||
})
|
}).json()
|
||||||
return reply.code(201).send({
|
return reply.code(201).send({
|
||||||
token: data.token
|
token
|
||||||
})
|
})
|
||||||
} catch ({ status, message }) {
|
} catch ({ status, message }) {
|
||||||
return errorHandler({ status, message })
|
return errorHandler({ status, message })
|
||||||
@ -819,7 +819,7 @@ export async function saveRepository(request, reply) {
|
|||||||
let { repository, branch, projectId, autodeploy, webhookToken, isPublicRepository = false } = request.body
|
let { repository, branch, projectId, autodeploy, webhookToken, isPublicRepository = false } = request.body
|
||||||
|
|
||||||
repository = repository.toLowerCase();
|
repository = repository.toLowerCase();
|
||||||
branch = branch.toLowerCase();
|
|
||||||
projectId = Number(projectId);
|
projectId = Number(projectId);
|
||||||
if (webhookToken) {
|
if (webhookToken) {
|
||||||
await prisma.application.update({
|
await prisma.application.update({
|
||||||
@ -970,6 +970,10 @@ export async function saveSecret(request: FastifyRequest<SaveSecret>, reply: Fas
|
|||||||
try {
|
try {
|
||||||
const { id } = request.params
|
const { id } = request.params
|
||||||
const { name, value, isBuildSecret = false } = request.body
|
const { name, value, isBuildSecret = false } = request.body
|
||||||
|
const found = await prisma.secret.findMany({ where: { applicationId: id, name } })
|
||||||
|
if (found.length > 0) {
|
||||||
|
throw ({ message: 'Secret already exists.' })
|
||||||
|
}
|
||||||
await prisma.secret.create({
|
await prisma.secret.create({
|
||||||
data: { name, value: encrypt(value.trim()), isBuildSecret, isPRMRSecret: false, application: { connect: { id } } }
|
data: { name, value: encrypt(value.trim()), isBuildSecret, isPRMRSecret: false, application: { connect: { id } } }
|
||||||
});
|
});
|
||||||
|
@ -204,8 +204,8 @@ export async function assignSSHKey(request: FastifyRequest) {
|
|||||||
}
|
}
|
||||||
export async function verifyRemoteDockerEngineFn(id: string) {
|
export async function verifyRemoteDockerEngineFn(id: string) {
|
||||||
await createRemoteEngineConfiguration(id);
|
await createRemoteEngineConfiguration(id);
|
||||||
const { remoteIpAddress, remoteUser, network, isCoolifyProxyUsed } = await prisma.destinationDocker.findFirst({ where: { id } })
|
const { remoteIpAddress, network, isCoolifyProxyUsed } = await prisma.destinationDocker.findFirst({ where: { id } })
|
||||||
const host = `ssh://${remoteUser}@${remoteIpAddress}`
|
const host = `ssh://${remoteIpAddress}-remote`
|
||||||
const { stdout } = await asyncExecShell(`DOCKER_HOST=${host} docker network ls --filter 'name=${network}' --no-trunc --format "{{json .}}"`);
|
const { stdout } = await asyncExecShell(`DOCKER_HOST=${host} docker network ls --filter 'name=${network}' --no-trunc --format "{{json .}}"`);
|
||||||
if (!stdout) {
|
if (!stdout) {
|
||||||
await asyncExecShell(`DOCKER_HOST=${host} docker network create --attachable ${network}`);
|
await asyncExecShell(`DOCKER_HOST=${host} docker network create --attachable ${network}`);
|
||||||
@ -215,8 +215,8 @@ export async function verifyRemoteDockerEngineFn(id: string) {
|
|||||||
await asyncExecShell(`DOCKER_HOST=${host} docker network create --attachable coolify-infra`);
|
await asyncExecShell(`DOCKER_HOST=${host} docker network create --attachable coolify-infra`);
|
||||||
}
|
}
|
||||||
if (isCoolifyProxyUsed) await startTraefikProxy(id);
|
if (isCoolifyProxyUsed) await startTraefikProxy(id);
|
||||||
const { stdout: daemonJson } = await executeSSHCmd({ dockerId: id, command: `cat /etc/docker/daemon.json` });
|
|
||||||
try {
|
try {
|
||||||
|
const { stdout: daemonJson } = await executeSSHCmd({ dockerId: id, command: `cat /etc/docker/daemon.json` });
|
||||||
let daemonJsonParsed = JSON.parse(daemonJson);
|
let daemonJsonParsed = JSON.parse(daemonJson);
|
||||||
let isUpdated = false;
|
let isUpdated = false;
|
||||||
if (!daemonJsonParsed['live-restore'] || daemonJsonParsed['live-restore'] !== true) {
|
if (!daemonJsonParsed['live-restore'] || daemonJsonParsed['live-restore'] !== true) {
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import axios from "axios";
|
|
||||||
import { compareVersions } from "compare-versions";
|
import { compareVersions } from "compare-versions";
|
||||||
import cuid from "cuid";
|
import cuid from "cuid";
|
||||||
import bcrypt from "bcryptjs";
|
import bcrypt from "bcryptjs";
|
||||||
|
import fs from 'fs/promises';
|
||||||
|
import yaml from 'js-yaml';
|
||||||
import {
|
import {
|
||||||
asyncExecShell,
|
asyncExecShell,
|
||||||
asyncSleep,
|
asyncSleep,
|
||||||
@ -13,7 +14,6 @@ import {
|
|||||||
uniqueName,
|
uniqueName,
|
||||||
version,
|
version,
|
||||||
} from "../../../lib/common";
|
} from "../../../lib/common";
|
||||||
import { supportedServiceTypesAndVersions } from "../../../lib/services/supportedVersions";
|
|
||||||
import { scheduler } from "../../../lib/scheduler";
|
import { scheduler } from "../../../lib/scheduler";
|
||||||
import type { FastifyReply, FastifyRequest } from "fastify";
|
import type { FastifyReply, FastifyRequest } from "fastify";
|
||||||
import type { Login, Update } from ".";
|
import type { Login, Update } from ".";
|
||||||
@ -36,16 +36,68 @@ export async function cleanupManually(request: FastifyRequest) {
|
|||||||
return errorHandler({ status, message });
|
return errorHandler({ status, message });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
export async function refreshTags() {
|
||||||
|
try {
|
||||||
|
const { default: got } = await import('got')
|
||||||
|
try {
|
||||||
|
if (isDev) {
|
||||||
|
const tags = await fs.readFile('./devTags.json', 'utf8')
|
||||||
|
await fs.writeFile('./tags.json', tags)
|
||||||
|
} else {
|
||||||
|
const tags = await got.get('https://get.coollabs.io/coolify/service-tags.json').text()
|
||||||
|
await fs.writeFile('/app/tags.json', tags)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
throw {
|
||||||
|
status: 500,
|
||||||
|
message: 'Could not fetch templates from get.coollabs.io'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {};
|
||||||
|
} catch ({ status, message }) {
|
||||||
|
return errorHandler({ status, message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export async function refreshTemplates() {
|
||||||
|
try {
|
||||||
|
const { default: got } = await import('got')
|
||||||
|
try {
|
||||||
|
if (isDev) {
|
||||||
|
const response = await fs.readFile('./devTemplates.yaml', 'utf8')
|
||||||
|
await fs.writeFile('./templates.json', JSON.stringify(yaml.load(response)))
|
||||||
|
} else {
|
||||||
|
const response = await got.get('https://get.coollabs.io/coolify/service-templates.yaml').text()
|
||||||
|
await fs.writeFile('/app/templates.json', JSON.stringify(yaml.load(response)))
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
throw {
|
||||||
|
status: 500,
|
||||||
|
message: 'Could not fetch templates from get.coollabs.io'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {};
|
||||||
|
} catch ({ status, message }) {
|
||||||
|
return errorHandler({ status, message });
|
||||||
|
}
|
||||||
|
}
|
||||||
export async function checkUpdate(request: FastifyRequest) {
|
export async function checkUpdate(request: FastifyRequest) {
|
||||||
try {
|
try {
|
||||||
|
const { default: got } = await import('got')
|
||||||
const isStaging =
|
const isStaging =
|
||||||
request.hostname === "staging.coolify.io" ||
|
request.hostname === "staging.coolify.io" ||
|
||||||
request.hostname === "arm.coolify.io";
|
request.hostname === "arm.coolify.io";
|
||||||
const currentVersion = version;
|
const currentVersion = version;
|
||||||
const { data: versions } = await axios.get(
|
const { coolify } = await got.get('https://get.coollabs.io/versions.json', {
|
||||||
`https://get.coollabs.io/versions.json?appId=${process.env["COOLIFY_APP_ID"]}&version=${currentVersion}`
|
searchParams: {
|
||||||
);
|
appId: process.env['COOLIFY_APP_ID'] || undefined,
|
||||||
const latestVersion = versions["coolify"].main.version;
|
version: currentVersion
|
||||||
|
}
|
||||||
|
}).json()
|
||||||
|
const latestVersion = coolify.main.version;
|
||||||
const isUpdateAvailable = compareVersions(latestVersion, currentVersion);
|
const isUpdateAvailable = compareVersions(latestVersion, currentVersion);
|
||||||
if (isStaging) {
|
if (isStaging) {
|
||||||
return {
|
return {
|
||||||
@ -357,7 +409,6 @@ export async function getCurrentUser(
|
|||||||
return {
|
return {
|
||||||
settings: await prisma.setting.findFirst(),
|
settings: await prisma.setting.findFirst(),
|
||||||
pendingInvitations,
|
pendingInvitations,
|
||||||
supportedServiceTypesAndVersions,
|
|
||||||
token,
|
token,
|
||||||
...request.user,
|
...request.user,
|
||||||
};
|
};
|
||||||
|
@ -1,9 +1,6 @@
|
|||||||
import { FastifyPluginAsync } from 'fastify';
|
import { FastifyPluginAsync } from 'fastify';
|
||||||
import { checkUpdate, login, showDashboard, update, resetQueue, getCurrentUser, cleanupManually, restartCoolify } from './handlers';
|
import { checkUpdate, login, showDashboard, update, resetQueue, getCurrentUser, cleanupManually, restartCoolify, refreshTemplates } from './handlers';
|
||||||
import { GetCurrentUser } from './types';
|
import { GetCurrentUser } from './types';
|
||||||
import pump from 'pump'
|
|
||||||
import fs from 'fs'
|
|
||||||
import { asyncExecShell, encrypt, errorHandler, prisma } from '../../../lib/common';
|
|
||||||
|
|
||||||
export interface Update {
|
export interface Update {
|
||||||
Body: { latestVersion: string }
|
Body: { latestVersion: string }
|
||||||
@ -55,6 +52,10 @@ const root: FastifyPluginAsync = async (fastify): Promise<void> => {
|
|||||||
fastify.post('/internal/cleanup', {
|
fastify.post('/internal/cleanup', {
|
||||||
onRequest: [fastify.authenticate]
|
onRequest: [fastify.authenticate]
|
||||||
}, async (request) => await cleanupManually(request));
|
}, async (request) => await cleanupManually(request));
|
||||||
|
|
||||||
|
fastify.post('/internal/refreshTemplates', {
|
||||||
|
onRequest: [fastify.authenticate]
|
||||||
|
}, async () => await refreshTemplates());
|
||||||
};
|
};
|
||||||
|
|
||||||
export default root;
|
export default root;
|
||||||
|
@ -1,15 +1,17 @@
|
|||||||
import type { FastifyReply, FastifyRequest } from 'fastify';
|
import type { FastifyReply, FastifyRequest } from 'fastify';
|
||||||
import fs from 'fs/promises';
|
import fs from 'fs/promises';
|
||||||
import yaml from 'js-yaml';
|
import yaml from 'js-yaml';
|
||||||
import { prisma, uniqueName, asyncExecShell, getServiceFromDB, getContainerUsage, isDomainConfigured, saveUpdateableFields, fixType, decrypt, encrypt, ComposeFile, getFreePublicPort, getDomain, errorHandler, generatePassword, isDev, stopTcpHttpProxy, executeDockerCmd, checkDomainsIsValidInDNS, checkExposedPort, listSettings } from '../../../../lib/common';
|
import bcrypt from 'bcryptjs';
|
||||||
import { day } from '../../../../lib/dayjs';
|
|
||||||
import { checkContainer, isContainerExited } from '../../../../lib/docker';
|
|
||||||
import cuid from 'cuid';
|
import cuid from 'cuid';
|
||||||
|
|
||||||
import type { OnlyId } from '../../../../types';
|
import { prisma, uniqueName, asyncExecShell, getServiceFromDB, getContainerUsage, isDomainConfigured, fixType, decrypt, encrypt, ComposeFile, getFreePublicPort, getDomain, errorHandler, generatePassword, isDev, stopTcpHttpProxy, executeDockerCmd, checkDomainsIsValidInDNS, checkExposedPort, listSettings } from '../../../../lib/common';
|
||||||
|
import { day } from '../../../../lib/dayjs';
|
||||||
|
import { checkContainer, } from '../../../../lib/docker';
|
||||||
|
import { removeService } from '../../../../lib/services/common';
|
||||||
|
import { getTags, getTemplates } from '../../../../lib/services';
|
||||||
|
|
||||||
import type { ActivateWordpressFtp, CheckService, CheckServiceDomain, DeleteServiceSecret, DeleteServiceStorage, GetServiceLogs, SaveService, SaveServiceDestination, SaveServiceSecret, SaveServiceSettings, SaveServiceStorage, SaveServiceType, SaveServiceVersion, ServiceStartStop, SetGlitchTipSettings, SetWordpressSettings } from './types';
|
import type { ActivateWordpressFtp, CheckService, CheckServiceDomain, DeleteServiceSecret, DeleteServiceStorage, GetServiceLogs, SaveService, SaveServiceDestination, SaveServiceSecret, SaveServiceSettings, SaveServiceStorage, SaveServiceType, SaveServiceVersion, ServiceStartStop, SetGlitchTipSettings, SetWordpressSettings } from './types';
|
||||||
import { supportedServiceTypesAndVersions } from '../../../../lib/services/supportedVersions';
|
import type { OnlyId } from '../../../../types';
|
||||||
import { configureServiceType, removeService } from '../../../../lib/services/common';
|
|
||||||
|
|
||||||
export async function listServices(request: FastifyRequest) {
|
export async function listServices(request: FastifyRequest) {
|
||||||
try {
|
try {
|
||||||
@ -67,30 +69,186 @@ export async function getServiceStatus(request: FastifyRequest<OnlyId>) {
|
|||||||
try {
|
try {
|
||||||
const teamId = request.user.teamId;
|
const teamId = request.user.teamId;
|
||||||
const { id } = request.params;
|
const { id } = request.params;
|
||||||
|
|
||||||
let isRunning = false;
|
|
||||||
let isExited = false
|
|
||||||
let isRestarting = false;
|
|
||||||
const service = await getServiceFromDB({ id, teamId });
|
const service = await getServiceFromDB({ id, teamId });
|
||||||
const { destinationDockerId, settings } = service;
|
const { destinationDockerId, settings } = service;
|
||||||
|
let payload = {}
|
||||||
if (destinationDockerId) {
|
if (destinationDockerId) {
|
||||||
const status = await checkContainer({ dockerId: service.destinationDocker.id, container: id });
|
const { stdout: containers } = await executeDockerCmd({
|
||||||
if (status?.found) {
|
dockerId: service.destinationDocker.id,
|
||||||
isRunning = status.status.isRunning;
|
command:
|
||||||
isExited = status.status.isExited;
|
`docker ps -a --filter "label=com.docker.compose.project=${id}" --format '{{json .}}'`
|
||||||
isRestarting = status.status.isRestarting
|
});
|
||||||
|
const containersArray = containers.trim().split('\n');
|
||||||
|
if (containersArray.length > 0 && containersArray[0] !== '') {
|
||||||
|
const templates = await getTemplates();
|
||||||
|
let template = templates.find(t => t.type === service.type);
|
||||||
|
template = JSON.parse(JSON.stringify(template).replaceAll('$$id', service.id));
|
||||||
|
for (const container of containersArray) {
|
||||||
|
let isRunning = false;
|
||||||
|
let isExited = false;
|
||||||
|
let isRestarting = false;
|
||||||
|
let isExcluded = false;
|
||||||
|
const containerObj = JSON.parse(container);
|
||||||
|
const exclude = template.services[containerObj.Names]?.exclude;
|
||||||
|
if (exclude) {
|
||||||
|
payload[containerObj.Names] = {
|
||||||
|
status: {
|
||||||
|
isExcluded: true,
|
||||||
|
isRunning: false,
|
||||||
|
isExited: false,
|
||||||
|
isRestarting: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const status = containerObj.State
|
||||||
|
if (status === 'running') {
|
||||||
|
isRunning = true;
|
||||||
|
}
|
||||||
|
if (status === 'exited') {
|
||||||
|
isExited = true;
|
||||||
|
}
|
||||||
|
if (status === 'restarting') {
|
||||||
|
isRestarting = true;
|
||||||
|
}
|
||||||
|
payload[containerObj.Names] = {
|
||||||
|
status: {
|
||||||
|
isExcluded,
|
||||||
|
isRunning,
|
||||||
|
isExited,
|
||||||
|
isRestarting
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return {
|
return payload
|
||||||
isRunning,
|
|
||||||
isExited,
|
|
||||||
settings
|
|
||||||
}
|
|
||||||
} catch ({ status, message }) {
|
} catch ({ status, message }) {
|
||||||
return errorHandler({ status, message })
|
return errorHandler({ status, message })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
export async function parseAndFindServiceTemplates(service: any, workdir?: string, isDeploy: boolean = false) {
|
||||||
|
const templates = await getTemplates()
|
||||||
|
const foundTemplate = templates.find(t => fixType(t.type) === service.type)
|
||||||
|
let parsedTemplate = {}
|
||||||
|
if (foundTemplate) {
|
||||||
|
if (!isDeploy) {
|
||||||
|
for (const [key, value] of Object.entries(foundTemplate.services)) {
|
||||||
|
const realKey = key.replace('$$id', service.id)
|
||||||
|
let name = value.name
|
||||||
|
if (!name) {
|
||||||
|
if (Object.keys(foundTemplate.services).length === 1) {
|
||||||
|
name = foundTemplate.name || service.name.toLowerCase()
|
||||||
|
} else {
|
||||||
|
if (key === '$$id') {
|
||||||
|
name = foundTemplate.name || key.replaceAll('$$id-', '') || service.name.toLowerCase()
|
||||||
|
} else {
|
||||||
|
name = key.replaceAll('$$id-', '') || service.name.toLowerCase()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
parsedTemplate[realKey] = {
|
||||||
|
name,
|
||||||
|
documentation: value.documentation || foundTemplate.documentation || 'https://docs.coollabs.io',
|
||||||
|
image: value.image,
|
||||||
|
environment: [],
|
||||||
|
fqdns: [],
|
||||||
|
proxy: {}
|
||||||
|
}
|
||||||
|
if (value.environment?.length > 0) {
|
||||||
|
for (const env of value.environment) {
|
||||||
|
let [envKey, ...envValue] = env.split('=')
|
||||||
|
envValue = envValue.join("=")
|
||||||
|
const variable = foundTemplate.variables.find(v => v.name === envKey) || foundTemplate.variables.find(v => v.id === envValue)
|
||||||
|
if (variable) {
|
||||||
|
const id = variable.id.replaceAll('$$', '')
|
||||||
|
const label = variable?.label
|
||||||
|
const description = variable?.description
|
||||||
|
const defaultValue = variable?.defaultValue
|
||||||
|
const main = variable?.main || '$$id'
|
||||||
|
const type = variable?.type || 'input'
|
||||||
|
const placeholder = variable?.placeholder || ''
|
||||||
|
const readOnly = variable?.readOnly || false
|
||||||
|
const required = variable?.required || false
|
||||||
|
if (envValue.startsWith('$$config') || variable?.showOnConfiguration) {
|
||||||
|
if (envValue.startsWith('$$config_coolify')) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
parsedTemplate[realKey].environment.push(
|
||||||
|
{ id, name: envKey, value: envValue, main, label, description, defaultValue, type, placeholder, required, readOnly }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (value?.proxy && value.proxy.length > 0) {
|
||||||
|
for (const proxyValue of value.proxy) {
|
||||||
|
if (proxyValue.domain) {
|
||||||
|
const variable = foundTemplate.variables.find(v => v.id === proxyValue.domain)
|
||||||
|
if (variable) {
|
||||||
|
const { id, name, label, description, defaultValue, required = false } = variable
|
||||||
|
const found = await prisma.serviceSetting.findFirst({ where: { serviceId: service.id , variableName: proxyValue.domain } })
|
||||||
|
parsedTemplate[realKey].fqdns.push(
|
||||||
|
{ id, name, value: found?.value || '', label, description, defaultValue, required }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
parsedTemplate = foundTemplate
|
||||||
|
}
|
||||||
|
let strParsedTemplate = JSON.stringify(parsedTemplate)
|
||||||
|
|
||||||
|
// replace $$id and $$workdir
|
||||||
|
strParsedTemplate = strParsedTemplate.replaceAll('$$id', service.id)
|
||||||
|
strParsedTemplate = strParsedTemplate.replaceAll('$$core_version', service.version || foundTemplate.defaultVersion)
|
||||||
|
|
||||||
|
// replace $$fqdn
|
||||||
|
if (workdir) {
|
||||||
|
strParsedTemplate = strParsedTemplate.replaceAll('$$workdir', workdir)
|
||||||
|
}
|
||||||
|
|
||||||
|
// replace $$config
|
||||||
|
if (service.serviceSetting.length > 0) {
|
||||||
|
for (const setting of service.serviceSetting) {
|
||||||
|
const { value, variableName } = setting
|
||||||
|
const regex = new RegExp(`\\$\\$config_${variableName.replace('$$config_', '')}\\"`, 'gi')
|
||||||
|
if (value === '$$generate_fqdn') {
|
||||||
|
strParsedTemplate = strParsedTemplate.replaceAll(regex, service.fqdn + "\"" || '' + "\"")
|
||||||
|
} else if (value === '$$generate_domain') {
|
||||||
|
strParsedTemplate = strParsedTemplate.replaceAll(regex, getDomain(service.fqdn) + "\"")
|
||||||
|
} else if (service.destinationDocker?.network && value === '$$generate_network') {
|
||||||
|
strParsedTemplate = strParsedTemplate.replaceAll(regex, service.destinationDocker.network + "\"")
|
||||||
|
} else {
|
||||||
|
strParsedTemplate = strParsedTemplate.replaceAll(regex, value + "\"")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// replace $$secret
|
||||||
|
if (service.serviceSecret.length > 0) {
|
||||||
|
for (const secret of service.serviceSecret) {
|
||||||
|
const { name, value } = secret
|
||||||
|
const regexHashed = new RegExp(`\\$\\$hashed\\$\\$secret_${name}\\"`, 'gi')
|
||||||
|
const regex = new RegExp(`\\$\\$secret_${name}\\"`, 'gi')
|
||||||
|
if (value) {
|
||||||
|
strParsedTemplate = strParsedTemplate.replaceAll(regexHashed, bcrypt.hashSync(value.replaceAll("\"", "\\\""), 10) + "\"")
|
||||||
|
strParsedTemplate = strParsedTemplate.replaceAll(regex, value.replaceAll("\"", "\\\"") + "\"")
|
||||||
|
} else {
|
||||||
|
strParsedTemplate = strParsedTemplate.replaceAll(regexHashed, "\"")
|
||||||
|
strParsedTemplate = strParsedTemplate.replaceAll(regex, "\"")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
parsedTemplate = JSON.parse(strParsedTemplate)
|
||||||
|
}
|
||||||
|
return parsedTemplate
|
||||||
|
}
|
||||||
|
|
||||||
export async function getService(request: FastifyRequest<OnlyId>) {
|
export async function getService(request: FastifyRequest<OnlyId>) {
|
||||||
try {
|
try {
|
||||||
@ -100,9 +258,17 @@ export async function getService(request: FastifyRequest<OnlyId>) {
|
|||||||
if (!service) {
|
if (!service) {
|
||||||
throw { status: 404, message: 'Service not found.' }
|
throw { status: 404, message: 'Service not found.' }
|
||||||
}
|
}
|
||||||
|
let template = {}
|
||||||
|
let tags = []
|
||||||
|
if (service.type) {
|
||||||
|
template = await parseAndFindServiceTemplates(service)
|
||||||
|
tags = await getTags(service.type)
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
settings: await listSettings(),
|
settings: await listSettings(),
|
||||||
service
|
service,
|
||||||
|
template,
|
||||||
|
tags
|
||||||
}
|
}
|
||||||
} catch ({ status, message }) {
|
} catch ({ status, message }) {
|
||||||
return errorHandler({ status, message })
|
return errorHandler({ status, message })
|
||||||
@ -111,7 +277,7 @@ export async function getService(request: FastifyRequest<OnlyId>) {
|
|||||||
export async function getServiceType(request: FastifyRequest) {
|
export async function getServiceType(request: FastifyRequest) {
|
||||||
try {
|
try {
|
||||||
return {
|
return {
|
||||||
types: supportedServiceTypesAndVersions
|
services: await getTemplates()
|
||||||
}
|
}
|
||||||
} catch ({ status, message }) {
|
} catch ({ status, message }) {
|
||||||
return errorHandler({ status, message })
|
return errorHandler({ status, message })
|
||||||
@ -121,25 +287,79 @@ export async function saveServiceType(request: FastifyRequest<SaveServiceType>,
|
|||||||
try {
|
try {
|
||||||
const { id } = request.params;
|
const { id } = request.params;
|
||||||
const { type } = request.body;
|
const { type } = request.body;
|
||||||
await configureServiceType({ id, type });
|
const templates = await getTemplates()
|
||||||
return reply.code(201).send()
|
let foundTemplate = templates.find(t => fixType(t.type) === fixType(type))
|
||||||
} catch ({ status, message }) {
|
if (foundTemplate) {
|
||||||
return errorHandler({ status, message })
|
foundTemplate = JSON.parse(JSON.stringify(foundTemplate).replaceAll('$$id', id))
|
||||||
}
|
if (foundTemplate.variables.length > 0) {
|
||||||
}
|
for (const variable of foundTemplate.variables) {
|
||||||
export async function getServiceVersions(request: FastifyRequest<OnlyId>) {
|
const { defaultValue } = variable;
|
||||||
try {
|
const regex = /^\$\$.*\((\d+)\)$/g;
|
||||||
const teamId = request.user.teamId;
|
const length = Number(regex.exec(defaultValue)?.[1]) || undefined
|
||||||
const { id } = request.params;
|
if (variable.defaultValue.startsWith('$$generate_password')) {
|
||||||
const { type } = await getServiceFromDB({ id, teamId });
|
variable.value = generatePassword({ length });
|
||||||
return {
|
} else if (variable.defaultValue.startsWith('$$generate_hex')) {
|
||||||
type,
|
variable.value = generatePassword({ length, isHex: true });
|
||||||
versions: supportedServiceTypesAndVersions.find((name) => name.name === type).versions
|
} else if (variable.defaultValue.startsWith('$$generate_username')) {
|
||||||
|
variable.value = cuid();
|
||||||
|
} else {
|
||||||
|
variable.value = variable.defaultValue || '';
|
||||||
|
}
|
||||||
|
const foundVariableSomewhereElse = foundTemplate.variables.find(v => v.defaultValue.includes(variable.id))
|
||||||
|
if (foundVariableSomewhereElse) {
|
||||||
|
foundVariableSomewhereElse.value = foundVariableSomewhereElse.value.replaceAll(variable.id, variable.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const variable of foundTemplate.variables) {
|
||||||
|
if (variable.id.startsWith('$$secret_')) {
|
||||||
|
const found = await prisma.serviceSecret.findFirst({ where: { name: variable.name, serviceId: id } })
|
||||||
|
if (!found) {
|
||||||
|
await prisma.serviceSecret.create({
|
||||||
|
data: { name: variable.name, value: encrypt(variable.value) || '', service: { connect: { id } } }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
if (variable.id.startsWith('$$config_')) {
|
||||||
|
const found = await prisma.serviceSetting.findFirst({ where: { name: variable.name, serviceId: id } })
|
||||||
|
if (!found) {
|
||||||
|
await prisma.serviceSetting.create({
|
||||||
|
data: { name: variable.name, value: variable.value.toString(), variableName: variable.id, service: { connect: { id } } }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const service of Object.keys(foundTemplate.services)) {
|
||||||
|
if (foundTemplate.services[service].volumes) {
|
||||||
|
for (const volume of foundTemplate.services[service].volumes) {
|
||||||
|
const [volumeName, path] = volume.split(':')
|
||||||
|
if (!volumeName.startsWith('/')) {
|
||||||
|
const found = await prisma.servicePersistentStorage.findFirst({ where: { volumeName, serviceId: id } })
|
||||||
|
if (!found) {
|
||||||
|
await prisma.servicePersistentStorage.create({
|
||||||
|
data: { volumeName, path, containerId: service, predefined: true, service: { connect: { id } } }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await prisma.service.update({ where: { id }, data: { type, version: foundTemplate.defaultVersion, templateVersion: foundTemplate.templateVersion } })
|
||||||
|
|
||||||
|
if (type.startsWith('wordpress')) {
|
||||||
|
await prisma.service.update({ where: { id }, data: { wordpress: { create: {} } } })
|
||||||
|
}
|
||||||
|
return reply.code(201).send()
|
||||||
|
} else {
|
||||||
|
throw { status: 404, message: 'Service type not found.' }
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch ({ status, message }) {
|
} catch ({ status, message }) {
|
||||||
return errorHandler({ status, message })
|
return errorHandler({ status, message })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function saveServiceVersion(request: FastifyRequest<SaveServiceVersion>, reply: FastifyReply) {
|
export async function saveServiceVersion(request: FastifyRequest<SaveServiceVersion>, reply: FastifyReply) {
|
||||||
try {
|
try {
|
||||||
const { id } = request.params;
|
const { id } = request.params;
|
||||||
@ -186,7 +406,7 @@ export async function getServiceUsage(request: FastifyRequest<OnlyId>) {
|
|||||||
}
|
}
|
||||||
export async function getServiceLogs(request: FastifyRequest<GetServiceLogs>) {
|
export async function getServiceLogs(request: FastifyRequest<GetServiceLogs>) {
|
||||||
try {
|
try {
|
||||||
const { id } = request.params;
|
const { id, containerId } = request.params;
|
||||||
let { since = 0 } = request.query
|
let { since = 0 } = request.query
|
||||||
if (since !== 0) {
|
if (since !== 0) {
|
||||||
since = day(since).unix();
|
since = day(since).unix();
|
||||||
@ -197,10 +417,8 @@ export async function getServiceLogs(request: FastifyRequest<GetServiceLogs>) {
|
|||||||
});
|
});
|
||||||
if (destinationDockerId) {
|
if (destinationDockerId) {
|
||||||
try {
|
try {
|
||||||
// const found = await checkContainer({ dockerId, container: id })
|
|
||||||
// if (found) {
|
|
||||||
const { default: ansi } = await import('strip-ansi')
|
const { default: ansi } = await import('strip-ansi')
|
||||||
const { stdout, stderr } = await executeDockerCmd({ dockerId, command: `docker logs --since ${since} --tail 5000 --timestamps ${id}` })
|
const { stdout, stderr } = await executeDockerCmd({ dockerId, command: `docker logs --since ${since} --tail 5000 --timestamps ${containerId}` })
|
||||||
const stripLogsStdout = stdout.toString().split('\n').map((l) => ansi(l)).filter((a) => a);
|
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 stripLogsStderr = stderr.toString().split('\n').map((l) => ansi(l)).filter((a) => a);
|
||||||
const logs = stripLogsStderr.concat(stripLogsStdout)
|
const logs = stripLogsStderr.concat(stripLogsStdout)
|
||||||
@ -208,7 +426,10 @@ export async function getServiceLogs(request: FastifyRequest<GetServiceLogs>) {
|
|||||||
return { logs: sortedLogs }
|
return { logs: sortedLogs }
|
||||||
// }
|
// }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const { statusCode } = error;
|
const { statusCode, stderr } = error;
|
||||||
|
if (stderr.startsWith('Error: No such container')) {
|
||||||
|
return { logs: [], noContainer: true }
|
||||||
|
}
|
||||||
if (statusCode === 404) {
|
if (statusCode === 404) {
|
||||||
return {
|
return {
|
||||||
logs: []
|
logs: []
|
||||||
@ -258,26 +479,22 @@ export async function checkServiceDomain(request: FastifyRequest<CheckServiceDom
|
|||||||
export async function checkService(request: FastifyRequest<CheckService>) {
|
export async function checkService(request: FastifyRequest<CheckService>) {
|
||||||
try {
|
try {
|
||||||
const { id } = request.params;
|
const { id } = request.params;
|
||||||
let { fqdn, exposePort, forceSave, otherFqdns, dualCerts } = request.body;
|
let { fqdn, exposePort, forceSave, dualCerts, otherFqdn = false } = request.body;
|
||||||
|
|
||||||
|
const domainsList = await prisma.serviceSetting.findMany({ where: { variableName: { startsWith: '$$config_coolify_fqdn' } } })
|
||||||
|
|
||||||
if (fqdn) fqdn = fqdn.toLowerCase();
|
if (fqdn) fqdn = fqdn.toLowerCase();
|
||||||
if (otherFqdns && otherFqdns.length > 0) otherFqdns = otherFqdns.map((f) => f.toLowerCase());
|
|
||||||
if (exposePort) exposePort = Number(exposePort);
|
if (exposePort) exposePort = Number(exposePort);
|
||||||
|
|
||||||
const { destinationDocker: { remoteIpAddress, remoteEngine, engine }, exposePort: configuredPort } = await prisma.service.findUnique({ where: { id }, include: { destinationDocker: true } })
|
const { destinationDocker: { remoteIpAddress, remoteEngine, engine }, exposePort: configuredPort } = await prisma.service.findUnique({ where: { id }, include: { destinationDocker: true } })
|
||||||
const { isDNSCheckEnabled } = await prisma.setting.findFirst({});
|
const { isDNSCheckEnabled } = await prisma.setting.findFirst({});
|
||||||
|
|
||||||
let found = await isDomainConfigured({ id, fqdn, remoteIpAddress });
|
let found = await isDomainConfigured({ id, fqdn, remoteIpAddress, checkOwn: otherFqdn });
|
||||||
if (found) {
|
if (found) {
|
||||||
throw { status: 500, message: `Domain ${getDomain(fqdn).replace('www.', '')} is already in use!` }
|
throw { status: 500, message: `Domain ${getDomain(fqdn).replace('www.', '')} is already in use!` }
|
||||||
}
|
}
|
||||||
if (otherFqdns && otherFqdns.length > 0) {
|
if (domainsList.find(d => getDomain(d.value) === getDomain(fqdn))) {
|
||||||
for (const ofqdn of otherFqdns) {
|
throw { status: 500, message: `Domain ${getDomain(fqdn).replace('www.', '')} is already in use!` }
|
||||||
found = await isDomainConfigured({ id, fqdn: ofqdn, remoteIpAddress });
|
|
||||||
if (found) {
|
|
||||||
throw { status: 500, message: `Domain ${getDomain(ofqdn).replace('www.', '')} is already in use!` }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (exposePort) await checkExposedPort({ id, configuredPort, exposePort, engine, remoteEngine, remoteIpAddress })
|
if (exposePort) await checkExposedPort({ id, configuredPort, exposePort, engine, remoteEngine, remoteIpAddress })
|
||||||
if (isDNSCheckEnabled && !isDev && !forceSave) {
|
if (isDNSCheckEnabled && !isDev && !forceSave) {
|
||||||
@ -293,20 +510,33 @@ export async function checkService(request: FastifyRequest<CheckService>) {
|
|||||||
export async function saveService(request: FastifyRequest<SaveService>, reply: FastifyReply) {
|
export async function saveService(request: FastifyRequest<SaveService>, reply: FastifyReply) {
|
||||||
try {
|
try {
|
||||||
const { id } = request.params;
|
const { id } = request.params;
|
||||||
let { name, fqdn, exposePort, type } = request.body;
|
let { name, fqdn, exposePort, type, serviceSetting, version } = request.body;
|
||||||
|
|
||||||
if (fqdn) fqdn = fqdn.toLowerCase();
|
if (fqdn) fqdn = fqdn.toLowerCase();
|
||||||
if (exposePort) exposePort = Number(exposePort);
|
if (exposePort) exposePort = Number(exposePort);
|
||||||
|
|
||||||
type = fixType(type)
|
type = fixType(type)
|
||||||
const update = saveUpdateableFields(type, request.body[type])
|
|
||||||
const data = {
|
const data = {
|
||||||
fqdn,
|
fqdn,
|
||||||
name,
|
name,
|
||||||
exposePort,
|
exposePort,
|
||||||
|
version,
|
||||||
}
|
}
|
||||||
if (Object.keys(update).length > 0) {
|
const templates = await getTemplates()
|
||||||
data[type] = { update: update }
|
const service = await prisma.service.findUnique({ where: { id } })
|
||||||
|
const foundTemplate = templates.find(t => fixType(t.type) === fixType(service.type))
|
||||||
|
for (const setting of serviceSetting) {
|
||||||
|
let { id: settingId, name, value, changed = false, isNew = false, variableName } = setting
|
||||||
|
if (value) {
|
||||||
|
if (changed) {
|
||||||
|
await prisma.serviceSetting.update({ where: { id: settingId }, data: { value } })
|
||||||
|
}
|
||||||
|
if (isNew) {
|
||||||
|
if (!variableName) {
|
||||||
|
variableName = foundTemplate.variables.find(v => v.name === name).id
|
||||||
|
}
|
||||||
|
await prisma.serviceSetting.create({ data: { name, value, variableName, service: { connect: { id } } } })
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
await prisma.service.update({
|
await prisma.service.update({
|
||||||
where: { id }, data
|
where: { id }, data
|
||||||
@ -320,11 +550,19 @@ export async function saveService(request: FastifyRequest<SaveService>, reply: F
|
|||||||
export async function getServiceSecrets(request: FastifyRequest<OnlyId>) {
|
export async function getServiceSecrets(request: FastifyRequest<OnlyId>) {
|
||||||
try {
|
try {
|
||||||
const { id } = request.params
|
const { id } = request.params
|
||||||
|
const teamId = request.user.teamId;
|
||||||
|
const service = await getServiceFromDB({ id, teamId });
|
||||||
let secrets = await prisma.serviceSecret.findMany({
|
let secrets = await prisma.serviceSecret.findMany({
|
||||||
where: { serviceId: id },
|
where: { serviceId: id },
|
||||||
orderBy: { createdAt: 'desc' }
|
orderBy: { createdAt: 'desc' }
|
||||||
});
|
});
|
||||||
|
const templates = await getTemplates()
|
||||||
|
const foundTemplate = templates.find(t => fixType(t.type) === service.type)
|
||||||
secrets = secrets.map((secret) => {
|
secrets = secrets.map((secret) => {
|
||||||
|
const foundVariable = foundTemplate?.variables.find(v => v.name === secret.name) || null
|
||||||
|
if (foundVariable) {
|
||||||
|
secret.readOnly = foundVariable.readOnly
|
||||||
|
}
|
||||||
secret.value = decrypt(secret.value);
|
secret.value = decrypt(secret.value);
|
||||||
return secret;
|
return secret;
|
||||||
});
|
});
|
||||||
@ -341,7 +579,6 @@ export async function saveServiceSecret(request: FastifyRequest<SaveServiceSecre
|
|||||||
try {
|
try {
|
||||||
const { id } = request.params
|
const { id } = request.params
|
||||||
let { name, value, isNew } = request.body
|
let { name, value, isNew } = request.body
|
||||||
|
|
||||||
if (isNew) {
|
if (isNew) {
|
||||||
const found = await prisma.serviceSecret.findFirst({ where: { name, serviceId: id } });
|
const found = await prisma.serviceSecret.findFirst({ where: { name, serviceId: id } });
|
||||||
if (found) {
|
if (found) {
|
||||||
@ -400,16 +637,21 @@ export async function getServiceStorages(request: FastifyRequest<OnlyId>) {
|
|||||||
export async function saveServiceStorage(request: FastifyRequest<SaveServiceStorage>, reply: FastifyReply) {
|
export async function saveServiceStorage(request: FastifyRequest<SaveServiceStorage>, reply: FastifyReply) {
|
||||||
try {
|
try {
|
||||||
const { id } = request.params
|
const { id } = request.params
|
||||||
const { path, newStorage, storageId } = request.body
|
const { path, isNewStorage, storageId, containerId } = request.body
|
||||||
|
|
||||||
if (newStorage) {
|
if (isNewStorage) {
|
||||||
|
const volumeName = `${id}-custom${path.replace(/\//gi, '-')}`
|
||||||
|
const found = await prisma.servicePersistentStorage.findFirst({ where: { path, containerId } });
|
||||||
|
if (found) {
|
||||||
|
throw { status: 500, message: 'Persistent storage already exists for this container and path.' }
|
||||||
|
}
|
||||||
await prisma.servicePersistentStorage.create({
|
await prisma.servicePersistentStorage.create({
|
||||||
data: { path, service: { connect: { id } } }
|
data: { path, volumeName, containerId, service: { connect: { id } } }
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
await prisma.servicePersistentStorage.update({
|
await prisma.servicePersistentStorage.update({
|
||||||
where: { id: storageId },
|
where: { id: storageId },
|
||||||
data: { path }
|
data: { path, containerId }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return reply.code(201).send()
|
return reply.code(201).send()
|
||||||
@ -420,9 +662,8 @@ export async function saveServiceStorage(request: FastifyRequest<SaveServiceStor
|
|||||||
|
|
||||||
export async function deleteServiceStorage(request: FastifyRequest<DeleteServiceStorage>) {
|
export async function deleteServiceStorage(request: FastifyRequest<DeleteServiceStorage>) {
|
||||||
try {
|
try {
|
||||||
const { id } = request.params
|
const { storageId } = request.body
|
||||||
const { path } = request.body
|
await prisma.servicePersistentStorage.deleteMany({ where: { id: storageId } });
|
||||||
await prisma.servicePersistentStorage.deleteMany({ where: { serviceId: id, path } });
|
|
||||||
return {}
|
return {}
|
||||||
} catch ({ status, message }) {
|
} catch ({ status, message }) {
|
||||||
return errorHandler({ status, message })
|
return errorHandler({ status, message })
|
||||||
@ -478,14 +719,17 @@ export async function activatePlausibleUsers(request: FastifyRequest<OnlyId>, re
|
|||||||
const {
|
const {
|
||||||
destinationDockerId,
|
destinationDockerId,
|
||||||
destinationDocker,
|
destinationDocker,
|
||||||
plausibleAnalytics: { postgresqlUser, postgresqlPassword, postgresqlDatabase }
|
serviceSecret
|
||||||
} = await getServiceFromDB({ id, teamId });
|
} = await getServiceFromDB({ id, teamId });
|
||||||
if (destinationDockerId) {
|
if (destinationDockerId) {
|
||||||
await executeDockerCmd({
|
const databaseUrl = serviceSecret.find((secret) => secret.name === 'DATABASE_URL');
|
||||||
dockerId: destinationDocker.id,
|
if (databaseUrl) {
|
||||||
command: `docker exec ${id}-postgresql psql -H postgresql://${postgresqlUser}:${postgresqlPassword}@localhost:5432/${postgresqlDatabase} -c "UPDATE users SET email_verified = true;"`
|
await executeDockerCmd({
|
||||||
})
|
dockerId: destinationDocker.id,
|
||||||
return await reply.code(201).send()
|
command: `docker exec ${id}-postgresql psql -H ${databaseUrl.value} -c "UPDATE users SET email_verified = true;"`
|
||||||
|
})
|
||||||
|
return await reply.code(201).send()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
throw { status: 500, message: 'Could not activate users.' }
|
throw { status: 500, message: 'Could not activate users.' }
|
||||||
} catch ({ status, message }) {
|
} catch ({ status, message }) {
|
||||||
|
@ -16,7 +16,6 @@ import {
|
|||||||
getServiceStorages,
|
getServiceStorages,
|
||||||
getServiceType,
|
getServiceType,
|
||||||
getServiceUsage,
|
getServiceUsage,
|
||||||
getServiceVersions,
|
|
||||||
listServices,
|
listServices,
|
||||||
newService,
|
newService,
|
||||||
saveService,
|
saveService,
|
||||||
@ -64,16 +63,15 @@ const root: FastifyPluginAsync = async (fastify): Promise<void> => {
|
|||||||
fastify.get('/:id/configuration/type', async (request) => await getServiceType(request));
|
fastify.get('/:id/configuration/type', async (request) => await getServiceType(request));
|
||||||
fastify.post<SaveServiceType>('/:id/configuration/type', async (request, reply) => await saveServiceType(request, reply));
|
fastify.post<SaveServiceType>('/:id/configuration/type', async (request, reply) => await saveServiceType(request, reply));
|
||||||
|
|
||||||
fastify.get<OnlyId>('/:id/configuration/version', async (request) => await getServiceVersions(request));
|
|
||||||
fastify.post<SaveServiceVersion>('/:id/configuration/version', async (request, reply) => await saveServiceVersion(request, reply));
|
fastify.post<SaveServiceVersion>('/:id/configuration/version', async (request, reply) => await saveServiceVersion(request, reply));
|
||||||
|
|
||||||
fastify.post<SaveServiceDestination>('/:id/configuration/destination', async (request, reply) => await saveServiceDestination(request, reply));
|
fastify.post<SaveServiceDestination>('/:id/configuration/destination', async (request, reply) => await saveServiceDestination(request, reply));
|
||||||
|
|
||||||
fastify.get<OnlyId>('/:id/usage', async (request) => await getServiceUsage(request));
|
fastify.get<OnlyId>('/:id/usage', async (request) => await getServiceUsage(request));
|
||||||
fastify.get<GetServiceLogs>('/:id/logs', async (request) => await getServiceLogs(request));
|
fastify.get<GetServiceLogs>('/:id/logs/:containerId', async (request) => await getServiceLogs(request));
|
||||||
|
|
||||||
fastify.post<ServiceStartStop>('/:id/:type/start', async (request) => await startService(request));
|
fastify.post<ServiceStartStop>('/:id/start', async (request) => await startService(request, fastify));
|
||||||
fastify.post<ServiceStartStop>('/:id/:type/stop', async (request) => await stopService(request));
|
fastify.post<ServiceStartStop>('/:id/stop', async (request) => await stopService(request));
|
||||||
fastify.post<ServiceStartStop & SetWordpressSettings & SetGlitchTipSettings>('/:id/:type/settings', async (request, reply) => await setSettingsService(request, reply));
|
fastify.post<ServiceStartStop & SetWordpressSettings & SetGlitchTipSettings>('/:id/:type/settings', async (request, reply) => await setSettingsService(request, reply));
|
||||||
|
|
||||||
fastify.post<OnlyId>('/:id/plausibleanalytics/activate', async (request, reply) => await activatePlausibleUsers(request, reply));
|
fastify.post<OnlyId>('/:id/plausibleanalytics/activate', async (request, reply) => await activatePlausibleUsers(request, reply));
|
||||||
|
@ -15,9 +15,13 @@ export interface SaveServiceDestination extends OnlyId {
|
|||||||
destinationId: string
|
destinationId: string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export interface GetServiceLogs extends OnlyId {
|
export interface GetServiceLogs{
|
||||||
|
Params: {
|
||||||
|
id: string,
|
||||||
|
containerId: string
|
||||||
|
},
|
||||||
Querystring: {
|
Querystring: {
|
||||||
since: number
|
since: number,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export interface SaveServiceSettings extends OnlyId {
|
export interface SaveServiceSettings extends OnlyId {
|
||||||
@ -36,7 +40,7 @@ export interface CheckService extends OnlyId {
|
|||||||
forceSave: boolean,
|
forceSave: boolean,
|
||||||
dualCerts: boolean,
|
dualCerts: boolean,
|
||||||
exposePort: number,
|
exposePort: number,
|
||||||
otherFqdns: Array<string>
|
otherFqdn: boolean
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export interface SaveService extends OnlyId {
|
export interface SaveService extends OnlyId {
|
||||||
@ -44,6 +48,8 @@ export interface SaveService extends OnlyId {
|
|||||||
name: string,
|
name: string,
|
||||||
fqdn: string,
|
fqdn: string,
|
||||||
exposePort: number,
|
exposePort: number,
|
||||||
|
version: string,
|
||||||
|
serviceSetting: any
|
||||||
type: string
|
type: string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -62,14 +68,15 @@ export interface DeleteServiceSecret extends OnlyId {
|
|||||||
export interface SaveServiceStorage extends OnlyId {
|
export interface SaveServiceStorage extends OnlyId {
|
||||||
Body: {
|
Body: {
|
||||||
path: string,
|
path: string,
|
||||||
newStorage: string,
|
containerId: string,
|
||||||
storageId: string,
|
storageId: string,
|
||||||
|
isNewStorage: boolean,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DeleteServiceStorage extends OnlyId {
|
export interface DeleteServiceStorage extends OnlyId {
|
||||||
Body: {
|
Body: {
|
||||||
path: string,
|
storageId: string,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export interface ServiceStartStop {
|
export interface ServiceStartStop {
|
||||||
|
@ -2,7 +2,7 @@ import { promises as dns } from 'dns';
|
|||||||
import { X509Certificate } from 'node:crypto';
|
import { X509Certificate } from 'node:crypto';
|
||||||
|
|
||||||
import type { FastifyReply, FastifyRequest } from 'fastify';
|
import type { FastifyReply, FastifyRequest } from 'fastify';
|
||||||
import { asyncExecShell, checkDomainsIsValidInDNS, decrypt, encrypt, errorHandler, isDNSValid, isDomainConfigured, listSettings, prisma } from '../../../../lib/common';
|
import { asyncExecShell, checkDomainsIsValidInDNS, decrypt, encrypt, errorHandler, isDev, isDNSValid, isDomainConfigured, listSettings, prisma } from '../../../../lib/common';
|
||||||
import { CheckDNS, CheckDomain, DeleteDomain, OnlyIdInBody, SaveSettings, SaveSSHKey } from './types';
|
import { CheckDNS, CheckDomain, DeleteDomain, OnlyIdInBody, SaveSettings, SaveSSHKey } from './types';
|
||||||
|
|
||||||
|
|
||||||
@ -44,16 +44,18 @@ export async function saveSettings(request: FastifyRequest<SaveSettings>, reply:
|
|||||||
maxPort,
|
maxPort,
|
||||||
isAutoUpdateEnabled,
|
isAutoUpdateEnabled,
|
||||||
isDNSCheckEnabled,
|
isDNSCheckEnabled,
|
||||||
DNSServers
|
DNSServers,
|
||||||
|
proxyDefaultRedirect
|
||||||
} = request.body
|
} = request.body
|
||||||
const { id } = await listSettings();
|
const { id } = await listSettings();
|
||||||
await prisma.setting.update({
|
await prisma.setting.update({
|
||||||
where: { id },
|
where: { id },
|
||||||
data: { isRegistrationEnabled, dualCerts, isAutoUpdateEnabled, isDNSCheckEnabled, DNSServers, isAPIDebuggingEnabled }
|
data: { isRegistrationEnabled, dualCerts, isAutoUpdateEnabled, isDNSCheckEnabled, DNSServers, isAPIDebuggingEnabled, }
|
||||||
});
|
});
|
||||||
if (fqdn) {
|
if (fqdn) {
|
||||||
await prisma.setting.update({ where: { id }, data: { fqdn } });
|
await prisma.setting.update({ where: { id }, data: { fqdn } });
|
||||||
}
|
}
|
||||||
|
await prisma.setting.update({ where: { id }, data: { proxyDefaultRedirect } });
|
||||||
if (minPort && maxPort) {
|
if (minPort && maxPort) {
|
||||||
await prisma.setting.update({ where: { id }, data: { minPort, maxPort } });
|
await prisma.setting.update({ where: { id }, data: { minPort, maxPort } });
|
||||||
}
|
}
|
||||||
@ -91,7 +93,7 @@ export async function checkDomain(request: FastifyRequest<CheckDomain>) {
|
|||||||
if (found) {
|
if (found) {
|
||||||
throw "Domain already configured";
|
throw "Domain already configured";
|
||||||
}
|
}
|
||||||
if (isDNSCheckEnabled && !forceSave) {
|
if (isDNSCheckEnabled && !forceSave && !isDev) {
|
||||||
const hostname = request.hostname.split(':')[0]
|
const hostname = request.hostname.split(':')[0]
|
||||||
return await checkDomainsIsValidInDNS({ hostname, fqdn, dualCerts });
|
return await checkDomainsIsValidInDNS({ hostname, fqdn, dualCerts });
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,8 @@ export interface SaveSettings {
|
|||||||
maxPort: number,
|
maxPort: number,
|
||||||
isAutoUpdateEnabled: boolean,
|
isAutoUpdateEnabled: boolean,
|
||||||
isDNSCheckEnabled: boolean,
|
isDNSCheckEnabled: boolean,
|
||||||
DNSServers: string
|
DNSServers: string,
|
||||||
|
proxyDefaultRedirect: string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export interface DeleteDomain {
|
export interface DeleteDomain {
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import axios from "axios";
|
|
||||||
import cuid from "cuid";
|
import cuid from "cuid";
|
||||||
import crypto from "crypto";
|
import crypto from "crypto";
|
||||||
import { encrypt, errorHandler, getDomain, getUIUrl, isDev, prisma } from "../../../lib/common";
|
import { encrypt, errorHandler, getDomain, getUIUrl, isDev, prisma } from "../../../lib/common";
|
||||||
@ -32,13 +31,14 @@ export async function installGithub(request: FastifyRequest<InstallGithub>, repl
|
|||||||
}
|
}
|
||||||
export async function configureGitHubApp(request, reply) {
|
export async function configureGitHubApp(request, reply) {
|
||||||
try {
|
try {
|
||||||
|
const { default: got } = await import('got')
|
||||||
const { code, state } = request.query;
|
const { code, state } = request.query;
|
||||||
const { apiUrl } = await prisma.gitSource.findFirst({
|
const { apiUrl } = await prisma.gitSource.findFirst({
|
||||||
where: { id: state },
|
where: { id: state },
|
||||||
include: { githubApp: true, gitlabApp: true }
|
include: { githubApp: true, gitlabApp: true }
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data }: any = await axios.post(`${apiUrl}/app-manifests/${code}/conversions`);
|
const data: any = await got.post(`${apiUrl}/app-manifests/${code}/conversions`).json()
|
||||||
const { id, client_id, slug, client_secret, pem, webhook_secret } = data
|
const { id, client_id, slug, client_secret, pem, webhook_secret } = data
|
||||||
|
|
||||||
const encryptedClientSecret = encrypt(client_secret);
|
const encryptedClientSecret = encrypt(client_secret);
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import axios from "axios";
|
|
||||||
import cuid from "cuid";
|
import cuid from "cuid";
|
||||||
import crypto from "crypto";
|
import crypto from "crypto";
|
||||||
import type { FastifyReply, FastifyRequest } from "fastify";
|
import type { FastifyReply, FastifyRequest } from "fastify";
|
||||||
@ -10,6 +9,7 @@ import type { ConfigureGitLabApp, GitLabEvents } from "./types";
|
|||||||
|
|
||||||
export async function configureGitLabApp(request: FastifyRequest<ConfigureGitLabApp>, reply: FastifyReply) {
|
export async function configureGitLabApp(request: FastifyRequest<ConfigureGitLabApp>, reply: FastifyReply) {
|
||||||
try {
|
try {
|
||||||
|
const { default: got } = await import('got')
|
||||||
const { code, state } = request.query;
|
const { code, state } = request.query;
|
||||||
const { fqdn } = await listSettings();
|
const { fqdn } = await listSettings();
|
||||||
const { gitSource: { gitlabApp: { appId, appSecret }, htmlUrl } }: any = await getApplicationFromDB(state, undefined);
|
const { gitSource: { gitlabApp: { appId, appSecret }, htmlUrl } }: any = await getApplicationFromDB(state, undefined);
|
||||||
@ -19,19 +19,21 @@ export async function configureGitLabApp(request: FastifyRequest<ConfigureGitLab
|
|||||||
if (isDev) {
|
if (isDev) {
|
||||||
domain = getAPIUrl();
|
domain = getAPIUrl();
|
||||||
}
|
}
|
||||||
const params = new URLSearchParams({
|
|
||||||
client_id: appId,
|
const { access_token } = await got.post(`${htmlUrl}/oauth/token`, {
|
||||||
client_secret: appSecret,
|
searchParams: {
|
||||||
code,
|
client_id: appId,
|
||||||
state,
|
client_secret: appSecret,
|
||||||
grant_type: 'authorization_code',
|
code,
|
||||||
redirect_uri: `${domain}/webhooks/gitlab`
|
state,
|
||||||
});
|
grant_type: 'authorization_code',
|
||||||
const { data } = await axios.post(`${htmlUrl}/oauth/token`, params)
|
redirect_uri: `${domain}/webhooks/gitlab`
|
||||||
|
}
|
||||||
|
}).json()
|
||||||
if (isDev) {
|
if (isDev) {
|
||||||
return reply.redirect(`${getUIUrl()}/webhooks/success?token=${data.access_token}`)
|
return reply.redirect(`${getUIUrl()}/webhooks/success?token=${access_token}`)
|
||||||
}
|
}
|
||||||
return reply.redirect(`/webhooks/success?token=${data.access_token}`)
|
return reply.redirect(`/webhooks/success?token=${access_token}`)
|
||||||
} catch ({ status, message, ...other }) {
|
} catch ({ status, message, ...other }) {
|
||||||
return errorHandler({ status, message })
|
return errorHandler({ status, message })
|
||||||
}
|
}
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -1,13 +1,12 @@
|
|||||||
import { FastifyPluginAsync } from 'fastify';
|
import { FastifyPluginAsync } from 'fastify';
|
||||||
import { OnlyId } from '../../../types';
|
import { OnlyId } from '../../../types';
|
||||||
import { remoteTraefikConfiguration, traefikConfiguration, traefikOtherConfiguration } from './handlers';
|
import { proxyConfiguration, otherProxyConfiguration } from './handlers';
|
||||||
import { TraefikOtherConfiguration } from './types';
|
import { OtherProxyConfiguration } from './types';
|
||||||
|
|
||||||
const root: FastifyPluginAsync = async (fastify): Promise<void> => {
|
const root: FastifyPluginAsync = async (fastify): Promise<void> => {
|
||||||
fastify.get('/main.json', async (request, reply) => traefikConfiguration(request, reply));
|
fastify.get<OnlyId>('/main.json', async (request, reply) => proxyConfiguration(request, false));
|
||||||
fastify.get<TraefikOtherConfiguration>('/other.json', async (request, reply) => traefikOtherConfiguration(request));
|
fastify.get<OnlyId>('/remote/:id', async (request) => proxyConfiguration(request, true));
|
||||||
|
fastify.get<OtherProxyConfiguration>('/other.json', async (request, reply) => otherProxyConfiguration(request));
|
||||||
fastify.get<OnlyId>('/remote/:id', async (request) => remoteTraefikConfiguration(request));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default root;
|
export default root;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
export interface TraefikOtherConfiguration {
|
export interface OtherProxyConfiguration {
|
||||||
Querystring: {
|
Querystring: {
|
||||||
id: string,
|
id: string,
|
||||||
privatePort: number,
|
privatePort: number,
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
export interface OnlyId {
|
export interface OnlyId {
|
||||||
Params: { id: string },
|
Params: { id?: string },
|
||||||
}
|
}
|
||||||
|
|
||||||
|
1
apps/api/tags.json
Normal file
1
apps/api/tags.json
Normal file
File diff suppressed because one or more lines are too long
1
apps/api/templates.json
Normal file
1
apps/api/templates.json
Normal file
File diff suppressed because one or more lines are too long
@ -14,43 +14,43 @@
|
|||||||
"format": "prettier --write --plugin-search-dir=. ."
|
"format": "prettier --write --plugin-search-dir=. ."
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@floating-ui/dom": "1.0.1",
|
"@floating-ui/dom": "1.0.3",
|
||||||
"@playwright/test": "1.25.1",
|
"@playwright/test": "1.27.1",
|
||||||
"@popperjs/core": "2.11.6",
|
"@popperjs/core": "2.11.6",
|
||||||
"@sveltejs/kit": "1.0.0-next.405",
|
"@sveltejs/kit": "1.0.0-next.405",
|
||||||
"@types/js-cookie": "3.0.2",
|
"@types/js-cookie": "3.0.2",
|
||||||
"@typescript-eslint/eslint-plugin": "5.36.1",
|
"@typescript-eslint/eslint-plugin": "5.41.0",
|
||||||
"@typescript-eslint/parser": "5.36.1",
|
"@typescript-eslint/parser": "5.41.0",
|
||||||
"autoprefixer": "10.4.8",
|
"autoprefixer": "10.4.12",
|
||||||
"classnames": "2.3.1",
|
"classnames": "2.3.2",
|
||||||
"eslint": "8.23.0",
|
"eslint": "8.26.0",
|
||||||
"eslint-config-prettier": "8.5.0",
|
"eslint-config-prettier": "8.5.0",
|
||||||
"eslint-plugin-svelte3": "4.0.0",
|
"eslint-plugin-svelte3": "4.0.0",
|
||||||
"flowbite": "1.5.2",
|
"flowbite": "1.5.3",
|
||||||
"flowbite-svelte": "0.26.2",
|
"flowbite-svelte": "0.27.11",
|
||||||
"postcss": "8.4.16",
|
"postcss": "8.4.18",
|
||||||
"prettier": "2.7.1",
|
"prettier": "2.7.1",
|
||||||
"prettier-plugin-svelte": "2.7.0",
|
"prettier-plugin-svelte": "2.8.0",
|
||||||
"svelte": "3.50.0",
|
"svelte": "3.52.0",
|
||||||
"svelte-check": "2.9.0",
|
"svelte-check": "2.9.2",
|
||||||
"svelte-preprocess": "4.10.7",
|
"svelte-preprocess": "4.10.7",
|
||||||
"tailwindcss": "3.1.8",
|
"tailwindcss": "3.2.1",
|
||||||
"tailwindcss-scrollbar": "0.1.0",
|
"tailwindcss-scrollbar": "0.1.0",
|
||||||
"tslib": "2.4.0",
|
"tslib": "2.4.0",
|
||||||
"typescript": "4.8.2",
|
"typescript": "4.8.4",
|
||||||
"vite": "3.1.0"
|
"vite": "3.2.0"
|
||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sveltejs/adapter-static": "1.0.0-next.39",
|
"@sveltejs/adapter-static": "1.0.0-next.46",
|
||||||
"@tailwindcss/typography": "^0.5.7",
|
"@tailwindcss/typography": "0.5.7",
|
||||||
"cuid": "2.1.8",
|
"cuid": "2.1.8",
|
||||||
"daisyui": "2.24.2",
|
"daisyui": "2.33.0",
|
||||||
"dayjs": "1.11.5",
|
"dayjs": "1.11.6",
|
||||||
"js-cookie": "3.0.1",
|
"js-cookie": "3.0.1",
|
||||||
"js-yaml": "4.1.0",
|
"js-yaml": "4.1.0",
|
||||||
"p-limit": "4.0.0",
|
"p-limit": "4.0.0",
|
||||||
"svelte-file-dropzone": "^1.0.0",
|
"socket.io-client": "4.5.3",
|
||||||
"svelte-select": "4.4.7",
|
"svelte-select": "4.4.7",
|
||||||
"sveltekit-i18n": "2.2.2"
|
"sveltekit-i18n": "2.2.2"
|
||||||
}
|
}
|
||||||
|
@ -38,6 +38,8 @@
|
|||||||
class={disabledClass}
|
class={disabledClass}
|
||||||
class:pr-10={true}
|
class:pr-10={true}
|
||||||
class:pr-20={value && isHttps}
|
class:pr-20={value && isHttps}
|
||||||
|
class:border={required && !value}
|
||||||
|
class:border-red-500={required && !value}
|
||||||
{placeholder}
|
{placeholder}
|
||||||
type="text"
|
type="text"
|
||||||
{id}
|
{id}
|
||||||
@ -54,6 +56,8 @@
|
|||||||
type="text"
|
type="text"
|
||||||
class:pr-10={true}
|
class:pr-10={true}
|
||||||
class:pr-20={value && isHttps}
|
class:pr-20={value && isHttps}
|
||||||
|
class:border={required && !value}
|
||||||
|
class:border-red-500={required && !value}
|
||||||
{id}
|
{id}
|
||||||
{name}
|
{name}
|
||||||
{required}
|
{required}
|
||||||
@ -70,6 +74,8 @@
|
|||||||
class={disabledClass}
|
class={disabledClass}
|
||||||
class:pr-10={true}
|
class:pr-10={true}
|
||||||
class:pr-20={value && isHttps}
|
class:pr-20={value && isHttps}
|
||||||
|
class:border={required && !value}
|
||||||
|
class:border-red-500={required && !value}
|
||||||
type="password"
|
type="password"
|
||||||
{id}
|
{id}
|
||||||
{name}
|
{name}
|
||||||
@ -85,6 +91,7 @@
|
|||||||
<div class="absolute top-0 right-0 flex justify-center items-center h-full cursor-pointer text-stone-600 hover:text-white mr-3">
|
<div class="absolute top-0 right-0 flex justify-center items-center h-full cursor-pointer text-stone-600 hover:text-white mr-3">
|
||||||
<div class="flex space-x-2">
|
<div class="flex space-x-2">
|
||||||
{#if isPasswordField}
|
{#if isPasswordField}
|
||||||
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
<div on:click={() => (showPassword = !showPassword)}>
|
<div on:click={() => (showPassword = !showPassword)}>
|
||||||
{#if showPassword}
|
{#if showPassword}
|
||||||
<svg
|
<svg
|
||||||
@ -126,6 +133,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if value && isHttps}
|
{#if value && isHttps}
|
||||||
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
<div on:click={copyToClipboard}>
|
<div on:click={copyToClipboard}>
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import ExternalLink from './ExternalLink.svelte';
|
||||||
import Tooltip from './Tooltip.svelte';
|
import Tooltip from './Tooltip.svelte';
|
||||||
export let url = 'https://docs.coollabs.io';
|
export let url = 'https://docs.coollabs.io';
|
||||||
|
export let text: any = '';
|
||||||
|
export let isExternal = false;
|
||||||
let id =
|
let id =
|
||||||
'cool-' +
|
'cool-' +
|
||||||
url
|
url
|
||||||
@ -10,10 +13,32 @@
|
|||||||
.slice(-16);
|
.slice(-16);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<a {id} href={url} target="_blank" class="icons inline-block cursor-pointer text-xs mx-2">
|
<a
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
|
{id}
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9.879 7.519c1.171-1.025 3.071-1.025 4.242 0 1.172 1.025 1.172 2.687 0 3.712-.203.179-.43.326-.67.442-.745.361-1.45.999-1.45 1.827v.75M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9 5.25h.008v.008H12v-.008z" />
|
href={url}
|
||||||
</svg>
|
target="_blank noreferrer"
|
||||||
|
class="flex no-underline inline-block cursor-pointer"
|
||||||
|
class:icons={!text}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke="currentColor"
|
||||||
|
class="w-6 h-6"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="M9.879 7.519c1.171-1.025 3.071-1.025 4.242 0 1.172 1.025 1.172 2.687 0 3.712-.203.179-.43.326-.67.442-.745.361-1.45.999-1.45 1.827v.75M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9 5.25h.008v.008H12v-.008z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
{text}
|
||||||
|
{#if isExternal}
|
||||||
|
<ExternalLink />
|
||||||
|
{/if}
|
||||||
</a>
|
</a>
|
||||||
<Tooltip triggeredBy={`#${id}`}>See details in the documentation</Tooltip>
|
{#if !text}
|
||||||
|
<Tooltip triggeredBy={`#${id}`}>See details in the documentation</Tooltip>
|
||||||
|
{/if}
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
// import Tooltip from './Tooltip.svelte';
|
// import Tooltip from './Tooltip.svelte';
|
||||||
export let explanation = '';
|
export let explanation = '';
|
||||||
export let position = 'dropdown-right'
|
export let position = 'dropdown-right';
|
||||||
// let id: any;
|
// let id: any;
|
||||||
// let self: any;
|
// let self: any;
|
||||||
// onMount(() => {
|
// onMount(() => {
|
||||||
@ -13,32 +13,26 @@
|
|||||||
|
|
||||||
<div class={`dropdown dropdown-end ${position}`}>
|
<div class={`dropdown dropdown-end ${position}`}>
|
||||||
<!-- svelte-ignore a11y-label-has-associated-control -->
|
<!-- svelte-ignore a11y-label-has-associated-control -->
|
||||||
|
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
|
||||||
<label tabindex="0" class="btn btn-circle btn-ghost btn-xs text-sky-500">
|
<label tabindex="0" class="btn btn-circle btn-ghost btn-xs text-sky-500">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="w-4 h-4 stroke-current"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
class="w-4 h-4 stroke-current"
|
||||||
|
><path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||||
|
/></svg
|
||||||
|
>
|
||||||
</label>
|
</label>
|
||||||
|
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
|
||||||
<div tabindex="0" class="card compact dropdown-content shadow bg-coolgray-400 rounded w-64">
|
<div tabindex="0" class="card compact dropdown-content shadow bg-coolgray-400 rounded w-64">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<!-- <h2 class="card-title">You needed more info?</h2> -->
|
<!-- <h2 class="card-title">You needed more info?</h2> -->
|
||||||
<p class="text-xs font-normal">{@html explanation}</p>
|
<p class="text-xs font-normal">{@html explanation}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<!-- <div {id} class="inline-block mx-2 cursor-pointer" bind:this={self}>
|
|
||||||
<svg
|
|
||||||
fill="none"
|
|
||||||
height="14"
|
|
||||||
shape-rendering="geometricPrecision"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="1.4"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
width="14"
|
|
||||||
><path d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10z" /><path
|
|
||||||
d="M9.09 9a3 3 0 015.83 1c0 2-3 3-3 3"
|
|
||||||
/><circle cx="12" cy="17" r=".5" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
</div>
|
||||||
{#if id}
|
|
||||||
<Tooltip triggeredBy={`#${id}`}>{@html explanation}</Tooltip>
|
|
||||||
{/if} -->
|
|
||||||
|
10
apps/ui/src/lib/components/ExternalLink.svelte
Normal file
10
apps/ui/src/lib/components/ExternalLink.svelte
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width="3"
|
||||||
|
stroke="currentColor"
|
||||||
|
class="w-3 h-3 text-white"
|
||||||
|
>
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M4.5 19.5l15-15m0 0H8.25m11.25 0v11.25" />
|
||||||
|
</svg>
|
After Width: | Height: | Size: 261 B |
37
apps/ui/src/lib/components/ServiceStatus.svelte
Normal file
37
apps/ui/src/lib/components/ServiceStatus.svelte
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
export let id: any;
|
||||||
|
import { status } from '$lib/store';
|
||||||
|
let serviceStatus = {
|
||||||
|
isExcluded: false,
|
||||||
|
isExited: false,
|
||||||
|
isRunning: false,
|
||||||
|
isRestarting: false,
|
||||||
|
isStopped: false
|
||||||
|
};
|
||||||
|
|
||||||
|
$: if (Object.keys($status.service.statuses).length > 0 && $status.service.statuses[id]?.status) {
|
||||||
|
let { isExited, isRunning, isRestarting, isExcluded } = $status.service.statuses[id].status;
|
||||||
|
|
||||||
|
serviceStatus.isExited = isExited;
|
||||||
|
serviceStatus.isRunning = isRunning;
|
||||||
|
serviceStatus.isExcluded = isExcluded;
|
||||||
|
serviceStatus.isRestarting = isRestarting;
|
||||||
|
serviceStatus.isStopped = !isExited && !isRunning && !isRestarting;
|
||||||
|
} else {
|
||||||
|
serviceStatus.isExited = false;
|
||||||
|
serviceStatus.isRunning = false;
|
||||||
|
serviceStatus.isExcluded = false;
|
||||||
|
serviceStatus.isRestarting = false;
|
||||||
|
serviceStatus.isStopped = true;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if serviceStatus.isExcluded}
|
||||||
|
<span class="badge font-bold uppercase rounded text-orange-500 mt-2">Excluded</span>
|
||||||
|
{:else if serviceStatus.isRunning}
|
||||||
|
<span class="badge font-bold uppercase rounded text-green-500 mt-2">Running</span>
|
||||||
|
{:else if serviceStatus.isStopped || serviceStatus.isExited}
|
||||||
|
<span class="badge font-bold uppercase rounded text-red-500 mt-2">Stopped</span>
|
||||||
|
{:else if serviceStatus.isRestarting}
|
||||||
|
<span class="badge font-bold uppercase rounded text-yellow-500 mt-2">Restarting</span>
|
||||||
|
{/if}
|
@ -8,7 +8,7 @@
|
|||||||
export let setting: any;
|
export let setting: any;
|
||||||
export let title: any;
|
export let title: any;
|
||||||
export let isBeta: any = false;
|
export let isBeta: any = false;
|
||||||
export let description: any;
|
export let description: any = null;
|
||||||
export let isCenter = true;
|
export let isCenter = true;
|
||||||
export let disabled = false;
|
export let disabled = false;
|
||||||
export let dataTooltip: any = null;
|
export let dataTooltip: any = null;
|
||||||
@ -31,6 +31,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class:text-center={isCenter} class={`flex justify-center ${customClass}`}>
|
<div class:text-center={isCenter} class={`flex justify-center ${customClass}`}>
|
||||||
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
<div
|
<div
|
||||||
on:click
|
on:click
|
||||||
aria-pressed="false"
|
aria-pressed="false"
|
||||||
|
@ -4,18 +4,19 @@
|
|||||||
export let type = 'info';
|
export let type = 'info';
|
||||||
function success() {
|
function success() {
|
||||||
if (type === 'success') {
|
if (type === 'success') {
|
||||||
return 'bg-coollabs';
|
return 'bg-dark lg:bg-primary';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
<div
|
<div
|
||||||
on:click={() => dispatch('click')}
|
on:click={() => dispatch('click')}
|
||||||
on:mouseover={() => dispatch('pause')}
|
on:mouseover={() => dispatch('pause')}
|
||||||
on:focus={() => dispatch('pause')}
|
on:focus={() => dispatch('pause')}
|
||||||
on:mouseout={() => dispatch('resume')}
|
on:mouseout={() => dispatch('resume')}
|
||||||
on:blur={() => dispatch('resume')}
|
on:blur={() => dispatch('resume')}
|
||||||
class={`flex flex-row alert shadow-lg text-white hover:scale-105 transition-all duration-100 cursor-pointer rounded ${success()}`}
|
class={` flex flex-row justify-center alert shadow-lg text-white hover:scale-105 transition-all duration-100 cursor-pointer rounded ${success()}`}
|
||||||
class:alert-error={type === 'error'}
|
class:alert-error={type === 'error'}
|
||||||
class:alert-info={type === 'info'}
|
class:alert-info={type === 'info'}
|
||||||
>
|
>
|
||||||
|
@ -4,11 +4,11 @@
|
|||||||
import { dismissToast, pauseToast, resumeToast, toasts } from '$lib/store';
|
import { dismissToast, pauseToast, resumeToast, toasts } from '$lib/store';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if $toasts}
|
{#if $toasts.length > 0}
|
||||||
<section>
|
<section>
|
||||||
<article class="toast toast-top toast-end rounded-none px-10" role="alert" >
|
<article class="toast toast-top toast-center rounded-none w-2/3 lg:w-[20rem]" role="alert">
|
||||||
{#each $toasts as toast (toast.id)}
|
{#each $toasts as toast (toast.id)}
|
||||||
<Toast
|
<Toast
|
||||||
type={toast.type}
|
type={toast.type}
|
||||||
on:resume={() => resumeToast(toast.id)}
|
on:resume={() => resumeToast(toast.id)}
|
||||||
on:pause={() => pauseToast(toast.id)}
|
on:pause={() => pauseToast(toast.id)}
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Tooltip } from 'flowbite-svelte';
|
import { Tooltip } from 'flowbite-svelte';
|
||||||
export let placement = 'bottom';
|
export let placement = 'bottom';
|
||||||
export let color = 'bg-coollabs font-thin text-left';
|
export let color = 'bg-coollabs';
|
||||||
export let triggeredBy = '#tooltip-default';
|
export let triggeredBy = '#tooltip-default';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Tooltip {triggeredBy} {placement} arrow={false} {color} style="custom"><slot /></Tooltip>
|
<Tooltip {triggeredBy} {placement} arrow={false} defaultClass={color + ' font-thin text-xs text-left border-none p-2'} style="custom"
|
||||||
|
><slot /></Tooltip
|
||||||
|
>
|
||||||
|
@ -68,12 +68,6 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="w-full relative p-5 ">
|
<div class="w-full relative p-5 ">
|
||||||
{#if loading.usage}
|
|
||||||
<span class="indicator-item badge bg-yellow-500 badge-sm" />
|
|
||||||
{:else}
|
|
||||||
<span class="indicator-item badge bg-success badge-sm" />
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<div class="w-full flex flex-col lg:flex-row space-y-4 lg:space-y-0 space-x-4">
|
<div class="w-full flex flex-col lg:flex-row space-y-4 lg:space-y-0 space-x-4">
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<h1 class="font-bold text-lg lg:text-xl truncate">
|
<h1 class="font-bold text-lg lg:text-xl truncate">
|
||||||
@ -99,6 +93,11 @@
|
|||||||
class="btn btn-sm">Cleanup Storage</button
|
class="btn btn-sm">Cleanup Storage</button
|
||||||
>
|
>
|
||||||
{/if}
|
{/if}
|
||||||
|
{#if loading.usage}
|
||||||
|
<button id="streaming" class=" btn btn-sm bg-transparent border-none loading"
|
||||||
|
>Getting data...</button
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex lg:flex-row flex-col gap-4">
|
<div class="flex lg:flex-row flex-col gap-4">
|
||||||
<div class="flex lg:flex-row flex-col space-x-0 lg:space-x-2 space-y-2 lg:space-y-0" />
|
<div class="flex lg:flex-row flex-col space-x-0 lg:space-x-2 space-y-2 lg:space-y-0" />
|
||||||
|
@ -5,5 +5,5 @@
|
|||||||
<img
|
<img
|
||||||
alt="docker compose logo"
|
alt="docker compose logo"
|
||||||
class={isAbsolute ? 'w-16 h-16 absolute top-0 left-0 -m-8' : 'w-8 h-8 mx-auto'}
|
class={isAbsolute ? 'w-16 h-16 absolute top-0 left-0 -m-8' : 'w-8 h-8 mx-auto'}
|
||||||
src="/docker-compose.png"
|
src="/icons/compose.png"
|
||||||
/>
|
/>
|
||||||
|
File diff suppressed because one or more lines are too long
@ -1,121 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
export let isAbsolute = false;
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<svg
|
|
||||||
viewBox="0 0 700 240"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
class={isAbsolute ? 'w-36 absolute top-0 left-0 -m-3 -mt-5' : 'w-full h-10 mx-auto'}
|
|
||||||
><path fill="#FDBC3D" d="m90.694 107.498-.981.39-20.608 8.23 6.332 6.547z" /><path
|
|
||||||
fill="#8EC63F"
|
|
||||||
d="M61.139 77.914 46.632 93 56.9 103.547c8.649-7.169 17.832-10.502 18.653-10.789L61.139 77.914z"
|
|
||||||
/><path fill="#208ECB" d="M61.139 77.914 46.367 63.247l-14.228 14.8 14.493 14.952z" /><path
|
|
||||||
fill="#273C8B"
|
|
||||||
d="m40.767 57.48-6.943 2.79a38.381 38.381 0 0 0-11.742 7.418L32.14 78.047l14.228-14.8-5.601-5.768z"
|
|
||||||
/><path
|
|
||||||
fill="#EE4649"
|
|
||||||
d="m119.074 138.128-.243-.25-5.653 5.675c1.897-1.516 4.287-3.66 5.896-5.425z"
|
|
||||||
/><path
|
|
||||||
fill="#F6944E"
|
|
||||||
d="m102.088 150.087 3.709-1.875a46.26 46.26 0 0 0 7.381-4.659l5.653-5.676-14.311-15.285-14.493 15.072 12.061 12.423z"
|
|
||||||
/><path fill="#FFC951" d="m90.279 107.926-14.842 14.74 14.589 14.998 14.493-15.072z" /><path
|
|
||||||
fill="#F6CC18"
|
|
||||||
d="m69.087 116.125-11.256 4.493c-3.301.973-6.096 2.843-8.434 5.081l11.548 11.892 14.493-14.926-6.35-6.54z"
|
|
||||||
/><path
|
|
||||||
fill="#C5D82D"
|
|
||||||
d="m56.886 103.559-10.253-10.56L32 107.926l11.784 11.991c3.304-6.888 8.174-12.272 13.103-16.358z"
|
|
||||||
/><path fill="#0D77B3" d="m32.14 78.047-14.507 14.94 14.365 14.939 14.634-14.927z" /><path
|
|
||||||
fill="#2A377E"
|
|
||||||
d="M32.14 78.047 22.08 67.688a38.573 38.573 0 0 0-11.093 18.455l6.645 6.843 14.506-14.94z"
|
|
||||||
/><path
|
|
||||||
fill="#DA2128"
|
|
||||||
d="m94.826 162.454-4.87 5.017 14.808 15.397c-.632-1.942-1.606-4.438-2.58-6.307l-7.357-14.107z"
|
|
||||||
/><path
|
|
||||||
fill="#F8A561"
|
|
||||||
d="m91.24 155.575 10.832-5.48-12.046-12.43-14.506 14.939 14.436 14.867 4.87-5.017z"
|
|
||||||
/><path fill="#FDBC3D" d="m75.437 122.665-14.493 14.926 14.576 15.013 14.506-14.94z" /><path
|
|
||||||
fill="#FAD412"
|
|
||||||
d="M49.397 125.7c-6.71 6.472-9.664 16.047-9.664 16.047-.3-4.606.06-8.83.907-12.698l-8.513 8.742 14.311 14.74 14.506-14.94-11.547-11.892z"
|
|
||||||
/><path
|
|
||||||
fill="#C4D52D"
|
|
||||||
d="m43.783 119.917-11.785-11.991-13.29 13.687 3.708 6.178 9.71 10 8.52-8.775a42.699 42.699 0 0 1 3.137-9.099z"
|
|
||||||
/><path
|
|
||||||
fill="#1B80C1"
|
|
||||||
d="m17.633 92.986-7.638 7.72c.65 5.1 2.35 10.3 5.193 15.04l3.52 5.867 13.29-13.687-14.365-14.94z"
|
|
||||||
/><path
|
|
||||||
fill="#1A4685"
|
|
||||||
d="M10.989 86.143c-1.22 4.667-1.597 9.683-.993 14.563l7.638-7.72-6.645-6.843z"
|
|
||||||
/><path
|
|
||||||
fill="#B12026"
|
|
||||||
d="m89.956 197.35 12.502 13.022c4.143-8.355 5.148-18.255 2.307-27.504l-.302-.311-14.507 14.793z"
|
|
||||||
/><path fill="#E42028" d="M89.956 167.47 75.52 182.484l14.436 14.867 14.506-14.793z" /><path
|
|
||||||
fill="#F16B4E"
|
|
||||||
d="m75.52 152.604-14.576 14.867 14.576 15.012 14.436-15.012z"
|
|
||||||
/><path fill="#FAD412" d="m60.944 137.591-14.506 14.94 14.506 14.94 14.576-14.867z" /><path
|
|
||||||
fill="#FFC951"
|
|
||||||
d="m32.127 137.792-2.293 2.36 10.933 18.22 5.671-5.841z"
|
|
||||||
/><path fill="#FFC951" d="m22.416 127.79 7.418 12.363 2.293-2.361z" /><path
|
|
||||||
fill="#981C20"
|
|
||||||
d="M102.458 210.371 89.955 197.35 75.45 212.29l12.918 13.304a36.951 36.951 0 0 0 14.09-15.222z"
|
|
||||||
/><path
|
|
||||||
fill="#C92039"
|
|
||||||
d="m75.52 182.483-12.59 12.823 6.423 10.704 6.097 6.28 14.506-14.94z"
|
|
||||||
/><path fill="#F05B41" d="m60.944 167.47-9.096 9.369 11.081 18.467 12.59-12.823z" /><path
|
|
||||||
fill="#F6CC18"
|
|
||||||
d="m46.438 152.53-5.671 5.842 11.081 18.467 9.096-9.368z"
|
|
||||||
/><path
|
|
||||||
fill="#7A1319"
|
|
||||||
d="m74.01 213.772 8.904 14.838 4.104-2.237c.429-.233.934-.533 1.35-.78L75.45 212.29l-1.44 1.482z"
|
|
||||||
/><path fill="#981C20" d="m69.353 206.01 4.658 7.762 1.44-1.482z" /><path
|
|
||||||
fill="#15796E"
|
|
||||||
d="m147.842 48.094 10.653-10.971a41.81 41.81 0 0 0 .943-6.94l-11.414-11.755-14.48 14.94 14.298 14.726z"
|
|
||||||
/><path fill="#29B364" d="m133.53 33.354 14.494-14.926-2.737-2.965-20.95 8.422z" /><path
|
|
||||||
fill="#21A29F"
|
|
||||||
d="M151.819 52.189c3.057-4.334 5.434-9.932 6.677-15.066l-10.653 10.971 3.976 4.095z"
|
|
||||||
/><path
|
|
||||||
fill="#12827F"
|
|
||||||
d="M159.438 30.183c.307-6.28-.783-12.862-3.488-19.006l-1.41.567-6.516 6.684 11.414 11.755zM154.54 11.744l-9.253 3.72 2.737 2.964z"
|
|
||||||
/><path fill="#0C6355" d="m133.336 63.034 14.506-14.94-14.311-14.713-14.493 14.926z" /><path
|
|
||||||
fill="#1B974D"
|
|
||||||
d="m104.532 33.368 14.506 14.94 14.48-14.94-9.2-9.476-17.363 6.98z"
|
|
||||||
/><path fill="#16669F" d="m106.955 30.872-3.485 1.401 1.062 1.095z" /><path
|
|
||||||
fill="#44BFBD"
|
|
||||||
d="M135.9 65.674A41.696 41.696 0 0 0 151.82 52.19l-3.977-4.095-14.506 14.94 2.564 2.64z"
|
|
||||||
/><path
|
|
||||||
fill="#0D5650"
|
|
||||||
d="m115.71 74.76 11.052-4.956 6.574-6.77-14.298-14.727-14.506 14.94z"
|
|
||||||
/><path fill="#3FAF49" d="m119.038 48.307-14.506-14.94-14.576 14.868 14.563 14.999z" /><path
|
|
||||||
fill="#0D77B3"
|
|
||||||
d="m104.532 33.368-1.062-1.095-20.97 8.43 7.456 7.532z"
|
|
||||||
/><path
|
|
||||||
fill="#0C6355"
|
|
||||||
d="M134.766 66.217c.352-.157.789-.376 1.134-.543l-2.564-2.64-6.574 6.77 8.004-3.587z"
|
|
||||||
/><path fill="#12827F" d="m115.71 74.76-11.178-11.513-14.506 14.94 5.47 5.633z" /><path
|
|
||||||
fill="#4EB648"
|
|
||||||
d="M104.532 63.247 89.956 48.235 75.52 63.247l14.493 14.927z"
|
|
||||||
/><path fill="#16669F" d="M89.956 48.235 82.5 40.703l-20.868 8.388L75.52 63.247z" /><path
|
|
||||||
fill="#FBB139"
|
|
||||||
d="M129.526 119.012c1.902-7.144 2.108-15.019.353-22.538l-11.048 11.379 10.695 11.16z"
|
|
||||||
/><path
|
|
||||||
fill="#E2B523"
|
|
||||||
d="m110.62 99.542 8.21 8.311 11.049-11.38a46.303 46.303 0 0 0-1.186-4.149l-18.074 7.218z"
|
|
||||||
/><path fill="#189590" d="M90.026 78.186 76.128 92.501l19.367-8.681z" /><path
|
|
||||||
fill="#8EC63F"
|
|
||||||
d="m76.083 92.521 13.943-14.335-14.506-14.94-14.381 14.668 14.413 14.844z"
|
|
||||||
/><path
|
|
||||||
fill="#0D77B3"
|
|
||||||
d="M75.52 63.247 61.633 49.09l-2.264.91-13.002 13.246L61.14 77.914z"
|
|
||||||
/><path fill="#1953A2" d="m59.37 50.002-18.603 7.477 5.6 5.768z" /><path
|
|
||||||
fill="#ED3551"
|
|
||||||
d="M119.324 137.84c.885-.988 2.15-2.59 2.942-3.646l-3.17 3.41.228.236z"
|
|
||||||
/><path
|
|
||||||
fill="#F8A561"
|
|
||||||
d="m118.83 137.877 3.437-3.683a46.268 46.268 0 0 0 7.259-15.182l-10.695-11.159-14.311 14.74 14.31 15.284z"
|
|
||||||
/><path
|
|
||||||
fill="#E9B520"
|
|
||||||
d="m90.279 107.926 14.24 14.666 14.312-14.739-8.212-8.311-19.925 7.956z"
|
|
||||||
/><path
|
|
||||||
fill="#EE4649"
|
|
||||||
d="m118.83 137.877.244.251c.085-.095.166-.193.25-.288l-.228-.235-.265.272z"
|
|
||||||
/></svg
|
|
||||||
>
|
|
@ -1,9 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
export let isAbsolute = false;
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<img
|
|
||||||
alt="ghost logo"
|
|
||||||
class={isAbsolute ? 'w-12 h-12 absolute top-0 left-0 -m-3 -mt-5' : 'w-8 h-8 mx-auto'}
|
|
||||||
src="/ghost.png"
|
|
||||||
/>
|
|
@ -1,9 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
export let isAbsolute = false;
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<img
|
|
||||||
alt="grafana logo"
|
|
||||||
class={isAbsolute ? 'w-12 h-12 absolute top-0 left-0 -m-3 -mt-5' : 'w-8 h-8 mx-auto'}
|
|
||||||
src="/grafana.png"
|
|
||||||
/>
|
|
@ -1,26 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
export let isAbsolute = false;
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<svg
|
|
||||||
class={isAbsolute ? 'w-10 h-10 absolute top-0 left-0 -m-5' : 'w-8 h-8 mx-auto'}
|
|
||||||
viewBox="0 0 81 84"
|
|
||||||
fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<g clip-path="url(#clip0_5273_21928)">
|
|
||||||
<path
|
|
||||||
d="M79.7186 28.6019C82.1218 21.073 80.6778 6.03601 76.0158 0.487861C75.4073 -0.238064 74.2624 -0.134361 73.757 0.664158L68.0121 9.72786C66.5887 11.5427 64.0308 11.9575 62.1124 10.6923C55.8827 6.59601 48.4359 4.21082 40.4322 4.21082C32.4285 4.21082 24.9817 6.59601 18.752 10.6923C16.8336 11.9575 14.2757 11.5323 12.8523 9.72786L7.10738 0.664158C6.60199 -0.134361 5.45712 -0.238064 4.84859 0.487861C0.186621 6.03601 -1.25735 21.073 1.14583 28.6019C1.94002 31.1012 2.16693 33.7456 1.69248 36.3279C1.22834 38.879 0.753897 41.9693 0.753897 44.1056C0.753897 66.1323 18.5251 84.0004 40.4322 84.0004C62.3497 84.0004 80.1105 66.1427 80.1105 44.1056C80.1105 41.959 79.6464 38.879 79.1719 36.3279C78.6975 33.7456 78.9244 31.1012 79.7186 28.6019ZM40.4322 75.0819C23.4965 75.0819 9.71684 61.2271 9.71684 44.199C9.71684 43.639 9.73747 43.0893 9.7581 42.5397C10.3769 30.9353 17.3802 21.0108 27.3024 16.2819C31.2836 14.3738 35.7393 13.316 40.4322 13.316C45.1251 13.316 49.5808 14.3842 53.5724 16.2923C63.4945 21.0212 70.4978 30.9456 71.1166 42.5397C71.1476 43.0893 71.1579 43.639 71.1579 44.199C71.1476 61.2271 57.3679 75.0819 40.4322 75.0819Z"
|
|
||||||
fill="#1EB4D4"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M53.7371 56.083L45.8881 42.4044L39.153 30.997C38.9983 30.7274 38.7095 30.5615 38.3898 30.5615H31.9538C31.634 30.5615 31.3452 30.7378 31.1905 31.0074C31.0358 31.2874 31.0358 31.6296 31.2008 31.8993L37.6368 42.7881L28.9936 56.0415C28.8183 56.3111 28.7977 56.6637 28.9524 56.9541C29.1071 57.2444 29.4062 57.4207 29.7259 57.4207H36.2032C36.5023 57.4207 36.7808 57.2652 36.9458 57.0163L41.6181 49.6741L45.8056 56.9748C45.9603 57.2548 46.2594 57.4207 46.5688 57.4207H52.9533C53.273 57.4207 53.5618 57.2548 53.7165 56.9748C53.9022 56.6948 53.9022 56.363 53.7371 56.083Z"
|
|
||||||
fill="#1EB4D4"
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
<defs>
|
|
||||||
<clipPath id="clip0_5273_21928">
|
|
||||||
<rect width="81" height="84" fill="white" />
|
|
||||||
</clipPath>
|
|
||||||
</defs>
|
|
||||||
</svg>
|
|
@ -1,27 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
export let isAbsolute = false;
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
class={isAbsolute ? 'w-10 h-10 absolute top-0 left-0 -m-5' : 'w-8 h-8 mx-auto'}
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 140 140"
|
|
||||||
data-lt-extension-installed="true"
|
|
||||||
><g clip-path="url(#clip0)"
|
|
||||||
><path
|
|
||||||
fill="#fff"
|
|
||||||
fill-rule="evenodd"
|
|
||||||
d="M140 43.602c0-1.662.001-3.324-.01-4.987-.008-1.4-.024-2.8-.062-4.2-.082-3.05-.262-6.126-.805-9.142-.55-3.06-1.448-5.907-2.864-8.688A29.227 29.227 0 0 0 123.476 3.81c-2.783-1.416-5.634-2.314-8.697-2.864-3.016-.542-6.094-.722-9.144-.804-1.4-.038-2.801-.054-4.202-.063C99.77.068 98.107.07 96.444.07L77.135 0H62.694L43.726.07c-1.666 0-3.332-.002-4.998.008-1.404.01-2.807.025-4.21.063-3.058.082-6.142.262-9.166.805-3.067.55-5.922 1.447-8.709 2.862a29.293 29.293 0 0 0-7.419 5.377 29.223 29.223 0 0 0-5.389 7.4c-1.42 2.78-2.32 5.63-2.871 8.691-.543 3.016-.723 6.091-.806 9.14-.038 1.4-.054 2.8-.062 4.2C.086 40.277 0 42.342 0 44.004v33.3l.086 19.102c0 1.665 0 3.33.01 4.994a200.6 200.6 0 0 0 .062 4.205c.083 3.054.263 6.135.807 9.155.551 3.064 1.451 5.916 2.87 8.7a29.294 29.294 0 0 0 12.807 12.794c2.788 1.418 5.645 2.317 8.714 2.868 3.022.542 6.105.722 9.162.804 1.403.038 2.806.054 4.21.063 1.666.01 3.332.009 4.998.009l19.14.001h14.477l19.101-.001c1.663 0 3.326.001 4.989-.009a202.92 202.92 0 0 0 4.202-.063c3.052-.082 6.13-.262 9.148-.805 3.061-.551 5.911-1.45 8.692-2.867a29.215 29.215 0 0 0 7.405-5.384 29.22 29.22 0 0 0 5.378-7.409c1.417-2.785 2.315-5.639 2.866-8.704.542-3.02.722-6.099.804-9.152.038-1.402.054-2.804.062-4.205.011-1.665.01-3.33.01-4.993l-.001-19.103V62.694L140 43.602"
|
|
||||||
clip-rule="evenodd"
|
|
||||||
/><path
|
|
||||||
fill="#000"
|
|
||||||
fill-rule="evenodd"
|
|
||||||
d="M39.375 40.188h8.313a6.25 6.25 0 0 1 6.25 6.25v24.25h16.25v8.75h-18.75a6.25 6.25 0 0 1-6.25-6.25v-24.25h-5.813v-8.75zm63.563 6.25v6.5h-8.75v-4h-6.876v30.5h-8.75v-30.5h-6.874v4h-8.75v-6.5a6.25 6.25 0 0 1 6.25-6.25h27.5a6.25 6.25 0 0 1 6.25 6.25z"
|
|
||||||
clip-rule="evenodd"
|
|
||||||
/><path
|
|
||||||
fill="#239AFF"
|
|
||||||
d="M35.319 102.906l-8.138-5.812c2.39-3.347 4.857-5.936 7.452-7.753 2.884-2.018 5.948-3.091 9.117-3.091 2.942 0 5.491.714 7.768 2.08a17.622 17.622 0 0 1 2.615 1.94c.589.518 1.009.926 1.903 1.82 1.355 1.354 1.917 1.851 2.591 2.255.731.439 1.503.655 2.623.655 1.121 0 1.896-.217 2.631-.657.677-.405 1.245-.905 2.6-2.257l.012-.012c.89-.888 1.314-1.299 1.902-1.817a17.643 17.643 0 0 1 2.61-1.933c2.273-1.362 4.814-2.074 7.745-2.074s5.472.712 7.745 2.074c.916.55 1.758 1.183 2.61 1.933.589.518 1.013.929 1.902 1.817l.013.012c1.354 1.352 1.922 1.852 2.599 2.257.735.44 1.51.657 2.631.657.998 0 2.1-.386 3.383-1.284 1.572-1.1 3.272-2.886 5.048-5.372l8.138 5.812c-2.391 3.347-4.857 5.936-7.452 7.753-2.884 2.018-5.948 3.091-9.117 3.091-2.941 0-5.49-.713-7.769-2.078a17.627 17.627 0 0 1-2.619-1.938c-.59-.519-1.015-.93-1.906-1.82l-.013-.013c-1.351-1.348-1.917-1.846-2.59-2.25-.728-.436-1.494-.651-2.603-.651-1.109 0-1.875.215-2.603.651-.673.404-1.239.902-2.59 2.25l-.012.013c-.892.89-1.317 1.301-1.907 1.82-.855.752-1.7 1.388-2.62 1.938C66.74 104.287 64.192 105 61.25 105c-2.942 0-5.49-.714-7.768-2.08a17.654 17.654 0 0 1-2.615-1.939c-.588-.519-1.009-.927-1.902-1.82-1.355-1.355-1.918-1.852-2.592-2.256-.731-.439-1.503-.655-2.623-.655-.998 0-2.1.386-3.383 1.284-1.572 1.1-3.272 2.886-5.048 5.372z"
|
|
||||||
/></g
|
|
||||||
><defs><clipPath id="clip0"><path fill="#fff" d="M0 0h140v140H0z" /></clipPath></defs></svg
|
|
||||||
>
|
|
@ -1,9 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
export let isAbsolute = false;
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<img
|
|
||||||
alt="minio logo"
|
|
||||||
class={isAbsolute ? 'w-7 h-12 absolute top-0 left-0 -m-3 -mt-5' : 'w-4 h-8 mx-auto'}
|
|
||||||
src="/minio.png"
|
|
||||||
/>
|
|
@ -1,8 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
export let isAbsolute = false;
|
|
||||||
</script>
|
|
||||||
<img
|
|
||||||
alt="moodle logo"
|
|
||||||
class={isAbsolute ? 'w-9 h-9 absolute top-0 left-0 -m-3 -mt-5' : 'w-8 h-8 mx-auto'}
|
|
||||||
src="/moodle.png"
|
|
||||||
/>
|
|
@ -1,24 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
export let isAbsolute = false;
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<svg
|
|
||||||
class={isAbsolute ? 'w-12 h-12 absolute top-0 left-0 -m-5' : 'w-8 h-8 mx-auto'}
|
|
||||||
viewBox="0 0 220 105"
|
|
||||||
>
|
|
||||||
<g>
|
|
||||||
<path
|
|
||||||
fill="#FF6D5A"
|
|
||||||
d="M183.9,0.2c-9.8,0-18,6.7-20.3,15.8h-29.2c-11.5,0-20.8,9.3-20.8,20.8c0,5.7-4.7,10.4-10.4,10.4H99
|
|
||||||
c-2.3-9.1-10.5-15.8-20.3-15.8c-9.8,0-18,6.7-20.3,15.8H41.7c-2.3-9.1-10.5-15.8-20.3-15.8c-11.6,0-21,9.4-21,21
|
|
||||||
c0,11.6,9.4,21,21,21c9.8,0,18-6.7,20.3-15.8h16.7c2.3,9.1,10.5,15.8,20.3,15.8c9.7,0,17.9-6.6,20.3-15.6h4.2
|
|
||||||
c5.7,0,10.4,4.7,10.4,10.4c0,11.5,9.3,20.8,20.8,20.8h6.8c2.3,9.1,10.5,15.8,20.3,15.8c11.6,0,21-9.4,21-21c0-11.6-9.4-21-21-21
|
|
||||||
c-9.8,0-18,6.7-20.3,15.8h-6.8c-5.7,0-10.4-4.7-10.4-10.4c0-6.3-2.8-11.9-7.2-15.7c4.4-3.8,7.2-9.4,7.2-15.7
|
|
||||||
c0-5.7,4.7-10.4,10.4-10.4h29.2c2.3,9.1,10.5,15.8,20.3,15.8c11.6,0,21-9.4,21-21C204.9,9.6,195.5,0.2,183.9,0.2z M21.4,63
|
|
||||||
c-5.8,0-10.6-4.8-10.6-10.6s4.8-10.6,10.6-10.6S32,46.6,32,52.4S27.3,63,21.4,63z M78.7,63c-5.8,0-10.6-4.8-10.6-10.6
|
|
||||||
s4.8-10.6,10.6-10.6s10.6,4.8,10.6,10.6S84.6,63,78.7,63z M161.5,73.2c5.8,0,10.6,4.8,10.6,10.6s-4.8,10.6-10.6,10.6
|
|
||||||
s-10.6-4.8-10.6-10.6C150.9,77.9,155.7,73.2,161.5,73.2z M183.9,31.8c-5.8,0-10.6-4.8-10.6-10.6s4.8-10.6,10.6-10.6
|
|
||||||
s10.6,4.8,10.6,10.6C194.5,27,189.8,31.8,183.9,31.8z"
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
@ -1,9 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
export let isAbsolute = false;
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<img
|
|
||||||
alt="nocodb logo"
|
|
||||||
class={isAbsolute ? 'w-10 h-10 absolute top-0 left-0 -m-5' : 'w-8 h-8 mx-auto'}
|
|
||||||
src="/nocodb.png"
|
|
||||||
/>
|
|
@ -1,9 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
export let isAbsolute = false;
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<img
|
|
||||||
alt="plausible logo"
|
|
||||||
class={isAbsolute ? 'w-9 h-12 absolute top-0 left-0 -m-4' : 'w-6 h-8 mx-auto'}
|
|
||||||
src="/plausible.png"
|
|
||||||
/>
|
|
@ -1,56 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
export let isAbsolute = false;
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<svg
|
|
||||||
xmlns:cc="http://creativecommons.org/ns#"
|
|
||||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
id="svg8"
|
|
||||||
version="1.1"
|
|
||||||
viewBox="0 0 92 92"
|
|
||||||
class={isAbsolute ? 'w-12 h-12 absolute top-0 left-0 -m-3 -mt-5' : 'w-8 h-8 mx-auto'}
|
|
||||||
>
|
|
||||||
<defs id="defs2" />
|
|
||||||
<metadata id="metadata5">
|
|
||||||
<rdf:RDF>
|
|
||||||
<cc:Work rdf:about="">
|
|
||||||
<dc:format>image/svg+xml</dc:format>
|
|
||||||
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
|
||||||
<dc:title />
|
|
||||||
</cc:Work>
|
|
||||||
</rdf:RDF>
|
|
||||||
</metadata>
|
|
||||||
<g transform="translate(-40.921303,-17.416526)" id="layer1">
|
|
||||||
<circle
|
|
||||||
r="0"
|
|
||||||
style="fill:none;stroke:#000000;stroke-width:12;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
|
||||||
cy="92"
|
|
||||||
cx="75"
|
|
||||||
id="path3713"
|
|
||||||
/>
|
|
||||||
<circle
|
|
||||||
r="30"
|
|
||||||
cy="53.902557"
|
|
||||||
cx="75.921303"
|
|
||||||
id="path834"
|
|
||||||
style="fill:none;fill-opacity:1;stroke:#3050ff;stroke-width:10;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="m 67.514849,37.91524 a 18,18 0 0 1 21.051475,3.312407 18,18 0 0 1 3.137312,21.078282"
|
|
||||||
id="path852"
|
|
||||||
style="fill:none;fill-opacity:1;stroke:#3050ff;stroke-width:5;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
|
||||||
/>
|
|
||||||
<rect
|
|
||||||
transform="rotate(-46.234709)"
|
|
||||||
ry="1.8669105e-13"
|
|
||||||
y="122.08995"
|
|
||||||
x="3.7063529"
|
|
||||||
height="39.963303"
|
|
||||||
width="18.846331"
|
|
||||||
id="rect912"
|
|
||||||
style="opacity:1;fill:#3050ff;fill-opacity:1;stroke:none;stroke-width:8;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
@ -1,49 +1,45 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
export let type: string;
|
export let type: string;
|
||||||
export let isAbsolute = true;
|
export let isAbsolute = false;
|
||||||
import * as Icons from '$lib/components/svg/services';
|
|
||||||
|
let extension = 'png';
|
||||||
|
let svgs = [
|
||||||
|
'languagetool',
|
||||||
|
'meilisearch',
|
||||||
|
'n8n',
|
||||||
|
'glitchtip',
|
||||||
|
'searxng',
|
||||||
|
'umami',
|
||||||
|
'uptimekuma',
|
||||||
|
'vaultwarden',
|
||||||
|
'weblate',
|
||||||
|
'wordpress'
|
||||||
|
];
|
||||||
|
|
||||||
|
const name: any =
|
||||||
|
type &&
|
||||||
|
(type[0].toUpperCase() + type.substring(1).toLowerCase())
|
||||||
|
.replaceAll('.', '')
|
||||||
|
.replaceAll(' ', '')
|
||||||
|
.split('-')[0]
|
||||||
|
.toLowerCase();
|
||||||
|
|
||||||
|
if (svgs.includes(name)) {
|
||||||
|
extension = 'svg';
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateClass() {
|
||||||
|
switch (name) {
|
||||||
|
case 'n8n':
|
||||||
|
return 'w-12 h-12 -mt-3';
|
||||||
|
case 'weblate':
|
||||||
|
return 'w-12 h-12 -mt-3';
|
||||||
|
default:
|
||||||
|
return isAbsolute ? 'w-10 h-10 absolute -m-4 -mt-9 left-0' : 'w-10 h-10';
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if type === 'plausibleanalytics'}
|
{#if name}
|
||||||
<Icons.PlausibleAnalytics {isAbsolute} />
|
<img class={generateClass()} src={`/icons/${name}.${extension}`} alt={`Icon of ${name}`} />
|
||||||
{:else if type === 'nocodb'}
|
|
||||||
<Icons.NocoDb {isAbsolute} />
|
|
||||||
{:else if type === 'minio'}
|
|
||||||
<Icons.MinIo {isAbsolute} />
|
|
||||||
{:else if type === 'vscodeserver'}
|
|
||||||
<Icons.VsCodeServer {isAbsolute} />
|
|
||||||
{:else if type === 'wordpress'}
|
|
||||||
<Icons.Wordpress {isAbsolute} />
|
|
||||||
{:else if type === 'vaultwarden'}
|
|
||||||
<Icons.VaultWarden {isAbsolute} />
|
|
||||||
{:else if type === 'languagetool'}
|
|
||||||
<Icons.LanguageTool {isAbsolute} />
|
|
||||||
{:else if type === 'n8n'}
|
|
||||||
<Icons.N8n {isAbsolute} />
|
|
||||||
{:else if type === 'uptimekuma'}
|
|
||||||
<Icons.UptimeKuma {isAbsolute} />
|
|
||||||
{:else if type === 'ghost'}
|
|
||||||
<Icons.Ghost {isAbsolute} />
|
|
||||||
{:else if type === 'meilisearch'}
|
|
||||||
<Icons.MeiliSearch {isAbsolute} />
|
|
||||||
{:else if type === 'umami'}
|
|
||||||
<Icons.Umami {isAbsolute} />
|
|
||||||
{:else if type === 'hasura'}
|
|
||||||
<Icons.Hasura {isAbsolute} />
|
|
||||||
{:else if type === 'fider'}
|
|
||||||
<Icons.Fider {isAbsolute} />
|
|
||||||
{:else if type === 'appwrite'}
|
|
||||||
<Icons.Appwrite {isAbsolute} />
|
|
||||||
{:else if type === 'moodle'}
|
|
||||||
<Icons.Moodle {isAbsolute} />
|
|
||||||
{:else if type === 'glitchTip'}
|
|
||||||
<Icons.GlitchTip {isAbsolute} />
|
|
||||||
{:else if type === 'searxng'}
|
|
||||||
<Icons.Searxng {isAbsolute} />
|
|
||||||
{:else if type === 'weblate'}
|
|
||||||
<Icons.Weblate {isAbsolute} />
|
|
||||||
{:else if type === 'grafana'}
|
|
||||||
<Icons.Grafana {isAbsolute} />
|
|
||||||
{:else if type === 'trilium'}
|
|
||||||
<Icons.Trilium {isAbsolute} />
|
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -1,9 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
export let isAbsolute = false;
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<img
|
|
||||||
alt="trilium logo"
|
|
||||||
class={isAbsolute ? 'w-9 h-9 absolute top-3 left-0 -m-3 -mt-5' : 'w-8 h-8 mx-auto'}
|
|
||||||
src="/trilium.png"
|
|
||||||
/>
|
|
@ -1,22 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
export let isAbsolute = false;
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<svg
|
|
||||||
class={isAbsolute ? 'w-10 h-10 absolute top-0 left-0 -m-5' : 'w-8 h-8mx-auto'}
|
|
||||||
viewBox="0 0 128 128"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M3.656 45.043s-3.027-2.191.61-5.113l8.468-7.594s2.426-2.559 4.989-.328l78.175 59.328v28.45s-.039 4.468-5.757 3.976zm0 0"
|
|
||||||
fill="#2489ca"
|
|
||||||
/><path
|
|
||||||
d="M23.809 63.379L3.656 81.742s-2.07 1.543 0 4.305l9.356 8.527s2.222 2.395 5.508-.328l21.359-16.238zm0 0"
|
|
||||||
fill="#1070b3"
|
|
||||||
/><path
|
|
||||||
d="M59.184 63.531l36.953-28.285-.239-28.297S94.32.773 89.055 3.99L39.879 48.851zm0 0"
|
|
||||||
fill="#0877b9"
|
|
||||||
/><path
|
|
||||||
d="M90.14 123.797c2.145 2.203 4.747 1.48 4.747 1.48l28.797-14.222c3.687-2.52 3.171-5.645 3.171-5.645V20.465c0-3.735-3.812-5.024-3.812-5.024L98.082 3.38c-5.453-3.379-9.027.61-9.027.61s4.593-3.317 6.843 2.96v112.317c0 .773-.164 1.53-.492 2.214-.656 1.332-2.086 2.57-5.504 2.051zm0 0"
|
|
||||||
fill="#3c99d4"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
@ -1,12 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
export let isAbsolute = false;
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<svg class={isAbsolute ? 'w-10 h-10 absolute top-0 left-0 -m-5' : 'w-8 h-8mx-auto'} viewBox="0 0 128 128">
|
|
||||||
<path
|
|
||||||
fill-rule="evenodd"
|
|
||||||
clip-rule="evenodd"
|
|
||||||
fill="white"
|
|
||||||
d="M64.094 126.224c34.275-.052 62.021-27.933 62.021-62.325 0-33.833-27.618-61.697-60.613-62.286C30.85.995 1.894 29.113 1.885 63.21c-.01 35.079 27.612 63.064 62.209 63.014zM63.993 4.63c32.907-.011 59.126 26.725 59.116 60.28-.011 31.679-26.925 58.18-59.092 58.187-32.771.007-59.125-26.563-59.124-59.608.002-32.193 26.766-58.848 59.1-58.859zM39.157 35.896c.538 1.793-.968 2.417-2.569 2.542-1.685.13-3.369.257-5.325.406 6.456 19.234 12.815 38.183 19.325 57.573.464-.759.655-.973.739-1.223 3.574-10.682 7.168-21.357 10.651-32.069.318-.977.16-2.271-.188-3.275-1.843-5.32-4.051-10.524-5.667-15.908-1.105-3.686-2.571-6.071-6.928-5.644-.742.073-1.648-1.524-2.479-2.349 1.005-.6 2.003-1.704 3.017-1.719a849.593 849.593 0 0126.618.008c1.018.017 2.016 1.15 3.021 1.765-.88.804-1.639 2.01-2.668 2.321-1.651.498-3.482.404-5.458.58l19.349 57.56c2.931-9.736 5.658-18.676 8.31-27.639 2.366-8.001.956-15.473-3.322-22.52-1.286-2.119-2.866-4.175-3.595-6.486-.828-2.629-1.516-5.622-1.077-8.259.745-4.469 4.174-6.688 8.814-7.113C74.333.881 34.431 9.317 19.728 34.922c5.66-.261 11.064-.604 16.472-.678 1.022-.013 2.717.851 2.957 1.652zm10.117 77.971c-.118.345-.125.729-.218 1.302 10.943 3.034 21.675 2.815 32.659-.886l-16.78-45.96c-5.37 15.611-10.52 30.575-15.661 45.544zm-8.456-2.078l-25.281-69.35c-11.405 22.278-2.729 56.268 25.281 69.35zm76.428-44.562c.802-10.534-2.832-25.119-5.97-27.125-.35 3.875-.106 8.186-1.218 12.114-2.617 9.255-5.817 18.349-8.899 27.468-3.35 9.912-6.832 19.779-10.257 29.666 16.092-9.539 24.935-23.618 26.344-42.123z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
@ -1,22 +0,0 @@
|
|||||||
//@ts-nocheck
|
|
||||||
export { default as PlausibleAnalytics } from './PlausibleAnalytics.svelte';
|
|
||||||
export { default as NocoDb } from './NocoDB.svelte';
|
|
||||||
export { default as MinIo } from './MinIO.svelte';
|
|
||||||
export { default as VsCodeServer } from './VSCodeServer.svelte';
|
|
||||||
export { default as Wordpress } from './Wordpress.svelte';
|
|
||||||
export { default as VaultWarden } from './VaultWarden.svelte';
|
|
||||||
export { default as LanguageTool } from './LanguageTool.svelte';
|
|
||||||
export { default as N8n } from './N8n.svelte';
|
|
||||||
export { default as UptimeKuma } from './UptimeKuma.svelte';
|
|
||||||
export { default as Ghost } from './Ghost.svelte';
|
|
||||||
export { default as MeiliSearch } from './MeiliSearch.svelte';
|
|
||||||
export { default as Umami } from './Umami.svelte';
|
|
||||||
export { default as Hasura } from './Hasura.svelte';
|
|
||||||
export { default as Fider } from './Fider.svelte';
|
|
||||||
export { default as Appwrite } from './Appwrite.svelte';
|
|
||||||
export { default as Moodle } from './Moodle.svelte';
|
|
||||||
export { default as GlitchTip } from './GlitchTip.svelte';
|
|
||||||
export { default as Searxng } from './Searxng.svelte';
|
|
||||||
export { default as Weblate } from './Weblate.svelte';
|
|
||||||
export { default as Grafana } from './Grafana.svelte';
|
|
||||||
export { default as Trilium } from './Trilium.svelte'
|
|
@ -41,7 +41,7 @@
|
|||||||
"saving": "Saving...",
|
"saving": "Saving...",
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
"value": "Value",
|
"value": "Value",
|
||||||
"action": "Action",
|
"action": "Actions",
|
||||||
"is_required": "is required.",
|
"is_required": "is required.",
|
||||||
"add": "Add",
|
"add": "Add",
|
||||||
"set": "Set",
|
"set": "Set",
|
||||||
|
@ -1,7 +1,11 @@
|
|||||||
import { dev } from '$app/env';
|
import { dev } from '$app/env';
|
||||||
import cuid from 'cuid';
|
import cuid from 'cuid';
|
||||||
|
import Cookies from 'js-cookie';
|
||||||
import { writable, readable, type Writable } from 'svelte/store';
|
import { writable, readable, type Writable } from 'svelte/store';
|
||||||
|
import { io as ioClient } from 'socket.io-client';
|
||||||
|
const socket = ioClient(dev ? 'http://localhost:3001' : '/', { auth: { token: Cookies.get('token') }, autoConnect: false });
|
||||||
|
|
||||||
|
export const io = socket;
|
||||||
interface AppSession {
|
interface AppSession {
|
||||||
isRegistrationEnabled: boolean;
|
isRegistrationEnabled: boolean;
|
||||||
ipv4: string | null,
|
ipv4: string | null,
|
||||||
@ -19,7 +23,6 @@ interface AppSession {
|
|||||||
github: string | null,
|
github: string | null,
|
||||||
gitlab: string | null,
|
gitlab: string | null,
|
||||||
},
|
},
|
||||||
supportedServiceTypesAndVersions: Array<any>
|
|
||||||
pendingInvitations: Array<any>
|
pendingInvitations: Array<any>
|
||||||
}
|
}
|
||||||
interface AddToast {
|
interface AddToast {
|
||||||
@ -48,7 +51,6 @@ export const appSession: Writable<AppSession> = writable({
|
|||||||
github: null,
|
github: null,
|
||||||
gitlab: null
|
gitlab: null
|
||||||
},
|
},
|
||||||
supportedServiceTypesAndVersions: [],
|
|
||||||
pendingInvitations: []
|
pendingInvitations: []
|
||||||
});
|
});
|
||||||
export const disabledButton: Writable<boolean> = writable(false);
|
export const disabledButton: Writable<boolean> = writable(false);
|
||||||
@ -81,9 +83,10 @@ export const status: Writable<any> = writable({
|
|||||||
initialLoading: true
|
initialLoading: true
|
||||||
},
|
},
|
||||||
service: {
|
service: {
|
||||||
isRunning: false,
|
statuses: [],
|
||||||
isExited: false,
|
overallStatus: 'stopped',
|
||||||
loading: false,
|
loading: false,
|
||||||
|
startup: {},
|
||||||
initialLoading: true
|
initialLoading: true
|
||||||
},
|
},
|
||||||
database: {
|
database: {
|
||||||
@ -161,4 +164,11 @@ export const addToast = (toast: AddToast) => {
|
|||||||
toasts.update((all: any) => [t, ...all])
|
toasts.update((all: any) => [t, ...all])
|
||||||
}
|
}
|
||||||
|
|
||||||
export const selectedBuildId: any = writable(null)
|
export const selectedBuildId: any = writable(null)
|
||||||
|
|
||||||
|
type State = {
|
||||||
|
requests: Array<Request>;
|
||||||
|
};
|
||||||
|
export const state = writable<State>({
|
||||||
|
requests: [],
|
||||||
|
});
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
|
||||||
<div class="dropdown dropdown-bottom">
|
<div class="dropdown dropdown-bottom">
|
||||||
<slot>
|
<slot>
|
||||||
<label for="new" tabindex="0" class="btn btn-sm text-sm bg-coollabs hover:bg-coollabs-100 w-64">
|
<label for="new" tabindex="0" class="btn btn-sm text-sm bg-coollabs hover:bg-coollabs-100 w-64">
|
||||||
|
@ -65,7 +65,6 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
export let baseSettings: any;
|
export let baseSettings: any;
|
||||||
export let supportedServiceTypesAndVersions: any;
|
|
||||||
export let pendingInvitations: any = 0;
|
export let pendingInvitations: any = 0;
|
||||||
|
|
||||||
$appSession.isRegistrationEnabled = baseSettings.isRegistrationEnabled;
|
$appSession.isRegistrationEnabled = baseSettings.isRegistrationEnabled;
|
||||||
@ -74,7 +73,6 @@
|
|||||||
$appSession.version = baseSettings.version;
|
$appSession.version = baseSettings.version;
|
||||||
$appSession.whiteLabeled = baseSettings.whiteLabeled;
|
$appSession.whiteLabeled = baseSettings.whiteLabeled;
|
||||||
$appSession.whiteLabeledDetails.icon = baseSettings.whiteLabeledIcon;
|
$appSession.whiteLabeledDetails.icon = baseSettings.whiteLabeledIcon;
|
||||||
$appSession.supportedServiceTypesAndVersions = supportedServiceTypesAndVersions;
|
|
||||||
|
|
||||||
$appSession.pendingInvitations = pendingInvitations;
|
$appSession.pendingInvitations = pendingInvitations;
|
||||||
|
|
||||||
@ -83,6 +81,7 @@
|
|||||||
export let permission: string;
|
export let permission: string;
|
||||||
export let isAdmin: boolean;
|
export let isAdmin: boolean;
|
||||||
|
|
||||||
|
import { status, io } from '$lib/store';
|
||||||
import '../tailwind.css';
|
import '../tailwind.css';
|
||||||
import Cookies from 'js-cookie';
|
import Cookies from 'js-cookie';
|
||||||
import { fade } from 'svelte/transition';
|
import { fade } from 'svelte/transition';
|
||||||
@ -95,6 +94,7 @@
|
|||||||
import { appSession } from '$lib/store';
|
import { appSession } from '$lib/store';
|
||||||
import Toasts from '$lib/components/Toasts.svelte';
|
import Toasts from '$lib/components/Toasts.svelte';
|
||||||
import Tooltip from '$lib/components/Tooltip.svelte';
|
import Tooltip from '$lib/components/Tooltip.svelte';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
if (userId) $appSession.userId = userId;
|
if (userId) $appSession.userId = userId;
|
||||||
if (teamId) $appSession.teamId = teamId;
|
if (teamId) $appSession.teamId = teamId;
|
||||||
@ -109,6 +109,16 @@
|
|||||||
return errorNotification(error);
|
return errorNotification(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
onMount(async () => {
|
||||||
|
io.connect();
|
||||||
|
io.on('start-service', (message) => {
|
||||||
|
const { serviceId, state } = message;
|
||||||
|
$status.service.startup[serviceId] = state;
|
||||||
|
if (state === 0 || state === 1) {
|
||||||
|
delete $status.service.startup[serviceId];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
@ -259,6 +269,7 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
<div
|
<div
|
||||||
id="logout"
|
id="logout"
|
||||||
class="icons bg-coolgray-200 hover:text-error cursor-pointer"
|
class="icons bg-coolgray-200 hover:text-error cursor-pointer"
|
||||||
@ -287,7 +298,7 @@
|
|||||||
<a
|
<a
|
||||||
class="text-[10px] no-underline"
|
class="text-[10px] no-underline"
|
||||||
href={`https://github.com/coollabsio/coolify/releases/tag/v${$appSession.version}`}
|
href={`https://github.com/coollabsio/coolify/releases/tag/v${$appSession.version}`}
|
||||||
target="_blank">v{$appSession.version}</a
|
target="_blank noreferrer">v{$appSession.version}</a
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -295,7 +306,7 @@
|
|||||||
</nav>
|
</nav>
|
||||||
{#if $appSession.whiteLabeled}
|
{#if $appSession.whiteLabeled}
|
||||||
<span class="fixed bottom-0 left-[50px] z-50 m-2 px-4 text-xs text-stone-700"
|
<span class="fixed bottom-0 left-[50px] z-50 m-2 px-4 text-xs text-stone-700"
|
||||||
>Powered by <a href="https://coolify.io" target="_blank">Coolify</a></span
|
>Powered by <a href="https://coolify.io" target="_blank noreferrer">Coolify</a></span
|
||||||
>
|
>
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
@ -436,6 +447,7 @@
|
|||||||
<UpdateAvailable />
|
<UpdateAvailable />
|
||||||
</div>
|
</div>
|
||||||
<li>
|
<li>
|
||||||
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
<div class="no-underline icons hover:bg-error" on:click={logout}>
|
<div class="no-underline icons hover:bg-error" on:click={logout}>
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
@ -460,7 +472,7 @@
|
|||||||
<a
|
<a
|
||||||
class="text-xs hover:bg-coolgray-200 no-underline hover:text-white text-right"
|
class="text-xs hover:bg-coolgray-200 no-underline hover:text-white text-right"
|
||||||
href={`https://github.com/coollabsio/coolify/releases/tag/v${$appSession.version}`}
|
href={`https://github.com/coollabsio/coolify/releases/tag/v${$appSession.version}`}
|
||||||
target="_blank">v{$appSession.version}</a
|
target="_blank noreferrer">v{$appSession.version}</a
|
||||||
>
|
>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
<ul class="menu border bg-coolgray-100 border-coolgray-200 rounded p-2 space-y-2 sticky top-4">
|
<ul class="menu border bg-coolgray-100 border-coolgray-200 rounded p-2 space-y-2 sticky top-4">
|
||||||
<li class="menu-title">
|
<li class="menu-title">
|
||||||
<span>Configuration</span>
|
<span>General</span>
|
||||||
</li>
|
</li>
|
||||||
{#if application.gitSource?.htmlUrl && application.repository && application.branch}
|
{#if application.gitSource?.htmlUrl && application.repository && application.branch}
|
||||||
<li>
|
<li>
|
||||||
@ -86,7 +86,7 @@
|
|||||||
<path
|
<path
|
||||||
d="M7 10h3v-3l-3.5 -3.5a6 6 0 0 1 8 8l6 6a2 2 0 0 1 -3 3l-6 -6a6 6 0 0 1 -8 -8l3.5 3.5"
|
d="M7 10h3v-3l-3.5 -3.5a6 6 0 0 1 8 8l6 6a2 2 0 0 1 -3 3l-6 -6a6 6 0 0 1 -8 -8l3.5 3.5"
|
||||||
/>
|
/>
|
||||||
</svg>Build & Deploy</a
|
</svg>Configuration</a
|
||||||
>
|
>
|
||||||
</li>
|
</li>
|
||||||
<li
|
<li
|
||||||
|
@ -39,11 +39,11 @@
|
|||||||
|
|
||||||
async function addNewSecret() {
|
async function addNewSecret() {
|
||||||
try {
|
try {
|
||||||
if (!name) return errorNotification({ message: 'Name is required.' });
|
if (!name.trim()) return errorNotification({ message: 'Name is required.' });
|
||||||
if (!value) return errorNotification({ message: 'Value is required.' });
|
if (!value.trim()) return errorNotification({ message: 'Value is required.' });
|
||||||
await post(`/applications/${id}/secrets`, {
|
await post(`/applications/${id}/secrets`, {
|
||||||
name,
|
name: name.trim(),
|
||||||
value,
|
value: value.trim(),
|
||||||
isBuildSecret
|
isBuildSecret
|
||||||
});
|
});
|
||||||
cleanupState();
|
cleanupState();
|
||||||
@ -64,8 +64,8 @@
|
|||||||
if (isNewSecret) return;
|
if (isNewSecret) return;
|
||||||
try {
|
try {
|
||||||
await put(`/applications/${id}/secrets`, {
|
await put(`/applications/${id}/secrets`, {
|
||||||
name,
|
name: name.trim(),
|
||||||
value,
|
value: value.trim(),
|
||||||
isBuildSecret: changeIsBuildSecret ? isBuildSecret : undefined
|
isBuildSecret: changeIsBuildSecret ? isBuildSecret : undefined
|
||||||
});
|
});
|
||||||
addToast({
|
addToast({
|
||||||
@ -91,7 +91,7 @@
|
|||||||
required
|
required
|
||||||
placeholder="EXAMPLE_VARIABLE"
|
placeholder="EXAMPLE_VARIABLE"
|
||||||
readonly={!isNewSecret}
|
readonly={!isNewSecret}
|
||||||
class=" w-full"
|
class="w-full"
|
||||||
class:bg-coolblack={!isNewSecret}
|
class:bg-coolblack={!isNewSecret}
|
||||||
class:border={!isNewSecret}
|
class:border={!isNewSecret}
|
||||||
class:border-dashed={!isNewSecret}
|
class:border-dashed={!isNewSecret}
|
||||||
@ -166,7 +166,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex flex-row lg:flex-col lg:items-center items-start">
|
<div class="flex flex-row lg:flex-col lg:items-center items-start">
|
||||||
{#if (index === 0 && !isNewSecret) || length === 0}
|
{#if (index === 0 && !isNewSecret) || length === 0}
|
||||||
<label for="name" class="pb-2 uppercase lg:block hidden">Actions</label>
|
<label for="name" class="pb-2 uppercase lg:block hidden">Action</label>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="flex justify-center h-full items-center pt-3">
|
<div class="flex justify-center h-full items-center pt-3">
|
||||||
|
@ -59,19 +59,33 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="w-full font-bold grid gap-2">
|
<div class="w-full lg:px-0 px-4">
|
||||||
<div class="flex flex-col pb-2">
|
<div class="grid grid-col-1 lg:grid-cols-3 lg:space-x-4" class:pt-8={isNew}>
|
||||||
|
{#if storage.id}
|
||||||
<div class="flex flex-col lg:flex-row lg:space-y-0 space-y-2">
|
<div class="flex flex-col">
|
||||||
|
<label for="name" class="pb-2 uppercase font-bold">Volume name</label>
|
||||||
|
<input
|
||||||
|
disabled
|
||||||
|
readonly
|
||||||
|
class="w-full lg:w-64"
|
||||||
|
value="{storage.id}{storage.path.replace(/\//gi, '-')}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<label for="name" class="pb-2 uppercase font-bold">{isNew ? 'New Path' : 'Path'}</label>
|
||||||
<input
|
<input
|
||||||
class="w-full lg:w-64"
|
class="w-full lg:w-64"
|
||||||
bind:value={storage.path}
|
bind:value={storage.path}
|
||||||
required
|
required
|
||||||
placeholder="eg: /sqlite.db"
|
placeholder="eg: /sqlite.db"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pt-8">
|
||||||
{#if isNew}
|
{#if isNew}
|
||||||
<div class="flex items-center justify-center w-full lg:w-64">
|
<div class="flex items-center justify-center w-full lg:w-64">
|
||||||
<button class="btn btn-sm btn-primary" on:click={() => saveStorage(true)}
|
<button class="btn btn-sm btn-primary w-full" on:click={() => saveStorage(true)}
|
||||||
>{$t('forms.add')}</button
|
>{$t('forms.add')}</button
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
@ -75,7 +75,7 @@
|
|||||||
|
|
||||||
let statusInterval: any;
|
let statusInterval: any;
|
||||||
let forceDelete = false;
|
let forceDelete = false;
|
||||||
|
let stopping = false;
|
||||||
const { id } = $page.params;
|
const { id } = $page.params;
|
||||||
$isDeploymentEnabled = checkIfDeploymentEnabledApplications($appSession.isAdmin, application);
|
$isDeploymentEnabled = checkIfDeploymentEnabledApplications($appSession.isAdmin, application);
|
||||||
|
|
||||||
@ -138,17 +138,17 @@
|
|||||||
}
|
}
|
||||||
async function stopApplication() {
|
async function stopApplication() {
|
||||||
try {
|
try {
|
||||||
$status.application.initialLoading = true;
|
stopping = true;
|
||||||
await post(`/applications/${id}/stop`, {});
|
await post(`/applications/${id}/stop`, {});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return errorNotification(error);
|
return errorNotification(error);
|
||||||
} finally {
|
} finally {
|
||||||
$status.application.initialLoading = false;
|
stopping = false;
|
||||||
await getStatus();
|
await getStatus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async function getStatus() {
|
async function getStatus() {
|
||||||
if ($status.application.loading) return;
|
if ($status.application.loading && stopping) return;
|
||||||
$status.application.loading = true;
|
$status.application.loading = true;
|
||||||
const data = await get(`/applications/${id}/status`);
|
const data = await get(`/applications/${id}/status`);
|
||||||
|
|
||||||
@ -194,6 +194,8 @@
|
|||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
$status.application.initialLoading = true;
|
$status.application.initialLoading = true;
|
||||||
$status.application.loading = false;
|
$status.application.loading = false;
|
||||||
|
$status.application.statuses = [];
|
||||||
|
$status.application.overallStatus = 'stopped';
|
||||||
$location = null;
|
$location = null;
|
||||||
$isDeploymentEnabled = false;
|
$isDeploymentEnabled = false;
|
||||||
clearInterval(statusInterval);
|
clearInterval(statusInterval);
|
||||||
@ -233,7 +235,7 @@
|
|||||||
class:text-red-500={$status.application.overallStatus === 'stopped'}
|
class:text-red-500={$status.application.overallStatus === 'stopped'}
|
||||||
>
|
>
|
||||||
{$status.application.overallStatus === 'healthy'
|
{$status.application.overallStatus === 'healthy'
|
||||||
? 'Running'
|
? 'Healthy'
|
||||||
: $status.application.overallStatus === 'degraded'
|
: $status.application.overallStatus === 'degraded'
|
||||||
? 'Degraded'
|
? 'Degraded'
|
||||||
: 'Stopped'}
|
: 'Stopped'}
|
||||||
@ -296,7 +298,29 @@
|
|||||||
Application Error
|
Application Error
|
||||||
</a>
|
</a>
|
||||||
{/if}
|
{/if}
|
||||||
{#if $status.application.initialLoading}
|
{#if stopping}
|
||||||
|
<button class="btn btn-ghost btn-sm gap-2">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
class="h-6 w-6 animate-spin duration-500 ease-in-out"
|
||||||
|
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="M9 4.55a8 8 0 0 1 6 14.9m0 -4.45v5h5" />
|
||||||
|
<line x1="5.63" y1="7.16" x2="5.63" y2="7.17" />
|
||||||
|
<line x1="4.06" y1="11" x2="4.06" y2="11.01" />
|
||||||
|
<line x1="4.63" y1="15.1" x2="4.63" y2="15.11" />
|
||||||
|
<line x1="7.16" y1="18.37" x2="7.16" y2="18.38" />
|
||||||
|
<line x1="11" y1="19.94" x2="11" y2="19.95" />
|
||||||
|
</svg>
|
||||||
|
Stopping...
|
||||||
|
</button>
|
||||||
|
{:else if $status.application.initialLoading}
|
||||||
<button class="btn btn-ghost btn-sm gap-2">
|
<button class="btn btn-ghost btn-sm gap-2">
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
@ -319,27 +343,6 @@
|
|||||||
Loading...
|
Loading...
|
||||||
</button>
|
</button>
|
||||||
{:else if $status.application.overallStatus === 'healthy'}
|
{:else if $status.application.overallStatus === 'healthy'}
|
||||||
<button
|
|
||||||
on:click={stopApplication}
|
|
||||||
type="submit"
|
|
||||||
disabled={!$isDeploymentEnabled}
|
|
||||||
class="btn btn-sm btn-error gap-2"
|
|
||||||
>
|
|
||||||
<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" />
|
|
||||||
<rect x="6" y="5" width="4" height="14" rx="1" />
|
|
||||||
<rect x="14" y="5" width="4" height="14" rx="1" />
|
|
||||||
</svg> Stop
|
|
||||||
</button>
|
|
||||||
{#if application.buildPack !== 'compose'}
|
{#if application.buildPack !== 'compose'}
|
||||||
<button
|
<button
|
||||||
on:click={restartApplication}
|
on:click={restartApplication}
|
||||||
@ -387,17 +390,38 @@
|
|||||||
|
|
||||||
Force Redeploy
|
Force Redeploy
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
on:click={stopApplication}
|
||||||
|
type="submit"
|
||||||
|
disabled={!$isDeploymentEnabled}
|
||||||
|
class="btn btn-sm gap-2"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
class="w-6 h-6 text-error"
|
||||||
|
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" />
|
||||||
|
<rect x="6" y="5" width="4" height="14" rx="1" />
|
||||||
|
<rect x="14" y="5" width="4" height="14" rx="1" />
|
||||||
|
</svg> Stop
|
||||||
|
</button>
|
||||||
{:else if $isDeploymentEnabled && !$page.url.pathname.startsWith(`/applications/${id}/configuration/`)}
|
{:else if $isDeploymentEnabled && !$page.url.pathname.startsWith(`/applications/${id}/configuration/`)}
|
||||||
{#if $status.application.overallStatus === 'degraded'}
|
{#if $status.application.overallStatus === 'degraded'}
|
||||||
<button
|
<button
|
||||||
on:click={stopApplication}
|
on:click={stopApplication}
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={!$isDeploymentEnabled}
|
disabled={!$isDeploymentEnabled}
|
||||||
class="btn btn-sm btn-error gap-2"
|
class="btn btn-sm gap-2"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
class="w-6 h-6 "
|
class="w-6 h-6 text-error"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
stroke-width="1.5"
|
stroke-width="1.5"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
@ -413,14 +437,13 @@
|
|||||||
{/if}
|
{/if}
|
||||||
<button
|
<button
|
||||||
class="btn btn-sm gap-2"
|
class="btn btn-sm gap-2"
|
||||||
class:btn-primary={$status.application.overallStatus !== 'degraded'}
|
|
||||||
disabled={!$isDeploymentEnabled}
|
disabled={!$isDeploymentEnabled}
|
||||||
on:click={() => handleDeploySubmit(false)}
|
on:click={() => handleDeploySubmit(false)}
|
||||||
>
|
>
|
||||||
{#if $status.application.overallStatus !== 'degraded'}
|
{#if $status.application.overallStatus !== 'degraded'}
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
class="w-6 h-6"
|
class="w-6 h-6 text-pink-500"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
stroke-width="1.5"
|
stroke-width="1.5"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
@ -457,7 +480,7 @@
|
|||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
{#if $location && $status.application.overallStatus === 'healthy'}
|
{#if $location && $status.application.overallStatus === 'healthy'}
|
||||||
<a href={$location} target="_blank" class="btn btn-sm gap-2 text-sm bg-primary"
|
<a href={$location} target="_blank noreferrer" class="btn btn-sm gap-2 text-sm bg-primary"
|
||||||
><svg
|
><svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
class="h-6 w-6"
|
class="h-6 w-6"
|
||||||
|
@ -92,27 +92,9 @@
|
|||||||
label: branch.name
|
label: branch.name
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
async function isBranchAlreadyUsed(event: any) {
|
async function selectBranch(event: any) {
|
||||||
selected.branch = event.detail.value;
|
selected.branch = event.detail.value;
|
||||||
try {
|
showSave = true;
|
||||||
// const data = await get(
|
|
||||||
// `/applications/${id}/configuration/repository?repository=${selected.repository}&branch=${selected.branch}`
|
|
||||||
// );
|
|
||||||
// if (data.used) {
|
|
||||||
// const sure = confirm($t('application.configuration.branch_already_in_use'));
|
|
||||||
// if (sure) {
|
|
||||||
// selected.autodeploy = false;
|
|
||||||
// showSave = true;
|
|
||||||
// return true;
|
|
||||||
// }
|
|
||||||
// showSave = false;
|
|
||||||
// return true;
|
|
||||||
// }
|
|
||||||
showSave = true;
|
|
||||||
} catch (error) {
|
|
||||||
showSave = false;
|
|
||||||
return errorNotification(error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
@ -178,7 +160,7 @@
|
|||||||
isWaiting={loading.branches}
|
isWaiting={loading.branches}
|
||||||
showIndicator={selected.repository && !loading.branches}
|
showIndicator={selected.repository && !loading.branches}
|
||||||
id="branches"
|
id="branches"
|
||||||
on:select={isBranchAlreadyUsed}
|
on:select={selectBranch}
|
||||||
items={branchSelectOptions}
|
items={branchSelectOptions}
|
||||||
isDisabled={loading.branches || !selected.repository}
|
isDisabled={loading.branches || !selected.repository}
|
||||||
isClearable={false}
|
isClearable={false}
|
||||||
@ -186,10 +168,9 @@
|
|||||||
</div></div>
|
</div></div>
|
||||||
<div class="pt-5 flex-col flex justify-center items-center space-y-4">
|
<div class="pt-5 flex-col flex justify-center items-center space-y-4">
|
||||||
<button
|
<button
|
||||||
class="btn btn-wide"
|
class="btn btn-wide btn-primary"
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={!showSave}
|
disabled={!showSave}
|
||||||
class:bg-applications={showSave}
|
|
||||||
>{$t('forms.save')}</button
|
>{$t('forms.save')}</button
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
@ -195,27 +195,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function isBranchAlreadyUsed(event) {
|
async function selectBranch(event: any) {
|
||||||
selected.branch = event.detail;
|
selected.branch = event.detail;
|
||||||
try {
|
showSave = true;
|
||||||
// const data = await get(
|
|
||||||
// `/applications/${id}/configuration/repository?repository=${selected.project.path_with_namespace}&branch=${selected.branch.name}`
|
|
||||||
// );
|
|
||||||
// if (data.used) {
|
|
||||||
// const sure = confirm($t('application.configuration.branch_already_in_use'));
|
|
||||||
// if (sure) {
|
|
||||||
// autodeploy = false;
|
|
||||||
// showSave = true;
|
|
||||||
// return true;
|
|
||||||
// }
|
|
||||||
// showSave = false;
|
|
||||||
// return true;
|
|
||||||
// }
|
|
||||||
showSave = true;
|
|
||||||
} catch (error) {
|
|
||||||
return errorNotification(error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function checkSSHKey(sshkeyUrl: any) {
|
async function checkSSHKey(sshkeyUrl: any) {
|
||||||
try {
|
try {
|
||||||
return await post(sshkeyUrl, {});
|
return await post(sshkeyUrl, {});
|
||||||
@ -394,7 +378,7 @@
|
|||||||
showIndicator={!loading.branches}
|
showIndicator={!loading.branches}
|
||||||
isWaiting={loading.branches}
|
isWaiting={loading.branches}
|
||||||
isDisabled={loading.branches || !selected.project}
|
isDisabled={loading.branches || !selected.project}
|
||||||
on:select={isBranchAlreadyUsed}
|
on:select={selectBranch}
|
||||||
on:clear={() => {
|
on:clear={() => {
|
||||||
showSave = false;
|
showSave = false;
|
||||||
selected.branch = null;
|
selected.branch = null;
|
||||||
@ -414,7 +398,6 @@
|
|||||||
class="btn btn-wide"
|
class="btn btn-wide"
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={!showSave || loading.save}
|
disabled={!showSave || loading.save}
|
||||||
class:bg-applications={showSave && !loading.save}
|
|
||||||
>{loading.save ? $t('forms.saving') : $t('forms.save')}</button
|
>{loading.save ? $t('forms.saving') : $t('forms.save')}</button
|
||||||
>
|
>
|
||||||
{#if tryAgain}
|
{#if tryAgain}
|
||||||
@ -423,7 +406,7 @@
|
|||||||
configuration <a href={`/sources/${application.gitSource.id}`}>here.</a>
|
configuration <a href={`/sources/${application.gitSource.id}`}>here.</a>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
class="btn btn-sm w-40 bg-green-600"
|
class="btn btn-sm w-40 btn-primary"
|
||||||
on:click|stopPropagation|preventDefault={() => window.location.reload()}
|
on:click|stopPropagation|preventDefault={() => window.location.reload()}
|
||||||
>
|
>
|
||||||
Try again
|
Try again
|
||||||
|
@ -39,6 +39,9 @@
|
|||||||
if (branch[0] === 'tree' && branch[1]) {
|
if (branch[0] === 'tree' && branch[1]) {
|
||||||
branchName = branch[1];
|
branchName = branch[1];
|
||||||
}
|
}
|
||||||
|
if (branch.length === 1) {
|
||||||
|
branchName = branch[0]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (host === 'gitlab.com') {
|
if (host === 'gitlab.com') {
|
||||||
host = 'gitlab.com/api/v4';
|
host = 'gitlab.com/api/v4';
|
||||||
@ -46,6 +49,9 @@
|
|||||||
if (branch[1] === 'tree' && branch[2]) {
|
if (branch[1] === 'tree' && branch[2]) {
|
||||||
branchName = branch[2];
|
branchName = branch[2];
|
||||||
}
|
}
|
||||||
|
if (branch.length === 1) {
|
||||||
|
branchName = branch[0]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const apiUrl = `${protocol}://${host}`;
|
const apiUrl = `${protocol}://${host}`;
|
||||||
if (type === 'github') {
|
if (type === 'github') {
|
||||||
@ -165,7 +171,7 @@
|
|||||||
placeholder="eg: https://github.com/coollabsio/nodejs-example/tree/main"
|
placeholder="eg: https://github.com/coollabsio/nodejs-example/tree/main"
|
||||||
bind:value={publicRepositoryLink}
|
bind:value={publicRepositoryLink}
|
||||||
/>
|
/>
|
||||||
<button class="btn bg-orange-600" type="submit">
|
<button class="btn btn-primary" disabled={loading.branches} type="submit" class:loading={loading.branches}>
|
||||||
Load Repository
|
Load Repository
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -88,31 +88,111 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row mx-auto">
|
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row mx-auto gap-4">
|
||||||
{#each ownDestinations as destination}
|
{#each ownDestinations as destination}
|
||||||
<div class="p-2">
|
<button
|
||||||
<form on:submit|preventDefault={() => handleSubmit(destination.id)}>
|
on:click={() => handleSubmit(destination.id)}
|
||||||
<button type="submit" class="box-selection hover:bg-sky-700 font-bold">
|
class="box-selection hover:bg-primary font-bold relative"
|
||||||
<div class="font-bold text-xl text-center truncate">{destination.name}</div>
|
>
|
||||||
<div class="text-center truncate">{destination.network}</div>
|
<svg
|
||||||
</button>
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
</form>
|
class="absolute top-0 left-0 -m-4 h-12 w-12 text-sky-500"
|
||||||
</div>
|
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-2 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="font-bold text-xl text-center truncate">{destination.name}</div>
|
||||||
|
<div class="text-center truncate">{destination.network}</div>
|
||||||
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{#if otherDestinations.length > 0 && $appSession.teamId === '0'}
|
{#if otherDestinations.length > 0 && $appSession.teamId === '0'}
|
||||||
<div class="px-6 pb-5 pt-10 title">Other Destinations</div>
|
<div class="px-6 pb-5 pt-10 title">Other Destinations</div>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row mx-auto">
|
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row mx-auto gap-4">
|
||||||
{#each otherDestinations as destination}
|
{#each otherDestinations as destination}
|
||||||
<div class="p-2">
|
<button
|
||||||
<form on:submit|preventDefault={() => handleSubmit(destination.id)}>
|
class="box-selection hover:bg-sky-700 font-bold relative"
|
||||||
<button type="submit" class="box-selection hover:bg-sky-700 font-bold">
|
on:click={() => handleSubmit(destination.id)}
|
||||||
<div class="font-bold text-xl text-center truncate">{destination.name}</div>
|
>
|
||||||
<div class="text-center truncate">{destination.network}</div>
|
<svg
|
||||||
</button>
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
</form>
|
class="absolute top-0 left-0 -m-4 h-12 w-12 text-sky-500"
|
||||||
</div>
|
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-2 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="font-bold text-xl text-center truncate">{destination.name}</div>
|
||||||
|
<div class="text-center truncate">{destination.network}</div>
|
||||||
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -68,7 +68,9 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="max-w-screen-2xl mx-auto px-9">
|
<div class="max-w-screen-2xl mx-auto px-9">
|
||||||
<div class="title pb-8">Git App</div>
|
{#if !filteredSources}
|
||||||
|
<div class="title pb-8">Git App</div>
|
||||||
|
{/if}
|
||||||
<div class="flex flex-wrap justify-center">
|
<div class="flex flex-wrap justify-center">
|
||||||
{#if !filteredSources}
|
{#if !filteredSources}
|
||||||
<div class="flex-col">
|
<div class="flex-col">
|
||||||
@ -76,10 +78,7 @@
|
|||||||
{$t('application.configuration.no_configurable_git')}
|
{$t('application.configuration.no_configurable_git')}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-center">
|
<div class="flex justify-center">
|
||||||
<a
|
<a href="/sources/new?from={$page.url.pathname}" class="add-icon">
|
||||||
href="/sources/new?from={$page.url.pathname}"
|
|
||||||
class="add-icon bg-orange-600 hover:bg-orange-500"
|
|
||||||
>
|
|
||||||
<svg
|
<svg
|
||||||
class="w-6"
|
class="w-6"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
@ -141,7 +140,7 @@
|
|||||||
<button
|
<button
|
||||||
disabled={source.gitlabApp && !source.gitlabAppId}
|
disabled={source.gitlabApp && !source.gitlabAppId}
|
||||||
type="submit"
|
type="submit"
|
||||||
class="disabled:opacity-95 bg-coolgray-200 disabled:text-white box-selection hover:bg-orange-700 group w-full lg:w-96"
|
class="disabled:opacity-95 disabled:text-white box-selection hover:btn-primary group w-full lg:w-96"
|
||||||
class:border-red-500={source.gitlabApp && !source.gitlabAppId}
|
class:border-red-500={source.gitlabApp && !source.gitlabAppId}
|
||||||
class:border-0={source.gitlabApp && !source.gitlabAppId}
|
class:border-0={source.gitlabApp && !source.gitlabAppId}
|
||||||
class:border-l-4={source.gitlabApp && !source.gitlabAppId}
|
class:border-l-4={source.gitlabApp && !source.gitlabAppId}
|
||||||
@ -244,7 +243,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-row items-center">
|
<div class="flex flex-row items-center">
|
||||||
<div class="title py-4">Public Repository</div>
|
<div class="title py-4 pr-4">Public Repository</div>
|
||||||
<DocLink url="https://docs.coollabs.io/coolify/applications/#public-repository" />
|
<DocLink url="https://docs.coollabs.io/coolify/applications/#public-repository" />
|
||||||
</div>
|
</div>
|
||||||
<PublicRepository />
|
<PublicRepository />
|
||||||
|
@ -61,7 +61,7 @@
|
|||||||
disabled={!$appSession.isAdmin}
|
disabled={!$appSession.isAdmin}
|
||||||
class:bg-red-600={$appSession.isAdmin}
|
class:bg-red-600={$appSession.isAdmin}
|
||||||
class:hover:bg-red-500={$appSession.isAdmin}
|
class:hover:bg-red-500={$appSession.isAdmin}
|
||||||
class="btn btn-sm btn-error text-sm"
|
class="btn btn-lg btn-error text-sm"
|
||||||
>
|
>
|
||||||
Force Delete Application
|
Force Delete Application
|
||||||
</button>
|
</button>
|
||||||
|
@ -459,7 +459,7 @@
|
|||||||
<form on:submit|preventDefault={() => handleSubmit()}>
|
<form on:submit|preventDefault={() => handleSubmit()}>
|
||||||
<div class="mx-auto w-full">
|
<div class="mx-auto w-full">
|
||||||
<div class="flex flex-row border-b border-coolgray-500 mb-6 space-x-2">
|
<div class="flex flex-row border-b border-coolgray-500 mb-6 space-x-2">
|
||||||
<div class="title font-bold pb-3 ">General</div>
|
<div class="title font-bold pb-3">General</div>
|
||||||
{#if $appSession.isAdmin}
|
{#if $appSession.isAdmin}
|
||||||
<button
|
<button
|
||||||
class="btn btn-sm btn-primary"
|
class="btn btn-sm btn-primary"
|
||||||
@ -644,7 +644,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{#if application.buildPack !== 'compose'}
|
{#if application.buildPack !== 'compose'}
|
||||||
<div class="title font-bold pb-3 pt-10 border-b border-coolgray-500 mb-6">
|
<div class="title font-bold pb-3 pt-10 border-b border-coolgray-500 mb-6">
|
||||||
Build & Deploy
|
Configuration
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-flow-row gap-2 px-4 pr-5">
|
<div class="grid grid-flow-row gap-2 px-4 pr-5">
|
||||||
{#if application.buildCommand || application.buildPack === 'rust' || application.buildPack === 'laravel'}
|
{#if application.buildCommand || application.buildPack === 'rust' || application.buildPack === 'laravel'}
|
||||||
@ -838,8 +838,8 @@
|
|||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
class="w-full"
|
class="w-full"
|
||||||
readonly={!isDisabled}
|
|
||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
|
readonly={!$appSession.isAdmin}
|
||||||
name="exposePort"
|
name="exposePort"
|
||||||
id="exposePort"
|
id="exposePort"
|
||||||
bind:value={application.exposePort}
|
bind:value={application.exposePort}
|
||||||
|
@ -1,10 +1,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
import { get } from '$lib/api';
|
import { get } from '$lib/api';
|
||||||
import { t } from '$lib/translations';
|
|
||||||
import { errorNotification } from '$lib/common';
|
import { errorNotification } from '$lib/common';
|
||||||
import { onMount, onDestroy } from 'svelte';
|
import { onMount, onDestroy } from 'svelte';
|
||||||
import Tooltip from '$lib/components/Tooltip.svelte';
|
|
||||||
|
|
||||||
let application: any = {};
|
let application: any = {};
|
||||||
let logsLoading = false;
|
let logsLoading = false;
|
||||||
@ -137,12 +135,7 @@
|
|||||||
{:else}
|
{:else}
|
||||||
<div class="relative w-full">
|
<div class="relative w-full">
|
||||||
<div class="flex justify-start sticky space-x-2 pb-2">
|
<div class="flex justify-start sticky space-x-2 pb-2">
|
||||||
<button
|
<button on:click={followBuild} class="btn btn-sm " class:bg-coollabs={followingLogs}>
|
||||||
on:click={followBuild}
|
|
||||||
class="btn btn-sm bg-coollabs"
|
|
||||||
class:bg-coolgray-300={followingLogs}
|
|
||||||
class:text-applications={followingLogs}
|
|
||||||
>
|
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
class="w-6 h-6 mr-2"
|
class="w-6 h-6 mr-2"
|
||||||
@ -162,8 +155,9 @@
|
|||||||
{followingLogs ? 'Following Logs...' : 'Follow Logs'}
|
{followingLogs ? 'Following Logs...' : 'Follow Logs'}
|
||||||
</button>
|
</button>
|
||||||
{#if loadLogsInterval}
|
{#if loadLogsInterval}
|
||||||
<button id="streaming" class="btn btn-sm bg-transparent border-none loading" />
|
<button id="streaming" class="btn btn-sm bg-transparent border-none loading"
|
||||||
<Tooltip triggeredBy="#streaming">Streaming logs</Tooltip>
|
>Streaming logs</button
|
||||||
|
>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
@ -216,7 +216,7 @@
|
|||||||
|
|
||||||
<div class="flex justify-end items-end space-x-2 h-10">
|
<div class="flex justify-end items-end space-x-2 h-10">
|
||||||
{#if preview.customDomain}
|
{#if preview.customDomain}
|
||||||
<a id="openpreview" href={preview.customDomain} target="_blank" class="icons">
|
<a id="openpreview" href={preview.customDomain} target="_blank noreferrer" class="icons">
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
class="h-6 w-6"
|
class="h-6 w-6"
|
||||||
|
@ -48,10 +48,10 @@
|
|||||||
.map((secret) => {
|
.map((secret) => {
|
||||||
const [name, ...rest] = secret.split('=');
|
const [name, ...rest] = secret.split('=');
|
||||||
const value = rest.join('=');
|
const value = rest.join('=');
|
||||||
const cleanValue = value?.replaceAll('"', '') || '';
|
const cleanValue = (value?.replaceAll('"', '') || '').trim();
|
||||||
return {
|
return {
|
||||||
name,
|
name: name.trim(),
|
||||||
value: cleanValue,
|
value: cleanValue.trim(),
|
||||||
createSecret: !secrets.find((secret: any) => name === secret.name)
|
createSecret: !secrets.find((secret: any) => name === secret.name)
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@ -60,6 +60,7 @@
|
|||||||
batchSecretsPairs.map(({ name, value, createSecret }) =>
|
batchSecretsPairs.map(({ name, value, createSecret }) =>
|
||||||
limit(async () => {
|
limit(async () => {
|
||||||
try {
|
try {
|
||||||
|
if (!name || !value) return;
|
||||||
if (createSecret) {
|
if (createSecret) {
|
||||||
await post(`/applications/${id}/secrets`, {
|
await post(`/applications/${id}/secrets`, {
|
||||||
name,
|
name,
|
||||||
@ -87,10 +88,6 @@
|
|||||||
);
|
);
|
||||||
batchSecrets = '';
|
batchSecrets = '';
|
||||||
await refreshSecrets();
|
await refreshSecrets();
|
||||||
addToast({
|
|
||||||
message: 'Secrets saved.',
|
|
||||||
type: 'success'
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -42,7 +42,7 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<label for="name" class="pb-2 uppercase font-bold">name</label>
|
|
||||||
{#each persistentStorages as storage}
|
{#each persistentStorages as storage}
|
||||||
{#key storage.id}
|
{#key storage.id}
|
||||||
<Storage on:refresh={refreshStorage} {storage} />
|
<Storage on:refresh={refreshStorage} {storage} />
|
||||||
|
@ -31,7 +31,7 @@
|
|||||||
|
|
||||||
<div class="flex justify-center px-6 pb-8">
|
<div class="flex justify-center px-6 pb-8">
|
||||||
<form on:submit|preventDefault={handleSubmit} class="grid grid-flow-row gap-2 py-4">
|
<form on:submit|preventDefault={handleSubmit} class="grid grid-flow-row gap-2 py-4">
|
||||||
<div class="flex items-start lg:items-center space-x-0 lg:space-x-4 pb-5 flex-col space-y-4 lg:space-y-0">
|
<div class="flex items-start lg:items-center space-x-0 lg:space-x-4 pb-5 flex-col lg:flex-row space-y-4 lg:space-y-0">
|
||||||
<div class="title font-bold">{$t('forms.configuration')}</div>
|
<div class="title font-bold">{$t('forms.configuration')}</div>
|
||||||
<button type="submit" class="btn btn-sm bg-destinations w-full lg:w-fit" class:loading disabled={loading}
|
<button type="submit" class="btn btn-sm bg-destinations w-full lg:w-fit" class:loading disabled={loading}
|
||||||
>{loading
|
>{loading
|
||||||
|
@ -184,14 +184,16 @@
|
|||||||
? 'Verify Remote Docker Engine'
|
? 'Verify Remote Docker Engine'
|
||||||
: 'Check Remote Docker Engine'}</button
|
: 'Check Remote Docker Engine'}</button
|
||||||
>
|
>
|
||||||
|
{#if destination.remoteVerified}
|
||||||
<button
|
<button
|
||||||
class="btn btn-sm"
|
class="btn btn-sm"
|
||||||
class:loading={loading.restart}
|
class:loading={loading.restart}
|
||||||
class:bg-error={!loading.restart}
|
class:bg-error={!loading.restart}
|
||||||
disabled={loading.restart}
|
disabled={loading.restart}
|
||||||
on:click|preventDefault={forceRestartProxy}>{$t('destination.force_restart_proxy')}</button
|
on:click|preventDefault={forceRestartProxy}
|
||||||
>
|
>{$t('destination.force_restart_proxy')}</button
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-2 items-center px-10 ">
|
<div class="grid grid-cols-2 items-center px-10 ">
|
||||||
|
@ -41,6 +41,7 @@
|
|||||||
import ServiceIcons from '$lib/components/svg/services/ServiceIcons.svelte';
|
import ServiceIcons from '$lib/components/svg/services/ServiceIcons.svelte';
|
||||||
import { dev } from '$app/env';
|
import { dev } from '$app/env';
|
||||||
import NewResource from './_NewResource.svelte';
|
import NewResource from './_NewResource.svelte';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
let numberOfGetStatus = 0;
|
let numberOfGetStatus = 0;
|
||||||
let status: any = {};
|
let status: any = {};
|
||||||
@ -54,8 +55,13 @@
|
|||||||
services: false,
|
services: false,
|
||||||
databases: false
|
databases: false
|
||||||
};
|
};
|
||||||
|
let searchInput: HTMLInputElement;
|
||||||
doSearch();
|
doSearch();
|
||||||
|
onMount(() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
searchInput.focus();
|
||||||
|
}, 100);
|
||||||
|
});
|
||||||
async function refreshStatusApplications() {
|
async function refreshStatusApplications() {
|
||||||
noInitialStatus.applications = false;
|
noInitialStatus.applications = false;
|
||||||
numberOfGetStatus = 0;
|
numberOfGetStatus = 0;
|
||||||
@ -171,7 +177,24 @@
|
|||||||
}
|
}
|
||||||
} else if (typeof dualCerts !== 'undefined') {
|
} else if (typeof dualCerts !== 'undefined') {
|
||||||
const response = await get(`/services/${id}/status`);
|
const response = await get(`/services/${id}/status`);
|
||||||
isRunning = response.isRunning;
|
if (Object.keys(response).length === 0) {
|
||||||
|
isRunning = false;
|
||||||
|
} else {
|
||||||
|
let overallStatus = false;
|
||||||
|
for (const oneStatus of Object.keys(response)) {
|
||||||
|
if (response[oneStatus].status.isRunning) {
|
||||||
|
overallStatus = true;
|
||||||
|
} else {
|
||||||
|
isDegraded = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (overallStatus) {
|
||||||
|
isRunning = true;
|
||||||
|
} else {
|
||||||
|
isRunning = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
const response = await get(`/databases/${id}/status`);
|
const response = await get(`/databases/${id}/status`);
|
||||||
isRunning = response.isRunning;
|
isRunning = response.isRunning;
|
||||||
@ -237,7 +260,8 @@
|
|||||||
(application.id && application.id.toLowerCase().includes($search.toLowerCase())) ||
|
(application.id && application.id.toLowerCase().includes($search.toLowerCase())) ||
|
||||||
(application.name && application.name.toLowerCase().includes($search.toLowerCase())) ||
|
(application.name && application.name.toLowerCase().includes($search.toLowerCase())) ||
|
||||||
(application.fqdn && application.fqdn.toLowerCase().includes($search.toLowerCase())) ||
|
(application.fqdn && application.fqdn.toLowerCase().includes($search.toLowerCase())) ||
|
||||||
(application.dockerComposeConfiguration && application.dockerComposeConfiguration.toLowerCase().includes($search.toLowerCase())) ||
|
(application.dockerComposeConfiguration &&
|
||||||
|
application.dockerComposeConfiguration.toLowerCase().includes($search.toLowerCase())) ||
|
||||||
(application.repository &&
|
(application.repository &&
|
||||||
application.repository.toLowerCase().includes($search.toLowerCase())) ||
|
application.repository.toLowerCase().includes($search.toLowerCase())) ||
|
||||||
(application.buildpack &&
|
(application.buildpack &&
|
||||||
@ -523,6 +547,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<div class="input-group flex w-full">
|
<div class="input-group flex w-full">
|
||||||
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
<div
|
<div
|
||||||
class="btn btn-square cursor-default no-animation hover:bg-error"
|
class="btn btn-square cursor-default no-animation hover:bg-error"
|
||||||
on:click={() => doSearch('')}
|
on:click={() => doSearch('')}
|
||||||
@ -544,6 +569,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
|
bind:this={searchInput}
|
||||||
id="search"
|
id="search"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Search: You can search for names, domains, types, database types, version, servers etc..."
|
placeholder="Search: You can search for names, domains, types, database types, version, servers etc..."
|
||||||
@ -643,7 +669,7 @@
|
|||||||
<div class="h-10 text-xs">
|
<div class="h-10 text-xs">
|
||||||
{#if application?.fqdn}
|
{#if application?.fqdn}
|
||||||
<h2>{application?.fqdn.replace('https://', '').replace('http://', '')}</h2>
|
<h2>{application?.fqdn.replace('https://', '').replace('http://', '')}</h2>
|
||||||
{:else if (!application.settings?.isBot && !application?.fqdn) && application.buildPack !== 'compose'}
|
{:else if !application.settings?.isBot && !application?.fqdn && application.buildPack !== 'compose'}
|
||||||
<h2 class="text-red-500">Not configured</h2>
|
<h2 class="text-red-500">Not configured</h2>
|
||||||
{/if}
|
{/if}
|
||||||
{#if application.destinationDocker?.name}
|
{#if application.destinationDocker?.name}
|
||||||
@ -656,7 +682,7 @@
|
|||||||
|
|
||||||
<div class="flex justify-end items-end space-x-2 h-10">
|
<div class="flex justify-end items-end space-x-2 h-10">
|
||||||
{#if application?.fqdn}
|
{#if application?.fqdn}
|
||||||
<a href={application?.fqdn} target="_blank" class="icons hover:bg-green-500">
|
<a href={application?.fqdn} target="_blank noreferrer" class="icons hover:bg-green-500">
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
class="h-6 w-6"
|
class="h-6 w-6"
|
||||||
@ -680,7 +706,7 @@
|
|||||||
href={`http://${dev ? 'localhost' : settings.ipv4}:${
|
href={`http://${dev ? 'localhost' : settings.ipv4}:${
|
||||||
application.exposePort
|
application.exposePort
|
||||||
}`}
|
}`}
|
||||||
target="_blank"
|
target="_blank noreferrer"
|
||||||
class="icons hover:bg-green-500"
|
class="icons hover:bg-green-500"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
@ -762,7 +788,7 @@
|
|||||||
|
|
||||||
<div class="flex justify-end items-end space-x-2 h-10">
|
<div class="flex justify-end items-end space-x-2 h-10">
|
||||||
{#if application?.fqdn}
|
{#if application?.fqdn}
|
||||||
<a href={application?.fqdn} target="_blank" class="icons hover:bg-green-500">
|
<a href={application?.fqdn} target="_blank noreferrer" class="icons hover:bg-green-500">
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
class="h-6 w-6"
|
class="h-6 w-6"
|
||||||
@ -784,7 +810,7 @@
|
|||||||
{#if application.settings?.isBot && application.exposePort}
|
{#if application.settings?.isBot && application.exposePort}
|
||||||
<a
|
<a
|
||||||
href={`http://${dev ? 'localhost' : settings.ipv4}:${application.exposePort}`}
|
href={`http://${dev ? 'localhost' : settings.ipv4}:${application.exposePort}`}
|
||||||
target="_blank"
|
target="_blank noreferrer"
|
||||||
class="icons hover:bg-green-500"
|
class="icons hover:bg-green-500"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
@ -871,7 +897,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex justify-end items-end space-x-2 h-10">
|
<div class="flex justify-end items-end space-x-2 h-10">
|
||||||
{#if service?.fqdn}
|
{#if service?.fqdn}
|
||||||
<a href={service?.fqdn} target="_blank" class="icons hover:bg-pink-500">
|
<a href={service?.fqdn} target="_blank noreferrer" class="icons hover:bg-pink-500">
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
class="h-6 w-6"
|
class="h-6 w-6"
|
||||||
@ -944,7 +970,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex justify-end items-end space-x-2 h-10">
|
<div class="flex justify-end items-end space-x-2 h-10">
|
||||||
{#if service?.fqdn}
|
{#if service?.fqdn}
|
||||||
<a href={service?.fqdn} target="_blank" class="icons hover:bg-pink-500">
|
<a href={service?.fqdn} target="_blank noreferrer" class="icons hover:bg-pink-500">
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
class="h-6 w-6"
|
class="h-6 w-6"
|
||||||
|
135
apps/ui/src/routes/services/[id]/_Menu.svelte
Normal file
135
apps/ui/src/routes/services/[id]/_Menu.svelte
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
export let service: any;
|
||||||
|
export let template: any;
|
||||||
|
import { page } from '$app/stores';
|
||||||
|
import ServiceLinks from './_ServiceLinks.svelte';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ul class="menu border bg-coolgray-100 border-coolgray-200 rounded p-2 space-y-2 sticky top-4">
|
||||||
|
<li class="menu-title">
|
||||||
|
<span>General</span>
|
||||||
|
</li>
|
||||||
|
<li class="rounded">
|
||||||
|
<ServiceLinks {template} {service} linkToDocs={true} />
|
||||||
|
</li>
|
||||||
|
<li class="rounded" class:bg-coollabs={$page.url.pathname === `/services/${$page.params.id}`}>
|
||||||
|
<a href={`/services/${$page.params.id}`} class="no-underline w-full"
|
||||||
|
><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="M7 10h3v-3l-3.5 -3.5a6 6 0 0 1 8 8l6 6a2 2 0 0 1 -3 3l-6 -6a6 6 0 0 1 -8 -8l3.5 3.5"
|
||||||
|
/>
|
||||||
|
</svg>Configurations</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
<li
|
||||||
|
class="rounded"
|
||||||
|
class:bg-coollabs={$page.url.pathname === `/services/${$page.params.id}/secrets`}
|
||||||
|
>
|
||||||
|
<a href={`/services/${$page.params.id}/secrets`} class="no-underline w-full"
|
||||||
|
><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="M12 3a12 12 0 0 0 8.5 3a12 12 0 0 1 -8.5 15a12 12 0 0 1 -8.5 -15a12 12 0 0 0 8.5 -3"
|
||||||
|
/>
|
||||||
|
<circle cx="12" cy="11" r="1" />
|
||||||
|
<line x1="12" y1="12" x2="12" y2="14.5" />
|
||||||
|
</svg>Secrets</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
<li
|
||||||
|
class="rounded"
|
||||||
|
class:bg-coollabs={$page.url.pathname === `/services/${$page.params.id}/storages`}
|
||||||
|
>
|
||||||
|
<a href={`/services/${$page.params.id}/storages`} class="no-underline w-full"
|
||||||
|
><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" />
|
||||||
|
<ellipse cx="12" cy="6" rx="8" ry="3" />
|
||||||
|
<path d="M4 6v6a8 3 0 0 0 16 0v-6" />
|
||||||
|
<path d="M4 12v6a8 3 0 0 0 16 0v-6" />
|
||||||
|
</svg>Persistent Volumes</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
<li class="menu-title">
|
||||||
|
<span>Logs</span>
|
||||||
|
</li>
|
||||||
|
<li
|
||||||
|
class="rounded"
|
||||||
|
class:bg-coollabs={$page.url.pathname === `/services/${$page.params.id}/logs`}
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href={`/services/${$page.params.id}/logs`}
|
||||||
|
class="no-underline w-full"
|
||||||
|
><svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
class="h-6 w-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="M3 19a9 9 0 0 1 9 0a9 9 0 0 1 9 0" />
|
||||||
|
<path d="M3 6a9 9 0 0 1 9 0a9 9 0 0 1 9 0" />
|
||||||
|
<line x1="3" y1="6" x2="3" y2="19" />
|
||||||
|
<line x1="12" y1="6" x2="12" y2="19" />
|
||||||
|
<line x1="21" y1="6" x2="21" y2="19" />
|
||||||
|
</svg>Service</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
<li class="menu-title">
|
||||||
|
<span>Advanced</span>
|
||||||
|
</li>
|
||||||
|
<li
|
||||||
|
class="rounded"
|
||||||
|
class:bg-coollabs={$page.url.pathname === `/services/${$page.params.id}/danger`}
|
||||||
|
>
|
||||||
|
<a href={`/services/${$page.params.id}/danger`} class="no-underline w-full"
|
||||||
|
><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="M12 9v2m0 4v.01" />
|
||||||
|
<path
|
||||||
|
d="M5 19h14a2 2 0 0 0 1.84 -2.75l-7.1 -12.25a2 2 0 0 0 -3.5 0l-7.1 12.25a2 2 0 0 0 1.75 2.75"
|
||||||
|
/>
|
||||||
|
</svg>Danger Zone</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
</ul>
|
@ -1,6 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
export let name = '';
|
export let name = '';
|
||||||
export let value = '';
|
export let value = '';
|
||||||
|
export let readonly = false;
|
||||||
export let isNewSecret = false;
|
export let isNewSecret = false;
|
||||||
|
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
@ -50,24 +51,27 @@
|
|||||||
|
|
||||||
<td>
|
<td>
|
||||||
<input
|
<input
|
||||||
|
style="min-width: 350px !important;"
|
||||||
style="min-width: 350px !important;"
|
|
||||||
id={isNewSecret ? 'secretName' : 'secretNameNew'}
|
id={isNewSecret ? 'secretName' : 'secretNameNew'}
|
||||||
bind:value={name}
|
bind:value={name}
|
||||||
required
|
required
|
||||||
placeholder="EXAMPLE_VARIABLE"
|
placeholder="EXAMPLE_VARIABLE"
|
||||||
readonly={!isNewSecret}
|
readonly={!isNewSecret || readonly}
|
||||||
class:bg-transparent={!isNewSecret}
|
class="w-full"
|
||||||
class:cursor-not-allowed={!isNewSecret}
|
class:bg-coolblack={!isNewSecret}
|
||||||
|
class:border={!isNewSecret}
|
||||||
|
class:border-dashed={!isNewSecret}
|
||||||
|
class:border-coolgray-300={!isNewSecret}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<CopyPasswordField
|
<CopyPasswordField
|
||||||
id={isNewSecret ? 'secretValue' : 'secretValueNew'}
|
id={isNewSecret ? 'secretValue' : 'secretValueNew'}
|
||||||
name={isNewSecret ? 'secretValue' : 'secretValueNew'}
|
name={isNewSecret ? 'secretValue' : 'secretValueNew'}
|
||||||
|
disabled={readonly}
|
||||||
|
{readonly}
|
||||||
isPasswordField={true}
|
isPasswordField={true}
|
||||||
bind:value
|
bind:value
|
||||||
required
|
|
||||||
placeholder="J$#@UIO%HO#$U%H"
|
placeholder="J$#@UIO%HO#$U%H"
|
||||||
inputStyle="min-width: 350px; !important"
|
inputStyle="min-width: 350px; !important"
|
||||||
/>
|
/>
|
||||||
@ -76,12 +80,12 @@
|
|||||||
<td>
|
<td>
|
||||||
{#if isNewSecret}
|
{#if isNewSecret}
|
||||||
<div class="flex items-center justify-center">
|
<div class="flex items-center justify-center">
|
||||||
<button class="btn btn-sm bg-services" on:click={() => saveSecret(true)}>Add</button>
|
<button class="btn btn-sm btn-primary" on:click={() => saveSecret(true)}>Add</button>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else if !readonly}
|
||||||
<div class="flex flex-row justify-center space-x-2">
|
<div class="flex flex-row justify-center space-x-2">
|
||||||
<div class="flex items-center justify-center">
|
<div class="flex items-center justify-center">
|
||||||
<button class="btn btn-sm bg-services" on:click={() => saveSecret(false)}>Set</button>
|
<button class="btn btn-sm btn-primary" on:click={() => saveSecret(false)}>Set</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-center items-end">
|
<div class="flex justify-center items-end">
|
||||||
<button class="btn btn-sm bg-error" on:click={removeSecret}>Remove</button>
|
<button class="btn btn-sm bg-error" on:click={removeSecret}>Remove</button>
|
||||||
|
@ -1,86 +1,14 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import DocLink from '$lib/components/DocLink.svelte';
|
||||||
|
import ServiceIcons from '$lib/components/svg/services/ServiceIcons.svelte';
|
||||||
export let service: any;
|
export let service: any;
|
||||||
import * as Icons from '$lib/components/svg/services';
|
export let template: any;
|
||||||
|
export let linkToDocs: boolean = false;
|
||||||
|
const name: any = service.type && service.type[0].toUpperCase() + service.type.substring(1);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if service.type === 'plausibleanalytics'}
|
{#if linkToDocs}
|
||||||
<a href="https://plausible.io" target="_blank">
|
<DocLink url={template[service.id].documentation} text={`Documentation`} isExternal={true} />
|
||||||
<Icons.PlausibleAnalytics />
|
{:else}
|
||||||
</a>
|
<ServiceIcons type={service.type} />
|
||||||
{:else if service.type === 'nocodb'}
|
|
||||||
<a href="https://nocodb.com" target="_blank">
|
|
||||||
<Icons.NocoDb />
|
|
||||||
</a>
|
|
||||||
{:else if service.type === 'minio'}
|
|
||||||
<a href="https://min.io" target="_blank">
|
|
||||||
<Icons.MinIo />
|
|
||||||
</a>
|
|
||||||
{:else if service.type === 'vscodeserver'}
|
|
||||||
<a href="https://coder.com" target="_blank">
|
|
||||||
<Icons.VsCodeServer />
|
|
||||||
</a>
|
|
||||||
{:else if service.type === 'wordpress'}
|
|
||||||
<a href="https://wordpress.org" target="_blank">
|
|
||||||
<Icons.Wordpress />
|
|
||||||
</a>
|
|
||||||
{:else if service.type === 'vaultwarden'}
|
|
||||||
<a href="https://github.com/dani-garcia/vaultwarden" target="_blank">
|
|
||||||
<Icons.VaultWarden />
|
|
||||||
</a>
|
|
||||||
{:else if service.type === 'languagetool'}
|
|
||||||
<a href="https://languagetool.org/dev" target="_blank">
|
|
||||||
<Icons.LanguageTool />
|
|
||||||
</a>
|
|
||||||
{:else if service.type === 'n8n'}
|
|
||||||
<a href="https://n8n.io" target="_blank">
|
|
||||||
<Icons.N8n />
|
|
||||||
</a>
|
|
||||||
{:else if service.type === 'uptimekuma'}
|
|
||||||
<a href="https://github.com/louislam/uptime-kuma" target="_blank">
|
|
||||||
<Icons.UptimeKuma />
|
|
||||||
</a>
|
|
||||||
{:else if service.type === 'ghost'}
|
|
||||||
<a href="https://ghost.org" target="_blank">
|
|
||||||
<Icons.Ghost />
|
|
||||||
</a>
|
|
||||||
{:else if service.type === 'umami'}
|
|
||||||
<a href="https://umami.is" target="_blank">
|
|
||||||
<Icons.Umami />
|
|
||||||
</a>
|
|
||||||
{:else if service.type === 'hasura'}
|
|
||||||
<a href="https://hasura.io" target="_blank">
|
|
||||||
<Icons.Hasura />
|
|
||||||
</a>
|
|
||||||
{:else if service.type === 'fider'}
|
|
||||||
<a href="https://fider.io" target="_blank">
|
|
||||||
<Icons.Fider />
|
|
||||||
</a>
|
|
||||||
{:else if service.type === 'appwrite'}
|
|
||||||
<a href="https://appwrite.io" target="_blank">
|
|
||||||
<Icons.Appwrite />
|
|
||||||
</a>
|
|
||||||
{:else if service.type === 'moodle'}
|
|
||||||
<a href="https://moodle.org" target="_blank">
|
|
||||||
<Icons.Moodle />
|
|
||||||
</a>
|
|
||||||
{:else if service.type === 'glitchTip'}
|
|
||||||
<a href="https://glitchtip.com" target="_blank">
|
|
||||||
<Icons.GlitchTip />
|
|
||||||
</a>
|
|
||||||
{:else if service.type === 'searxng'}
|
|
||||||
<a href="https://searxng.org" target="_blank">
|
|
||||||
<Icons.Searxng />
|
|
||||||
</a>
|
|
||||||
{:else if service.type === 'weblate'}
|
|
||||||
<a href="https://weblate.org" target="_blank">
|
|
||||||
<Icons.Weblate />
|
|
||||||
</a>
|
|
||||||
{:else if service.type === 'grafana'}
|
|
||||||
<a href="https://github.com/grafana/grafana" target="_blank">
|
|
||||||
<Icons.Grafana />
|
|
||||||
</a>
|
|
||||||
{:else if service.type === 'trilium'}
|
|
||||||
<a href="https://github.com/zadam/trilium" target="_blank">
|
|
||||||
<Icons.Trilium />
|
|
||||||
</a>
|
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -1,93 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
|
|
||||||
import { t } from '$lib/translations';
|
|
||||||
import Select from 'svelte-select';
|
|
||||||
export let service: any;
|
|
||||||
export let readOnly: any;
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="flex space-x-1 py-5 font-bold">
|
|
||||||
<div class="title">Appwrite</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="space-y-2">
|
|
||||||
<div class="grid grid-cols-2 items-center lg:px-10 px-2">
|
|
||||||
<label for="opensslKeyV1">Encryption Key</label>
|
|
||||||
<CopyPasswordField
|
|
||||||
name="opensslKeyV1"
|
|
||||||
id="opensslKeyV1"
|
|
||||||
isPasswordField
|
|
||||||
value={service.appwrite.opensslKeyV1}
|
|
||||||
readonly
|
|
||||||
disabled
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="grid grid-cols-2 items-center lg:px-10 px-2">
|
|
||||||
<label for="executorSecret">Executor Secret</label>
|
|
||||||
<CopyPasswordField
|
|
||||||
name="executorSecret"
|
|
||||||
id="executorSecret"
|
|
||||||
isPasswordField
|
|
||||||
value={service.appwrite.executorSecret}
|
|
||||||
readonly
|
|
||||||
disabled
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex space-x-1 py-5 font-bold">
|
|
||||||
<div class="title">MariaDB</div>
|
|
||||||
</div>
|
|
||||||
<div class="space-y-2">
|
|
||||||
<div class="grid grid-cols-2 items-center lg:px-10 px-2">
|
|
||||||
<label for="mariadbUser">{$t('forms.username')}</label>
|
|
||||||
<CopyPasswordField
|
|
||||||
name="mariadbUser"
|
|
||||||
id="mariadbUser"
|
|
||||||
value={service.appwrite.mariadbUser}
|
|
||||||
readonly
|
|
||||||
disabled
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="grid grid-cols-2 items-center lg:px-10 px-2 ">
|
|
||||||
<label for="mariadbPassword">{$t('forms.password')}</label>
|
|
||||||
<CopyPasswordField
|
|
||||||
id="mariadbPassword"
|
|
||||||
isPasswordField
|
|
||||||
readonly
|
|
||||||
disabled
|
|
||||||
name="mariadbPassword"
|
|
||||||
value={service.appwrite.mariadbPassword}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="grid grid-cols-2 items-center lg:px-10 px-2">
|
|
||||||
<label for="mariadbRootUser">Root User</label>
|
|
||||||
<CopyPasswordField
|
|
||||||
name="mariadbRootUser"
|
|
||||||
id="mariadbRootUser"
|
|
||||||
value={service.appwrite.mariadbRootUser}
|
|
||||||
readonly
|
|
||||||
disabled
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="grid grid-cols-2 items-center lg:px-10 px-2 ">
|
|
||||||
<label for="mariadbRootUserPassword">Root Password</label>
|
|
||||||
<CopyPasswordField
|
|
||||||
id="mariadbRootUserPassword"
|
|
||||||
isPasswordField
|
|
||||||
readonly
|
|
||||||
disabled
|
|
||||||
name="mariadbRootUserPassword"
|
|
||||||
value={service.appwrite.mariadbRootUserPassword}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="grid grid-cols-2 items-center lg:px-10 px-2">
|
|
||||||
<label for="mariadbDatabase">{$t('index.database')}</label>
|
|
||||||
<CopyPasswordField
|
|
||||||
name="mariadbDatabase"
|
|
||||||
id="mariadbDatabase"
|
|
||||||
value={service.appwrite.mariadbDatabase}
|
|
||||||
readonly
|
|
||||||
disabled
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
@ -1,194 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
|
|
||||||
import { t } from '$lib/translations';
|
|
||||||
import Select from 'svelte-select';
|
|
||||||
export let service: any;
|
|
||||||
export let readOnly: any;
|
|
||||||
|
|
||||||
let mailgunRegions = [
|
|
||||||
{
|
|
||||||
value: 'EU',
|
|
||||||
label: 'EU'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'US',
|
|
||||||
label: 'US'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="flex space-x-1 py-5 font-bold">
|
|
||||||
<div class="title">Fider</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="space-y-2">
|
|
||||||
<div class="grid grid-cols-2 items-center lg:px-10 px-2">
|
|
||||||
<label for="jwtSecret">JWT Secret</label>
|
|
||||||
<CopyPasswordField
|
|
||||||
name="jwtSecret"
|
|
||||||
id="jwtSecret"
|
|
||||||
isPasswordField
|
|
||||||
value={service.fider.jwtSecret}
|
|
||||||
readonly
|
|
||||||
disabled
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="grid grid-cols-2 items-center lg:px-10 px-2">
|
|
||||||
<label for="emailNoreply">Noreply Email</label>
|
|
||||||
<input
|
|
||||||
class="w-full"
|
|
||||||
name="emailNoreply"
|
|
||||||
id="emailNoreply"
|
|
||||||
type="email"
|
|
||||||
required
|
|
||||||
readonly={readOnly}
|
|
||||||
disabled={readOnly}
|
|
||||||
bind:value={service.fider.emailNoreply}
|
|
||||||
placeholder="{$t('forms.eg')}: noreply@yourdomain.com"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex space-x-1 py-5 font-bold">
|
|
||||||
<div class="title">Email</div>
|
|
||||||
</div>
|
|
||||||
<div class="space-y-2">
|
|
||||||
<div class="grid grid-cols-2 items-center lg:px-10 px-2">
|
|
||||||
<label for="emailMailgunApiKey">Mailgun API Key</label>
|
|
||||||
<CopyPasswordField
|
|
||||||
name="emailMailgunApiKey"
|
|
||||||
id="emailMailgunApiKey"
|
|
||||||
isPasswordField
|
|
||||||
bind:value={service.fider.emailMailgunApiKey}
|
|
||||||
readonly={readOnly}
|
|
||||||
disabled={readOnly}
|
|
||||||
placeholder="{$t('forms.eg')}: key-yourkeygoeshere"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="grid grid-cols-2 items-center lg:px-10 px-2">
|
|
||||||
<label for="emailMailgunDomain">Mailgun Domain</label>
|
|
||||||
<input
|
|
||||||
class="w-full"
|
|
||||||
name="emailMailgunDomain"
|
|
||||||
id="emailMailgunDomain"
|
|
||||||
readonly={readOnly}
|
|
||||||
disabled={readOnly}
|
|
||||||
bind:value={service.fider.emailMailgunDomain}
|
|
||||||
placeholder="{$t('forms.eg')}: yourdomain.com"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="grid grid-cols-2 items-center lg:px-10 px-2">
|
|
||||||
<label for="emailMailgunRegion">Mailgun Region</label>
|
|
||||||
<div class="custom-select-wrapper">
|
|
||||||
<Select
|
|
||||||
id="baseBuildImages"
|
|
||||||
items={mailgunRegions}
|
|
||||||
showIndicator
|
|
||||||
on:select={(event) => (service.fider.emailMailgunRegion = event.detail.value)}
|
|
||||||
value={service.fider.emailMailgunRegion || 'EU'}
|
|
||||||
isClearable={false}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex space-x-1 py-5 lg:px-10 px-2 font-bold">
|
|
||||||
<div class="text-lg">Or</div>
|
|
||||||
</div>
|
|
||||||
<div class="grid grid-cols-2 items-center lg:px-10 px-2">
|
|
||||||
<label for="emailSmtpHost">SMTP Host</label>
|
|
||||||
<input
|
|
||||||
class="w-full"
|
|
||||||
name="emailSmtpHost"
|
|
||||||
id="emailSmtpHost"
|
|
||||||
readonly={readOnly}
|
|
||||||
disabled={readOnly}
|
|
||||||
bind:value={service.fider.emailSmtpHost}
|
|
||||||
placeholder="{$t('forms.eg')}: smtp.yourdomain.com"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="grid grid-cols-2 items-center lg:px-10 px-2">
|
|
||||||
<label for="emailSmtpPort">SMTP Port</label>
|
|
||||||
<input
|
|
||||||
class="w-full"
|
|
||||||
name="emailSmtpPort"
|
|
||||||
id="emailSmtpPort"
|
|
||||||
readonly={readOnly}
|
|
||||||
disabled={readOnly}
|
|
||||||
bind:value={service.fider.emailSmtpPort}
|
|
||||||
placeholder="{$t('forms.eg')}: 587"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="grid grid-cols-2 items-center lg:px-10 px-2">
|
|
||||||
<label for="emailSmtpUser">SMTP User</label>
|
|
||||||
<input
|
|
||||||
class="w-full"
|
|
||||||
name="emailSmtpUser"
|
|
||||||
id="emailSmtpUser"
|
|
||||||
readonly={readOnly}
|
|
||||||
disabled={readOnly}
|
|
||||||
bind:value={service.fider.emailSmtpUser}
|
|
||||||
placeholder="{$t('forms.eg')}: user@yourdomain.com"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="grid grid-cols-2 items-center lg:px-10 px-2">
|
|
||||||
<label for="emailSmtpPassword">SMTP Password</label>
|
|
||||||
<CopyPasswordField
|
|
||||||
name="emailSmtpPassword"
|
|
||||||
id="emailSmtpPassword"
|
|
||||||
isPasswordField
|
|
||||||
bind:value={service.fider.emailSmtpPassword}
|
|
||||||
readonly={readOnly}
|
|
||||||
disabled={readOnly}
|
|
||||||
placeholder="{$t('forms.eg')}: s0m3p4ssw0rd"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="grid grid-cols-2 items-center lg:px-10 px-2">
|
|
||||||
<label for="emailSmtpEnableStartTls">SMTP Start TLS</label>
|
|
||||||
<input
|
|
||||||
class="w-full"
|
|
||||||
name="emailSmtpEnableStartTls"
|
|
||||||
id="emailSmtpEnableStartTls"
|
|
||||||
readonly={readOnly}
|
|
||||||
disabled={readOnly}
|
|
||||||
bind:value={service.fider.emailSmtpEnableStartTls}
|
|
||||||
placeholder="{$t('forms.eg')}: true"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex space-x-1 py-5 font-bold">
|
|
||||||
<div class="title">PostgreSQL</div>
|
|
||||||
</div>
|
|
||||||
<div class="space-y-2">
|
|
||||||
<div class="grid grid-cols-2 items-center lg:px-10 px-2">
|
|
||||||
<label for="postgresqlUser">{$t('forms.username')}</label>
|
|
||||||
<CopyPasswordField
|
|
||||||
name="postgresqlUser"
|
|
||||||
id="postgresqlUser"
|
|
||||||
value={service.fider.postgresqlUser}
|
|
||||||
readonly
|
|
||||||
disabled
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="grid grid-cols-2 items-center lg:px-10 px-2">
|
|
||||||
<label for="postgresqlPassword">{$t('forms.password')}</label>
|
|
||||||
<CopyPasswordField
|
|
||||||
id="postgresqlPassword"
|
|
||||||
isPasswordField
|
|
||||||
readonly
|
|
||||||
disabled
|
|
||||||
name="postgresqlPassword"
|
|
||||||
value={service.fider.postgresqlPassword}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="grid grid-cols-2 items-center lg:px-10 px-2">
|
|
||||||
<label for="postgresqlDatabase">{$t('index.database')}</label>
|
|
||||||
<CopyPasswordField
|
|
||||||
name="postgresqlDatabase"
|
|
||||||
id="postgresqlDatabase"
|
|
||||||
value={service.fider.postgresqlDatabase}
|
|
||||||
readonly
|
|
||||||
disabled
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
@ -1,99 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
|
|
||||||
import Explainer from '$lib/components/Explainer.svelte';
|
|
||||||
import { t } from '$lib/translations';
|
|
||||||
export let readOnly: any;
|
|
||||||
export let service: any;
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="flex space-x-1 py-5">
|
|
||||||
<div class="title">
|
|
||||||
Ghost <Explainer explanation="You can change these values in the <span class='text-settings'>Ghost admin panel<span>." />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="space-y-2">
|
|
||||||
<div class="grid grid-cols-2 items-center lg:px-10 px-2">
|
|
||||||
<label for="email">{$t('forms.default_email_address')}</label>
|
|
||||||
<input
|
|
||||||
class="w-full"
|
|
||||||
name="email"
|
|
||||||
id="email"
|
|
||||||
disabled
|
|
||||||
readonly
|
|
||||||
placeholder={$t('forms.email')}
|
|
||||||
value={service.ghost.defaultEmail}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="grid grid-cols-2 items-center lg:px-10 px-2">
|
|
||||||
<label for="defaultPassword">{$t('forms.default_password')}</label>
|
|
||||||
<CopyPasswordField
|
|
||||||
id="defaultPassword"
|
|
||||||
isPasswordField
|
|
||||||
readonly
|
|
||||||
disabled
|
|
||||||
name="defaultPassword"
|
|
||||||
value={service.ghost.defaultPassword}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex space-x-1 py-5 font-bold">
|
|
||||||
<div class="title">MariaDB</div>
|
|
||||||
</div>
|
|
||||||
<div class="space-y-2">
|
|
||||||
<div class="grid grid-cols-2 items-center lg:px-10 px-2">
|
|
||||||
<label for="mariadbUser">{$t('forms.username')}</label>
|
|
||||||
<CopyPasswordField
|
|
||||||
name="mariadbUser"
|
|
||||||
id="mariadbUser"
|
|
||||||
value={service.ghost.mariadbUser}
|
|
||||||
readonly
|
|
||||||
disabled
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="grid grid-cols-2 items-center lg:px-10 px-2">
|
|
||||||
<label for="mariadbPassword">{$t('forms.password')}</label>
|
|
||||||
<CopyPasswordField
|
|
||||||
id="mariadbPassword"
|
|
||||||
isPasswordField
|
|
||||||
readonly
|
|
||||||
disabled
|
|
||||||
name="mariadbPassword"
|
|
||||||
value={service.ghost.mariadbPassword}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="grid grid-cols-2 items-center lg:px-10 px-2">
|
|
||||||
<label for="mariadbDatabase">{$t('index.database')}</label>
|
|
||||||
<input
|
|
||||||
class="w-full"
|
|
||||||
name="mariadbDatabase"
|
|
||||||
id="mariadbDatabase"
|
|
||||||
required
|
|
||||||
readonly={readOnly}
|
|
||||||
disabled={readOnly}
|
|
||||||
bind:value={service.ghost.mariadbDatabase}
|
|
||||||
placeholder="{$t('forms.eg')}: ghost_db"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="grid grid-cols-2 items-center lg:px-10 px-2">
|
|
||||||
<label for="mariadbRootUser">{$t('forms.root_db_user')}</label>
|
|
||||||
<CopyPasswordField
|
|
||||||
id="mariadbRootUser"
|
|
||||||
readonly
|
|
||||||
disabled
|
|
||||||
name="mariadbRootUser"
|
|
||||||
value={service.ghost.mariadbRootUser}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="grid grid-cols-2 items-center lg:px-10 px-2">
|
|
||||||
<label for="mariadbRootUserPassword">{$t('forms.root_db_password')}</label>
|
|
||||||
<CopyPasswordField
|
|
||||||
id="mariadbRootUserPassword"
|
|
||||||
isPasswordField
|
|
||||||
readonly
|
|
||||||
disabled
|
|
||||||
name="mariadbRootUserPassword"
|
|
||||||
value={service.ghost.mariadbRootUserPassword}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user