Compare commits
No commits in common. "e11fc00840f2fe68a076ea56305ca67bd2ace332" and "4ec759296b08649e791f39bad26d358efb85dd0a" have entirely different histories.
e11fc00840
...
4ec759296b
10 changed files with 1196 additions and 4621 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -1,2 +0,0 @@
|
||||||
result
|
|
||||||
result-*
|
|
||||||
150
API.md
150
API.md
|
|
@ -12,11 +12,6 @@ Default bind: `127.0.0.1:9911`
|
||||||
|--------|------|-------------|
|
|--------|------|-------------|
|
||||||
| GET | `/` | Web UI (served from `frontend/index.html`) |
|
| GET | `/` | Web UI (served from `frontend/index.html`) |
|
||||||
| GET | `/health` | Health check |
|
| GET | `/health` | Health check |
|
||||||
| GET | `/status` | All apps with routes, container status and running operation (what the UI polls) |
|
|
||||||
| GET | `/integrations` | Forgejo connection (`configured`, `url`, `has_token`, `user`) and the SSH deploy public key |
|
|
||||||
| POST | `/integrations/forgejo` | `{"token": "..."}` — verify against Forgejo and store; `""` disconnects |
|
|
||||||
| GET | `/forgejo/repos?q=` | Search repositories visible to the stored token (public ones without) |
|
|
||||||
| GET | `/forgejo/branches?repo=owner/name` | Branch names of a Forgejo repository |
|
|
||||||
|
|
||||||
### Apps — Read
|
### Apps — Read
|
||||||
|
|
||||||
|
|
@ -24,27 +19,17 @@ Default bind: `127.0.0.1:9911`
|
||||||
|--------|------|-------------|
|
|--------|------|-------------|
|
||||||
| GET | `/apps` | List all apps |
|
| GET | `/apps` | List all apps |
|
||||||
| GET | `/apps/<name>` | Show single app manifest |
|
| GET | `/apps/<name>` | Show single app manifest |
|
||||||
| GET | `/apps/<name>/routes` | Get parsed route entries |
|
|
||||||
| GET | `/apps/<name>/status` | Container status (running/stopped) |
|
| GET | `/apps/<name>/status` | Container status (running/stopped) |
|
||||||
| GET | `/apps/<name>/compose` | Read compose.yaml content |
|
| GET | `/apps/<name>/compose` | Read compose.yaml content |
|
||||||
| GET | `/apps/<name>/logs?tail=N` | Fetch last N log lines (default 100) |
|
| GET | `/apps/<name>/logs?tail=N` | Fetch last N log lines (default 100) |
|
||||||
| GET | `/apps/<name>/backups` | List available backups |
|
| GET | `/apps/<name>/backups` | List available backups |
|
||||||
| GET | `/apps/<name>/backups/<file>` | Download backup zip |
|
| GET | `/apps/<name>/backups/<file>` | Download backup zip |
|
||||||
| GET | `/apps/<name>/env` | Environment variables: `{"vars": [{"key", "value"}], "inject": true}` |
|
|
||||||
| GET | `/apps/<name>/repo` | Git source info (URL, web URL, provider, branch, deployed commit, local changes, deploy key for ssh) |
|
|
||||||
| GET | `/apps/<name>/repo?fetch=1` | Same, plus fetches the remote and reports `behind` / `remote` |
|
|
||||||
| GET | `/apps/<name>/volumes` | Volumes the file browser can open |
|
|
||||||
| GET | `/apps/<name>/volume/files?vol=&path=` | List a folder in a volume |
|
|
||||||
| GET | `/apps/<name>/volume/download?vol=&path=` | Download a file from a volume |
|
|
||||||
| PUT | `/apps/<name>/volume/files?vol=&path=` | Upload a file (raw body) |
|
|
||||||
| DELETE | `/apps/<name>/volume/files?vol=&path=` | Delete a file or folder |
|
|
||||||
|
|
||||||
### Apps — Write
|
### Apps — Write
|
||||||
|
|
||||||
| Method | Path | Description |
|
| Method | Path | Description |
|
||||||
|--------|------|-------------|
|
|--------|------|-------------|
|
||||||
| POST | `/apps/init` | Create a new app |
|
| POST | `/apps/init` | Create a new app |
|
||||||
| POST | `/apps/<name>/routes` | Update routes (hot — Caddy reloads automatically) |
|
|
||||||
| POST | `/apps/<name>/deploy` | Deploy (compose up + caddy reload) |
|
| POST | `/apps/<name>/deploy` | Deploy (compose up + caddy reload) |
|
||||||
| POST | `/apps/<name>/restart` | Restart (compose down + up) |
|
| POST | `/apps/<name>/restart` | Restart (compose down + up) |
|
||||||
| POST | `/apps/<name>/stop` | Stop (compose down) |
|
| POST | `/apps/<name>/stop` | Stop (compose down) |
|
||||||
|
|
@ -54,151 +39,55 @@ Default bind: `127.0.0.1:9911`
|
||||||
| POST | `/apps/<name>/backup` | Create volume backup (zip) |
|
| POST | `/apps/<name>/backup` | Create volume backup (zip) |
|
||||||
| POST | `/apps/<name>/restore` | Restore from backup |
|
| POST | `/apps/<name>/restore` | Restore from backup |
|
||||||
| POST | `/apps/<name>/remove` | Remove app |
|
| POST | `/apps/<name>/remove` | Remove app |
|
||||||
| POST | `/apps/<name>/repo-pull` | Git apps: fetch branch, hard-reset checkout to it, redeploy |
|
|
||||||
| POST | `/apps/<name>/env` | Replace environment variables: `{"vars": [...], "inject": true, "deploy": false}` |
|
|
||||||
| POST | `/apps/<name>/volume-clear` | Stop the app and empty its default data folder |
|
|
||||||
|
|
||||||
Write operations are serialised per app. While one runs, another write to the
|
|
||||||
same app returns `409` with `{"ok": false, "error": "...", "busy": "deploy"}`.
|
|
||||||
`deploy` returns the compose output in `stdout` (or `stderr` on failure).
|
|
||||||
|
|
||||||
### Create app (git repository, with environment variables)
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"name": "blog",
|
|
||||||
"routes": [{"domain": "blog.reudy.net", "upstream": "127.0.0.1:18090"}],
|
|
||||||
"auth": true,
|
|
||||||
"source_type": "git",
|
|
||||||
"repo_url": "https://git.reudy.net/reudy-net/blog.git",
|
|
||||||
"repo_branch": "",
|
|
||||||
"use_forgejo_token": true,
|
|
||||||
"env": [{"key": "DATABASE_URL", "value": "postgres://..."}],
|
|
||||||
"env_inject": true
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
- `repo_url` may be `https://…`, `ssh://git@host:port/owner/repo.git` or
|
|
||||||
`git@host:owner/repo.git`. ssh URLs use the panel's deploy key.
|
|
||||||
- `repo_token` sets an https token explicitly; `use_forgejo_token` uses the
|
|
||||||
token stored in Settings (only for URLs on the configured Forgejo host).
|
|
||||||
- An empty branch uses the repository's default branch. The compose file must
|
|
||||||
be at the repository root.
|
|
||||||
- The older `source_type: "github"` with `github_url` / `github_branch` /
|
|
||||||
`github_pat` is still accepted.
|
|
||||||
|
|
||||||
### Sync response (`repo-pull`)
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"ok": true,
|
|
||||||
"stdout": "HEAD is now at d7df557 Bump image tag\n...compose output...",
|
|
||||||
"before": {"sha": "4fb7976...", "short": "4fb7976", "subject": "Initial compose", "author": "reudy", "time": 1790460618},
|
|
||||||
"after": {"sha": "d7df557...", "short": "d7df557", "subject": "Bump image tag", "author": "reudy", "time": 1790460643},
|
|
||||||
"changed": true
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Status response (`/status`)
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"ok": true,
|
|
||||||
"time": 1790460650,
|
|
||||||
"apps": [
|
|
||||||
{
|
|
||||||
"name": "whoami",
|
|
||||||
"routes": [{"domain": "whoami.reudy.net", "upstream": "127.0.0.1:18080"}],
|
|
||||||
"auth": true,
|
|
||||||
"compose_file": "/var/lib/containers/stacks/whoami/compose.yaml",
|
|
||||||
"repo_url": "",
|
|
||||||
"repo_branch": "",
|
|
||||||
"busy": null,
|
|
||||||
"status": {
|
|
||||||
"state": "running",
|
|
||||||
"running": true,
|
|
||||||
"running_count": 1,
|
|
||||||
"total": 1,
|
|
||||||
"containers": [{"name": "whoami-app-1", "state": "running", "status": "Up 3 minutes", "image": "docker.io/traefik/whoami:latest", "running": true}]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
`state` is one of `running`, `partial` (some containers down), `stopped` or `unknown`.
|
|
||||||
|
|
||||||
## Example payloads
|
## Example payloads
|
||||||
|
|
||||||
### Create app (single route)
|
### Create app (single domain)
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"name": "whoami",
|
"name": "whoami",
|
||||||
"routes": [
|
"domain": "whoami.srazka.com",
|
||||||
{"domain": "whoami.reudy.net", "upstream": "127.0.0.1:18080"}
|
"port": 18080,
|
||||||
],
|
|
||||||
"auth": true
|
"auth": true
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Create app (multiple routes, different ports)
|
### Create app (multiple domains)
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"name": "myapp",
|
"name": "myapp",
|
||||||
"routes": [
|
"domain": "app.srazka.com,www.app.srazka.com",
|
||||||
{"domain": "app.reudy.net", "upstream": "127.0.0.1:18080"},
|
"port": 18081,
|
||||||
{"domain": "api.app.reudy.net", "upstream": "127.0.0.1:18081"}
|
|
||||||
],
|
|
||||||
"auth": true
|
"auth": true
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Create app (multiple routes, different ports, with paths)
|
Or using the `domains` array format:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"name": "pocketbase",
|
"name": "myapp",
|
||||||
"routes": [
|
"domains": ["app.srazka.com", "www.app.srazka.com"],
|
||||||
{"domain": "pb.reudy.net", "upstream": "127.0.0.1:8090", "path": "/_/*"}
|
"port": 18081,
|
||||||
],
|
|
||||||
"auth": true
|
"auth": true
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
The `path` field is optional. When present, it generates a Caddy `reverse_proxy /_/* 127.0.0.1:8090` rule, letting you route requests to a specific path prefix within a domain.
|
|
||||||
|
|
||||||
### Create app (wildcard domain)
|
### Create app (wildcard domain)
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"name": "wildcard",
|
"name": "wildcard",
|
||||||
"routes": [
|
"domain": "*.srazka.com",
|
||||||
{"domain": "*.reudy.net", "upstream": "127.0.0.1:18082"}
|
"port": 18082,
|
||||||
],
|
|
||||||
"auth": false
|
"auth": false
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Note: Wildcard domains require DNS challenge configuration in Caddy.
|
Note: Wildcard domains require DNS challenge configuration in Caddy.
|
||||||
|
|
||||||
### Update routes (hot)
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"routes": [
|
|
||||||
{"domain": "app.reudy.net", "upstream": "127.0.0.1:18080"},
|
|
||||||
{"domain": "api.reudy.net", "upstream": "127.0.0.1:18081"},
|
|
||||||
{"domain": "pb.reudy.net", "upstream": "127.0.0.1:8090", "path": "/_/*"}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
The optional `path` field generates a Caddy `reverse_proxy <path> <upstream>` rule for sub-path routing.
|
|
||||||
|
|
||||||
Caddy reloads automatically via the systemd path watcher. Containers stay running.
|
|
||||||
|
|
||||||
### Save compose
|
### Save compose
|
||||||
|
|
||||||
```json
|
```json
|
||||||
|
|
@ -223,21 +112,6 @@ Caddy reloads automatically via the systemd path watcher. Containers stay runnin
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Routes response
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"ok": true,
|
|
||||||
"name": "myapp",
|
|
||||||
"routes": [
|
|
||||||
{"domain": "app.reudy.net", "upstream": "127.0.0.1:18080"},
|
|
||||||
{"domain": "api.reudy.net", "upstream": "127.0.0.1:18081", "path": "/api/*"}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
The `path` field is only present when a route has a path configured.
|
|
||||||
|
|
||||||
## Response format
|
## Response format
|
||||||
|
|
||||||
All JSON responses include an `ok` boolean:
|
All JSON responses include an `ok` boolean:
|
||||||
|
|
|
||||||
139
README.md
139
README.md
|
|
@ -2,51 +2,6 @@
|
||||||
|
|
||||||
Minimal container management panel for rootless Podman + Caddy.
|
Minimal container management panel for rootless Podman + Caddy.
|
||||||
|
|
||||||
## Installing on NixOS
|
|
||||||
|
|
||||||
This repository is a flake that provides the panel as a package
|
|
||||||
(`packages.<system>.default`) and a NixOS module (`nixosModules.default`).
|
|
||||||
Add it to your system flake:
|
|
||||||
|
|
||||||
```nix
|
|
||||||
{
|
|
||||||
inputs.panel = {
|
|
||||||
url = "git+https://git.reudy.net/reudy-net/panel";
|
|
||||||
inputs.nixpkgs.follows = "nixpkgs";
|
|
||||||
};
|
|
||||||
|
|
||||||
outputs = { nixpkgs, panel, ... }: {
|
|
||||||
nixosConfigurations.vps = nixpkgs.lib.nixosSystem {
|
|
||||||
modules = [
|
|
||||||
panel.nixosModules.default
|
|
||||||
{
|
|
||||||
services.reudy-panel = {
|
|
||||||
enable = true;
|
|
||||||
domain = "panel.example.com"; # Caddy virtual host
|
|
||||||
autheliaAddress = "127.0.0.1:9091"; # forward_auth in front of it
|
|
||||||
};
|
|
||||||
}
|
|
||||||
];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
The module sets up the `panel-api` service, the directories below, the
|
|
||||||
`panelroutes` group shared with Caddy, the Caddy import of the generated routes
|
|
||||||
and a path unit that reloads Caddy when they change. With Forgejo enabled on
|
|
||||||
the same host, its URLs are passed to the panel automatically
|
|
||||||
(`services.reudy-panel.forgejo.enable`). See `nix/module.nix` for all options.
|
|
||||||
|
|
||||||
To deploy a new panel version, bump the input and rebuild:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
nix flake update panel
|
|
||||||
sudo nixos-rebuild switch --flake .#vps
|
|
||||||
```
|
|
||||||
|
|
||||||
Checks (package build and a module evaluation) run with `nix flake check`.
|
|
||||||
|
|
||||||
## Base directory
|
## Base directory
|
||||||
|
|
||||||
`/var/lib/containers`
|
`/var/lib/containers`
|
||||||
|
|
@ -67,19 +22,14 @@ All app routes are written to a single `routes/routes.caddy` file that Caddy imp
|
||||||
## Quick workflow
|
## Quick workflow
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Routes are "domain|upstream[|path]" entries, comma-separated.
|
# Create a new app (single domain)
|
||||||
|
panelctl init whoami whoami.srazka.com 18080 true
|
||||||
|
|
||||||
# Create a new app (single route, protected by Authelia)
|
# Create with multiple domains
|
||||||
panelctl init whoami "whoami.reudy.net|127.0.0.1:18080" true
|
panelctl init myapp "app.srazka.com,www.srazka.com" 18081 true
|
||||||
|
|
||||||
# Create with multiple routes (different ports, optional path)
|
|
||||||
panelctl init myapp "app.reudy.net|127.0.0.1:18081,api.reudy.net|127.0.0.1:18082|/api/*" true
|
|
||||||
|
|
||||||
# Create with wildcard domain (requires DNS challenge in Caddy)
|
# Create with wildcard domain (requires DNS challenge in Caddy)
|
||||||
panelctl init wild "*.reudy.net|127.0.0.1:18083" false
|
panelctl init wild "*.srazka.com" 18082 false
|
||||||
|
|
||||||
# Change routes later (Caddy reloads automatically)
|
|
||||||
panelctl set-routes whoami "whoami.reudy.net|127.0.0.1:18080,who.reudy.net|127.0.0.1:18080"
|
|
||||||
|
|
||||||
# Deploy (compose up + caddy reload)
|
# Deploy (compose up + caddy reload)
|
||||||
panelctl deploy whoami
|
panelctl deploy whoami
|
||||||
|
|
@ -135,78 +85,15 @@ panelctl remove whoami
|
||||||
## Web UI & API
|
## Web UI & API
|
||||||
|
|
||||||
- Nix runs `panel-api` as a systemd service on `127.0.0.1:9911`.
|
- Nix runs `panel-api` as a systemd service on `127.0.0.1:9911`.
|
||||||
- Caddy proxies `https://panel.reudy.net` → panel-api with Authelia forward_auth.
|
- Caddy proxies `https://panel.srazka.com` → panel-api with Authelia forward_auth.
|
||||||
- Open `https://panel.reudy.net` for the web UI.
|
- Open `https://panel.srazka.com` for the web UI.
|
||||||
- API docs: [API.md](API.md)
|
- API docs: [API.md](API.md)
|
||||||
|
|
||||||
### Web UI features
|
### Web UI features
|
||||||
|
|
||||||
- Live status: one `/status` poll every few seconds (faster while something is
|
- Create apps with multiple domains and wildcard support
|
||||||
running, paused when the tab is hidden) updates cards in place, so open tabs,
|
- Live container status indicators (auto-refreshes)
|
||||||
unsaved edits and scroll positions are never lost. The header shows when the
|
- Deploy, restart, stop, remove from the UI
|
||||||
panel last synced and warns when the Authelia session has expired.
|
- Inline compose editor with save, validate, and save+deploy
|
||||||
- Per-app status (running / partial / stopped), container list, and a busy
|
- Log viewer with configurable tail length
|
||||||
indicator that is shared between browsers while an operation runs.
|
- Volume backup management: create, list, download, restore
|
||||||
- New-app dialog: starter container, pasted compose file or git repository;
|
|
||||||
suggests the next free port and a domain based on the app name.
|
|
||||||
- Compose editor with unsaved-changes tracking, Ctrl+S, save & deploy, validate.
|
|
||||||
- Logs with follow mode, routes editor with validation, file browser with
|
|
||||||
drag-and-drop upload, backups with restore (and optional redeploy).
|
|
||||||
- Git source tab: deployed commit, "check for updates", and sync & deploy.
|
|
||||||
- Activity drawer with the output of every operation (e.g. why a deploy failed).
|
|
||||||
- Keyboard: `/` search, `N` new app, `Esc` closes menus. Deep links like
|
|
||||||
`#/whoami/logs` open an app on a specific tab.
|
|
||||||
|
|
||||||
### Git-backed apps
|
|
||||||
|
|
||||||
Apps created from a repository (Forgejo, GitHub or any git host, over https or
|
|
||||||
ssh) are cloned to `stacks/<app>/repo`.
|
|
||||||
|
|
||||||
**Forgejo.** `panel.nix` points the panel at the local Forgejo
|
|
||||||
(`PANEL_FORGEJO_URL`, `PANEL_FORGEJO_API_URL`, `PANEL_FORGEJO_SSH_URL`, taken
|
|
||||||
from `forgejo.nix`). In the panel's **Settings** you can connect a Forgejo
|
|
||||||
access token (read access to repositories and user). With it, the new-app
|
|
||||||
dialog lists your repositories and branches, and private ones are cloned over
|
|
||||||
https with the token. Without it, public repositories are listed and private
|
|
||||||
ones are cloned over ssh with the deploy key. Commit and compare links point at
|
|
||||||
Forgejo. The token is stored in `state/panel/forgejo-token` (mode 0600).
|
|
||||||
|
|
||||||
**SSH / deploy key.** The panel generates an ed25519 key pair in
|
|
||||||
`state/panel/ssh/` the first time it is needed. Its public half is shown in
|
|
||||||
Settings (and next to ssh URLs); add it as a read-only deploy key to a
|
|
||||||
repository — or to your Forgejo account for access to all repositories — to
|
|
||||||
clone `ssh://git@git.reudy.net:14922/owner/repo.git` style URLs.
|
|
||||||
|
|
||||||
**Sync** fetches the configured branch and
|
|
||||||
hard-resets the checkout to it before redeploying, so the repository is the
|
|
||||||
source of truth: compose edits made in the panel are discarded on the next sync
|
|
||||||
(the UI warns about this). An access token for a private repository is stored in
|
|
||||||
the clone's `.git/config`; use a read-only token.
|
|
||||||
|
|
||||||
### Environment variables
|
|
||||||
|
|
||||||
Each app can have environment variables (the **Environment** tab, or when
|
|
||||||
creating the app; `.env` text can be pasted in). They are stored in
|
|
||||||
`state/env/<app>.env` as `KEY=VALUE` lines (mode 0600) — outside the repository
|
|
||||||
and stack directory, so git syncs never touch them — and `panelctl` passes them
|
|
||||||
to every compose command:
|
|
||||||
|
|
||||||
- They are always available for `${VAR}` interpolation in the compose file.
|
|
||||||
The UI points out variables the compose file uses without a default that
|
|
||||||
aren't set.
|
|
||||||
- With **Pass to every container** (the default), `deploy`/`restart` also
|
|
||||||
generate `stacks/<app>/.panel-env.yaml`, a compose override that lists the
|
|
||||||
keys under every service's `environment:`. Compose reads the values from its
|
|
||||||
own environment, so they are never quoted into YAML, and they take
|
|
||||||
precedence over values set in the compose file.
|
|
||||||
|
|
||||||
Values must be single-line. Names that would change how podman/compose run
|
|
||||||
(`PATH`, `HOME`, `XDG_*`, `DOCKER_*`, `COMPOSE_*`, `PODMAN_*`, …) are rejected.
|
|
||||||
Changes apply on the next deploy. Backups do not include variables.
|
|
||||||
|
|
||||||
### Concurrency
|
|
||||||
|
|
||||||
`panel-api` handles requests concurrently, so a long deploy never blocks status
|
|
||||||
or logs. Mutating operations are serialised per app — a second operation on a
|
|
||||||
busy app gets HTTP 409 — and `panelctl` takes a `flock` on the shared routes
|
|
||||||
file while rewriting it.
|
|
||||||
|
|
|
||||||
27
flake.lock
generated
27
flake.lock
generated
|
|
@ -1,27 +0,0 @@
|
||||||
{
|
|
||||||
"nodes": {
|
|
||||||
"nixpkgs": {
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1782847189,
|
|
||||||
"narHash": "sha256-twXPFqFsrrY5r28Zh7Homgcp2gUMBgQ6WDS98Q/3xFI=",
|
|
||||||
"owner": "NixOS",
|
|
||||||
"repo": "nixpkgs",
|
|
||||||
"rev": "b6018f87da91d19d0ab4cf979885689b469cdd41",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "NixOS",
|
|
||||||
"ref": "nixos-25.11",
|
|
||||||
"repo": "nixpkgs",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"root": {
|
|
||||||
"inputs": {
|
|
||||||
"nixpkgs": "nixpkgs"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"root": "root",
|
|
||||||
"version": 7
|
|
||||||
}
|
|
||||||
58
flake.nix
58
flake.nix
|
|
@ -1,58 +0,0 @@
|
||||||
{
|
|
||||||
description = "Small web panel for deploying Podman compose apps behind Caddy";
|
|
||||||
|
|
||||||
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11";
|
|
||||||
|
|
||||||
outputs =
|
|
||||||
{ self, nixpkgs }:
|
|
||||||
let
|
|
||||||
systems = [
|
|
||||||
"x86_64-linux"
|
|
||||||
"aarch64-linux"
|
|
||||||
];
|
|
||||||
forAllSystems = f: nixpkgs.lib.genAttrs systems (system: f nixpkgs.legacyPackages.${system});
|
|
||||||
in
|
|
||||||
{
|
|
||||||
packages = forAllSystems (pkgs: rec {
|
|
||||||
reudy-panel = pkgs.callPackage ./nix/package.nix { };
|
|
||||||
default = reudy-panel;
|
|
||||||
});
|
|
||||||
|
|
||||||
overlays.default = final: _prev: {
|
|
||||||
reudy-panel = final.callPackage ./nix/package.nix { };
|
|
||||||
};
|
|
||||||
|
|
||||||
nixosModules = rec {
|
|
||||||
reudy-panel = ./nix/module.nix;
|
|
||||||
default = reudy-panel;
|
|
||||||
};
|
|
||||||
|
|
||||||
checks = forAllSystems (pkgs: {
|
|
||||||
package = self.packages.${pkgs.stdenv.hostPlatform.system}.default;
|
|
||||||
|
|
||||||
# Evaluates a small host using the module, so option or wiring mistakes
|
|
||||||
# fail here rather than on the server.
|
|
||||||
module =
|
|
||||||
(nixpkgs.lib.nixosSystem {
|
|
||||||
system = pkgs.stdenv.hostPlatform.system;
|
|
||||||
modules = [
|
|
||||||
self.nixosModules.default
|
|
||||||
{
|
|
||||||
boot.isContainer = true;
|
|
||||||
system.stateVersion = "25.11";
|
|
||||||
users.users.reudy.isNormalUser = true;
|
|
||||||
services.caddy.enable = true;
|
|
||||||
services.forgejo.enable = true;
|
|
||||||
services.reudy-panel = {
|
|
||||||
enable = true;
|
|
||||||
domain = "panel.example.com";
|
|
||||||
autheliaAddress = "127.0.0.1:9091";
|
|
||||||
};
|
|
||||||
}
|
|
||||||
];
|
|
||||||
}).config.system.build.toplevel;
|
|
||||||
});
|
|
||||||
|
|
||||||
formatter = forAllSystems (pkgs: pkgs.nixfmt-rfc-style);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
3740
frontend/index.html
3740
frontend/index.html
File diff suppressed because it is too large
Load diff
178
nix/module.nix
178
nix/module.nix
|
|
@ -1,178 +0,0 @@
|
||||||
{
|
|
||||||
config,
|
|
||||||
lib,
|
|
||||||
pkgs,
|
|
||||||
...
|
|
||||||
}:
|
|
||||||
|
|
||||||
let
|
|
||||||
cfg = config.services.reudy-panel;
|
|
||||||
forgejoServer = config.services.forgejo.settings.server;
|
|
||||||
routesFile = "${cfg.baseDir}/routes/routes.caddy";
|
|
||||||
in
|
|
||||||
{
|
|
||||||
options.services.reudy-panel = {
|
|
||||||
enable = lib.mkEnableOption "the panel for deploying Podman compose apps behind Caddy";
|
|
||||||
|
|
||||||
package = lib.mkOption {
|
|
||||||
type = lib.types.package;
|
|
||||||
default = pkgs.callPackage ./package.nix { };
|
|
||||||
defaultText = lib.literalExpression "pkgs.callPackage ./package.nix { }";
|
|
||||||
description = "The panel package to use.";
|
|
||||||
};
|
|
||||||
|
|
||||||
user = lib.mkOption {
|
|
||||||
type = lib.types.str;
|
|
||||||
default = "reudy";
|
|
||||||
description = ''
|
|
||||||
Existing user the panel runs as. Apps are deployed as rootless Podman
|
|
||||||
containers of this user, so it should have lingering enabled.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
group = lib.mkOption {
|
|
||||||
type = lib.types.str;
|
|
||||||
default = "panelroutes";
|
|
||||||
description = "Group shared by the panel and Caddy for the generated routes file.";
|
|
||||||
};
|
|
||||||
|
|
||||||
baseDir = lib.mkOption {
|
|
||||||
type = lib.types.path;
|
|
||||||
default = "/var/lib/containers";
|
|
||||||
description = "Where stacks, volumes, routes, state and backups are kept.";
|
|
||||||
};
|
|
||||||
|
|
||||||
listenAddress = lib.mkOption {
|
|
||||||
type = lib.types.str;
|
|
||||||
default = "127.0.0.1";
|
|
||||||
description = "Address the panel API listens on.";
|
|
||||||
};
|
|
||||||
|
|
||||||
port = lib.mkOption {
|
|
||||||
type = lib.types.port;
|
|
||||||
default = 9911;
|
|
||||||
description = "Port the panel API listens on.";
|
|
||||||
};
|
|
||||||
|
|
||||||
domain = lib.mkOption {
|
|
||||||
type = lib.types.nullOr lib.types.str;
|
|
||||||
default = null;
|
|
||||||
example = "panel.example.com";
|
|
||||||
description = "Serve the panel on this domain through Caddy. Null disables the virtual host.";
|
|
||||||
};
|
|
||||||
|
|
||||||
autheliaAddress = lib.mkOption {
|
|
||||||
type = lib.types.nullOr lib.types.str;
|
|
||||||
default = null;
|
|
||||||
example = "127.0.0.1:9091";
|
|
||||||
description = ''
|
|
||||||
Authelia instance that protects the panel's virtual host with
|
|
||||||
forward_auth. The panel has no login of its own, so leave this null
|
|
||||||
only if something else guards it.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
forgejo.enable = lib.mkOption {
|
|
||||||
type = lib.types.bool;
|
|
||||||
default = config.services.forgejo.enable;
|
|
||||||
defaultText = lib.literalExpression "config.services.forgejo.enable";
|
|
||||||
description = ''
|
|
||||||
Point the panel at the local Forgejo instance (repo picker, private
|
|
||||||
clones, commit links). URLs are taken from services.forgejo.settings.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
environment = lib.mkOption {
|
|
||||||
type = lib.types.attrsOf lib.types.str;
|
|
||||||
default = { };
|
|
||||||
description = "Extra environment variables for the panel API service.";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
config = lib.mkIf cfg.enable {
|
|
||||||
environment.systemPackages = [ cfg.package ];
|
|
||||||
|
|
||||||
users.groups.${cfg.group} = { };
|
|
||||||
users.users.${cfg.user}.extraGroups = [ cfg.group ];
|
|
||||||
users.users.caddy.extraGroups = lib.mkIf config.services.caddy.enable [ cfg.group ];
|
|
||||||
|
|
||||||
systemd.tmpfiles.rules =
|
|
||||||
map (dir: "d ${cfg.baseDir}${dir} 0750 ${cfg.user} ${cfg.group} -") [
|
|
||||||
""
|
|
||||||
"/stacks"
|
|
||||||
"/volumes"
|
|
||||||
"/routes"
|
|
||||||
"/state"
|
|
||||||
"/state/apps"
|
|
||||||
"/backups"
|
|
||||||
]
|
|
||||||
++ [ "f ${routesFile} 0640 ${cfg.user} ${cfg.group} -" ];
|
|
||||||
|
|
||||||
systemd.services.panel-api = {
|
|
||||||
description = "Panel API";
|
|
||||||
after = [ "network.target" ];
|
|
||||||
wantedBy = [ "multi-user.target" ];
|
|
||||||
|
|
||||||
serviceConfig = {
|
|
||||||
Type = "simple";
|
|
||||||
User = cfg.user;
|
|
||||||
Group = cfg.group;
|
|
||||||
Restart = "always";
|
|
||||||
RestartSec = 3;
|
|
||||||
WorkingDirectory = cfg.baseDir;
|
|
||||||
ExecStart = lib.getExe cfg.package;
|
|
||||||
};
|
|
||||||
|
|
||||||
environment = {
|
|
||||||
PANEL_API_BIND = cfg.listenAddress;
|
|
||||||
PANEL_API_PORT = toString cfg.port;
|
|
||||||
PANEL_BASE_DIR = toString cfg.baseDir;
|
|
||||||
PANEL_USER = cfg.user;
|
|
||||||
PANEL_GROUP = cfg.group;
|
|
||||||
PANELCTL_PATH = lib.getExe' cfg.package "panelctl";
|
|
||||||
}
|
|
||||||
// lib.optionalAttrs cfg.forgejo.enable {
|
|
||||||
# The API is reached on localhost; clones use the public URLs.
|
|
||||||
PANEL_FORGEJO_URL = lib.removeSuffix "/" forgejoServer.ROOT_URL;
|
|
||||||
PANEL_FORGEJO_API_URL = "http://${forgejoServer.HTTP_ADDR}:${toString forgejoServer.HTTP_PORT}";
|
|
||||||
PANEL_FORGEJO_SSH_URL = "ssh://${
|
|
||||||
forgejoServer.BUILTIN_SSH_SERVER_USER or config.services.forgejo.user
|
|
||||||
}@${forgejoServer.DOMAIN}:${toString forgejoServer.SSH_PORT}";
|
|
||||||
}
|
|
||||||
// cfg.environment;
|
|
||||||
};
|
|
||||||
|
|
||||||
services.caddy = lib.mkIf config.services.caddy.enable {
|
|
||||||
# App routes generated by panelctl.
|
|
||||||
extraConfig = ''
|
|
||||||
import ${routesFile}
|
|
||||||
'';
|
|
||||||
|
|
||||||
virtualHosts = lib.mkIf (cfg.domain != null) {
|
|
||||||
${cfg.domain}.extraConfig =
|
|
||||||
lib.optionalString (cfg.autheliaAddress != null) ''
|
|
||||||
forward_auth ${cfg.autheliaAddress} {
|
|
||||||
uri /api/authz/forward-auth
|
|
||||||
copy_headers Remote-User Remote-Groups Remote-Email Remote-Name
|
|
||||||
}
|
|
||||||
''
|
|
||||||
+ ''
|
|
||||||
reverse_proxy ${cfg.listenAddress}:${toString cfg.port}
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
# Reload Caddy whenever panelctl rewrites the routes file.
|
|
||||||
systemd.paths.caddy-routes-reload = lib.mkIf config.services.caddy.enable {
|
|
||||||
wantedBy = [ "multi-user.target" ];
|
|
||||||
pathConfig.PathChanged = routesFile;
|
|
||||||
};
|
|
||||||
|
|
||||||
systemd.services.caddy-routes-reload = lib.mkIf config.services.caddy.enable {
|
|
||||||
serviceConfig = {
|
|
||||||
Type = "oneshot";
|
|
||||||
ExecStart = "${config.systemd.package}/bin/systemctl reload caddy.service";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -1,85 +0,0 @@
|
||||||
{
|
|
||||||
lib,
|
|
||||||
stdenvNoCC,
|
|
||||||
makeWrapper,
|
|
||||||
bash,
|
|
||||||
python3,
|
|
||||||
podman,
|
|
||||||
podman-compose,
|
|
||||||
curl,
|
|
||||||
coreutils,
|
|
||||||
gnugrep,
|
|
||||||
gnused,
|
|
||||||
gawk,
|
|
||||||
findutils,
|
|
||||||
zip,
|
|
||||||
unzip,
|
|
||||||
git,
|
|
||||||
util-linux,
|
|
||||||
openssh,
|
|
||||||
}:
|
|
||||||
|
|
||||||
let
|
|
||||||
# Tools panelctl shells out to. They are appended to PATH, so the host's own
|
|
||||||
# versions (e.g. the system podman) still take precedence when present.
|
|
||||||
runtimeDeps = [
|
|
||||||
podman
|
|
||||||
podman-compose
|
|
||||||
curl
|
|
||||||
coreutils
|
|
||||||
gnugrep
|
|
||||||
gnused
|
|
||||||
gawk
|
|
||||||
findutils
|
|
||||||
zip
|
|
||||||
unzip
|
|
||||||
git
|
|
||||||
util-linux # flock, used to serialise routes file writes
|
|
||||||
openssh # cloning repositories over ssh with the panel's deploy key
|
|
||||||
];
|
|
||||||
in
|
|
||||||
stdenvNoCC.mkDerivation {
|
|
||||||
pname = "reudy-panel";
|
|
||||||
version = "0.1.0";
|
|
||||||
|
|
||||||
src = lib.fileset.toSource {
|
|
||||||
root = ../.;
|
|
||||||
fileset = lib.fileset.unions [
|
|
||||||
../panel-api.py
|
|
||||||
../panelctl.sh
|
|
||||||
../frontend
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
nativeBuildInputs = [ makeWrapper ];
|
|
||||||
buildInputs = [ bash ];
|
|
||||||
|
|
||||||
dontConfigure = true;
|
|
||||||
dontBuild = true;
|
|
||||||
|
|
||||||
installPhase = ''
|
|
||||||
runHook preInstall
|
|
||||||
|
|
||||||
install -Dm644 panel-api.py $out/share/panel/panel-api.py
|
|
||||||
cp -r frontend $out/share/panel/frontend
|
|
||||||
|
|
||||||
install -Dm755 panelctl.sh $out/bin/panelctl
|
|
||||||
patchShebangs --host $out/bin/panelctl
|
|
||||||
wrapProgram $out/bin/panelctl \
|
|
||||||
--suffix PATH : ${lib.makeBinPath runtimeDeps}
|
|
||||||
|
|
||||||
makeWrapper ${python3.interpreter} $out/bin/panel-api \
|
|
||||||
--add-flags $out/share/panel/panel-api.py \
|
|
||||||
--suffix PATH : ${lib.makeBinPath runtimeDeps} \
|
|
||||||
--set-default PANELCTL_PATH $out/bin/panelctl \
|
|
||||||
--set-default PANEL_FRONTEND_DIR $out/share/panel/frontend
|
|
||||||
|
|
||||||
runHook postInstall
|
|
||||||
'';
|
|
||||||
|
|
||||||
meta = {
|
|
||||||
description = "Small web panel for deploying Podman compose apps behind Caddy";
|
|
||||||
mainProgram = "panel-api";
|
|
||||||
platforms = lib.platforms.linux;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
1078
panel-api.py
1078
panel-api.py
File diff suppressed because it is too large
Load diff
358
panelctl.sh
358
panelctl.sh
|
|
@ -7,17 +7,7 @@ VOLUMES_DIR="${BASE_DIR}/volumes"
|
||||||
ROUTES_DIR="${BASE_DIR}/routes"
|
ROUTES_DIR="${BASE_DIR}/routes"
|
||||||
STATE_DIR="${BASE_DIR}/state"
|
STATE_DIR="${BASE_DIR}/state"
|
||||||
APPS_DIR="${STATE_DIR}/apps"
|
APPS_DIR="${STATE_DIR}/apps"
|
||||||
ENV_DIR="${STATE_DIR}/env"
|
|
||||||
BACKUPS_DIR="${BASE_DIR}/backups"
|
BACKUPS_DIR="${BASE_DIR}/backups"
|
||||||
# Owner of generated files that Caddy has to read (via the shared group).
|
|
||||||
PANEL_USER="${PANEL_USER:-reudy}"
|
|
||||||
PANEL_GROUP="${PANEL_GROUP:-panelroutes}"
|
|
||||||
|
|
||||||
# Set by load_app: compose file arguments, and the app's environment variables
|
|
||||||
# as KEY=VALUE words (passed to compose via env(1), never sourced).
|
|
||||||
COMPOSE_ARGS=()
|
|
||||||
APP_ENV_ARGS=()
|
|
||||||
APP_ENV_KEYS=()
|
|
||||||
|
|
||||||
FORWARD_AUTH_BLOCK=' forward_auth 127.0.0.1:9091 {
|
FORWARD_AUTH_BLOCK=' forward_auth 127.0.0.1:9091 {
|
||||||
uri /api/authz/forward-auth
|
uri /api/authz/forward-auth
|
||||||
|
|
@ -25,39 +15,12 @@ FORWARD_AUTH_BLOCK=' forward_auth 127.0.0.1:9091 {
|
||||||
}
|
}
|
||||||
'
|
'
|
||||||
|
|
||||||
validate_route_entry() {
|
|
||||||
local entry="$1"
|
|
||||||
# Format: domain|upstream[/path] or domain|upstream (path is optional)
|
|
||||||
IFS='|' read -r domain upstream path <<< "${entry}"
|
|
||||||
[[ -n "${domain}" ]] || fail "empty domain in route entry '${entry}'"
|
|
||||||
[[ -n "${upstream}" ]] || fail "empty upstream in route entry '${entry}'"
|
|
||||||
validate_single_domain "${domain}"
|
|
||||||
# Validate upstream has a port
|
|
||||||
local upstream_port="${upstream##*:}"
|
|
||||||
[[ "${upstream_port}" =~ ^[0-9]+$ ]] || fail "upstream '${upstream}' missing numeric port in route entry '${entry}'"
|
|
||||||
validate_port "${upstream_port}"
|
|
||||||
if [[ -n "${path}" ]]; then
|
|
||||||
[[ "${path}" == /* ]] || fail "path '${path}' must start with / in route entry '${entry}'"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
validate_routes() {
|
|
||||||
local routes_str="$1"
|
|
||||||
IFS=',' read -ra entries <<< "${routes_str}"
|
|
||||||
[[ ${#entries[@]} -ge 1 ]] || fail "at least one route is required"
|
|
||||||
for entry in "${entries[@]}"; do
|
|
||||||
entry="$(echo "${entry}" | xargs)"
|
|
||||||
validate_route_entry "${entry}"
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
usage() {
|
usage() {
|
||||||
cat <<'EOF'
|
cat <<'EOF'
|
||||||
panelctl - minimal app panel helper
|
panelctl - minimal app panel helper
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
panelctl init <name> "<domain>|<upstream>[,...]" [auth]
|
panelctl init <name> <domains> <port> [auth]
|
||||||
panelctl set-routes <name> "<domain>|<upstream>[,...]"
|
|
||||||
panelctl render-route <name>
|
panelctl render-route <name>
|
||||||
panelctl deploy <name>
|
panelctl deploy <name>
|
||||||
panelctl restart <name>
|
panelctl restart <name>
|
||||||
|
|
@ -73,15 +36,14 @@ Usage:
|
||||||
panelctl list
|
panelctl list
|
||||||
panelctl show <name>
|
panelctl show <name>
|
||||||
|
|
||||||
Each route is a domain|upstream pair. Upstream is host:port.
|
Domains can be comma-separated for multiple domains:
|
||||||
Multiple routes are comma-separated:
|
panelctl init myapp "app.example.com,www.example.com" 18080 true
|
||||||
panelctl init myapp "app.example.com|127.0.0.1:18080,api.example.com|127.0.0.1:18081" true
|
|
||||||
|
|
||||||
Wildcard domains are supported (requires DNS challenge in Caddy):
|
Wildcard domains are supported (requires DNS challenge in Caddy):
|
||||||
panelctl init myapp "*.example.com|127.0.0.1:18080" true
|
panelctl init myapp "*.example.com" 18080 true
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
panelctl init whoami "whoami.reudy.net|127.0.0.1:18080" true
|
panelctl init whoami whoami.srazka.com 18080 true
|
||||||
panelctl deploy whoami
|
panelctl deploy whoami
|
||||||
panelctl restart whoami
|
panelctl restart whoami
|
||||||
panelctl status whoami
|
panelctl status whoami
|
||||||
|
|
@ -159,19 +121,6 @@ app_route_file() {
|
||||||
echo "${ROUTES_DIR}/routes.caddy"
|
echo "${ROUTES_DIR}/routes.caddy"
|
||||||
}
|
}
|
||||||
|
|
||||||
# The routes file is shared by all apps and rewritten read-modify-write, so
|
|
||||||
# concurrent panelctl runs (the API handles requests in parallel) must take turns.
|
|
||||||
routes_lock() {
|
|
||||||
exec 9>"${ROUTES_DIR}/.routes.lock"
|
|
||||||
if command -v flock >/dev/null 2>&1; then
|
|
||||||
flock -w 30 9 || fail "timed out waiting for the routes file lock"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
routes_unlock() {
|
|
||||||
exec 9>&-
|
|
||||||
}
|
|
||||||
|
|
||||||
load_app() {
|
load_app() {
|
||||||
local name="$1"
|
local name="$1"
|
||||||
local manifest
|
local manifest
|
||||||
|
|
@ -179,92 +128,6 @@ load_app() {
|
||||||
[[ -f "${manifest}" ]] || fail "app '${name}' does not exist"
|
[[ -f "${manifest}" ]] || fail "app '${name}' does not exist"
|
||||||
# shellcheck disable=SC1090
|
# shellcheck disable=SC1090
|
||||||
source "${manifest}"
|
source "${manifest}"
|
||||||
|
|
||||||
# Backward compat: migrate old APP_DOMAIN/APP_PORT/APP_UPSTREAM to APP_ROUTES
|
|
||||||
if [[ -z "${APP_ROUTES:-}" && -n "${APP_DOMAIN:-}" ]]; then
|
|
||||||
local upstream="${APP_UPSTREAM:-127.0.0.1:${APP_PORT:-18080}}"
|
|
||||||
local routes=""
|
|
||||||
local domains_str="${APP_DOMAINS:-${APP_DOMAIN}}"
|
|
||||||
IFS=',' read -ra domain_arr <<< "${domains_str}"
|
|
||||||
for d in "${domain_arr[@]}"; do
|
|
||||||
d="$(echo "${d}" | xargs)"
|
|
||||||
if [[ -n "${routes}" ]]; then
|
|
||||||
routes="${routes},${d}|${upstream}"
|
|
||||||
else
|
|
||||||
routes="${d}|${upstream}"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
APP_ROUTES="${routes}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
load_app_env "${name}"
|
|
||||||
}
|
|
||||||
|
|
||||||
app_env_file() {
|
|
||||||
echo "${ENV_DIR}/$1.env"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Generated compose override that passes the app's variables into every service.
|
|
||||||
app_env_override() {
|
|
||||||
echo "${STACKS_DIR}/$1/.panel-env.yaml"
|
|
||||||
}
|
|
||||||
|
|
||||||
load_app_env() {
|
|
||||||
local name="$1"
|
|
||||||
local file line key override
|
|
||||||
file="$(app_env_file "${name}")"
|
|
||||||
APP_ENV_ARGS=()
|
|
||||||
APP_ENV_KEYS=()
|
|
||||||
if [[ -f "${file}" ]]; then
|
|
||||||
while IFS= read -r line || [[ -n "${line}" ]]; do
|
|
||||||
[[ -z "${line}" || "${line}" == \#* || "${line}" != *=* ]] && continue
|
|
||||||
key="${line%%=*}"
|
|
||||||
[[ "${key}" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]] || continue
|
|
||||||
APP_ENV_ARGS+=("${line}")
|
|
||||||
APP_ENV_KEYS+=("${key}")
|
|
||||||
done <"${file}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
COMPOSE_ARGS=(-f "${APP_COMPOSE_FILE}")
|
|
||||||
override="$(app_env_override "${name}")"
|
|
||||||
if [[ -f "${override}" ]]; then
|
|
||||||
COMPOSE_ARGS+=(-f "${override}")
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# (Re)generate the env override before containers are created. Variables are
|
|
||||||
# always available for ${VAR} interpolation; with APP_ENV_INJECT (default true)
|
|
||||||
# every service also receives them. Bare keys make compose read the values from
|
|
||||||
# its own environment, so values never have to be quoted into YAML.
|
|
||||||
prepare_env_override() {
|
|
||||||
local name="$1"
|
|
||||||
local override services svc key tmp
|
|
||||||
override="$(app_env_override "${name}")"
|
|
||||||
|
|
||||||
if [[ ${#APP_ENV_KEYS[@]} -eq 0 || "${APP_ENV_INJECT:-true}" != "true" ]]; then
|
|
||||||
rm -f "${override}"
|
|
||||||
COMPOSE_ARGS=(-f "${APP_COMPOSE_FILE}")
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
services="$(run_compose -f "${APP_COMPOSE_FILE}" config --services 2>/dev/null)" \
|
|
||||||
|| fail "could not list compose services to pass environment variables (is the compose file valid?)"
|
|
||||||
|
|
||||||
tmp="$(mktemp)"
|
|
||||||
{
|
|
||||||
echo "# Generated by panelctl from the app's environment variables. Do not edit."
|
|
||||||
echo "services:"
|
|
||||||
while IFS= read -r svc; do
|
|
||||||
[[ "${svc}" =~ ^[A-Za-z0-9._-]+$ ]] || continue
|
|
||||||
printf ' "%s":\n environment:\n' "${svc}"
|
|
||||||
for key in "${APP_ENV_KEYS[@]}"; do
|
|
||||||
printf ' - %s\n' "${key}"
|
|
||||||
done
|
|
||||||
done <<<"${services}"
|
|
||||||
} >"${tmp}"
|
|
||||||
install -m 0640 "${tmp}" "${override}"
|
|
||||||
rm -f "${tmp}"
|
|
||||||
COMPOSE_ARGS=(-f "${APP_COMPOSE_FILE}" -f "${override}")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
compose_command() {
|
compose_command() {
|
||||||
|
|
@ -331,33 +194,28 @@ run_compose() {
|
||||||
|
|
||||||
if [[ "${compose}" == *" compose" ]]; then
|
if [[ "${compose}" == *" compose" ]]; then
|
||||||
local podman_bin="${compose% compose}"
|
local podman_bin="${compose% compose}"
|
||||||
env "${APP_ENV_ARGS[@]}" "${podman_bin}" compose "$@"
|
"${podman_bin}" compose "$@"
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
|
|
||||||
env "${APP_ENV_ARGS[@]}" "${compose}" "$@"
|
"${compose}" "$@"
|
||||||
}
|
}
|
||||||
|
|
||||||
write_default_compose() {
|
write_default_compose() {
|
||||||
local name="$1"
|
local name="$1"
|
||||||
local routes="$2"
|
local port="$2"
|
||||||
local stack_dir
|
local stack_dir
|
||||||
local volume_dir
|
local volume_dir
|
||||||
stack_dir="$(app_stack_dir "${name}")"
|
stack_dir="$(app_stack_dir "${name}")"
|
||||||
volume_dir="$(app_volume_dir "${name}")"
|
volume_dir="$(app_volume_dir "${name}")"
|
||||||
|
|
||||||
# Use first route's upstream port for the default compose mapping
|
|
||||||
local first_route="${routes%%,*}"
|
|
||||||
local first_upstream="${first_route#*|}"
|
|
||||||
local container_port="${first_upstream##*:}"
|
|
||||||
|
|
||||||
cat >"${stack_dir}/compose.yaml" <<EOF
|
cat >"${stack_dir}/compose.yaml" <<EOF
|
||||||
services:
|
services:
|
||||||
app:
|
app:
|
||||||
image: docker.io/traefik/whoami:latest
|
image: docker.io/traefik/whoami:latest
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
- "127.0.0.1:${container_port}:80"
|
- "127.0.0.1:${port}:80"
|
||||||
volumes:
|
volumes:
|
||||||
- ${volume_dir}/data:/data
|
- ${volume_dir}/data:/data
|
||||||
EOF
|
EOF
|
||||||
|
|
@ -365,8 +223,9 @@ EOF
|
||||||
|
|
||||||
write_manifest() {
|
write_manifest() {
|
||||||
local name="$1"
|
local name="$1"
|
||||||
local routes="$2"
|
local domains="$2"
|
||||||
local auth="$3"
|
local port="$3"
|
||||||
|
local auth="$4"
|
||||||
local manifest
|
local manifest
|
||||||
local stack_dir
|
local stack_dir
|
||||||
local volume_dir
|
local volume_dir
|
||||||
|
|
@ -377,9 +236,17 @@ write_manifest() {
|
||||||
volume_dir="$(app_volume_dir "${name}")"
|
volume_dir="$(app_volume_dir "${name}")"
|
||||||
route_file="$(app_route_file)"
|
route_file="$(app_route_file)"
|
||||||
|
|
||||||
|
# First domain is the primary (used for APP_DOMAIN backward compat)
|
||||||
|
local primary_domain
|
||||||
|
IFS=',' read -ra domain_arr <<< "${domains}"
|
||||||
|
primary_domain="$(echo "${domain_arr[0]}" | xargs)"
|
||||||
|
|
||||||
cat >"${manifest}" <<EOF
|
cat >"${manifest}" <<EOF
|
||||||
APP_NAME="${name}"
|
APP_NAME="${name}"
|
||||||
APP_ROUTES="${routes}"
|
APP_DOMAIN="${primary_domain}"
|
||||||
|
APP_DOMAINS="${domains}"
|
||||||
|
APP_PORT="${port}"
|
||||||
|
APP_UPSTREAM="127.0.0.1:${port}"
|
||||||
APP_AUTH_PROTECTED="${auth}"
|
APP_AUTH_PROTECTED="${auth}"
|
||||||
APP_STACK_DIR="${stack_dir}"
|
APP_STACK_DIR="${stack_dir}"
|
||||||
APP_COMPOSE_FILE="${stack_dir}/compose.yaml"
|
APP_COMPOSE_FILE="${stack_dir}/compose.yaml"
|
||||||
|
|
@ -390,11 +257,13 @@ EOF
|
||||||
|
|
||||||
cmd_init() {
|
cmd_init() {
|
||||||
local name="$1"
|
local name="$1"
|
||||||
local routes="$2"
|
local domains="$2"
|
||||||
local auth="${3:-true}"
|
local port="$3"
|
||||||
|
local auth="${4:-true}"
|
||||||
|
|
||||||
validate_name "${name}"
|
validate_name "${name}"
|
||||||
validate_routes "${routes}"
|
validate_domains "${domains}"
|
||||||
|
validate_port "${port}"
|
||||||
[[ "${auth}" == "true" || "${auth}" == "false" ]] || fail "auth must be true or false"
|
[[ "${auth}" == "true" || "${auth}" == "false" ]] || fail "auth must be true or false"
|
||||||
|
|
||||||
ensure_base_dirs
|
ensure_base_dirs
|
||||||
|
|
@ -409,8 +278,8 @@ cmd_init() {
|
||||||
[[ ! -f "${manifest}" ]] || fail "app '${name}' already exists"
|
[[ ! -f "${manifest}" ]] || fail "app '${name}' already exists"
|
||||||
|
|
||||||
mkdir -p "${stack_dir}" "${volume_dir}/data"
|
mkdir -p "${stack_dir}" "${volume_dir}/data"
|
||||||
write_default_compose "${name}" "${routes}"
|
write_default_compose "${name}" "${port}"
|
||||||
write_manifest "${name}" "${routes}" "${auth}"
|
write_manifest "${name}" "${domains}" "${port}" "${auth}"
|
||||||
cmd_render_route "${name}"
|
cmd_render_route "${name}"
|
||||||
|
|
||||||
log info "initialized app '${name}'"
|
log info "initialized app '${name}'"
|
||||||
|
|
@ -426,11 +295,22 @@ cmd_render_route() {
|
||||||
auth_block="${FORWARD_AUTH_BLOCK}"
|
auth_block="${FORWARD_AUTH_BLOCK}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Build domain list for Caddy block header.
|
||||||
|
local caddy_domains=""
|
||||||
|
local domains_str="${APP_DOMAINS:-${APP_DOMAIN}}"
|
||||||
|
IFS=',' read -ra domain_arr <<< "${domains_str}"
|
||||||
|
for d in "${domain_arr[@]}"; do
|
||||||
|
d="$(echo "${d}" | xargs)"
|
||||||
|
if [[ -n "${caddy_domains}" ]]; then
|
||||||
|
caddy_domains="${caddy_domains}, ${d}"
|
||||||
|
else
|
||||||
|
caddy_domains="${d}"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
local route_file
|
local route_file
|
||||||
route_file="$(app_route_file)"
|
route_file="$(app_route_file)"
|
||||||
|
|
||||||
routes_lock
|
|
||||||
|
|
||||||
# Strip any existing block for this app from the aggregate file.
|
# Strip any existing block for this app from the aggregate file.
|
||||||
local tmp
|
local tmp
|
||||||
tmp="$(mktemp)"
|
tmp="$(mktemp)"
|
||||||
|
|
@ -443,35 +323,16 @@ cmd_render_route() {
|
||||||
|
|
||||||
{
|
{
|
||||||
printf "# route:%s:start\n" "${name}"
|
printf "# route:%s:start\n" "${name}"
|
||||||
IFS=',' read -ra route_entries <<< "${APP_ROUTES}"
|
printf "%s {\n" "${caddy_domains}"
|
||||||
for entry in "${route_entries[@]}"; do
|
if [[ -n "${auth_block}" ]]; then
|
||||||
entry="$(echo "${entry}" | xargs)"
|
printf "%s\n" "${auth_block}"
|
||||||
IFS='|' read -r domain upstream path <<< "${entry}"
|
fi
|
||||||
# If upstream is empty (no second pipe), this is the old format
|
printf " reverse_proxy %s\n" "${APP_UPSTREAM}"
|
||||||
if [[ -z "${upstream}" ]]; then
|
printf "}\n"
|
||||||
upstream="${path}"
|
|
||||||
path=""
|
|
||||||
fi
|
|
||||||
if [[ -n "${path}" ]]; then
|
|
||||||
if [[ -n "${auth_block}" ]]; then
|
|
||||||
printf "%s {\n%s reverse_proxy %s %s\n}\n" "${domain}" "${auth_block}" "${path}" "${upstream}"
|
|
||||||
else
|
|
||||||
printf "%s {\n reverse_proxy %s %s\n}\n" "${domain}" "${path}" "${upstream}"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
if [[ -n "${auth_block}" ]]; then
|
|
||||||
printf "%s {\n%s reverse_proxy %s\n}\n" "${domain}" "${auth_block}" "${upstream}"
|
|
||||||
else
|
|
||||||
printf "%s {\n reverse_proxy %s\n}\n" "${domain}" "${upstream}"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
printf "# route:%s:end\n" "${name}"
|
printf "# route:%s:end\n" "${name}"
|
||||||
} >>"${tmp}"
|
} >>"${tmp}"
|
||||||
|
|
||||||
install -m 0664 -o "${PANEL_USER}" -g "${PANEL_GROUP}" "${tmp}" "${route_file}"
|
install -m 0664 -o reudy -g panelroutes "${tmp}" "${route_file}"
|
||||||
rm -f "${tmp}"
|
|
||||||
routes_unlock
|
|
||||||
log info "rendered route ${route_file}"
|
log info "rendered route ${route_file}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -483,19 +344,11 @@ cmd_deploy() {
|
||||||
log info "Starting deployment for app '${name}'"
|
log info "Starting deployment for app '${name}'"
|
||||||
|
|
||||||
cmd_render_route "${name}"
|
cmd_render_route "${name}"
|
||||||
prepare_env_override "${name}"
|
|
||||||
|
|
||||||
# Capture compose output so callers (the web UI) can show why a deploy failed,
|
if ! run_compose -f "${APP_COMPOSE_FILE}" up -d --remove-orphans 2>&1 | systemd-cat -t panelctl -p info 2>/dev/null; then
|
||||||
# and still forward it to the journal.
|
|
||||||
local output
|
|
||||||
if ! output="$(run_compose "${COMPOSE_ARGS[@]}" up -d --build --remove-orphans 2>&1)"; then
|
|
||||||
printf '%s\n' "${output}" | systemd-cat -t panelctl -p err 2>/dev/null || true
|
|
||||||
printf '%s\n' "${output}" >&2
|
|
||||||
log err "Deployment failed for app '${name}'"
|
log err "Deployment failed for app '${name}'"
|
||||||
fail "compose up failed"
|
fail "compose up failed"
|
||||||
fi
|
fi
|
||||||
printf '%s\n' "${output}" | systemd-cat -t panelctl -p info 2>/dev/null || true
|
|
||||||
printf '%s\n' "${output}"
|
|
||||||
|
|
||||||
log info "Successfully deployed app '${name}'"
|
log info "Successfully deployed app '${name}'"
|
||||||
}
|
}
|
||||||
|
|
@ -507,10 +360,9 @@ cmd_restart() {
|
||||||
|
|
||||||
log info "Restarting app '${name}'"
|
log info "Restarting app '${name}'"
|
||||||
|
|
||||||
run_compose "${COMPOSE_ARGS[@]}" down --remove-orphans || fail "compose down failed"
|
run_compose -f "${APP_COMPOSE_FILE}" down --remove-orphans || fail "compose down failed"
|
||||||
prepare_env_override "${name}"
|
|
||||||
|
|
||||||
if ! run_compose "${COMPOSE_ARGS[@]}" up -d --build --remove-orphans 2>&1; then
|
if ! run_compose -f "${APP_COMPOSE_FILE}" up -d --remove-orphans 2>&1; then
|
||||||
fail "compose up failed during restart"
|
fail "compose up failed during restart"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
@ -522,7 +374,7 @@ cmd_stop() {
|
||||||
validate_name "${name}"
|
validate_name "${name}"
|
||||||
load_app "${name}"
|
load_app "${name}"
|
||||||
|
|
||||||
run_compose "${COMPOSE_ARGS[@]}" down --remove-orphans || fail "compose down failed"
|
run_compose -f "${APP_COMPOSE_FILE}" down --remove-orphans || fail "compose down failed"
|
||||||
log info "stopped app '${name}'"
|
log info "stopped app '${name}'"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -531,8 +383,8 @@ cmd_status() {
|
||||||
validate_name "${name}"
|
validate_name "${name}"
|
||||||
load_app "${name}"
|
load_app "${name}"
|
||||||
|
|
||||||
run_compose "${COMPOSE_ARGS[@]}" ps --format json 2>/dev/null || \
|
run_compose -f "${APP_COMPOSE_FILE}" ps --format json 2>/dev/null || \
|
||||||
run_compose "${COMPOSE_ARGS[@]}" ps 2>/dev/null || \
|
run_compose -f "${APP_COMPOSE_FILE}" ps 2>/dev/null || \
|
||||||
log info "no containers running"
|
log info "no containers running"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -555,7 +407,7 @@ cmd_logs() {
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
run_compose "${COMPOSE_ARGS[@]}" logs --tail "${tail_lines}" 2>&1 || log info "no logs available"
|
run_compose -f "${APP_COMPOSE_FILE}" logs --tail "${tail_lines}" 2>&1 || log info "no logs available"
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd_validate_compose() {
|
cmd_validate_compose() {
|
||||||
|
|
@ -563,11 +415,11 @@ cmd_validate_compose() {
|
||||||
validate_name "${name}"
|
validate_name "${name}"
|
||||||
load_app "${name}"
|
load_app "${name}"
|
||||||
|
|
||||||
if run_compose "${COMPOSE_ARGS[@]}" config >/dev/null 2>&1; then
|
if run_compose -f "${APP_COMPOSE_FILE}" config >/dev/null 2>&1; then
|
||||||
log info "compose file is valid"
|
log info "compose file is valid"
|
||||||
else
|
else
|
||||||
local output
|
local output
|
||||||
output="$(run_compose "${COMPOSE_ARGS[@]}" config 2>&1 || true)"
|
output="$(run_compose -f "${APP_COMPOSE_FILE}" config 2>&1 || true)"
|
||||||
fail "compose validation failed: ${output}"
|
fail "compose validation failed: ${output}"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
@ -578,22 +430,19 @@ cmd_remove() {
|
||||||
validate_name "${name}"
|
validate_name "${name}"
|
||||||
load_app "${name}"
|
load_app "${name}"
|
||||||
|
|
||||||
run_compose "${COMPOSE_ARGS[@]}" down --remove-orphans 2>/dev/null || true
|
run_compose -f "${APP_COMPOSE_FILE}" down --remove-orphans 2>/dev/null || true
|
||||||
|
|
||||||
# Remove this app's block from the aggregate routes file.
|
# Remove this app's block from the aggregate routes file.
|
||||||
local route_file
|
local route_file
|
||||||
route_file="$(app_route_file)"
|
route_file="$(app_route_file)"
|
||||||
if [[ -f "${route_file}" ]]; then
|
if [[ -f "${route_file}" ]]; then
|
||||||
routes_lock
|
|
||||||
local tmp
|
local tmp
|
||||||
tmp="$(mktemp)"
|
tmp="$(mktemp)"
|
||||||
sed "/^# route:${name}:start$/,/^# route:${name}:end$/d" "${route_file}" >"${tmp}" || true
|
sed "/^# route:${name}:start$/,/^# route:${name}:end$/d" "${route_file}" >"${tmp}" || true
|
||||||
install -m 0664 -o "${PANEL_USER}" -g "${PANEL_GROUP}" "${tmp}" "${route_file}"
|
install -m 0664 -o reudy -g panelroutes "${tmp}" "${route_file}"
|
||||||
rm -f "${tmp}"
|
|
||||||
routes_unlock
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
rm -f "$(app_manifest "${name}")" "$(app_env_file "${name}")"
|
rm -f "$(app_manifest "${name}")"
|
||||||
rm -rf "${APP_STACK_DIR}"
|
rm -rf "${APP_STACK_DIR}"
|
||||||
|
|
||||||
if [[ "${keep_volumes}" != "--keep-volumes" ]]; then
|
if [[ "${keep_volumes}" != "--keep-volumes" ]]; then
|
||||||
|
|
@ -603,45 +452,6 @@ cmd_remove() {
|
||||||
log info "removed app '${name}'"
|
log info "removed app '${name}'"
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd_set_routes() {
|
|
||||||
local name="$1"
|
|
||||||
local routes="$2"
|
|
||||||
local manifest
|
|
||||||
|
|
||||||
validate_name "${name}"
|
|
||||||
validate_routes "${routes}"
|
|
||||||
manifest="$(app_manifest "${name}")"
|
|
||||||
[[ -f "${manifest}" ]] || fail "app '${name}' does not exist"
|
|
||||||
|
|
||||||
# Update APP_ROUTES in the manifest file, strip old fields, preserve others
|
|
||||||
local tmp
|
|
||||||
tmp="$(mktemp)"
|
|
||||||
local found_routes=false
|
|
||||||
while IFS= read -r line; do
|
|
||||||
case "${line}" in
|
|
||||||
APP_ROUTES=*)
|
|
||||||
printf 'APP_ROUTES="%s"\n' "${routes}" >> "${tmp}"
|
|
||||||
found_routes=true
|
|
||||||
;;
|
|
||||||
APP_DOMAIN=*|APP_DOMAINS=*|APP_PORT=*|APP_UPSTREAM=*)
|
|
||||||
# Strip old format fields
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
printf '%s\n' "${line}" >> "${tmp}"
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done < "${manifest}"
|
|
||||||
if ! "${found_routes}"; then
|
|
||||||
printf 'APP_ROUTES="%s"\n' "${routes}" >> "${tmp}"
|
|
||||||
fi
|
|
||||||
install -m 0664 "${tmp}" "${manifest}"
|
|
||||||
|
|
||||||
# Re-render Caddy routes (auto-reloads via systemd.path watcher)
|
|
||||||
cmd_render_route "${name}"
|
|
||||||
|
|
||||||
log info "updated routes for app '${name}'. Edit compose file if new ports need exposing."
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd_backup() {
|
cmd_backup() {
|
||||||
local name="$1"
|
local name="$1"
|
||||||
validate_name "${name}"
|
validate_name "${name}"
|
||||||
|
|
@ -659,10 +469,10 @@ cmd_backup() {
|
||||||
|
|
||||||
# Stop containers before backup for consistency
|
# Stop containers before backup for consistency
|
||||||
local was_running=false
|
local was_running=false
|
||||||
if run_compose "${COMPOSE_ARGS[@]}" ps --format json 2>/dev/null | grep -q '"running"' 2>/dev/null; then
|
if run_compose -f "${APP_COMPOSE_FILE}" ps --format json 2>/dev/null | grep -q '"running"' 2>/dev/null; then
|
||||||
was_running=true
|
was_running=true
|
||||||
log info "stopping containers for consistent backup..."
|
log info "stopping containers for consistent backup..."
|
||||||
run_compose "${COMPOSE_ARGS[@]}" down 2>/dev/null || true
|
run_compose -f "${APP_COMPOSE_FILE}" down 2>/dev/null || true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
(cd "${volume_dir}" && zip -r "${backup_file}" .) || fail "zip failed"
|
(cd "${volume_dir}" && zip -r "${backup_file}" .) || fail "zip failed"
|
||||||
|
|
@ -677,7 +487,7 @@ cmd_backup() {
|
||||||
# Restart if it was running
|
# Restart if it was running
|
||||||
if [[ "${was_running}" == "true" ]]; then
|
if [[ "${was_running}" == "true" ]]; then
|
||||||
log info "restarting containers after backup..."
|
log info "restarting containers after backup..."
|
||||||
run_compose "${COMPOSE_ARGS[@]}" up -d 2>/dev/null || true
|
run_compose -f "${APP_COMPOSE_FILE}" up -d 2>/dev/null || true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local size
|
local size
|
||||||
|
|
@ -714,7 +524,7 @@ cmd_volume_clear() {
|
||||||
load_app "${name}"
|
load_app "${name}"
|
||||||
|
|
||||||
log info "clearing volume data for app '${name}'"
|
log info "clearing volume data for app '${name}'"
|
||||||
run_compose "${COMPOSE_ARGS[@]}" down --remove-orphans 2>/dev/null || true
|
run_compose -f "${APP_COMPOSE_FILE}" down --remove-orphans 2>/dev/null || true
|
||||||
|
|
||||||
local data_dir="${APP_VOLUME_DIR}/data"
|
local data_dir="${APP_VOLUME_DIR}/data"
|
||||||
if [[ -d "${data_dir}" ]]; then
|
if [[ -d "${data_dir}" ]]; then
|
||||||
|
|
@ -751,7 +561,7 @@ cmd_restore() {
|
||||||
|
|
||||||
# Stop containers before restore
|
# Stop containers before restore
|
||||||
log info "stopping containers for restore..."
|
log info "stopping containers for restore..."
|
||||||
run_compose "${COMPOSE_ARGS[@]}" down 2>/dev/null || true
|
run_compose -f "${APP_COMPOSE_FILE}" down 2>/dev/null || true
|
||||||
|
|
||||||
# Clear existing volume data and extract backup
|
# Clear existing volume data and extract backup
|
||||||
rm -rf "${volume_dir:?}"/*
|
rm -rf "${volume_dir:?}"/*
|
||||||
|
|
@ -779,8 +589,7 @@ cmd_inspect_volumes() {
|
||||||
if [[ -n "${podman_bin}" ]]; then
|
if [[ -n "${podman_bin}" ]]; then
|
||||||
"${podman_bin}" volume ls --filter label=com.docker.compose.project="${name}" --format '{{.Name}}|{{.Mountpoint}}' 2>/dev/null || true
|
"${podman_bin}" volume ls --filter label=com.docker.compose.project="${name}" --format '{{.Name}}|{{.Mountpoint}}' 2>/dev/null || true
|
||||||
"${podman_bin}" volume ls --filter label=io.podman.compose.project="${name}" --format '{{.Name}}|{{.Mountpoint}}' 2>/dev/null || true
|
"${podman_bin}" volume ls --filter label=io.podman.compose.project="${name}" --format '{{.Name}}|{{.Mountpoint}}' 2>/dev/null || true
|
||||||
# grep exits 1 when there are no named volumes; that is not an error.
|
fi | sort -u | grep -v '^$'
|
||||||
fi | sort -u | grep -v '^$' || true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd_list() {
|
cmd_list() {
|
||||||
|
|
@ -790,32 +599,9 @@ cmd_list() {
|
||||||
[[ -e "${mf}" ]] || continue
|
[[ -e "${mf}" ]] || continue
|
||||||
found=1
|
found=1
|
||||||
# shellcheck disable=SC1090
|
# shellcheck disable=SC1090
|
||||||
source /dev/null # reset any leftover variables
|
|
||||||
unset APP_REPO_URL APP_REPO_BRANCH APP_REPO_DIR 2>/dev/null || true
|
|
||||||
source "${mf}"
|
source "${mf}"
|
||||||
# Backward compat: build APP_ROUTES from old format
|
local domains="${APP_DOMAINS:-${APP_DOMAIN}}"
|
||||||
local routes="${APP_ROUTES:-}"
|
echo "${APP_NAME} ${domains} ${APP_UPSTREAM} auth=${APP_AUTH_PROTECTED}"
|
||||||
if [[ -z "${routes}" && -n "${APP_DOMAIN:-}" ]]; then
|
|
||||||
local upstream="${APP_UPSTREAM:-127.0.0.1:${APP_PORT:-18080}}"
|
|
||||||
local domains_str="${APP_DOMAINS:-${APP_DOMAIN}}"
|
|
||||||
IFS=',' read -ra domain_arr <<< "${domains_str}"
|
|
||||||
for d in "${domain_arr[@]}"; do
|
|
||||||
d="$(echo "${d}" | xargs)"
|
|
||||||
if [[ -n "${routes}" ]]; then
|
|
||||||
routes="${routes},${d}|${upstream}"
|
|
||||||
else
|
|
||||||
routes="${d}|${upstream}"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
# Show abbreviated: first route's domain + upstream, and count
|
|
||||||
local first_route="${routes%%,*}"
|
|
||||||
local route_count=1
|
|
||||||
if [[ "${routes}" == *","* ]]; then
|
|
||||||
route_count="$(( $(grep -o ',' <<< "${routes}" | wc -l) + 1 ))"
|
|
||||||
fi
|
|
||||||
local repo_info="${APP_REPO_URL:-}"
|
|
||||||
echo "${APP_NAME} ${first_route} routes=${route_count} auth=${APP_AUTH_PROTECTED} ${repo_info}"
|
|
||||||
done
|
done
|
||||||
|
|
||||||
if [[ "${found}" -eq 0 ]]; then
|
if [[ "${found}" -eq 0 ]]; then
|
||||||
|
|
@ -837,12 +623,8 @@ main() {
|
||||||
|
|
||||||
case "${cmd}" in
|
case "${cmd}" in
|
||||||
init)
|
init)
|
||||||
[[ $# -ge 3 ]] || fail "usage: panelctl init <name> <routes> [auth]"
|
[[ $# -ge 4 ]] || fail "usage: panelctl init <name> <domains> <port> [auth]"
|
||||||
cmd_init "$2" "$3" "${4:-true}"
|
cmd_init "$2" "$3" "$4" "${5:-true}"
|
||||||
;;
|
|
||||||
set-routes)
|
|
||||||
[[ $# -eq 3 ]] || fail "usage: panelctl set-routes <name> <routes>"
|
|
||||||
cmd_set_routes "$2" "$3"
|
|
||||||
;;
|
;;
|
||||||
render-route)
|
render-route)
|
||||||
[[ $# -eq 2 ]] || fail "usage: panelctl render-route <name>"
|
[[ $# -eq 2 ]] || fail "usage: panelctl render-route <name>"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue