refactor: remove config import from startup and update README for CLI usage
This commit is contained in:
parent
b25ed606b2
commit
04cbb58559
4 changed files with 27 additions and 76 deletions
|
|
@ -1,6 +1,5 @@
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import fs from "node:fs/promises";
|
import fs from "node:fs/promises";
|
||||||
import path from "node:path";
|
|
||||||
import slugify from "slugify";
|
import slugify from "slugify";
|
||||||
import { db } from "../../db/db";
|
import { db } from "../../db/db";
|
||||||
import {
|
import {
|
||||||
|
|
@ -70,23 +69,6 @@ function interpolateEnvVars(value: unknown): unknown {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadConfigFromFile(): Promise<unknown | null> {
|
|
||||||
try {
|
|
||||||
const configPath = process.env.ZEROBYTE_CONFIG_PATH || "zerobyte.config.json";
|
|
||||||
const configFullPath = path.resolve(process.cwd(), configPath);
|
|
||||||
try {
|
|
||||||
const raw = await fs.readFile(configFullPath, "utf-8");
|
|
||||||
return JSON.parse(raw);
|
|
||||||
} catch (error) {
|
|
||||||
if (isRecord(error) && error.code === "ENOENT") return null;
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
logger.warn(`No config file loaded or error parsing config: ${toError(error).message}`);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseImportConfig(configRaw: unknown): ImportConfig {
|
function parseImportConfig(configRaw: unknown): ImportConfig {
|
||||||
const root = isRecord(configRaw) ? configRaw : {};
|
const root = isRecord(configRaw) ? configRaw : {};
|
||||||
const config = isRecord(root.config) ? (root.config as Record<string, unknown>) : root;
|
const config = isRecord(root.config) ? (root.config as Record<string, unknown>) : root;
|
||||||
|
|
@ -838,21 +820,3 @@ export async function applyConfigImport(configRaw: unknown, options: ImportOptio
|
||||||
logImportSummary(result);
|
logImportSummary(result);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Import configuration from a file (used by env var startup)
|
|
||||||
*/
|
|
||||||
export async function applyConfigImportFromFile(): Promise<void> {
|
|
||||||
const configRaw = await loadConfigFromFile();
|
|
||||||
if (configRaw === null) return; // No config file, nothing to do
|
|
||||||
|
|
||||||
logger.info("Starting config import from file...");
|
|
||||||
const config = parseImportConfig(configRaw);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await runImport(config);
|
|
||||||
logImportSummary(result);
|
|
||||||
} catch (e) {
|
|
||||||
logger.error(`Config import failed: ${toError(e).message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,6 @@ import { VolumeHealthCheckJob } from "../../jobs/healthchecks";
|
||||||
import { RepositoryHealthCheckJob } from "../../jobs/repository-healthchecks";
|
import { RepositoryHealthCheckJob } from "../../jobs/repository-healthchecks";
|
||||||
import { BackupExecutionJob } from "../../jobs/backup-execution";
|
import { BackupExecutionJob } from "../../jobs/backup-execution";
|
||||||
import { CleanupSessionsJob } from "../../jobs/cleanup-sessions";
|
import { CleanupSessionsJob } from "../../jobs/cleanup-sessions";
|
||||||
import { applyConfigImportFromFile } from "./config-import";
|
|
||||||
import { repositoriesService } from "../repositories/repositories.service";
|
import { repositoriesService } from "../repositories/repositories.service";
|
||||||
import { notificationsService } from "../notifications/notifications.service";
|
import { notificationsService } from "../notifications/notifications.service";
|
||||||
import { VolumeAutoRemountJob } from "~/server/jobs/auto-remount";
|
import { VolumeAutoRemountJob } from "~/server/jobs/auto-remount";
|
||||||
|
|
@ -45,13 +44,6 @@ export const startup = async () => {
|
||||||
await Scheduler.start();
|
await Scheduler.start();
|
||||||
await Scheduler.clear();
|
await Scheduler.clear();
|
||||||
|
|
||||||
if (process.env.ZEROBYTE_CONFIG_IMPORT === "true") {
|
|
||||||
logger.info("Config import enabled (ZEROBYTE_CONFIG_IMPORT=true)");
|
|
||||||
await applyConfigImportFromFile();
|
|
||||||
} else {
|
|
||||||
logger.info("Config import skipped (set ZEROBYTE_CONFIG_IMPORT=true to enable)");
|
|
||||||
}
|
|
||||||
|
|
||||||
await restic.ensurePassfile().catch((err) => {
|
await restic.ensurePassfile().catch((err) => {
|
||||||
logger.error(`Error ensuring restic passfile exists: ${err.message}`);
|
logger.error(`Error ensuring restic passfile exists: ${err.message}`);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# Config file import (Infrastructure as Code)
|
# Config file import (Infrastructure as Code)
|
||||||
|
|
||||||
Zerobyte supports **config file import** on startup.
|
Zerobyte supports **config file import** via the CLI.
|
||||||
This lets you pre-configure volumes, repositories, backup schedules, notification destinations, and an initial user.
|
This lets you pre-configure volumes, repositories, backup schedules, notification destinations, and an initial user.
|
||||||
|
|
||||||
This example includes:
|
This example includes:
|
||||||
|
|
@ -13,7 +13,7 @@ This example includes:
|
||||||
|
|
||||||
- Docker + Docker Compose
|
- Docker + Docker Compose
|
||||||
|
|
||||||
This example includes `SYS_ADMIN` and `/dev/fuse` because it’s compatible with remote volume mounts (SMB/NFS/WebDAV).
|
This example includes `SYS_ADMIN` and `/dev/fuse` because it's compatible with remote volume mounts (SMB/NFS/WebDAV).
|
||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
|
|
@ -48,31 +48,19 @@ This is the recommended workflow for quick testing: if you don't have your own J
|
||||||
docker compose up -d
|
docker compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
6. Access the UI at `http://localhost:4096`.
|
6. Run the config import:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose exec zerobyte bun run cli import-config --config /app/zerobyte.config.json
|
||||||
|
```
|
||||||
|
|
||||||
|
7. Access the UI at `http://localhost:4096`.
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
### Import methods
|
### CLI import command
|
||||||
|
|
||||||
Zerobyte supports two ways to import configuration:
|
Import configuration using the CLI:
|
||||||
|
|
||||||
#### Method 1: Environment variable (automatic on startup)
|
|
||||||
|
|
||||||
Set `ZEROBYTE_CONFIG_IMPORT=true` and the import runs automatically when the container starts:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
services:
|
|
||||||
zerobyte:
|
|
||||||
environment:
|
|
||||||
- ZEROBYTE_CONFIG_IMPORT=true
|
|
||||||
- ZEROBYTE_CONFIG_PATH=/app/zerobyte.config.json # optional, this is the default
|
|
||||||
```
|
|
||||||
|
|
||||||
This is ideal for automated deployments where you want `docker compose up` to fully configure the instance.
|
|
||||||
|
|
||||||
#### Method 2: CLI command (manual control)
|
|
||||||
|
|
||||||
Run the import explicitly using the CLI:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Import from a mounted config file (starts a new temporary container)
|
# Import from a mounted config file (starts a new temporary container)
|
||||||
|
|
@ -89,15 +77,23 @@ Get-Content zerobyte.config.json | docker compose exec -T zerobyte bun run cli i
|
||||||
|
|
||||||
# Validate config without importing (dry run)
|
# Validate config without importing (dry run)
|
||||||
docker compose run --rm zerobyte bun run cli import-config --config /app/zerobyte.config.json --dry-run
|
docker compose run --rm zerobyte bun run cli import-config --config /app/zerobyte.config.json --dry-run
|
||||||
|
|
||||||
|
# Get JSON output for scripting
|
||||||
|
docker compose exec zerobyte bun run cli import-config --config /app/zerobyte.config.json --json
|
||||||
```
|
```
|
||||||
|
|
||||||
The `--stdin` option is useful when you don't want to mount the config file - just pipe it directly.
|
The `--stdin` option is useful when you don't want to mount the config file - just pipe it directly.
|
||||||
|
|
||||||
This is useful when you want to:
|
### CLI options
|
||||||
- See import output directly in your terminal
|
|
||||||
- Re-run import after fixing issues
|
| Option | Description |
|
||||||
- Test config files before applying them
|
|--------|-------------|
|
||||||
- Import without modifying your docker-compose.yml
|
| `--config <path>` | Path to the configuration file inside the container |
|
||||||
|
| `--stdin` | Read configuration from stdin |
|
||||||
|
| `--dry-run` | Validate the config without importing |
|
||||||
|
| `--json` | Output results in JSON format |
|
||||||
|
| `--log-level <level>` | Set log level (debug, info, warn, error) |
|
||||||
|
| `--overwrite-recovery-key` | Overwrite existing recovery key (only allowed if database is empty) |
|
||||||
|
|
||||||
### Secrets via env vars
|
### Secrets via env vars
|
||||||
|
|
||||||
|
|
@ -170,7 +166,7 @@ Supported formats:
|
||||||
- `env://VAR_NAME` → reads `process.env.VAR_NAME` at runtime
|
- `env://VAR_NAME` → reads `process.env.VAR_NAME` at runtime
|
||||||
- `file://secret_name` → reads `/run/secrets/secret_name` (Docker secrets)
|
- `file://secret_name` → reads `/run/secrets/secret_name` (Docker secrets)
|
||||||
|
|
||||||
This is useful when you want to keep secrets out of the database and rotate them without editing Zerobyte’s stored config.
|
This is useful when you want to keep secrets out of the database and rotate them without editing Zerobyte's stored config.
|
||||||
|
|
||||||
See the runnable example:
|
See the runnable example:
|
||||||
|
|
||||||
|
|
@ -178,14 +174,14 @@ See the runnable example:
|
||||||
|
|
||||||
### Config file behavior (create-only)
|
### Config file behavior (create-only)
|
||||||
|
|
||||||
The config file is applied on startup using a **create-only** approach:
|
The config file is applied using a **create-only** approach:
|
||||||
|
|
||||||
- **Volumes, notifications, schedules**: Skipped if a resource with the same name already exists
|
- **Volumes, notifications, schedules**: Skipped if a resource with the same name already exists
|
||||||
- **Repositories**: Skipped if any of these conditions are met:
|
- **Repositories**: Skipped if any of these conditions are met:
|
||||||
- A repository pointing to the same location (path/bucket/endpoint) is already registered
|
- A repository pointing to the same location (path/bucket/endpoint) is already registered
|
||||||
- For local repos: the path is already a restic repository (set `isExistingRepository: true` to import it)
|
- For local repos: the path is already a restic repository (set `isExistingRepository: true` to import it)
|
||||||
- A repository with the same name already exists
|
- A repository with the same name already exists
|
||||||
- Changes made via the UI are preserved across container restarts
|
- Changes made via the UI are preserved across imports
|
||||||
- To update a resource from config, either modify it via the UI or delete it first
|
- To update a resource from config, either modify it via the UI or delete it first
|
||||||
|
|
||||||
This makes the config file better suited as "initial setup" than as a "desired state sync".
|
This makes the config file better suited as "initial setup" than as a "desired state sync".
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,6 @@ services:
|
||||||
- .env
|
- .env
|
||||||
environment:
|
environment:
|
||||||
- TZ=${TZ:-UTC}
|
- TZ=${TZ:-UTC}
|
||||||
- ZEROBYTE_CONFIG_IMPORT=true
|
|
||||||
volumes:
|
volumes:
|
||||||
- /etc/localtime:/etc/localtime:ro
|
- /etc/localtime:/etc/localtime:ro
|
||||||
- /var/lib/zerobyte:/var/lib/zerobyte
|
- /var/lib/zerobyte:/var/lib/zerobyte
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue