mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-06-18 21:55:24 +02:00
Compare commits
35 Commits
dosu/doc-u
...
dosu/doc-u
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
10c6b2bd73 | ||
|
|
290a03ccfb | ||
|
|
63aa60f7e2 | ||
|
|
fe9b0ebcea | ||
|
|
8ccdb66ced | ||
|
|
e38f07d286 | ||
|
|
035d39e3b7 | ||
|
|
82a908a865 | ||
|
|
4bbb2ece49 | ||
|
|
ddfcd1a671 | ||
|
|
401b177a4e | ||
|
|
88b56ca0a2 | ||
|
|
3d48b25f71 | ||
|
|
b7e30d7ec3 | ||
|
|
b1ef5dc2c6 | ||
|
|
3846e41d7f | ||
|
|
ac76f2d97a | ||
|
|
d6056972f4 | ||
|
|
58b9a0d3d0 | ||
|
|
fe78f282f8 | ||
|
|
4941a80b50 | ||
|
|
5ea2ee5dcd | ||
|
|
76d6de5337 | ||
|
|
3374737db6 | ||
|
|
27a67af190 | ||
|
|
7e6a7d2cd4 | ||
|
|
4f5f1ad841 | ||
|
|
cbbf7f3a6d | ||
|
|
68f6d4a558 | ||
|
|
970905198b | ||
|
|
a0c87358eb | ||
|
|
8ee38a1463 | ||
|
|
e726bf31f6 | ||
|
|
f4248760a8 | ||
|
|
b715e21236 |
1
.github/workflows/pr-quality.yml
vendored
1
.github/workflows/pr-quality.yml
vendored
@@ -16,7 +16,6 @@ jobs:
|
||||
steps:
|
||||
- uses: peakoss/anti-slop@v0
|
||||
with:
|
||||
max-failures: 4
|
||||
blocked-commit-authors: "claude,copilot"
|
||||
require-description: true
|
||||
min-account-age: 5
|
||||
|
||||
@@ -62,22 +62,6 @@ pnpm install
|
||||
cp apps/dokploy/.env.example apps/dokploy/.env
|
||||
```
|
||||
|
||||
### Optional Docker Configuration
|
||||
|
||||
Docker socket detection is automatic for local development. The system automatically detects and uses Docker sockets in the following order:
|
||||
|
||||
- DOCKER_HOST environment variable (if set)
|
||||
- Rancher Desktop socket (~/.rd/docker.sock)
|
||||
- Standard Docker socket (/var/run/docker.sock)
|
||||
|
||||
Contributors using Docker Desktop, Rancher Desktop, Colima, or other Docker alternatives can run `pnpm run dokploy:setup` without any additional configuration.
|
||||
|
||||
The following environment variables are only needed for remote Docker host configurations:
|
||||
|
||||
- **DOKPLOY_DOCKER_HOST**: Specify a remote Docker daemon host
|
||||
- **DOKPLOY_DOCKER_PORT**: Specify a remote Docker daemon port
|
||||
- **DOKPLOY_DOCKER_API_VERSION**: Specify which Docker API version to use (optional)
|
||||
|
||||
## Requirements
|
||||
|
||||
- [Docker](/GUIDES.md#docker)
|
||||
@@ -115,7 +99,14 @@ pnpm run dokploy:build
|
||||
|
||||
## Docker
|
||||
|
||||
To build the docker image
|
||||
To build the docker image first run commands to copy .env files
|
||||
|
||||
```bash
|
||||
cp apps/dokploy/.env.production.example .env.production
|
||||
cp apps/dokploy/.env.production.example apps/dokploy/.env.production
|
||||
```
|
||||
|
||||
then run build command
|
||||
|
||||
```bash
|
||||
pnpm run docker:build
|
||||
@@ -187,11 +178,6 @@ curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.39.1/pack-v0.
|
||||
- **Issue Association:** For any significant change, it's highly recommended to open an issue first to discuss the proposed solution with the community and maintainers. This ensures alignment and avoids duplicated effort. If your PR resolves an existing issue, please link it in the description (e.g., `Fixes #123`, `Closes #456`).
|
||||
- **Large Features:** Pull Requests that introduce very large or broad features **will not be accepted** unless the idea is first outlined and discussed in a GitHub issue. Large features should be designed together with the Dokploy team so the project stays coherent and moves in the same direction. Open an issue to propose and align on the design before implementing.
|
||||
|
||||
### Pull Request Guidelines
|
||||
|
||||
- **Keep PRs small and focused.** Avoid very large PRs; prefer several smaller PRs (e.g., one template or one logical change per PR). This speeds up review and keeps the history clear.
|
||||
- **Test before submitting.** Any PR that has not been tested by the contributor will be closed. This keeps the PR queue tidy and ensures that only contributions that have been verified by their authors are considered.
|
||||
|
||||
Thank you for your contribution!
|
||||
|
||||
## Templates
|
||||
|
||||
@@ -20,7 +20,7 @@ Dokploy includes multiple features to make your life easier.
|
||||
|
||||
- **Applications**: Deploy any type of application (Node.js, PHP, Python, Go, Ruby, etc.).
|
||||
- **Databases**: Create and manage databases with support for MySQL, PostgreSQL, MongoDB, MariaDB, libsql, and Redis.
|
||||
- **Backups**: Automate backups for databases to an external storage destination.
|
||||
- **Backups**: Automate backups for databases to external storage destinations (S3, SFTP, FTP, Google Drive).
|
||||
- **Docker Compose**: Native support for Docker Compose to manage complex applications.
|
||||
- **Multi Node**: Scale applications to multiple nodes using Docker Swarm to manage the cluster.
|
||||
- **Templates**: Deploy open-source templates (Plausible, Pocketbase, Calcom, etc.) with a single click.
|
||||
|
||||
@@ -6,401 +6,3 @@ npm run dev
|
||||
```
|
||||
open http://localhost:3000
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
The API server requires the following environment variables for configuration:
|
||||
|
||||
### Inngest Configuration
|
||||
|
||||
Required for the GET /jobs endpoint to list deployment jobs:
|
||||
|
||||
- **INNGEST_BASE_URL** - The base URL for the Inngest instance
|
||||
- Self-hosted: `http://localhost:8288`
|
||||
- Production: `https://dev-inngest.dokploy.com`
|
||||
|
||||
- **INNGEST_SIGNING_KEY** - The signing key for authenticating with Inngest
|
||||
|
||||
Optional configuration for filtering and pagination:
|
||||
|
||||
- **INNGEST_EVENTS_RECEIVED_AFTER** (optional) - An RFC3339 timestamp to filter events received after a specific date (e.g., `2024-01-01T00:00:00Z`). If unset, no date filter is applied.
|
||||
|
||||
- **INNGEST_JOBS_MAX_EVENTS** (optional) - Maximum number of events to fetch when listing jobs. Default is 100, maximum is 10000. Used for pagination with cursor.
|
||||
|
||||
### Lemon Squeezy Integration
|
||||
|
||||
- **LEMON_SQUEEZY_API_KEY** - API key for Lemon Squeezy integration
|
||||
- **LEMON_SQUEEZY_STORE_ID** - Store ID for Lemon Squeezy integration
|
||||
|
||||
### Docker Configuration
|
||||
|
||||
Dokploy automatically detects Docker sockets in the following priority order:
|
||||
|
||||
1. **DOCKER_HOST** environment variable (if set)
|
||||
2. Rancher Desktop socket (`~/.rd/docker.sock`)
|
||||
3. Standard Docker socket (`/var/run/docker.sock`)
|
||||
|
||||
This automatic detection means that Docker Desktop, Rancher Desktop, Colima, and other Docker alternatives work out-of-the-box without manual configuration.
|
||||
|
||||
**Optional Environment Variables:**
|
||||
|
||||
- **DOCKER_HOST** (optional) - Specifies a custom Docker socket path (e.g., `unix:///path/to/docker.sock`). When set, this takes priority over automatic socket detection.
|
||||
|
||||
- **DOCKER_API_VERSION** (optional) - Specifies which Docker API version to use when connecting to the Docker daemon. If not set, the Docker client uses the default API version.
|
||||
|
||||
**Remote Docker Host Configuration:**
|
||||
|
||||
For connecting to remote Docker daemons, use the following variables:
|
||||
|
||||
- **DOKPLOY_DOCKER_HOST** (optional) - Specifies the remote Docker daemon host to connect to (e.g., `tcp://remote-host`).
|
||||
|
||||
- **DOKPLOY_DOCKER_PORT** (optional) - Specifies the port for connecting to the remote Docker daemon.
|
||||
|
||||
Note: `DOKPLOY_DOCKER_HOST` and `DOKPLOY_DOCKER_PORT` are intended for remote Docker host configurations. For local Docker installations, the automatic socket detection handles connection setup without requiring these variables.
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### GET /jobs
|
||||
|
||||
Lists deployment jobs (Inngest runs) for a specified server.
|
||||
|
||||
**Query Parameters:**
|
||||
- `serverId` (required) - The ID of the server to list deployment jobs for
|
||||
|
||||
**Response:**
|
||||
Returns an array of deployment job objects with the same shape as BullMQ queue jobs:
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "string",
|
||||
"name": "string",
|
||||
"data": {},
|
||||
"timestamp": 0,
|
||||
"processedOn": 0,
|
||||
"finishedOn": 0,
|
||||
"failedReason": "string",
|
||||
"state": "string"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
**Error Responses:**
|
||||
- `400` - serverId is not provided
|
||||
- `503` - INNGEST_BASE_URL is not configured
|
||||
- `200` - Empty array on other errors
|
||||
|
||||
This endpoint is used by the UI to display deployment queue information in the dashboard.
|
||||
|
||||
### POST /drop-deployment
|
||||
|
||||
Upload and deploy application code via ZIP file.
|
||||
|
||||
**Content-Type:** `multipart/form-data`
|
||||
|
||||
**Form Fields:**
|
||||
- `applicationId` (required) - The ID of the application to deploy
|
||||
- `zip` (required) - A ZIP file containing the application code
|
||||
- `dropBuildPath` (optional) - Custom build path within the ZIP file
|
||||
|
||||
**Response:**
|
||||
Initiates a deployment using the uploaded ZIP file.
|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
curl -X POST https://your-dokploy-instance.com/api/drop-deployment \
|
||||
-H "Authorization: Bearer YOUR_API_TOKEN" \
|
||||
-F "applicationId=YOUR_APP_ID" \
|
||||
-F "zip=@/path/to/your/app.zip" \
|
||||
-F "dropBuildPath=optional/build/path"
|
||||
```
|
||||
|
||||
## Search Endpoints
|
||||
|
||||
The following search endpoints provide flexible querying capabilities with pagination support. All search endpoints respect member permissions, returning only resources the user has access to.
|
||||
|
||||
### application.search
|
||||
|
||||
Search applications across name, appName, description, repository, owner, and dockerImage fields.
|
||||
|
||||
**Query Parameters:**
|
||||
- `q` (optional string) - General search term that searches across name, appName, description, repository, owner, and dockerImage
|
||||
- `name` (optional string) - Filter by application name
|
||||
- `appName` (optional string) - Filter by app name
|
||||
- `description` (optional string) - Filter by description
|
||||
- `repository` (optional string) - Filter by repository
|
||||
- `owner` (optional string) - Filter by owner
|
||||
- `dockerImage` (optional string) - Filter by Docker image
|
||||
- `projectId` (optional string) - Filter by project ID
|
||||
- `environmentId` (optional string) - Filter by environment ID
|
||||
- `limit` (number, default 20, min 1, max 100) - Maximum number of results
|
||||
- `offset` (number, default 0, min 0) - Pagination offset
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"applicationId": "string",
|
||||
"name": "string",
|
||||
"appName": "string",
|
||||
"description": "string",
|
||||
"environmentId": "string",
|
||||
"applicationStatus": "string",
|
||||
"sourceType": "string",
|
||||
"createdAt": "string"
|
||||
}
|
||||
],
|
||||
"total": 0
|
||||
}
|
||||
```
|
||||
|
||||
### compose.search
|
||||
|
||||
Search compose services with filtering by name, appName, and description.
|
||||
|
||||
**Query Parameters:**
|
||||
- `q` (optional string) - General search term across name, appName, description
|
||||
- `name` (optional string) - Filter by name
|
||||
- `appName` (optional string) - Filter by app name
|
||||
- `description` (optional string) - Filter by description
|
||||
- `projectId` (optional string) - Filter by project ID
|
||||
- `environmentId` (optional string) - Filter by environment ID
|
||||
- `limit` (number, default 20, min 1, max 100) - Maximum results
|
||||
- `offset` (number, default 0, min 0) - Pagination offset
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"composeId": "string",
|
||||
"name": "string",
|
||||
"appName": "string",
|
||||
"description": "string",
|
||||
"environmentId": "string",
|
||||
"composeStatus": "string",
|
||||
"sourceType": "string",
|
||||
"createdAt": "string"
|
||||
}
|
||||
],
|
||||
"total": 0
|
||||
}
|
||||
```
|
||||
|
||||
### environment.search
|
||||
|
||||
Search environments by name and description.
|
||||
|
||||
**Query Parameters:**
|
||||
- `q` (optional string) - General search term across name and description
|
||||
- `name` (optional string) - Filter by name
|
||||
- `description` (optional string) - Filter by description
|
||||
- `projectId` (optional string) - Filter by project ID
|
||||
- `limit` (number, default 20, min 1, max 100) - Maximum results
|
||||
- `offset` (number, default 0, min 0) - Pagination offset
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"environmentId": "string",
|
||||
"name": "string",
|
||||
"description": "string",
|
||||
"createdAt": "string",
|
||||
"env": "string",
|
||||
"projectId": "string",
|
||||
"isDefault": true
|
||||
}
|
||||
],
|
||||
"total": 0
|
||||
}
|
||||
```
|
||||
|
||||
### project.search
|
||||
|
||||
Search projects by name and description.
|
||||
|
||||
**Query Parameters:**
|
||||
- `q` (optional string) - General search term across name and description
|
||||
- `name` (optional string) - Filter by name
|
||||
- `description` (optional string) - Filter by description
|
||||
- `limit` (number, default 20, min 1, max 100) - Maximum results
|
||||
- `offset` (number, default 0, min 0) - Pagination offset
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"projectId": "string",
|
||||
"name": "string",
|
||||
"description": "string",
|
||||
"createdAt": "string",
|
||||
"organizationId": "string",
|
||||
"env": "string"
|
||||
}
|
||||
],
|
||||
"total": 0
|
||||
}
|
||||
```
|
||||
|
||||
### Database Service Search Endpoints
|
||||
|
||||
The following database services all share the same search interface:
|
||||
- **postgres.search**
|
||||
- **mysql.search**
|
||||
- **mariadb.search**
|
||||
- **mongo.search**
|
||||
- **redis.search**
|
||||
|
||||
**Query Parameters:**
|
||||
- `q` (optional string) - General search term across name, appName, description
|
||||
- `name` (optional string) - Filter by name
|
||||
- `appName` (optional string) - Filter by app name
|
||||
- `description` (optional string) - Filter by description
|
||||
- `projectId` (optional string) - Filter by project ID
|
||||
- `environmentId` (optional string) - Filter by environment ID
|
||||
- `limit` (number, default 20, min 1, max 100) - Maximum results
|
||||
- `offset` (number, default 0, min 0) - Pagination offset
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"postgresId": "string",
|
||||
"name": "string",
|
||||
"appName": "string",
|
||||
"description": "string",
|
||||
"environmentId": "string",
|
||||
"applicationStatus": "string",
|
||||
"createdAt": "string"
|
||||
}
|
||||
],
|
||||
"total": 0
|
||||
}
|
||||
```
|
||||
|
||||
*Note: The response shape is similar across all database services, with the ID field varying (e.g., `mysqlId`, `mariadbId`, `mongoId`, `redisId`).*
|
||||
|
||||
**Search Behavior:**
|
||||
- All searches use case-insensitive pattern matching with wildcards
|
||||
- Results are ordered by creation date (descending)
|
||||
- Members only see services they have access to
|
||||
- Returns total count for pagination UI
|
||||
|
||||
## Database Service Update Endpoints
|
||||
|
||||
All database services support update operations with flexible configuration options. The following database services share a common update interface:
|
||||
- **postgres.update** (apiUpdatePostgres)
|
||||
- **mysql.update** (apiUpdateMySql)
|
||||
- **mariadb.update** (apiUpdateMariaDB)
|
||||
- **mongo.update** (apiUpdateMongo)
|
||||
- **redis.update** (apiUpdateRedis)
|
||||
|
||||
**Common Parameters:**
|
||||
|
||||
All database update endpoints accept their respective ID field (e.g., `postgresId`, `mysqlId`, `mariadbId`, `mongoId`, `redisId`) as a required parameter, along with optional configuration fields.
|
||||
|
||||
**Optional Configuration:**
|
||||
- `dockerImage` (optional string) - Specifies a custom Docker image for the database service. This allows users to use specific versions or custom-built images instead of the default image for the database type. Available for all five database services (PostgreSQL, MySQL, MariaDB, MongoDB, and Redis).
|
||||
|
||||
Additional service-specific parameters are available depending on the database type. The `dockerImage` field provides enhanced configuration flexibility for advanced use cases such as version pinning or using specialized database distributions.
|
||||
|
||||
## Whitelabeling Endpoints
|
||||
|
||||
The whitelabeling endpoints allow enterprise/self-hosted Dokploy instances to customize branding, logos, colors, and UI appearance. These endpoints are only available in self-hosted mode (not cloud).
|
||||
|
||||
### whitelabeling.get
|
||||
|
||||
Get the current whitelabeling configuration.
|
||||
|
||||
**Requirements:**
|
||||
- Enterprise license required
|
||||
- Only available for self-hosted (not cloud)
|
||||
|
||||
**Response:**
|
||||
Returns the whitelabeling configuration object or null if not configured.
|
||||
|
||||
```json
|
||||
{
|
||||
"appName": "string | null",
|
||||
"appDescription": "string | null",
|
||||
"logoUrl": "string | null",
|
||||
"faviconUrl": "string | null",
|
||||
"primaryColor": "string | null",
|
||||
"customCss": "string | null",
|
||||
"loginLogoUrl": "string | null",
|
||||
"supportUrl": "string | null",
|
||||
"docsUrl": "string | null",
|
||||
"errorPageTitle": "string | null",
|
||||
"errorPageDescription": "string | null",
|
||||
"metaTitle": "string | null",
|
||||
"footerText": "string | null"
|
||||
}
|
||||
```
|
||||
|
||||
### whitelabeling.update
|
||||
|
||||
Update the whitelabeling configuration.
|
||||
|
||||
**Requirements:**
|
||||
- Enterprise license required
|
||||
- Owner role required
|
||||
- Only available for self-hosted (not cloud)
|
||||
|
||||
**Input:**
|
||||
```json
|
||||
{
|
||||
"whitelabelingConfig": {
|
||||
"appName": "string | null",
|
||||
"appDescription": "string | null",
|
||||
"logoUrl": "string | null",
|
||||
"faviconUrl": "string | null",
|
||||
"primaryColor": "string | null",
|
||||
"customCss": "string | null",
|
||||
"loginLogoUrl": "string | null",
|
||||
"supportUrl": "string | null",
|
||||
"docsUrl": "string | null",
|
||||
"errorPageTitle": "string | null",
|
||||
"errorPageDescription": "string | null",
|
||||
"metaTitle": "string | null",
|
||||
"footerText": "string | null"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
### whitelabeling.reset
|
||||
|
||||
Reset whitelabeling configuration to default values (all fields set to null).
|
||||
|
||||
**Requirements:**
|
||||
- Enterprise license required
|
||||
- Owner role required
|
||||
- Only available for self-hosted (not cloud)
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
### whitelabeling.getPublic
|
||||
|
||||
Public endpoint to fetch whitelabeling configuration. This endpoint can be accessed without authentication, allowing the whitelabeling settings to be applied globally (including on the login page before auth).
|
||||
|
||||
**Requirements:**
|
||||
- No authentication required
|
||||
- Only available for self-hosted (not cloud)
|
||||
|
||||
**Response:**
|
||||
Returns the whitelabeling configuration object or null if not configured. Response shape is identical to `whitelabeling.get`.
|
||||
@@ -0,0 +1,66 @@
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
|
||||
import { api } from "@/utils/api";
|
||||
|
||||
interface Props {
|
||||
containerId: string;
|
||||
serverId?: string;
|
||||
}
|
||||
|
||||
export const RemoveContainerDialog = ({ containerId, serverId }: Props) => {
|
||||
const utils = api.useUtils();
|
||||
const { mutateAsync, isPending } = api.docker.removeContainer.useMutation();
|
||||
|
||||
return (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<DropdownMenuItem
|
||||
className="w-full cursor-pointer text-red-500 hover:!text-red-600"
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
>
|
||||
Remove Container
|
||||
</DropdownMenuItem>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Are you sure?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This will permanently remove the container{" "}
|
||||
<span className="font-semibold">{containerId}</span>. If the
|
||||
container is running, it will be forcefully stopped and removed.
|
||||
This action cannot be undone.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
disabled={isPending}
|
||||
onClick={async () => {
|
||||
await mutateAsync({ containerId, serverId })
|
||||
.then(async () => {
|
||||
toast.success("Container removed successfully");
|
||||
await utils.docker.getContainers.invalidate();
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error(err.message);
|
||||
});
|
||||
}}
|
||||
>
|
||||
Confirm
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
);
|
||||
};
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { ShowContainerConfig } from "../config/show-container-config";
|
||||
import { ShowDockerModalLogs } from "../logs/show-docker-modal-logs";
|
||||
import { RemoveContainerDialog } from "../remove/remove-container";
|
||||
import { DockerTerminalModal } from "../terminal/docker-terminal-modal";
|
||||
import type { Container } from "./show-containers";
|
||||
|
||||
@@ -127,6 +128,10 @@ export const columns: ColumnDef<Container>[] = [
|
||||
>
|
||||
Terminal
|
||||
</DockerTerminalModal>
|
||||
<RemoveContainerDialog
|
||||
containerId={container.containerId}
|
||||
serverId={container.serverId ?? undefined}
|
||||
/>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
|
||||
import { PenBoxIcon, PlusIcon } from "lucide-react";
|
||||
import { PenBoxIcon, PlusIcon, Trash2 } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useFieldArray, useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
@@ -35,6 +35,10 @@ import {
|
||||
} from "@/components/ui/select";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { api } from "@/utils/api";
|
||||
import {
|
||||
ADDITIONAL_FLAG_ERROR,
|
||||
ADDITIONAL_FLAG_REGEX,
|
||||
} from "@dokploy/server/db/validations/destination";
|
||||
import { S3_PROVIDERS } from "./constants";
|
||||
|
||||
const addDestination = z.object({
|
||||
@@ -46,6 +50,16 @@ const addDestination = z.object({
|
||||
region: z.string(),
|
||||
endpoint: z.string().min(1, "Endpoint is required"),
|
||||
serverId: z.string().optional(),
|
||||
additionalFlags: z
|
||||
.array(
|
||||
z.object({
|
||||
value: z
|
||||
.string()
|
||||
.min(1, "Flag cannot be empty")
|
||||
.regex(ADDITIONAL_FLAG_REGEX, ADDITIONAL_FLAG_ERROR),
|
||||
}),
|
||||
)
|
||||
.optional(),
|
||||
});
|
||||
|
||||
type AddDestination = z.infer<typeof addDestination>;
|
||||
@@ -89,9 +103,16 @@ export const HandleDestinations = ({ destinationId }: Props) => {
|
||||
region: "",
|
||||
secretAccessKey: "",
|
||||
endpoint: "",
|
||||
additionalFlags: [],
|
||||
},
|
||||
resolver: zodResolver(addDestination),
|
||||
});
|
||||
|
||||
const { fields, append, remove } = useFieldArray({
|
||||
control: form.control,
|
||||
name: "additionalFlags",
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (destination) {
|
||||
form.reset({
|
||||
@@ -102,6 +123,8 @@ export const HandleDestinations = ({ destinationId }: Props) => {
|
||||
bucket: destination.bucket,
|
||||
region: destination.region,
|
||||
endpoint: destination.endpoint,
|
||||
additionalFlags:
|
||||
destination.additionalFlags?.map((f) => ({ value: f })) ?? [],
|
||||
});
|
||||
} else {
|
||||
form.reset();
|
||||
@@ -118,6 +141,7 @@ export const HandleDestinations = ({ destinationId }: Props) => {
|
||||
region: data.region,
|
||||
secretAccessKey: data.secretAccessKey,
|
||||
destinationId: destinationId || "",
|
||||
additionalFlags: data.additionalFlags?.map((f) => f.value) ?? [],
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success(`Destination ${destinationId ? "Updated" : "Created"}`);
|
||||
@@ -127,9 +151,12 @@ export const HandleDestinations = ({ destinationId }: Props) => {
|
||||
}
|
||||
setOpen(false);
|
||||
})
|
||||
.catch(() => {
|
||||
.catch((e) => {
|
||||
toast.error(
|
||||
`Error ${destinationId ? "Updating" : "Creating"} the Destination`,
|
||||
{
|
||||
description: e.message,
|
||||
},
|
||||
);
|
||||
});
|
||||
};
|
||||
@@ -141,6 +168,7 @@ export const HandleDestinations = ({ destinationId }: Props) => {
|
||||
"secretAccessKey",
|
||||
"bucket",
|
||||
"endpoint",
|
||||
"additionalFlags",
|
||||
]);
|
||||
|
||||
if (!result) {
|
||||
@@ -179,6 +207,8 @@ export const HandleDestinations = ({ destinationId }: Props) => {
|
||||
region,
|
||||
secretAccessKey: secretKey,
|
||||
serverId,
|
||||
additionalFlags:
|
||||
form.getValues("additionalFlags")?.map((f) => f.value) ?? [],
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("Connection Success");
|
||||
@@ -358,6 +388,48 @@ export const HandleDestinations = ({ destinationId }: Props) => {
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<FormLabel>Additional Flags (Optional)</FormLabel>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => append({ value: "" })}
|
||||
>
|
||||
<PlusIcon className="size-4" />
|
||||
Add Flag
|
||||
</Button>
|
||||
</div>
|
||||
{fields.map((field, index) => (
|
||||
<FormField
|
||||
key={field.id}
|
||||
control={form.control}
|
||||
name={`additionalFlags.${index}.value`}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<div className="flex items-center gap-2">
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="--s3-sign-accept-encoding=false"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => remove(index)}
|
||||
>
|
||||
<Trash2 className="size-4 text-muted-foreground" />
|
||||
</Button>
|
||||
</div>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<DialogFooter
|
||||
|
||||
@@ -12,6 +12,7 @@ import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import {
|
||||
DiscordIcon,
|
||||
MattermostIcon,
|
||||
GotifyIcon,
|
||||
LarkIcon,
|
||||
NtfyIcon,
|
||||
@@ -134,6 +135,14 @@ export const notificationSchema = z.discriminatedUnion("type", [
|
||||
priority: z.number().min(1).max(5).default(3),
|
||||
})
|
||||
.merge(notificationBaseSchema),
|
||||
z
|
||||
.object({
|
||||
type: z.literal("mattermost"),
|
||||
webhookUrl: z.string().min(1, { message: "Webhook URL is required" }),
|
||||
channel: z.string().optional(),
|
||||
username: z.string().optional(),
|
||||
})
|
||||
.merge(notificationBaseSchema),
|
||||
z
|
||||
.object({
|
||||
type: z.literal("pushover"),
|
||||
@@ -210,6 +219,10 @@ export const notificationsMap = {
|
||||
icon: <NtfyIcon />,
|
||||
label: "ntfy",
|
||||
},
|
||||
mattermost: {
|
||||
icon: <MattermostIcon />,
|
||||
label: "Mattermost",
|
||||
},
|
||||
pushover: {
|
||||
icon: <PushoverIcon />,
|
||||
label: "Pushover",
|
||||
@@ -253,14 +266,16 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
||||
api.notification.testGotifyConnection.useMutation();
|
||||
const { mutateAsync: testNtfyConnection, isPending: isLoadingNtfy } =
|
||||
api.notification.testNtfyConnection.useMutation();
|
||||
const {
|
||||
mutateAsync: testMattermostConnection,
|
||||
isPending: isLoadingMattermost,
|
||||
} = api.notification.testMattermostConnection.useMutation();
|
||||
const { mutateAsync: testLarkConnection, isPending: isLoadingLark } =
|
||||
api.notification.testLarkConnection.useMutation();
|
||||
const { mutateAsync: testTeamsConnection, isPending: isLoadingTeams } =
|
||||
api.notification.testTeamsConnection.useMutation();
|
||||
|
||||
const { mutateAsync: testCustomConnection, isPending: isLoadingCustom } =
|
||||
api.notification.testCustomConnection.useMutation();
|
||||
|
||||
const { mutateAsync: testPushoverConnection, isPending: isLoadingPushover } =
|
||||
api.notification.testPushoverConnection.useMutation();
|
||||
|
||||
@@ -288,6 +303,9 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
||||
const ntfyMutation = notificationId
|
||||
? api.notification.updateNtfy.useMutation()
|
||||
: api.notification.createNtfy.useMutation();
|
||||
const mattermostMutation = notificationId
|
||||
? api.notification.updateMattermost.useMutation()
|
||||
: api.notification.createMattermost.useMutation();
|
||||
const larkMutation = notificationId
|
||||
? api.notification.updateLark.useMutation()
|
||||
: api.notification.createLark.useMutation();
|
||||
@@ -438,6 +456,21 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
||||
dockerCleanup: notification.dockerCleanup,
|
||||
serverThreshold: notification.serverThreshold,
|
||||
});
|
||||
} else if (notification.notificationType === "mattermost") {
|
||||
form.reset({
|
||||
appBuildError: notification.appBuildError,
|
||||
appDeploy: notification.appDeploy,
|
||||
dokployRestart: notification.dokployRestart,
|
||||
databaseBackup: notification.databaseBackup,
|
||||
volumeBackup: notification.volumeBackup,
|
||||
type: notification.notificationType,
|
||||
webhookUrl: notification.mattermost?.webhookUrl,
|
||||
channel: notification.mattermost?.channel || "",
|
||||
username: notification.mattermost?.username || "",
|
||||
name: notification.name,
|
||||
dockerCleanup: notification.dockerCleanup,
|
||||
serverThreshold: notification.serverThreshold,
|
||||
});
|
||||
} else if (notification.notificationType === "lark") {
|
||||
form.reset({
|
||||
appBuildError: notification.appBuildError,
|
||||
@@ -516,6 +549,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
||||
resend: resendMutation,
|
||||
gotify: gotifyMutation,
|
||||
ntfy: ntfyMutation,
|
||||
mattermost: mattermostMutation,
|
||||
lark: larkMutation,
|
||||
teams: teamsMutation,
|
||||
custom: customMutation,
|
||||
@@ -646,6 +680,22 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
||||
notificationId: notificationId || "",
|
||||
ntfyId: notification?.ntfyId || "",
|
||||
});
|
||||
} else if (data.type === "mattermost") {
|
||||
promise = mattermostMutation.mutateAsync({
|
||||
appBuildError: appBuildError,
|
||||
appDeploy: appDeploy,
|
||||
dokployRestart: dokployRestart,
|
||||
databaseBackup: databaseBackup,
|
||||
volumeBackup: volumeBackup,
|
||||
webhookUrl: data.webhookUrl,
|
||||
channel: data.channel || undefined,
|
||||
username: data.username || undefined,
|
||||
name: data.name,
|
||||
dockerCleanup: dockerCleanup,
|
||||
notificationId: notificationId || "",
|
||||
mattermostId: notification?.mattermostId || "",
|
||||
serverThreshold: serverThreshold,
|
||||
});
|
||||
} else if (data.type === "lark") {
|
||||
promise = larkMutation.mutateAsync({
|
||||
appBuildError: appBuildError,
|
||||
@@ -1406,6 +1456,62 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{type === "mattermost" && (
|
||||
<>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="webhookUrl"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Webhook URL</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="https://your-mattermost.com/hooks/xxx-generatedkey-xxx"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="channel"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Channel</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="deployments" {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Optional. Channel to post to (without #).
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="username"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Username</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Dokploy" {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Optional. Display name for the webhook.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{type === "custom" && (
|
||||
<div className="space-y-4">
|
||||
<FormField
|
||||
@@ -1492,6 +1598,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{type === "lark" && (
|
||||
<>
|
||||
<FormField
|
||||
@@ -1852,6 +1959,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
||||
isLoadingResend ||
|
||||
isLoadingGotify ||
|
||||
isLoadingNtfy ||
|
||||
isLoadingMattermost ||
|
||||
isLoadingLark ||
|
||||
isLoadingTeams ||
|
||||
isLoadingCustom ||
|
||||
@@ -1911,6 +2019,12 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
||||
accessToken: data.accessToken || "",
|
||||
priority: data.priority ?? 0,
|
||||
});
|
||||
} else if (data.type === "mattermost") {
|
||||
await testMattermostConnection({
|
||||
webhookUrl: data.webhookUrl,
|
||||
channel: data.channel || undefined,
|
||||
username: data.username || undefined,
|
||||
});
|
||||
} else if (data.type === "lark") {
|
||||
await testLarkConnection({
|
||||
webhookUrl: data.webhookUrl,
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
DiscordIcon,
|
||||
GotifyIcon,
|
||||
LarkIcon,
|
||||
MattermostIcon,
|
||||
NtfyIcon,
|
||||
ResendIcon,
|
||||
SlackIcon,
|
||||
@@ -121,6 +122,12 @@ export const ShowNotifications = () => {
|
||||
<TeamsIcon className="size-7 text-muted-foreground" />
|
||||
</div>
|
||||
)}
|
||||
{notification.notificationType ===
|
||||
"mattermost" && (
|
||||
<div className="flex items-center justify-center rounded-lg">
|
||||
<MattermostIcon className="size-7" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{notification.name}
|
||||
</span>
|
||||
|
||||
@@ -20,6 +20,7 @@ import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
@@ -409,7 +410,10 @@ export const HandleServers = ({ serverId, asButton = false }: Props) => {
|
||||
<FormControl>
|
||||
<Input placeholder="root" {...field} />
|
||||
</FormControl>
|
||||
|
||||
<FormDescription>
|
||||
Use "root" or a non-root user with passwordless
|
||||
sudo access.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
|
||||
@@ -118,9 +118,10 @@ export const SetupServer = ({ serverId, asButton = false }: Props) => {
|
||||
</div>
|
||||
) : (
|
||||
<div id="hook-form-add-gitlab" className="grid w-full gap-4">
|
||||
<AlertBlock type="warning">
|
||||
Using a root user is required to ensure everything works as
|
||||
expected.
|
||||
<AlertBlock type="info">
|
||||
You can connect as root or as a non-root user with passwordless
|
||||
sudo access. If using a non-root user, ensure passwordless sudo is
|
||||
configured.
|
||||
</AlertBlock>
|
||||
|
||||
<Tabs defaultValue="ssh-keys">
|
||||
|
||||
@@ -163,6 +163,29 @@ export const ValidateServer = ({ serverId }: Props) => {
|
||||
: "Not Created"
|
||||
}
|
||||
/>
|
||||
<StatusRow
|
||||
label="Privilege Mode"
|
||||
isEnabled={
|
||||
data?.privilegeMode === "root" ||
|
||||
data?.privilegeMode === "sudo"
|
||||
}
|
||||
description={
|
||||
data?.privilegeMode === "root"
|
||||
? "Running as root"
|
||||
: data?.privilegeMode === "sudo"
|
||||
? "Running with sudo"
|
||||
: "No sudo access (required for non-root)"
|
||||
}
|
||||
/>
|
||||
<StatusRow
|
||||
label="Docker Group"
|
||||
isEnabled={data?.dockerGroupMember}
|
||||
description={
|
||||
data?.dockerGroupMember
|
||||
? "User is in docker group"
|
||||
: "User is not in docker group"
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -88,6 +88,21 @@ export const DiscordIcon = ({ className }: Props) => {
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export const MattermostIcon = ({ className }: Props) => {
|
||||
return (
|
||||
<svg
|
||||
fill="#0061ff"
|
||||
viewBox="0 0 501 501"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={cn("size-8", className)}
|
||||
>
|
||||
<path d="M236 .7C137.7 7.5 54 68.2 18.2 158.5c-32 81-19.6 172.8 33 242.5 39.8 53 97.2 87 164.3 97 16.5 2.7 48 3.2 63.5 1.2 48.7-6.3 92.2-24.6 129-54.2 13-10.5 33-31.2 42.2-43.7 26.4-35.5 42.8-75.8 49-120.3 1.6-12.3 1.6-48.7 0-61-4-28.3-12-54.8-24.2-79.5-12.8-26-26.5-45.3-46.8-65.8C417.8 64 400.2 49 398.4 49c-.6 0-.4 10.5.3 26l1.3 26 7 8.7c19 23.7 32.8 53.5 38.2 83 2.5 14 3 43 1 55.8-4.5 27.8-15.2 54-31 76.5-8.6 12.2-28 31.6-40.2 40.2-24 17-50 27.6-80 33-10 1.8-49 1.8-59 0-43-7.7-78.8-26-107.2-54.8-29.3-29.7-46.5-64-52.4-104.4-2-14-1.5-42 1-55C90 121.4 132 72 192 49.7c8-3 18.4-5.8 29.5-8.2 1.7-.4 34.4-38 35.3-40.6.3-1-10.2-1-20.8-.4z" />
|
||||
<path d="M322.2 24.6c-1.3.8-8.4 9.3-16 18.7-7.4 9.5-22.4 28-33.2 41.2-51 62.2-66 81.6-70.6 91-6 12-8.4 21-9 33-1.2 19.8 5 36 19 50C222 268 230 273 243 277.2c9 3 10.4 3.2 24 3.2 13.8 0 15 0 22.6-3 23.2-9 39-28.4 45-55.7 2-8.2 2-28.7.4-79.7l-2-72c-1-36.8-1.4-41.8-3-44-2-3-4.8-3.6-7.8-1.4z" />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export const TeamsIcon = ({ className }: Props) => {
|
||||
return (
|
||||
<svg
|
||||
|
||||
10
apps/dokploy/drizzle/0154_careful_eternals.sql
Normal file
10
apps/dokploy/drizzle/0154_careful_eternals.sql
Normal file
@@ -0,0 +1,10 @@
|
||||
ALTER TYPE "public"."notificationType" ADD VALUE 'mattermost' BEFORE 'pushover';--> statement-breakpoint
|
||||
CREATE TABLE "mattermost" (
|
||||
"mattermostId" text PRIMARY KEY NOT NULL,
|
||||
"webhookUrl" text NOT NULL,
|
||||
"channel" text,
|
||||
"username" text
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "notification" ADD COLUMN "mattermostId" text;--> statement-breakpoint
|
||||
ALTER TABLE "notification" ADD CONSTRAINT "notification_mattermostId_mattermost_mattermostId_fk" FOREIGN KEY ("mattermostId") REFERENCES "public"."mattermost"("mattermostId") ON DELETE cascade ON UPDATE no action;
|
||||
1
apps/dokploy/drizzle/0155_careless_clea.sql
Normal file
1
apps/dokploy/drizzle/0155_careless_clea.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE "destination" ADD COLUMN "additionalFlags" text[];
|
||||
8237
apps/dokploy/drizzle/meta/0154_snapshot.json
Normal file
8237
apps/dokploy/drizzle/meta/0154_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
8243
apps/dokploy/drizzle/meta/0155_snapshot.json
Normal file
8243
apps/dokploy/drizzle/meta/0155_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1079,6 +1079,20 @@
|
||||
"when": 1774322599182,
|
||||
"tag": "0153_motionless_mastermind",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 154,
|
||||
"version": "7",
|
||||
"when": 1774337356154,
|
||||
"tag": "0154_careful_eternals",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 155,
|
||||
"version": "7",
|
||||
"when": 1774794547865,
|
||||
"tag": "0155_careless_clea",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -47,8 +47,15 @@ export const destinationRouter = createTRPCRouter({
|
||||
testConnection: withPermission("destination", "create")
|
||||
.input(apiCreateDestination)
|
||||
.mutation(async ({ input }) => {
|
||||
const { secretAccessKey, bucket, region, endpoint, accessKey, provider } =
|
||||
input;
|
||||
const {
|
||||
secretAccessKey,
|
||||
bucket,
|
||||
region,
|
||||
endpoint,
|
||||
accessKey,
|
||||
provider,
|
||||
additionalFlags,
|
||||
} = input;
|
||||
try {
|
||||
const rcloneFlags = [
|
||||
`--s3-access-key-id="${accessKey}"`,
|
||||
@@ -65,6 +72,9 @@ export const destinationRouter = createTRPCRouter({
|
||||
if (provider) {
|
||||
rcloneFlags.unshift(`--s3-provider="${provider}"`);
|
||||
}
|
||||
if (additionalFlags?.length) {
|
||||
rcloneFlags.push(...additionalFlags);
|
||||
}
|
||||
const rcloneDestination = `:s3:${bucket}`;
|
||||
const rcloneCommand = `rclone ls ${rcloneFlags.join(" ")} "${rcloneDestination}"`;
|
||||
|
||||
@@ -159,7 +169,14 @@ export const destinationRouter = createTRPCRouter({
|
||||
});
|
||||
return result;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message:
|
||||
error instanceof Error
|
||||
? error?.message
|
||||
: "Error connecting to bucket",
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
containerRemove,
|
||||
containerRestart,
|
||||
findServerById,
|
||||
getConfig,
|
||||
@@ -52,6 +53,32 @@ export const dockerRouter = createTRPCRouter({
|
||||
return result;
|
||||
}),
|
||||
|
||||
removeContainer: withPermission("docker", "read")
|
||||
.input(
|
||||
z.object({
|
||||
containerId: z
|
||||
.string()
|
||||
.min(1)
|
||||
.regex(containerIdRegex, "Invalid container id."),
|
||||
serverId: z.string().optional(),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
if (input.serverId) {
|
||||
const server = await findServerById(input.serverId);
|
||||
if (server.organizationId !== ctx.session?.activeOrganizationId) {
|
||||
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||
}
|
||||
}
|
||||
await containerRemove(input.containerId, input.serverId);
|
||||
await audit(ctx, {
|
||||
action: "delete",
|
||||
resourceType: "docker",
|
||||
resourceId: input.containerId,
|
||||
resourceName: input.containerId,
|
||||
});
|
||||
}),
|
||||
|
||||
getConfig: withPermission("docker", "read")
|
||||
.input(
|
||||
z.object({
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
createEmailNotification,
|
||||
createGotifyNotification,
|
||||
createLarkNotification,
|
||||
createMattermostNotification,
|
||||
createNtfyNotification,
|
||||
createPushoverNotification,
|
||||
createResendNotification,
|
||||
@@ -19,6 +20,7 @@ import {
|
||||
sendEmailNotification,
|
||||
sendGotifyNotification,
|
||||
sendLarkNotification,
|
||||
sendMattermostNotification,
|
||||
sendNtfyNotification,
|
||||
sendPushoverNotification,
|
||||
sendResendNotification,
|
||||
@@ -31,6 +33,7 @@ import {
|
||||
updateEmailNotification,
|
||||
updateGotifyNotification,
|
||||
updateLarkNotification,
|
||||
updateMattermostNotification,
|
||||
updateNtfyNotification,
|
||||
updatePushoverNotification,
|
||||
updateResendNotification,
|
||||
@@ -54,6 +57,7 @@ import {
|
||||
apiCreateEmail,
|
||||
apiCreateGotify,
|
||||
apiCreateLark,
|
||||
apiCreateMattermost,
|
||||
apiCreateNtfy,
|
||||
apiCreatePushover,
|
||||
apiCreateResend,
|
||||
@@ -66,6 +70,7 @@ import {
|
||||
apiTestEmailConnection,
|
||||
apiTestGotifyConnection,
|
||||
apiTestLarkConnection,
|
||||
apiTestMattermostConnection,
|
||||
apiTestNtfyConnection,
|
||||
apiTestPushoverConnection,
|
||||
apiTestResendConnection,
|
||||
@@ -77,6 +82,7 @@ import {
|
||||
apiUpdateEmail,
|
||||
apiUpdateGotify,
|
||||
apiUpdateLark,
|
||||
apiUpdateMattermost,
|
||||
apiUpdateNtfy,
|
||||
apiUpdatePushover,
|
||||
apiUpdateResend,
|
||||
@@ -473,6 +479,7 @@ export const notificationRouter = createTRPCRouter({
|
||||
resend: true,
|
||||
gotify: true,
|
||||
ntfy: true,
|
||||
mattermost: true,
|
||||
custom: true,
|
||||
lark: true,
|
||||
pushover: true,
|
||||
@@ -675,6 +682,74 @@ export const notificationRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
}),
|
||||
createMattermost: withPermission("notification", "create")
|
||||
.input(apiCreateMattermost)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
await createMattermostNotification(
|
||||
input,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
await audit(ctx, {
|
||||
action: "create",
|
||||
resourceType: "notification",
|
||||
resourceName: input.name,
|
||||
});
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error creating the notification",
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
}),
|
||||
updateMattermost: withPermission("notification", "update")
|
||||
.input(apiUpdateMattermost)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
const notification = await findNotificationById(input.notificationId);
|
||||
if (
|
||||
IS_CLOUD &&
|
||||
notification.organizationId !== ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to update this notification",
|
||||
});
|
||||
}
|
||||
const result = await updateMattermostNotification({
|
||||
...input,
|
||||
organizationId: ctx.session.activeOrganizationId,
|
||||
});
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "notification",
|
||||
resourceId: input.notificationId,
|
||||
resourceName: notification.name,
|
||||
});
|
||||
return result;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}),
|
||||
testMattermostConnection: withPermission("notification", "create")
|
||||
.input(apiTestMattermostConnection)
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
await sendMattermostNotification(input, {
|
||||
text: "Hi, From Dokploy 👋",
|
||||
channel: input.channel,
|
||||
username: input.username || "Dokploy Bot",
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error testing the notification",
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
}),
|
||||
createCustom: withPermission("notification", "create")
|
||||
.input(apiCreateCustom)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
|
||||
@@ -252,6 +252,8 @@ export const serverRouter = createTRPCRouter({
|
||||
isDokployNetworkInstalled: boolean;
|
||||
isSwarmInstalled: boolean;
|
||||
isMainDirectoryInstalled: boolean;
|
||||
privilegeMode: string;
|
||||
dockerGroupMember: boolean;
|
||||
};
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
|
||||
@@ -3,6 +3,10 @@ import { pgTable, text, timestamp } from "drizzle-orm/pg-core";
|
||||
import { createInsertSchema } from "drizzle-zod";
|
||||
import { nanoid } from "nanoid";
|
||||
import { z } from "zod";
|
||||
import {
|
||||
ADDITIONAL_FLAG_ERROR,
|
||||
ADDITIONAL_FLAG_REGEX,
|
||||
} from "../validations/destination";
|
||||
import { organization } from "./account";
|
||||
import { backups } from "./backups";
|
||||
|
||||
@@ -18,6 +22,7 @@ export const destinations = pgTable("destination", {
|
||||
bucket: text("bucket").notNull(),
|
||||
region: text("region").notNull(),
|
||||
endpoint: text("endpoint").notNull(),
|
||||
additionalFlags: text("additionalFlags").array(),
|
||||
organizationId: text("organizationId")
|
||||
.notNull()
|
||||
.references(() => organization.id, { onDelete: "cascade" }),
|
||||
@@ -44,6 +49,9 @@ const createSchema = createInsertSchema(destinations, {
|
||||
endpoint: z.string(),
|
||||
secretAccessKey: z.string(),
|
||||
region: z.string(),
|
||||
additionalFlags: z
|
||||
.array(z.string().regex(ADDITIONAL_FLAG_REGEX, ADDITIONAL_FLAG_ERROR))
|
||||
.default([]),
|
||||
});
|
||||
|
||||
export const apiCreateDestination = createSchema
|
||||
@@ -55,6 +63,7 @@ export const apiCreateDestination = createSchema
|
||||
region: true,
|
||||
endpoint: true,
|
||||
secretAccessKey: true,
|
||||
additionalFlags: true,
|
||||
})
|
||||
.required()
|
||||
.extend({
|
||||
@@ -81,6 +90,7 @@ export const apiUpdateDestination = createSchema
|
||||
secretAccessKey: true,
|
||||
destinationId: true,
|
||||
provider: true,
|
||||
additionalFlags: true,
|
||||
})
|
||||
.required()
|
||||
.extend({
|
||||
|
||||
@@ -20,6 +20,7 @@ export const notificationType = pgEnum("notificationType", [
|
||||
"resend",
|
||||
"gotify",
|
||||
"ntfy",
|
||||
"mattermost",
|
||||
"pushover",
|
||||
"custom",
|
||||
"lark",
|
||||
@@ -64,6 +65,9 @@ export const notifications = pgTable("notification", {
|
||||
ntfyId: text("ntfyId").references(() => ntfy.ntfyId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
mattermostId: text("mattermostId").references(() => mattermost.mattermostId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
customId: text("customId").references(() => custom.customId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
@@ -154,6 +158,16 @@ export const ntfy = pgTable("ntfy", {
|
||||
priority: integer("priority").notNull().default(3),
|
||||
});
|
||||
|
||||
export const mattermost = pgTable("mattermost", {
|
||||
mattermostId: text("mattermostId")
|
||||
.notNull()
|
||||
.primaryKey()
|
||||
.$defaultFn(() => nanoid()),
|
||||
webhookUrl: text("webhookUrl").notNull(),
|
||||
channel: text("channel"),
|
||||
username: text("username"),
|
||||
});
|
||||
|
||||
export const custom = pgTable("custom", {
|
||||
customId: text("customId")
|
||||
.notNull()
|
||||
@@ -220,6 +234,10 @@ export const notificationsRelations = relations(notifications, ({ one }) => ({
|
||||
fields: [notifications.ntfyId],
|
||||
references: [ntfy.ntfyId],
|
||||
}),
|
||||
mattermost: one(mattermost, {
|
||||
fields: [notifications.mattermostId],
|
||||
references: [mattermost.mattermostId],
|
||||
}),
|
||||
custom: one(custom, {
|
||||
fields: [notifications.customId],
|
||||
references: [custom.customId],
|
||||
@@ -464,6 +482,51 @@ export const apiTestNtfyConnection = apiCreateNtfy.pick({
|
||||
priority: true,
|
||||
});
|
||||
|
||||
export const apiCreateMattermost = notificationsSchema
|
||||
.pick({
|
||||
appBuildError: true,
|
||||
databaseBackup: true,
|
||||
volumeBackup: true,
|
||||
dokployRestart: true,
|
||||
name: true,
|
||||
appDeploy: true,
|
||||
dockerCleanup: true,
|
||||
serverThreshold: true,
|
||||
})
|
||||
.extend({
|
||||
webhookUrl: z.string().url(),
|
||||
channel: z.string().optional(),
|
||||
username: z.string().optional(),
|
||||
})
|
||||
.required({
|
||||
name: true,
|
||||
webhookUrl: true,
|
||||
appBuildError: true,
|
||||
databaseBackup: true,
|
||||
volumeBackup: true,
|
||||
dokployRestart: true,
|
||||
appDeploy: true,
|
||||
dockerCleanup: true,
|
||||
serverThreshold: true,
|
||||
});
|
||||
|
||||
export const apiUpdateMattermost = apiCreateMattermost.partial().extend({
|
||||
notificationId: z.string().min(1),
|
||||
mattermostId: z.string().min(1),
|
||||
organizationId: z.string().optional(),
|
||||
});
|
||||
|
||||
export const apiTestMattermostConnection = apiCreateMattermost
|
||||
.pick({
|
||||
webhookUrl: true,
|
||||
channel: true,
|
||||
username: true,
|
||||
})
|
||||
.extend({
|
||||
channel: z.string().optional(),
|
||||
username: z.string().optional(),
|
||||
});
|
||||
|
||||
export const apiFindOneNotification = z.object({
|
||||
notificationId: z.string().min(1),
|
||||
});
|
||||
|
||||
3
packages/server/src/db/validations/destination.ts
Normal file
3
packages/server/src/db/validations/destination.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export const ADDITIONAL_FLAG_REGEX = /^--[a-zA-Z0-9-]+(=[a-zA-Z0-9._:/@-]+)?$/;
|
||||
export const ADDITIONAL_FLAG_ERROR =
|
||||
"Invalid flag format. Must start with -- (e.g. --s3-sign-accept-encoding=false)";
|
||||
@@ -1,6 +1,7 @@
|
||||
export * from "./auth/random-password";
|
||||
export * from "./constants/index";
|
||||
export * from "./db/constants";
|
||||
export * from "./db/validations/destination";
|
||||
export * from "./db/validations/domain";
|
||||
export * from "./db/validations/index";
|
||||
export * from "./lib/auth";
|
||||
|
||||
@@ -371,6 +371,21 @@ export const containerRestart = async (containerId: string) => {
|
||||
} catch {}
|
||||
};
|
||||
|
||||
export const containerRemove = async (
|
||||
containerId: string,
|
||||
serverId?: string,
|
||||
) => {
|
||||
const command = `docker rm -f ${containerId}`;
|
||||
const { stderr } = serverId
|
||||
? await execAsyncRemote(serverId, command)
|
||||
: await execAsync(command);
|
||||
|
||||
if (stderr) {
|
||||
console.error(`Error: ${stderr}`);
|
||||
throw new Error(stderr);
|
||||
}
|
||||
};
|
||||
|
||||
export const getSwarmNodes = async (serverId?: string) => {
|
||||
try {
|
||||
let stdout = "";
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
type apiCreateEmail,
|
||||
type apiCreateGotify,
|
||||
type apiCreateLark,
|
||||
type apiCreateMattermost,
|
||||
type apiCreateNtfy,
|
||||
type apiCreatePushover,
|
||||
type apiCreateResend,
|
||||
@@ -16,6 +17,7 @@ import {
|
||||
type apiUpdateEmail,
|
||||
type apiUpdateGotify,
|
||||
type apiUpdateLark,
|
||||
type apiUpdateMattermost,
|
||||
type apiUpdateNtfy,
|
||||
type apiUpdatePushover,
|
||||
type apiUpdateResend,
|
||||
@@ -27,6 +29,7 @@ import {
|
||||
email,
|
||||
gotify,
|
||||
lark,
|
||||
mattermost,
|
||||
notifications,
|
||||
ntfy,
|
||||
pushover,
|
||||
@@ -726,6 +729,7 @@ export const createCustomNotification = async (
|
||||
appDeploy: input.appDeploy,
|
||||
appBuildError: input.appBuildError,
|
||||
databaseBackup: input.databaseBackup,
|
||||
volumeBackup: input.volumeBackup,
|
||||
dokployRestart: input.dokployRestart,
|
||||
dockerCleanup: input.dockerCleanup,
|
||||
notificationType: "custom",
|
||||
@@ -797,6 +801,7 @@ export const findNotificationById = async (notificationId: string) => {
|
||||
resend: true,
|
||||
gotify: true,
|
||||
ntfy: true,
|
||||
mattermost: true,
|
||||
custom: true,
|
||||
lark: true,
|
||||
pushover: true,
|
||||
@@ -849,6 +854,7 @@ export const createLarkNotification = async (
|
||||
appDeploy: input.appDeploy,
|
||||
appBuildError: input.appBuildError,
|
||||
databaseBackup: input.databaseBackup,
|
||||
volumeBackup: input.volumeBackup,
|
||||
dokployRestart: input.dokployRestart,
|
||||
dockerCleanup: input.dockerCleanup,
|
||||
notificationType: "lark",
|
||||
@@ -1015,6 +1021,100 @@ export const updateNotificationById = async (
|
||||
return result[0];
|
||||
};
|
||||
|
||||
export const createMattermostNotification = async (
|
||||
input: z.infer<typeof apiCreateMattermost>,
|
||||
organizationId: string,
|
||||
) => {
|
||||
await db.transaction(async (tx) => {
|
||||
const newMattermost = await tx
|
||||
.insert(mattermost)
|
||||
.values({
|
||||
webhookUrl: input.webhookUrl,
|
||||
channel: input.channel,
|
||||
username: input.username,
|
||||
})
|
||||
.returning()
|
||||
.then((value) => value[0]);
|
||||
|
||||
if (!newMattermost) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error input: Inserting mattermost",
|
||||
});
|
||||
}
|
||||
|
||||
const newDestination = await tx
|
||||
.insert(notifications)
|
||||
.values({
|
||||
mattermostId: newMattermost.mattermostId,
|
||||
name: input.name,
|
||||
appDeploy: input.appDeploy,
|
||||
appBuildError: input.appBuildError,
|
||||
databaseBackup: input.databaseBackup,
|
||||
volumeBackup: input.volumeBackup,
|
||||
dokployRestart: input.dokployRestart,
|
||||
dockerCleanup: input.dockerCleanup,
|
||||
notificationType: "mattermost",
|
||||
organizationId: organizationId,
|
||||
serverThreshold: input.serverThreshold,
|
||||
})
|
||||
.returning()
|
||||
.then((value) => value[0]);
|
||||
|
||||
if (!newDestination) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error input: Inserting notification",
|
||||
});
|
||||
}
|
||||
|
||||
return newDestination;
|
||||
});
|
||||
};
|
||||
|
||||
export const updateMattermostNotification = async (
|
||||
input: z.infer<typeof apiUpdateMattermost>,
|
||||
) => {
|
||||
await db.transaction(async (tx) => {
|
||||
const newDestination = await tx
|
||||
.update(notifications)
|
||||
.set({
|
||||
name: input.name,
|
||||
appDeploy: input.appDeploy,
|
||||
appBuildError: input.appBuildError,
|
||||
databaseBackup: input.databaseBackup,
|
||||
volumeBackup: input.volumeBackup,
|
||||
dokployRestart: input.dokployRestart,
|
||||
dockerCleanup: input.dockerCleanup,
|
||||
organizationId: input.organizationId,
|
||||
serverThreshold: input.serverThreshold,
|
||||
})
|
||||
.where(eq(notifications.notificationId, input.notificationId))
|
||||
.returning()
|
||||
.then((value) => value[0]);
|
||||
|
||||
if (!newDestination) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error Updating notification",
|
||||
});
|
||||
}
|
||||
|
||||
await tx
|
||||
.update(mattermost)
|
||||
.set({
|
||||
webhookUrl: input.webhookUrl,
|
||||
channel: input.channel,
|
||||
username: input.username,
|
||||
})
|
||||
.where(eq(mattermost.mattermostId, input.mattermostId))
|
||||
.returning()
|
||||
.then((value) => value[0]);
|
||||
|
||||
return newDestination;
|
||||
});
|
||||
};
|
||||
|
||||
export const createPushoverNotification = async (
|
||||
input: z.infer<typeof apiCreatePushover>,
|
||||
organizationId: string,
|
||||
|
||||
@@ -115,9 +115,20 @@ SYS_ARCH=$(uname -m)
|
||||
CURRENT_USER=$USER
|
||||
|
||||
echo "Installing requirements for: OS: $OS_TYPE"
|
||||
if [ $EUID != 0 ]; then
|
||||
echo "Please run this script as root or with sudo ❌"
|
||||
exit
|
||||
|
||||
# Auto-detect sudo requirement
|
||||
if [ "$EUID" -eq 0 ]; then
|
||||
SUDO_CMD=""
|
||||
echo "Running as root"
|
||||
else
|
||||
if sudo -n true 2>/dev/null; then
|
||||
SUDO_CMD="sudo"
|
||||
echo "Running as $CURRENT_USER with sudo privileges"
|
||||
else
|
||||
echo "Error: Non-root user requires passwordless sudo access. ❌"
|
||||
echo "Configure with: echo '$CURRENT_USER ALL=(ALL) NOPASSWD:ALL' | sudo tee /etc/sudoers.d/$CURRENT_USER"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check if the OS is manjaro, if so, change it to arch
|
||||
@@ -152,7 +163,7 @@ else
|
||||
fi
|
||||
|
||||
if [ "$OS_TYPE" = 'amzn' ]; then
|
||||
dnf install -y findutils >/dev/null
|
||||
$SUDO_CMD dnf install -y findutils >/dev/null
|
||||
fi
|
||||
|
||||
case "$OS_TYPE" in
|
||||
@@ -218,6 +229,9 @@ ${installBuildpacks()}
|
||||
|
||||
echo -e "13. Installing Railpack"
|
||||
${installRailpack()}
|
||||
|
||||
echo -e "14. Configuring permissions"
|
||||
${setupPermissions()}
|
||||
`
|
||||
: `
|
||||
echo -e "2. Installing Docker. "
|
||||
@@ -235,6 +249,9 @@ ${installBuildpacks()}
|
||||
|
||||
echo -e "6. Installing Railpack"
|
||||
${installRailpack()}
|
||||
|
||||
echo -e "7. Configuring permissions"
|
||||
${setupPermissions()}
|
||||
`
|
||||
}
|
||||
`;
|
||||
@@ -352,16 +369,18 @@ const setupMainDirectory = () => `
|
||||
echo "/etc/dokploy already exists ✅"
|
||||
else
|
||||
# Create the /etc/dokploy directory
|
||||
mkdir -p /etc/dokploy
|
||||
chmod 777 /etc/dokploy
|
||||
|
||||
$SUDO_CMD mkdir -p /etc/dokploy
|
||||
echo "Directory /etc/dokploy created ✅"
|
||||
fi
|
||||
# Ensure the current user owns the directory
|
||||
if [ -n "$SUDO_CMD" ]; then
|
||||
$SUDO_CMD chown -R $CURRENT_USER:$CURRENT_USER /etc/dokploy
|
||||
fi
|
||||
`;
|
||||
|
||||
export const setupSwarm = () => `
|
||||
# Check if the node is already part of a Docker Swarm
|
||||
if docker info | grep -q 'Swarm: active'; then
|
||||
if $SUDO_CMD docker info | grep -q 'Swarm: active'; then
|
||||
echo "Already part of a Docker Swarm ✅"
|
||||
else
|
||||
# Get IP address
|
||||
@@ -411,18 +430,18 @@ export const setupSwarm = () => `
|
||||
echo "Advertise address: \$advertise_addr"
|
||||
|
||||
# Initialize Docker Swarm
|
||||
docker swarm init --advertise-addr \$advertise_addr
|
||||
$SUDO_CMD docker swarm init --advertise-addr \$advertise_addr
|
||||
echo "Swarm initialized ✅"
|
||||
fi
|
||||
`;
|
||||
|
||||
const setupNetwork = () => `
|
||||
# Check if the dokploy-network already exists
|
||||
if docker network ls | grep -q 'dokploy-network'; then
|
||||
if $SUDO_CMD docker network ls | grep -q 'dokploy-network'; then
|
||||
echo "Network dokploy-network already exists ✅"
|
||||
else
|
||||
# Create the dokploy-network if it doesn't exist
|
||||
if docker network create --driver overlay --attachable dokploy-network; then
|
||||
if $SUDO_CMD docker network create --driver overlay --attachable dokploy-network; then
|
||||
echo "Network created ✅"
|
||||
else
|
||||
echo "Failed to create dokploy-network ❌" >&2
|
||||
@@ -447,33 +466,34 @@ const installUtilities = () => `
|
||||
|
||||
case "$OS_TYPE" in
|
||||
arch)
|
||||
pacman -Sy --noconfirm --needed curl wget git git-lfs jq openssl >/dev/null || true
|
||||
$SUDO_CMD pacman -Sy --noconfirm --needed curl wget git git-lfs jq openssl >/dev/null || true
|
||||
;;
|
||||
alpine)
|
||||
sed -i '/^#.*\/community/s/^#//' /etc/apk/repositories
|
||||
apk update >/dev/null
|
||||
apk add curl wget git git-lfs jq openssl sudo unzip tar >/dev/null
|
||||
$SUDO_CMD sed -i '/^#.*\/community/s/^#//' /etc/apk/repositories
|
||||
$SUDO_CMD apk update >/dev/null
|
||||
$SUDO_CMD apk add curl wget git git-lfs jq openssl sudo unzip tar >/dev/null
|
||||
;;
|
||||
ubuntu | debian | raspbian)
|
||||
DEBIAN_FRONTEND=noninteractive apt-get update -y >/dev/null
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y unzip curl wget git git-lfs jq openssl >/dev/null
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
$SUDO_CMD apt-get update -y >/dev/null
|
||||
$SUDO_CMD apt-get install -y unzip curl wget git git-lfs jq openssl >/dev/null
|
||||
;;
|
||||
centos | fedora | rhel | ol | rocky | almalinux | opencloudos | amzn)
|
||||
if [ "$OS_TYPE" = "amzn" ]; then
|
||||
dnf install -y wget git git-lfs jq openssl >/dev/null
|
||||
$SUDO_CMD dnf install -y wget git git-lfs jq openssl >/dev/null
|
||||
else
|
||||
if ! command -v dnf >/dev/null; then
|
||||
yum install -y dnf >/dev/null
|
||||
$SUDO_CMD yum install -y dnf >/dev/null
|
||||
fi
|
||||
if ! command -v curl >/dev/null; then
|
||||
dnf install -y curl >/dev/null
|
||||
$SUDO_CMD dnf install -y curl >/dev/null
|
||||
fi
|
||||
dnf install -y wget git git-lfs jq openssl unzip >/dev/null
|
||||
$SUDO_CMD dnf install -y wget git git-lfs jq openssl unzip >/dev/null
|
||||
fi
|
||||
;;
|
||||
sles | opensuse-leap | opensuse-tumbleweed)
|
||||
zypper refresh >/dev/null
|
||||
zypper install -y curl wget git git-lfs jq openssl >/dev/null
|
||||
$SUDO_CMD zypper refresh >/dev/null
|
||||
$SUDO_CMD zypper install -y curl wget git git-lfs jq openssl >/dev/null
|
||||
;;
|
||||
*)
|
||||
echo "This script only supports Debian, Redhat, Arch Linux, or SLES based operating systems for now."
|
||||
@@ -500,41 +520,41 @@ if ! [ -x "$(command -v docker)" ]; then
|
||||
echo " - Docker is not installed. Installing Docker. It may take a while."
|
||||
case "$OS_TYPE" in
|
||||
"almalinux")
|
||||
dnf config-manager --add-repo=https://download.docker.com/linux/centos/docker-ce.repo >/dev/null 2>&1
|
||||
dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin >/dev/null 2>&1
|
||||
$SUDO_CMD dnf config-manager --add-repo=https://download.docker.com/linux/centos/docker-ce.repo >/dev/null 2>&1
|
||||
$SUDO_CMD dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin >/dev/null 2>&1
|
||||
if ! [ -x "$(command -v docker)" ]; then
|
||||
echo " - Docker could not be installed automatically. Please visit https://docs.docker.com/engine/install/ and install Docker manually to continue."
|
||||
exit 1
|
||||
fi
|
||||
systemctl start docker >/dev/null 2>&1
|
||||
systemctl enable docker >/dev/null 2>&1
|
||||
$SUDO_CMD systemctl start docker >/dev/null 2>&1
|
||||
$SUDO_CMD systemctl enable docker >/dev/null 2>&1
|
||||
;;
|
||||
"opencloudos")
|
||||
# Special handling for OpenCloud OS
|
||||
echo " - Installing Docker for OpenCloud OS..."
|
||||
dnf install -y docker >/dev/null 2>&1
|
||||
$SUDO_CMD dnf install -y docker >/dev/null 2>&1
|
||||
if ! [ -x "$(command -v docker)" ]; then
|
||||
echo " - Docker could not be installed automatically. Please visit https://docs.docker.com/engine/install/ and install Docker manually to continue."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
# Remove --live-restore parameter from Docker configuration if it exists
|
||||
if [ -f "/etc/sysconfig/docker" ]; then
|
||||
echo " - Removing --live-restore parameter from Docker configuration..."
|
||||
sed -i 's/--live-restore[^[:space:]]*//' /etc/sysconfig/docker >/dev/null 2>&1
|
||||
sed -i 's/--live-restore//' /etc/sysconfig/docker >/dev/null 2>&1
|
||||
$SUDO_CMD sed -i 's/--live-restore[^[:space:]]*//' /etc/sysconfig/docker >/dev/null 2>&1
|
||||
$SUDO_CMD sed -i 's/--live-restore//' /etc/sysconfig/docker >/dev/null 2>&1
|
||||
# Clean up any double spaces that might be left
|
||||
sed -i 's/ */ /g' /etc/sysconfig/docker >/dev/null 2>&1
|
||||
$SUDO_CMD sed -i 's/ */ /g' /etc/sysconfig/docker >/dev/null 2>&1
|
||||
fi
|
||||
|
||||
systemctl enable docker >/dev/null 2>&1
|
||||
systemctl start docker >/dev/null 2>&1
|
||||
|
||||
$SUDO_CMD systemctl enable docker >/dev/null 2>&1
|
||||
$SUDO_CMD systemctl start docker >/dev/null 2>&1
|
||||
echo " - Docker configured for OpenCloud OS"
|
||||
;;
|
||||
"alpine")
|
||||
apk add docker docker-cli-compose >/dev/null 2>&1
|
||||
rc-update add docker default >/dev/null 2>&1
|
||||
service docker start >/dev/null 2>&1
|
||||
$SUDO_CMD apk add docker docker-cli-compose >/dev/null 2>&1
|
||||
$SUDO_CMD rc-update add docker default >/dev/null 2>&1
|
||||
$SUDO_CMD service docker start >/dev/null 2>&1
|
||||
if ! [ -x "$(command -v docker)" ]; then
|
||||
echo " - Failed to install Docker with apk. Try to install it manually."
|
||||
echo " Please visit https://wiki.alpinelinux.org/wiki/Docker for more information."
|
||||
@@ -542,8 +562,8 @@ if ! [ -x "$(command -v docker)" ]; then
|
||||
fi
|
||||
;;
|
||||
"arch")
|
||||
pacman -Sy docker docker-compose --noconfirm >/dev/null 2>&1
|
||||
systemctl enable docker.service >/dev/null 2>&1
|
||||
$SUDO_CMD pacman -Sy docker docker-compose --noconfirm >/dev/null 2>&1
|
||||
$SUDO_CMD systemctl enable docker.service >/dev/null 2>&1
|
||||
if ! [ -x "$(command -v docker)" ]; then
|
||||
echo " - Failed to install Docker with pacman. Try to install it manually."
|
||||
echo " Please visit https://wiki.archlinux.org/title/docker for more information."
|
||||
@@ -551,13 +571,13 @@ if ! [ -x "$(command -v docker)" ]; then
|
||||
fi
|
||||
;;
|
||||
"amzn")
|
||||
dnf install docker -y >/dev/null 2>&1
|
||||
$SUDO_CMD dnf install docker -y >/dev/null 2>&1
|
||||
DOCKER_CONFIG=/usr/local/lib/docker
|
||||
mkdir -p $DOCKER_CONFIG/cli-plugins >/dev/null 2>&1
|
||||
curl -sL https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m) -o $DOCKER_CONFIG/cli-plugins/docker-compose >/dev/null 2>&1
|
||||
chmod +x $DOCKER_CONFIG/cli-plugins/docker-compose >/dev/null 2>&1
|
||||
systemctl start docker >/dev/null 2>&1
|
||||
systemctl enable docker >/dev/null 2>&1
|
||||
$SUDO_CMD mkdir -p $DOCKER_CONFIG/cli-plugins >/dev/null 2>&1
|
||||
$SUDO_CMD curl -sL https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m) -o $DOCKER_CONFIG/cli-plugins/docker-compose >/dev/null 2>&1
|
||||
$SUDO_CMD chmod +x $DOCKER_CONFIG/cli-plugins/docker-compose >/dev/null 2>&1
|
||||
$SUDO_CMD systemctl start docker >/dev/null 2>&1
|
||||
$SUDO_CMD systemctl enable docker >/dev/null 2>&1
|
||||
if ! [ -x "$(command -v docker)" ]; then
|
||||
echo " - Failed to install Docker with dnf. Try to install it manually."
|
||||
echo " Please visit https://www.cyberciti.biz/faq/how-to-install-docker-on-amazon-linux-2/ for more information."
|
||||
@@ -567,18 +587,18 @@ if ! [ -x "$(command -v docker)" ]; then
|
||||
"fedora")
|
||||
if [ -x "$(command -v dnf5)" ]; then
|
||||
# dnf5 is available
|
||||
dnf config-manager addrepo --from-repofile=https://download.docker.com/linux/fedora/docker-ce.repo --overwrite >/dev/null 2>&1
|
||||
$SUDO_CMD dnf config-manager addrepo --from-repofile=https://download.docker.com/linux/fedora/docker-ce.repo --overwrite >/dev/null 2>&1
|
||||
else
|
||||
# dnf5 is not available, use dnf
|
||||
dnf config-manager --add-repo=https://download.docker.com/linux/fedora/docker-ce.repo >/dev/null 2>&1
|
||||
$SUDO_CMD dnf config-manager --add-repo=https://download.docker.com/linux/fedora/docker-ce.repo >/dev/null 2>&1
|
||||
fi
|
||||
dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin >/dev/null 2>&1
|
||||
$SUDO_CMD dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin >/dev/null 2>&1
|
||||
if ! [ -x "$(command -v docker)" ]; then
|
||||
echo " - Docker could not be installed automatically. Please visit https://docs.docker.com/engine/install/ and install Docker manually to continue."
|
||||
exit 1
|
||||
fi
|
||||
systemctl start docker >/dev/null 2>&1
|
||||
systemctl enable docker >/dev/null 2>&1
|
||||
$SUDO_CMD systemctl start docker >/dev/null 2>&1
|
||||
$SUDO_CMD systemctl enable docker >/dev/null 2>&1
|
||||
;;
|
||||
*)
|
||||
if [ "$OS_TYPE" = "ubuntu" ] && [ "$OS_VERSION" = "24.10" ]; then
|
||||
@@ -586,9 +606,9 @@ if ! [ -x "$(command -v docker)" ]; then
|
||||
echo "Please install Docker manually."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
if ! [ -x "$(command -v docker)" ]; then
|
||||
curl -s https://get.docker.com | sh -s -- --version $DOCKER_VERSION 2>&1
|
||||
curl -s https://get.docker.com | $SUDO_CMD sh -s -- --version $DOCKER_VERSION 2>&1
|
||||
if ! [ -x "$(command -v docker)" ]; then
|
||||
echo " - Docker installation failed."
|
||||
echo " Maybe your OS is not supported?"
|
||||
@@ -597,13 +617,13 @@ if ! [ -x "$(command -v docker)" ]; then
|
||||
fi
|
||||
fi
|
||||
if [ "$OS_TYPE" = "rocky" ]; then
|
||||
systemctl start docker >/dev/null 2>&1
|
||||
systemctl enable docker >/dev/null 2>&1
|
||||
$SUDO_CMD systemctl start docker >/dev/null 2>&1
|
||||
$SUDO_CMD systemctl enable docker >/dev/null 2>&1
|
||||
fi
|
||||
|
||||
if [ "$OS_TYPE" = "centos" ]; then
|
||||
systemctl start docker >/dev/null 2>&1
|
||||
systemctl enable docker >/dev/null 2>&1
|
||||
$SUDO_CMD systemctl start docker >/dev/null 2>&1
|
||||
$SUDO_CMD systemctl enable docker >/dev/null 2>&1
|
||||
fi
|
||||
|
||||
|
||||
@@ -647,7 +667,7 @@ export const installRClone = () => `
|
||||
if command_exists rclone; then
|
||||
echo "RClone already installed ✅"
|
||||
else
|
||||
curl https://rclone.org/install.sh | sudo bash
|
||||
curl https://rclone.org/install.sh | $SUDO_CMD bash
|
||||
RCLONE_VERSION=$(rclone --version | head -n 1 | awk '{print $2}' | sed 's/^v//')
|
||||
echo "RClone version $RCLONE_VERSION installed ✅"
|
||||
fi
|
||||
@@ -656,19 +676,19 @@ export const installRClone = () => `
|
||||
export const createTraefikInstance = () => {
|
||||
const command = `
|
||||
# Check if dokpyloy-traefik exists
|
||||
if docker service inspect dokploy-traefik > /dev/null 2>&1; then
|
||||
if $SUDO_CMD docker service inspect dokploy-traefik > /dev/null 2>&1; then
|
||||
echo "Migrating Traefik to Standalone..."
|
||||
docker service rm dokploy-traefik
|
||||
$SUDO_CMD docker service rm dokploy-traefik
|
||||
sleep 8
|
||||
echo "Traefik migrated to Standalone ✅"
|
||||
fi
|
||||
|
||||
if docker inspect dokploy-traefik > /dev/null 2>&1; then
|
||||
if $SUDO_CMD docker inspect dokploy-traefik > /dev/null 2>&1; then
|
||||
echo "Traefik already exists ✅"
|
||||
else
|
||||
# Create the dokploy-traefik container
|
||||
TRAEFIK_VERSION=${TRAEFIK_VERSION}
|
||||
docker run -d \
|
||||
$SUDO_CMD docker run -d \
|
||||
--name dokploy-traefik \
|
||||
--restart always \
|
||||
-v /etc/dokploy/traefik/traefik.yml:/etc/traefik/traefik.yml \
|
||||
@@ -679,7 +699,7 @@ export const createTraefikInstance = () => {
|
||||
-p ${TRAEFIK_HTTP3_PORT}:${TRAEFIK_HTTP3_PORT}/udp \
|
||||
traefik:v$TRAEFIK_VERSION
|
||||
|
||||
docker network connect dokploy-network dokploy-traefik;
|
||||
$SUDO_CMD docker network connect dokploy-network dokploy-traefik;
|
||||
echo "Traefik version $TRAEFIK_VERSION installed ✅"
|
||||
fi
|
||||
`;
|
||||
@@ -692,7 +712,7 @@ const installNixpacks = () => `
|
||||
echo "Nixpacks already installed ✅"
|
||||
else
|
||||
export NIXPACKS_VERSION=1.41.0
|
||||
bash -c "$(curl -fsSL https://nixpacks.com/install.sh)"
|
||||
$SUDO_CMD bash -c "$(curl -fsSL https://nixpacks.com/install.sh)"
|
||||
echo "Nixpacks version $NIXPACKS_VERSION installed ✅"
|
||||
fi
|
||||
`;
|
||||
@@ -702,11 +722,28 @@ const installRailpack = () => `
|
||||
echo "Railpack already installed ✅"
|
||||
else
|
||||
export RAILPACK_VERSION=0.15.4
|
||||
bash -c "$(curl -fsSL https://railpack.com/install.sh)"
|
||||
$SUDO_CMD bash -c "$(curl -fsSL https://railpack.com/install.sh)"
|
||||
echo "Railpack version $RAILPACK_VERSION installed ✅"
|
||||
fi
|
||||
`;
|
||||
|
||||
const setupPermissions = () => `
|
||||
# Add user to docker group if not root
|
||||
if [ -n "$SUDO_CMD" ]; then
|
||||
if ! groups $CURRENT_USER | grep -qw docker; then
|
||||
$SUDO_CMD usermod -aG docker $CURRENT_USER
|
||||
echo "User $CURRENT_USER added to docker group ✅"
|
||||
else
|
||||
echo "User $CURRENT_USER already in docker group ✅"
|
||||
fi
|
||||
# Ensure the user owns the dokploy directory
|
||||
$SUDO_CMD chown -R $CURRENT_USER:$CURRENT_USER /etc/dokploy
|
||||
echo "Permissions configured for $CURRENT_USER ✅"
|
||||
else
|
||||
echo "Running as root, no extra permissions needed ✅"
|
||||
fi
|
||||
`;
|
||||
|
||||
const installBuildpacks = () => `
|
||||
SUFFIX=""
|
||||
if [ "$SYS_ARCH" = "aarch64" ] || [ "$SYS_ARCH" = "arm64" ]; then
|
||||
@@ -716,7 +753,7 @@ const installBuildpacks = () => `
|
||||
echo "Buildpacks already installed ✅"
|
||||
else
|
||||
BUILDPACKS_VERSION=0.39.1
|
||||
curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.39.1/pack-v$BUILDPACKS_VERSION-linux$SUFFIX.tgz" | tar -C /usr/local/bin/ --no-same-owner -xzv pack
|
||||
curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.39.1/pack-v$BUILDPACKS_VERSION-linux$SUFFIX.tgz" | $SUDO_CMD tar -C /usr/local/bin/ --no-same-owner -xzv pack
|
||||
echo "Buildpacks version $BUILDPACKS_VERSION installed ✅"
|
||||
fi
|
||||
`;
|
||||
|
||||
@@ -79,6 +79,24 @@ export const validateDokployNetwork = () => `
|
||||
fi
|
||||
`;
|
||||
|
||||
export const validateSudoAccess = () => `
|
||||
if [ "$(id -u)" -eq 0 ]; then
|
||||
echo "root true"
|
||||
elif sudo -n true 2>/dev/null; then
|
||||
echo "sudo true"
|
||||
else
|
||||
echo "none false"
|
||||
fi
|
||||
`;
|
||||
|
||||
export const validateDockerGroup = () => `
|
||||
if groups | grep -qw docker; then
|
||||
echo true
|
||||
else
|
||||
echo false
|
||||
fi
|
||||
`;
|
||||
|
||||
export const serverValidate = async (serverId: string) => {
|
||||
const client = new Client();
|
||||
const server = await findServerById(serverId);
|
||||
@@ -118,7 +136,11 @@ export const serverValidate = async (serverId: string) => {
|
||||
isSwarmInstalled=$(${validateSwarm()})
|
||||
isMainDirectoryInstalled=$(${validateMainDirectory()})
|
||||
|
||||
echo "{\\"docker\\": {\\"version\\": \\"$dockerVersion\\", \\"enabled\\": $dockerEnabled}, \\"rclone\\": {\\"version\\": \\"$rcloneVersion\\", \\"enabled\\": $rcloneEnabled}, \\"nixpacks\\": {\\"version\\": \\"$nixpacksVersion\\", \\"enabled\\": $nixpacksEnabled}, \\"buildpacks\\": {\\"version\\": \\"$buildpacksVersion\\", \\"enabled\\": $buildpacksEnabled}, \\"railpack\\": {\\"version\\": \\"$railpackVersion\\", \\"enabled\\": $railpackEnabled}, \\"isDokployNetworkInstalled\\": $isDokployNetworkInstalled, \\"isSwarmInstalled\\": $isSwarmInstalled, \\"isMainDirectoryInstalled\\": $isMainDirectoryInstalled}"
|
||||
sudoAccessResult=$(${validateSudoAccess()})
|
||||
privilegeMode=$(echo $sudoAccessResult | awk '{print $1}')
|
||||
isDockerGroupMember=$(${validateDockerGroup()})
|
||||
|
||||
echo "{\\"docker\\": {\\"version\\": \\"$dockerVersion\\", \\"enabled\\": $dockerEnabled}, \\"rclone\\": {\\"version\\": \\"$rcloneVersion\\", \\"enabled\\": $rcloneEnabled}, \\"nixpacks\\": {\\"version\\": \\"$nixpacksVersion\\", \\"enabled\\": $nixpacksEnabled}, \\"buildpacks\\": {\\"version\\": \\"$buildpacksVersion\\", \\"enabled\\": $buildpacksEnabled}, \\"railpack\\": {\\"version\\": \\"$railpackVersion\\", \\"enabled\\": $railpackEnabled}, \\"isDokployNetworkInstalled\\": $isDokployNetworkInstalled, \\"isSwarmInstalled\\": $isSwarmInstalled, \\"isMainDirectoryInstalled\\": $isMainDirectoryInstalled, \\"privilegeMode\\": \\"$privilegeMode\\", \\"dockerGroupMember\\": $isDockerGroupMember}"
|
||||
`;
|
||||
client.exec(bashCommand, (err, stream) => {
|
||||
if (err) {
|
||||
|
||||
@@ -19,7 +19,7 @@ export const runComposeBackup = async (
|
||||
const project = await findProjectById(environment.projectId);
|
||||
const { prefix, databaseType, serviceName } = backup;
|
||||
const destination = backup.destination;
|
||||
const backupFileName = `${new Date().toISOString()}.sql.gz`;
|
||||
const backupFileName = `${new Date().toISOString()}.${databaseType === "mongo" ? "bson" : "sql"}.gz`;
|
||||
const s3AppName = serviceName ? `${appName}_${serviceName}` : appName;
|
||||
const bucketDestination = `${s3AppName}/${normalizeS3Path(prefix)}${backupFileName}`;
|
||||
const deployment = await createDeploymentBackup({
|
||||
|
||||
@@ -135,8 +135,8 @@ export const keepLatestNBackups = async (
|
||||
const appName = getServiceAppName(backup);
|
||||
const backupFilesPath = `:s3:${backup.destination.bucket}/${appName}/${normalizeS3Path(backup.prefix)}`;
|
||||
|
||||
// --include "*.sql.gz" or "*.zip" ensures nothing else other than the dokploy backup files are touched by rclone
|
||||
const rcloneList = `rclone lsf ${rcloneFlags.join(" ")} --include "*${backup.databaseType === "web-server" ? ".zip" : ".sql.gz"}" ${backupFilesPath}`;
|
||||
// --include "*.bson.gz" or "*.sql.gz" or "*.zip" ensures nothing else other than the dokploy backup files are touched by rclone
|
||||
const rcloneList = `rclone lsf ${rcloneFlags.join(" ")} --include "*${backup.databaseType === "web-server" ? ".zip" : ".{sql.gz,bson.gz}"}" ${backupFilesPath}`;
|
||||
// when we pipe the above command with this one, we only get the list of files we want to delete
|
||||
const sortAndPickUnwantedBackups = `sort -r | tail -n +$((${backup.keepLatestCount}+1)) | xargs -I{}`;
|
||||
// this command deletes the files
|
||||
|
||||
@@ -16,7 +16,7 @@ export const runMongoBackup = async (mongo: Mongo, backup: BackupSchedule) => {
|
||||
const project = await findProjectById(environment.projectId);
|
||||
const { prefix } = backup;
|
||||
const destination = backup.destination;
|
||||
const backupFileName = `${new Date().toISOString()}.sql.gz`;
|
||||
const backupFileName = `${new Date().toISOString()}.bson.gz`;
|
||||
const bucketDestination = `${appName}/${normalizeS3Path(prefix)}${backupFileName}`;
|
||||
const deployment = await createDeploymentBackup({
|
||||
backupId: backup.backupId,
|
||||
|
||||
@@ -79,6 +79,10 @@ export const getS3Credentials = (destination: Destination) => {
|
||||
rcloneFlags.unshift(`--s3-provider="${provider}"`);
|
||||
}
|
||||
|
||||
if (destination.additionalFlags?.length) {
|
||||
rcloneFlags.push(...destination.additionalFlags);
|
||||
}
|
||||
|
||||
return rcloneFlags;
|
||||
};
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
sendEmailNotification,
|
||||
sendGotifyNotification,
|
||||
sendLarkNotification,
|
||||
sendMattermostNotification,
|
||||
sendNtfyNotification,
|
||||
sendPushoverNotification,
|
||||
sendResendNotification,
|
||||
@@ -50,6 +51,7 @@ export const sendBuildErrorNotifications = async ({
|
||||
resend: true,
|
||||
gotify: true,
|
||||
ntfy: true,
|
||||
mattermost: true,
|
||||
custom: true,
|
||||
lark: true,
|
||||
pushover: true,
|
||||
@@ -66,6 +68,7 @@ export const sendBuildErrorNotifications = async ({
|
||||
slack,
|
||||
gotify,
|
||||
ntfy,
|
||||
mattermost,
|
||||
custom,
|
||||
lark,
|
||||
pushover,
|
||||
@@ -250,6 +253,26 @@ export const sendBuildErrorNotifications = async ({
|
||||
});
|
||||
}
|
||||
|
||||
if (mattermost) {
|
||||
await sendMattermostNotification(mattermost, {
|
||||
text: `:warning: **Build Failed**
|
||||
|
||||
**Project:** ${projectName}
|
||||
**Application:** ${applicationName}
|
||||
**Type:** ${applicationType}
|
||||
**Time:** ${date.toLocaleString()}
|
||||
|
||||
**Error:**
|
||||
\`\`\`
|
||||
${errorMessage}
|
||||
\`\`\`
|
||||
|
||||
[View Build Details](${buildLink})`,
|
||||
channel: mattermost.channel,
|
||||
username: mattermost.username || "Dokploy Bot",
|
||||
});
|
||||
}
|
||||
|
||||
if (custom) {
|
||||
await sendCustomNotification(custom, {
|
||||
title: "Build Error",
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
sendEmailNotification,
|
||||
sendGotifyNotification,
|
||||
sendLarkNotification,
|
||||
sendMattermostNotification,
|
||||
sendNtfyNotification,
|
||||
sendPushoverNotification,
|
||||
sendResendNotification,
|
||||
@@ -53,6 +54,7 @@ export const sendBuildSuccessNotifications = async ({
|
||||
resend: true,
|
||||
gotify: true,
|
||||
ntfy: true,
|
||||
mattermost: true,
|
||||
custom: true,
|
||||
lark: true,
|
||||
pushover: true,
|
||||
@@ -69,6 +71,7 @@ export const sendBuildSuccessNotifications = async ({
|
||||
slack,
|
||||
gotify,
|
||||
ntfy,
|
||||
mattermost,
|
||||
custom,
|
||||
lark,
|
||||
pushover,
|
||||
@@ -266,6 +269,14 @@ export const sendBuildSuccessNotifications = async ({
|
||||
});
|
||||
}
|
||||
|
||||
if (mattermost) {
|
||||
await sendMattermostNotification(mattermost, {
|
||||
text: `**✅ Build Success**\n\n**Project:** ${projectName}\n**Application:** ${applicationName}\n**Type:** ${applicationType}\n**Date:** ${format(date, "PP")}\n**Time:** ${format(date, "pp")}\n\n[View Build Details](${buildLink})`,
|
||||
channel: mattermost.channel,
|
||||
username: mattermost.username || "Dokploy",
|
||||
});
|
||||
}
|
||||
|
||||
if (custom) {
|
||||
await sendCustomNotification(custom, {
|
||||
title: "Build Success",
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
sendEmailNotification,
|
||||
sendGotifyNotification,
|
||||
sendLarkNotification,
|
||||
sendMattermostNotification,
|
||||
sendNtfyNotification,
|
||||
sendPushoverNotification,
|
||||
sendResendNotification,
|
||||
@@ -50,6 +51,7 @@ export const sendDatabaseBackupNotifications = async ({
|
||||
resend: true,
|
||||
gotify: true,
|
||||
ntfy: true,
|
||||
mattermost: true,
|
||||
custom: true,
|
||||
lark: true,
|
||||
pushover: true,
|
||||
@@ -66,6 +68,7 @@ export const sendDatabaseBackupNotifications = async ({
|
||||
slack,
|
||||
gotify,
|
||||
ntfy,
|
||||
mattermost,
|
||||
custom,
|
||||
lark,
|
||||
pushover,
|
||||
@@ -272,6 +275,21 @@ export const sendDatabaseBackupNotifications = async ({
|
||||
});
|
||||
}
|
||||
|
||||
if (mattermost) {
|
||||
const statusEmoji = type === "success" ? "✅" : "❌";
|
||||
const typeStatus = type === "success" ? "Successful" : "Failed";
|
||||
const errorMsg =
|
||||
type === "error" && errorMessage
|
||||
? `\n\n**Error:**\n\`\`\`\n${errorMessage}\n\`\`\``
|
||||
: "";
|
||||
|
||||
await sendMattermostNotification(mattermost, {
|
||||
text: `**${statusEmoji} Database Backup ${typeStatus}**\n\n**Project:** ${projectName}\n**Application:** ${applicationName}\n**Type:** ${databaseType}\n**Database Name:** ${databaseName}\n**Date:** ${format(date, "PP")}\n**Time:** ${format(date, "pp")}${errorMsg}`,
|
||||
channel: mattermost.channel,
|
||||
username: mattermost.username || "Dokploy",
|
||||
});
|
||||
}
|
||||
|
||||
if (custom) {
|
||||
await sendCustomNotification(custom, {
|
||||
title: `Database Backup ${type === "success" ? "Successful" : "Failed"}`,
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
sendEmailNotification,
|
||||
sendGotifyNotification,
|
||||
sendLarkNotification,
|
||||
sendMattermostNotification,
|
||||
sendNtfyNotification,
|
||||
sendPushoverNotification,
|
||||
sendResendNotification,
|
||||
@@ -37,6 +38,7 @@ export const sendDockerCleanupNotifications = async (
|
||||
resend: true,
|
||||
gotify: true,
|
||||
ntfy: true,
|
||||
mattermost: true,
|
||||
custom: true,
|
||||
lark: true,
|
||||
pushover: true,
|
||||
@@ -53,6 +55,7 @@ export const sendDockerCleanupNotifications = async (
|
||||
slack,
|
||||
gotify,
|
||||
ntfy,
|
||||
mattermost,
|
||||
custom,
|
||||
lark,
|
||||
pushover,
|
||||
@@ -168,6 +171,14 @@ export const sendDockerCleanupNotifications = async (
|
||||
});
|
||||
}
|
||||
|
||||
if (mattermost) {
|
||||
await sendMattermostNotification(mattermost, {
|
||||
text: `**✅ Docker Cleanup**\n\n**Message:** ${message}\n**Date:** ${format(date, "PP")}\n**Time:** ${format(date, "pp")}`,
|
||||
channel: mattermost.channel,
|
||||
username: mattermost.username || "Dokploy",
|
||||
});
|
||||
}
|
||||
|
||||
if (custom) {
|
||||
await sendCustomNotification(custom, {
|
||||
title: "Docker Cleanup",
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
sendEmailNotification,
|
||||
sendGotifyNotification,
|
||||
sendLarkNotification,
|
||||
sendMattermostNotification,
|
||||
sendNtfyNotification,
|
||||
sendPushoverNotification,
|
||||
sendResendNotification,
|
||||
@@ -32,6 +33,7 @@ export const sendDokployRestartNotifications = async () => {
|
||||
resend: true,
|
||||
gotify: true,
|
||||
ntfy: true,
|
||||
mattermost: true,
|
||||
custom: true,
|
||||
lark: true,
|
||||
pushover: true,
|
||||
@@ -48,6 +50,7 @@ export const sendDokployRestartNotifications = async () => {
|
||||
slack,
|
||||
gotify,
|
||||
ntfy,
|
||||
mattermost,
|
||||
custom,
|
||||
lark,
|
||||
pushover,
|
||||
@@ -158,6 +161,14 @@ export const sendDokployRestartNotifications = async () => {
|
||||
});
|
||||
}
|
||||
|
||||
if (mattermost) {
|
||||
await sendMattermostNotification(mattermost, {
|
||||
text: `**✅ Dokploy Server Restarted**\n\n**Date:** ${format(date, "PP")}\n**Time:** ${format(date, "pp")}`,
|
||||
channel: mattermost.channel,
|
||||
username: mattermost.username || "Dokploy",
|
||||
});
|
||||
}
|
||||
|
||||
if (custom) {
|
||||
try {
|
||||
await sendCustomNotification(custom, {
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
sendCustomNotification,
|
||||
sendDiscordNotification,
|
||||
sendLarkNotification,
|
||||
sendMattermostNotification,
|
||||
sendPushoverNotification,
|
||||
sendSlackNotification,
|
||||
sendTeamsNotification,
|
||||
@@ -38,6 +39,7 @@ export const sendServerThresholdNotifications = async (
|
||||
discord: true,
|
||||
telegram: true,
|
||||
slack: true,
|
||||
mattermost: true,
|
||||
custom: true,
|
||||
lark: true,
|
||||
pushover: true,
|
||||
@@ -49,63 +51,72 @@ export const sendServerThresholdNotifications = async (
|
||||
const typeColor = 0xff0000; // Rojo para indicar alerta
|
||||
|
||||
for (const notification of notificationList) {
|
||||
const { discord, telegram, slack, custom, lark, pushover, teams } =
|
||||
notification;
|
||||
const {
|
||||
discord,
|
||||
telegram,
|
||||
slack,
|
||||
mattermost,
|
||||
custom,
|
||||
lark,
|
||||
pushover,
|
||||
teams,
|
||||
} = notification;
|
||||
|
||||
if (discord) {
|
||||
const decorate = (decoration: string, text: string) =>
|
||||
`${discord.decoration ? decoration : ""} ${text}`.trim();
|
||||
try {
|
||||
if (discord) {
|
||||
const decorate = (decoration: string, text: string) =>
|
||||
`${discord.decoration ? decoration : ""} ${text}`.trim();
|
||||
|
||||
await sendDiscordNotification(discord, {
|
||||
title: decorate(">", `\`⚠️\` Server ${payload.Type} Alert`),
|
||||
color: typeColor,
|
||||
fields: [
|
||||
{
|
||||
name: decorate("`🏷️`", "Server Name"),
|
||||
value: payload.ServerName,
|
||||
inline: true,
|
||||
await sendDiscordNotification(discord, {
|
||||
title: decorate(">", `\`⚠️\` Server ${payload.Type} Alert`),
|
||||
color: typeColor,
|
||||
fields: [
|
||||
{
|
||||
name: decorate("`🏷️`", "Server Name"),
|
||||
value: payload.ServerName,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: decorate("`📅`", "Date"),
|
||||
value: `<t:${unixDate}:D>`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: decorate("`⌚`", "Time"),
|
||||
value: `<t:${unixDate}:t>`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: decorate(typeEmoji, "Type"),
|
||||
value: payload.Type,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: decorate("📊", "Current Value"),
|
||||
value: `${payload.Value.toFixed(2)}%`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: decorate("⚠️", "Threshold"),
|
||||
value: `${payload.Threshold.toFixed(2)}%`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: decorate("`📜`", "Message"),
|
||||
value: `\`\`\`${payload.Message}\`\`\``,
|
||||
},
|
||||
],
|
||||
timestamp: date.toISOString(),
|
||||
footer: {
|
||||
text: "Dokploy Server Monitoring Alert",
|
||||
},
|
||||
{
|
||||
name: decorate("`📅`", "Date"),
|
||||
value: `<t:${unixDate}:D>`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: decorate("`⌚`", "Time"),
|
||||
value: `<t:${unixDate}:t>`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: decorate(typeEmoji, "Type"),
|
||||
value: payload.Type,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: decorate("📊", "Current Value"),
|
||||
value: `${payload.Value.toFixed(2)}%`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: decorate("⚠️", "Threshold"),
|
||||
value: `${payload.Threshold.toFixed(2)}%`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: decorate("`📜`", "Message"),
|
||||
value: `\`\`\`${payload.Message}\`\`\``,
|
||||
},
|
||||
],
|
||||
timestamp: date.toISOString(),
|
||||
footer: {
|
||||
text: "Dokploy Server Monitoring Alert",
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (telegram) {
|
||||
await sendTelegramNotification(
|
||||
telegram,
|
||||
`
|
||||
if (telegram) {
|
||||
await sendTelegramNotification(
|
||||
telegram,
|
||||
`
|
||||
<b>⚠️ Server ${payload.Type} Alert</b>
|
||||
<b>Server Name:</b> ${payload.ServerName}
|
||||
<b>Type:</b> ${payload.Type}
|
||||
@@ -114,170 +125,181 @@ export const sendServerThresholdNotifications = async (
|
||||
<b>Message:</b> ${payload.Message}
|
||||
<b>Time:</b> ${date.toLocaleString()}
|
||||
`,
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (slack) {
|
||||
const { channel } = slack;
|
||||
await sendSlackNotification(slack, {
|
||||
channel: channel,
|
||||
attachments: [
|
||||
{
|
||||
color: "#FF0000",
|
||||
pretext: `:warning: *Server ${payload.Type} Alert*`,
|
||||
fields: [
|
||||
{
|
||||
title: "Server Name",
|
||||
value: payload.ServerName,
|
||||
short: true,
|
||||
},
|
||||
{
|
||||
title: "Type",
|
||||
value: payload.Type,
|
||||
short: true,
|
||||
},
|
||||
{
|
||||
title: "Current Value",
|
||||
value: `${payload.Value.toFixed(2)}%`,
|
||||
short: true,
|
||||
},
|
||||
{
|
||||
title: "Threshold",
|
||||
value: `${payload.Threshold.toFixed(2)}%`,
|
||||
short: true,
|
||||
},
|
||||
{
|
||||
title: "Message",
|
||||
value: payload.Message,
|
||||
},
|
||||
{
|
||||
title: "Time",
|
||||
value: date.toLocaleString(),
|
||||
short: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
if (slack) {
|
||||
const { channel } = slack;
|
||||
await sendSlackNotification(slack, {
|
||||
channel: channel,
|
||||
attachments: [
|
||||
{
|
||||
color: "#FF0000",
|
||||
pretext: `:warning: *Server ${payload.Type} Alert*`,
|
||||
fields: [
|
||||
{
|
||||
title: "Server Name",
|
||||
value: payload.ServerName,
|
||||
short: true,
|
||||
},
|
||||
{
|
||||
title: "Type",
|
||||
value: payload.Type,
|
||||
short: true,
|
||||
},
|
||||
{
|
||||
title: "Current Value",
|
||||
value: `${payload.Value.toFixed(2)}%`,
|
||||
short: true,
|
||||
},
|
||||
{
|
||||
title: "Threshold",
|
||||
value: `${payload.Threshold.toFixed(2)}%`,
|
||||
short: true,
|
||||
},
|
||||
{
|
||||
title: "Message",
|
||||
value: payload.Message,
|
||||
},
|
||||
{
|
||||
title: "Time",
|
||||
value: date.toLocaleString(),
|
||||
short: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
if (custom) {
|
||||
await sendCustomNotification(custom, {
|
||||
title: `Server ${payload.Type} Alert`,
|
||||
message: payload.Message,
|
||||
serverName: payload.ServerName,
|
||||
type: payload.Type,
|
||||
currentValue: payload.Value,
|
||||
threshold: payload.Threshold,
|
||||
timestamp: date.toISOString(),
|
||||
date: date.toLocaleString(),
|
||||
status: "alert",
|
||||
alertType: "server-threshold",
|
||||
});
|
||||
}
|
||||
if (mattermost) {
|
||||
await sendMattermostNotification(mattermost, {
|
||||
text: `**⚠️ Server ${payload.Type} Alert**\n\n**Server Name:** ${payload.ServerName}\n**Type:** ${payload.Type}\n**Current Value:** ${payload.Value.toFixed(2)}%\n**Threshold:** ${payload.Threshold.toFixed(2)}%\n**Message:** ${payload.Message}\n**Time:** ${date.toLocaleString()}`,
|
||||
channel: mattermost.channel,
|
||||
username: mattermost.username || "Dokploy",
|
||||
});
|
||||
}
|
||||
|
||||
if (lark) {
|
||||
await sendLarkNotification(lark, {
|
||||
msg_type: "interactive",
|
||||
card: {
|
||||
schema: "2.0",
|
||||
config: {
|
||||
update_multi: true,
|
||||
style: {
|
||||
text_size: {
|
||||
normal_v2: {
|
||||
default: "normal",
|
||||
pc: "normal",
|
||||
mobile: "heading",
|
||||
if (custom) {
|
||||
await sendCustomNotification(custom, {
|
||||
title: `Server ${payload.Type} Alert`,
|
||||
message: payload.Message,
|
||||
serverName: payload.ServerName,
|
||||
type: payload.Type,
|
||||
currentValue: payload.Value,
|
||||
threshold: payload.Threshold,
|
||||
timestamp: date.toISOString(),
|
||||
date: date.toLocaleString(),
|
||||
status: "alert",
|
||||
alertType: "server-threshold",
|
||||
});
|
||||
}
|
||||
|
||||
if (lark) {
|
||||
await sendLarkNotification(lark, {
|
||||
msg_type: "interactive",
|
||||
card: {
|
||||
schema: "2.0",
|
||||
config: {
|
||||
update_multi: true,
|
||||
style: {
|
||||
text_size: {
|
||||
normal_v2: {
|
||||
default: "normal",
|
||||
pc: "normal",
|
||||
mobile: "heading",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
header: {
|
||||
title: {
|
||||
tag: "plain_text",
|
||||
content: `⚠️ Server ${payload.Type} Alert`,
|
||||
},
|
||||
subtitle: {
|
||||
tag: "plain_text",
|
||||
content: "",
|
||||
},
|
||||
template: "red",
|
||||
padding: "12px 12px 12px 12px",
|
||||
},
|
||||
body: {
|
||||
direction: "vertical",
|
||||
padding: "12px 12px 12px 12px",
|
||||
elements: [
|
||||
{
|
||||
tag: "column_set",
|
||||
columns: [
|
||||
{
|
||||
tag: "column",
|
||||
width: "weighted",
|
||||
elements: [
|
||||
{
|
||||
tag: "markdown",
|
||||
content: `**Server Name:**\n${payload.ServerName}`,
|
||||
text_align: "left",
|
||||
text_size: "normal_v2",
|
||||
},
|
||||
{
|
||||
tag: "markdown",
|
||||
content: `**Current Value:**\n${payload.Value.toFixed(2)}%`,
|
||||
text_align: "left",
|
||||
text_size: "normal_v2",
|
||||
},
|
||||
{
|
||||
tag: "markdown",
|
||||
content: `**Alert Message:**\n${payload.Message}`,
|
||||
text_align: "left",
|
||||
text_size: "normal_v2",
|
||||
},
|
||||
],
|
||||
vertical_align: "top",
|
||||
weight: 1,
|
||||
},
|
||||
{
|
||||
tag: "column",
|
||||
width: "weighted",
|
||||
elements: [
|
||||
{
|
||||
tag: "markdown",
|
||||
content: `**Type:**\n${payload.Type === "CPU" ? "🔲" : "💾"} ${payload.Type}`,
|
||||
text_align: "left",
|
||||
text_size: "normal_v2",
|
||||
},
|
||||
{
|
||||
tag: "markdown",
|
||||
content: `**Threshold:**\n${payload.Threshold.toFixed(2)}%`,
|
||||
text_align: "left",
|
||||
text_size: "normal_v2",
|
||||
},
|
||||
{
|
||||
tag: "markdown",
|
||||
content: `**Alert Time:**\n${date.toLocaleString()}`,
|
||||
text_align: "left",
|
||||
text_size: "normal_v2",
|
||||
},
|
||||
],
|
||||
vertical_align: "top",
|
||||
weight: 1,
|
||||
},
|
||||
],
|
||||
header: {
|
||||
title: {
|
||||
tag: "plain_text",
|
||||
content: `⚠️ Server ${payload.Type} Alert`,
|
||||
},
|
||||
],
|
||||
subtitle: {
|
||||
tag: "plain_text",
|
||||
content: "",
|
||||
},
|
||||
template: "red",
|
||||
padding: "12px 12px 12px 12px",
|
||||
},
|
||||
body: {
|
||||
direction: "vertical",
|
||||
padding: "12px 12px 12px 12px",
|
||||
elements: [
|
||||
{
|
||||
tag: "column_set",
|
||||
columns: [
|
||||
{
|
||||
tag: "column",
|
||||
width: "weighted",
|
||||
elements: [
|
||||
{
|
||||
tag: "markdown",
|
||||
content: `**Server Name:**\n${payload.ServerName}`,
|
||||
text_align: "left",
|
||||
text_size: "normal_v2",
|
||||
},
|
||||
{
|
||||
tag: "markdown",
|
||||
content: `**Current Value:**\n${payload.Value.toFixed(2)}%`,
|
||||
text_align: "left",
|
||||
text_size: "normal_v2",
|
||||
},
|
||||
{
|
||||
tag: "markdown",
|
||||
content: `**Alert Message:**\n${payload.Message}`,
|
||||
text_align: "left",
|
||||
text_size: "normal_v2",
|
||||
},
|
||||
],
|
||||
vertical_align: "top",
|
||||
weight: 1,
|
||||
},
|
||||
{
|
||||
tag: "column",
|
||||
width: "weighted",
|
||||
elements: [
|
||||
{
|
||||
tag: "markdown",
|
||||
content: `**Type:**\n${payload.Type === "CPU" ? "🔲" : "💾"} ${payload.Type}`,
|
||||
text_align: "left",
|
||||
text_size: "normal_v2",
|
||||
},
|
||||
{
|
||||
tag: "markdown",
|
||||
content: `**Threshold:**\n${payload.Threshold.toFixed(2)}%`,
|
||||
text_align: "left",
|
||||
text_size: "normal_v2",
|
||||
},
|
||||
{
|
||||
tag: "markdown",
|
||||
content: `**Alert Time:**\n${date.toLocaleString()}`,
|
||||
text_align: "left",
|
||||
text_size: "normal_v2",
|
||||
},
|
||||
],
|
||||
vertical_align: "top",
|
||||
weight: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (pushover) {
|
||||
await sendPushoverNotification(
|
||||
pushover,
|
||||
`Server ${payload.Type} Alert`,
|
||||
`Server: ${payload.ServerName}\nType: ${payload.Type}\nCurrent: ${payload.Value.toFixed(2)}%\nThreshold: ${payload.Threshold.toFixed(2)}%\nMessage: ${payload.Message}\nTime: ${date.toLocaleString()}`,
|
||||
);
|
||||
if (pushover) {
|
||||
await sendPushoverNotification(
|
||||
pushover,
|
||||
`Server ${payload.Type} Alert`,
|
||||
`Server: ${payload.ServerName}\nType: ${payload.Type}\nCurrent: ${payload.Value.toFixed(2)}%\nThreshold: ${payload.Threshold.toFixed(2)}%\nMessage: ${payload.Message}\nTime: ${date.toLocaleString()}`,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
|
||||
if (teams) {
|
||||
|
||||
@@ -4,6 +4,7 @@ import type {
|
||||
email,
|
||||
gotify,
|
||||
lark,
|
||||
mattermost,
|
||||
ntfy,
|
||||
pushover,
|
||||
resend,
|
||||
@@ -206,6 +207,33 @@ export const sendNtfyNotification = async (
|
||||
}
|
||||
};
|
||||
|
||||
export const sendMattermostNotification = async (
|
||||
connection: typeof mattermost.$inferInsert,
|
||||
message: any,
|
||||
) => {
|
||||
const payload = {
|
||||
...message,
|
||||
// Only include username if it's provided and not empty
|
||||
...(message.username?.trim() && { username: message.username }),
|
||||
// Only include channel if it's provided and not empty
|
||||
...(message.channel?.trim() && {
|
||||
channel: `#${message.channel.replace("#", "")}`,
|
||||
}),
|
||||
};
|
||||
|
||||
const response = await fetch(connection.webhookUrl, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to send Mattermost notification: ${response.statusText}`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const sendCustomNotification = async (
|
||||
connection: typeof custom.$inferInsert,
|
||||
payload: Record<string, any>,
|
||||
|
||||
@@ -9,6 +9,8 @@ import {
|
||||
sendDiscordNotification,
|
||||
sendEmailNotification,
|
||||
sendGotifyNotification,
|
||||
sendLarkNotification,
|
||||
sendMattermostNotification,
|
||||
sendNtfyNotification,
|
||||
sendPushoverNotification,
|
||||
sendResendNotification,
|
||||
@@ -59,9 +61,11 @@ export const sendVolumeBackupNotifications = async ({
|
||||
resend: true,
|
||||
gotify: true,
|
||||
ntfy: true,
|
||||
mattermost: true,
|
||||
custom: true,
|
||||
lark: true,
|
||||
pushover: true,
|
||||
teams: true,
|
||||
custom: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -74,278 +78,420 @@ export const sendVolumeBackupNotifications = async ({
|
||||
slack,
|
||||
gotify,
|
||||
ntfy,
|
||||
mattermost,
|
||||
custom,
|
||||
lark,
|
||||
pushover,
|
||||
teams,
|
||||
custom,
|
||||
} = notification;
|
||||
|
||||
if (email || resend) {
|
||||
const subject = `Volume Backup ${type === "success" ? "Successful" : "Failed"} - ${applicationName}`;
|
||||
const htmlContent = await renderAsync(
|
||||
VolumeBackupEmail({
|
||||
try {
|
||||
if (email || resend) {
|
||||
const subject = `Volume Backup ${type === "success" ? "Successful" : "Failed"} - ${applicationName}`;
|
||||
const htmlContent = await renderAsync(
|
||||
VolumeBackupEmail({
|
||||
projectName,
|
||||
applicationName,
|
||||
volumeName,
|
||||
serviceType,
|
||||
type,
|
||||
errorMessage,
|
||||
backupSize,
|
||||
date: date.toISOString(),
|
||||
}),
|
||||
);
|
||||
if (email) {
|
||||
await sendEmailNotification(email, subject, htmlContent);
|
||||
}
|
||||
if (resend) {
|
||||
await sendResendNotification(resend, subject, htmlContent);
|
||||
}
|
||||
}
|
||||
|
||||
if (discord) {
|
||||
const decorate = (decoration: string, text: string) =>
|
||||
`${discord.decoration ? decoration : ""} ${text}`.trim();
|
||||
|
||||
await sendDiscordNotification(discord, {
|
||||
title:
|
||||
type === "success"
|
||||
? decorate(">", "`✅` Volume Backup Successful")
|
||||
: decorate(">", "`❌` Volume Backup Failed"),
|
||||
color: type === "success" ? 0x57f287 : 0xed4245,
|
||||
fields: [
|
||||
{
|
||||
name: decorate("`🛠️`", "Project"),
|
||||
value: projectName,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: decorate("`⚙️`", "Application"),
|
||||
value: applicationName,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: decorate("`💾`", "Volume Name"),
|
||||
value: volumeName,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: decorate("`🔧`", "Service Type"),
|
||||
value: serviceType,
|
||||
inline: true,
|
||||
},
|
||||
...(backupSize
|
||||
? [
|
||||
{
|
||||
name: decorate("`📊`", "Backup Size"),
|
||||
value: backupSize,
|
||||
inline: true,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
name: decorate("`📅`", "Date"),
|
||||
value: `<t:${unixDate}:D>`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: decorate("`⌚`", "Time"),
|
||||
value: `<t:${unixDate}:t>`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: decorate("`❓`", "Type"),
|
||||
value: type
|
||||
.replace("error", "Failed")
|
||||
.replace("success", "Successful"),
|
||||
inline: true,
|
||||
},
|
||||
...(type === "error" && errorMessage
|
||||
? [
|
||||
{
|
||||
name: decorate("`⚠️`", "Error Message"),
|
||||
value: `\`\`\`${errorMessage}\`\`\``,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
timestamp: date.toISOString(),
|
||||
footer: {
|
||||
text: "Dokploy Volume Backup Notification",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (gotify) {
|
||||
const decorate = (decoration: string, text: string) =>
|
||||
`${gotify.decoration ? decoration : ""} ${text}\n`;
|
||||
|
||||
await sendGotifyNotification(
|
||||
gotify,
|
||||
decorate(
|
||||
type === "success" ? "✅" : "❌",
|
||||
`Volume Backup ${type === "success" ? "Successful" : "Failed"}`,
|
||||
),
|
||||
`${decorate("🛠️", `Project: ${projectName}`)}` +
|
||||
`${decorate("⚙️", `Application: ${applicationName}`)}` +
|
||||
`${decorate("💾", `Volume Name: ${volumeName}`)}` +
|
||||
`${decorate("🔧", `Service Type: ${serviceType}`)}` +
|
||||
`${backupSize ? decorate("📊", `Backup Size: ${backupSize}`) : ""}` +
|
||||
`${decorate("🕒", `Date: ${date.toLocaleString()}`)}` +
|
||||
`${type === "error" && errorMessage ? decorate("❌", `Error:\n${errorMessage}`) : ""}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (ntfy) {
|
||||
await sendNtfyNotification(
|
||||
ntfy,
|
||||
`Volume Backup ${type === "success" ? "Successful" : "Failed"}`,
|
||||
`${type === "success" ? "white_check_mark" : "x"}`,
|
||||
"",
|
||||
`🛠️Project: ${projectName}\n` +
|
||||
`⚙️Application: ${applicationName}\n` +
|
||||
`💾Volume Name: ${volumeName}\n` +
|
||||
`🔧Service Type: ${serviceType}\n` +
|
||||
`${backupSize ? `📊Backup Size: ${backupSize}\n` : ""}` +
|
||||
`🕒Date: ${date.toLocaleString()}\n` +
|
||||
`${type === "error" && errorMessage ? `❌Error:\n${errorMessage}` : ""}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (telegram) {
|
||||
const isError = type === "error" && errorMessage;
|
||||
|
||||
const statusEmoji = type === "success" ? "✅" : "❌";
|
||||
const typeStatus = type === "success" ? "Successful" : "Failed";
|
||||
const errorMsg = isError
|
||||
? `\n\n<b>Error:</b>\n<pre>${errorMessage}</pre>`
|
||||
: "";
|
||||
const sizeInfo = backupSize
|
||||
? `\n<b>Backup Size:</b> ${backupSize}`
|
||||
: "";
|
||||
|
||||
const messageText = `<b>${statusEmoji} Volume Backup ${typeStatus}</b>\n\n<b>Project:</b> ${projectName}\n<b>Application:</b> ${applicationName}\n<b>Volume Name:</b> ${volumeName}\n<b>Service Type:</b> ${serviceType}${sizeInfo}\n<b>Date:</b> ${format(date, "PP")}\n<b>Time:</b> ${format(date, "pp")}${isError ? errorMsg : ""}`;
|
||||
|
||||
await sendTelegramNotification(telegram, messageText);
|
||||
}
|
||||
|
||||
if (slack) {
|
||||
const { channel } = slack;
|
||||
await sendSlackNotification(slack, {
|
||||
channel: channel,
|
||||
attachments: [
|
||||
{
|
||||
color: type === "success" ? "#00FF00" : "#FF0000",
|
||||
pretext:
|
||||
type === "success"
|
||||
? ":white_check_mark: *Volume Backup Successful*"
|
||||
: ":x: *Volume Backup Failed*",
|
||||
fields: [
|
||||
...(type === "error" && errorMessage
|
||||
? [
|
||||
{
|
||||
title: "Error Message",
|
||||
value: errorMessage,
|
||||
short: false,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
title: "Project",
|
||||
value: projectName,
|
||||
short: true,
|
||||
},
|
||||
{
|
||||
title: "Application",
|
||||
value: applicationName,
|
||||
short: true,
|
||||
},
|
||||
{
|
||||
title: "Volume Name",
|
||||
value: volumeName,
|
||||
short: true,
|
||||
},
|
||||
{
|
||||
title: "Service Type",
|
||||
value: serviceType,
|
||||
short: true,
|
||||
},
|
||||
...(backupSize
|
||||
? [
|
||||
{
|
||||
title: "Backup Size",
|
||||
value: backupSize,
|
||||
short: true,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
title: "Time",
|
||||
value: date.toLocaleString(),
|
||||
short: true,
|
||||
},
|
||||
{
|
||||
title: "Type",
|
||||
value: type,
|
||||
short: true,
|
||||
},
|
||||
{
|
||||
title: "Status",
|
||||
value: type === "success" ? "Successful" : "Failed",
|
||||
short: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
if (mattermost) {
|
||||
const statusEmoji = type === "success" ? "✅" : "❌";
|
||||
const typeStatus = type === "success" ? "Successful" : "Failed";
|
||||
const errorMsg =
|
||||
type === "error" && errorMessage
|
||||
? `\n\n**Error:**\n\`\`\`\n${errorMessage}\n\`\`\``
|
||||
: "";
|
||||
const sizeInfo = backupSize ? `\n**Backup Size:** ${backupSize}` : "";
|
||||
|
||||
await sendMattermostNotification(mattermost, {
|
||||
text: `**${statusEmoji} Volume Backup ${typeStatus}**\n\n**Project:** ${projectName}\n**Application:** ${applicationName}\n**Volume Name:** ${volumeName}\n**Service Type:** ${serviceType}${sizeInfo}\n**Date:** ${format(date, "PP")}\n**Time:** ${format(date, "pp")}${errorMsg}`,
|
||||
channel: mattermost.channel,
|
||||
username: mattermost.username || "Dokploy",
|
||||
});
|
||||
}
|
||||
|
||||
if (lark) {
|
||||
const limitCharacter = 800;
|
||||
const truncatedErrorMessage =
|
||||
errorMessage && errorMessage.length > limitCharacter
|
||||
? errorMessage.substring(0, limitCharacter)
|
||||
: errorMessage;
|
||||
|
||||
await sendLarkNotification(lark, {
|
||||
msg_type: "interactive",
|
||||
card: {
|
||||
schema: "2.0",
|
||||
config: {
|
||||
update_multi: true,
|
||||
style: {
|
||||
text_size: {
|
||||
normal_v2: {
|
||||
default: "normal",
|
||||
pc: "normal",
|
||||
mobile: "heading",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
header: {
|
||||
title: {
|
||||
tag: "plain_text",
|
||||
content:
|
||||
type === "success"
|
||||
? "✅ Volume Backup Successful"
|
||||
: "❌ Volume Backup Failed",
|
||||
},
|
||||
subtitle: {
|
||||
tag: "plain_text",
|
||||
content: "",
|
||||
},
|
||||
template: type === "success" ? "green" : "red",
|
||||
padding: "12px 12px 12px 12px",
|
||||
},
|
||||
body: {
|
||||
direction: "vertical",
|
||||
padding: "12px 12px 12px 12px",
|
||||
elements: [
|
||||
{
|
||||
tag: "column_set",
|
||||
columns: [
|
||||
{
|
||||
tag: "column",
|
||||
width: "weighted",
|
||||
elements: [
|
||||
{
|
||||
tag: "markdown",
|
||||
content: `**Project:**\n${projectName}`,
|
||||
text_align: "left",
|
||||
text_size: "normal_v2",
|
||||
},
|
||||
{
|
||||
tag: "markdown",
|
||||
content: `**Volume Name:**\n${volumeName}`,
|
||||
text_align: "left",
|
||||
text_size: "normal_v2",
|
||||
},
|
||||
{
|
||||
tag: "markdown",
|
||||
content: `**Status:**\n${type === "success" ? "Successful" : "Failed"}`,
|
||||
text_align: "left",
|
||||
text_size: "normal_v2",
|
||||
},
|
||||
],
|
||||
vertical_align: "top",
|
||||
weight: 1,
|
||||
},
|
||||
{
|
||||
tag: "column",
|
||||
width: "weighted",
|
||||
elements: [
|
||||
{
|
||||
tag: "markdown",
|
||||
content: `**Application:**\n${applicationName}`,
|
||||
text_align: "left",
|
||||
text_size: "normal_v2",
|
||||
},
|
||||
{
|
||||
tag: "markdown",
|
||||
content: `**Service Type:**\n${serviceType}`,
|
||||
text_align: "left",
|
||||
text_size: "normal_v2",
|
||||
},
|
||||
{
|
||||
tag: "markdown",
|
||||
content: `**Date:**\n${format(date, "PP pp")}`,
|
||||
text_align: "left",
|
||||
text_size: "normal_v2",
|
||||
},
|
||||
],
|
||||
vertical_align: "top",
|
||||
weight: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
...(type === "error" && truncatedErrorMessage
|
||||
? [
|
||||
{
|
||||
tag: "markdown",
|
||||
content: `**Error Message:**\n\`\`\`\n${truncatedErrorMessage}\n\`\`\``,
|
||||
text_align: "left",
|
||||
text_size: "normal_v2",
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (pushover) {
|
||||
await sendPushoverNotification(
|
||||
pushover,
|
||||
`Volume Backup ${type === "success" ? "Successful" : "Failed"}`,
|
||||
`Project: ${projectName}\nApplication: ${applicationName}\nVolume: ${volumeName}\nService Type: ${serviceType}${backupSize ? `\nBackup Size: ${backupSize}` : ""}\nDate: ${date.toLocaleString()}${type === "error" && errorMessage ? `\nError: ${errorMessage}` : ""}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (teams) {
|
||||
const facts = [
|
||||
{ name: "Project", value: projectName },
|
||||
{ name: "Application", value: applicationName },
|
||||
{ name: "Volume Name", value: volumeName },
|
||||
{ name: "Service Type", value: serviceType },
|
||||
{ name: "Date", value: format(date, "PP pp") },
|
||||
{
|
||||
name: "Status",
|
||||
value: type === "success" ? "Successful" : "Failed",
|
||||
},
|
||||
];
|
||||
if (backupSize) {
|
||||
facts.push({ name: "Backup Size", value: backupSize });
|
||||
}
|
||||
if (type === "error" && errorMessage) {
|
||||
facts.push({ name: "Error", value: errorMessage.substring(0, 500) });
|
||||
}
|
||||
await sendTeamsNotification(teams, {
|
||||
title:
|
||||
type === "success"
|
||||
? "✅ Volume Backup Successful"
|
||||
: "❌ Volume Backup Failed",
|
||||
facts,
|
||||
});
|
||||
}
|
||||
|
||||
if (custom) {
|
||||
await sendCustomNotification(custom, {
|
||||
title: `Volume Backup ${type === "success" ? "Successful" : "Failed"}`,
|
||||
message:
|
||||
type === "success"
|
||||
? "Volume backup completed successfully"
|
||||
: "Volume backup failed",
|
||||
projectName,
|
||||
applicationName,
|
||||
volumeName,
|
||||
serviceType,
|
||||
type,
|
||||
errorMessage,
|
||||
backupSize,
|
||||
date: date.toISOString(),
|
||||
}),
|
||||
);
|
||||
if (email) {
|
||||
await sendEmailNotification(email, subject, htmlContent);
|
||||
errorMessage: errorMessage ?? "",
|
||||
backupSize: backupSize ?? "",
|
||||
timestamp: date.toISOString(),
|
||||
date: date.toLocaleString(),
|
||||
status: type,
|
||||
});
|
||||
}
|
||||
if (resend) {
|
||||
await sendResendNotification(resend, subject, htmlContent);
|
||||
}
|
||||
}
|
||||
|
||||
if (discord) {
|
||||
const decorate = (decoration: string, text: string) =>
|
||||
`${discord.decoration ? decoration : ""} ${text}`.trim();
|
||||
|
||||
await sendDiscordNotification(discord, {
|
||||
title:
|
||||
type === "success"
|
||||
? decorate(">", "`✅` Volume Backup Successful")
|
||||
: decorate(">", "`❌` Volume Backup Failed"),
|
||||
color: type === "success" ? 0x57f287 : 0xed4245,
|
||||
fields: [
|
||||
{
|
||||
name: decorate("`🛠️`", "Project"),
|
||||
value: projectName,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: decorate("`⚙️`", "Application"),
|
||||
value: applicationName,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: decorate("`💾`", "Volume Name"),
|
||||
value: volumeName,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: decorate("`🔧`", "Service Type"),
|
||||
value: serviceType,
|
||||
inline: true,
|
||||
},
|
||||
...(backupSize
|
||||
? [
|
||||
{
|
||||
name: decorate("`📊`", "Backup Size"),
|
||||
value: backupSize,
|
||||
inline: true,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
name: decorate("`📅`", "Date"),
|
||||
value: `<t:${unixDate}:D>`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: decorate("`⌚`", "Time"),
|
||||
value: `<t:${unixDate}:t>`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: decorate("`❓`", "Type"),
|
||||
value: type
|
||||
.replace("error", "Failed")
|
||||
.replace("success", "Successful"),
|
||||
inline: true,
|
||||
},
|
||||
...(type === "error" && errorMessage
|
||||
? [
|
||||
{
|
||||
name: decorate("`⚠️`", "Error Message"),
|
||||
value: `\`\`\`${errorMessage.substring(0, 1010)}\`\`\``,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
timestamp: date.toISOString(),
|
||||
footer: {
|
||||
text: "Dokploy Volume Backup Notification",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (gotify) {
|
||||
const decorate = (decoration: string, text: string) =>
|
||||
`${gotify.decoration ? decoration : ""} ${text}\n`;
|
||||
|
||||
await sendGotifyNotification(
|
||||
gotify,
|
||||
decorate(
|
||||
type === "success" ? "✅" : "❌",
|
||||
`Volume Backup ${type === "success" ? "Successful" : "Failed"}`,
|
||||
),
|
||||
`${decorate("🛠️", `Project: ${projectName}`)}` +
|
||||
`${decorate("⚙️", `Application: ${applicationName}`)}` +
|
||||
`${decorate("💾", `Volume Name: ${volumeName}`)}` +
|
||||
`${decorate("🔧", `Service Type: ${serviceType}`)}` +
|
||||
`${backupSize ? decorate("📊", `Backup Size: ${backupSize}`) : ""}` +
|
||||
`${decorate("🕒", `Date: ${date.toLocaleString()}`)}` +
|
||||
`${type === "error" && errorMessage ? decorate("❌", `Error:\n${errorMessage}`) : ""}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (ntfy) {
|
||||
await sendNtfyNotification(
|
||||
ntfy,
|
||||
`Volume Backup ${type === "success" ? "Successful" : "Failed"}`,
|
||||
`${type === "success" ? "white_check_mark" : "x"}`,
|
||||
"",
|
||||
`🛠️Project: ${projectName}\n` +
|
||||
`⚙️Application: ${applicationName}\n` +
|
||||
`💾Volume Name: ${volumeName}\n` +
|
||||
`🔧Service Type: ${serviceType}\n` +
|
||||
`${backupSize ? `📊Backup Size: ${backupSize}\n` : ""}` +
|
||||
`🕒Date: ${date.toLocaleString()}\n` +
|
||||
`${type === "error" && errorMessage ? `❌Error:\n${errorMessage}` : ""}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (telegram) {
|
||||
const isError = type === "error" && errorMessage;
|
||||
|
||||
const statusEmoji = type === "success" ? "✅" : "❌";
|
||||
const typeStatus = type === "success" ? "Successful" : "Failed";
|
||||
const errorMsg = isError
|
||||
? `\n\n<b>Error:</b>\n<pre>${errorMessage}</pre>`
|
||||
: "";
|
||||
const sizeInfo = backupSize ? `\n<b>Backup Size:</b> ${backupSize}` : "";
|
||||
|
||||
const messageText = `<b>${statusEmoji} Volume Backup ${typeStatus}</b>\n\n<b>Project:</b> ${projectName}\n<b>Application:</b> ${applicationName}\n<b>Volume Name:</b> ${volumeName}\n<b>Service Type:</b> ${serviceType}${sizeInfo}\n<b>Date:</b> ${format(date, "PP")}\n<b>Time:</b> ${format(date, "pp")}${isError ? errorMsg : ""}`;
|
||||
|
||||
await sendTelegramNotification(telegram, messageText);
|
||||
}
|
||||
|
||||
if (slack) {
|
||||
const { channel } = slack;
|
||||
await sendSlackNotification(slack, {
|
||||
channel: channel,
|
||||
attachments: [
|
||||
{
|
||||
color: type === "success" ? "#00FF00" : "#FF0000",
|
||||
pretext:
|
||||
type === "success"
|
||||
? ":white_check_mark: *Volume Backup Successful*"
|
||||
: ":x: *Volume Backup Failed*",
|
||||
fields: [
|
||||
...(type === "error" && errorMessage
|
||||
? [
|
||||
{
|
||||
title: "Error Message",
|
||||
value: errorMessage,
|
||||
short: false,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
title: "Project",
|
||||
value: projectName,
|
||||
short: true,
|
||||
},
|
||||
{
|
||||
title: "Application",
|
||||
value: applicationName,
|
||||
short: true,
|
||||
},
|
||||
{
|
||||
title: "Volume Name",
|
||||
value: volumeName,
|
||||
short: true,
|
||||
},
|
||||
{
|
||||
title: "Service Type",
|
||||
value: serviceType,
|
||||
short: true,
|
||||
},
|
||||
...(backupSize
|
||||
? [
|
||||
{
|
||||
title: "Backup Size",
|
||||
value: backupSize,
|
||||
short: true,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
title: "Time",
|
||||
value: date.toLocaleString(),
|
||||
short: true,
|
||||
},
|
||||
{
|
||||
title: "Type",
|
||||
value: type,
|
||||
short: true,
|
||||
},
|
||||
{
|
||||
title: "Status",
|
||||
value: type === "success" ? "Successful" : "Failed",
|
||||
short: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
if (pushover) {
|
||||
await sendPushoverNotification(
|
||||
pushover,
|
||||
`Volume Backup ${type === "success" ? "Successful" : "Failed"}`,
|
||||
`Project: ${projectName}\nApplication: ${applicationName}\nVolume: ${volumeName}\nService Type: ${serviceType}${backupSize ? `\nBackup Size: ${backupSize}` : ""}\nDate: ${date.toLocaleString()}${type === "error" && errorMessage ? `\nError: ${errorMessage}` : ""}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (teams) {
|
||||
const facts = [
|
||||
{ name: "Project", value: projectName },
|
||||
{ name: "Application", value: applicationName },
|
||||
{ name: "Volume Name", value: volumeName },
|
||||
{ name: "Service Type", value: serviceType },
|
||||
{ name: "Date", value: format(date, "PP pp") },
|
||||
{ name: "Status", value: type === "success" ? "Successful" : "Failed" },
|
||||
];
|
||||
if (backupSize) {
|
||||
facts.push({ name: "Backup Size", value: backupSize });
|
||||
}
|
||||
if (type === "error" && errorMessage) {
|
||||
facts.push({ name: "Error", value: errorMessage.substring(0, 500) });
|
||||
}
|
||||
await sendTeamsNotification(teams, {
|
||||
title:
|
||||
type === "success"
|
||||
? "✅ Volume Backup Successful"
|
||||
: "❌ Volume Backup Failed",
|
||||
facts,
|
||||
});
|
||||
}
|
||||
|
||||
if (custom) {
|
||||
await sendCustomNotification(custom, {
|
||||
title: `Volume Backup ${type === "success" ? "Successful" : "Failed"}`,
|
||||
message:
|
||||
type === "success"
|
||||
? "Volume backup completed successfully"
|
||||
: "Volume backup failed",
|
||||
projectName,
|
||||
applicationName,
|
||||
volumeName,
|
||||
serviceType,
|
||||
type,
|
||||
errorMessage: errorMessage ?? "",
|
||||
backupSize: backupSize ?? "",
|
||||
timestamp: date.toISOString(),
|
||||
date: date.toLocaleString(),
|
||||
status: type,
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user