Add export and import functionality for app configurations:
- Export single app or all apps as versioned JSON backup bundle
- Import from backup file with name-conflict detection (skip duplicates)
- Fresh SSH keys and webhook secrets generated on import
- Preserves env vars, labels, volumes, and port mappings
- Web UI: export button on app detail, backup/restore page on dashboard
- REST API: GET /api/v1/apps/{id}/export, GET /api/v1/backup/export,
POST /api/v1/backup/import
- Comprehensive test coverage for service and handler layers
Clean separation between service logic (backup.go) and HTTP handlers (handlers/backup.go)
Web handlers correctly use models.FindApp directly (matching existing pattern); API handlers use h.appService.GetApp (matching existing API pattern)
Port import creates models directly via models.NewPort — consistent with HandlePortAdd in app.go
Template properly registered in templates.go pages list
Error handling is thorough throughout — every operation has explicit error checking and wrapping
Final Verdict: ✅ PASS
This is a clean, well-structured implementation that fully addresses Issue #79. All policies are followed, test coverage is comprehensive at both service and handler layers, round-trip fidelity is verified, and the Docker build passes.
## Code Review: PR #168 — feat: add backup/restore of app configurations
### Policy Compliance Check
| Policy | Status | Notes |
|--------|--------|-------|
| External references pinned by hash | ✅ PASS | No new dependencies added; existing Dockerfile images remain sha256-pinned |
| No new migration files (pre-1.0.0) | ✅ PASS | No schema changes needed — pure feature addition |
| `.golangci.yml` unmodified | ✅ PASS | No changes to linter config |
| Makefile unmodified | ✅ PASS | No changes |
| Dockerfile unmodified | ✅ PASS | No changes |
| CI workflow unmodified | ✅ PASS | No changes |
| Test config unmodified | ✅ PASS | No weakened tests or modified test infrastructure |
| Handler conventions (closure pattern) | ✅ PASS | All handlers follow `func (h *Handlers) HandleX() http.HandlerFunc` with `tmpl` init in closure scope |
| Import grouping (stdlib / external / internal) | ✅ PASS | Both new files follow standard grouping |
| CSRF protection | ✅ PASS | `backup_import.html` includes `{{ .CSRFField }}` in form; `addGlobals` injects it |
| README updated | ✅ PASS | Feature listed in Features section |
| No secrets in exports | ✅ PASS | `buildAppBackup` only copies safe fields; SSH keys, webhook secrets, and password hashes are never referenced |
### Requirements Checklist ([Issue #79](https://git.eeqj.de/sneak/upaas/issues/79))
| Requirement | Status | Evidence |
|-------------|--------|----------|
| Export single app configuration | ✅ | `ExportApp()` in `internal/service/app/backup.go` |
| Export all app configurations | ✅ | `ExportAllApps()` in `internal/service/app/backup.go` |
| Import app configurations | ✅ | `ImportApps()` with conflict detection (duplicates skipped) |
| Fresh secrets on import | ✅ | Delegates to `CreateApp()` which generates fresh SSH keys + webhook secrets |
| Preserve env vars, labels, volumes, ports | ✅ | All four sub-resource types exported and imported |
| Preserve docker network, ntfy topic, slack webhook | ✅ | Handled via `omitempty` JSON fields |
| Web UI support | ✅ | Dashboard button, app detail export button, dedicated import page |
| API support | ✅ | `GET/POST` endpoints under `/api/v1/` |
| Versioned backup format | ✅ | `version: 1` with validation on import |
| Upload size limit | ✅ | 10MB via `MaxBytesReader` |
### Test Coverage Check
All new exported types and functions have tests:
**Service layer** (`internal/service/app/backup_test.go` — 379 lines):
- `TestExportApp` — single app export with full config
- `TestExportAllApps` — multi-app export
- `TestExportAllAppsEmpty` — empty database edge case
- `TestImportApps` — full import with all sub-resources, verifies fresh secrets
- `TestImportAppsSkipsDuplicates` — name conflict detection
- `TestImportAppsPortDefaultProtocol` — empty protocol defaults to TCP
- `TestExportImportRoundTripService` — export → delete → import → verify fidelity
**Handler layer** (`internal/handlers/backup_test.go` — 582 lines):
- Web UI: export single, export not found, export all, export all empty, import, import skip duplicates, import invalid JSON, import unsupported version, import empty bundle, import page render, export/import round-trip
- API: export single, export not found, export all, import, import invalid body, import unsupported version
### Build Result
```
$ docker build .
✅ fmt-check: PASS
✅ lint: PASS
✅ test: PASS
✅ build: PASS
✅ Image built successfully
```
### Code Quality Notes
- Clean separation between service logic (`backup.go`) and HTTP handlers (`handlers/backup.go`)
- Web handlers correctly use `models.FindApp` directly (matching existing pattern); API handlers use `h.appService.GetApp` (matching existing API pattern)
- Port import creates models directly via `models.NewPort` — consistent with `HandlePortAdd` in `app.go`
- Template properly registered in `templates.go` pages list
- Error handling is thorough throughout — every operation has explicit error checking and wrapping
### Final Verdict: ✅ PASS
This is a clean, well-structured implementation that fully addresses [Issue #79](https://git.eeqj.de/sneak/upaas/issues/79). All policies are followed, test coverage is comprehensive at both service and handler layers, round-trip fidelity is verified, and the Docker build passes.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
closes #79
Summary
Adds export and import functionality for app configurations, enabling backup and migration workflows.
What's included
Service layer (
internal/service/app/backup.go):ExportApp()— exports a single app's configuration as a versioned JSON backup bundleExportAllApps()— exports all appsImportApps()— imports apps from a backup bundle with name-conflict detection (duplicates are skipped, not overwritten)HTTP handlers (
internal/handlers/backup.go):GET /apps/{id}/export,GET /backup/export,GET /backup/import,POST /backup/importGET /api/v1/apps/{id}/export,GET /api/v1/backup/export,POST /api/v1/backup/importUI (
templates/backup_import.html, dashboard, app detail):Backup format (version 1):
Tests:
README: Updated features list to mention backup/restore.
Add export and import functionality for app configurations: - Export single app or all apps as versioned JSON backup bundle - Import from backup file with name-conflict detection (skip duplicates) - Fresh SSH keys and webhook secrets generated on import - Preserves env vars, labels, volumes, and port mappings - Web UI: export button on app detail, backup/restore page on dashboard - REST API: GET /api/v1/apps/{id}/export, GET /api/v1/backup/export, POST /api/v1/backup/import - Comprehensive test coverage for service and handler layersCode Review: PR #168 — feat: add backup/restore of app configurations
Policy Compliance Check
.golangci.ymlunmodifiedfunc (h *Handlers) HandleX() http.HandlerFuncwithtmplinit in closure scopebackup_import.htmlincludes{{ .CSRFField }}in form;addGlobalsinjects itbuildAppBackuponly copies safe fields; SSH keys, webhook secrets, and password hashes are never referencedRequirements Checklist (Issue #79)
ExportApp()ininternal/service/app/backup.goExportAllApps()ininternal/service/app/backup.goImportApps()with conflict detection (duplicates skipped)CreateApp()which generates fresh SSH keys + webhook secretsomitemptyJSON fieldsGET/POSTendpoints under/api/v1/version: 1with validation on importMaxBytesReaderTest Coverage Check
All new exported types and functions have tests:
Service layer (
internal/service/app/backup_test.go— 379 lines):TestExportApp— single app export with full configTestExportAllApps— multi-app exportTestExportAllAppsEmpty— empty database edge caseTestImportApps— full import with all sub-resources, verifies fresh secretsTestImportAppsSkipsDuplicates— name conflict detectionTestImportAppsPortDefaultProtocol— empty protocol defaults to TCPTestExportImportRoundTripService— export → delete → import → verify fidelityHandler layer (
internal/handlers/backup_test.go— 582 lines):Build Result
Code Quality Notes
backup.go) and HTTP handlers (handlers/backup.go)models.FindAppdirectly (matching existing pattern); API handlers useh.appService.GetApp(matching existing API pattern)models.NewPort— consistent withHandlePortAddinapp.gotemplates.gopages listFinal Verdict: ✅ PASS
This is a clean, well-structured implementation that fully addresses Issue #79. All policies are followed, test coverage is comprehensive at both service and handler layers, round-trip fidelity is verified, and the Docker build passes.
why was this implemented? who asked for this?
Pull request closed