Merge branch 'main' into ui
This commit is contained in:
commit
f957008c1c
1
.gitignore
vendored
1
.gitignore
vendored
@ -13,3 +13,4 @@ apps/api/db/*.db
|
||||
local-serve
|
||||
apps/api/db/migration.db-journal
|
||||
apps/api/core*
|
||||
logs
|
256
CONTRIBUTING.md
256
CONTRIBUTING.md
@ -1,256 +0,0 @@
|
||||
# 👋 Welcome
|
||||
|
||||
First of all, thank you for considering contributing to my project! It means a lot 💜.
|
||||
|
||||
|
||||
## 🙋 Want to help?
|
||||
|
||||
If you begin in GitHub contribution, you can find the [first contribution](https://github.com/firstcontributions/first-contributions) and follow this guide.
|
||||
|
||||
Follow the [introduction](#introduction) to get started then start contributing!
|
||||
|
||||
This is a little list of what you can do to help the project:
|
||||
|
||||
- [🧑💻 Develop your own ideas](#developer-contribution)
|
||||
- [🌐 Translate the project](#translation)
|
||||
|
||||
## 👋 Introduction
|
||||
|
||||
### Setup with Github codespaces
|
||||
|
||||
If you have github codespaces enabled then you can just create a codespace and run `pnpm dev` to run your the dev environment. All the required dependencies and packages has been configured for you already.
|
||||
|
||||
### Setup with Gitpod
|
||||
|
||||
If you have a [Gitpod](https://gitpod.io), you can just create a workspace from this repository, run `pnpm install && pnpm db:push && pnpm db:seed` and then `pnpm dev`. All the required dependencies and packages has been configured for you already.
|
||||
|
||||
### Setup locally in your machine
|
||||
|
||||
> 🔴 At the moment, Coolify **doesn't support Windows**. You must use Linux or MacOS. Consider using Gitpod or Github Codespaces.
|
||||
|
||||
#### Recommended Pull Request Guideline
|
||||
|
||||
- Fork the project
|
||||
- Clone your fork repo to local
|
||||
- Create a new branch
|
||||
- Push to your fork repo
|
||||
- Create a pull request: https://github.com/coollabsio/coolify/compare
|
||||
- Write a proper description
|
||||
- Open the pull request to review against `next` branch
|
||||
|
||||
---
|
||||
|
||||
# 🧑💻 Developer contribution
|
||||
## Technical skills required
|
||||
|
||||
- **Languages**: Node.js / Javascript / Typescript
|
||||
- **Framework JS/TS**: [SvelteKit](https://kit.svelte.dev/) & [Fastify](https://www.fastify.io/)
|
||||
- **Database ORM**: [Prisma.io](https://www.prisma.io/)
|
||||
- **Docker Engine API**
|
||||
|
||||
---
|
||||
|
||||
## How to start after you set up your local fork?
|
||||
|
||||
### Prerequisites
|
||||
1. Due to the lock file, this repository is best with [pnpm](https://pnpm.io). I recommend you try and use `pnpm` because it is cool and efficient!
|
||||
|
||||
2. You need to have [Docker Engine](https://docs.docker.com/engine/install/) installed locally.
|
||||
3. You need to have [Docker Compose Plugin](https://docs.docker.com/compose/install/compose-plugin/) installed locally.
|
||||
4. You need to have [GIT LFS Support](https://git-lfs.github.com/) installed locally.
|
||||
|
||||
Optional:
|
||||
|
||||
4. To test Heroku buildpacks, you need [pack](https://github.com/buildpacks/pack) binary installed locally.
|
||||
|
||||
### Steps for local setup
|
||||
|
||||
1. Copy `apps/api/.env.template` to `apps/api/.env.template` and set the `COOLIFY_APP_ID` environment variable to something cool.
|
||||
2. Install dependencies with `pnpm install`.
|
||||
3. Need to create a local SQlite database with `pnpm db:push`.
|
||||
|
||||
This will apply all migrations at `db/dev.db`.
|
||||
|
||||
4. Seed the database with base entities with `pnpm db:seed`
|
||||
5. You can start coding after starting `pnpm dev`.
|
||||
|
||||
---
|
||||
|
||||
## Database migrations
|
||||
|
||||
During development, if you change the database layout, you need to run `pnpm db:push` to migrate the database and create types for Prisma. You also need to restart the development process.
|
||||
|
||||
If the schema is finalized, you need to create a migration file with `pnpm db:migrate <nameOfMigration>` where `nameOfMigration` is given by you. Make it sense. :)
|
||||
|
||||
---
|
||||
|
||||
## How to add new services
|
||||
|
||||
You can add any open-source and self-hostable software (service/application) to Coolify if the following statements are true:
|
||||
|
||||
- Self-hostable (obviously)
|
||||
- Open-source
|
||||
- Maintained (I do not want to add software full of bugs)
|
||||
|
||||
## Backend
|
||||
|
||||
There are 5 steps you should make on the backend side.
|
||||
|
||||
1. Create Prisma / database schema for the new service.
|
||||
2. Add supported versions of the service.
|
||||
3. Update global functions.
|
||||
4. Create API endpoints.
|
||||
5. Define automatically generated variables.
|
||||
|
||||
> I will use [Umami](https://umami.is/) as an example service.
|
||||
|
||||
### Create Prisma / Database schema for the new service.
|
||||
|
||||
You only need to do this if you store passwords or any persistent configuration. Mostly it is required by all services, but there are some exceptions, like NocoDB.
|
||||
|
||||
Update Prisma schema in [prisma/schema.prisma](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.
|
||||
|
||||
If you are finished with the Prisma schema, you should update the database schema with `pnpm db:push` command.
|
||||
|
||||
> You must restart the running development environment to be able to use the new model
|
||||
|
||||
> If you use VSCode/TLS, you probably need to restart the `Typescript Language Server` to get the new types loaded in the running environment.
|
||||
|
||||
### Add supported versions
|
||||
|
||||
Supported versions are hardcoded into Coolify (for now).
|
||||
|
||||
You need to update `supportedServiceTypesAndVersions` function at [apps/api/src/lib/services/supportedVersions.ts](apps/api/src/lib/services/supportedVersions.ts). Example JSON:
|
||||
|
||||
```js
|
||||
{
|
||||
// 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
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Add required functions/properties
|
||||
|
||||
1. Add the new service to the `includeServices` variable in [apps/api/src/lib/services/common.ts](apps/api/src/lib/services/common.ts), so it will be included in all places in the database queries where it is required.
|
||||
|
||||
```js
|
||||
const include: any = {
|
||||
destinationDocker: true,
|
||||
persistentStorage: true,
|
||||
serviceSecret: true,
|
||||
minio: true,
|
||||
plausibleAnalytics: true,
|
||||
vscodeserver: true,
|
||||
wordpress: true,
|
||||
ghost: true,
|
||||
meiliSearch: true,
|
||||
umami: true // This line!
|
||||
};
|
||||
```
|
||||
|
||||
2. Update the database update query with the new service type to `configureServiceType` function in [apps/api/src/lib/services/common.ts](apps/api/src/lib/services/common.ts). This function defines the automatically generated variables (passwords, users, etc.) and it's encryption process (if applicable).
|
||||
|
||||
```js
|
||||
[...]
|
||||
else if (type === 'umami') {
|
||||
const postgresqlUser = cuid();
|
||||
const postgresqlPassword = encrypt(generatePassword());
|
||||
const postgresqlDatabase = 'umami';
|
||||
const hashSalt = encrypt(generatePassword(64));
|
||||
await prisma.service.update({
|
||||
where: { id },
|
||||
data: {
|
||||
type,
|
||||
umami: {
|
||||
create: {
|
||||
postgresqlDatabase,
|
||||
postgresqlPassword,
|
||||
postgresqlUser,
|
||||
hashSalt,
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
3. Add field details to [apps/api/src/lib/services/serviceFields.ts](apps/api/src/lib/services/serviceFields.ts), so every component will know what to do with the values (decrypt/show it by default/readonly)
|
||||
|
||||
```js
|
||||
export const umami = [{
|
||||
name: 'postgresqlUser',
|
||||
isEditable: false,
|
||||
isLowerCase: false,
|
||||
isNumber: false,
|
||||
isBoolean: false,
|
||||
isEncrypted: false
|
||||
}]
|
||||
```
|
||||
|
||||
4. Add service deletion query to `removeService` function in [apps/api/src/lib/services/common.ts](apps/api/src/lib/services/common.ts)
|
||||
|
||||
5. Add start process for the new service in [apps/api/src/lib/services/handlers.ts](apps/api/src/lib/services/handlers.ts)
|
||||
|
||||
> See startUmamiService() function as example.
|
||||
|
||||
6. Add the newly added start process to `startService` in [apps/api/src/routes/api/v1/services/handlers.ts](apps/api/src/routes/api/v1/services/handlers.ts)
|
||||
|
||||
7. You need to add a custom logo at [apps/ui/src/lib/components/svg/services](apps/ui/src/lib/components/svg/services) as a svelte component and export it in [apps/ui/src/lib/components/svg/services/index.ts](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.
|
||||
|
||||
8. You need to include it the logo at:
|
||||
|
||||
- [apps/ui/src/lib/components/svg/services/ServiceIcons.svelte](apps/ui/src/lib/components/svg/services/ServiceIcons.svelte) with `isAbsolute`.
|
||||
- [apps/ui/src/routes/services/[id]/_ServiceLinks.svelte](apps/ui/src/routes/services/[id]/_ServiceLinks.svelte) with the link to the docs/main site of the service
|
||||
|
||||
9. By default the URL and the name frontend forms are included in [apps/ui/src/routes/services/[id]/_Services/_Services.svelte](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 to [apps/ui/src/routes/services/[id]/_Services](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! 👏
|
||||
|
||||
<!-- # 🌐 Translate the project
|
||||
|
||||
The project use [sveltekit-i18n](https://github.com/sveltekit-i18n/lib) to translate the project.
|
||||
It follows the [ISO 639-1](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) to name languages.
|
||||
|
||||
### Installation
|
||||
|
||||
You must have gone throw all the [intro](#introduction) steps before you can start translating.
|
||||
|
||||
It's only an advice, but I recommend you to use:
|
||||
|
||||
- Visual Studio Code
|
||||
- [i18n Ally for Visual Studio Code](https://marketplace.visualstudio.com/items?itemName=Lokalise.i18n-ally): ideal to see the progress of the translation.
|
||||
- [Svelte for VS Code](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode): to get the syntax color for the project
|
||||
|
||||
### Adding a language
|
||||
|
||||
If your language doesn't appear in the [locales folder list](src/lib/locales/), follow the step below:
|
||||
|
||||
1. In `src/lib/locales/`, Copy paste `en.json` and rename it with your language (eg: `cz.json`).
|
||||
2. In the [lang.json](src/lib/lang.json) file, add a line after the first bracket (`{`) with `"ISO of your language": "Language",` (eg: `"cz": "Czech",`).
|
||||
3. Have fun translating! -->
|
@ -1,45 +1,3 @@
|
||||
---
|
||||
head:
|
||||
- - meta
|
||||
- name: description
|
||||
content: Coolify - Databases
|
||||
- - meta
|
||||
- name: keywords
|
||||
content: databases coollabs coolify
|
||||
- - meta
|
||||
- name: twitter:card
|
||||
content: summary_large_image
|
||||
- - meta
|
||||
- name: twitter:site
|
||||
content: '@andrasbacsai'
|
||||
- - meta
|
||||
- name: twitter:title
|
||||
content: Coolify
|
||||
- - meta
|
||||
- name: twitter:description
|
||||
content: An open-source & self-hostable Heroku / Netlify alternative.
|
||||
- - meta
|
||||
- name: twitter:image
|
||||
content: https://cdn.coollabs.io/assets/coollabs/og-image-databases.png
|
||||
- - meta
|
||||
- property: og:type
|
||||
content: website
|
||||
- - meta
|
||||
- property: og:url
|
||||
content: https://coolify.io
|
||||
- - meta
|
||||
- property: og:title
|
||||
content: Coolify
|
||||
- - meta
|
||||
- property: og:description
|
||||
content: An open-source & self-hostable Heroku / Netlify alternative.
|
||||
- - meta
|
||||
- property: og:site_name
|
||||
content: Coolify
|
||||
- - meta
|
||||
- property: og:image
|
||||
content: https://cdn.coollabs.io/assets/coollabs/og-image-databases.png
|
||||
---
|
||||
# Contribution
|
||||
|
||||
First, thanks for considering to contribute to my project. It really means a lot! :)
|
||||
@ -100,9 +58,58 @@ ### Create Prisma / Database schema for the new service.
|
||||
|
||||
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/api/prisma/schema.prisma](https://github.com/coollabsio/coolify/blob/main/apps/api/prisma/schema.prisma).
|
||||
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! 👏
|
@ -33,6 +33,7 @@ RUN chmod +x ~/.docker/cli-plugins/docker-compose /usr/bin/docker
|
||||
RUN (curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.27.0/pack-v0.27.0-linux.tgz" | tar -C /usr/local/bin/ --no-same-owner -xzv pack)
|
||||
|
||||
COPY --from=build /app/apps/api/build/ .
|
||||
COPY --from=build /app/others/fluentbit/ ./fluentbit
|
||||
COPY --from=build /app/apps/ui/build/ ./public
|
||||
COPY --from=build /app/apps/api/prisma/ ./prisma
|
||||
COPY --from=build /app/apps/api/package.json .
|
||||
|
@ -29,6 +29,8 @@
|
||||
"bree": "9.1.2",
|
||||
"cabin": "9.1.2",
|
||||
"compare-versions": "5.0.1",
|
||||
"csv-parse": "^5.3.0",
|
||||
"csvtojson": "^2.0.10",
|
||||
"cuid": "2.1.8",
|
||||
"dayjs": "1.11.5",
|
||||
"dockerode": "3.3.4",
|
||||
|
@ -0,0 +1,18 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Build" ADD COLUMN "previewApplicationId" TEXT;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "PreviewApplication" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"pullmergeRequestId" TEXT NOT NULL,
|
||||
"sourceBranch" TEXT NOT NULL,
|
||||
"isRandomDomain" BOOLEAN NOT NULL DEFAULT false,
|
||||
"customDomain" TEXT,
|
||||
"applicationId" TEXT NOT NULL,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" DATETIME NOT NULL,
|
||||
CONSTRAINT "PreviewApplication_applicationId_fkey" FOREIGN KEY ("applicationId") REFERENCES "Application" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "PreviewApplication_applicationId_key" ON "PreviewApplication"("applicationId");
|
@ -119,6 +119,19 @@ model Application {
|
||||
secrets Secret[]
|
||||
teams Team[]
|
||||
connectedDatabase ApplicationConnectedDatabase?
|
||||
previewApplication PreviewApplication[]
|
||||
}
|
||||
|
||||
model PreviewApplication {
|
||||
id String @id @default(cuid())
|
||||
pullmergeRequestId String
|
||||
sourceBranch String
|
||||
isRandomDomain Boolean @default(false)
|
||||
customDomain String?
|
||||
applicationId String @unique
|
||||
application Application @relation(fields: [applicationId], references: [id])
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model ApplicationConnectedDatabase {
|
||||
@ -219,6 +232,7 @@ model Build {
|
||||
gitlabAppId String?
|
||||
commit String?
|
||||
pullmergeRequestId String?
|
||||
previewApplicationId String?
|
||||
forceRebuild Boolean @default(false)
|
||||
sourceBranch String?
|
||||
branch String?
|
||||
|
@ -38,8 +38,16 @@ import * as buildpacks from '../lib/buildPacks';
|
||||
for (const queueBuild of queuedBuilds) {
|
||||
actions.push(async () => {
|
||||
let application = await prisma.application.findUnique({ where: { id: queueBuild.applicationId }, include: { destinationDocker: true, gitSource: { include: { githubApp: true, gitlabApp: true } }, persistentStorage: true, secrets: true, settings: true, teams: true } })
|
||||
let { id: buildId, type, sourceBranch = null, pullmergeRequestId = null, forceRebuild } = queueBuild
|
||||
let { id: buildId, type, sourceBranch = null, pullmergeRequestId = null, previewApplicationId = null, forceRebuild } = queueBuild
|
||||
application = decryptApplication(application)
|
||||
const originalApplicationId = application.id
|
||||
if (pullmergeRequestId) {
|
||||
const previewApplications = await prisma.previewApplication.findMany({ where: { applicationId: originalApplicationId, pullmergeRequestId } })
|
||||
if (previewApplications.length > 0) {
|
||||
previewApplicationId = previewApplications[0].id
|
||||
}
|
||||
}
|
||||
const usableApplicationId = previewApplicationId || originalApplicationId
|
||||
try {
|
||||
if (queueBuild.status === 'running') {
|
||||
await saveBuildLog({ line: 'Building halted, restarting...', buildId, applicationId: application.id });
|
||||
@ -104,17 +112,17 @@ import * as buildpacks from '../lib/buildPacks';
|
||||
)
|
||||
.digest('hex');
|
||||
const { debug } = settings;
|
||||
if (concurrency === 1) {
|
||||
await prisma.build.updateMany({
|
||||
where: {
|
||||
status: { in: ['queued', 'running'] },
|
||||
id: { not: buildId },
|
||||
applicationId,
|
||||
createdAt: { lt: new Date(new Date().getTime() - 10 * 1000) }
|
||||
},
|
||||
data: { status: 'failed' }
|
||||
});
|
||||
}
|
||||
// if (concurrency === 1) {
|
||||
// await prisma.build.updateMany({
|
||||
// where: {
|
||||
// status: { in: ['queued', 'running'] },
|
||||
// id: { not: buildId },
|
||||
// applicationId,
|
||||
// createdAt: { lt: new Date(new Date().getTime() - 10 * 1000) }
|
||||
// },
|
||||
// data: { status: 'failed' }
|
||||
// });
|
||||
// }
|
||||
let imageId = applicationId;
|
||||
let domain = getDomain(fqdn);
|
||||
const volumes =
|
||||
@ -338,10 +346,15 @@ import * as buildpacks from '../lib/buildPacks';
|
||||
await saveBuildLog({ line: 'Deployment successful!', buildId, applicationId });
|
||||
} catch (error) {
|
||||
await saveBuildLog({ line: error, buildId, applicationId });
|
||||
await prisma.build.updateMany({
|
||||
where: { id: buildId, status: { in: ['queued', 'running'] } },
|
||||
data: { status: 'failed' }
|
||||
const foundBuild = await prisma.build.findUnique({ where: { id: buildId } })
|
||||
if (foundBuild) {
|
||||
await prisma.build.update({
|
||||
where: { id: buildId },
|
||||
data: {
|
||||
status: 'failed'
|
||||
}
|
||||
});
|
||||
}
|
||||
throw new Error(error);
|
||||
}
|
||||
await saveBuildLog({ line: 'Proxy will be updated shortly.', buildId, applicationId });
|
||||
@ -353,10 +366,15 @@ import * as buildpacks from '../lib/buildPacks';
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
await prisma.build.updateMany({
|
||||
where: { id: buildId, status: { in: ['queued', 'running'] } },
|
||||
data: { status: 'failed' }
|
||||
const foundBuild = await prisma.build.findUnique({ where: { id: buildId } })
|
||||
if (foundBuild) {
|
||||
await prisma.build.update({
|
||||
where: { id: buildId },
|
||||
data: {
|
||||
status: 'failed'
|
||||
}
|
||||
});
|
||||
}
|
||||
if (error !== 1) {
|
||||
await saveBuildLog({ line: error, buildId, applicationId: application.id });
|
||||
}
|
||||
|
@ -29,7 +29,7 @@ async function autoUpdater() {
|
||||
`sed -i '/COOLIFY_AUTO_UPDATE=/cCOOLIFY_AUTO_UPDATE=${isAutoUpdateEnabled}' .env`
|
||||
);
|
||||
await asyncExecShell(
|
||||
`docker run --rm -tid --env-file .env -v /var/run/docker.sock:/var/run/docker.sock -v coolify-db coollabsio/coolify:${latestVersion} /bin/sh -c "env | grep COOLIFY > .env && echo 'TAG=${latestVersion}' >> .env && docker stop -t 0 coolify && docker rm coolify && docker compose up -d --force-recreate"`
|
||||
`docker run --rm -tid --env-file .env -v /var/run/docker.sock:/var/run/docker.sock -v coolify-db coollabsio/coolify:${latestVersion} /bin/sh -c "env | grep COOLIFY > .env && echo 'TAG=${latestVersion}' >> .env && docker stop -t 0 coolify coolify-fluentbit && docker rm coolify coolify-fluentbit && docker compose pull && docker compose up -d --force-recreate"`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { base64Encode, executeDockerCmd, generateTimestamp, getDomain, isDev, prisma, version } from "../common";
|
||||
import { base64Encode, encrypt, executeDockerCmd, generateTimestamp, getDomain, isDev, prisma, version } from "../common";
|
||||
import { promises as fs } from 'fs';
|
||||
import { day } from "../dayjs";
|
||||
|
||||
@ -461,17 +461,32 @@ export const saveBuildLog = async ({
|
||||
buildId: string;
|
||||
applicationId: string;
|
||||
}): Promise<any> => {
|
||||
const { default: got } = await import('got')
|
||||
|
||||
if (line && typeof line === 'string' && line.includes('ghs_')) {
|
||||
const regex = /ghs_.*@/g;
|
||||
line = line.replace(regex, '<SENSITIVE_DATA_DELETED>@');
|
||||
}
|
||||
const addTimestamp = `[${generateTimestamp()}] ${line}`;
|
||||
if (isDev) console.debug(`[${applicationId}] ${addTimestamp}`);
|
||||
const fluentBitUrl = isDev ? 'http://localhost:24224' : 'http://coolify-fluentbit:24224';
|
||||
|
||||
if (isDev) {
|
||||
console.debug(`[${applicationId}] ${addTimestamp}`);
|
||||
}
|
||||
try {
|
||||
return await got.post(`${fluentBitUrl}/${applicationId}_buildlog_${buildId}.csv`, {
|
||||
json: {
|
||||
line: encrypt(line)
|
||||
}
|
||||
})
|
||||
} catch(error) {
|
||||
return await prisma.buildLog.create({
|
||||
data: {
|
||||
line: addTimestamp, buildId, time: Number(day().valueOf()), applicationId
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
export async function copyBaseConfigurationFiles(
|
||||
@ -707,7 +722,6 @@ export async function buildCacheImageWithNode(data, imageForBuild) {
|
||||
Dockerfile.push(`RUN ${installCommand}`);
|
||||
}
|
||||
Dockerfile.push(`RUN ${buildCommand}`);
|
||||
console.log(Dockerfile.join('\n'))
|
||||
await fs.writeFile(`${workdir}/Dockerfile-cache`, Dockerfile.join('\n'));
|
||||
await buildImage({ ...data, isCache: true });
|
||||
}
|
||||
|
@ -21,7 +21,7 @@ import { scheduler } from './scheduler';
|
||||
import { supportedServiceTypesAndVersions } from './services/supportedVersions';
|
||||
import { includeServices } from './services/common';
|
||||
|
||||
export const version = '3.10.3';
|
||||
export const version = '3.10.4';
|
||||
export const isDev = process.env.NODE_ENV === 'development';
|
||||
|
||||
const algorithm = 'aes-256-ctr';
|
||||
@ -45,9 +45,7 @@ export function getAPIUrl() {
|
||||
if (process.env.CODESANDBOX_HOST) {
|
||||
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() {
|
||||
|
@ -1374,10 +1374,6 @@ async function startAppWriteService(request: FastifyRequest<ServiceStartStop>) {
|
||||
const teamId = request.user.teamId;
|
||||
const { version, fqdn, destinationDocker, secrets, exposePort, network, port, workdir, image, appwrite } = await defaultServiceConfigurations({ id, teamId })
|
||||
|
||||
let isStatsEnabled = false
|
||||
if (secrets.find(s => s === '_APP_USAGE_STATS=enabled')) {
|
||||
isStatsEnabled = true
|
||||
}
|
||||
const {
|
||||
opensslKeyV1,
|
||||
executorSecret,
|
||||
@ -1755,7 +1751,6 @@ async function startAppWriteService(request: FastifyRequest<ServiceStartStop>) {
|
||||
},
|
||||
|
||||
};
|
||||
if (isStatsEnabled) {
|
||||
dockerCompose[id].depends_on.push(`${id}-influxdb`);
|
||||
dockerCompose[`${id}-usage`] = {
|
||||
image: `${image}:${version}`,
|
||||
@ -1799,7 +1794,6 @@ async function startAppWriteService(request: FastifyRequest<ServiceStartStop>) {
|
||||
],
|
||||
...defaultComposeConfiguration(network),
|
||||
}
|
||||
}
|
||||
|
||||
const composeFile: any = {
|
||||
version: '3.8',
|
||||
|
@ -1,3 +1,24 @@
|
||||
/*
|
||||
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',
|
||||
@ -151,7 +172,7 @@ export const supportedServiceTypesAndVersions = [
|
||||
fancyName: 'Appwrite',
|
||||
baseImage: 'appwrite/appwrite',
|
||||
images: ['mariadb:10.7', 'redis:6.2-alpine', 'appwrite/telegraf:1.4.0'],
|
||||
versions: ['latest', '0.15.3'],
|
||||
versions: ['latest', '1.0','0.15.3'],
|
||||
recommendedVersion: '0.15.3',
|
||||
ports: {
|
||||
main: 80
|
||||
|
@ -5,6 +5,7 @@ import axios from 'axios';
|
||||
import { FastifyReply } from 'fastify';
|
||||
import fs from 'fs/promises';
|
||||
import yaml from 'js-yaml';
|
||||
import csv from 'csvtojson';
|
||||
|
||||
import { day } from '../../../../lib/dayjs';
|
||||
import { makeLabelForStandaloneApplication, setDefaultBaseImage, setDefaultConfiguration } from '../../../../lib/buildPacks/common';
|
||||
@ -12,8 +13,9 @@ import { checkDomainsIsValidInDNS, checkDoubleBranch, checkExposedPort, createDi
|
||||
import { checkContainer, formatLabelsOnDocker, isContainerExited, removeContainer } from '../../../../lib/docker';
|
||||
|
||||
import type { FastifyRequest } from 'fastify';
|
||||
import type { GetImages, CancelDeployment, CheckDNS, CheckRepository, DeleteApplication, DeleteSecret, DeleteStorage, GetApplicationLogs, GetBuildIdLogs, GetBuildLogs, SaveApplication, SaveApplicationSettings, SaveApplicationSource, SaveDeployKey, SaveDestination, SaveSecret, SaveStorage, DeployApplication, CheckDomain, StopPreviewApplication } 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';
|
||||
import { OnlyId } from '../../../../types';
|
||||
import path from 'node:path';
|
||||
|
||||
function filterObject(obj, callback) {
|
||||
return Object.fromEntries(Object.entries(obj).
|
||||
@ -83,8 +85,6 @@ export async function getApplicationStatus(request: FastifyRequest<OnlyId>) {
|
||||
isExited = status.status.isExited;
|
||||
isRestarting = status.status.isRestarting
|
||||
}
|
||||
|
||||
// isExited = await isContainerExited(application.destinationDocker.id, id);
|
||||
}
|
||||
return {
|
||||
isRunning,
|
||||
@ -164,7 +164,8 @@ export async function getApplicationFromDB(id: string, teamId: string) {
|
||||
gitSource: { include: { githubApp: true, gitlabApp: true } },
|
||||
secrets: true,
|
||||
persistentStorage: true,
|
||||
connectedDatabase: true
|
||||
connectedDatabase: true,
|
||||
previewApplication: true
|
||||
}
|
||||
});
|
||||
if (!application) {
|
||||
@ -350,6 +351,7 @@ export async function stopPreviewApplication(request: FastifyRequest<StopPreview
|
||||
if (found) {
|
||||
await removeContainer({ id: container, dockerId: application.destinationDocker.id });
|
||||
}
|
||||
await prisma.previewApplication.deleteMany({ where: { applicationId: application.id, pullmergeRequestId } })
|
||||
}
|
||||
return reply.code(201).send();
|
||||
} catch ({ status, message }) {
|
||||
@ -617,7 +619,7 @@ export async function deployApplication(request: FastifyRequest<DeployApplicatio
|
||||
githubAppId: application.gitSource?.githubApp?.id,
|
||||
gitlabAppId: application.gitSource?.gitlabApp?.id,
|
||||
status: 'queued',
|
||||
type: 'manual'
|
||||
type: pullmergeRequestId ? application.gitSource?.githubApp?.id ? 'manual_pr' : 'manual_mr' : 'manual'
|
||||
}
|
||||
});
|
||||
return {
|
||||
@ -808,7 +810,6 @@ export async function saveSecret(request: FastifyRequest<SaveSecret>, reply: Fas
|
||||
try {
|
||||
const { id } = request.params
|
||||
let { name, value, isBuildSecret, isPRMRSecret, isNew } = request.body
|
||||
|
||||
if (isNew) {
|
||||
const found = await prisma.secret.findFirst({ where: { name, applicationId: id, isPRMRSecret } });
|
||||
if (found) {
|
||||
@ -820,14 +821,24 @@ export async function saveSecret(request: FastifyRequest<SaveSecret>, reply: Fas
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (value) {
|
||||
value = encrypt(value.trim());
|
||||
}
|
||||
const found = await prisma.secret.findFirst({ where: { applicationId: id, name, isPRMRSecret } });
|
||||
|
||||
if (found) {
|
||||
if (!value && isPRMRSecret) {
|
||||
await prisma.secret.deleteMany({
|
||||
where: { applicationId: id, name, isPRMRSecret }
|
||||
});
|
||||
} else {
|
||||
|
||||
await prisma.secret.updateMany({
|
||||
where: { applicationId: id, name, isPRMRSecret },
|
||||
data: { value, isBuildSecret, isPRMRSecret }
|
||||
});
|
||||
}
|
||||
|
||||
} else {
|
||||
await prisma.secret.create({
|
||||
data: { name, value, isBuildSecret, isPRMRSecret, application: { connect: { id } } }
|
||||
@ -894,6 +905,181 @@ export async function deleteStorage(request: FastifyRequest<DeleteStorage>) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function restartPreview(request: FastifyRequest<RestartPreviewApplication>, reply: FastifyReply) {
|
||||
try {
|
||||
const { id, pullmergeRequestId } = request.params
|
||||
const { teamId } = request.user
|
||||
let application: any = await getApplicationFromDB(id, teamId);
|
||||
if (application?.destinationDockerId) {
|
||||
const buildId = cuid();
|
||||
const { id: dockerId, network } = application.destinationDocker;
|
||||
const { secrets, port, repository, persistentStorage, id: applicationId, buildPack, exposePort } = application;
|
||||
|
||||
const envs = [
|
||||
`PORT=${port}`
|
||||
];
|
||||
if (secrets.length > 0) {
|
||||
secrets.forEach((secret) => {
|
||||
if (pullmergeRequestId) {
|
||||
const isSecretFound = secrets.filter(s => s.name === secret.name && s.isPRMRSecret)
|
||||
if (isSecretFound.length > 0) {
|
||||
envs.push(`${secret.name}=${isSecretFound[0].value}`);
|
||||
} else {
|
||||
envs.push(`${secret.name}=${secret.value}`);
|
||||
}
|
||||
} else {
|
||||
if (!secret.isPRMRSecret) {
|
||||
envs.push(`${secret.name}=${secret.value}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
const { workdir } = await createDirectories({ repository, buildId });
|
||||
const labels = []
|
||||
let image = null
|
||||
const { stdout: container } = await executeDockerCmd({ dockerId, command: `docker container ls --filter 'label=com.docker.compose.service=${id}-${pullmergeRequestId}' --format '{{json .}}'` })
|
||||
const containersArray = container.trim().split('\n');
|
||||
for (const container of containersArray) {
|
||||
const containerObj = formatLabelsOnDocker(container);
|
||||
image = containerObj[0].Image
|
||||
Object.keys(containerObj[0].Labels).forEach(function (key) {
|
||||
if (key.startsWith('coolify')) {
|
||||
labels.push(`${key}=${containerObj[0].Labels[key]}`)
|
||||
}
|
||||
})
|
||||
}
|
||||
let imageFound = false;
|
||||
try {
|
||||
await executeDockerCmd({
|
||||
dockerId,
|
||||
command: `docker image inspect ${image}`
|
||||
})
|
||||
imageFound = true;
|
||||
} catch (error) {
|
||||
//
|
||||
}
|
||||
if (!imageFound) {
|
||||
throw { status: 500, message: 'Image not found, cannot restart application.' }
|
||||
}
|
||||
await fs.writeFile(`${workdir}/.env`, envs.join('\n'));
|
||||
|
||||
let envFound = false;
|
||||
try {
|
||||
envFound = !!(await fs.stat(`${workdir}/.env`));
|
||||
} catch (error) {
|
||||
//
|
||||
}
|
||||
const volumes =
|
||||
persistentStorage?.map((storage) => {
|
||||
return `${applicationId}${storage.path.replace(/\//gi, '-')}:${buildPack !== 'docker' ? '/app' : ''
|
||||
}${storage.path}`;
|
||||
}) || [];
|
||||
const composeVolumes = volumes.map((volume) => {
|
||||
return {
|
||||
[`${volume.split(':')[0]}`]: {
|
||||
name: volume.split(':')[0]
|
||||
}
|
||||
};
|
||||
});
|
||||
const composeFile = {
|
||||
version: '3.8',
|
||||
services: {
|
||||
[`${applicationId}-${pullmergeRequestId}`]: {
|
||||
image,
|
||||
container_name: `${applicationId}-${pullmergeRequestId}`,
|
||||
volumes,
|
||||
env_file: envFound ? [`${workdir}/.env`] : [],
|
||||
labels,
|
||||
depends_on: [],
|
||||
expose: [port],
|
||||
...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}),
|
||||
...defaultComposeConfiguration(network),
|
||||
}
|
||||
},
|
||||
networks: {
|
||||
[network]: {
|
||||
external: true
|
||||
}
|
||||
},
|
||||
volumes: Object.assign({}, ...composeVolumes)
|
||||
};
|
||||
await fs.writeFile(`${workdir}/docker-compose.yml`, yaml.dump(composeFile));
|
||||
await executeDockerCmd({ dockerId, command: `docker stop -t 0 ${id}-${pullmergeRequestId}` })
|
||||
await executeDockerCmd({ dockerId, command: `docker rm ${id}-${pullmergeRequestId}` })
|
||||
await executeDockerCmd({ dockerId, command: `docker compose --project-directory ${workdir} up -d` })
|
||||
return reply.code(201).send();
|
||||
}
|
||||
throw { status: 500, message: 'Application cannot be restarted.' }
|
||||
} catch ({ status, message }) {
|
||||
return errorHandler({ status, message })
|
||||
}
|
||||
}
|
||||
export async function getPreviewStatus(request: FastifyRequest<RestartPreviewApplication>) {
|
||||
try {
|
||||
const { id, pullmergeRequestId } = request.params
|
||||
const { teamId } = request.user
|
||||
let isRunning = false;
|
||||
let isExited = false;
|
||||
let isRestarting = false;
|
||||
let isBuilding = false
|
||||
const application: any = await getApplicationFromDB(id, teamId);
|
||||
if (application?.destinationDockerId) {
|
||||
const status = await checkContainer({ dockerId: application.destinationDocker.id, container: `${id}-${pullmergeRequestId}` });
|
||||
if (status?.found) {
|
||||
isRunning = status.status.isRunning;
|
||||
isExited = status.status.isExited;
|
||||
isRestarting = status.status.isRestarting
|
||||
}
|
||||
const building = await prisma.build.findMany({ where: { applicationId: id, pullmergeRequestId, status: { in: ['queued', 'running'] } } })
|
||||
isBuilding = building.length > 0
|
||||
}
|
||||
return {
|
||||
isBuilding,
|
||||
isRunning,
|
||||
isRestarting,
|
||||
isExited,
|
||||
};
|
||||
} catch ({ status, message }) {
|
||||
return errorHandler({ status, message })
|
||||
}
|
||||
}
|
||||
export async function loadPreviews(request: FastifyRequest<OnlyId>) {
|
||||
try {
|
||||
const { id } = request.params
|
||||
const application = await prisma.application.findUnique({ where: { id }, include: { destinationDocker: true } });
|
||||
const { stdout } = await executeDockerCmd({ dockerId: application.destinationDocker.id, command: `docker container ls --filter 'name=${id}-' --format "{{json .}}"` })
|
||||
if (stdout === '') {
|
||||
throw { status: 500, message: 'No previews found.' }
|
||||
}
|
||||
const containers = formatLabelsOnDocker(stdout).filter(container => container.Labels['coolify.configuration'] && container.Labels['coolify.type'] === 'standalone-application')
|
||||
|
||||
const jsonContainers = containers
|
||||
.map((container) =>
|
||||
JSON.parse(Buffer.from(container.Labels['coolify.configuration'], 'base64').toString())
|
||||
)
|
||||
.filter((container) => {
|
||||
return container.pullmergeRequestId && container.applicationId === id;
|
||||
});
|
||||
for (const container of jsonContainers) {
|
||||
const found = await prisma.previewApplication.findMany({ where: { applicationId: container.applicationId, pullmergeRequestId: container.pullmergeRequestId } })
|
||||
if (found.length === 0) {
|
||||
await prisma.previewApplication.create({
|
||||
data: {
|
||||
pullmergeRequestId: container.pullmergeRequestId,
|
||||
sourceBranch: container.branch,
|
||||
customDomain: container.fqdn,
|
||||
application: { connect: { id: container.applicationId } }
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
return {
|
||||
previews: await prisma.previewApplication.findMany({ where: { applicationId: id } })
|
||||
}
|
||||
} catch ({ status, message }) {
|
||||
return errorHandler({ status, message })
|
||||
}
|
||||
}
|
||||
export async function getPreviews(request: FastifyRequest<OnlyId>) {
|
||||
try {
|
||||
const { id } = request.params
|
||||
@ -909,26 +1095,7 @@ export async function getPreviews(request: FastifyRequest<OnlyId>) {
|
||||
|
||||
const applicationSecrets = secrets.filter((secret) => !secret.isPRMRSecret);
|
||||
const PRMRSecrets = secrets.filter((secret) => secret.isPRMRSecret);
|
||||
const application = await prisma.application.findUnique({ where: { id }, include: { destinationDocker: true } });
|
||||
const { stdout } = await executeDockerCmd({ dockerId: application.destinationDocker.id, command: `docker container ls --filter 'name=${id}-' --format "{{json .}}"` })
|
||||
if (stdout === '') {
|
||||
return {
|
||||
containers: [],
|
||||
applicationSecrets: [],
|
||||
PRMRSecrets: []
|
||||
}
|
||||
}
|
||||
const containers = formatLabelsOnDocker(stdout).filter(container => container.Labels['coolify.configuration'] && container.Labels['coolify.type'] === 'standalone-application')
|
||||
|
||||
const jsonContainers = containers
|
||||
.map((container) =>
|
||||
JSON.parse(Buffer.from(container.Labels['coolify.configuration'], 'base64').toString())
|
||||
)
|
||||
.filter((container) => {
|
||||
return container.pullmergeRequestId && container.applicationId === id;
|
||||
});
|
||||
return {
|
||||
containers: jsonContainers,
|
||||
applicationSecrets: applicationSecrets.sort((a, b) => {
|
||||
return ('' + a.name).localeCompare(b.name);
|
||||
}),
|
||||
@ -980,7 +1147,7 @@ export async function getApplicationLogs(request: FastifyRequest<GetApplicationL
|
||||
return errorHandler({ status, message })
|
||||
}
|
||||
}
|
||||
export async function getBuildLogs(request: FastifyRequest<GetBuildLogs>) {
|
||||
export async function getBuilds(request: FastifyRequest<GetBuilds>) {
|
||||
try {
|
||||
const { id } = request.params
|
||||
let { buildId, skip = 0 } = request.query
|
||||
@ -997,17 +1164,15 @@ export async function getBuildLogs(request: FastifyRequest<GetBuildLogs>) {
|
||||
builds = await prisma.build.findMany({
|
||||
where: { applicationId: id },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
take: 5,
|
||||
skip
|
||||
take: 5 + skip
|
||||
});
|
||||
}
|
||||
|
||||
builds = builds.map((build) => {
|
||||
const updatedAt = day(build.updatedAt).utc();
|
||||
build.took = updatedAt.diff(day(build.createdAt)) / 1000;
|
||||
build.since = updatedAt.fromNow();
|
||||
return build;
|
||||
});
|
||||
if (build.status === 'running') {
|
||||
build.elapsed = (day().utc().diff(day(build.createdAt)) / 1000).toFixed(0);
|
||||
}
|
||||
return build
|
||||
})
|
||||
return {
|
||||
builds,
|
||||
buildCount
|
||||
@ -1019,11 +1184,21 @@ export async function getBuildLogs(request: FastifyRequest<GetBuildLogs>) {
|
||||
|
||||
export async function getBuildIdLogs(request: FastifyRequest<GetBuildIdLogs>) {
|
||||
try {
|
||||
const { buildId } = request.params
|
||||
// TODO: Fluentbit could still hold the logs, so we need to check if the logs are done
|
||||
const { buildId, id } = request.params
|
||||
let { sequence = 0 } = request.query
|
||||
if (typeof sequence !== 'number') {
|
||||
sequence = Number(sequence)
|
||||
}
|
||||
let file = `/app/logs/${id}_buildlog_${buildId}.csv`
|
||||
if (isDev) {
|
||||
file = `${process.cwd()}/../../logs/${id}_buildlog_${buildId}.csv`
|
||||
}
|
||||
const data = await prisma.build.findFirst({ where: { id: buildId } });
|
||||
const createdAt = day(data.createdAt).utc();
|
||||
try {
|
||||
await fs.stat(file)
|
||||
} catch (error) {
|
||||
let logs = await prisma.buildLog.findMany({
|
||||
where: { buildId, time: { gt: sequence } },
|
||||
orderBy: { time: 'asc' }
|
||||
@ -1035,6 +1210,23 @@ export async function getBuildIdLogs(request: FastifyRequest<GetBuildIdLogs>) {
|
||||
log.time = Number(log.time)
|
||||
return log
|
||||
}),
|
||||
fromDb: true,
|
||||
took: day().diff(createdAt) / 1000,
|
||||
status: data?.status || 'queued'
|
||||
}
|
||||
}
|
||||
let fileLogs = (await fs.readFile(file)).toString()
|
||||
let decryptedLogs = await csv({ noheader: true }).fromString(fileLogs)
|
||||
let logs = decryptedLogs.map(log => {
|
||||
const parsed = {
|
||||
time: log['field1'],
|
||||
line: decrypt(log['field2'] + '","' + log['field3'])
|
||||
}
|
||||
return parsed
|
||||
}).filter(log => log.time > sequence)
|
||||
return {
|
||||
logs,
|
||||
fromDb: false,
|
||||
took: day().diff(createdAt) / 1000,
|
||||
status: data?.status || 'queued'
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { FastifyPluginAsync } from 'fastify';
|
||||
import { OnlyId } from '../../../../types';
|
||||
import { cancelDeployment, checkDNS, checkDomain, checkRepository, deleteApplication, deleteSecret, deleteStorage, deployApplication, getApplication, getApplicationLogs, getApplicationStatus, getBuildIdLogs, getBuildLogs, getBuildPack, getGitHubToken, getGitLabSSHKey, getImages, getPreviews, getSecrets, getStorages, getUsage, listApplications, newApplication, restartApplication, saveApplication, saveApplicationSettings, saveApplicationSource, saveBuildPack, saveConnectedDatabase, saveDeployKey, saveDestination, saveGitLabSSHKey, saveRepository, saveSecret, saveStorage, stopApplication, stopPreviewApplication } from './handlers';
|
||||
import { cancelDeployment, checkDNS, checkDomain, checkRepository, deleteApplication, deleteSecret, deleteStorage, deployApplication, getApplication, getApplicationLogs, getApplicationStatus, getBuildIdLogs, getBuildPack, getBuilds, getGitHubToken, getGitLabSSHKey, getImages, getPreviews, getPreviewStatus, getSecrets, getStorages, getUsage, listApplications, loadPreviews, newApplication, restartApplication, restartPreview, saveApplication, saveApplicationSettings, saveApplicationSource, saveBuildPack, saveConnectedDatabase, saveDeployKey, saveDestination, saveGitLabSSHKey, saveRepository, saveSecret, saveStorage, stopApplication, stopPreviewApplication } from './handlers';
|
||||
|
||||
import type { CancelDeployment, CheckDNS, CheckDomain, CheckRepository, DeleteApplication, DeleteSecret, DeleteStorage, DeployApplication, GetApplicationLogs, GetBuildIdLogs, GetBuildLogs, GetImages, SaveApplication, SaveApplicationSettings, SaveApplicationSource, SaveDeployKey, SaveDestination, SaveSecret, SaveStorage, StopPreviewApplication } from './types';
|
||||
import type { CancelDeployment, CheckDNS, CheckDomain, CheckRepository, DeleteApplication, DeleteSecret, DeleteStorage, DeployApplication, GetApplicationLogs, GetBuildIdLogs, GetBuilds, GetImages, RestartPreviewApplication, SaveApplication, SaveApplicationSettings, SaveApplicationSource, SaveDeployKey, SaveDestination, SaveSecret, SaveStorage, StopPreviewApplication } from './types';
|
||||
|
||||
const root: FastifyPluginAsync = async (fastify): Promise<void> => {
|
||||
fastify.addHook('onRequest', async (request) => {
|
||||
@ -37,9 +37,12 @@ const root: FastifyPluginAsync = async (fastify): Promise<void> => {
|
||||
fastify.delete<DeleteStorage>('/:id/storages', async (request) => await deleteStorage(request));
|
||||
|
||||
fastify.get<OnlyId>('/:id/previews', async (request) => await getPreviews(request));
|
||||
fastify.post<OnlyId>('/:id/previews/load', async (request) => await loadPreviews(request));
|
||||
fastify.get<RestartPreviewApplication>('/:id/previews/:pullmergeRequestId/status', async (request) => await getPreviewStatus(request));
|
||||
fastify.post<RestartPreviewApplication>('/:id/previews/:pullmergeRequestId/restart', async (request, reply) => await restartPreview(request, reply));
|
||||
|
||||
fastify.get<GetApplicationLogs>('/:id/logs', async (request) => await getApplicationLogs(request));
|
||||
fastify.get<GetBuildLogs>('/:id/logs/build', async (request) => await getBuildLogs(request));
|
||||
fastify.get<GetBuilds>('/:id/logs/build', async (request) => await getBuilds(request));
|
||||
fastify.get<GetBuildIdLogs>('/:id/logs/build/:buildId', async (request) => await getBuildIdLogs(request));
|
||||
|
||||
fastify.get('/:id/usage', async (request) => await getUsage(request))
|
||||
|
@ -89,7 +89,7 @@ export interface GetApplicationLogs extends OnlyId {
|
||||
since: number,
|
||||
}
|
||||
}
|
||||
export interface GetBuildLogs extends OnlyId {
|
||||
export interface GetBuilds extends OnlyId {
|
||||
Querystring: {
|
||||
buildId: string
|
||||
skip: number,
|
||||
@ -97,6 +97,7 @@ export interface GetBuildLogs extends OnlyId {
|
||||
}
|
||||
export interface GetBuildIdLogs {
|
||||
Params: {
|
||||
id: string,
|
||||
buildId: string
|
||||
},
|
||||
Querystring: {
|
||||
@ -127,3 +128,9 @@ export interface StopPreviewApplication extends OnlyId {
|
||||
pullmergeRequestId: string | null,
|
||||
}
|
||||
}
|
||||
export interface RestartPreviewApplication {
|
||||
Params: {
|
||||
id: string,
|
||||
pullmergeRequestId: string | null,
|
||||
}
|
||||
}
|
@ -73,7 +73,7 @@ export async function update(request: FastifyRequest<Update>) {
|
||||
`sed -i '/COOLIFY_AUTO_UPDATE=/cCOOLIFY_AUTO_UPDATE=${isAutoUpdateEnabled}' .env`
|
||||
);
|
||||
await asyncExecShell(
|
||||
`docker run --rm -tid --env-file .env -v /var/run/docker.sock:/var/run/docker.sock -v coolify-db coollabsio/coolify:${latestVersion} /bin/sh -c "env | grep COOLIFY > .env && echo 'TAG=${latestVersion}' >> .env && docker stop -t 0 coolify && docker rm coolify && docker compose up -d --force-recreate"`
|
||||
`docker run --rm -tid --env-file .env -v /var/run/docker.sock:/var/run/docker.sock -v coolify-db coollabsio/coolify:${latestVersion} /bin/sh -c "env | grep COOLIFY > .env && echo 'TAG=${latestVersion}' >> .env && docker stop -t 0 coolify coolify-fluentbit && docker rm coolify coolify-fluentbit && docker compose pull && docker compose up -d --force-recreate"`
|
||||
);
|
||||
return {};
|
||||
} else {
|
||||
|
@ -8,9 +8,7 @@ export async function listServers(request: FastifyRequest) {
|
||||
try {
|
||||
const userId = request.user.userId;
|
||||
const teamId = request.user.teamId;
|
||||
const servers = await prisma.destinationDocker.findMany({ where: { teams: { some: { id: teamId === '0' ? undefined : teamId } }, remoteEngine: false }, distinct: ['engine'] })
|
||||
// const remoteServers = await prisma.destinationDocker.findMany({ where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } }, distinct: ['remoteIpAddress', 'engine'] })
|
||||
|
||||
const servers = await prisma.destinationDocker.findMany({ where: { teams: { some: { id: teamId === '0' ? undefined : teamId } }}, distinct: ['remoteIpAddress', 'engine'] })
|
||||
return {
|
||||
servers
|
||||
}
|
||||
@ -67,8 +65,7 @@ export async function showUsage(request: FastifyRequest) {
|
||||
const { stdout: stats } = await executeSSHCmd({ dockerId: id, command: `vmstat -s` })
|
||||
const { stdout: disks } = await executeSSHCmd({ dockerId: id, command: `df -m / --output=size,used,pcent|grep -v 'Used'| xargs` })
|
||||
const { stdout: cpus } = await executeSSHCmd({ dockerId: id, command: `nproc --all` })
|
||||
// const { stdout: cpuUsage } = await executeSSHCmd({ dockerId: id, command: `echo $[100-$(vmstat 1 2|tail -1|awk '{print $15}')]` })
|
||||
// console.log(cpuUsage)
|
||||
const { stdout: cpuUsage } = await executeSSHCmd({ dockerId: id, command: `echo $[100-$(vmstat 1 2|tail -1|awk '{print $15}')]` })
|
||||
const parsed: any = parseFromText(stats)
|
||||
return {
|
||||
usage: {
|
||||
@ -81,8 +78,8 @@ export async function showUsage(request: FastifyRequest) {
|
||||
freeMemPercentage: (parsed.totalMemoryKB - parsed.usedMemoryKB) / parsed.totalMemoryKB * 100
|
||||
},
|
||||
cpu: {
|
||||
load: 0,
|
||||
usage: 0,
|
||||
load: [0,0,0],
|
||||
usage: cpuUsage,
|
||||
count: cpus
|
||||
},
|
||||
disk: {
|
||||
|
@ -456,7 +456,7 @@ export async function activatePlausibleUsers(request: FastifyRequest<OnlyId>, re
|
||||
if (destinationDockerId) {
|
||||
await executeDockerCmd({
|
||||
dockerId: destinationDocker.id,
|
||||
command: `docker exec ${id} 'psql -H postgresql://${postgresqlUser}:${postgresqlPassword}@localhost:5432/${postgresqlDatabase} -c "UPDATE users SET email_verified = true;"'`
|
||||
command: `docker exec ${id}-postgresql psql -H postgresql://${postgresqlUser}:${postgresqlPassword}@localhost:5432/${postgresqlDatabase} -c "UPDATE users SET email_verified = true;"`
|
||||
})
|
||||
return await reply.code(201).send()
|
||||
}
|
||||
@ -476,7 +476,7 @@ export async function cleanupPlausibleLogs(request: FastifyRequest<OnlyId>, repl
|
||||
if (destinationDockerId) {
|
||||
await executeDockerCmd({
|
||||
dockerId: destinationDocker.id,
|
||||
command: `docker exec ${id}-clickhouse sh -c "/usr/bin/clickhouse-client -q \\"SELECT name FROM system.tables WHERE name LIKE '%log%';\\"| xargs -I{} /usr/bin/clickhouse-client -q \"TRUNCATE TABLE system.{};\""`
|
||||
command: `docker exec ${id}-clickhouse /usr/bin/clickhouse-client -q \\"SELECT name FROM system.tables WHERE name LIKE '%log%';\\"| xargs -I{} /usr/bin/clickhouse-client -q \"TRUNCATE TABLE system.{};\"`
|
||||
})
|
||||
return await reply.code(201).send()
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import axios from "axios";
|
||||
import cuid from "cuid";
|
||||
import crypto from "crypto";
|
||||
import { encrypt, errorHandler, getUIUrl, isDev, prisma } from "../../../lib/common";
|
||||
import { encrypt, errorHandler, getDomain, getUIUrl, isDev, prisma } from "../../../lib/common";
|
||||
import { checkContainer, removeContainer } from "../../../lib/docker";
|
||||
import { createdBranchDatabase, getApplicationFromDBWebhook, removeBranchDatabase } from "../../api/v1/applications/handlers";
|
||||
|
||||
@ -169,10 +169,29 @@ export async function gitHubEvents(request: FastifyRequest<GitHubEvents>): Promi
|
||||
pullmergeRequestAction === 'reopened' ||
|
||||
pullmergeRequestAction === 'synchronize'
|
||||
) {
|
||||
|
||||
await prisma.application.update({
|
||||
where: { id: application.id },
|
||||
data: { updatedAt: new Date() }
|
||||
});
|
||||
let previewApplicationId = undefined
|
||||
if (pullmergeRequestId) {
|
||||
const foundPreviewApplications = await prisma.previewApplication.findMany({ where: { applicationId: application.id, pullmergeRequestId } })
|
||||
if (foundPreviewApplications.length > 0) {
|
||||
previewApplicationId = foundPreviewApplications[0].id
|
||||
} else {
|
||||
const protocol = application.fqdn.includes('https://') ? 'https://' : 'http://'
|
||||
const previewApplication = await prisma.previewApplication.create({
|
||||
data: {
|
||||
pullmergeRequestId,
|
||||
sourceBranch,
|
||||
customDomain: `${protocol}${pullmergeRequestId}.${getDomain(application.fqdn)}`,
|
||||
application: { connect: { id: application.id } }
|
||||
}
|
||||
})
|
||||
previewApplicationId = previewApplication.id
|
||||
}
|
||||
}
|
||||
// if (application.connectedDatabase && pullmergeRequestAction === 'opened' || pullmergeRequestAction === 'reopened') {
|
||||
// // Coolify hosted database
|
||||
// if (application.connectedDatabase.databaseId) {
|
||||
@ -187,6 +206,7 @@ export async function gitHubEvents(request: FastifyRequest<GitHubEvents>): Promi
|
||||
data: {
|
||||
id: buildId,
|
||||
pullmergeRequestId,
|
||||
previewApplicationId,
|
||||
sourceBranch,
|
||||
applicationId: application.id,
|
||||
destinationDockerId: application.destinationDocker.id,
|
||||
@ -198,7 +218,9 @@ export async function gitHubEvents(request: FastifyRequest<GitHubEvents>): Promi
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
return {
|
||||
message: 'Queued. Thank you!'
|
||||
};
|
||||
} else if (pullmergeRequestAction === 'closed') {
|
||||
if (application.destinationDockerId) {
|
||||
const id = `${application.id}-${pullmergeRequestId}`;
|
||||
@ -206,13 +228,22 @@ export async function gitHubEvents(request: FastifyRequest<GitHubEvents>): Promi
|
||||
await removeContainer({ id, dockerId: application.destinationDocker.id });
|
||||
} catch (error) { }
|
||||
}
|
||||
if (application.connectedDatabase.databaseId) {
|
||||
const databaseId = application.connectedDatabase.databaseId;
|
||||
const database = await prisma.database.findUnique({ where: { id: databaseId } });
|
||||
if (database) {
|
||||
await removeBranchDatabase(database, pullmergeRequestId);
|
||||
const foundPreviewApplications = await prisma.previewApplication.findMany({ where: { applicationId: application.id, pullmergeRequestId } })
|
||||
if (foundPreviewApplications.length > 0) {
|
||||
for (const preview of foundPreviewApplications) {
|
||||
await prisma.previewApplication.delete({ where: { id: preview.id } })
|
||||
}
|
||||
}
|
||||
return {
|
||||
message: 'PR closed. Thank you!'
|
||||
};
|
||||
// if (application?.connectedDatabase?.databaseId) {
|
||||
// const databaseId = application.connectedDatabase.databaseId;
|
||||
// const database = await prisma.database.findUnique({ where: { id: databaseId } });
|
||||
// if (database) {
|
||||
// await removeBranchDatabase(database, pullmergeRequestId);
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ import axios from "axios";
|
||||
import cuid from "cuid";
|
||||
import crypto from "crypto";
|
||||
import type { FastifyReply, FastifyRequest } from "fastify";
|
||||
import { errorHandler, getAPIUrl, getUIUrl, isDev, listSettings, prisma } from "../../../lib/common";
|
||||
import { errorHandler, getAPIUrl, getDomain, getUIUrl, isDev, listSettings, prisma } from "../../../lib/common";
|
||||
import { checkContainer, removeContainer } from "../../../lib/docker";
|
||||
import { getApplicationFromDB, getApplicationFromDBWebhook } from "../../api/v1/applications/handlers";
|
||||
|
||||
@ -91,8 +91,8 @@ export async function gitLabEvents(request: FastifyRequest<GitLabEvents>) {
|
||||
}
|
||||
}
|
||||
} else if (objectKind === 'merge_request') {
|
||||
const { object_attributes: { work_in_progress: isDraft, action, source_branch: sourceBranch, target_branch: targetBranch, iid: pullmergeRequestId }, project: { id } } = request.body
|
||||
|
||||
const { object_attributes: { work_in_progress: isDraft, action, source_branch: sourceBranch, target_branch: targetBranch }, project: { id } } = request.body
|
||||
const pullmergeRequestId = request.body.object_attributes.iid.toString();
|
||||
const projectId = Number(id);
|
||||
if (!allowedActions.includes(action)) {
|
||||
throw { status: 500, message: 'Action not allowed.' }
|
||||
@ -130,10 +130,29 @@ export async function gitLabEvents(request: FastifyRequest<GitLabEvents>) {
|
||||
where: { id: application.id },
|
||||
data: { updatedAt: new Date() }
|
||||
});
|
||||
let previewApplicationId = undefined
|
||||
if (pullmergeRequestId) {
|
||||
const foundPreviewApplications = await prisma.previewApplication.findMany({ where: { applicationId: application.id, pullmergeRequestId } })
|
||||
if (foundPreviewApplications.length > 0) {
|
||||
previewApplicationId = foundPreviewApplications[0].id
|
||||
} else {
|
||||
const protocol = application.fqdn.includes('https://') ? 'https://' : 'http://'
|
||||
const previewApplication = await prisma.previewApplication.create({
|
||||
data: {
|
||||
pullmergeRequestId,
|
||||
sourceBranch,
|
||||
customDomain: `${protocol}${pullmergeRequestId}.${getDomain(application.fqdn)}`,
|
||||
application: { connect: { id: application.id } }
|
||||
}
|
||||
})
|
||||
previewApplicationId = previewApplication.id
|
||||
}
|
||||
}
|
||||
await prisma.build.create({
|
||||
data: {
|
||||
id: buildId,
|
||||
pullmergeRequestId: pullmergeRequestId.toString(),
|
||||
pullmergeRequestId,
|
||||
previewApplicationId,
|
||||
sourceBranch,
|
||||
applicationId: application.id,
|
||||
destinationDockerId: application.destinationDocker.id,
|
||||
@ -150,8 +169,19 @@ export async function gitLabEvents(request: FastifyRequest<GitLabEvents>) {
|
||||
} else if (action === 'close') {
|
||||
if (application.destinationDockerId) {
|
||||
const id = `${application.id}-${pullmergeRequestId}`;
|
||||
try {
|
||||
await removeContainer({ id, dockerId: application.destinationDocker.id });
|
||||
} catch (error) { }
|
||||
}
|
||||
const foundPreviewApplications = await prisma.previewApplication.findMany({ where: { applicationId: application.id, pullmergeRequestId } })
|
||||
if (foundPreviewApplications.length > 0) {
|
||||
for (const preview of foundPreviewApplications) {
|
||||
await prisma.previewApplication.delete({ where: { id: preview.id } })
|
||||
}
|
||||
}
|
||||
return {
|
||||
message: 'MR closed. Thank you!'
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -12,7 +12,7 @@ function configureMiddleware(
|
||||
if (isHttps) {
|
||||
traefik.http.routers[id] = {
|
||||
entrypoints: ['web'],
|
||||
rule: `Host(\`${nakedDomain}\`) || Host(\`www.${nakedDomain}\`)`,
|
||||
rule: `(Host(\`${nakedDomain}\`) || Host(\`www.${nakedDomain}\`)) && PathPrefix(\`/\`)`,
|
||||
service: `${id}`,
|
||||
middlewares: ['redirect-to-https']
|
||||
};
|
||||
@ -53,7 +53,7 @@ function configureMiddleware(
|
||||
if (isDualCerts) {
|
||||
traefik.http.routers[`${id}-secure`] = {
|
||||
entrypoints: ['websecure'],
|
||||
rule: `Host(\`${nakedDomain}\`) || Host(\`www.${nakedDomain}\`)`,
|
||||
rule: `(Host(\`${nakedDomain}\`) || Host(\`www.${nakedDomain}\`)) && PathPrefix(\`/\`)`,
|
||||
service: `${id}`,
|
||||
tls: {
|
||||
certresolver: 'letsencrypt'
|
||||
@ -64,7 +64,7 @@ function configureMiddleware(
|
||||
if (isWWW) {
|
||||
traefik.http.routers[`${id}-secure-www`] = {
|
||||
entrypoints: ['websecure'],
|
||||
rule: `Host(\`www.${nakedDomain}\`)`,
|
||||
rule: `Host(\`www.${nakedDomain}\`) && PathPrefix(\`/\`)`,
|
||||
service: `${id}`,
|
||||
tls: {
|
||||
certresolver: 'letsencrypt'
|
||||
@ -73,7 +73,7 @@ function configureMiddleware(
|
||||
};
|
||||
traefik.http.routers[`${id}-secure`] = {
|
||||
entrypoints: ['websecure'],
|
||||
rule: `Host(\`${nakedDomain}\`)`,
|
||||
rule: `Host(\`${nakedDomain}\`) && PathPrefix(\`/\`)`,
|
||||
service: `${id}`,
|
||||
tls: {
|
||||
domains: {
|
||||
@ -86,7 +86,7 @@ function configureMiddleware(
|
||||
} else {
|
||||
traefik.http.routers[`${id}-secure-www`] = {
|
||||
entrypoints: ['websecure'],
|
||||
rule: `Host(\`www.${nakedDomain}\`)`,
|
||||
rule: `Host(\`www.${nakedDomain}\`) && PathPrefix(\`/\`)`,
|
||||
service: `${id}`,
|
||||
tls: {
|
||||
domains: {
|
||||
@ -97,7 +97,7 @@ function configureMiddleware(
|
||||
};
|
||||
traefik.http.routers[`${id}-secure`] = {
|
||||
entrypoints: ['websecure'],
|
||||
rule: `Host(\`${domain}\`)`,
|
||||
rule: `Host(\`${domain}\`) && PathPrefix(\`/\`)`,
|
||||
service: `${id}`,
|
||||
tls: {
|
||||
certresolver: 'letsencrypt'
|
||||
@ -110,14 +110,14 @@ function configureMiddleware(
|
||||
} else {
|
||||
traefik.http.routers[id] = {
|
||||
entrypoints: ['web'],
|
||||
rule: `Host(\`${nakedDomain}\`) || Host(\`www.${nakedDomain}\`)`,
|
||||
rule: `(Host(\`${nakedDomain}\`) || Host(\`www.${nakedDomain}\`)) && PathPrefix(\`/\`)`,
|
||||
service: `${id}`,
|
||||
middlewares: []
|
||||
};
|
||||
|
||||
traefik.http.routers[`${id}-secure`] = {
|
||||
entrypoints: ['websecure'],
|
||||
rule: `Host(\`${nakedDomain}\`) || Host(\`www.${nakedDomain}\`)`,
|
||||
rule: `(Host(\`${nakedDomain}\`) || Host(\`www.${nakedDomain}\`)) && PathPrefix(\`/\`)`,
|
||||
service: `${id}`,
|
||||
tls: {
|
||||
domains: {
|
||||
|
@ -42,6 +42,7 @@
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"dayjs": "1.11.5",
|
||||
"@sveltejs/adapter-static": "1.0.0-next.39",
|
||||
"@tailwindcss/typography": "^0.5.7",
|
||||
"cuid": "2.1.8",
|
||||
|
@ -84,3 +84,7 @@ export function handlerNotFoundLoad(error: any, url: URL) {
|
||||
error: new Error(`Could not load ${url}`)
|
||||
};
|
||||
}
|
||||
|
||||
export function getRndInteger(min: number, max: number) {
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
}
|
@ -94,9 +94,11 @@
|
||||
</div>
|
||||
{#if $appSession.teamId === '0'}
|
||||
<button
|
||||
disabled={loading.cleanup}
|
||||
on:click={manuallyCleanupStorage}
|
||||
class:loading={loading.cleanup}
|
||||
class="btn btn-sm bg-coollabs">Cleanup Storage</button
|
||||
class:bg-coollabs={!loading.cleanup}
|
||||
class="btn btn-sm">Cleanup Storage</button
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
@ -108,21 +110,21 @@
|
||||
<div class="stats stats-vertical min-w-[16rem] mb-5 rounded bg-transparent">
|
||||
<div class="stat">
|
||||
<div class="stat-title">Total Memory</div>
|
||||
<div class="stat-value text-2xl">
|
||||
<div class="stat-value text-2xl text-white">
|
||||
{(usage?.memory?.totalMemMb).toFixed(0)}<span class="text-sm">MB</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat">
|
||||
<div class="stat-title">Used Memory</div>
|
||||
<div class="stat-value text-2xl">
|
||||
<div class="stat-value text-2xl text-white">
|
||||
{(usage?.memory?.usedMemMb).toFixed(0)}<span class="text-sm">MB</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat">
|
||||
<div class="stat-title">Free Memory</div>
|
||||
<div class="stat-value text-2xl">
|
||||
<div class="stat-value text-2xl text-white">
|
||||
{(usage?.memory?.freeMemPercentage).toFixed(0)}<span class="text-sm">%</span>
|
||||
</div>
|
||||
</div>
|
||||
@ -131,41 +133,41 @@
|
||||
<div class="stats stats-vertical min-w-[20rem] mb-5 bg-transparent rounded">
|
||||
<div class="stat">
|
||||
<div class="stat-title">Total CPU</div>
|
||||
<div class="stat-value text-2xl">
|
||||
<div class="stat-value text-2xl text-white">
|
||||
{usage?.cpu?.count}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat">
|
||||
<div class="stat-title">CPU Usage</div>
|
||||
<div class="stat-value text-2xl">
|
||||
<div class="stat-value text-2xl text-white">
|
||||
{usage?.cpu?.usage}<span class="text-sm">%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat">
|
||||
<div class="stat-title">Load Average (5,10,30mins)</div>
|
||||
<div class="stat-value text-2xl">{usage?.cpu?.load}</div>
|
||||
<div class="stat-value text-2xl text-white">{usage?.cpu?.load}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stats stats-vertical min-w-[16rem] mb-5 bg-transparent rounded">
|
||||
<div class="stat">
|
||||
<div class="stat-title">Total Disk</div>
|
||||
<div class="stat-value text-2xl">
|
||||
<div class="stat-value text-2xl text-white">
|
||||
{usage?.disk?.totalGb}<span class="text-sm">GB</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat">
|
||||
<div class="stat-title">Used Disk</div>
|
||||
<div class="stat-value text-2xl">
|
||||
<div class="stat-value text-2xl text-white">
|
||||
{usage?.disk?.usedGb}<span class="text-sm">GB</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat">
|
||||
<div class="stat-title">Free Disk</div>
|
||||
<div class="stat-value text-2xl">
|
||||
<div class="stat-value text-2xl text-white">
|
||||
{usage?.disk?.freePercentage}<span class="text-sm">%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
7
apps/ui/src/lib/dayjs.ts
Normal file
7
apps/ui/src/lib/dayjs.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import dayjs from 'dayjs';
|
||||
import utc from 'dayjs/plugin/utc.js';
|
||||
import relativeTime from 'dayjs/plugin/relativeTime.js';
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(relativeTime);
|
||||
|
||||
export { dayjs as day };
|
@ -157,3 +157,5 @@ export const addToast = (toast: AddToast) => {
|
||||
if (t.timeout) t.timeoutInterval = setTimeout(() => dismissToast(id), t.timeout)
|
||||
toasts.update((all: any) => [t, ...all])
|
||||
}
|
||||
|
||||
export const selectedBuildId: any = writable(null)
|
@ -320,10 +320,12 @@
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
class="no-underline"
|
||||
class="no-underline icons hover:text-white"
|
||||
sveltekit:prefetch
|
||||
href="/"
|
||||
class:bg-primary={$page.url.pathname === '/'}
|
||||
class:text-pink-500={$page.url.pathname === '/'}
|
||||
class:bg-coolgray-500={$page.url.pathname === '/'}
|
||||
class:bg-coolgray-200={!($page.url.pathname === '/')}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@ -396,6 +398,7 @@
|
||||
</svg>
|
||||
IAM
|
||||
</a>
|
||||
<Tooltip triggeredBy="#iam" placement="right" color="bg-iam">IAM</Tooltip>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
@ -422,6 +425,9 @@
|
||||
</svg>
|
||||
Settings
|
||||
</a>
|
||||
<Tooltip triggeredBy="#settings" placement="right" color="bg-settings text-black"
|
||||
>Settings</Tooltip
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<div class="no-underline hover:bg-error" on:click={logout}>
|
||||
@ -443,12 +449,8 @@
|
||||
</svg>
|
||||
Logout
|
||||
</div>
|
||||
<Tooltip triggeredBy="#logout" placement="right" color="bg-red-600">Logout</Tooltip>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Tooltip triggeredBy="#iam" placement="right" color="bg-iam">IAM</Tooltip>
|
||||
<Tooltip triggeredBy="#settings" placement="right" color="bg-settings text-black">Settings</Tooltip
|
||||
>
|
||||
<Tooltip triggeredBy="#logout" placement="right" color="bg-red-600">Logout</Tooltip>
|
||||
|
@ -5,7 +5,6 @@
|
||||
export let isNewSecret = false;
|
||||
export let isPRMRSecret = false;
|
||||
export let PRMRSecret: any = {};
|
||||
|
||||
if (isPRMRSecret) value = PRMRSecret.value;
|
||||
|
||||
import { page } from '$app/stores';
|
||||
@ -39,7 +38,15 @@
|
||||
|
||||
async function createSecret(isNew: any) {
|
||||
try {
|
||||
if (isNew) {
|
||||
if (!name || !value) return;
|
||||
}
|
||||
if (value === undefined && isPRMRSecret) {
|
||||
return
|
||||
}
|
||||
if (value === '' && !isPRMRSecret) {
|
||||
throw new Error('Value is required.')
|
||||
}
|
||||
await saveSecret({
|
||||
isNew,
|
||||
name,
|
||||
@ -109,7 +116,6 @@
|
||||
name={isNewSecret ? 'secretValue' : 'secretValueNew'}
|
||||
isPasswordField={true}
|
||||
bind:value
|
||||
required
|
||||
placeholder="J$#@UIO%HO#$U%H"
|
||||
inputStyle="min-width: 350px; !important"
|
||||
/>
|
||||
|
@ -67,7 +67,8 @@
|
||||
setLocation,
|
||||
addToast,
|
||||
isDeploymentEnabled,
|
||||
checkIfDeploymentEnabledApplications
|
||||
checkIfDeploymentEnabledApplications,
|
||||
selectedBuildId
|
||||
} from '$lib/store';
|
||||
import { errorNotification, handlerNotFoundLoad } from '$lib/common';
|
||||
import Tooltip from '$lib/components/Tooltip.svelte';
|
||||
@ -89,13 +90,10 @@
|
||||
message: $t('application.deployment_queued'),
|
||||
type: 'success'
|
||||
});
|
||||
if ($page.url.pathname.startsWith(`/applications/${id}/logs/build`)) {
|
||||
return window.location.assign(`/applications/${id}/logs/build?buildId=${buildId}`);
|
||||
} else {
|
||||
$selectedBuildId = buildId;
|
||||
return await goto(`/applications/${id}/logs/build?buildId=${buildId}`, {
|
||||
replaceState: true
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
|
@ -1,6 +1,4 @@
|
||||
<script lang="ts">
|
||||
export let buildId: any;
|
||||
|
||||
import { createEventDispatcher, onDestroy, onMount } from 'svelte';
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
@ -11,6 +9,8 @@
|
||||
import LoadingLogs from '$lib/components/LoadingLogs.svelte';
|
||||
import { errorNotification } from '$lib/common';
|
||||
import Tooltip from '$lib/components/Tooltip.svelte';
|
||||
import { day } from '$lib/dayjs';
|
||||
import { selectedBuildId } from '$lib/store';
|
||||
|
||||
let logs: any = [];
|
||||
let currentStatus: any;
|
||||
@ -18,7 +18,7 @@
|
||||
let followingBuild: any;
|
||||
let followingInterval: any;
|
||||
let logsEl: any;
|
||||
|
||||
let fromDb = false;
|
||||
let cancelInprogress = false;
|
||||
|
||||
const { id } = $page.params;
|
||||
@ -38,13 +38,18 @@
|
||||
}
|
||||
async function streamLogs(sequence = 0) {
|
||||
try {
|
||||
let { logs: responseLogs, status } = await get(
|
||||
`/applications/${id}/logs/build/${buildId}?sequence=${sequence}`
|
||||
);
|
||||
let {
|
||||
logs: responseLogs,
|
||||
status,
|
||||
fromDb: from
|
||||
} = await get(`/applications/${id}/logs/build/${$selectedBuildId}?sequence=${sequence}`);
|
||||
|
||||
currentStatus = status;
|
||||
logs = logs.concat(
|
||||
responseLogs.map((log: any) => ({ ...log, line: cleanAnsiCodes(log.line) }))
|
||||
);
|
||||
fromDb = from;
|
||||
|
||||
streamInterval = setInterval(async () => {
|
||||
if (status !== 'running' && status !== 'queued') {
|
||||
clearInterval(streamInterval);
|
||||
@ -53,10 +58,11 @@
|
||||
const nextSequence = logs[logs.length - 1]?.time || 0;
|
||||
try {
|
||||
const data = await get(
|
||||
`/applications/${id}/logs/build/${buildId}?sequence=${nextSequence}`
|
||||
`/applications/${id}/logs/build/${$selectedBuildId}?sequence=${nextSequence}`
|
||||
);
|
||||
status = data.status;
|
||||
currentStatus = status;
|
||||
fromDb = data.fromDb;
|
||||
|
||||
logs = logs.concat(
|
||||
data.logs.map((log: any) => ({ ...log, line: cleanAnsiCodes(log.line) }))
|
||||
@ -75,7 +81,7 @@
|
||||
try {
|
||||
cancelInprogress = true;
|
||||
await post(`/applications/${id}/cancel`, {
|
||||
buildId,
|
||||
buildId: $selectedBuildId,
|
||||
applicationId: id
|
||||
});
|
||||
} catch (error) {
|
||||
@ -156,7 +162,11 @@
|
||||
{#if logs.length > 0}
|
||||
<div class="font-mono w-full rounder bg-coolgray-200 p-5 overflow-x-auto overflox-y-auto max-h-[80vh] rounded-md mb-20 flex flex-col whitespace-nowrap -mt-12 scrollbar-thumb-coollabs scrollbar-track-coolgray-200 scrollbar-w-1">
|
||||
{#each logs as log}
|
||||
<p>{log.line + '\n'}</p>
|
||||
{#if fromDb}
|
||||
<div>{log.line + '\n'}</div>
|
||||
{:else}
|
||||
<div>[{day.unix(log.time).format('HH:mm:ss.SSS')}] {log.line + '\n'}</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
|
@ -23,55 +23,45 @@
|
||||
export let application: any;
|
||||
export let buildCount: any;
|
||||
import { page } from '$app/stores';
|
||||
|
||||
import {addToast} from '$lib/store';
|
||||
import { addToast, selectedBuildId } from '$lib/store';
|
||||
import BuildLog from './_BuildLog.svelte';
|
||||
import { get, post } from '$lib/api';
|
||||
import { t } from '$lib/translations';
|
||||
import { changeQueryParams, dateOptions, errorNotification, asyncSleep } from '$lib/common';
|
||||
import Tooltip from '$lib/components/Tooltip.svelte';
|
||||
import { day } from '$lib/dayjs';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
const { id } = $page.params;
|
||||
|
||||
let buildId: any;
|
||||
let loadBuildLogsInterval: any = null;
|
||||
|
||||
let skip = 0;
|
||||
let noMoreBuilds = buildCount < 5 || buildCount <= skip;
|
||||
|
||||
let buildTook = 0;
|
||||
const { id } = $page.params;
|
||||
let preselectedBuildId = $page.url.searchParams.get('buildId');
|
||||
if (preselectedBuildId) buildId = preselectedBuildId;
|
||||
if (preselectedBuildId) $selectedBuildId = preselectedBuildId;
|
||||
|
||||
onMount(async () => {
|
||||
getBuildLogs();
|
||||
loadBuildLogsInterval = setInterval(() => {
|
||||
getBuildLogs();
|
||||
}, 2000);
|
||||
|
||||
async function updateBuildStatus({ detail }: { detail: any }) {
|
||||
const { status, took } = detail;
|
||||
if (status !== 'running') {
|
||||
try {
|
||||
const data = await get(`/applications/${id}/logs/build?buildId=${buildId}`);
|
||||
builds = builds.filter((build: any) => {
|
||||
if (build.id === data.builds[0].id) {
|
||||
build.status = data.builds[0].status;
|
||||
build.took = data.builds[0].took;
|
||||
build.since = data.builds[0].since;
|
||||
}
|
||||
return build;
|
||||
});
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
} else {
|
||||
builds = builds.filter((build: any) => {
|
||||
if (build.id === buildId) build.status = status;
|
||||
return build;
|
||||
onDestroy(() => {
|
||||
clearInterval(loadBuildLogsInterval);
|
||||
});
|
||||
buildTook = took;
|
||||
}
|
||||
async function getBuildLogs() {
|
||||
const response = await get(`/applications/${$page.params.id}/logs/build?skip=${skip}`);
|
||||
builds = response.builds;
|
||||
}
|
||||
|
||||
async function loadMoreBuilds() {
|
||||
if (buildCount >= skip) {
|
||||
skip = skip + 5;
|
||||
noMoreBuilds = buildCount >= skip;
|
||||
noMoreBuilds = buildCount <= skip;
|
||||
try {
|
||||
const data = await get(`/applications/${id}/logs/build?skip=${skip}`);
|
||||
builds = builds.concat(data.builds);
|
||||
builds = data.builds
|
||||
return;
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
@ -81,26 +71,40 @@ import {addToast} from '$lib/store';
|
||||
}
|
||||
}
|
||||
function loadBuild(build: any) {
|
||||
buildId = build;
|
||||
return changeQueryParams(buildId);
|
||||
$selectedBuildId = build;
|
||||
return changeQueryParams($selectedBuildId);
|
||||
}
|
||||
async function resetQueue() {
|
||||
const sure = confirm('It will reset all build queues for all applications. If something is queued, it will be canceled automatically. Are you sure? ');
|
||||
const sure = confirm(
|
||||
'It will reset all build queues for all applications. If something is queued, it will be canceled automatically. Are you sure? '
|
||||
);
|
||||
if (sure) {
|
||||
|
||||
try {
|
||||
await post(`/internal/resetQueue`, {});
|
||||
addToast({
|
||||
message: 'Queue reset done.',
|
||||
type: 'success'
|
||||
});
|
||||
await asyncSleep(500)
|
||||
return window.location.reload()
|
||||
await asyncSleep(500);
|
||||
return window.location.reload();
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
function generateBadgeColors(status: string) {
|
||||
if (status === 'failed') {
|
||||
return 'text-red-500';
|
||||
} else if (status === 'running') {
|
||||
return 'text-yellow-300';
|
||||
} else if (status === 'success') {
|
||||
return 'text-green-500';
|
||||
} else if (status === 'canceled') {
|
||||
return 'text-orange-500';
|
||||
} else {
|
||||
return 'text-white';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex items-center space-x-2 p-5 px-6 font-bold">
|
||||
@ -156,7 +160,9 @@ import {addToast} from '$lib/store';
|
||||
</div>
|
||||
<div class="block flex-row justify-start space-x-2 px-5 pt-6 sm:px-10 md:flex">
|
||||
<div class="mb-4 min-w-[16rem] space-y-2 md:mb-0 ">
|
||||
<button class="btn btn-sm text-xs w-full bg-error" on:click={resetQueue}>Reset Build Queue</button>
|
||||
<button class="btn btn-sm text-xs w-full bg-error" on:click={resetQueue}
|
||||
>Reset Build Queue</button
|
||||
>
|
||||
<div class="top-4 md:sticky">
|
||||
{#each builds as build, index (build.id)}
|
||||
<div
|
||||
@ -164,8 +170,8 @@ import {addToast} from '$lib/store';
|
||||
on:click={() => loadBuild(build.id)}
|
||||
class:rounded-tr={index === 0}
|
||||
class:rounded-br={index === builds.length - 1}
|
||||
class="flex cursor-pointer items-center justify-center py-4 no-underline transition-all duration-100 hover:bg-coolgray-400 hover:shadow-xl"
|
||||
class:bg-coolgray-400={buildId === build.id}
|
||||
class="flex cursor-pointer items-center justify-center py-4 no-underline transition-all duration-100 hover:bg-coolgray-300 hover:shadow-xl"
|
||||
class:bg-coolgray-200={$selectedBuildId === build.id}
|
||||
>
|
||||
<div class="flex-col px-2 text-center min-w-[10rem]">
|
||||
<div class="text-sm font-bold">
|
||||
@ -174,50 +180,55 @@ import {addToast} from '$lib/store';
|
||||
<div class="text-xs">
|
||||
{build.type}
|
||||
</div>
|
||||
<div class="badge badge-sm text-xs text-white uppercase rounded bg-coolgray-300 border-none font-bold"
|
||||
class:text-red-500={build.status === 'failed'}
|
||||
class:text-orange-500={build.status === 'canceled'}
|
||||
class:text-green-500={build.status === 'success'}
|
||||
class:text-yellow-500={build.status === 'running'}>{build.status}</div>
|
||||
<div
|
||||
class={`badge badge-sm text-xs uppercase rounded bg-coolgray-300 border-none font-bold ${generateBadgeColors(
|
||||
build.status
|
||||
)}`}
|
||||
>
|
||||
{build.status}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="w-48 text-center text-xs">
|
||||
{#if build.status === 'running'}
|
||||
<div class="font-bold">{$t('application.build.running')}</div>
|
||||
<div>
|
||||
Elapsed
|
||||
<span class="font-bold">{buildTook}s</span>
|
||||
<span class="font-bold text-xl"
|
||||
>{build.elapsed}s</span
|
||||
>
|
||||
</div>
|
||||
{:else if build.status === 'queued'}
|
||||
<div class="font-bold">{$t('application.build.queued')}</div>
|
||||
{:else}
|
||||
<div>{build.since}</div>
|
||||
{:else if build.status !== 'queued'}
|
||||
<div>{day(build.updatedAt).utc().fromNow()}</div>
|
||||
<div>
|
||||
{$t('application.build.finished_in')} <span class="font-bold">{build.took}s</span>
|
||||
{$t('application.build.finished_in')}
|
||||
<span class="font-bold"
|
||||
>{day(build.updatedAt).utc().diff(day(build.createdAt)) / 1000}s</span
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<Tooltip triggeredBy={`#building-${build.id}`}
|
||||
>{new Intl.DateTimeFormat('default', dateOptions).format(new Date(build.createdAt)) +
|
||||
`\n${build.status}`}</Tooltip
|
||||
`\n`}</Tooltip
|
||||
>
|
||||
{/each}
|
||||
</div>
|
||||
{#if !noMoreBuilds}
|
||||
{#if buildCount > 5}
|
||||
<div class="flex space-x-2">
|
||||
<button disabled={noMoreBuilds} class=" btn btn-sm w-full text-xs" on:click={loadMoreBuilds}
|
||||
>{$t('application.build.load_more')}</button
|
||||
<div class="flex space-x-2 pb-10">
|
||||
<button
|
||||
disabled={noMoreBuilds}
|
||||
class=" btn btn-sm w-full text-xs"
|
||||
on:click={loadMoreBuilds}>{$t('application.build.load_more')}</button
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex-1 md:w-96">
|
||||
{#if buildId}
|
||||
{#key buildId}
|
||||
<svelte:component this={BuildLog} {buildId} on:updateBuildStatus={updateBuildStatus} />
|
||||
{#if $selectedBuildId}
|
||||
{#key $selectedBuildId}
|
||||
<svelte:component this={BuildLog} />
|
||||
{/key}
|
||||
{/if}
|
||||
</div>
|
||||
|
@ -1,174 +0,0 @@
|
||||
<script context="module" lang="ts">
|
||||
import type { Load } from '@sveltejs/kit';
|
||||
export const load: Load = async ({ stuff, url }) => {
|
||||
try {
|
||||
return {
|
||||
props: {
|
||||
application: stuff.application
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
status: 500,
|
||||
error: new Error(`Could not load ${url}`)
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export let application: any;
|
||||
import Secret from './_Secret.svelte';
|
||||
import { get, post } from '$lib/api';
|
||||
import { page } from '$app/stores';
|
||||
import { t } from '$lib/translations';
|
||||
import { goto } from '$app/navigation';
|
||||
import { errorNotification, getDomain } from '$lib/common';
|
||||
import { onMount } from 'svelte';
|
||||
import Loading from '$lib/components/Loading.svelte';
|
||||
import { addToast } from '$lib/store';
|
||||
import SimpleExplainer from '$lib/components/SimpleExplainer.svelte';
|
||||
|
||||
const { id } = $page.params;
|
||||
|
||||
let containers: any;
|
||||
let PRMRSecrets: any;
|
||||
let applicationSecrets: any;
|
||||
let loading = {
|
||||
init: true,
|
||||
removing: false
|
||||
};
|
||||
async function refreshSecrets() {
|
||||
const data = await get(`/applications/${id}/secrets`);
|
||||
PRMRSecrets = [...data.secrets];
|
||||
}
|
||||
async function removeApplication(container: any) {
|
||||
try {
|
||||
loading.removing = true;
|
||||
await post(`/applications/${id}/stop/preview`, {
|
||||
pullmergeRequestId: container.pullmergeRequestId
|
||||
});
|
||||
return window.location.reload();
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
async function redeploy(container: any) {
|
||||
try {
|
||||
const { buildId } = await post(`/applications/${id}/deploy`, {
|
||||
pullmergeRequestId: container.pullmergeRequestId,
|
||||
branch: container.branch
|
||||
});
|
||||
addToast({
|
||||
message: 'Deployment queued',
|
||||
type: 'success'
|
||||
});
|
||||
if ($page.url.pathname.startsWith(`/applications/${id}/logs/build`)) {
|
||||
return window.location.assign(`/applications/${id}/logs/build?buildId=${buildId}`);
|
||||
} else {
|
||||
return await goto(`/applications/${id}/logs/build?buildId=${buildId}`, {
|
||||
replaceState: true
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
onMount(async () => {
|
||||
try {
|
||||
loading.init = true;
|
||||
const response = await get(`/applications/${id}/previews`);
|
||||
containers = response.containers;
|
||||
PRMRSecrets = response.PRMRSecrets;
|
||||
applicationSecrets = response.applicationSecrets;
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
} finally {
|
||||
loading.init = false;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if loading.init}
|
||||
<Loading />
|
||||
{:else}
|
||||
<div class="mx-auto max-w-6xl px-6 pt-4">
|
||||
<div class="flex flex-col justify-center py-4 text-center">
|
||||
<h1 class="md:max-w-64 truncate text-base tracking-tight md:text-2xl lg:block font-bold mb-4">
|
||||
Preview Deployments
|
||||
</h1>
|
||||
<SimpleExplainer
|
||||
customClass="w-full"
|
||||
text={applicationSecrets.length === 0
|
||||
? "You can add secrets to PR/MR deployments. Please add secrets to the application first. <br>Useful for creating <span class='text-green-500 font-bold'>staging</span> environments."
|
||||
: "These values overwrite application secrets in PR/MR deployments. <br>Useful for creating <span class='text-green-500 font-bold'>staging</span> environments."}
|
||||
/>
|
||||
</div>
|
||||
{#if applicationSecrets.length !== 0}
|
||||
<table class="mx-auto border-separate text-left">
|
||||
<thead>
|
||||
<tr class="h-12">
|
||||
<th scope="col">{$t('forms.name')}</th>
|
||||
<th scope="col">{$t('forms.value')}</th>
|
||||
<th scope="col" class="w-64 text-center"
|
||||
>{$t('application.preview.need_during_buildtime')}</th
|
||||
>
|
||||
<th scope="col" class="w-96 text-center">{$t('forms.action')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each applicationSecrets as secret}
|
||||
{#key secret.id}
|
||||
<tr>
|
||||
<Secret
|
||||
PRMRSecret={PRMRSecrets.find((s) => s.name === secret.name)}
|
||||
isPRMRSecret
|
||||
name={secret.name}
|
||||
value={secret.value}
|
||||
isBuildSecret={secret.isBuildSecret}
|
||||
on:refresh={refreshSecrets}
|
||||
/>
|
||||
</tr>
|
||||
{/key}
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="mx-auto max-w-4xl py-10">
|
||||
<div class="flex flex-wrap justify-center space-x-2">
|
||||
{#if containers.length > 0}
|
||||
{#each containers as container}
|
||||
<a href={container.fqdn} class="p-2 no-underline" target="_blank">
|
||||
<div class="box-selection text-center hover:border-transparent hover:bg-green-600">
|
||||
<div class="truncate text-center text-xl font-bold">{getDomain(container.fqdn)}</div>
|
||||
</div>
|
||||
</a>
|
||||
<div class="flex items-center justify-center">
|
||||
<button
|
||||
class="btn btn-sm bg-coollabs hover:bg-coollabs-100"
|
||||
on:click={() => redeploy(container)}>{$t('application.preview.redeploy')}</button
|
||||
>
|
||||
</div>
|
||||
<div class="flex items-center justify-center">
|
||||
<button
|
||||
class="btn btn-sm"
|
||||
class:bg-red-600={!loading.removing}
|
||||
class:hover:bg-red-500={!loading.removing}
|
||||
disabled={loading.removing}
|
||||
on:click={() => removeApplication(container)}
|
||||
>{loading.removing ? 'Removing...' : 'Remove Application'}
|
||||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
{:else}
|
||||
<div class="flex-col">
|
||||
<div class="text-center font-bold text-xl">
|
||||
{$t('application.preview.no_previews_available')}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
436
apps/ui/src/routes/applications/[id]/previews/index.svelte
Normal file
436
apps/ui/src/routes/applications/[id]/previews/index.svelte
Normal file
@ -0,0 +1,436 @@
|
||||
<script context="module" lang="ts">
|
||||
import type { Load } from '@sveltejs/kit';
|
||||
export const load: Load = async ({ fetch, params, stuff, url }) => {
|
||||
try {
|
||||
return {
|
||||
props: {
|
||||
application: stuff.application
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
status: 500,
|
||||
error: new Error(`Could not load ${url}`)
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export let application: any;
|
||||
import Secret from '../_Secret.svelte';
|
||||
import { get, post } from '$lib/api';
|
||||
import { page } from '$app/stores';
|
||||
import { t } from '$lib/translations';
|
||||
import { goto } from '$app/navigation';
|
||||
import { asyncSleep, errorNotification, getDomain, getRndInteger } from '$lib/common';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import { addToast } from '$lib/store';
|
||||
import SimpleExplainer from '$lib/components/SimpleExplainer.svelte';
|
||||
import Tooltip from '$lib/components/Tooltip.svelte';
|
||||
import DeleteIcon from '$lib/components/DeleteIcon.svelte';
|
||||
|
||||
const { id } = $page.params;
|
||||
let loadBuildingStatusInterval: any = null;
|
||||
let PRMRSecrets: any;
|
||||
let applicationSecrets: any;
|
||||
let loading = {
|
||||
init: true,
|
||||
restart: false,
|
||||
removing: false
|
||||
};
|
||||
let numberOfGetStatus = 0;
|
||||
let status: any = {};
|
||||
async function refreshSecrets() {
|
||||
const data = await get(`/applications/${id}/secrets`);
|
||||
PRMRSecrets = [...data.secrets];
|
||||
}
|
||||
async function removeApplication(preview: any) {
|
||||
try {
|
||||
loading.removing = true;
|
||||
await post(`/applications/${id}/stop/preview`, {
|
||||
pullmergeRequestId: preview.pullmergeRequestId
|
||||
});
|
||||
return window.location.reload();
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
async function redeploy(preview: any) {
|
||||
try {
|
||||
const { buildId } = await post(`/applications/${id}/deploy`, {
|
||||
pullmergeRequestId: preview.pullmergeRequestId,
|
||||
branch: preview.sourceBranch
|
||||
});
|
||||
addToast({
|
||||
message: 'Deployment queued',
|
||||
type: 'success'
|
||||
});
|
||||
if ($page.url.pathname.startsWith(`/applications/${id}/logs/build`)) {
|
||||
return window.location.assign(`/applications/${id}/logs/build?buildId=${buildId}`);
|
||||
} else {
|
||||
return await goto(`/applications/${id}/logs/build?buildId=${buildId}`, {
|
||||
replaceState: true
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
async function loadPreviewsFromDocker() {
|
||||
try {
|
||||
const { previews } = await post(`/applications/${id}/previews/load`, {});
|
||||
addToast({
|
||||
message: 'Previews loaded.',
|
||||
type: 'success'
|
||||
});
|
||||
application.previewApplication = previews;
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
async function getStatus(resources: any) {
|
||||
const { applicationId, pullmergeRequestId, id } = resources;
|
||||
if (status[id]) return status[id];
|
||||
while (numberOfGetStatus > 1) {
|
||||
await asyncSleep(getRndInteger(100, 200));
|
||||
}
|
||||
try {
|
||||
numberOfGetStatus++;
|
||||
let isRunning = false;
|
||||
let isBuilding = false;
|
||||
const response = await get(
|
||||
`/applications/${applicationId}/previews/${pullmergeRequestId}/status`
|
||||
);
|
||||
isRunning = response.isRunning;
|
||||
isBuilding = response.isBuilding;
|
||||
if (isBuilding) {
|
||||
status[id] = 'building';
|
||||
return 'building';
|
||||
} else if (isRunning) {
|
||||
status[id] = 'running';
|
||||
return 'running';
|
||||
} else {
|
||||
status[id] = 'stopped';
|
||||
return 'stopped';
|
||||
}
|
||||
} catch (error) {
|
||||
status[id] = 'error';
|
||||
return 'error';
|
||||
} finally {
|
||||
numberOfGetStatus--;
|
||||
status = status
|
||||
}
|
||||
}
|
||||
async function restartPreview(preview: any) {
|
||||
try {
|
||||
loading.restart = true;
|
||||
const { pullmergeRequestId } = preview;
|
||||
await post(`/applications/${id}/previews/${pullmergeRequestId}/restart`, {});
|
||||
addToast({
|
||||
type: 'success',
|
||||
message: 'Restart successful.'
|
||||
});
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
} finally {
|
||||
await getStatus(preview);
|
||||
loading.restart = false;
|
||||
}
|
||||
}
|
||||
onDestroy(() => {
|
||||
clearInterval(loadBuildingStatusInterval);
|
||||
});
|
||||
onMount(async () => {
|
||||
loadBuildingStatusInterval = setInterval(() => {
|
||||
application.previewApplication.forEach(async (preview: any) => {
|
||||
const { applicationId, pullmergeRequestId } = preview;
|
||||
if (status[preview.id] === 'building') {
|
||||
const response = await get(
|
||||
`/applications/${applicationId}/previews/${pullmergeRequestId}/status`
|
||||
);
|
||||
if (response.isBuilding) {
|
||||
status[preview.id] = 'building';
|
||||
} else if (response.isRunning) {
|
||||
status[preview.id] = 'running';
|
||||
return 'running';
|
||||
} else {
|
||||
status[preview.id] = 'stopped';
|
||||
return 'stopped';
|
||||
}
|
||||
}
|
||||
});
|
||||
}, 2000);
|
||||
try {
|
||||
loading.init = true;
|
||||
loading.restart = true;
|
||||
const response = await get(`/applications/${id}/previews`);
|
||||
PRMRSecrets = response.PRMRSecrets;
|
||||
applicationSecrets = response.applicationSecrets;
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
} finally {
|
||||
loading.init = false;
|
||||
loading.restart = false;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="flex items-center space-x-2 p-5 px-6 font-bold">
|
||||
<div class="-mb-5 flex-col">
|
||||
<div class="md:max-w-64 truncate text-base tracking-tight md:text-2xl lg:block">
|
||||
Preview Deployments
|
||||
</div>
|
||||
<span class="text-xs">{application?.name}</span>
|
||||
</div>
|
||||
{#if application.gitSource?.htmlUrl && application.repository && application.branch}
|
||||
<a
|
||||
href="{application.gitSource.htmlUrl}/{application.repository}/tree/{application.branch}"
|
||||
target="_blank"
|
||||
class="w-10"
|
||||
>
|
||||
{#if application.gitSource?.type === 'gitlab'}
|
||||
<svg viewBox="0 0 128 128" class="icons">
|
||||
<path
|
||||
fill="#FC6D26"
|
||||
d="M126.615 72.31l-7.034-21.647L105.64 7.76c-.716-2.206-3.84-2.206-4.556 0l-13.94 42.903H40.856L26.916 7.76c-.717-2.206-3.84-2.206-4.557 0L8.42 50.664 1.385 72.31a4.792 4.792 0 001.74 5.358L64 121.894l60.874-44.227a4.793 4.793 0 001.74-5.357"
|
||||
/><path fill="#E24329" d="M64 121.894l23.144-71.23H40.856L64 121.893z" /><path
|
||||
fill="#FC6D26"
|
||||
d="M64 121.894l-23.144-71.23H8.42L64 121.893z"
|
||||
/><path
|
||||
fill="#FCA326"
|
||||
d="M8.42 50.663L1.384 72.31a4.79 4.79 0 001.74 5.357L64 121.894 8.42 50.664z"
|
||||
/><path
|
||||
fill="#E24329"
|
||||
d="M8.42 50.663h32.436L26.916 7.76c-.717-2.206-3.84-2.206-4.557 0L8.42 50.664z"
|
||||
/><path fill="#FC6D26" d="M64 121.894l23.144-71.23h32.437L64 121.893z" /><path
|
||||
fill="#FCA326"
|
||||
d="M119.58 50.663l7.035 21.647a4.79 4.79 0 01-1.74 5.357L64 121.894l55.58-71.23z"
|
||||
/><path
|
||||
fill="#E24329"
|
||||
d="M119.58 50.663H87.145l13.94-42.902c.717-2.206 3.84-2.206 4.557 0l13.94 42.903z"
|
||||
/>
|
||||
</svg>
|
||||
{:else if application.gitSource?.type === 'github'}
|
||||
<svg viewBox="0 0 128 128" class="icons">
|
||||
<g fill="#ffffff"
|
||||
><path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M64 5.103c-33.347 0-60.388 27.035-60.388 60.388 0 26.682 17.303 49.317 41.297 57.303 3.017.56 4.125-1.31 4.125-2.905 0-1.44-.056-6.197-.082-11.243-16.8 3.653-20.345-7.125-20.345-7.125-2.747-6.98-6.705-8.836-6.705-8.836-5.48-3.748.413-3.67.413-3.67 6.063.425 9.257 6.223 9.257 6.223 5.386 9.23 14.127 6.562 17.573 5.02.542-3.903 2.107-6.568 3.834-8.076-13.413-1.525-27.514-6.704-27.514-29.843 0-6.593 2.36-11.98 6.223-16.21-.628-1.52-2.695-7.662.584-15.98 0 0 5.07-1.623 16.61 6.19C53.7 35 58.867 34.327 64 34.304c5.13.023 10.3.694 15.127 2.033 11.526-7.813 16.59-6.19 16.59-6.19 3.287 8.317 1.22 14.46.593 15.98 3.872 4.23 6.215 9.617 6.215 16.21 0 23.194-14.127 28.3-27.574 29.796 2.167 1.874 4.097 5.55 4.097 11.183 0 8.08-.07 14.583-.07 16.572 0 1.607 1.088 3.49 4.148 2.897 23.98-7.994 41.263-30.622 41.263-57.294C124.388 32.14 97.35 5.104 64 5.104z"
|
||||
/><path
|
||||
d="M26.484 91.806c-.133.3-.605.39-1.035.185-.44-.196-.685-.605-.543-.906.13-.31.603-.395 1.04-.188.44.197.69.61.537.91zm2.446 2.729c-.287.267-.85.143-1.232-.28-.396-.42-.47-.983-.177-1.254.298-.266.844-.14 1.24.28.394.426.472.984.17 1.255zM31.312 98.012c-.37.258-.976.017-1.35-.52-.37-.538-.37-1.183.01-1.44.373-.258.97-.025 1.35.507.368.545.368 1.19-.01 1.452zm3.261 3.361c-.33.365-1.036.267-1.552-.23-.527-.487-.674-1.18-.343-1.544.336-.366 1.045-.264 1.564.23.527.486.686 1.18.333 1.543zm4.5 1.951c-.147.473-.825.688-1.51.486-.683-.207-1.13-.76-.99-1.238.14-.477.823-.7 1.512-.485.683.206 1.13.756.988 1.237zm4.943.361c.017.498-.563.91-1.28.92-.723.017-1.308-.387-1.315-.877 0-.503.568-.91 1.29-.924.717-.013 1.306.387 1.306.88zm4.598-.782c.086.485-.413.984-1.126 1.117-.7.13-1.35-.172-1.44-.653-.086-.498.422-.997 1.122-1.126.714-.123 1.354.17 1.444.663zm0 0"
|
||||
/></g
|
||||
>
|
||||
</svg>
|
||||
{/if}
|
||||
</a>
|
||||
{/if}
|
||||
</div>
|
||||
{#if loading.init}
|
||||
<div class="mx-auto max-w-6xl px-6 pt-4">
|
||||
<div class="flex justify-center py-4 text-center text-xl font-bold">Loading...</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="mx-auto max-w-6xl px-6 pt-4">
|
||||
<div class="flex justify-center py-4 text-center">
|
||||
<SimpleExplainer
|
||||
customClass="w-full"
|
||||
text={applicationSecrets.length === 0
|
||||
? "You can add secrets to PR/MR deployments. Please add secrets to the application first. <br>Useful for creating <span class='text-green-500 font-bold'>staging</span> environments."
|
||||
: "These values overwrite application secrets in PR/MR deployments. <br>Useful for creating <span class='text-green-500 font-bold'>staging</span> environments."}
|
||||
/>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<SimpleExplainer
|
||||
customClass="w-full"
|
||||
text={'If your preview is not shown, try load them directly from Docker Engine.<br>(Changed previews process flow in <span class="font-bold text-white">v3.10.4</span>)'}
|
||||
/>
|
||||
<button class="btn btn-sm bg-coollabs" on:click={loadPreviewsFromDocker}
|
||||
>Fetch Previews</button
|
||||
>
|
||||
</div>
|
||||
{#if applicationSecrets.length !== 0}
|
||||
<table class="mx-auto border-separate text-left">
|
||||
<thead>
|
||||
<tr class="h-12">
|
||||
<th scope="col">{$t('forms.name')}</th>
|
||||
<th scope="col">{$t('forms.value')}</th>
|
||||
<th scope="col" class="w-64 text-center"
|
||||
>{$t('application.preview.need_during_buildtime')}</th
|
||||
>
|
||||
<th scope="col" class="w-96 text-center">{$t('forms.action')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each applicationSecrets as secret}
|
||||
{#key secret.id}
|
||||
<tr>
|
||||
<Secret
|
||||
PRMRSecret={PRMRSecrets.find((s) => s.name === secret.name)}
|
||||
isPRMRSecret
|
||||
name={secret.name}
|
||||
value={secret.value}
|
||||
isBuildSecret={secret.isBuildSecret}
|
||||
on:refresh={refreshSecrets}
|
||||
/>
|
||||
</tr>
|
||||
{/key}
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="container lg:mx-auto lg:p-0 px-8 p-5 lg:pt-10">
|
||||
{#if application.previewApplication.length > 0}
|
||||
<div
|
||||
class="grid grid-col gap-8 auto-cols-max grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 p-4"
|
||||
>
|
||||
{#each application.previewApplication as preview}
|
||||
<div class="no-underline mb-5">
|
||||
<div class="w-full rounded p-5 bg-coolgray-200 indicator">
|
||||
{#await getStatus(preview)}
|
||||
<span class="indicator-item badge bg-yellow-500 badge-sm" />
|
||||
{:then}
|
||||
{#if status[preview.id] === 'running'}
|
||||
<span class="indicator-item badge bg-success badge-sm" />
|
||||
{:else}
|
||||
<span class="indicator-item badge bg-error badge-sm" />
|
||||
{/if}
|
||||
{/await}
|
||||
<div class="w-full flex flex-row">
|
||||
<div class="w-full flex flex-col">
|
||||
<h1 class="font-bold text-lg lg:text-xl truncate">
|
||||
PR #{preview.pullmergeRequestId}
|
||||
{#if status[preview.id] === 'building'}
|
||||
<span
|
||||
class="badge badge-sm text-xs uppercase rounded bg-coolgray-300 text-green-500 border-none font-bold"
|
||||
>
|
||||
BUILDING
|
||||
</span>
|
||||
{/if}
|
||||
</h1>
|
||||
<div class="h-10 text-xs">
|
||||
<h2>{preview.customDomain.replace('https://', '').replace('http://', '')}</h2>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end items-end space-x-2 h-10">
|
||||
{#if preview.customDomain}
|
||||
<a id="openpreview" href={preview.customDomain} target="_blank" class="icons">
|
||||
<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="M11 7h-5a2 2 0 0 0 -2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2 -2v-5" />
|
||||
<line x1="10" y1="14" x2="20" y2="4" />
|
||||
<polyline points="15 4 20 4 20 9" />
|
||||
</svg>
|
||||
</a>
|
||||
{/if}
|
||||
<Tooltip triggeredBy="#openpreview">Open Preview</Tooltip>
|
||||
<div class="border border-coolgray-500 h-8" />
|
||||
{#if loading.restart}
|
||||
<button
|
||||
class="icons flex animate-spin items-center space-x-2 bg-transparent text-sm duration-500 ease-in-out hover:bg-transparent"
|
||||
>
|
||||
<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="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>
|
||||
</button>
|
||||
{:else}
|
||||
<button
|
||||
id="restart"
|
||||
on:click={() => restartPreview(preview)}
|
||||
type="submit"
|
||||
class="icons bg-transparent text-sm flex items-center space-x-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" />
|
||||
<path d="M20 11a8.1 8.1 0 0 0 -15.5 -2m-.5 -4v4h4" />
|
||||
<path d="M4 13a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4" />
|
||||
</svg>
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<Tooltip triggeredBy="#restart">Restart (useful to change secrets)</Tooltip>
|
||||
<button
|
||||
id="forceredeploypreview"
|
||||
class="icons"
|
||||
on:click={() => redeploy(preview)}
|
||||
>
|
||||
<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="M16.3 5h.7a2 2 0 0 1 2 2v10a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2 -2v-10a2 2 0 0 1 2 -2h5l-2.82 -2.82m0 5.64l2.82 -2.82"
|
||||
transform="rotate(-45 12 12)"
|
||||
/>
|
||||
</svg></button
|
||||
>
|
||||
<Tooltip triggeredBy="#forceredeploypreview"
|
||||
>Force redeploy (without cache)</Tooltip
|
||||
>
|
||||
<div class="border border-coolgray-500 h-8" />
|
||||
<button
|
||||
id="deletepreview"
|
||||
class="icons"
|
||||
class:hover:text-error={!loading.removing}
|
||||
disabled={loading.removing}
|
||||
on:click={() => removeApplication(preview)}
|
||||
><DeleteIcon />
|
||||
</button>
|
||||
<Tooltip triggeredBy="#deletepreview">Delete Preview</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex-col">
|
||||
<div class="text-center font-bold text-xl pb-10">Previews will shown here.</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
@ -22,7 +22,7 @@ export async function saveSecret({
|
||||
applicationId
|
||||
}: Props): Promise<void> {
|
||||
if (!name) return errorNotification(`${t.get('forms.name')} ${t.get('forms.is_required')}`);
|
||||
if (!value) return errorNotification(`${t.get('forms.value')} ${t.get('forms.is_required')}`);
|
||||
if (!value && isNew) return errorNotification(`${t.get('forms.value')} ${t.get('forms.is_required')}`);
|
||||
try {
|
||||
await post(`/applications/${applicationId}/secrets`, {
|
||||
name,
|
||||
|
@ -31,7 +31,7 @@
|
||||
import { get, post } from '$lib/api';
|
||||
import Usage from '$lib/components/Usage.svelte';
|
||||
import { t } from '$lib/translations';
|
||||
import { asyncSleep } from '$lib/common';
|
||||
import { asyncSleep, getRndInteger } from '$lib/common';
|
||||
import { appSession, search, addToast} from '$lib/store';
|
||||
|
||||
import ApplicationsIcons from '$lib/components/svg/applications/ApplicationIcons.svelte';
|
||||
@ -87,9 +87,7 @@
|
||||
filtered.destinations = [];
|
||||
filtered.otherDestinations = [];
|
||||
}
|
||||
function getRndInteger(min: number, max: number) {
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
}
|
||||
|
||||
|
||||
async function getStatus(resources: any) {
|
||||
const { id, buildPack, dualCerts } = resources;
|
||||
|
@ -28,11 +28,7 @@
|
||||
Cookies.set('token', token, {
|
||||
path: '/'
|
||||
});
|
||||
$appSession.teamId = payload.teamId;
|
||||
$appSession.userId = payload.userId;
|
||||
$appSession.permission = payload.permission;
|
||||
$appSession.isAdmin = payload.isAdmin;
|
||||
return await goto('/');
|
||||
return window.location.assign('/');
|
||||
} catch (error) {
|
||||
return errorNotification(error);
|
||||
} finally {
|
||||
|
@ -37,7 +37,7 @@
|
||||
<div class="grid grid-col gap-8 auto-cols-max grid-cols-1 p-4">
|
||||
{#each servers as server}
|
||||
<div class="no-underline mb-5">
|
||||
<div class="w-full rounded bg-coolgray-100 indicator">
|
||||
<div class="w-full rounded bg-coolgray-200 indicator">
|
||||
{#if $appSession.teamId === '0'}
|
||||
<Usage {server} />
|
||||
{/if}
|
||||
@ -49,4 +49,3 @@
|
||||
<h1 class="text-center text-xs">Nothing here.</h1>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="text-xs text-center">Remote servers will be here soon</div>
|
||||
|
@ -21,7 +21,9 @@
|
||||
name="scriptName"
|
||||
id="scriptName"
|
||||
readonly={!$appSession.isAdmin && !$status.service.isRunning}
|
||||
disabled={!$appSession.isAdmin || $status.service.isRunning}
|
||||
disabled={!$appSession.isAdmin ||
|
||||
$status.service.isRunning ||
|
||||
$status.service.initialLoading}
|
||||
placeholder="plausible.js"
|
||||
bind:value={service.plausibleAnalytics.scriptName}
|
||||
required
|
||||
@ -33,7 +35,9 @@
|
||||
class="w-full"
|
||||
name="email"
|
||||
id="email"
|
||||
disabled={readOnly}
|
||||
disabled={!$appSession.isAdmin ||
|
||||
$status.service.isRunning ||
|
||||
$status.service.initialLoading}
|
||||
readonly={readOnly}
|
||||
placeholder={$t('forms.email')}
|
||||
bind:value={service.plausibleAnalytics.email}
|
||||
@ -45,7 +49,9 @@
|
||||
<CopyPasswordField
|
||||
name="username"
|
||||
id="username"
|
||||
disabled={readOnly}
|
||||
disabled={!$appSession.isAdmin ||
|
||||
$status.service.isRunning ||
|
||||
$status.service.initialLoading}
|
||||
readonly={readOnly}
|
||||
placeholder={$t('forms.username')}
|
||||
bind:value={service.plausibleAnalytics.username}
|
||||
|
17
docker-compose-dev.yaml
Normal file
17
docker-compose-dev.yaml
Normal file
@ -0,0 +1,17 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
fluent-bit:
|
||||
image: coollabsio/coolify-fluent-bit:1.0.0
|
||||
command: /fluent-bit/bin/fluent-bit -c /fluent-bit/etc/fluent-bit-dev.conf
|
||||
container_name: coolify-fluentbit
|
||||
volumes:
|
||||
- ./logs:/logs
|
||||
ports:
|
||||
- "24224:24224"
|
||||
networks:
|
||||
- coolify-infra
|
||||
networks:
|
||||
coolify-infra:
|
||||
attachable: true
|
||||
name: coolify-infra
|
@ -12,6 +12,7 @@ services:
|
||||
mode: host
|
||||
volumes:
|
||||
- 'coolify-db:/app/db'
|
||||
- 'coolify-logs:/app/logs'
|
||||
- 'coolify-ssl-certs:/app/ssl'
|
||||
- 'coolify-traefik-letsencrypt:/etc/traefik/acme'
|
||||
- 'coolify-letsencrypt:/etc/letsencrypt'
|
||||
@ -20,15 +21,25 @@ services:
|
||||
- '.env'
|
||||
networks:
|
||||
- coolify-infra
|
||||
|
||||
fluent-bit:
|
||||
image: coollabsio/coolify-fluent-bit:1.0.0
|
||||
container_name: coolify-fluentbit
|
||||
volumes:
|
||||
- 'coolify-logs:/app/logs'
|
||||
networks:
|
||||
- coolify-infra
|
||||
networks:
|
||||
coolify-infra:
|
||||
attachable: true
|
||||
name: coolify-infra
|
||||
|
||||
volumes:
|
||||
coolify-logs:
|
||||
name: coolify-logs
|
||||
coolify-db:
|
||||
name: coolify-db
|
||||
coolify-pgdb:
|
||||
name: coolify-pgdb
|
||||
coolify-ssl-certs:
|
||||
name: coolify-ssl-certs
|
||||
coolify-letsencrypt:
|
||||
|
@ -1,35 +0,0 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
redis:
|
||||
image: redis:6.2-alpine
|
||||
container_name: coolify-redis
|
||||
networks:
|
||||
- coolify-infra
|
||||
ports:
|
||||
- target: 6379
|
||||
published: 6379
|
||||
protocol: tcp
|
||||
mode: host
|
||||
# fluentbit:
|
||||
# container_name: coolify-fluentbit
|
||||
# build:
|
||||
# context: ./data/fluentd
|
||||
# dockerfile: Dockerfile-dev
|
||||
# ports:
|
||||
# - target: 24224
|
||||
# published: 24224
|
||||
# protocol: tcp
|
||||
# mode: host
|
||||
# - target: 24224
|
||||
# published: 24224
|
||||
# protocol: udp
|
||||
# mode: host
|
||||
# networks:
|
||||
# - coolify-infra
|
||||
# extra_hosts:
|
||||
# - 'host.docker.internal:host-gateway'
|
||||
networks:
|
||||
coolify-infra:
|
||||
attachable: true
|
||||
name: coolify-infra
|
@ -1,29 +0,0 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
proxy:
|
||||
image: traefik:v2.6
|
||||
command:
|
||||
- --api.insecure=true
|
||||
- --entrypoints.web.address=:80
|
||||
- --entrypoints.websecure.address=:443
|
||||
- --providers.docker=false
|
||||
- --providers.docker.exposedbydefault=false
|
||||
- --providers.http.endpoint=http://host.docker.internal:3000/traefik.json
|
||||
- --providers.http.pollTimeout=5s
|
||||
- --log.level=error
|
||||
ports:
|
||||
- '80:80'
|
||||
- '443:443'
|
||||
- '8080:8080'
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
extra_hosts:
|
||||
- 'host.docker.internal:host-gateway'
|
||||
networks:
|
||||
- coolify-infra
|
||||
|
||||
networks:
|
||||
coolify-infra:
|
||||
attachable: true
|
||||
name: coolify-infra
|
4
others/fluentbit/Dockerfile
Normal file
4
others/fluentbit/Dockerfile
Normal file
@ -0,0 +1,4 @@
|
||||
FROM fluent/fluent-bit:1.9.8
|
||||
COPY ./fluent-bit.conf /fluent-bit/etc/fluent-bit.conf
|
||||
COPY ./fluent-bit-dev.conf /fluent-bit/etc/fluent-bit-dev.conf
|
||||
COPY ./parsers.conf /fluent-bit/etc/parsers.conf
|
30
others/fluentbit/fluent-bit-dev.conf
Normal file
30
others/fluentbit/fluent-bit-dev.conf
Normal file
@ -0,0 +1,30 @@
|
||||
[SERVICE]
|
||||
Parsers_file /fluent-bit/etc/parsers.conf
|
||||
Flush 1
|
||||
Grace 30
|
||||
[INPUT]
|
||||
Name http
|
||||
Host 0.0.0.0
|
||||
Port 24224
|
||||
[FILTER]
|
||||
Name parser
|
||||
Match *
|
||||
Key_Name log
|
||||
Parser jsonparser
|
||||
Reserve_Data True
|
||||
[OUTPUT]
|
||||
Name file
|
||||
Match *
|
||||
Path /logs
|
||||
Mkdir true
|
||||
Format csv
|
||||
# [OUTPUT]
|
||||
# Name influxdb
|
||||
# match *
|
||||
# Host coolify-influxdb
|
||||
# Port 8086
|
||||
# Database coolify
|
||||
# Bucket coolify
|
||||
# Org coolify
|
||||
# HTTP_Token 12345678
|
||||
# Sequence_Tag _seq
|
30
others/fluentbit/fluent-bit.conf
Normal file
30
others/fluentbit/fluent-bit.conf
Normal file
@ -0,0 +1,30 @@
|
||||
[SERVICE]
|
||||
Parsers_file /fluent-bit/etc/parsers.conf
|
||||
Flush 1
|
||||
Grace 30
|
||||
[INPUT]
|
||||
Name http
|
||||
Host 0.0.0.0
|
||||
Port 24224
|
||||
[FILTER]
|
||||
Name parser
|
||||
Match *
|
||||
Key_Name log
|
||||
Parser jsonparser
|
||||
Reserve_Data True
|
||||
[OUTPUT]
|
||||
Name file
|
||||
Match *
|
||||
Path /app/logs
|
||||
Mkdir true
|
||||
Format csv
|
||||
# [OUTPUT]
|
||||
# Name influxdb
|
||||
# match *
|
||||
# Host coolify-influxdb
|
||||
# Port 8086
|
||||
# Database coolify
|
||||
# Bucket coolify
|
||||
# Org coolify
|
||||
# HTTP_Token 12345678
|
||||
# Sequence_Tag _seq
|
6
others/fluentbit/parsers.conf
Normal file
6
others/fluentbit/parsers.conf
Normal file
@ -0,0 +1,6 @@
|
||||
[PARSER]
|
||||
Name jsonparser
|
||||
Format json
|
||||
Time_Key time
|
||||
Time_Format %Y-%m-%dT%H:%M:%S.%L
|
||||
Time_Keep On
|
@ -1,6 +0,0 @@
|
||||
FROM fluent/fluent-bit:1.9.0
|
||||
COPY fluentbit-dev.conf /tmp/fluentbit.conf
|
||||
ENTRYPOINT ["/fluent-bit/bin/fluent-bit", "-c", "/tmp/fluentbit.conf"]
|
||||
# USER root
|
||||
# RUN ["gem", "install", "fluent-plugin-mongo"]
|
||||
# USER fluent
|
@ -1,24 +0,0 @@
|
||||
[INPUT]
|
||||
Name forward
|
||||
Listen 0.0.0.0
|
||||
Port 24224
|
||||
Buffer_Chunk_Size 32KB
|
||||
Buffer_Max_Size 64KB
|
||||
|
||||
[OUTPUT]
|
||||
Name influxdb
|
||||
Match *
|
||||
Host coolify-influxdb
|
||||
Port 8086
|
||||
Bucket containerlogs
|
||||
Org organization
|
||||
HTTP_Token supertoken
|
||||
Sequence_Tag _seq
|
||||
Tag_Keys container_name
|
||||
[OUTPUT]
|
||||
Name http
|
||||
Match *
|
||||
Host host.docker.internal
|
||||
Port 3000
|
||||
URI /logs.json
|
||||
Format json
|
@ -1,28 +0,0 @@
|
||||
<source>
|
||||
@type forward
|
||||
port 24224
|
||||
bind 0.0.0.0
|
||||
</source>
|
||||
|
||||
<match **>
|
||||
@type http
|
||||
endpoint http://host.docker.internal:3000/logs.json
|
||||
<buffer>
|
||||
flush_at_shutdown true
|
||||
flush_mode immediate
|
||||
flush_thread_count 8
|
||||
flush_thread_interval 1
|
||||
flush_thread_burst_interval 1
|
||||
retry_forever true
|
||||
retry_type exponential_backoff
|
||||
</buffer>
|
||||
</match>
|
||||
|
||||
<filter docker.**>
|
||||
@type parser
|
||||
key_name log
|
||||
reserve_data true
|
||||
<parse>
|
||||
@type json
|
||||
</parse>
|
||||
</filter>
|
@ -1,23 +0,0 @@
|
||||
version: '3.5'
|
||||
|
||||
services:
|
||||
${ID}:
|
||||
container_name: proxy-for-${PORT}
|
||||
image: traefik:v2.6
|
||||
command:
|
||||
- --api.insecure=true
|
||||
- --entrypoints.web.address=:${PORT}
|
||||
- --providers.docker=false
|
||||
- --providers.docker.exposedbydefault=false
|
||||
- --providers.http.endpoint=http://host.docker.internal:3000/traefik.json?id=${ID}
|
||||
- --providers.http.pollTimeout=5s
|
||||
- --log.level=error
|
||||
ports:
|
||||
- '${PORT}:${PORT}'
|
||||
networks:
|
||||
- ${NETWORK}
|
||||
|
||||
networks:
|
||||
net:
|
||||
external: false
|
||||
name: ${NETWORK}
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "coolify",
|
||||
"description": "An open-source & self-hostable Heroku / Netlify alternative.",
|
||||
"version": "3.10.3",
|
||||
"version": "3.10.4",
|
||||
"license": "Apache-2.0",
|
||||
"repository": "github:coollabsio/coolify",
|
||||
"scripts": {
|
||||
|
@ -33,6 +33,8 @@ importers:
|
||||
bree: 9.1.2
|
||||
cabin: 9.1.2
|
||||
compare-versions: 5.0.1
|
||||
csv-parse: ^5.3.0
|
||||
csvtojson: ^2.0.10
|
||||
cuid: 2.1.8
|
||||
dayjs: 1.11.5
|
||||
dockerode: 3.3.4
|
||||
@ -65,7 +67,7 @@ importers:
|
||||
typescript: 4.8.2
|
||||
unique-names-generator: 4.7.1
|
||||
dependencies:
|
||||
'@breejs/ts-worker': 2.0.0_d3un4r7p64mpe4ydkpns6lvpxy
|
||||
'@breejs/ts-worker': 2.0.0_zx7xfusupi724hd5vcuaoj6jni
|
||||
'@fastify/autoload': 5.3.1
|
||||
'@fastify/cookie': 8.1.0
|
||||
'@fastify/cors': 8.1.0
|
||||
@ -80,6 +82,8 @@ importers:
|
||||
bree: 9.1.2
|
||||
cabin: 9.1.2
|
||||
compare-versions: 5.0.1
|
||||
csv-parse: 5.3.0
|
||||
csvtojson: 2.0.10
|
||||
cuid: 2.1.8
|
||||
dayjs: 1.11.5
|
||||
dockerode: 3.3.4
|
||||
@ -144,6 +148,7 @@ importers:
|
||||
classnames: 2.3.1
|
||||
cuid: 2.1.8
|
||||
daisyui: 2.24.2
|
||||
dayjs: 1.11.5
|
||||
eslint: 8.23.0
|
||||
eslint-config-prettier: 8.5.0
|
||||
eslint-plugin-svelte3: 4.0.0
|
||||
@ -169,6 +174,7 @@ importers:
|
||||
'@tailwindcss/typography': 0.5.7_tailwindcss@3.1.8
|
||||
cuid: 2.1.8
|
||||
daisyui: 2.24.2_25hquoklqeoqwmt7fwvvcyxm5e
|
||||
dayjs: 1.11.5
|
||||
js-cookie: 3.0.1
|
||||
p-limit: 4.0.0
|
||||
svelte-select: 4.4.7
|
||||
@ -239,11 +245,12 @@ packages:
|
||||
engines: {node: '>= 10'}
|
||||
dev: false
|
||||
|
||||
/@breejs/ts-worker/2.0.0_d3un4r7p64mpe4ydkpns6lvpxy:
|
||||
/@breejs/ts-worker/2.0.0_zx7xfusupi724hd5vcuaoj6jni:
|
||||
resolution: {integrity: sha512-6anHRcmgYlF7mrm/YVRn6rx2cegLuiY3VBxkkimOTWC/dVQeH336imVSuIKEGKTwiuNTPr2hswVdDSneNuXg3A==}
|
||||
engines: {node: '>= 12.11'}
|
||||
peerDependencies:
|
||||
bree: '>=9.0.0'
|
||||
tsconfig-paths: '>= 4'
|
||||
dependencies:
|
||||
bree: 9.1.2
|
||||
ts-node: 10.8.2_r4hqq7vrw4pxsipnb7ha25ylfe
|
||||
@ -1868,6 +1875,10 @@ packages:
|
||||
readable-stream: 3.6.0
|
||||
dev: false
|
||||
|
||||
/bluebird/3.7.2:
|
||||
resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==}
|
||||
dev: false
|
||||
|
||||
/bn.js/4.12.0:
|
||||
resolution: {integrity: sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==}
|
||||
dev: false
|
||||
@ -2290,6 +2301,20 @@ packages:
|
||||
engines: {node: '>=4'}
|
||||
hasBin: true
|
||||
|
||||
/csv-parse/5.3.0:
|
||||
resolution: {integrity: sha512-UXJCGwvJ2fep39purtAn27OUYmxB1JQto+zhZ4QlJpzsirtSFbzLvip1aIgziqNdZp/TptvsKEV5BZSxe10/DQ==}
|
||||
dev: false
|
||||
|
||||
/csvtojson/2.0.10:
|
||||
resolution: {integrity: sha512-lUWFxGKyhraKCW8Qghz6Z0f2l/PqB1W3AO0HKJzGIQ5JRSlR651ekJDiGJbBT4sRNNv5ddnSGVEnsxP9XRCVpQ==}
|
||||
engines: {node: '>=4.0.0'}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
bluebird: 3.7.2
|
||||
lodash: 4.17.21
|
||||
strip-bom: 2.0.0
|
||||
dev: false
|
||||
|
||||
/cuid/2.1.8:
|
||||
resolution: {integrity: sha512-xiEMER6E7TlTPnDxrM4eRiC6TRgjNX9xzEZ5U/Se2YJKr7Mq4pJn/2XEHjl3STcSh96GmkHPcBXLES8M29wyyg==}
|
||||
dev: false
|
||||
@ -3897,6 +3922,10 @@ packages:
|
||||
has-symbols: 1.0.3
|
||||
dev: true
|
||||
|
||||
/is-utf8/0.2.1:
|
||||
resolution: {integrity: sha512-rMYPYvCzsXywIsldgLaSoPlw5PfoB/ssr7hY4pLfcodrA5M/eArza1a9VmTiNIBNMjOGr1Ow9mTyU2o69U6U9Q==}
|
||||
dev: false
|
||||
|
||||
/is-uuid/1.0.2:
|
||||
resolution: {integrity: sha512-tCByphFcJgf2qmiMo5hMCgNAquNSagOetVetDvBXswGkNfoyEMvGH1yDlF8cbZbKnbVBr4Y5/rlpMz9umxyBkQ==}
|
||||
dev: false
|
||||
@ -5593,6 +5622,13 @@ packages:
|
||||
ansi-regex: 6.0.1
|
||||
dev: false
|
||||
|
||||
/strip-bom/2.0.0:
|
||||
resolution: {integrity: sha512-kwrX1y7czp1E69n2ajbG65mIo9dqvJ+8aBQXOGVxqwvNbsXdFM6Lq37dLAY3mknUwru8CfcCbfOLL/gMo+fi3g==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
dependencies:
|
||||
is-utf8: 0.2.1
|
||||
dev: false
|
||||
|
||||
/strip-bom/3.0.0:
|
||||
resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==}
|
||||
engines: {node: '>=4'}
|
||||
|
Loading…
Reference in New Issue
Block a user