initial commit
This commit is contained in:
commit
5c01fbdd5f
37 files changed
+6082
No files matched your search
@@ -0,0 +1,69 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## What this is
|
||||
|
||||
NodeMaster is a beego v2 REST API (Go module `nodemaster`) for managing a single Docker/Podman host ("the node"), a registry of remote NodeMaster nodes, and the compose-based services running on the local node — including a scheduled/on-demand backup system for both the node and individual services.
|
||||
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
go build ./... # build
|
||||
go vet ./... # vet
|
||||
gofmt -l . # list files needing formatting (gofmt -w to fix)
|
||||
go test ./... # run all tests
|
||||
go test ./tests/ -run TestGetNode # run a single test
|
||||
go run . scan [folder] [--json] # CLI: scan compose folders, don't start the HTTP server
|
||||
go run . info # CLI: print node summary, don't start the HTTP server
|
||||
go run . # start the HTTP API (beego.Run(), port from conf/app.conf)
|
||||
bee run -gendoc=true -downdoc=true # dev server: rebuild-on-change + regenerate swagger/swagger.json on every save
|
||||
bee generate routers # regenerate routers/commentsRouter.go from @router annotations (run after adding/changing any controller method)
|
||||
```
|
||||
|
||||
Swagger UI is served at `/swagger` when `runmode = dev` (see `conf/app.conf`). The spec files `swagger/swagger.json` / `swagger/swagger.yml` and `routers/commentsRouter.go` are all **generated** — never hand-edit them. Run `bee run -gendoc=true -downdoc=true` for day-to-day dev (it rebuilds and regenerates the swagger spec on every file change, and downloads the swagger UI static assets via `-downdoc` if `swagger/index.html` is missing); after adding, removing, or changing any controller method's `@router` annotation, also run `bee generate routers` to regenerate `routers/commentsRouter.go` (see routing note below). Two pre-existing `@Success`/`@Param` annotations reference types `bee generate docs` can't resolve (a package-local unexported struct `nodeServices` in `controllers/nodes.go`, and the pseudo-type `object` in `controllers/node.go`'s scan endpoint) — this prints a harmless `Cannot find the object: ...` warning and generates a placeholder empty-object schema for those two entries; it does not affect generation success or the routes themselves.
|
||||
|
||||
There is no README and no lint config beyond `go vet`/`gofmt`.
|
||||
|
||||
## Architecture
|
||||
|
||||
**Config persistence (`models/store.go`):** the entire application state — local node info, remote node registry, and services — is one `AppConfig` struct, serialized as JSON to `/etc/nodemaster/nodemaster.conf` (falls back to `~/.nodemaster.conf` if that path isn't writable). A single package-level `sync.RWMutex` (`mu`) guards the in-memory `cfg *AppConfig`. Every mutating function calls `persist()` to write the JSON back out. Because the config can hold a restic repository password (see below), `persist()` writes/chmods the file `0600`.
|
||||
|
||||
**API namespaces (all under `/v1`, wired in `routers/router.go`):**
|
||||
- `/node` — the local host: info, update history, backup config/targets/run, compose scan
|
||||
- `/nodes` — remote node registry, plus `GET /nodes/aggregated` which calls each registered node's own `/v1/services` over HTTP and merges the results
|
||||
- `/services` — compose-based services on the local node: CRUD, start/stop, backup config/targets/run
|
||||
|
||||
**Critical routing gotcha:** beego's `NSInclude` resolves controller routes from `beego.GlobalControllerRouter`, which is populated by `routers/commentsRouter.go` — a **generated** file, rebuilt by `bee generate routers` (with no flags: this project's `ctrlDir`/`routersFile` match bee v2's defaults, `controllers` and `routers/commentsRouter.go`). If you add, remove, or change a controller method's `@router` annotation, you must run `bee generate routers` afterwards, or the route will silently 404 (it won't error at startup — it just won't be registered). Never hand-edit `commentsRouter.go`; a plain rerun of `bee generate routers` overwrites it in place from the current `@router` comments. (Historically this file was hand-maintained under the name `commentsRouter_controllers.go` after `bee generate routers` seemed unreliable — the actual issue was just that the default bee output path is `routers/commentsRouter.go`, not `commentsRouter_controllers.go`; running the generator with that stale filename created a second, duplicate router file instead of updating the existing one. Regenerating at the correct default path resolved it.)
|
||||
|
||||
**Backup execution (`models/backup.go`):** three methods — `MethodRsync`, `MethodRestic`, `MethodExternal` (no `rclone`; it was removed in favor of restic). `BackupTarget.Remote` is overloaded by method: an rsync destination (`user@host:/path`) or a restic repository string (`/mnt/backup/repo`, `s3:...`, `sftp:...`, etc.); `BackupTarget.Params` becomes extra `--key=value` CLI flags for whichever tool runs. `MethodExternal` is a no-op (the operator manages that backup outside the API). Restic's repository password is **node-global, not per-target**: `NodeInfo.ResticPassword`, read via `models.GetResticPassword()`, set through `PUT /v1/node`'s `restic_password` field (never echoed back — `GET /v1/node` only exposes `restic_password_set: bool`). It's passed to the `restic` binary via `RESTIC_PASSWORD`/`RESTIC_REPOSITORY` env vars, never argv. `runTarget` calls `ensureResticRepo()` (runs `restic init`, tolerates "already initialized") before every restic backup rather than tracking init state separately.
|
||||
|
||||
**Backup targets are individually CRUD-able**, not just a whole-`BackupConfig` replace: `/v1/node/backup/targets[...]` and `/v1/services/:id/backup/targets[...]`, each with `:tid` sub-routes for get/put/delete/run/progress, plus a `.../backup/progress` aggregate view. `BackupTarget.Schedule` is a 5-field standard cron expression (empty = manual-only). Validated centrally in `validateBackupTarget` (`models/backup.go`) — this is the single place that knows the set of valid `BackupMethod` values and schedule syntax.
|
||||
|
||||
**Scheduling (`models/scheduler.go`):** an in-process `robfig/cron/v3` scheduler. `StartScheduler()` is called once from `main.go`, only on the normal HTTP-server path (not on the `scan`/`info` CLI paths). `SyncSchedule()` fully tears down and rebuilds every cron entry from the current target definitions in `cfg`, and must be called after *any* mutation that can change `Targets` — not just the per-target CRUD endpoints, but also whole-object replace paths (`UpdateNodeBackupConfig`, `AddService`, `UpdateService`, `DeleteService`), since those can silently add/remove/alter schedules too. Any new way of mutating backup targets needs a `SyncSchedule()` call after `persist()`.
|
||||
|
||||
**Progress tracking (`models/progress.go`):** an in-memory-only `RunState` map (its own mutex, deliberately kept out of `cfg`/`persist()` — a crash/restart must never leave a stale "running" flag on disk). Populated by `models/backup.go`'s `progressWriter`, wired as both `cmd.Stdout` and `cmd.Stderr`: rsync via `--info=progress2` (regex on the `NN%` token), restic via `backup --json` (parses `message_type` lines: `status` for percent/ETA, `summary` for the final snapshot id, `error` for failures). Parsing is best-effort — an unparsed line just leaves the last known percent/message in place rather than failing the backup.
|
||||
|
||||
**Shared run helpers:** `models.RunNodeBackup(targetIDs []string)` and `models.RunServiceBackup(id string, targetIDs []string)` are the single code path used by manual-trigger controllers, the scheduler, and the "run all targets" endpoints. Don't duplicate stop/start/backup/persist logic in a controller — route new triggers through these.
|
||||
|
||||
**Compose support:** `docker compose` (default), `docker-compose`, `podman-compose`, selected per-service via `Service.ComposeType` (`models/backup.go`'s `composeCmd`). Services can be configured to stop before backup (`StopOnBackup=true`) and restart after; all backup runs are asynchronous (goroutine) and append a `BackupExecution` record to history.
|
||||
|
||||
**Compose scan (`models/scan.go`, `POST /v1/node/scan` or `nodemaster scan [folder] [--json]`):**
|
||||
- Scans `/opt/compose`, `/root/compose`, `/home/*/compose` by default, plus an optional extra folder
|
||||
- Parses compose YAML to deduce a backup folder (prefers `/opt/data/containers/<name>`, falls back to the common path prefix across volume mounts)
|
||||
- Probes live status via `docker ps --filter label=com.docker.compose.project=<name>`
|
||||
- Falls back to a text-based parser for compose files with duplicate YAML keys (which `gopkg.in/yaml.v3` rejects)
|
||||
|
||||
## Key files
|
||||
|
||||
- `models/types.go` — every persisted struct (`AppConfig`, `NodeInfo`, `RemoteNode`, `Service`, `BackupConfig`, `BackupTarget`, `BackupExecution`, `ScanResult`, etc.)
|
||||
- `models/store.go` — the persistence layer (`cfg`, `mu`, `persist()`, config path resolution)
|
||||
- `models/backup.go` — `RunBackup`/`runTarget`, `progressWriter`, `validateBackupTarget`, `StartService`/`StopService`/`composeCmd`
|
||||
- `models/progress.go` — in-memory `RunState` tracking (not persisted)
|
||||
- `models/scheduler.go` — cron wiring: `StartScheduler`/`SyncSchedule`
|
||||
- `models/scan.go` — `ScanAll`, compose YAML parsing, backup-folder deduction
|
||||
- `models/node.go` / `models/nodes.go` / `models/services.go` — CRUD for the local node, remote node registry, and services respectively
|
||||
- `controllers/*.go` — beego controllers with `@router` annotations (source of truth for API surface)
|
||||
- `routers/commentsRouter.go` — generated beego route registration, rebuilt via `bee generate routers` (see routing gotcha above) — never hand-edit
|
||||
- `routers/router.go` — namespace wiring + dev-mode CORS filter
|
||||
- `main.go` — entrypoint; also implements the `scan`/`info` CLI subcommands ahead of the normal `beego.Run()` path
|
||||
@@ -0,0 +1,10 @@
|
||||
appname = nodemaster
|
||||
httpport = 8080
|
||||
runmode = dev
|
||||
autorender = false
|
||||
copyrequestbody = true
|
||||
EnableDocs = true
|
||||
sqlconn =
|
||||
; comma-separated list of origins allowed to call this API from a browser
|
||||
; (dev mode only for now). Supports "*" wildcards, e.g. "http://localhost:*".
|
||||
corsalloworigins = http://localhost:*
|
||||
@@ -0,0 +1,352 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"nodemaster/models"
|
||||
|
||||
beego "github.com/beego/beego/v2/server/web"
|
||||
)
|
||||
|
||||
// NodeController manages the current host node.
|
||||
type NodeController struct {
|
||||
beego.Controller
|
||||
}
|
||||
|
||||
// @Title GetNode
|
||||
// @Description get current node info including runtime config_file path
|
||||
// @Success 200 {object} models.NodeInfo
|
||||
// @router / [get]
|
||||
func (c *NodeController) Get() {
|
||||
n := models.GetLocalNode()
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"hostname": n.Hostname,
|
||||
"description": n.Description,
|
||||
"update_history": n.UpdateHistory,
|
||||
"backup": n.Backup,
|
||||
"config_file": models.ConfigPath(),
|
||||
"restic_password_set": n.ResticPassword != "",
|
||||
}
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// @Title UpdateNode
|
||||
// @Description update current node hostname, description, or restic_password (the repository password used to encrypt/decrypt all restic backup targets on this node)
|
||||
// @Param body body models.NodeInfo true "fields to update"
|
||||
// @Success 200 {string} update success
|
||||
// @Failure 400 invalid body
|
||||
// @router / [put]
|
||||
func (c *NodeController) Put() {
|
||||
var n models.NodeInfo
|
||||
if err := json.Unmarshal(c.Ctx.Input.RequestBody, &n); err != nil {
|
||||
c.Ctx.Output.SetStatus(400)
|
||||
c.Data["json"] = map[string]string{"error": err.Error()}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
if err := models.UpdateLocalNode(n); err != nil {
|
||||
c.Ctx.Output.SetStatus(500)
|
||||
c.Data["json"] = map[string]string{"error": err.Error()}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
c.Data["json"] = map[string]string{"message": "update success"}
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// @Title GetUpdates
|
||||
// @Description list update history
|
||||
// @Success 200 {object} []models.UpdateRecord
|
||||
// @router /updates [get]
|
||||
func (c *NodeController) GetUpdates() {
|
||||
c.Data["json"] = models.GetUpdateHistory()
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// @Title GetUpdate
|
||||
// @Description get a single update record
|
||||
// @Param id path string true "update record id"
|
||||
// @Success 200 {object} models.UpdateRecord
|
||||
// @Failure 404 not found
|
||||
// @router /updates/:id [get]
|
||||
func (c *NodeController) GetUpdate() {
|
||||
id := c.Ctx.Input.Param(":id")
|
||||
r, err := models.GetUpdateRecord(id)
|
||||
if err != nil {
|
||||
c.Ctx.Output.SetStatus(404)
|
||||
c.Data["json"] = map[string]string{"error": err.Error()}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
c.Data["json"] = r
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// @Title AddUpdate
|
||||
// @Description record a new update entry
|
||||
// @Param body body models.UpdateRecord true "update record"
|
||||
// @Success 201 {object} models.UpdateRecord
|
||||
// @Failure 400 invalid body
|
||||
// @router /updates [post]
|
||||
func (c *NodeController) AddUpdate() {
|
||||
var r models.UpdateRecord
|
||||
if err := json.Unmarshal(c.Ctx.Input.RequestBody, &r); err != nil {
|
||||
c.Ctx.Output.SetStatus(400)
|
||||
c.Data["json"] = map[string]string{"error": err.Error()}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
record, err := models.AddUpdateRecord(r)
|
||||
if err != nil {
|
||||
c.Ctx.Output.SetStatus(500)
|
||||
c.Data["json"] = map[string]string{"error": err.Error()}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
c.Ctx.Output.SetStatus(201)
|
||||
c.Data["json"] = record
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// @Title DeleteUpdate
|
||||
// @Description delete an update record
|
||||
// @Param id path string true "update record id"
|
||||
// @Success 200 {string} delete success
|
||||
// @Failure 404 not found
|
||||
// @router /updates/:id [delete]
|
||||
func (c *NodeController) DeleteUpdate() {
|
||||
id := c.Ctx.Input.Param(":id")
|
||||
if err := models.DeleteUpdateRecord(id); err != nil {
|
||||
c.Ctx.Output.SetStatus(404)
|
||||
c.Data["json"] = map[string]string{"error": err.Error()}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
c.Data["json"] = map[string]string{"message": "delete success"}
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// @Title GetBackup
|
||||
// @Description get node backup configuration
|
||||
// @Success 200 {object} models.BackupConfig
|
||||
// @router /backup [get]
|
||||
func (c *NodeController) GetBackup() {
|
||||
c.Data["json"] = models.GetNodeBackupConfig()
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// @Title UpdateBackup
|
||||
// @Description update node backup configuration
|
||||
// @Param body body models.BackupConfig true "backup config"
|
||||
// @Success 200 {string} update success
|
||||
// @Failure 400 invalid body
|
||||
// @router /backup [put]
|
||||
func (c *NodeController) PutBackup() {
|
||||
var bc models.BackupConfig
|
||||
if err := json.Unmarshal(c.Ctx.Input.RequestBody, &bc); err != nil {
|
||||
c.Ctx.Output.SetStatus(400)
|
||||
c.Data["json"] = map[string]string{"error": err.Error()}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
if err := models.UpdateNodeBackupConfig(bc); err != nil {
|
||||
c.Ctx.Output.SetStatus(500)
|
||||
c.Data["json"] = map[string]string{"error": err.Error()}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
c.Data["json"] = map[string]string{"message": "update success"}
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// @Title Scan
|
||||
// @Description scan compose folders and index discovered services; accepts optional {"folder":"/path"} body
|
||||
// @Param body body object false "optional folder override"
|
||||
// @Success 200 {object} models.ScanResult
|
||||
// @router /scan [post]
|
||||
func (c *NodeController) Scan() {
|
||||
var req struct {
|
||||
Folder string `json:"folder"`
|
||||
}
|
||||
_ = json.Unmarshal(c.Ctx.Input.RequestBody, &req)
|
||||
result := models.ScanAll(req.Folder)
|
||||
c.Data["json"] = result
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// @Title RunBackup
|
||||
// @Description trigger an immediate node backup across all targets (asynchronous)
|
||||
// @Success 202 {string} backup started
|
||||
// @Failure 400 no targets configured
|
||||
// @router /backup/run [post]
|
||||
func (c *NodeController) RunBackup() {
|
||||
bc := models.GetNodeBackupConfig()
|
||||
if len(bc.Targets) == 0 {
|
||||
c.Ctx.Output.SetStatus(400)
|
||||
c.Data["json"] = map[string]string{"error": "no backup targets configured"}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
c.Ctx.Output.SetStatus(202)
|
||||
c.Data["json"] = map[string]string{"message": "backup started"}
|
||||
c.ServeJSON()
|
||||
go func() {
|
||||
_ = models.RunNodeBackup(nil)
|
||||
}()
|
||||
}
|
||||
|
||||
// @Title GetBackupTargets
|
||||
// @Description list node backup target definitions
|
||||
// @Success 200 {object} []models.BackupTarget
|
||||
// @router /backup/targets [get]
|
||||
func (c *NodeController) GetBackupTargets() {
|
||||
c.Data["json"] = models.GetNodeBackupTargets()
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// @Title AddBackupTarget
|
||||
// @Description define a new node backup target
|
||||
// @Param body body models.BackupTarget true "backup target definition"
|
||||
// @Success 201 {object} models.BackupTarget
|
||||
// @Failure 400 invalid body
|
||||
// @router /backup/targets [post]
|
||||
func (c *NodeController) AddBackupTarget() {
|
||||
var t models.BackupTarget
|
||||
if err := json.Unmarshal(c.Ctx.Input.RequestBody, &t); err != nil {
|
||||
c.Ctx.Output.SetStatus(400)
|
||||
c.Data["json"] = map[string]string{"error": err.Error()}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
created, err := models.AddNodeBackupTarget(t)
|
||||
if err != nil {
|
||||
c.Ctx.Output.SetStatus(400)
|
||||
c.Data["json"] = map[string]string{"error": err.Error()}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
c.Ctx.Output.SetStatus(201)
|
||||
c.Data["json"] = created
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// @Title GetBackupTarget
|
||||
// @Description get a node backup target by id
|
||||
// @Param tid path string true "backup target id"
|
||||
// @Success 200 {object} models.BackupTarget
|
||||
// @Failure 404 not found
|
||||
// @router /backup/targets/:tid [get]
|
||||
func (c *NodeController) GetBackupTarget() {
|
||||
tid := c.Ctx.Input.Param(":tid")
|
||||
t, err := models.GetNodeBackupTarget(tid)
|
||||
if err != nil {
|
||||
c.Ctx.Output.SetStatus(404)
|
||||
c.Data["json"] = map[string]string{"error": err.Error()}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
c.Data["json"] = t
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// @Title PutBackupTarget
|
||||
// @Description update a node backup target definition
|
||||
// @Param tid path string true "backup target id"
|
||||
// @Param body body models.BackupTarget true "updated backup target"
|
||||
// @Success 200 {object} models.BackupTarget
|
||||
// @Failure 400 invalid body
|
||||
// @Failure 404 not found
|
||||
// @router /backup/targets/:tid [put]
|
||||
func (c *NodeController) PutBackupTarget() {
|
||||
tid := c.Ctx.Input.Param(":tid")
|
||||
var t models.BackupTarget
|
||||
if err := json.Unmarshal(c.Ctx.Input.RequestBody, &t); err != nil {
|
||||
c.Ctx.Output.SetStatus(400)
|
||||
c.Data["json"] = map[string]string{"error": err.Error()}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
updated, err := models.UpdateNodeBackupTarget(tid, t)
|
||||
if err != nil {
|
||||
status := 404
|
||||
if err.Error() != "backup target not found" {
|
||||
status = 400
|
||||
}
|
||||
c.Ctx.Output.SetStatus(status)
|
||||
c.Data["json"] = map[string]string{"error": err.Error()}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
c.Data["json"] = updated
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// @Title DeleteBackupTarget
|
||||
// @Description delete a node backup target definition
|
||||
// @Param tid path string true "backup target id"
|
||||
// @Success 200 {string} delete success
|
||||
// @Failure 404 not found
|
||||
// @router /backup/targets/:tid [delete]
|
||||
func (c *NodeController) DeleteBackupTarget() {
|
||||
tid := c.Ctx.Input.Param(":tid")
|
||||
if err := models.DeleteNodeBackupTarget(tid); err != nil {
|
||||
c.Ctx.Output.SetStatus(404)
|
||||
c.Data["json"] = map[string]string{"error": err.Error()}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
c.Data["json"] = map[string]string{"message": "delete success"}
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// @Title RunBackupTarget
|
||||
// @Description trigger an immediate backup for a single node target (asynchronous)
|
||||
// @Param tid path string true "backup target id"
|
||||
// @Success 202 {string} backup started
|
||||
// @Failure 404 not found
|
||||
// @router /backup/targets/:tid/run [post]
|
||||
func (c *NodeController) RunBackupTarget() {
|
||||
tid := c.Ctx.Input.Param(":tid")
|
||||
if _, err := models.GetNodeBackupTarget(tid); err != nil {
|
||||
c.Ctx.Output.SetStatus(404)
|
||||
c.Data["json"] = map[string]string{"error": err.Error()}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
c.Ctx.Output.SetStatus(202)
|
||||
c.Data["json"] = map[string]string{"message": "backup started"}
|
||||
c.ServeJSON()
|
||||
go func() {
|
||||
_ = models.RunNodeBackup([]string{tid})
|
||||
}()
|
||||
}
|
||||
|
||||
// @Title GetBackupTargetProgress
|
||||
// @Description get the current/last run state for a node backup target
|
||||
// @Param tid path string true "backup target id"
|
||||
// @Success 200 {object} models.RunState
|
||||
// @Failure 404 not found
|
||||
// @router /backup/targets/:tid/progress [get]
|
||||
func (c *NodeController) GetBackupTargetProgress() {
|
||||
tid := c.Ctx.Input.Param(":tid")
|
||||
if _, err := models.GetNodeBackupTarget(tid); err != nil {
|
||||
c.Ctx.Output.SetStatus(404)
|
||||
c.Data["json"] = map[string]string{"error": err.Error()}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
state, ok := models.GetProgress("node", tid)
|
||||
if !ok {
|
||||
state = models.RunState{TargetID: tid}
|
||||
}
|
||||
c.Data["json"] = state
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// @Title GetBackupProgress
|
||||
// @Description get the current/last run state for every node backup target
|
||||
// @Success 200 {object} []models.RunState
|
||||
// @router /backup/progress [get]
|
||||
func (c *NodeController) GetBackupProgress() {
|
||||
c.Data["json"] = models.GetProgressForScope("node")
|
||||
c.ServeJSON()
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"nodemaster/models"
|
||||
"time"
|
||||
|
||||
beego "github.com/beego/beego/v2/server/web"
|
||||
)
|
||||
|
||||
// NodesController manages the registry of remote nodes.
|
||||
type NodesController struct {
|
||||
beego.Controller
|
||||
}
|
||||
|
||||
// @Title GetAllNodes
|
||||
// @Description list all registered remote nodes
|
||||
// @Success 200 {object} []models.RemoteNode
|
||||
// @router / [get]
|
||||
func (c *NodesController) GetAll() {
|
||||
c.Data["json"] = models.GetAllRemoteNodes()
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// @Title AddNode
|
||||
// @Description register a remote node
|
||||
// @Param body body models.RemoteNode true "node to register"
|
||||
// @Success 201 {object} models.RemoteNode
|
||||
// @Failure 400 invalid body
|
||||
// @router / [post]
|
||||
func (c *NodesController) Post() {
|
||||
var n models.RemoteNode
|
||||
if err := json.Unmarshal(c.Ctx.Input.RequestBody, &n); err != nil {
|
||||
c.Ctx.Output.SetStatus(400)
|
||||
c.Data["json"] = map[string]string{"error": err.Error()}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
created, err := models.AddRemoteNode(n)
|
||||
if err != nil {
|
||||
c.Ctx.Output.SetStatus(500)
|
||||
c.Data["json"] = map[string]string{"error": err.Error()}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
c.Ctx.Output.SetStatus(201)
|
||||
c.Data["json"] = created
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// @Title GetNode
|
||||
// @Description get a remote node by id
|
||||
// @Param id path string true "node id"
|
||||
// @Success 200 {object} models.RemoteNode
|
||||
// @Failure 404 not found
|
||||
// @router /:id [get]
|
||||
func (c *NodesController) Get() {
|
||||
id := c.Ctx.Input.Param(":id")
|
||||
n, err := models.GetRemoteNode(id)
|
||||
if err != nil {
|
||||
c.Ctx.Output.SetStatus(404)
|
||||
c.Data["json"] = map[string]string{"error": err.Error()}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
c.Data["json"] = n
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// @Title UpdateNode
|
||||
// @Description update a remote node
|
||||
// @Param id path string true "node id"
|
||||
// @Param body body models.RemoteNode true "updated node data"
|
||||
// @Success 200 {object} models.RemoteNode
|
||||
// @Failure 400 invalid body
|
||||
// @Failure 404 not found
|
||||
// @router /:id [put]
|
||||
func (c *NodesController) Put() {
|
||||
id := c.Ctx.Input.Param(":id")
|
||||
var n models.RemoteNode
|
||||
if err := json.Unmarshal(c.Ctx.Input.RequestBody, &n); err != nil {
|
||||
c.Ctx.Output.SetStatus(400)
|
||||
c.Data["json"] = map[string]string{"error": err.Error()}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
updated, err := models.UpdateRemoteNode(id, n)
|
||||
if err != nil {
|
||||
c.Ctx.Output.SetStatus(404)
|
||||
c.Data["json"] = map[string]string{"error": err.Error()}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
c.Data["json"] = updated
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// @Title DeleteNode
|
||||
// @Description delete a remote node
|
||||
// @Param id path string true "node id"
|
||||
// @Success 200 {string} delete success
|
||||
// @Failure 404 not found
|
||||
// @router /:id [delete]
|
||||
func (c *NodesController) Delete() {
|
||||
id := c.Ctx.Input.Param(":id")
|
||||
if err := models.DeleteRemoteNode(id); err != nil {
|
||||
c.Ctx.Output.SetStatus(404)
|
||||
c.Data["json"] = map[string]string{"error": err.Error()}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
c.Data["json"] = map[string]string{"message": "delete success"}
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
type nodeServices struct {
|
||||
Node models.RemoteNode `json:"node"`
|
||||
Services []models.Service `json:"services"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// @Title GetAggregated
|
||||
// @Description query all registered nodes for their services and return an aggregated view
|
||||
// @Success 200 {object} []nodeServices
|
||||
// @router /aggregated [get]
|
||||
func (c *NodesController) GetAggregated() {
|
||||
nodes := models.GetAllRemoteNodes()
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
results := make([]nodeServices, 0, len(nodes))
|
||||
|
||||
for _, node := range nodes {
|
||||
entry := nodeServices{Node: node}
|
||||
resp, err := client.Get(node.URL + "/v1/services")
|
||||
if err != nil {
|
||||
entry.Error = err.Error()
|
||||
results = append(results, entry)
|
||||
continue
|
||||
}
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
resp.Body.Close()
|
||||
if err != nil {
|
||||
entry.Error = err.Error()
|
||||
results = append(results, entry)
|
||||
continue
|
||||
}
|
||||
var svcs []models.Service
|
||||
if err := json.Unmarshal(body, &svcs); err != nil {
|
||||
entry.Error = "failed to parse services: " + err.Error()
|
||||
} else {
|
||||
entry.Services = svcs
|
||||
}
|
||||
results = append(results, entry)
|
||||
}
|
||||
|
||||
c.Data["json"] = results
|
||||
c.ServeJSON()
|
||||
}
|
||||
@@ -0,0 +1,397 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"nodemaster/models"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
beego "github.com/beego/beego/v2/server/web"
|
||||
)
|
||||
|
||||
// ServicesController manages services on this host.
|
||||
type ServicesController struct {
|
||||
beego.Controller
|
||||
}
|
||||
|
||||
// @Title GetAllServices
|
||||
// @Description list all services
|
||||
// @Success 200 {object} []models.Service
|
||||
// @router / [get]
|
||||
func (c *ServicesController) GetAll() {
|
||||
c.Data["json"] = models.GetAllServices()
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// @Title AddService
|
||||
// @Description register a new service
|
||||
// @Param body body models.Service true "service definition"
|
||||
// @Success 201 {object} models.Service
|
||||
// @Failure 400 invalid body
|
||||
// @router / [post]
|
||||
func (c *ServicesController) Post() {
|
||||
var s models.Service
|
||||
if err := json.Unmarshal(c.Ctx.Input.RequestBody, &s); err != nil {
|
||||
c.Ctx.Output.SetStatus(400)
|
||||
c.Data["json"] = map[string]string{"error": err.Error()}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
created, err := models.AddService(s)
|
||||
if err != nil {
|
||||
c.Ctx.Output.SetStatus(500)
|
||||
c.Data["json"] = map[string]string{"error": err.Error()}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
c.Ctx.Output.SetStatus(201)
|
||||
c.Data["json"] = created
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// @Title GetService
|
||||
// @Description get a service by id
|
||||
// @Param id path string true "service id"
|
||||
// @Success 200 {object} models.Service
|
||||
// @Failure 404 not found
|
||||
// @router /:id [get]
|
||||
func (c *ServicesController) Get() {
|
||||
id := c.Ctx.Input.Param(":id")
|
||||
s, err := models.GetService(id)
|
||||
if err != nil {
|
||||
c.Ctx.Output.SetStatus(404)
|
||||
c.Data["json"] = map[string]string{"error": err.Error()}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
c.Data["json"] = s
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// @Title UpdateService
|
||||
// @Description update a service definition
|
||||
// @Param id path string true "service id"
|
||||
// @Param body body models.Service true "updated service data"
|
||||
// @Success 200 {object} models.Service
|
||||
// @Failure 400 invalid body
|
||||
// @Failure 404 not found
|
||||
// @router /:id [put]
|
||||
func (c *ServicesController) Put() {
|
||||
id := c.Ctx.Input.Param(":id")
|
||||
var s models.Service
|
||||
if err := json.Unmarshal(c.Ctx.Input.RequestBody, &s); err != nil {
|
||||
c.Ctx.Output.SetStatus(400)
|
||||
c.Data["json"] = map[string]string{"error": err.Error()}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
updated, err := models.UpdateService(id, s)
|
||||
if err != nil {
|
||||
c.Ctx.Output.SetStatus(404)
|
||||
c.Data["json"] = map[string]string{"error": err.Error()}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
c.Data["json"] = updated
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// @Title DeleteService
|
||||
// @Description delete a service
|
||||
// @Param id path string true "service id"
|
||||
// @Success 200 {string} delete success
|
||||
// @Failure 404 not found
|
||||
// @router /:id [delete]
|
||||
func (c *ServicesController) Delete() {
|
||||
id := c.Ctx.Input.Param(":id")
|
||||
if err := models.DeleteService(id); err != nil {
|
||||
c.Ctx.Output.SetStatus(404)
|
||||
c.Data["json"] = map[string]string{"error": err.Error()}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
c.Data["json"] = map[string]string{"message": "delete success"}
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// @Title StartService
|
||||
// @Description start a service via its compose file
|
||||
// @Param id path string true "service id"
|
||||
// @Success 200 {string} started
|
||||
// @Failure 404 not found
|
||||
// @router /:id/start [post]
|
||||
func (c *ServicesController) Start() {
|
||||
id := c.Ctx.Input.Param(":id")
|
||||
svc, err := models.GetService(id)
|
||||
if err != nil {
|
||||
c.Ctx.Output.SetStatus(404)
|
||||
c.Data["json"] = map[string]string{"error": err.Error()}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
if err := models.StartService(svc); err != nil {
|
||||
c.Ctx.Output.SetStatus(500)
|
||||
c.Data["json"] = map[string]string{"error": err.Error()}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
_ = models.UpdateServiceStatus(id, "up")
|
||||
c.Data["json"] = map[string]string{"message": "service started"}
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// @Title StopService
|
||||
// @Description stop a service via its compose file
|
||||
// @Param id path string true "service id"
|
||||
// @Success 200 {string} stopped
|
||||
// @Failure 404 not found
|
||||
// @router /:id/stop [post]
|
||||
func (c *ServicesController) Stop() {
|
||||
id := c.Ctx.Input.Param(":id")
|
||||
svc, err := models.GetService(id)
|
||||
if err != nil {
|
||||
c.Ctx.Output.SetStatus(404)
|
||||
c.Data["json"] = map[string]string{"error": err.Error()}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
if err := models.StopService(svc); err != nil {
|
||||
_ = models.UpdateServiceStatus(id, "down")
|
||||
c.Ctx.Output.SetStatus(500)
|
||||
c.Data["json"] = map[string]string{"error": err.Error()}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
_ = models.UpdateServiceStatus(id, "down")
|
||||
c.Data["json"] = map[string]string{"message": "service stopped"}
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// @Title BackupService
|
||||
// @Description trigger an immediate backup for a service across all targets (asynchronous)
|
||||
// @Param id path string true "service id"
|
||||
// @Success 202 {string} backup started
|
||||
// @Failure 400 no backup configured
|
||||
// @Failure 404 not found
|
||||
// @router /:id/backup [post]
|
||||
func (c *ServicesController) Backup() {
|
||||
id := c.Ctx.Input.Param(":id")
|
||||
svc, err := models.GetService(id)
|
||||
if err != nil {
|
||||
c.Ctx.Output.SetStatus(404)
|
||||
c.Data["json"] = map[string]string{"error": err.Error()}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
if svc.Backup == nil || len(svc.Backup.Targets) == 0 {
|
||||
c.Ctx.Output.SetStatus(400)
|
||||
c.Data["json"] = map[string]string{"error": "no backup targets configured for this service"}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
c.Ctx.Output.SetStatus(202)
|
||||
c.Data["json"] = map[string]string{
|
||||
"message": "backup started",
|
||||
"started": fmt.Sprintf("%s", time.Now().UTC().Format(time.RFC3339)),
|
||||
}
|
||||
c.ServeJSON()
|
||||
|
||||
go func() {
|
||||
_ = models.RunServiceBackup(id, nil)
|
||||
}()
|
||||
}
|
||||
|
||||
// @Title GetServiceBackupTargets
|
||||
// @Description list a service's backup target definitions
|
||||
// @Param id path string true "service id"
|
||||
// @Success 200 {object} []models.BackupTarget
|
||||
// @Failure 404 not found
|
||||
// @router /:id/backup/targets [get]
|
||||
func (c *ServicesController) GetBackupTargets() {
|
||||
id := c.Ctx.Input.Param(":id")
|
||||
targets, err := models.GetServiceBackupTargets(id)
|
||||
if err != nil {
|
||||
c.Ctx.Output.SetStatus(404)
|
||||
c.Data["json"] = map[string]string{"error": err.Error()}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
c.Data["json"] = targets
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// @Title AddServiceBackupTarget
|
||||
// @Description define a new backup target for a service
|
||||
// @Param id path string true "service id"
|
||||
// @Param body body models.BackupTarget true "backup target definition"
|
||||
// @Success 201 {object} models.BackupTarget
|
||||
// @Failure 400 invalid body
|
||||
// @Failure 404 not found
|
||||
// @router /:id/backup/targets [post]
|
||||
func (c *ServicesController) AddBackupTarget() {
|
||||
id := c.Ctx.Input.Param(":id")
|
||||
var t models.BackupTarget
|
||||
if err := json.Unmarshal(c.Ctx.Input.RequestBody, &t); err != nil {
|
||||
c.Ctx.Output.SetStatus(400)
|
||||
c.Data["json"] = map[string]string{"error": err.Error()}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
created, err := models.AddServiceBackupTarget(id, t)
|
||||
if err != nil {
|
||||
status := 400
|
||||
if err.Error() == "service not found" {
|
||||
status = 404
|
||||
}
|
||||
c.Ctx.Output.SetStatus(status)
|
||||
c.Data["json"] = map[string]string{"error": err.Error()}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
c.Ctx.Output.SetStatus(201)
|
||||
c.Data["json"] = created
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// @Title GetServiceBackupTarget
|
||||
// @Description get a service backup target by id
|
||||
// @Param id path string true "service id"
|
||||
// @Param tid path string true "backup target id"
|
||||
// @Success 200 {object} models.BackupTarget
|
||||
// @Failure 404 not found
|
||||
// @router /:id/backup/targets/:tid [get]
|
||||
func (c *ServicesController) GetBackupTarget() {
|
||||
id := c.Ctx.Input.Param(":id")
|
||||
tid := c.Ctx.Input.Param(":tid")
|
||||
t, err := models.GetServiceBackupTarget(id, tid)
|
||||
if err != nil {
|
||||
c.Ctx.Output.SetStatus(404)
|
||||
c.Data["json"] = map[string]string{"error": err.Error()}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
c.Data["json"] = t
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// @Title PutServiceBackupTarget
|
||||
// @Description update a service backup target definition
|
||||
// @Param id path string true "service id"
|
||||
// @Param tid path string true "backup target id"
|
||||
// @Param body body models.BackupTarget true "updated backup target"
|
||||
// @Success 200 {object} models.BackupTarget
|
||||
// @Failure 400 invalid body
|
||||
// @Failure 404 not found
|
||||
// @router /:id/backup/targets/:tid [put]
|
||||
func (c *ServicesController) PutBackupTarget() {
|
||||
id := c.Ctx.Input.Param(":id")
|
||||
tid := c.Ctx.Input.Param(":tid")
|
||||
var t models.BackupTarget
|
||||
if err := json.Unmarshal(c.Ctx.Input.RequestBody, &t); err != nil {
|
||||
c.Ctx.Output.SetStatus(400)
|
||||
c.Data["json"] = map[string]string{"error": err.Error()}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
updated, err := models.UpdateServiceBackupTarget(id, tid, t)
|
||||
if err != nil {
|
||||
status := 400
|
||||
if strings.Contains(err.Error(), "not found") {
|
||||
status = 404
|
||||
}
|
||||
c.Ctx.Output.SetStatus(status)
|
||||
c.Data["json"] = map[string]string{"error": err.Error()}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
c.Data["json"] = updated
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// @Title DeleteServiceBackupTarget
|
||||
// @Description delete a service backup target definition
|
||||
// @Param id path string true "service id"
|
||||
// @Param tid path string true "backup target id"
|
||||
// @Success 200 {string} delete success
|
||||
// @Failure 404 not found
|
||||
// @router /:id/backup/targets/:tid [delete]
|
||||
func (c *ServicesController) DeleteBackupTarget() {
|
||||
id := c.Ctx.Input.Param(":id")
|
||||
tid := c.Ctx.Input.Param(":tid")
|
||||
if err := models.DeleteServiceBackupTarget(id, tid); err != nil {
|
||||
c.Ctx.Output.SetStatus(404)
|
||||
c.Data["json"] = map[string]string{"error": err.Error()}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
c.Data["json"] = map[string]string{"message": "delete success"}
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// @Title RunServiceBackupTarget
|
||||
// @Description trigger an immediate backup for a single service target (asynchronous)
|
||||
// @Param id path string true "service id"
|
||||
// @Param tid path string true "backup target id"
|
||||
// @Success 202 {string} backup started
|
||||
// @Failure 404 not found
|
||||
// @router /:id/backup/targets/:tid/run [post]
|
||||
func (c *ServicesController) RunBackupTarget() {
|
||||
id := c.Ctx.Input.Param(":id")
|
||||
tid := c.Ctx.Input.Param(":tid")
|
||||
if _, err := models.GetServiceBackupTarget(id, tid); err != nil {
|
||||
c.Ctx.Output.SetStatus(404)
|
||||
c.Data["json"] = map[string]string{"error": err.Error()}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
c.Ctx.Output.SetStatus(202)
|
||||
c.Data["json"] = map[string]string{"message": "backup started"}
|
||||
c.ServeJSON()
|
||||
go func() {
|
||||
_ = models.RunServiceBackup(id, []string{tid})
|
||||
}()
|
||||
}
|
||||
|
||||
// @Title GetServiceBackupTargetProgress
|
||||
// @Description get the current/last run state for a service backup target
|
||||
// @Param id path string true "service id"
|
||||
// @Param tid path string true "backup target id"
|
||||
// @Success 200 {object} models.RunState
|
||||
// @Failure 404 not found
|
||||
// @router /:id/backup/targets/:tid/progress [get]
|
||||
func (c *ServicesController) GetBackupTargetProgress() {
|
||||
id := c.Ctx.Input.Param(":id")
|
||||
tid := c.Ctx.Input.Param(":tid")
|
||||
if _, err := models.GetServiceBackupTarget(id, tid); err != nil {
|
||||
c.Ctx.Output.SetStatus(404)
|
||||
c.Data["json"] = map[string]string{"error": err.Error()}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
state, ok := models.GetProgress("svc:"+id, tid)
|
||||
if !ok {
|
||||
state = models.RunState{TargetID: tid}
|
||||
}
|
||||
c.Data["json"] = state
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// @Title GetServiceBackupProgress
|
||||
// @Description get the current/last run state for every backup target of a service
|
||||
// @Param id path string true "service id"
|
||||
// @Success 200 {object} []models.RunState
|
||||
// @Failure 404 not found
|
||||
// @router /:id/backup/progress [get]
|
||||
func (c *ServicesController) GetBackupProgress() {
|
||||
id := c.Ctx.Input.Param(":id")
|
||||
if _, err := models.GetService(id); err != nil {
|
||||
c.Ctx.Output.SetStatus(404)
|
||||
c.Data["json"] = map[string]string{"error": err.Error()}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
c.Data["json"] = models.GetProgressForScope("svc:" + id)
|
||||
c.ServeJSON()
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
module nodemaster
|
||||
|
||||
go 1.24
|
||||
|
||||
require github.com/beego/beego/v2 v2.1.0
|
||||
|
||||
require (
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/smartystreets/goconvey v1.6.4
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 // indirect
|
||||
github.com/hashicorp/golang-lru v0.5.4 // indirect
|
||||
github.com/jtolds/gls v4.20.0+incompatible // indirect
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/prometheus/client_golang v1.15.1 // indirect
|
||||
github.com/prometheus/client_model v0.3.0 // indirect
|
||||
github.com/prometheus/common v0.42.0 // indirect
|
||||
github.com/prometheus/procfs v0.9.0 // indirect
|
||||
github.com/shiena/ansicolor v0.0.0-20200904210342-c7312218db18 // indirect
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d // indirect
|
||||
golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd // indirect
|
||||
golang.org/x/net v0.7.0 // indirect
|
||||
golang.org/x/sys v0.6.0 // indirect
|
||||
golang.org/x/text v0.7.0 // indirect
|
||||
google.golang.org/protobuf v1.30.0 // indirect
|
||||
)
|
||||
@@ -0,0 +1,81 @@
|
||||
github.com/beego/beego/v2 v2.1.0 h1:Lk0FtQGvDQCx5V5yEu4XwDsIgt+QOlNjt5emUa3/ZmA=
|
||||
github.com/beego/beego/v2 v2.1.0/go.mod h1:6h36ISpaxNrrpJ27siTpXBG8d/Icjzsc7pU1bWpp0EE=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/elazarl/go-bindata-assetfs v1.0.1 h1:m0kkaHRKEu7tUIUFVwhGGGYClXvyl4RE03qmvRTNfbw=
|
||||
github.com/elazarl/go-bindata-assetfs v1.0.1/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
|
||||
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
|
||||
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v1.15.1 h1:8tXpTmJbyH5lydzFPoxSIJ0J46jdh3tylbvM1xCv0LI=
|
||||
github.com/prometheus/client_golang v1.15.1/go.mod h1:e9yaBhRPU2pPNsZwE+JdQl0KEt1N9XgF6zxWmaC0xOk=
|
||||
github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4=
|
||||
github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w=
|
||||
github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM=
|
||||
github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc=
|
||||
github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI=
|
||||
github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY=
|
||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||
github.com/shiena/ansicolor v0.0.0-20200904210342-c7312218db18 h1:DAYUYH5869yV94zvCES9F51oYtN5oGlwjxJJz7ZCnik=
|
||||
github.com/shiena/ansicolor v0.0.0-20200904210342-c7312218db18/go.mod h1:nkxAfR/5quYxwPZhyDxgasBMnRtBZd0FCEpawpjMUFg=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
|
||||
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd h1:XcWmESyNjXJMLahc3mqVQJcgSTDxFxhETVlfk9uGc38=
|
||||
golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
|
||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
|
||||
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
@@ -0,0 +1,125 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"nodemaster/models"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
_ "nodemaster/routers"
|
||||
|
||||
beego "github.com/beego/beego/v2/server/web"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if len(os.Args) > 1 {
|
||||
switch os.Args[1] {
|
||||
case "scan":
|
||||
runScanCLI(os.Args[2:])
|
||||
return
|
||||
case "info":
|
||||
runInfoCLI()
|
||||
return
|
||||
}
|
||||
}
|
||||
if beego.BConfig.RunMode == "dev" {
|
||||
beego.BConfig.WebConfig.DirectoryIndex = true
|
||||
beego.BConfig.WebConfig.StaticDir["/swagger"] = "swagger"
|
||||
}
|
||||
models.StartScheduler()
|
||||
beego.Run()
|
||||
}
|
||||
|
||||
func runInfoCLI() {
|
||||
n := models.GetLocalNode()
|
||||
svcs := models.GetAllServices()
|
||||
nodes := models.GetAllRemoteNodes()
|
||||
fmt.Println("NodeMaster info")
|
||||
fmt.Println(strings.Repeat("─", 60))
|
||||
fmt.Printf("Config file : %s\n", models.ConfigPath())
|
||||
fmt.Printf("Hostname : %s\n", n.Hostname)
|
||||
if n.Description != "" {
|
||||
fmt.Printf("Description : %s\n", n.Description)
|
||||
}
|
||||
fmt.Printf("Services : %d registered\n", len(svcs))
|
||||
fmt.Printf("Remote nodes: %d registered\n", len(nodes))
|
||||
fmt.Printf("Updates : %d records\n", len(n.UpdateHistory))
|
||||
}
|
||||
|
||||
func runScanCLI(args []string) {
|
||||
var folder string
|
||||
jsonOutput := false
|
||||
for _, a := range args {
|
||||
switch {
|
||||
case a == "--json":
|
||||
jsonOutput = true
|
||||
case !strings.HasPrefix(a, "-"):
|
||||
folder = a
|
||||
}
|
||||
}
|
||||
|
||||
result := models.ScanAll(folder)
|
||||
|
||||
if jsonOutput {
|
||||
out, _ := json.MarshalIndent(result, "", " ")
|
||||
fmt.Println(string(out))
|
||||
if len(result.Errors) > 0 {
|
||||
os.Exit(1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("NodeMaster scan")
|
||||
fmt.Println(strings.Repeat("─", 60))
|
||||
fmt.Printf("Config file : %s\n\n", models.ConfigPath())
|
||||
|
||||
if len(result.ScannedPaths) == 0 {
|
||||
fmt.Println("No scan paths found (no /opt/compose, no ~/compose dirs)")
|
||||
} else {
|
||||
fmt.Println("Paths scanned:")
|
||||
for _, p := range result.ScannedPaths {
|
||||
fmt.Printf(" %s\n", p)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("\nCompose files found: %d\n", result.Found)
|
||||
|
||||
if len(result.Created) > 0 {
|
||||
fmt.Printf("\nCreated (%d):\n", len(result.Created))
|
||||
for _, s := range result.Created {
|
||||
printScanEntry(s)
|
||||
}
|
||||
}
|
||||
|
||||
if len(result.Updated) > 0 {
|
||||
fmt.Printf("\nUpdated (%d):\n", len(result.Updated))
|
||||
for _, s := range result.Updated {
|
||||
printScanEntry(s)
|
||||
}
|
||||
}
|
||||
|
||||
if len(result.Created) == 0 && len(result.Updated) == 0 {
|
||||
fmt.Println("\nNo changes (all services already up to date)")
|
||||
}
|
||||
|
||||
if len(result.Errors) > 0 {
|
||||
fmt.Printf("\nErrors:\n")
|
||||
for _, e := range result.Errors {
|
||||
fmt.Fprintf(os.Stderr, " ! %s\n", e)
|
||||
}
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func printScanEntry(s models.ServiceScanInfo) {
|
||||
status := s.Status
|
||||
if status == "" {
|
||||
status = "unknown"
|
||||
}
|
||||
backup := s.BackupFolder
|
||||
if backup == "" {
|
||||
backup = "(none deduced)"
|
||||
}
|
||||
fmt.Printf(" %-24s [%-7s] backup: %s\n", s.Name, status, backup)
|
||||
}
|
||||
@@ -0,0 +1,298 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/robfig/cron/v3"
|
||||
)
|
||||
|
||||
// validateBackupTarget checks that a backup target is well-formed enough to
|
||||
// store: a known method, and (if set) a parseable cron schedule.
|
||||
func validateBackupTarget(t BackupTarget) error {
|
||||
switch t.Method {
|
||||
case MethodRsync, MethodRestic, MethodExternal:
|
||||
default:
|
||||
return fmt.Errorf("unknown backup method: %s", t.Method)
|
||||
}
|
||||
if t.Schedule != "" {
|
||||
if _, err := cron.ParseStandard(t.Schedule); err != nil {
|
||||
return fmt.Errorf("invalid schedule %q: %w", t.Schedule, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RunBackup executes backup for all targets against sourcePath and returns one
|
||||
// execution record per target. scope identifies where progress is published
|
||||
// ("node" or "svc:<serviceID>").
|
||||
func RunBackup(scope string, targets []BackupTarget, sourcePath string) []BackupExecution {
|
||||
results := make([]BackupExecution, 0, len(targets))
|
||||
for _, t := range targets {
|
||||
results = append(results, runTarget(scope, t, sourcePath))
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
func runTarget(scope string, t BackupTarget, sourcePath string) BackupExecution {
|
||||
start := time.Now()
|
||||
ex := BackupExecution{
|
||||
ID: fmt.Sprintf("bex_%d", start.UnixNano()),
|
||||
Timestamp: start.UTC(),
|
||||
TargetID: t.ID,
|
||||
}
|
||||
key := progressKey(scope, t.ID)
|
||||
startedAt := start.UTC()
|
||||
setProgress(key, RunState{Running: true, TargetID: t.ID, Method: t.Method, StartedAt: &startedAt})
|
||||
|
||||
if t.Method == MethodExternal {
|
||||
ex.Success = true
|
||||
ex.Message = "external backup managed externally; no action taken by API"
|
||||
setProgress(key, RunState{Running: false, TargetID: t.ID, Method: t.Method, Percent: 100, Message: ex.Message, StartedAt: &startedAt})
|
||||
return ex
|
||||
}
|
||||
|
||||
var cmd *exec.Cmd
|
||||
switch t.Method {
|
||||
case MethodRsync:
|
||||
args := []string{"-avz", "--info=progress2"}
|
||||
for k, v := range t.Params {
|
||||
args = append(args, "--"+k+"="+v)
|
||||
}
|
||||
args = append(args, sourcePath, t.Remote)
|
||||
cmd = exec.Command("rsync", args...)
|
||||
case MethodRestic:
|
||||
password := GetResticPassword()
|
||||
if password == "" {
|
||||
ex.Success = false
|
||||
ex.Message = "no restic repository password configured on this node; set restic_password via PUT /v1/node"
|
||||
setProgress(key, RunState{Running: false, TargetID: t.ID, Method: t.Method, Message: ex.Message, StartedAt: &startedAt})
|
||||
return ex
|
||||
}
|
||||
env := append(os.Environ(), "RESTIC_REPOSITORY="+t.Remote, "RESTIC_PASSWORD="+password)
|
||||
if err := ensureResticRepo(env); err != nil {
|
||||
ex.Success = false
|
||||
ex.Message = fmt.Sprintf("restic init failed: %v", err)
|
||||
setProgress(key, RunState{Running: false, TargetID: t.ID, Method: t.Method, Message: ex.Message, StartedAt: &startedAt})
|
||||
return ex
|
||||
}
|
||||
args := []string{"backup", sourcePath, "--json"}
|
||||
for k, v := range t.Params {
|
||||
args = append(args, "--"+k+"="+v)
|
||||
}
|
||||
cmd = exec.Command("restic", args...)
|
||||
cmd.Env = env
|
||||
default:
|
||||
ex.Success = false
|
||||
ex.Message = fmt.Sprintf("unknown backup method: %s", t.Method)
|
||||
setProgress(key, RunState{Running: false, TargetID: t.ID, Method: t.Method, Message: ex.Message, StartedAt: &startedAt})
|
||||
return ex
|
||||
}
|
||||
|
||||
pw := &progressWriter{key: key, targetID: t.ID, method: t.Method, startedAt: startedAt}
|
||||
cmd.Stdout = pw
|
||||
cmd.Stderr = pw
|
||||
|
||||
err := cmd.Run()
|
||||
ex.Duration = int64(time.Since(start).Seconds())
|
||||
out := strings.TrimSpace(pw.buf.String())
|
||||
if err != nil {
|
||||
ex.Success = false
|
||||
ex.Message = fmt.Sprintf("%v | %s", err, out)
|
||||
} else {
|
||||
ex.Success = true
|
||||
if pw.lastMessage != "" {
|
||||
ex.Message = pw.lastMessage
|
||||
} else {
|
||||
ex.Message = out
|
||||
}
|
||||
}
|
||||
|
||||
final := pw.state()
|
||||
final.Running = false
|
||||
if ex.Success {
|
||||
final.Percent = 100
|
||||
}
|
||||
final.Message = ex.Message
|
||||
setProgress(key, final)
|
||||
|
||||
return ex
|
||||
}
|
||||
|
||||
// ensureResticRepo initializes the restic repository named by env's
|
||||
// RESTIC_REPOSITORY on first use. Running "init" against a repository that
|
||||
// already exists fails with a message containing "already"; that case is
|
||||
// treated as success so callers don't need to track init state themselves.
|
||||
func ensureResticRepo(env []string) error {
|
||||
cmd := exec.Command("restic", "init")
|
||||
cmd.Env = env
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
if strings.Contains(strings.ToLower(string(out)), "already") {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("%v: %s", err, strings.TrimSpace(string(out)))
|
||||
}
|
||||
|
||||
// progressWriter is assigned as both Stdout and Stderr of a backup command. It
|
||||
// buffers the full output (for the final BackupExecution.Message) while also
|
||||
// scanning completed lines for progress info to publish via setProgress.
|
||||
// Best-effort only: a line that doesn't parse just leaves the last known
|
||||
// percentage/ETA in place rather than failing the backup.
|
||||
type progressWriter struct {
|
||||
buf bytes.Buffer
|
||||
pending []byte
|
||||
key string
|
||||
targetID string
|
||||
method BackupMethod
|
||||
startedAt time.Time
|
||||
percent float64
|
||||
eta string
|
||||
lastMessage string
|
||||
}
|
||||
|
||||
func (w *progressWriter) Write(p []byte) (int, error) {
|
||||
w.buf.Write(p)
|
||||
w.pending = append(w.pending, p...)
|
||||
for {
|
||||
idx := bytes.IndexAny(w.pending, "\r\n")
|
||||
if idx < 0 {
|
||||
break
|
||||
}
|
||||
line := bytes.TrimSpace(w.pending[:idx])
|
||||
w.pending = w.pending[idx+1:]
|
||||
if len(line) > 0 {
|
||||
w.handleLine(line)
|
||||
}
|
||||
}
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func (w *progressWriter) handleLine(line []byte) {
|
||||
switch w.method {
|
||||
case MethodRsync:
|
||||
w.handleRsyncLine(line)
|
||||
case MethodRestic:
|
||||
w.handleResticLine(line)
|
||||
}
|
||||
setProgress(w.key, w.state())
|
||||
}
|
||||
|
||||
var rsyncPercentRe = regexp.MustCompile(`(\d+)%`)
|
||||
|
||||
func (w *progressWriter) handleRsyncLine(line []byte) {
|
||||
w.lastMessage = string(line)
|
||||
m := rsyncPercentRe.FindSubmatch(line)
|
||||
if m == nil {
|
||||
return
|
||||
}
|
||||
var pct float64
|
||||
if _, err := fmt.Sscanf(string(m[1]), "%f", &pct); err == nil {
|
||||
w.percent = pct
|
||||
}
|
||||
}
|
||||
|
||||
// resticStatusLine models the subset of restic's `backup --json` line kinds
|
||||
// (message_type "status", "summary", "error") that are useful for progress
|
||||
// reporting; unrecognized/unparseable lines are ignored.
|
||||
type resticStatusLine struct {
|
||||
MessageType string `json:"message_type"`
|
||||
PercentDone float64 `json:"percent_done"`
|
||||
SecondsRemaining float64 `json:"seconds_remaining"`
|
||||
SnapshotID string `json:"snapshot_id"`
|
||||
Error *struct {
|
||||
Message string `json:"message"`
|
||||
} `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
func (w *progressWriter) handleResticLine(line []byte) {
|
||||
var sl resticStatusLine
|
||||
if err := json.Unmarshal(line, &sl); err != nil {
|
||||
return
|
||||
}
|
||||
switch sl.MessageType {
|
||||
case "status":
|
||||
w.percent = sl.PercentDone * 100
|
||||
if sl.SecondsRemaining > 0 {
|
||||
w.eta = fmt.Sprintf("%.0fs", sl.SecondsRemaining)
|
||||
}
|
||||
case "summary":
|
||||
w.percent = 100
|
||||
if sl.SnapshotID != "" {
|
||||
w.lastMessage = fmt.Sprintf("snapshot %s created", sl.SnapshotID)
|
||||
}
|
||||
case "error":
|
||||
if sl.Error != nil && sl.Error.Message != "" {
|
||||
w.lastMessage = sl.Error.Message
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (w *progressWriter) state() RunState {
|
||||
startedAt := w.startedAt
|
||||
return RunState{
|
||||
Running: true,
|
||||
TargetID: w.targetID,
|
||||
Method: w.method,
|
||||
Percent: w.percent,
|
||||
ETA: w.eta,
|
||||
Message: w.lastMessage,
|
||||
StartedAt: &startedAt,
|
||||
}
|
||||
}
|
||||
|
||||
// StartService runs the compose start command for the given service.
|
||||
func StartService(svc Service) error {
|
||||
cmd := composeCmd(svc, "start")
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("%v: %s", err, strings.TrimSpace(string(out)))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// StopService runs the compose stop command for the given service.
|
||||
func StopService(svc Service) error {
|
||||
cmd := composeCmd(svc, "stop")
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("%v: %s", err, strings.TrimSpace(string(out)))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func composeCmd(svc Service, action string) *exec.Cmd {
|
||||
switch svc.ComposeType {
|
||||
case "docker-compose":
|
||||
return exec.Command("docker-compose", "-f", svc.ComposeFile, action)
|
||||
case "podman-compose":
|
||||
return exec.Command("podman-compose", "-f", svc.ComposeFile, action)
|
||||
default:
|
||||
return exec.Command("docker", "compose", "-f", svc.ComposeFile, action)
|
||||
}
|
||||
}
|
||||
|
||||
// filterTargets returns the subset of targets whose ID is in ids. A nil or
|
||||
// empty ids means "all targets".
|
||||
func filterTargets(targets []BackupTarget, ids []string) []BackupTarget {
|
||||
if len(ids) == 0 {
|
||||
return targets
|
||||
}
|
||||
want := make(map[string]bool, len(ids))
|
||||
for _, id := range ids {
|
||||
want[id] = true
|
||||
}
|
||||
result := make([]BackupTarget, 0, len(ids))
|
||||
for _, t := range targets {
|
||||
if want[t.ID] {
|
||||
result = append(result, t)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
+221
@@ -0,0 +1,221 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
func GetLocalNode() NodeInfo {
|
||||
mu.RLock()
|
||||
defer mu.RUnlock()
|
||||
return cfg.Node
|
||||
}
|
||||
|
||||
func UpdateLocalNode(n NodeInfo) error {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
if n.Hostname != "" {
|
||||
cfg.Node.Hostname = n.Hostname
|
||||
}
|
||||
if n.Description != "" {
|
||||
cfg.Node.Description = n.Description
|
||||
}
|
||||
if n.ResticPassword != "" {
|
||||
cfg.Node.ResticPassword = n.ResticPassword
|
||||
}
|
||||
return persist()
|
||||
}
|
||||
|
||||
// GetResticPassword returns the node-wide restic repository password used to
|
||||
// encrypt/decrypt every restic backup target on this node.
|
||||
func GetResticPassword() string {
|
||||
mu.RLock()
|
||||
defer mu.RUnlock()
|
||||
return cfg.Node.ResticPassword
|
||||
}
|
||||
|
||||
func GetUpdateHistory() []UpdateRecord {
|
||||
mu.RLock()
|
||||
defer mu.RUnlock()
|
||||
result := make([]UpdateRecord, len(cfg.Node.UpdateHistory))
|
||||
copy(result, cfg.Node.UpdateHistory)
|
||||
return result
|
||||
}
|
||||
|
||||
func GetUpdateRecord(id string) (UpdateRecord, error) {
|
||||
mu.RLock()
|
||||
defer mu.RUnlock()
|
||||
for _, r := range cfg.Node.UpdateHistory {
|
||||
if r.ID == id {
|
||||
return r, nil
|
||||
}
|
||||
}
|
||||
return UpdateRecord{}, errors.New("update record not found")
|
||||
}
|
||||
|
||||
func AddUpdateRecord(r UpdateRecord) (UpdateRecord, error) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
r.ID = fmt.Sprintf("upd_%d", time.Now().UnixNano())
|
||||
r.Timestamp = time.Now().UTC()
|
||||
cfg.Node.UpdateHistory = append(cfg.Node.UpdateHistory, r)
|
||||
return r, persist()
|
||||
}
|
||||
|
||||
func DeleteUpdateRecord(id string) error {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
for i, r := range cfg.Node.UpdateHistory {
|
||||
if r.ID == id {
|
||||
cfg.Node.UpdateHistory = append(cfg.Node.UpdateHistory[:i], cfg.Node.UpdateHistory[i+1:]...)
|
||||
return persist()
|
||||
}
|
||||
}
|
||||
return errors.New("update record not found")
|
||||
}
|
||||
|
||||
func GetNodeBackupConfig() BackupConfig {
|
||||
mu.RLock()
|
||||
defer mu.RUnlock()
|
||||
return cfg.Node.Backup
|
||||
}
|
||||
|
||||
func UpdateNodeBackupConfig(bc BackupConfig) error {
|
||||
mu.Lock()
|
||||
history := cfg.Node.Backup.History
|
||||
cfg.Node.Backup = bc
|
||||
cfg.Node.Backup.History = history
|
||||
err := persist()
|
||||
mu.Unlock()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
SyncSchedule()
|
||||
return nil
|
||||
}
|
||||
|
||||
func AppendNodeBackupExecution(ex BackupExecution) error {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
cfg.Node.Backup.History = append(cfg.Node.Backup.History, ex)
|
||||
now := time.Now().UTC()
|
||||
cfg.Node.Backup.LastRun = &now
|
||||
for i, t := range cfg.Node.Backup.Targets {
|
||||
if t.ID == ex.TargetID {
|
||||
cfg.Node.Backup.Targets[i].LastRun = &now
|
||||
break
|
||||
}
|
||||
}
|
||||
return persist()
|
||||
}
|
||||
|
||||
// GetNodeBackupTargets lists the node's individual backup target definitions.
|
||||
func GetNodeBackupTargets() []BackupTarget {
|
||||
mu.RLock()
|
||||
defer mu.RUnlock()
|
||||
result := make([]BackupTarget, len(cfg.Node.Backup.Targets))
|
||||
copy(result, cfg.Node.Backup.Targets)
|
||||
return result
|
||||
}
|
||||
|
||||
// GetNodeBackupTarget returns a single node backup target by ID.
|
||||
func GetNodeBackupTarget(tid string) (BackupTarget, error) {
|
||||
mu.RLock()
|
||||
defer mu.RUnlock()
|
||||
for _, t := range cfg.Node.Backup.Targets {
|
||||
if t.ID == tid {
|
||||
return t, nil
|
||||
}
|
||||
}
|
||||
return BackupTarget{}, errors.New("backup target not found")
|
||||
}
|
||||
|
||||
// AddNodeBackupTarget creates a new backup target definition for the node.
|
||||
func AddNodeBackupTarget(t BackupTarget) (BackupTarget, error) {
|
||||
if err := validateBackupTarget(t); err != nil {
|
||||
return BackupTarget{}, err
|
||||
}
|
||||
mu.Lock()
|
||||
t.ID = fmt.Sprintf("bt_%d", time.Now().UnixNano())
|
||||
cfg.Node.Backup.Targets = append(cfg.Node.Backup.Targets, t)
|
||||
err := persist()
|
||||
mu.Unlock()
|
||||
if err != nil {
|
||||
return BackupTarget{}, err
|
||||
}
|
||||
SyncSchedule()
|
||||
return t, nil
|
||||
}
|
||||
|
||||
// UpdateNodeBackupTarget replaces an existing node backup target definition.
|
||||
func UpdateNodeBackupTarget(tid string, t BackupTarget) (BackupTarget, error) {
|
||||
if err := validateBackupTarget(t); err != nil {
|
||||
return BackupTarget{}, err
|
||||
}
|
||||
mu.Lock()
|
||||
found := false
|
||||
for i, existing := range cfg.Node.Backup.Targets {
|
||||
if existing.ID == tid {
|
||||
t.ID = tid
|
||||
t.LastRun = existing.LastRun
|
||||
cfg.Node.Backup.Targets[i] = t
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
mu.Unlock()
|
||||
return BackupTarget{}, errors.New("backup target not found")
|
||||
}
|
||||
err := persist()
|
||||
mu.Unlock()
|
||||
if err != nil {
|
||||
return BackupTarget{}, err
|
||||
}
|
||||
SyncSchedule()
|
||||
return t, nil
|
||||
}
|
||||
|
||||
// DeleteNodeBackupTarget removes a node backup target definition.
|
||||
func DeleteNodeBackupTarget(tid string) error {
|
||||
mu.Lock()
|
||||
found := false
|
||||
for i, t := range cfg.Node.Backup.Targets {
|
||||
if t.ID == tid {
|
||||
cfg.Node.Backup.Targets = append(cfg.Node.Backup.Targets[:i], cfg.Node.Backup.Targets[i+1:]...)
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
mu.Unlock()
|
||||
return errors.New("backup target not found")
|
||||
}
|
||||
err := persist()
|
||||
mu.Unlock()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
SyncSchedule()
|
||||
return nil
|
||||
}
|
||||
|
||||
// RunNodeBackup runs the node's backup for the given target IDs (nil/empty
|
||||
// means all targets), appending an execution record per target.
|
||||
func RunNodeBackup(targetIDs []string) error {
|
||||
bc := GetNodeBackupConfig()
|
||||
targets := filterTargets(bc.Targets, targetIDs)
|
||||
if len(targets) == 0 {
|
||||
return errors.New("no matching backup targets")
|
||||
}
|
||||
sourcePath := bc.SourcePath
|
||||
if sourcePath == "" {
|
||||
sourcePath = ConfigPath()
|
||||
}
|
||||
results := RunBackup("node", targets, sourcePath)
|
||||
for _, ex := range results {
|
||||
_ = AppendNodeBackupExecution(ex)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
func GetAllRemoteNodes() []RemoteNode {
|
||||
mu.RLock()
|
||||
defer mu.RUnlock()
|
||||
result := make([]RemoteNode, len(cfg.Nodes))
|
||||
copy(result, cfg.Nodes)
|
||||
return result
|
||||
}
|
||||
|
||||
func GetRemoteNode(id string) (RemoteNode, error) {
|
||||
mu.RLock()
|
||||
defer mu.RUnlock()
|
||||
for _, n := range cfg.Nodes {
|
||||
if n.ID == id {
|
||||
return n, nil
|
||||
}
|
||||
}
|
||||
return RemoteNode{}, errors.New("node not found")
|
||||
}
|
||||
|
||||
func AddRemoteNode(n RemoteNode) (RemoteNode, error) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
n.ID = fmt.Sprintf("node_%d", time.Now().UnixNano())
|
||||
cfg.Nodes = append(cfg.Nodes, n)
|
||||
return n, persist()
|
||||
}
|
||||
|
||||
func UpdateRemoteNode(id string, n RemoteNode) (RemoteNode, error) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
for i, node := range cfg.Nodes {
|
||||
if node.ID == id {
|
||||
n.ID = id
|
||||
cfg.Nodes[i] = n
|
||||
return n, persist()
|
||||
}
|
||||
}
|
||||
return RemoteNode{}, errors.New("node not found")
|
||||
}
|
||||
|
||||
func DeleteRemoteNode(id string) error {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
for i, n := range cfg.Nodes {
|
||||
if n.ID == id {
|
||||
cfg.Nodes = append(cfg.Nodes[:i], cfg.Nodes[i+1:]...)
|
||||
return persist()
|
||||
}
|
||||
}
|
||||
return errors.New("node not found")
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// RunState is the in-memory, non-persisted status of a backup target's current
|
||||
// or most recent run. It intentionally lives outside cfg/persist(): a crash or
|
||||
// restart should never leave a stale "still running" flag behind.
|
||||
type RunState struct {
|
||||
Running bool `json:"running"`
|
||||
TargetID string `json:"target_id"`
|
||||
Method BackupMethod `json:"method,omitempty"`
|
||||
Percent float64 `json:"percent"`
|
||||
ETA string `json:"eta,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
StartedAt *time.Time `json:"started_at,omitempty"`
|
||||
}
|
||||
|
||||
var (
|
||||
progressMu sync.RWMutex
|
||||
progress = map[string]RunState{}
|
||||
)
|
||||
|
||||
// progressKey builds the scoped key a target's run state is stored under.
|
||||
// scope is "node" or "svc:<serviceID>".
|
||||
func progressKey(scope, targetID string) string {
|
||||
return scope + ":" + targetID
|
||||
}
|
||||
|
||||
func setProgress(key string, s RunState) {
|
||||
progressMu.Lock()
|
||||
defer progressMu.Unlock()
|
||||
progress[key] = s
|
||||
}
|
||||
|
||||
// GetProgress returns the current run state for a single target, if any is known.
|
||||
func GetProgress(scope, targetID string) (RunState, bool) {
|
||||
progressMu.RLock()
|
||||
defer progressMu.RUnlock()
|
||||
s, ok := progress[progressKey(scope, targetID)]
|
||||
return s, ok
|
||||
}
|
||||
|
||||
// GetProgressForScope returns all known run states for a scope ("node" or "svc:<id>").
|
||||
func GetProgressForScope(scope string) []RunState {
|
||||
prefix := scope + ":"
|
||||
progressMu.RLock()
|
||||
defer progressMu.RUnlock()
|
||||
result := make([]RunState, 0)
|
||||
for k, s := range progress {
|
||||
if strings.HasPrefix(k, prefix) {
|
||||
result = append(result, s)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
+366
@@ -0,0 +1,366 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
var composeFileNames = []string{
|
||||
"docker-compose.yml",
|
||||
"docker-compose.yaml",
|
||||
"compose.yml",
|
||||
"compose.yaml",
|
||||
}
|
||||
|
||||
// Host paths excluded from backup folder deduction.
|
||||
var excludedPrefixes = []string{"/var/run/", "/proc/", "/sys/", "/dev/", "/run/", "/etc/"}
|
||||
var excludedExact = []string{
|
||||
"/var/run/docker.sock",
|
||||
"/etc/timezone", "/etc/localtime", "/etc/hosts", "/etc/hostname", "/etc/resolv.conf",
|
||||
}
|
||||
|
||||
// ScanAll discovers compose-based services under all default paths plus an optional
|
||||
// extra folder. Existing services matched by compose file path are updated (status,
|
||||
// and backup folder if previously unset). New services are created.
|
||||
func ScanAll(extraFolder string) ScanResult {
|
||||
paths := resolveScanPaths(extraFolder)
|
||||
result := ScanResult{
|
||||
ScannedPaths: paths,
|
||||
Created: []ServiceScanInfo{},
|
||||
Updated: []ServiceScanInfo{},
|
||||
Errors: []string{},
|
||||
}
|
||||
for _, p := range paths {
|
||||
r := scanFolder(p)
|
||||
result.Found += r.Found
|
||||
result.Created = append(result.Created, r.Created...)
|
||||
result.Updated = append(result.Updated, r.Updated...)
|
||||
result.Errors = append(result.Errors, r.Errors...)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// resolveScanPaths returns the directories to scan: /opt/compose, /root/compose,
|
||||
// /home/*/compose, and any explicitly supplied extra path.
|
||||
func resolveScanPaths(extra string) []string {
|
||||
var paths []string
|
||||
for _, candidate := range []string{"/opt/compose", "/root/compose"} {
|
||||
if isAccessibleDir(candidate) {
|
||||
paths = append(paths, candidate)
|
||||
}
|
||||
}
|
||||
if entries, err := os.ReadDir("/home"); err == nil {
|
||||
for _, e := range entries {
|
||||
if !e.IsDir() {
|
||||
continue
|
||||
}
|
||||
p := filepath.Join("/home", e.Name(), "compose")
|
||||
if isAccessibleDir(p) {
|
||||
paths = append(paths, p)
|
||||
}
|
||||
}
|
||||
}
|
||||
if extra != "" && isAccessibleDir(extra) {
|
||||
paths = append(paths, extra)
|
||||
}
|
||||
return paths
|
||||
}
|
||||
|
||||
func isAccessibleDir(path string) bool {
|
||||
info, err := os.Stat(path)
|
||||
return err == nil && info.IsDir()
|
||||
}
|
||||
|
||||
// scanFolder scans the immediate subdirectories of folder for compose files.
|
||||
func scanFolder(folder string) ScanResult {
|
||||
result := ScanResult{Created: []ServiceScanInfo{}, Updated: []ServiceScanInfo{}}
|
||||
entries, err := os.ReadDir(folder)
|
||||
if err != nil {
|
||||
result.Errors = []string{fmt.Sprintf("cannot read %s: %v", folder, err)}
|
||||
return result
|
||||
}
|
||||
for _, entry := range entries {
|
||||
if !entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
serviceDir := filepath.Join(folder, entry.Name())
|
||||
composeFile := findComposeFile(serviceDir)
|
||||
if composeFile == "" {
|
||||
continue
|
||||
}
|
||||
result.Found++
|
||||
info, err := processComposeFile(entry.Name(), composeFile)
|
||||
if err != nil {
|
||||
result.Errors = append(result.Errors, fmt.Sprintf("%s: %v", composeFile, err))
|
||||
continue
|
||||
}
|
||||
created, isNew := upsertService(info)
|
||||
if created {
|
||||
if isNew {
|
||||
result.Created = append(result.Created, info)
|
||||
} else {
|
||||
result.Updated = append(result.Updated, info)
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func findComposeFile(dir string) string {
|
||||
for _, name := range composeFileNames {
|
||||
p := filepath.Join(dir, name)
|
||||
if _, err := os.Stat(p); err == nil {
|
||||
return p
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// processComposeFile parses a compose file and produces a ServiceScanInfo.
|
||||
func processComposeFile(name, composeFile string) (ServiceScanInfo, error) {
|
||||
hostPaths, err := extractHostPaths(composeFile)
|
||||
if err != nil {
|
||||
return ServiceScanInfo{}, err
|
||||
}
|
||||
return ServiceScanInfo{
|
||||
Name: name,
|
||||
ComposeFile: composeFile,
|
||||
BackupFolder: deduceBackupFolder(hostPaths, name),
|
||||
Status: probeServiceStatus(composeFile),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// extractHostPaths parses all volume bind-mount host paths from a compose YAML file,
|
||||
// filtering out system/runtime paths that should not be backed up.
|
||||
// If yaml.v3 strict parsing fails (e.g. duplicate mapping keys in real-world compose
|
||||
// files), it falls back to a lenient text-based extraction.
|
||||
func extractHostPaths(composeFile string) ([]string, error) {
|
||||
data, err := os.ReadFile(composeFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var doc map[string]interface{}
|
||||
if err := yaml.Unmarshal(data, &doc); err != nil {
|
||||
// Malformed YAML (duplicate keys etc.) — fall back to text scan.
|
||||
return extractHostPathsFallback(data), nil
|
||||
}
|
||||
|
||||
servicesRaw, ok := doc["services"]
|
||||
if !ok {
|
||||
return nil, nil
|
||||
}
|
||||
services, _ := servicesRaw.(map[string]interface{})
|
||||
|
||||
seen := map[string]bool{}
|
||||
var paths []string
|
||||
|
||||
for _, svcRaw := range services {
|
||||
svc, ok := svcRaw.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
volumes, _ := svc["volumes"].([]interface{})
|
||||
for _, v := range volumes {
|
||||
var hostPath string
|
||||
switch vv := v.(type) {
|
||||
case string:
|
||||
hostPath = hostPathFromShortForm(vv)
|
||||
case map[string]interface{}:
|
||||
if t, _ := vv["type"].(string); t == "bind" {
|
||||
hostPath, _ = vv["source"].(string)
|
||||
}
|
||||
}
|
||||
hostPath = filepath.Clean(hostPath)
|
||||
if hostPath != "" && hostPath != "." && isBackupablePath(hostPath) && !seen[hostPath] {
|
||||
seen[hostPath] = true
|
||||
paths = append(paths, hostPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
return paths, nil
|
||||
}
|
||||
|
||||
// extractHostPathsFallback uses simple line scanning to extract volume bind-mounts
|
||||
// from compose files that fail strict YAML parsing (duplicate keys, etc.).
|
||||
// It looks for list items that contain an absolute host path followed by a colon.
|
||||
func extractHostPathsFallback(data []byte) []string {
|
||||
seen := map[string]bool{}
|
||||
var paths []string
|
||||
for _, line := range strings.Split(string(data), "\n") {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
// Volume list entries: "- /abs/path:/container/path" (with or without quotes)
|
||||
if !strings.HasPrefix(trimmed, "- ") {
|
||||
continue
|
||||
}
|
||||
spec := strings.Trim(strings.TrimPrefix(trimmed, "- "), "\"' ")
|
||||
if !strings.HasPrefix(spec, "/") {
|
||||
continue
|
||||
}
|
||||
hostPath := hostPathFromShortForm(spec)
|
||||
if hostPath == "" {
|
||||
continue
|
||||
}
|
||||
hostPath = filepath.Clean(hostPath)
|
||||
if isBackupablePath(hostPath) && !seen[hostPath] {
|
||||
seen[hostPath] = true
|
||||
paths = append(paths, hostPath)
|
||||
}
|
||||
}
|
||||
return paths
|
||||
}
|
||||
|
||||
// hostPathFromShortForm extracts the host path from "host:container[:opts]" volume spec.
|
||||
func hostPathFromShortForm(spec string) string {
|
||||
parts := strings.SplitN(spec, ":", 3)
|
||||
if len(parts) < 2 {
|
||||
return ""
|
||||
}
|
||||
src := parts[0]
|
||||
if !filepath.IsAbs(src) {
|
||||
return "" // named volume or relative path
|
||||
}
|
||||
return src
|
||||
}
|
||||
|
||||
// isBackupablePath returns false for system/runtime paths that should not be backed up.
|
||||
func isBackupablePath(path string) bool {
|
||||
for _, exact := range excludedExact {
|
||||
if path == exact {
|
||||
return false
|
||||
}
|
||||
}
|
||||
for _, prefix := range excludedPrefixes {
|
||||
if strings.HasPrefix(path, prefix) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// deduceBackupFolder finds the most appropriate backup root for a service.
|
||||
//
|
||||
// Priority:
|
||||
// 1. /opt/data/containers/<serviceName> — standard layout in the examples
|
||||
// 2. Longest common directory prefix across all collected host paths
|
||||
// 3. First collected path as-is
|
||||
func deduceBackupFolder(hostPaths []string, serviceName string) string {
|
||||
if len(hostPaths) == 0 {
|
||||
return ""
|
||||
}
|
||||
// 1. Standard layout
|
||||
standard := "/opt/data/containers/" + serviceName
|
||||
for _, p := range hostPaths {
|
||||
if strings.HasPrefix(p, standard) {
|
||||
return standard
|
||||
}
|
||||
}
|
||||
// 2. Common prefix
|
||||
prefix := commonPathPrefix(hostPaths)
|
||||
if prefix != "" && prefix != "/" {
|
||||
return prefix
|
||||
}
|
||||
// 3. Fallback
|
||||
return hostPaths[0]
|
||||
}
|
||||
|
||||
// commonPathPrefix returns the longest common directory prefix shared by all paths.
|
||||
func commonPathPrefix(paths []string) string {
|
||||
if len(paths) == 0 {
|
||||
return ""
|
||||
}
|
||||
if len(paths) == 1 {
|
||||
return paths[0]
|
||||
}
|
||||
sep := string(filepath.Separator)
|
||||
parts := strings.Split(paths[0], sep)
|
||||
for _, p := range paths[1:] {
|
||||
other := strings.Split(p, sep)
|
||||
limit := len(parts)
|
||||
if len(other) < limit {
|
||||
limit = len(other)
|
||||
}
|
||||
i := 0
|
||||
for i < limit && parts[i] == other[i] {
|
||||
i++
|
||||
}
|
||||
parts = parts[:i]
|
||||
}
|
||||
if len(parts) <= 1 {
|
||||
return "/"
|
||||
}
|
||||
return strings.Join(parts, sep)
|
||||
}
|
||||
|
||||
// probeServiceStatus checks whether the service has any running containers.
|
||||
// Uses the docker project label set by compose, then falls back to compose CLI.
|
||||
func probeServiceStatus(composeFile string) string {
|
||||
projectName := strings.ToLower(filepath.Base(filepath.Dir(composeFile)))
|
||||
|
||||
// Method 1: docker ps with project label (no compose binary required)
|
||||
cmd := exec.Command("docker", "ps", "-q",
|
||||
"--filter", "label=com.docker.compose.project="+projectName)
|
||||
if out, err := cmd.Output(); err == nil {
|
||||
if strings.TrimSpace(string(out)) != "" {
|
||||
return "up"
|
||||
}
|
||||
return "down"
|
||||
}
|
||||
|
||||
// Method 2: docker compose ps
|
||||
cmd = exec.Command("docker", "compose", "-f", composeFile, "ps", "-q")
|
||||
if out, err := cmd.Output(); err == nil {
|
||||
if strings.TrimSpace(string(out)) != "" {
|
||||
return "up"
|
||||
}
|
||||
return "down"
|
||||
}
|
||||
|
||||
// Method 3: legacy docker-compose
|
||||
cmd = exec.Command("docker-compose", "-f", composeFile, "ps", "-q")
|
||||
if out, err := cmd.Output(); err == nil {
|
||||
if strings.TrimSpace(string(out)) != "" {
|
||||
return "up"
|
||||
}
|
||||
return "down"
|
||||
}
|
||||
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
// upsertService creates or updates a service entry indexed by compose file path.
|
||||
// Returns (true, true) = created; (true, false) = updated; (false, _) = no change.
|
||||
func upsertService(info ServiceScanInfo) (bool, bool) {
|
||||
services := GetAllServices()
|
||||
for _, svc := range services {
|
||||
if svc.ComposeFile != info.ComposeFile {
|
||||
continue
|
||||
}
|
||||
// Found — update status and backup folder if previously unset
|
||||
changed := svc.Status != info.Status
|
||||
if svc.BackupFolder == "" && info.BackupFolder != "" {
|
||||
svc.BackupFolder = info.BackupFolder
|
||||
changed = true
|
||||
}
|
||||
svc.Status = info.Status
|
||||
if changed {
|
||||
_, _ = UpdateService(svc.ID, svc)
|
||||
return true, false
|
||||
}
|
||||
return false, false
|
||||
}
|
||||
// Not found — create
|
||||
_, _ = AddService(Service{
|
||||
Name: info.Name,
|
||||
ComposeFile: info.ComposeFile,
|
||||
BackupFolder: info.BackupFolder,
|
||||
Status: info.Status,
|
||||
ComposeType: "docker compose",
|
||||
})
|
||||
return true, true
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"log"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/robfig/cron/v3"
|
||||
)
|
||||
|
||||
var (
|
||||
schedMu sync.Mutex
|
||||
schedCron *cron.Cron
|
||||
entryTarget = map[cron.EntryID]targetRef{}
|
||||
)
|
||||
|
||||
type targetRef struct {
|
||||
scope string // "node" or "svc:<serviceID>"
|
||||
targetID string
|
||||
}
|
||||
|
||||
// StartScheduler starts the in-process cron scheduler and performs an initial
|
||||
// sync from the current configuration. Call once from main.go before
|
||||
// beego.Run() — not from the scan/info CLI paths.
|
||||
func StartScheduler() {
|
||||
schedMu.Lock()
|
||||
schedCron = cron.New(cron.WithChain(cron.Recover(cron.DefaultLogger)))
|
||||
schedCron.Start()
|
||||
schedMu.Unlock()
|
||||
SyncSchedule()
|
||||
}
|
||||
|
||||
// SyncSchedule rebuilds all cron entries from the current backup target
|
||||
// definitions (node + every service). Called once at startup and after any
|
||||
// target add/update/delete so the running scheduler reflects the latest
|
||||
// definitions.
|
||||
func SyncSchedule() {
|
||||
schedMu.Lock()
|
||||
defer schedMu.Unlock()
|
||||
if schedCron == nil {
|
||||
return
|
||||
}
|
||||
for _, e := range schedCron.Entries() {
|
||||
schedCron.Remove(e.ID)
|
||||
}
|
||||
entryTarget = map[cron.EntryID]targetRef{}
|
||||
|
||||
mu.RLock()
|
||||
nodeTargets := make([]BackupTarget, len(cfg.Node.Backup.Targets))
|
||||
copy(nodeTargets, cfg.Node.Backup.Targets)
|
||||
services := make([]Service, len(cfg.Services))
|
||||
copy(services, cfg.Services)
|
||||
mu.RUnlock()
|
||||
|
||||
for _, t := range nodeTargets {
|
||||
registerJob("node", t)
|
||||
}
|
||||
for _, s := range services {
|
||||
if s.Backup == nil {
|
||||
continue
|
||||
}
|
||||
for _, t := range s.Backup.Targets {
|
||||
registerJob("svc:"+s.ID, t)
|
||||
}
|
||||
}
|
||||
refreshNextRunLocked()
|
||||
}
|
||||
|
||||
// registerJob schedules a single target's cron job. Caller must hold schedMu.
|
||||
func registerJob(scope string, t BackupTarget) {
|
||||
if t.Schedule == "" {
|
||||
return
|
||||
}
|
||||
schedule, err := cron.ParseStandard(t.Schedule)
|
||||
if err != nil {
|
||||
// Already validated at CRUD time; defensively skip a bad schedule
|
||||
// rather than let it wedge the whole sync.
|
||||
return
|
||||
}
|
||||
targetID := t.ID
|
||||
job := cron.NewChain(cron.SkipIfStillRunning(cron.DefaultLogger)).Then(cron.FuncJob(func() {
|
||||
runScheduledTarget(scope, targetID)
|
||||
}))
|
||||
id := schedCron.Schedule(schedule, job)
|
||||
entryTarget[id] = targetRef{scope: scope, targetID: targetID}
|
||||
}
|
||||
|
||||
func runScheduledTarget(scope, targetID string) {
|
||||
var err error
|
||||
switch {
|
||||
case scope == "node":
|
||||
err = RunNodeBackup([]string{targetID})
|
||||
case strings.HasPrefix(scope, "svc:"):
|
||||
err = RunServiceBackup(strings.TrimPrefix(scope, "svc:"), []string{targetID})
|
||||
}
|
||||
if err != nil {
|
||||
log.Printf("nodemaster: scheduled backup %s/%s failed: %v", scope, targetID, err)
|
||||
}
|
||||
schedMu.Lock()
|
||||
refreshNextRunLocked()
|
||||
schedMu.Unlock()
|
||||
}
|
||||
|
||||
// refreshNextRunLocked writes each scheduled target's NextRun from the cron
|
||||
// entries' computed next-fire time. Caller must hold schedMu.
|
||||
func refreshNextRunLocked() {
|
||||
if schedCron == nil {
|
||||
return
|
||||
}
|
||||
updates := map[targetRef]time.Time{}
|
||||
for _, e := range schedCron.Entries() {
|
||||
if ref, ok := entryTarget[e.ID]; ok {
|
||||
updates[ref] = e.Next
|
||||
}
|
||||
}
|
||||
if len(updates) == 0 {
|
||||
return
|
||||
}
|
||||
mu.Lock()
|
||||
for ref, next := range updates {
|
||||
n := next
|
||||
if ref.scope == "node" {
|
||||
for i, t := range cfg.Node.Backup.Targets {
|
||||
if t.ID == ref.targetID {
|
||||
cfg.Node.Backup.Targets[i].NextRun = &n
|
||||
break
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
svcID := strings.TrimPrefix(ref.scope, "svc:")
|
||||
for i, s := range cfg.Services {
|
||||
if s.ID != svcID || s.Backup == nil {
|
||||
continue
|
||||
}
|
||||
for j, t := range s.Backup.Targets {
|
||||
if t.ID == ref.targetID {
|
||||
cfg.Services[i].Backup.Targets[j].NextRun = &n
|
||||
break
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
_ = persist()
|
||||
mu.Unlock()
|
||||
}
|
||||
@@ -0,0 +1,316 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
func GetAllServices() []Service {
|
||||
mu.RLock()
|
||||
defer mu.RUnlock()
|
||||
result := make([]Service, len(cfg.Services))
|
||||
copy(result, cfg.Services)
|
||||
return result
|
||||
}
|
||||
|
||||
func GetService(id string) (Service, error) {
|
||||
mu.RLock()
|
||||
defer mu.RUnlock()
|
||||
for _, s := range cfg.Services {
|
||||
if s.ID == id {
|
||||
return s, nil
|
||||
}
|
||||
}
|
||||
return Service{}, errors.New("service not found")
|
||||
}
|
||||
|
||||
func AddService(s Service) (Service, error) {
|
||||
mu.Lock()
|
||||
s.ID = fmt.Sprintf("svc_%d", time.Now().UnixNano())
|
||||
if s.Backup != nil {
|
||||
if s.Backup.Targets == nil {
|
||||
s.Backup.Targets = []BackupTarget{}
|
||||
}
|
||||
if s.Backup.History == nil {
|
||||
s.Backup.History = []BackupExecution{}
|
||||
}
|
||||
}
|
||||
cfg.Services = append(cfg.Services, s)
|
||||
err := persist()
|
||||
mu.Unlock()
|
||||
if err != nil {
|
||||
return Service{}, err
|
||||
}
|
||||
SyncSchedule()
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func UpdateService(id string, s Service) (Service, error) {
|
||||
mu.Lock()
|
||||
found := false
|
||||
for i, svc := range cfg.Services {
|
||||
if svc.ID == id {
|
||||
s.ID = id
|
||||
if s.Backup != nil && svc.Backup != nil {
|
||||
s.Backup.History = svc.Backup.History
|
||||
}
|
||||
cfg.Services[i] = s
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
mu.Unlock()
|
||||
return Service{}, errors.New("service not found")
|
||||
}
|
||||
err := persist()
|
||||
mu.Unlock()
|
||||
if err != nil {
|
||||
return Service{}, err
|
||||
}
|
||||
SyncSchedule()
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func DeleteService(id string) error {
|
||||
mu.Lock()
|
||||
found := false
|
||||
for i, s := range cfg.Services {
|
||||
if s.ID == id {
|
||||
cfg.Services = append(cfg.Services[:i], cfg.Services[i+1:]...)
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
mu.Unlock()
|
||||
return errors.New("service not found")
|
||||
}
|
||||
err := persist()
|
||||
mu.Unlock()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
SyncSchedule()
|
||||
return nil
|
||||
}
|
||||
|
||||
func UpdateServiceStatus(id, status string) error {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
for i, s := range cfg.Services {
|
||||
if s.ID == id {
|
||||
cfg.Services[i].Status = status
|
||||
return persist()
|
||||
}
|
||||
}
|
||||
return errors.New("service not found")
|
||||
}
|
||||
|
||||
func AppendServiceBackupExecution(id string, ex BackupExecution) error {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
for i, s := range cfg.Services {
|
||||
if s.ID == id {
|
||||
if cfg.Services[i].Backup == nil {
|
||||
cfg.Services[i].Backup = &BackupConfig{
|
||||
Targets: []BackupTarget{},
|
||||
History: []BackupExecution{},
|
||||
}
|
||||
}
|
||||
cfg.Services[i].Backup.History = append(cfg.Services[i].Backup.History, ex)
|
||||
now := time.Now().UTC()
|
||||
cfg.Services[i].Backup.LastRun = &now
|
||||
for j, t := range cfg.Services[i].Backup.Targets {
|
||||
if t.ID == ex.TargetID {
|
||||
cfg.Services[i].Backup.Targets[j].LastRun = &now
|
||||
break
|
||||
}
|
||||
}
|
||||
return persist()
|
||||
}
|
||||
}
|
||||
return errors.New("service not found")
|
||||
}
|
||||
|
||||
// GetServiceBackupTargets lists a service's individual backup target definitions.
|
||||
func GetServiceBackupTargets(id string) ([]BackupTarget, error) {
|
||||
mu.RLock()
|
||||
defer mu.RUnlock()
|
||||
for _, s := range cfg.Services {
|
||||
if s.ID == id {
|
||||
if s.Backup == nil {
|
||||
return []BackupTarget{}, nil
|
||||
}
|
||||
result := make([]BackupTarget, len(s.Backup.Targets))
|
||||
copy(result, s.Backup.Targets)
|
||||
return result, nil
|
||||
}
|
||||
}
|
||||
return nil, errors.New("service not found")
|
||||
}
|
||||
|
||||
// GetServiceBackupTarget returns a single backup target by ID for a service.
|
||||
func GetServiceBackupTarget(id, tid string) (BackupTarget, error) {
|
||||
mu.RLock()
|
||||
defer mu.RUnlock()
|
||||
for _, s := range cfg.Services {
|
||||
if s.ID == id {
|
||||
if s.Backup == nil {
|
||||
return BackupTarget{}, errors.New("backup target not found")
|
||||
}
|
||||
for _, t := range s.Backup.Targets {
|
||||
if t.ID == tid {
|
||||
return t, nil
|
||||
}
|
||||
}
|
||||
return BackupTarget{}, errors.New("backup target not found")
|
||||
}
|
||||
}
|
||||
return BackupTarget{}, errors.New("service not found")
|
||||
}
|
||||
|
||||
// AddServiceBackupTarget creates a new backup target definition for a service.
|
||||
func AddServiceBackupTarget(id string, t BackupTarget) (BackupTarget, error) {
|
||||
if err := validateBackupTarget(t); err != nil {
|
||||
return BackupTarget{}, err
|
||||
}
|
||||
mu.Lock()
|
||||
idx := -1
|
||||
for i, s := range cfg.Services {
|
||||
if s.ID == id {
|
||||
idx = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if idx == -1 {
|
||||
mu.Unlock()
|
||||
return BackupTarget{}, errors.New("service not found")
|
||||
}
|
||||
if cfg.Services[idx].Backup == nil {
|
||||
cfg.Services[idx].Backup = &BackupConfig{
|
||||
Targets: []BackupTarget{},
|
||||
History: []BackupExecution{},
|
||||
}
|
||||
}
|
||||
t.ID = fmt.Sprintf("bt_%d", time.Now().UnixNano())
|
||||
cfg.Services[idx].Backup.Targets = append(cfg.Services[idx].Backup.Targets, t)
|
||||
err := persist()
|
||||
mu.Unlock()
|
||||
if err != nil {
|
||||
return BackupTarget{}, err
|
||||
}
|
||||
SyncSchedule()
|
||||
return t, nil
|
||||
}
|
||||
|
||||
// UpdateServiceBackupTarget replaces an existing backup target definition for a service.
|
||||
func UpdateServiceBackupTarget(id, tid string, t BackupTarget) (BackupTarget, error) {
|
||||
if err := validateBackupTarget(t); err != nil {
|
||||
return BackupTarget{}, err
|
||||
}
|
||||
mu.Lock()
|
||||
svcIdx := -1
|
||||
for i, s := range cfg.Services {
|
||||
if s.ID == id {
|
||||
svcIdx = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if svcIdx == -1 || cfg.Services[svcIdx].Backup == nil {
|
||||
mu.Unlock()
|
||||
return BackupTarget{}, errors.New("service not found")
|
||||
}
|
||||
found := false
|
||||
for i, existing := range cfg.Services[svcIdx].Backup.Targets {
|
||||
if existing.ID == tid {
|
||||
t.ID = tid
|
||||
t.LastRun = existing.LastRun
|
||||
cfg.Services[svcIdx].Backup.Targets[i] = t
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
mu.Unlock()
|
||||
return BackupTarget{}, errors.New("backup target not found")
|
||||
}
|
||||
err := persist()
|
||||
mu.Unlock()
|
||||
if err != nil {
|
||||
return BackupTarget{}, err
|
||||
}
|
||||
SyncSchedule()
|
||||
return t, nil
|
||||
}
|
||||
|
||||
// DeleteServiceBackupTarget removes a backup target definition from a service.
|
||||
func DeleteServiceBackupTarget(id, tid string) error {
|
||||
mu.Lock()
|
||||
svcIdx := -1
|
||||
for i, s := range cfg.Services {
|
||||
if s.ID == id {
|
||||
svcIdx = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if svcIdx == -1 || cfg.Services[svcIdx].Backup == nil {
|
||||
mu.Unlock()
|
||||
return errors.New("service not found")
|
||||
}
|
||||
found := false
|
||||
for i, t := range cfg.Services[svcIdx].Backup.Targets {
|
||||
if t.ID == tid {
|
||||
cfg.Services[svcIdx].Backup.Targets = append(
|
||||
cfg.Services[svcIdx].Backup.Targets[:i],
|
||||
cfg.Services[svcIdx].Backup.Targets[i+1:]...,
|
||||
)
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
mu.Unlock()
|
||||
return errors.New("backup target not found")
|
||||
}
|
||||
err := persist()
|
||||
mu.Unlock()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
SyncSchedule()
|
||||
return nil
|
||||
}
|
||||
|
||||
// RunServiceBackup runs a service's backup for the given target IDs (nil/empty
|
||||
// means all targets), honoring StopOnBackup, and appends an execution record
|
||||
// per target.
|
||||
func RunServiceBackup(id string, targetIDs []string) error {
|
||||
svc, err := GetService(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if svc.Backup == nil || len(svc.Backup.Targets) == 0 {
|
||||
return errors.New("no backup targets configured for this service")
|
||||
}
|
||||
targets := filterTargets(svc.Backup.Targets, targetIDs)
|
||||
if len(targets) == 0 {
|
||||
return errors.New("no matching backup targets")
|
||||
}
|
||||
sourcePath := svc.BackupFolder
|
||||
if svc.Backup.SourcePath != "" {
|
||||
sourcePath = svc.Backup.SourcePath
|
||||
}
|
||||
if svc.StopOnBackup {
|
||||
_ = StopService(svc)
|
||||
}
|
||||
results := RunBackup("svc:"+id, targets, sourcePath)
|
||||
for _, ex := range results {
|
||||
_ = AppendServiceBackupExecution(id, ex)
|
||||
}
|
||||
if svc.StopOnBackup {
|
||||
_ = StartService(svc)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var (
|
||||
mu sync.RWMutex
|
||||
cfg *AppConfig
|
||||
cfgPath string
|
||||
)
|
||||
|
||||
func init() {
|
||||
cfgPath = resolveCfgPath()
|
||||
hostname, _ := os.Hostname()
|
||||
cfg = &AppConfig{
|
||||
Node: NodeInfo{
|
||||
Hostname: hostname,
|
||||
UpdateHistory: []UpdateRecord{},
|
||||
Backup: BackupConfig{
|
||||
Targets: []BackupTarget{},
|
||||
History: []BackupExecution{},
|
||||
},
|
||||
},
|
||||
Nodes: []RemoteNode{},
|
||||
Services: []Service{},
|
||||
}
|
||||
if data, err := os.ReadFile(cfgPath); err == nil {
|
||||
_ = json.Unmarshal(data, cfg)
|
||||
}
|
||||
ensureSlices()
|
||||
}
|
||||
|
||||
func ensureSlices() {
|
||||
if cfg.Node.UpdateHistory == nil {
|
||||
cfg.Node.UpdateHistory = []UpdateRecord{}
|
||||
}
|
||||
if cfg.Node.Backup.Targets == nil {
|
||||
cfg.Node.Backup.Targets = []BackupTarget{}
|
||||
}
|
||||
if cfg.Node.Backup.History == nil {
|
||||
cfg.Node.Backup.History = []BackupExecution{}
|
||||
}
|
||||
if cfg.Nodes == nil {
|
||||
cfg.Nodes = []RemoteNode{}
|
||||
}
|
||||
if cfg.Services == nil {
|
||||
cfg.Services = []Service{}
|
||||
}
|
||||
}
|
||||
|
||||
func resolveCfgPath() string {
|
||||
const etcPath = "/etc/nodemaster/nodemaster.conf"
|
||||
if err := os.MkdirAll(filepath.Dir(etcPath), 0755); err == nil {
|
||||
if f, err := os.OpenFile(etcPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600); err == nil {
|
||||
f.Close()
|
||||
return etcPath
|
||||
}
|
||||
}
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
home = "."
|
||||
}
|
||||
return filepath.Join(home, ".nodemaster.conf")
|
||||
}
|
||||
|
||||
func persist() error {
|
||||
data, err := json.MarshalIndent(cfg, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(cfgPath), 0755); err != nil {
|
||||
return fmt.Errorf("mkdir: %w", err)
|
||||
}
|
||||
// 0600: the config now embeds the restic repository password, so it must
|
||||
// not be group/world-readable. Chmod covers pre-existing files that were
|
||||
// created before this field existed (WriteFile's mode only applies on
|
||||
// create, not to files that already exist with looser permissions).
|
||||
if err := os.WriteFile(cfgPath, data, 0600); err != nil {
|
||||
return err
|
||||
}
|
||||
_ = os.Chmod(cfgPath, 0600)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ConfigPath returns the resolved config file path.
|
||||
func ConfigPath() string {
|
||||
return cfgPath
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
type BackupMethod string
|
||||
|
||||
const (
|
||||
MethodRsync BackupMethod = "rsync"
|
||||
MethodRestic BackupMethod = "restic"
|
||||
MethodExternal BackupMethod = "external"
|
||||
)
|
||||
|
||||
type BackupTarget struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Method BackupMethod `json:"method"`
|
||||
Remote string `json:"remote"` // destination: rsync target path (e.g. "user@host:/path") or restic repository string (e.g. "/mnt/backup/repo", "s3:s3.amazonaws.com/bucket/path", "sftp:user@host:/path")
|
||||
Params map[string]string `json:"params,omitempty"`
|
||||
Schedule string `json:"schedule,omitempty"` // cron expression (5-field, standard). Empty = manual-only.
|
||||
LastRun *time.Time `json:"last_run,omitempty"`
|
||||
NextRun *time.Time `json:"next_run,omitempty"`
|
||||
}
|
||||
|
||||
type BackupExecution struct {
|
||||
ID string `json:"id"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
Success bool `json:"success"`
|
||||
Duration int64 `json:"duration_seconds"`
|
||||
Message string `json:"message"`
|
||||
TargetID string `json:"target_id,omitempty"`
|
||||
}
|
||||
|
||||
type BackupConfig struct {
|
||||
SourcePath string `json:"source_path,omitempty"`
|
||||
Targets []BackupTarget `json:"targets"`
|
||||
NextRun *time.Time `json:"next_run,omitempty"` // aggregate: earliest NextRun across Targets
|
||||
LastRun *time.Time `json:"last_run,omitempty"` // aggregate: latest LastRun across Targets
|
||||
History []BackupExecution `json:"history"`
|
||||
}
|
||||
|
||||
type UpdateRecord struct {
|
||||
ID string `json:"id"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
Success bool `json:"success"`
|
||||
Packages []string `json:"packages,omitempty"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
type NodeInfo struct {
|
||||
Hostname string `json:"hostname"`
|
||||
Description string `json:"description,omitempty"`
|
||||
UpdateHistory []UpdateRecord `json:"update_history"`
|
||||
Backup BackupConfig `json:"backup"`
|
||||
ResticPassword string `json:"restic_password,omitempty"` // repository password used to encrypt/decrypt all restic backup targets on this node
|
||||
}
|
||||
|
||||
type RemoteNode struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
URL string `json:"url"`
|
||||
Status string `json:"status"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Tags []string `json:"tags,omitempty"`
|
||||
LastSeen *time.Time `json:"last_seen,omitempty"`
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
ComposeFile string `json:"compose_file"`
|
||||
ComposeType string `json:"compose_type,omitempty"`
|
||||
BackupFolder string `json:"backup_folder,omitempty"`
|
||||
Status string `json:"status,omitempty"` // "up", "down", "unknown"
|
||||
StopOnBackup bool `json:"stop_on_backup,omitempty"`
|
||||
ExternalBackupStopMinutes int `json:"external_backup_stop_minutes,omitempty"`
|
||||
ExternalBackupStartMinutes int `json:"external_backup_start_minutes,omitempty"`
|
||||
Backup *BackupConfig `json:"backup,omitempty"`
|
||||
}
|
||||
|
||||
type ScanResult struct {
|
||||
ScannedPaths []string `json:"scanned_paths"`
|
||||
Found int `json:"found"`
|
||||
Created []ServiceScanInfo `json:"created"`
|
||||
Updated []ServiceScanInfo `json:"updated"`
|
||||
Errors []string `json:"errors,omitempty"`
|
||||
}
|
||||
|
||||
type ServiceScanInfo struct {
|
||||
Name string `json:"name"`
|
||||
ComposeFile string `json:"compose_file"`
|
||||
BackupFolder string `json:"backup_folder,omitempty"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
type AppConfig struct {
|
||||
Node NodeInfo `json:"node"`
|
||||
Nodes []RemoteNode `json:"nodes"`
|
||||
Services []Service `json:"services"`
|
||||
}
|
||||
@@ -0,0 +1,370 @@
|
||||
package routers
|
||||
|
||||
import (
|
||||
beego "github.com/beego/beego/v2/server/web"
|
||||
"github.com/beego/beego/v2/server/web/context/param"
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
||||
beego.GlobalControllerRouter["nodemaster/controllers:NodeController"] = append(beego.GlobalControllerRouter["nodemaster/controllers:NodeController"],
|
||||
beego.ControllerComments{
|
||||
Method: "Put",
|
||||
Router: `/`,
|
||||
AllowHTTPMethods: []string{"put"},
|
||||
MethodParams: param.Make(),
|
||||
Filters: nil,
|
||||
Params: nil})
|
||||
|
||||
beego.GlobalControllerRouter["nodemaster/controllers:NodeController"] = append(beego.GlobalControllerRouter["nodemaster/controllers:NodeController"],
|
||||
beego.ControllerComments{
|
||||
Method: "Get",
|
||||
Router: `/`,
|
||||
AllowHTTPMethods: []string{"get"},
|
||||
MethodParams: param.Make(),
|
||||
Filters: nil,
|
||||
Params: nil})
|
||||
|
||||
beego.GlobalControllerRouter["nodemaster/controllers:NodeController"] = append(beego.GlobalControllerRouter["nodemaster/controllers:NodeController"],
|
||||
beego.ControllerComments{
|
||||
Method: "GetBackup",
|
||||
Router: `/backup`,
|
||||
AllowHTTPMethods: []string{"get"},
|
||||
MethodParams: param.Make(),
|
||||
Filters: nil,
|
||||
Params: nil})
|
||||
|
||||
beego.GlobalControllerRouter["nodemaster/controllers:NodeController"] = append(beego.GlobalControllerRouter["nodemaster/controllers:NodeController"],
|
||||
beego.ControllerComments{
|
||||
Method: "PutBackup",
|
||||
Router: `/backup`,
|
||||
AllowHTTPMethods: []string{"put"},
|
||||
MethodParams: param.Make(),
|
||||
Filters: nil,
|
||||
Params: nil})
|
||||
|
||||
beego.GlobalControllerRouter["nodemaster/controllers:NodeController"] = append(beego.GlobalControllerRouter["nodemaster/controllers:NodeController"],
|
||||
beego.ControllerComments{
|
||||
Method: "GetBackupProgress",
|
||||
Router: `/backup/progress`,
|
||||
AllowHTTPMethods: []string{"get"},
|
||||
MethodParams: param.Make(),
|
||||
Filters: nil,
|
||||
Params: nil})
|
||||
|
||||
beego.GlobalControllerRouter["nodemaster/controllers:NodeController"] = append(beego.GlobalControllerRouter["nodemaster/controllers:NodeController"],
|
||||
beego.ControllerComments{
|
||||
Method: "RunBackup",
|
||||
Router: `/backup/run`,
|
||||
AllowHTTPMethods: []string{"post"},
|
||||
MethodParams: param.Make(),
|
||||
Filters: nil,
|
||||
Params: nil})
|
||||
|
||||
beego.GlobalControllerRouter["nodemaster/controllers:NodeController"] = append(beego.GlobalControllerRouter["nodemaster/controllers:NodeController"],
|
||||
beego.ControllerComments{
|
||||
Method: "GetBackupTargets",
|
||||
Router: `/backup/targets`,
|
||||
AllowHTTPMethods: []string{"get"},
|
||||
MethodParams: param.Make(),
|
||||
Filters: nil,
|
||||
Params: nil})
|
||||
|
||||
beego.GlobalControllerRouter["nodemaster/controllers:NodeController"] = append(beego.GlobalControllerRouter["nodemaster/controllers:NodeController"],
|
||||
beego.ControllerComments{
|
||||
Method: "AddBackupTarget",
|
||||
Router: `/backup/targets`,
|
||||
AllowHTTPMethods: []string{"post"},
|
||||
MethodParams: param.Make(),
|
||||
Filters: nil,
|
||||
Params: nil})
|
||||
|
||||
beego.GlobalControllerRouter["nodemaster/controllers:NodeController"] = append(beego.GlobalControllerRouter["nodemaster/controllers:NodeController"],
|
||||
beego.ControllerComments{
|
||||
Method: "DeleteBackupTarget",
|
||||
Router: `/backup/targets/:tid`,
|
||||
AllowHTTPMethods: []string{"delete"},
|
||||
MethodParams: param.Make(),
|
||||
Filters: nil,
|
||||
Params: nil})
|
||||
|
||||
beego.GlobalControllerRouter["nodemaster/controllers:NodeController"] = append(beego.GlobalControllerRouter["nodemaster/controllers:NodeController"],
|
||||
beego.ControllerComments{
|
||||
Method: "PutBackupTarget",
|
||||
Router: `/backup/targets/:tid`,
|
||||
AllowHTTPMethods: []string{"put"},
|
||||
MethodParams: param.Make(),
|
||||
Filters: nil,
|
||||
Params: nil})
|
||||
|
||||
beego.GlobalControllerRouter["nodemaster/controllers:NodeController"] = append(beego.GlobalControllerRouter["nodemaster/controllers:NodeController"],
|
||||
beego.ControllerComments{
|
||||
Method: "GetBackupTarget",
|
||||
Router: `/backup/targets/:tid`,
|
||||
AllowHTTPMethods: []string{"get"},
|
||||
MethodParams: param.Make(),
|
||||
Filters: nil,
|
||||
Params: nil})
|
||||
|
||||
beego.GlobalControllerRouter["nodemaster/controllers:NodeController"] = append(beego.GlobalControllerRouter["nodemaster/controllers:NodeController"],
|
||||
beego.ControllerComments{
|
||||
Method: "GetBackupTargetProgress",
|
||||
Router: `/backup/targets/:tid/progress`,
|
||||
AllowHTTPMethods: []string{"get"},
|
||||
MethodParams: param.Make(),
|
||||
Filters: nil,
|
||||
Params: nil})
|
||||
|
||||
beego.GlobalControllerRouter["nodemaster/controllers:NodeController"] = append(beego.GlobalControllerRouter["nodemaster/controllers:NodeController"],
|
||||
beego.ControllerComments{
|
||||
Method: "RunBackupTarget",
|
||||
Router: `/backup/targets/:tid/run`,
|
||||
AllowHTTPMethods: []string{"post"},
|
||||
MethodParams: param.Make(),
|
||||
Filters: nil,
|
||||
Params: nil})
|
||||
|
||||
beego.GlobalControllerRouter["nodemaster/controllers:NodeController"] = append(beego.GlobalControllerRouter["nodemaster/controllers:NodeController"],
|
||||
beego.ControllerComments{
|
||||
Method: "Scan",
|
||||
Router: `/scan`,
|
||||
AllowHTTPMethods: []string{"post"},
|
||||
MethodParams: param.Make(),
|
||||
Filters: nil,
|
||||
Params: nil})
|
||||
|
||||
beego.GlobalControllerRouter["nodemaster/controllers:NodeController"] = append(beego.GlobalControllerRouter["nodemaster/controllers:NodeController"],
|
||||
beego.ControllerComments{
|
||||
Method: "AddUpdate",
|
||||
Router: `/updates`,
|
||||
AllowHTTPMethods: []string{"post"},
|
||||
MethodParams: param.Make(),
|
||||
Filters: nil,
|
||||
Params: nil})
|
||||
|
||||
beego.GlobalControllerRouter["nodemaster/controllers:NodeController"] = append(beego.GlobalControllerRouter["nodemaster/controllers:NodeController"],
|
||||
beego.ControllerComments{
|
||||
Method: "GetUpdates",
|
||||
Router: `/updates`,
|
||||
AllowHTTPMethods: []string{"get"},
|
||||
MethodParams: param.Make(),
|
||||
Filters: nil,
|
||||
Params: nil})
|
||||
|
||||
beego.GlobalControllerRouter["nodemaster/controllers:NodeController"] = append(beego.GlobalControllerRouter["nodemaster/controllers:NodeController"],
|
||||
beego.ControllerComments{
|
||||
Method: "DeleteUpdate",
|
||||
Router: `/updates/:id`,
|
||||
AllowHTTPMethods: []string{"delete"},
|
||||
MethodParams: param.Make(),
|
||||
Filters: nil,
|
||||
Params: nil})
|
||||
|
||||
beego.GlobalControllerRouter["nodemaster/controllers:NodeController"] = append(beego.GlobalControllerRouter["nodemaster/controllers:NodeController"],
|
||||
beego.ControllerComments{
|
||||
Method: "GetUpdate",
|
||||
Router: `/updates/:id`,
|
||||
AllowHTTPMethods: []string{"get"},
|
||||
MethodParams: param.Make(),
|
||||
Filters: nil,
|
||||
Params: nil})
|
||||
|
||||
beego.GlobalControllerRouter["nodemaster/controllers:NodesController"] = append(beego.GlobalControllerRouter["nodemaster/controllers:NodesController"],
|
||||
beego.ControllerComments{
|
||||
Method: "GetAll",
|
||||
Router: `/`,
|
||||
AllowHTTPMethods: []string{"get"},
|
||||
MethodParams: param.Make(),
|
||||
Filters: nil,
|
||||
Params: nil})
|
||||
|
||||
beego.GlobalControllerRouter["nodemaster/controllers:NodesController"] = append(beego.GlobalControllerRouter["nodemaster/controllers:NodesController"],
|
||||
beego.ControllerComments{
|
||||
Method: "Post",
|
||||
Router: `/`,
|
||||
AllowHTTPMethods: []string{"post"},
|
||||
MethodParams: param.Make(),
|
||||
Filters: nil,
|
||||
Params: nil})
|
||||
|
||||
beego.GlobalControllerRouter["nodemaster/controllers:NodesController"] = append(beego.GlobalControllerRouter["nodemaster/controllers:NodesController"],
|
||||
beego.ControllerComments{
|
||||
Method: "Get",
|
||||
Router: `/:id`,
|
||||
AllowHTTPMethods: []string{"get"},
|
||||
MethodParams: param.Make(),
|
||||
Filters: nil,
|
||||
Params: nil})
|
||||
|
||||
beego.GlobalControllerRouter["nodemaster/controllers:NodesController"] = append(beego.GlobalControllerRouter["nodemaster/controllers:NodesController"],
|
||||
beego.ControllerComments{
|
||||
Method: "Put",
|
||||
Router: `/:id`,
|
||||
AllowHTTPMethods: []string{"put"},
|
||||
MethodParams: param.Make(),
|
||||
Filters: nil,
|
||||
Params: nil})
|
||||
|
||||
beego.GlobalControllerRouter["nodemaster/controllers:NodesController"] = append(beego.GlobalControllerRouter["nodemaster/controllers:NodesController"],
|
||||
beego.ControllerComments{
|
||||
Method: "Delete",
|
||||
Router: `/:id`,
|
||||
AllowHTTPMethods: []string{"delete"},
|
||||
MethodParams: param.Make(),
|
||||
Filters: nil,
|
||||
Params: nil})
|
||||
|
||||
beego.GlobalControllerRouter["nodemaster/controllers:NodesController"] = append(beego.GlobalControllerRouter["nodemaster/controllers:NodesController"],
|
||||
beego.ControllerComments{
|
||||
Method: "GetAggregated",
|
||||
Router: `/aggregated`,
|
||||
AllowHTTPMethods: []string{"get"},
|
||||
MethodParams: param.Make(),
|
||||
Filters: nil,
|
||||
Params: nil})
|
||||
|
||||
beego.GlobalControllerRouter["nodemaster/controllers:ServicesController"] = append(beego.GlobalControllerRouter["nodemaster/controllers:ServicesController"],
|
||||
beego.ControllerComments{
|
||||
Method: "Post",
|
||||
Router: `/`,
|
||||
AllowHTTPMethods: []string{"post"},
|
||||
MethodParams: param.Make(),
|
||||
Filters: nil,
|
||||
Params: nil})
|
||||
|
||||
beego.GlobalControllerRouter["nodemaster/controllers:ServicesController"] = append(beego.GlobalControllerRouter["nodemaster/controllers:ServicesController"],
|
||||
beego.ControllerComments{
|
||||
Method: "GetAll",
|
||||
Router: `/`,
|
||||
AllowHTTPMethods: []string{"get"},
|
||||
MethodParams: param.Make(),
|
||||
Filters: nil,
|
||||
Params: nil})
|
||||
|
||||
beego.GlobalControllerRouter["nodemaster/controllers:ServicesController"] = append(beego.GlobalControllerRouter["nodemaster/controllers:ServicesController"],
|
||||
beego.ControllerComments{
|
||||
Method: "Get",
|
||||
Router: `/:id`,
|
||||
AllowHTTPMethods: []string{"get"},
|
||||
MethodParams: param.Make(),
|
||||
Filters: nil,
|
||||
Params: nil})
|
||||
|
||||
beego.GlobalControllerRouter["nodemaster/controllers:ServicesController"] = append(beego.GlobalControllerRouter["nodemaster/controllers:ServicesController"],
|
||||
beego.ControllerComments{
|
||||
Method: "Put",
|
||||
Router: `/:id`,
|
||||
AllowHTTPMethods: []string{"put"},
|
||||
MethodParams: param.Make(),
|
||||
Filters: nil,
|
||||
Params: nil})
|
||||
|
||||
beego.GlobalControllerRouter["nodemaster/controllers:ServicesController"] = append(beego.GlobalControllerRouter["nodemaster/controllers:ServicesController"],
|
||||
beego.ControllerComments{
|
||||
Method: "Delete",
|
||||
Router: `/:id`,
|
||||
AllowHTTPMethods: []string{"delete"},
|
||||
MethodParams: param.Make(),
|
||||
Filters: nil,
|
||||
Params: nil})
|
||||
|
||||
beego.GlobalControllerRouter["nodemaster/controllers:ServicesController"] = append(beego.GlobalControllerRouter["nodemaster/controllers:ServicesController"],
|
||||
beego.ControllerComments{
|
||||
Method: "Backup",
|
||||
Router: `/:id/backup`,
|
||||
AllowHTTPMethods: []string{"post"},
|
||||
MethodParams: param.Make(),
|
||||
Filters: nil,
|
||||
Params: nil})
|
||||
|
||||
beego.GlobalControllerRouter["nodemaster/controllers:ServicesController"] = append(beego.GlobalControllerRouter["nodemaster/controllers:ServicesController"],
|
||||
beego.ControllerComments{
|
||||
Method: "GetBackupProgress",
|
||||
Router: `/:id/backup/progress`,
|
||||
AllowHTTPMethods: []string{"get"},
|
||||
MethodParams: param.Make(),
|
||||
Filters: nil,
|
||||
Params: nil})
|
||||
|
||||
beego.GlobalControllerRouter["nodemaster/controllers:ServicesController"] = append(beego.GlobalControllerRouter["nodemaster/controllers:ServicesController"],
|
||||
beego.ControllerComments{
|
||||
Method: "GetBackupTargets",
|
||||
Router: `/:id/backup/targets`,
|
||||
AllowHTTPMethods: []string{"get"},
|
||||
MethodParams: param.Make(),
|
||||
Filters: nil,
|
||||
Params: nil})
|
||||
|
||||
beego.GlobalControllerRouter["nodemaster/controllers:ServicesController"] = append(beego.GlobalControllerRouter["nodemaster/controllers:ServicesController"],
|
||||
beego.ControllerComments{
|
||||
Method: "AddBackupTarget",
|
||||
Router: `/:id/backup/targets`,
|
||||
AllowHTTPMethods: []string{"post"},
|
||||
MethodParams: param.Make(),
|
||||
Filters: nil,
|
||||
Params: nil})
|
||||
|
||||
beego.GlobalControllerRouter["nodemaster/controllers:ServicesController"] = append(beego.GlobalControllerRouter["nodemaster/controllers:ServicesController"],
|
||||
beego.ControllerComments{
|
||||
Method: "GetBackupTarget",
|
||||
Router: `/:id/backup/targets/:tid`,
|
||||
AllowHTTPMethods: []string{"get"},
|
||||
MethodParams: param.Make(),
|
||||
Filters: nil,
|
||||
Params: nil})
|
||||
|
||||
beego.GlobalControllerRouter["nodemaster/controllers:ServicesController"] = append(beego.GlobalControllerRouter["nodemaster/controllers:ServicesController"],
|
||||
beego.ControllerComments{
|
||||
Method: "PutBackupTarget",
|
||||
Router: `/:id/backup/targets/:tid`,
|
||||
AllowHTTPMethods: []string{"put"},
|
||||
MethodParams: param.Make(),
|
||||
Filters: nil,
|
||||
Params: nil})
|
||||
|
||||
beego.GlobalControllerRouter["nodemaster/controllers:ServicesController"] = append(beego.GlobalControllerRouter["nodemaster/controllers:ServicesController"],
|
||||
beego.ControllerComments{
|
||||
Method: "DeleteBackupTarget",
|
||||
Router: `/:id/backup/targets/:tid`,
|
||||
AllowHTTPMethods: []string{"delete"},
|
||||
MethodParams: param.Make(),
|
||||
Filters: nil,
|
||||
Params: nil})
|
||||
|
||||
beego.GlobalControllerRouter["nodemaster/controllers:ServicesController"] = append(beego.GlobalControllerRouter["nodemaster/controllers:ServicesController"],
|
||||
beego.ControllerComments{
|
||||
Method: "GetBackupTargetProgress",
|
||||
Router: `/:id/backup/targets/:tid/progress`,
|
||||
AllowHTTPMethods: []string{"get"},
|
||||
MethodParams: param.Make(),
|
||||
Filters: nil,
|
||||
Params: nil})
|
||||
|
||||
beego.GlobalControllerRouter["nodemaster/controllers:ServicesController"] = append(beego.GlobalControllerRouter["nodemaster/controllers:ServicesController"],
|
||||
beego.ControllerComments{
|
||||
Method: "RunBackupTarget",
|
||||
Router: `/:id/backup/targets/:tid/run`,
|
||||
AllowHTTPMethods: []string{"post"},
|
||||
MethodParams: param.Make(),
|
||||
Filters: nil,
|
||||
Params: nil})
|
||||
|
||||
beego.GlobalControllerRouter["nodemaster/controllers:ServicesController"] = append(beego.GlobalControllerRouter["nodemaster/controllers:ServicesController"],
|
||||
beego.ControllerComments{
|
||||
Method: "Start",
|
||||
Router: `/:id/start`,
|
||||
AllowHTTPMethods: []string{"post"},
|
||||
MethodParams: param.Make(),
|
||||
Filters: nil,
|
||||
Params: nil})
|
||||
|
||||
beego.GlobalControllerRouter["nodemaster/controllers:ServicesController"] = append(beego.GlobalControllerRouter["nodemaster/controllers:ServicesController"],
|
||||
beego.ControllerComments{
|
||||
Method: "Stop",
|
||||
Router: `/:id/stop`,
|
||||
AllowHTTPMethods: []string{"post"},
|
||||
MethodParams: param.Make(),
|
||||
Filters: nil,
|
||||
Params: nil})
|
||||
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
// @APIVersion 1.0.0
|
||||
// @Title NodeMaster API
|
||||
// @Description API for managing a node, its services, and a cluster of remote nodes.
|
||||
// @Contact yves.cerezal@irt-saintexupery.com
|
||||
// @License Apache 2.0
|
||||
// @LicenseUrl http://www.apache.org/licenses/LICENSE-2.0.html
|
||||
package routers
|
||||
|
||||
import (
|
||||
"nodemaster/controllers"
|
||||
|
||||
beego "github.com/beego/beego/v2/server/web"
|
||||
"github.com/beego/beego/v2/server/web/filter/cors"
|
||||
)
|
||||
|
||||
func init() {
|
||||
// The API has no authentication of its own, so CORS is intentionally
|
||||
// scoped to explicit origins (never "*") and only enabled in dev mode
|
||||
// for now — enabling it for other run modes, with an operator-supplied
|
||||
// origin list, is tracked as a follow-up hardening task.
|
||||
if beego.BConfig.RunMode == "dev" {
|
||||
origins, err := beego.AppConfig.Strings("corsalloworigins")
|
||||
if err != nil || len(origins) == 0 {
|
||||
origins = []string{"http://localhost:*"}
|
||||
}
|
||||
beego.InsertFilter("*", beego.BeforeRouter, cors.Allow(&cors.Options{
|
||||
AllowOrigins: origins,
|
||||
AllowMethods: []string{"GET", "POST", "PUT", "DELETE"},
|
||||
AllowHeaders: []string{"Origin", "Content-Type"},
|
||||
}))
|
||||
}
|
||||
|
||||
ns := beego.NewNamespace("/v1",
|
||||
beego.NSNamespace("/node",
|
||||
beego.NSInclude(
|
||||
&controllers.NodeController{},
|
||||
),
|
||||
),
|
||||
beego.NSNamespace("/nodes",
|
||||
beego.NSInclude(
|
||||
&controllers.NodesController{},
|
||||
),
|
||||
),
|
||||
beego.NSNamespace("/services",
|
||||
beego.NSInclude(
|
||||
&controllers.ServicesController{},
|
||||
),
|
||||
),
|
||||
)
|
||||
beego.AddNamespace(ns)
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 665 B |
Binary file not shown.
|
After Width: | Height: | Size: 628 B |
@@ -0,0 +1,60 @@
|
||||
<!-- HTML for static distribution bundle build -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Swagger UI</title>
|
||||
<link rel="stylesheet" type="text/css" href="./swagger-ui.css" />
|
||||
<link rel="icon" type="image/png" href="./favicon-32x32.png" sizes="32x32" />
|
||||
<link rel="icon" type="image/png" href="./favicon-16x16.png" sizes="16x16" />
|
||||
<style>
|
||||
html
|
||||
{
|
||||
box-sizing: border-box;
|
||||
overflow: -moz-scrollbars-vertical;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
*,
|
||||
*:before,
|
||||
*:after
|
||||
{
|
||||
box-sizing: inherit;
|
||||
}
|
||||
|
||||
body
|
||||
{
|
||||
margin:0;
|
||||
background: #fafafa;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="swagger-ui"></div>
|
||||
|
||||
<script src="./swagger-ui-bundle.js" charset="UTF-8"> </script>
|
||||
<script src="./swagger-ui-standalone-preset.js" charset="UTF-8"> </script>
|
||||
<script>
|
||||
window.onload = function() {
|
||||
// Begin Swagger UI call region
|
||||
const ui = SwaggerUIBundle({
|
||||
url: "swagger.json",
|
||||
dom_id: '#swagger-ui',
|
||||
deepLinking: true,
|
||||
presets: [
|
||||
SwaggerUIBundle.presets.apis,
|
||||
SwaggerUIStandalonePreset
|
||||
],
|
||||
plugins: [
|
||||
SwaggerUIBundle.plugins.DownloadUrl
|
||||
],
|
||||
layout: "StandaloneLayout"
|
||||
});
|
||||
// End Swagger UI call region
|
||||
|
||||
window.ui = ui;
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,79 @@
|
||||
<!doctype html>
|
||||
<html lang="en-US">
|
||||
<head>
|
||||
<title>Swagger UI: OAuth2 Redirect</title>
|
||||
</head>
|
||||
<body>
|
||||
<script>
|
||||
'use strict';
|
||||
function run () {
|
||||
var oauth2 = window.opener.swaggerUIRedirectOauth2;
|
||||
var sentState = oauth2.state;
|
||||
var redirectUrl = oauth2.redirectUrl;
|
||||
var isValid, qp, arr;
|
||||
|
||||
if (/code|token|error/.test(window.location.hash)) {
|
||||
qp = window.location.hash.substring(1);
|
||||
} else {
|
||||
qp = location.search.substring(1);
|
||||
}
|
||||
|
||||
arr = qp.split("&");
|
||||
arr.forEach(function (v,i,_arr) { _arr[i] = '"' + v.replace('=', '":"') + '"';});
|
||||
qp = qp ? JSON.parse('{' + arr.join() + '}',
|
||||
function (key, value) {
|
||||
return key === "" ? value : decodeURIComponent(value);
|
||||
}
|
||||
) : {};
|
||||
|
||||
isValid = qp.state === sentState;
|
||||
|
||||
if ((
|
||||
oauth2.auth.schema.get("flow") === "accessCode" ||
|
||||
oauth2.auth.schema.get("flow") === "authorizationCode" ||
|
||||
oauth2.auth.schema.get("flow") === "authorization_code"
|
||||
) && !oauth2.auth.code) {
|
||||
if (!isValid) {
|
||||
oauth2.errCb({
|
||||
authId: oauth2.auth.name,
|
||||
source: "auth",
|
||||
level: "warning",
|
||||
message: "Authorization may be unsafe, passed state was changed in server Passed state wasn't returned from auth server"
|
||||
});
|
||||
}
|
||||
|
||||
if (qp.code) {
|
||||
delete oauth2.state;
|
||||
oauth2.auth.code = qp.code;
|
||||
oauth2.callback({auth: oauth2.auth, redirectUrl: redirectUrl});
|
||||
} else {
|
||||
let oauthErrorMsg;
|
||||
if (qp.error) {
|
||||
oauthErrorMsg = "["+qp.error+"]: " +
|
||||
(qp.error_description ? qp.error_description+ ". " : "no accessCode received from the server. ") +
|
||||
(qp.error_uri ? "More info: "+qp.error_uri : "");
|
||||
}
|
||||
|
||||
oauth2.errCb({
|
||||
authId: oauth2.auth.name,
|
||||
source: "auth",
|
||||
level: "error",
|
||||
message: oauthErrorMsg || "[Authorization failed]: no accessCode received from the server"
|
||||
});
|
||||
}
|
||||
} else {
|
||||
oauth2.callback({auth: oauth2.auth, token: qp, isValid: isValid, redirectUrl: redirectUrl});
|
||||
}
|
||||
window.close();
|
||||
}
|
||||
|
||||
if (document.readyState !== 'loading') {
|
||||
run();
|
||||
} else {
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
run();
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,3 @@
|
||||
/*! For license information please see swagger-ui-bundle.js.LICENSE.txt */
|
||||
!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.SwaggerUIBundle=t():e.SwaggerUIBundle=t()}(this,(function(){return(()=>{var e={66419:(e,t,n)=>{e.exports=n(24848)},41511:(e,t,n)=>{e.exports=n(83363)},11128:(e,t,n)=>{e.exports=n(57784)},54103:(e,t,n)=>{e.exports=n(28196)},77766:(e,t,n)=>{e.exports=n(8065)},72119:(e,t,n)=>{e.exports=n(57448)},10062:(e,t,n)=>{e.exports=n(29455)},44494:(e,t,n)=>{e.exports=n(69743)},20116:(e,t,n)=>{e.exports=n(11955)},62462:(e,t,n)=>{e.exports=n(96064)},94473:(e,t,n)=>{e.exports=n(61577)},78914:(e,t,n)=>{e.exports=n(46279)},78580:(e,t,n)=>{e.exports=n(33778)},81643:(e,t,n)=>{e.exports=n(19373)},69301:(e,t,n)=>{e.exports=n(73819)},23054:(e,t,n)=>{e.exports=n(11022)},2991:(e,t,n)=>{e.exports=n(61798)},32366:(e,t,n)=>{e.exports=n(52527)},39291:(e,t,n)=>{e.exports=n(36857)},3649:(e,t,n)=>{e.exports=n(82073)},77149:(e,t,n)=>{e.exports=n(45286)},47302:(e,t,n)=>{e.exports=n(62856)},92762:(e,t,n)=>{e.exports=n(2348)},29828:(e,t,n)=>{e.exports=n(35178)},25843:(e,t,n)=>{e.exports=n(76361)},89400:(e,t,n)=>{e.exports=n(71815)},59340:(e,t,n)=>{e.exports=n(8933)},39392:(e,t,n)=>{e.exports=n(15868)},51942:(e,t,n)=>{e.exports=n(63383)},63978:(e,t,n)=>{e.exports=n(41910)},26295:(e,t,n)=>{e.exports=n(86209)},86902:(e,t,n)=>{e.exports=n(23059)},20455:(e,t,n)=>{e.exports=n(47795)},93476:(e,t,n)=>{e.exports=n(27460)},33032:(e,t,n)=>{e.exports=n(27989)},94435:(e,t,n)=>{e.exports=n(73926)},39969:(e,t,n)=>{e.exports=n(57641)},52424:(e,t,n)=>{e.exports=n(72010)},53592:(e,t,n)=>{e.exports=n(27385)},78363:(e,t,n)=>{e.exports=n(81522)},19996:(e,t,n)=>{e.exports=n(32209)},51445:(e,t,n)=>{e.exports=n(14122)},30699:(e,t,n)=>{e.exports=n(44442)},28834:(e,t,n)=>{e.exports=n(57152)},95683:(e,t,n)=>{e.exports=n(69447)},95238:(e,t,n)=>{e.exports=n(81493)},90:(e,t,n)=>{e.exports=n(86672)},12088:(e,t,n)=>{e.exports=n(60269)},189:(e,t,n)=>{e.exports=n(76094)},32752:(e,t,n)=>{e.exports=n(70573)},44341:(e,t,n)=>{e.exports=n(73685)},58377:(e,t,n)=>{e.exports=n(27533)},13038:(e,t,n)=>{e.exports=n(39057)},63263:(e,t,n)=>{e.exports=n(84710)},24889:(e,t,n)=>{e.exports=n(74303)},89356:(e,t,n)=>{e.exports=n(93799)},79542:(e,t,n)=>{e.exports=n(55122)},69798:(e,t,n)=>{e.exports=n(29531)},13535:(e,t,n)=>{e.exports=n(10856)},83863:(e,t,n)=>{e.exports=n(31524)},51446:(e,t,n)=>{e.exports=n(86600)},23882:(e,t,n)=>{e.exports=n(9759)},34243:e=>{e.exports=function(e,t){(null==t||t>e.length)&&(t=e.length);for(var n=0,r=new Array(t);n<t;n++)r[n]=e[n];return r},e.exports.__esModule=!0,e.exports.default=e.exports},57726:(e,t,n)=>{var r=n(78363);e.exports=function(e){if(r(e))return e},e.exports.__esModule=!0,e.exports.default=e.exports},46868:(e,t,n)=>{var r=n(78363),o=n(34243);e.exports=function(e){if(r(e))return o(e)},e.exports.__esModule=!0,e.exports.default=e.exports},5824:e=>{e.exports=function(e){if(void 0===e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return e},e.exports.__esModule=!0,e.exports.default=e.exports},51161:(e,t,n)=>{var r=n(69798);function o(e,t,n,o,a,i,s){try{var u=e[i](s),l=u.value}catch(e){return void n(e)}u.done?t(l):r.resolve(l).then(o,a)}e.exports=function(e){return function(){var t=this,n=arguments;return new r((function(r,a){var i=e.apply(t,n);function s(e){o(i,r,a,s,u,"next",e)}function u(e){o(i,r,a,s,u,"throw",e)}s(void 0)}))}},e.exports.__esModule=!0,e.exports.default=e.exports},26394:e=>{e.exports=function(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")},e.exports.__esModule=!0,e.exports.default=e.exports},74003:(e,t,n)=>{var r=n(13535),o=n(51445),a=n(5613),i=n(8647);function s(t,n,u){return i()?(e.exports=s=r,e.exports.__esModule=!0,e.exports.default=e.exports):(e.exports=s=function(e,t,n){var r=[null];r.push.apply(r,t);var i=new(o(Function).apply(e,r));return n&&a(i,n.prototype),i},e.exports.__esModule=!0,e.exports.default=e.exports),s.apply(null,arguments)}e.exports=s,e.exports.__esModule=!0,e.exports.default=e.exports},69198:(e,t,n)=>{var r=n(44341);function o(e,t){for(var n=0;n<t.length;n++){var o=t[n];o.enumerable=o.enumerable||!1,o.configurable=!0,"value"in o&&(o.writable=!0),r(e,o.key,o)}}e.exports=function(e,t,n){return t&&o(e.prototype,t),n&&o(e,n),r(e,"prototype",{writable:!1}),e},e.exports.__esModule=!0,e.exports.default=e.exports},86418:(e,t,n)=>{var r=n(51446),o=n(19996),a=n(78363),i=n(79299);e.exports=function(e,t){var n=void 0!==r&&o(e)||e["@@iterator"];if(!n){if(a(e)||(n=i(e))||t&&e&&"number"==typeof e.length){n&&(e=n);var s=0,u=function(){};return{s:u,n:function(){return s>=e.length?{done:!0}:{done:!1,value:e[s++]}},e:function(e){throw e},f:u}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var l,c=!0,p=!1;return{s:function(){n=n.call(e)},n:function(){var e=n.next();return c=e.done,e},e:funLine truncated
|
||||
//# sourceMappingURL=swagger-ui-bundle.js.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"sources":["webpack://SwaggerUIBundle/webpack/universalModuleDefinition","webpack://SwaggerUIBundle/webpack/bootstrap","webpack://SwaggerUIBundle/./node_modules/react/react.js","webpack://SwaggerUIBundle/./node_modules/immutable/dist/immutable.js","webpack://SwaggerUIBundle/./node_modules/@babel/runtime-corejs2/helpers/defineProperty.js","webpack://SwaggerUIBundle/./src/core/utils.js","webpack://SwaggerUIBundle/./node_modules/@babel/runtime-corejs2/helpers/classCallCheck.js","webpack://SwaggerUIBundle/./node_modules/@babel/runtime-corejs2/helpers/createClass.js","webpack://SwaggerUIBundle/./node_modules/@babel/runtime-corejs2/helpers/possibleConstructorReturn.js","webpack://SwaggerUIBundle/./node_modules/@babel/runtime-corejs2/helpers/getPrototypeOf.js","webpack://SwaggerUIBundle/./node_modules/@babel/runtime-corejs2/helpers/inherits.js","webpack://SwaggerUIBundle/./node_modules/@babel/runtime-corejs2/helpers/assertThisInitialized.js","webpack://SwaggerUIBundle/./node_modules/prop-types/index.js","webpack://SwaggerUIBundle/./node_modules/reselect/lib/index.js","webpack://SwaggerUIBundle/./node_modules/@babel/runtime-corejs2/helpers/toConsumableArray.js","webpack://SwaggerUIBundle/./node_modules/@babel/runtime-corejs2/helpers/slicedToArray.js","webpack://SwaggerUIBundle/./node_modules/@babel/runtime-corejs2/core-js/array/is-array.js","webpack://SwaggerUIBundle/./node_modules/fbjs/lib/invariant.js","webpack://SwaggerUIBundle/./node_modules/@babel/runtime-corejs2/core-js/object/assign.js","webpack://SwaggerUIBundle/./node_modules/@babel/runtime-corejs2/core-js/object/keys.js","webpack://SwaggerUIBundle/./src/core/window.js","webpack://SwaggerUIBundle/./node_modules/react-immutable-proptypes/dist/ImmutablePropTypes.js","webpack://SwaggerUIBundle/./node_modules/@babel/runtime-corejs2/helpers/extends.js","webpack://SwaggerUIBundle/./node_modules/react-dom/lib/reactProdInvariant.js","webpack://SwaggerUIBundle/./node_modules/core-js/library/modules/_core.js","webpack://SwaggerUIBundle/./node_modules/fbjs/lib/warning.js","webpack://SwaggerUIBundle/./src/core/plugins/oas3/helpers.jsx","webpack://SwaggerUIBundle/./node_modules/object-assign/index.js","webpack://SwaggerUIBundle/./node_modules/@babel/runtime-corejs2/helpers/typeof.js","webpack://SwaggerUIBundle/./node_modules/react-dom/lib/ReactDOMComponentTree.js","webpack://SwaggerUIBundle/./node_modules/@babel/runtime-corejs2/core-js/json/stringify.js","webpack://SwaggerUIBundle/./src/core/plugins/spec/actions.js","webpack://SwaggerUIBundle/./node_modules/core-js/library/modules/_export.js","webpack://SwaggerUIBundle/./node_modules/js-yaml/lib/js-yaml/type.js","webpack://SwaggerUIBundle/./node_modules/core-js/library/modules/_global.js","webpack://SwaggerUIBundle/./node_modules/core-js/modules/_wks.js","webpack://SwaggerUIBundle/./node_modules/core-js/library/modules/_wks.js","webpack://SwaggerUIBundle/./node_modules/core-js/modules/_export.js","webpack://SwaggerUIBundle/(webpack)/buildin/global.js","webpack://SwaggerUIBundle/./node_modules/lodash/isArray.js","webpack://SwaggerUIBundle/./node_modules/fbjs/lib/ExecutionEnvironment.js","webpack://SwaggerUIBundle/./node_modules/remarkable/lib/common/utils.js","webpack://SwaggerUIBundle/./node_modules/@babel/runtime-corejs2/helpers/objectWithoutProperties.js","webpack://SwaggerUIBundle/./node_modules/core-js/modules/_global.js","webpack://SwaggerUIBundle/./node_modules/core-js/modules/_string-html.js","webpack://SwaggerUIBundle/./node_modules/core-js/library/modules/_is-object.js","webpack://SwaggerUIBundle/./src/core/plugins/err/actions.js","webpack://SwaggerUIBundle/./node_modules/core-js/modules/_an-object.js","webpack://SwaggerUIBundle/./node_modules/core-js/library/modules/_an-object.js","webpack://SwaggerUIBundle/./node_modules/inherits/inherits_browser.js","webpack://SwaggerUIBundle/./node_modules/safe-buffer/index.js","webpack://SwaggerUIBundle/./node_modules/core-js/library/modules/_object-dp.js","webpack://SwaggerUIBundle/./node_modules/core-js/library/modules/_descriptors.js","webpack://SwaggerUIBundle/./node_modules/lodash/_root.js","webpack://SwaggerUIBundle/./node_modules/lodash/isObject.js","webpack://SwaggerUIBundle/./node_modules/react-dom/lib/ReactInstrumentation.js","webpack://SwaggerUIBundle/./node_modules/@babel/runtime-corejs2/core-js/object/define-property.js","webpack://SwaggerUIBundle/./node_modules/@babel/runtime-corejs2/core-js/object/get-own-property-symbols.js","webpack://SwaggerUIBundle/./node_modules/swagger-client/dist/index.js","webpack://SwaggerUIBundle/./node_modules/fbjs/lib/emptyFunction.js","webpack://SwaggerUIBundle/./node_modules/react-dom/lib/ReactUpdates.js","webpack://SwaggerUIBundle/./node_modules/classnames/index.js","webpack://SwaggerUIBundle/./node_modules/@babel/runtime-corejs2/core-js/object/get-own-property-descriptors.js","webpack://SwaggerUIBundle/./node_modules/@babel/runtime-corejs2/core-js/object/get-own-property-descriptor.js","webpack://SwaggerUIBundlLine truncated
|
||||
@@ -0,0 +1,2 @@
|
||||
(()=>{var e={66419:(e,t,r)=>{e.exports=r(27698)},41511:(e,t,r)=>{e.exports=r(83363)},11128:(e,t,r)=>{e.exports=r(57784)},54103:(e,t,r)=>{e.exports=r(28196)},77766:(e,t,r)=>{e.exports=r(8065)},72119:(e,t,r)=>{e.exports=r(57448)},10062:(e,t,r)=>{e.exports=r(29455)},44494:(e,t,r)=>{e.exports=r(69743)},20116:(e,t,r)=>{e.exports=r(11955)},62462:(e,t,r)=>{e.exports=r(96064)},94473:(e,t,r)=>{e.exports=r(61577)},78914:(e,t,r)=>{e.exports=r(46279)},78580:(e,t,r)=>{e.exports=r(33778)},81643:(e,t,r)=>{e.exports=r(19373)},69301:(e,t,r)=>{e.exports=r(73819)},23054:(e,t,r)=>{e.exports=r(11022)},2991:(e,t,r)=>{e.exports=r(61798)},32366:(e,t,r)=>{e.exports=r(52527)},39291:(e,t,r)=>{e.exports=r(36857)},3649:(e,t,r)=>{e.exports=r(82073)},77149:(e,t,r)=>{e.exports=r(45286)},47302:(e,t,r)=>{e.exports=r(62856)},92762:(e,t,r)=>{e.exports=r(2348)},29828:(e,t,r)=>{e.exports=r(35178)},25843:(e,t,r)=>{e.exports=r(76361)},89400:(e,t,r)=>{e.exports=r(71815)},59340:(e,t,r)=>{e.exports=r(8933)},39392:(e,t,r)=>{e.exports=r(15868)},51942:(e,t,r)=>{e.exports=r(63383)},63978:(e,t,r)=>{e.exports=r(41910)},26295:(e,t,r)=>{e.exports=r(86209)},86902:(e,t,r)=>{e.exports=r(23059)},20455:(e,t,r)=>{e.exports=r(47795)},93476:(e,t,r)=>{e.exports=r(27460)},33032:(e,t,r)=>{e.exports=r(27989)},94435:(e,t,r)=>{e.exports=r(73926)},39969:(e,t,r)=>{e.exports=r(57641)},52424:(e,t,r)=>{e.exports=r(72010)},53592:(e,t,r)=>{e.exports=r(27385)},78363:(e,t,r)=>{e.exports=r(81522)},19996:(e,t,r)=>{e.exports=r(32209)},51445:(e,t,r)=>{e.exports=r(14122)},30699:(e,t,r)=>{e.exports=r(44442)},28834:(e,t,r)=>{e.exports=r(57152)},95683:(e,t,r)=>{e.exports=r(69447)},95238:(e,t,r)=>{e.exports=r(81493)},90:(e,t,r)=>{e.exports=r(86672)},12088:(e,t,r)=>{e.exports=r(60269)},189:(e,t,r)=>{e.exports=r(76094)},32752:(e,t,r)=>{e.exports=r(70573)},44341:(e,t,r)=>{e.exports=r(73685)},58377:(e,t,r)=>{e.exports=r(27533)},13038:(e,t,r)=>{e.exports=r(39057)},63263:(e,t,r)=>{e.exports=r(84710)},24889:(e,t,r)=>{e.exports=r(74303)},89356:(e,t,r)=>{e.exports=r(93799)},79542:(e,t,r)=>{e.exports=r(55122)},69798:(e,t,r)=>{e.exports=r(29531)},13535:(e,t,r)=>{e.exports=r(10856)},83863:(e,t,r)=>{e.exports=r(31524)},51446:(e,t,r)=>{e.exports=r(86600)},23882:(e,t,r)=>{e.exports=r(9759)},34243:e=>{e.exports=function(e,t){(null==t||t>e.length)&&(t=e.length);for(var r=0,n=new Array(t);r<t;r++)n[r]=e[r];return n},e.exports.__esModule=!0,e.exports.default=e.exports},57726:(e,t,r)=>{var n=r(78363);e.exports=function(e){if(n(e))return e},e.exports.__esModule=!0,e.exports.default=e.exports},46868:(e,t,r)=>{var n=r(78363),a=r(34243);e.exports=function(e){if(n(e))return a(e)},e.exports.__esModule=!0,e.exports.default=e.exports},5824:e=>{e.exports=function(e){if(void 0===e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return e},e.exports.__esModule=!0,e.exports.default=e.exports},51161:(e,t,r)=>{var n=r(69798);function a(e,t,r,a,o,s,i){try{var l=e[s](i),c=l.value}catch(e){return void r(e)}l.done?t(c):n.resolve(c).then(a,o)}e.exports=function(e){return function(){var t=this,r=arguments;return new n((function(n,o){var s=e.apply(t,r);function i(e){a(s,n,o,i,l,"next",e)}function l(e){a(s,n,o,i,l,"throw",e)}i(void 0)}))}},e.exports.__esModule=!0,e.exports.default=e.exports},26394:e=>{e.exports=function(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")},e.exports.__esModule=!0,e.exports.default=e.exports},74003:(e,t,r)=>{var n=r(13535),a=r(51445),o=r(5613),s=r(8647);function i(t,r,l){return s()?(e.exports=i=n,e.exports.__esModule=!0,e.exports.default=e.exports):(e.exports=i=function(e,t,r){var n=[null];n.push.apply(n,t);var s=new(a(Function).apply(e,n));return r&&o(s,r.prototype),s},e.exports.__esModule=!0,e.exports.default=e.exports),i.apply(null,arguments)}e.exports=i,e.exports.__esModule=!0,e.exports.default=e.exports},69198:(e,t,r)=>{var n=r(44341);function a(e,t){for(var r=0;r<t.length;r++){var a=t[r];a.enumerable=a.enumerable||!1,a.configurable=!0,"value"in a&&(a.writable=!0),n(e,a.key,a)}}e.exports=function(e,t,r){return t&&a(e.prototype,t),r&&a(e,r),n(e,"prototype",{writable:!1}),e},e.exports.__esModule=!0,e.exports.default=e.exports},86418:(e,t,r)=>{var n=r(51446),a=r(19996),o=r(78363),s=r(79299);e.exports=function(e,t){var r=void 0!==n&&a(e)||e["@@iterator"];if(!r){if(o(e)||(r=s(e))||t&&e&&"number"==typeof e.length){r&&(e=r);var i=0,l=function(){};return{s:l,n:function(){return i>=e.length?{done:!0}:{done:!1,value:e[i++]}},e:function(e){throw e},f:l}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var c,u=!0,p=!1;return{s:function(){r=r.call(e)},n:function(){var e=r.next();return u=e.done,e},e:function(e){p=!0,c=e},f:function(){try{u||null==r.return||r.return()}finally{if(p)throw c}}}},e.exports.__esModule=!0,e.exports.default=e.exports},10098:(e,t,r)=>{var n=r(13535),a=r(66380),o=r(8647),s=r(214);e.exports=function(e){var t=oLine truncated
|
||||
//# sourceMappingURL=swagger-ui-es-bundle-core.js.map
|
||||
@@ -0,0 +1,3 @@
|
||||
/*! For license information please see swagger-ui-es-bundle.js.LICENSE.txt */
|
||||
(()=>{var e={66419:(e,t,n)=>{e.exports=n(24848)},41511:(e,t,n)=>{e.exports=n(83363)},11128:(e,t,n)=>{e.exports=n(57784)},54103:(e,t,n)=>{e.exports=n(28196)},77766:(e,t,n)=>{e.exports=n(8065)},72119:(e,t,n)=>{e.exports=n(57448)},10062:(e,t,n)=>{e.exports=n(29455)},44494:(e,t,n)=>{e.exports=n(69743)},20116:(e,t,n)=>{e.exports=n(11955)},62462:(e,t,n)=>{e.exports=n(96064)},94473:(e,t,n)=>{e.exports=n(61577)},78914:(e,t,n)=>{e.exports=n(46279)},78580:(e,t,n)=>{e.exports=n(33778)},81643:(e,t,n)=>{e.exports=n(19373)},69301:(e,t,n)=>{e.exports=n(73819)},23054:(e,t,n)=>{e.exports=n(11022)},2991:(e,t,n)=>{e.exports=n(61798)},32366:(e,t,n)=>{e.exports=n(52527)},39291:(e,t,n)=>{e.exports=n(36857)},3649:(e,t,n)=>{e.exports=n(82073)},77149:(e,t,n)=>{e.exports=n(45286)},47302:(e,t,n)=>{e.exports=n(62856)},92762:(e,t,n)=>{e.exports=n(2348)},29828:(e,t,n)=>{e.exports=n(35178)},25843:(e,t,n)=>{e.exports=n(76361)},89400:(e,t,n)=>{e.exports=n(71815)},59340:(e,t,n)=>{e.exports=n(8933)},39392:(e,t,n)=>{e.exports=n(15868)},51942:(e,t,n)=>{e.exports=n(63383)},63978:(e,t,n)=>{e.exports=n(41910)},26295:(e,t,n)=>{e.exports=n(86209)},86902:(e,t,n)=>{e.exports=n(23059)},20455:(e,t,n)=>{e.exports=n(47795)},93476:(e,t,n)=>{e.exports=n(27460)},33032:(e,t,n)=>{e.exports=n(27989)},94435:(e,t,n)=>{e.exports=n(73926)},39969:(e,t,n)=>{e.exports=n(57641)},52424:(e,t,n)=>{e.exports=n(72010)},53592:(e,t,n)=>{e.exports=n(27385)},78363:(e,t,n)=>{e.exports=n(81522)},19996:(e,t,n)=>{e.exports=n(32209)},51445:(e,t,n)=>{e.exports=n(14122)},30699:(e,t,n)=>{e.exports=n(44442)},28834:(e,t,n)=>{e.exports=n(57152)},95683:(e,t,n)=>{e.exports=n(69447)},95238:(e,t,n)=>{e.exports=n(81493)},90:(e,t,n)=>{e.exports=n(86672)},12088:(e,t,n)=>{e.exports=n(60269)},189:(e,t,n)=>{e.exports=n(76094)},32752:(e,t,n)=>{e.exports=n(70573)},44341:(e,t,n)=>{e.exports=n(73685)},58377:(e,t,n)=>{e.exports=n(27533)},13038:(e,t,n)=>{e.exports=n(39057)},63263:(e,t,n)=>{e.exports=n(84710)},24889:(e,t,n)=>{e.exports=n(74303)},89356:(e,t,n)=>{e.exports=n(93799)},79542:(e,t,n)=>{e.exports=n(55122)},69798:(e,t,n)=>{e.exports=n(29531)},13535:(e,t,n)=>{e.exports=n(10856)},83863:(e,t,n)=>{e.exports=n(31524)},51446:(e,t,n)=>{e.exports=n(86600)},23882:(e,t,n)=>{e.exports=n(9759)},34243:e=>{e.exports=function(e,t){(null==t||t>e.length)&&(t=e.length);for(var n=0,r=new Array(t);n<t;n++)r[n]=e[n];return r},e.exports.__esModule=!0,e.exports.default=e.exports},57726:(e,t,n)=>{var r=n(78363);e.exports=function(e){if(r(e))return e},e.exports.__esModule=!0,e.exports.default=e.exports},46868:(e,t,n)=>{var r=n(78363),o=n(34243);e.exports=function(e){if(r(e))return o(e)},e.exports.__esModule=!0,e.exports.default=e.exports},5824:e=>{e.exports=function(e){if(void 0===e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return e},e.exports.__esModule=!0,e.exports.default=e.exports},51161:(e,t,n)=>{var r=n(69798);function o(e,t,n,o,a,i,s){try{var u=e[i](s),l=u.value}catch(e){return void n(e)}u.done?t(l):r.resolve(l).then(o,a)}e.exports=function(e){return function(){var t=this,n=arguments;return new r((function(r,a){var i=e.apply(t,n);function s(e){o(i,r,a,s,u,"next",e)}function u(e){o(i,r,a,s,u,"throw",e)}s(void 0)}))}},e.exports.__esModule=!0,e.exports.default=e.exports},26394:e=>{e.exports=function(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")},e.exports.__esModule=!0,e.exports.default=e.exports},74003:(e,t,n)=>{var r=n(13535),o=n(51445),a=n(5613),i=n(8647);function s(t,n,u){return i()?(e.exports=s=r,e.exports.__esModule=!0,e.exports.default=e.exports):(e.exports=s=function(e,t,n){var r=[null];r.push.apply(r,t);var i=new(o(Function).apply(e,r));return n&&a(i,n.prototype),i},e.exports.__esModule=!0,e.exports.default=e.exports),s.apply(null,arguments)}e.exports=s,e.exports.__esModule=!0,e.exports.default=e.exports},69198:(e,t,n)=>{var r=n(44341);function o(e,t){for(var n=0;n<t.length;n++){var o=t[n];o.enumerable=o.enumerable||!1,o.configurable=!0,"value"in o&&(o.writable=!0),r(e,o.key,o)}}e.exports=function(e,t,n){return t&&o(e.prototype,t),n&&o(e,n),r(e,"prototype",{writable:!1}),e},e.exports.__esModule=!0,e.exports.default=e.exports},86418:(e,t,n)=>{var r=n(51446),o=n(19996),a=n(78363),i=n(79299);e.exports=function(e,t){var n=void 0!==r&&o(e)||e["@@iterator"];if(!n){if(a(e)||(n=i(e))||t&&e&&"number"==typeof e.length){n&&(e=n);var s=0,u=function(){};return{s:u,n:function(){return s>=e.length?{done:!0}:{done:!1,value:e[s++]}},e:function(e){throw e},f:u}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var l,c=!0,p=!1;return{s:function(){n=n.call(e)},n:function(){var e=n.next();return c=e.done,e},e:function(e){p=!0,l=e},f:function(){try{c||null==n.return||n.return()}finally{if(p)throw l}}}},e.exports.__esModule=!0,e.exports.default=e.exports},10098:(e,t,n)=>{var r=n(13535),o=n(66380),a=n(8647),i=n(214);e.exports=function(e){var t=aLine truncated
|
||||
//# sourceMappingURL=swagger-ui-es-bundle.js.map
|
||||
@@ -0,0 +1,3 @@
|
||||
/*! For license information please see swagger-ui-standalone-preset.js.LICENSE.txt */
|
||||
!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.SwaggerUIStandalonePreset=e():t.SwaggerUIStandalonePreset=e()}(this,(function(){return(()=>{var t={66419:(t,e,r)=>{t.exports=r(27698)},41511:(t,e,r)=>{t.exports=r(83363)},54103:(t,e,r)=>{r(28196)},77766:(t,e,r)=>{t.exports=r(8065)},72119:(t,e,r)=>{r(57448)},10062:(t,e,r)=>{t.exports=r(29455)},44494:(t,e,r)=>{t.exports=r(69743)},20116:(t,e,r)=>{r(11955)},62462:(t,e,r)=>{t.exports=r(96064)},94473:(t,e,r)=>{t.exports=r(61577)},78914:(t,e,r)=>{t.exports=r(46279)},78580:(t,e,r)=>{t.exports=r(33778)},81643:(t,e,r)=>{t.exports=r(19373)},69301:(t,e,r)=>{t.exports=r(73819)},2991:(t,e,r)=>{t.exports=r(61798)},32366:(t,e,r)=>{r(52527)},3649:(t,e,r)=>{t.exports=r(82073)},77149:(t,e,r)=>{t.exports=r(45286)},47302:(t,e,r)=>{r(62856)},29828:(t,e,r)=>{r(35178)},25843:(t,e,r)=>{r(76361)},59340:(t,e,r)=>{t.exports=r(8933)},39392:(t,e,r)=>{t.exports=r(15868)},51942:(t,e,r)=>{t.exports=r(63383)},86902:(t,e,r)=>{t.exports=r(23059)},53592:(t,e,r)=>{t.exports=r(27385)},78363:(t,e,r)=>{t.exports=r(81522)},19996:(t,e,r)=>{t.exports=r(32209)},51445:(t,e,r)=>{t.exports=r(14122)},95683:(t,e,r)=>{t.exports=r(69447)},95238:(t,e,r)=>{t.exports=r(81493)},90:(t,e,r)=>{t.exports=r(86672)},12088:(t,e,r)=>{t.exports=r(60269)},189:(t,e,r)=>{t.exports=r(76094)},44341:(t,e,r)=>{t.exports=r(73685)},58377:(t,e,r)=>{t.exports=r(27533)},24889:(t,e,r)=>{t.exports=r(74303)},79542:(t,e,r)=>{t.exports=r(55122)},13535:(t,e,r)=>{t.exports=r(10856)},83863:(t,e,r)=>{t.exports=r(31524)},51446:(t,e,r)=>{t.exports=r(86600)},23882:(t,e,r)=>{t.exports=r(9759)},34243:t=>{t.exports=function(t,e){(null==e||e>t.length)&&(e=t.length);for(var r=0,n=new Array(e);r<e;r++)n[r]=t[r];return n},t.exports.__esModule=!0,t.exports.default=t.exports},57726:(t,e,r)=>{var n=r(78363);t.exports=function(t){if(n(t))return t},t.exports.__esModule=!0,t.exports.default=t.exports},46868:(t,e,r)=>{var n=r(78363),i=r(34243);t.exports=function(t){if(n(t))return i(t)},t.exports.__esModule=!0,t.exports.default=t.exports},5824:t=>{t.exports=function(t){if(void 0===t)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return t},t.exports.__esModule=!0,t.exports.default=t.exports},26394:t=>{t.exports=function(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")},t.exports.__esModule=!0,t.exports.default=t.exports},74003:(t,e,r)=>{var n=r(13535),i=r(51445),o=r(5613),u=r(8647);function s(e,r,a){return u()?(t.exports=s=n,t.exports.__esModule=!0,t.exports.default=t.exports):(t.exports=s=function(t,e,r){var n=[null];n.push.apply(n,e);var u=new(i(Function).apply(t,n));return r&&o(u,r.prototype),u},t.exports.__esModule=!0,t.exports.default=t.exports),s.apply(null,arguments)}t.exports=s,t.exports.__esModule=!0,t.exports.default=t.exports},69198:(t,e,r)=>{var n=r(44341);function i(t,e){for(var r=0;r<e.length;r++){var i=e[r];i.enumerable=i.enumerable||!1,i.configurable=!0,"value"in i&&(i.writable=!0),n(t,i.key,i)}}t.exports=function(t,e,r){return e&&i(t.prototype,e),r&&i(t,r),n(t,"prototype",{writable:!1}),t},t.exports.__esModule=!0,t.exports.default=t.exports},86418:(t,e,r)=>{var n=r(51446),i=r(19996),o=r(78363),u=r(79299);t.exports=function(t,e){var r=void 0!==n&&i(t)||t["@@iterator"];if(!r){if(o(t)||(r=u(t))||e&&t&&"number"==typeof t.length){r&&(t=r);var s=0,a=function(){};return{s:a,n:function(){return s>=t.length?{done:!0}:{done:!1,value:t[s++]}},e:function(t){throw t},f:a}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var c,f=!0,l=!1;return{s:function(){r=r.call(t)},n:function(){var t=r.next();return f=t.done,t},e:function(t){l=!0,c=t},f:function(){try{f||null==r.return||r.return()}finally{if(l)throw c}}}},t.exports.__esModule=!0,t.exports.default=t.exports},10098:(t,e,r)=>{var n=r(13535),i=r(66380),o=r(8647),u=r(214);t.exports=function(t){var e=o();return function(){var r,o=i(t);if(e){var s=i(this).constructor;r=n(o,arguments,s)}else r=o.apply(this,arguments);return u(this,r)}},t.exports.__esModule=!0,t.exports.default=t.exports},87672:(t,e,r)=>{var n=r(44341);t.exports=function(t,e,r){return e in t?n(t,e,{value:r,enumerable:!0,configurable:!0,writable:!0}):t[e]=r,t},t.exports.__esModule=!0,t.exports.default=t.exports},5872:(t,e,r)=>{var n=r(12088);function i(){return t.exports=i=n||function(t){for(var e=1;e<arguments.length;e++){var r=arguments[e];for(var n in r)Object.prototype.hasOwnProperty.call(r,n)&&(t[n]=r[n])}return t},t.exports.__esModule=!0,t.exports.default=t.exports,i.apply(this,arguments)}t.exports=i,t.exports.__esModule=!0,t.exports.default=t.exports},31474:(t,e,r)=>{var n=r(83863),i=r(58377),o=r(16649);function u(){return"undefined"!=typeof Reflect&&n?(t.exports=u=n,t.exports.__esModule=!0,t.exports.default=t.exports):(t.exports=u=function(t,e,rLine truncated
|
||||
//# sourceMappingURL=swagger-ui-standalone-preset.js.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"sources":["webpack://SwaggerUIStandalonePreset/webpack/universalModuleDefinition","webpack://SwaggerUIStandalonePreset/webpack/bootstrap","webpack://SwaggerUIStandalonePreset/./node_modules/immutable/dist/immutable.js","webpack://SwaggerUIStandalonePreset/./node_modules/react/react.js","webpack://SwaggerUIStandalonePreset/./node_modules/@babel/runtime-corejs2/core-js/array/is-array.js","webpack://SwaggerUIStandalonePreset/./node_modules/js-yaml/lib/js-yaml/type.js","webpack://SwaggerUIStandalonePreset/./node_modules/core-js/library/modules/_core.js","webpack://SwaggerUIStandalonePreset/./node_modules/@babel/runtime-corejs2/helpers/defineProperty.js","webpack://SwaggerUIStandalonePreset/./src/core/utils.js","webpack://SwaggerUIStandalonePreset/./node_modules/inherits/inherits_browser.js","webpack://SwaggerUIStandalonePreset/./node_modules/safe-buffer/index.js","webpack://SwaggerUIStandalonePreset/./node_modules/@babel/runtime-corejs2/helpers/typeof.js","webpack://SwaggerUIStandalonePreset/(webpack)/buildin/global.js","webpack://SwaggerUIStandalonePreset/./node_modules/lodash/_root.js","webpack://SwaggerUIStandalonePreset/./node_modules/lodash/isArray.js","webpack://SwaggerUIStandalonePreset/./node_modules/@babel/runtime-corejs2/helpers/assertThisInitialized.js","webpack://SwaggerUIStandalonePreset/./node_modules/@babel/runtime-corejs2/core-js/object/keys.js","webpack://SwaggerUIStandalonePreset/./node_modules/core-js/library/modules/_export.js","webpack://SwaggerUIStandalonePreset/./node_modules/core-js/library/modules/_global.js","webpack://SwaggerUIStandalonePreset/./node_modules/core-js/library/modules/_wks.js","webpack://SwaggerUIStandalonePreset/./node_modules/es5-ext/object/valid-callable.js","webpack://SwaggerUIStandalonePreset/./node_modules/core-js/library/modules/_object-dp.js","webpack://SwaggerUIStandalonePreset/./node_modules/core-js/library/modules/_descriptors.js","webpack://SwaggerUIStandalonePreset/./node_modules/core-js/library/modules/_has.js","webpack://SwaggerUIStandalonePreset/./node_modules/process/browser.js","webpack://SwaggerUIStandalonePreset/./node_modules/readable-stream/lib/_stream_duplex.js","webpack://SwaggerUIStandalonePreset/./node_modules/es5-ext/object/is-value.js","webpack://SwaggerUIStandalonePreset/./node_modules/es5-ext/number/to-pos-integer.js","webpack://SwaggerUIStandalonePreset/./node_modules/core-js/library/modules/_hide.js","webpack://SwaggerUIStandalonePreset/./node_modules/core-js/library/modules/_an-object.js","webpack://SwaggerUIStandalonePreset/./node_modules/core-js/library/modules/_is-object.js","webpack://SwaggerUIStandalonePreset/./node_modules/core-js/library/modules/_fails.js","webpack://SwaggerUIStandalonePreset/./node_modules/core-js/library/modules/_to-iobject.js","webpack://SwaggerUIStandalonePreset/./node_modules/react/lib/ReactElement.js","webpack://SwaggerUIStandalonePreset/./node_modules/lodash/_getNative.js","webpack://SwaggerUIStandalonePreset/./node_modules/es5-ext/object/valid-value.js","webpack://SwaggerUIStandalonePreset/./node_modules/sha.js/hash.js","webpack://SwaggerUIStandalonePreset/./node_modules/js-yaml/lib/js-yaml/common.js","webpack://SwaggerUIStandalonePreset/./node_modules/js-yaml/lib/js-yaml/schema.js","webpack://SwaggerUIStandalonePreset/./node_modules/lodash/eq.js","webpack://SwaggerUIStandalonePreset/./node_modules/core-js/library/modules/_object-keys.js","webpack://SwaggerUIStandalonePreset/./node_modules/object-assign/index.js","webpack://SwaggerUIStandalonePreset/./node_modules/fbjs/lib/invariant.js","webpack://SwaggerUIStandalonePreset/./node_modules/lodash/toString.js","webpack://SwaggerUIStandalonePreset/./node_modules/lodash/_baseGetTag.js","webpack://SwaggerUIStandalonePreset/./node_modules/lodash/isObjectLike.js","webpack://SwaggerUIStandalonePreset/./node_modules/lodash/isObject.js","webpack://SwaggerUIStandalonePreset/./node_modules/core-util-is/lib/util.js","webpack://SwaggerUIStandalonePreset/./node_modules/js-yaml/lib/js-yaml/exception.js","webpack://SwaggerUIStandalonePreset/./node_modules/js-yaml/lib/js-yaml/schema/default_safe.js","webpack://SwaggerUIStandalonePreset/./src/core/window.js","webpack://SwaggerUIStandalonePreset/./node_modules/core-js/library/modules/_property-desc.js","webpack://SwaggerUIStandalonePreset/./node_modules/core-js/library/modules/_library.js","webpack://SwaggerUIStandalonePreset/./node_modules/core-js/library/modules/_iterators.js","webpack://SwaggerUIStandalonePreset/./node_modules/core-js/library/modules/_uid.js","webpack://SwaggerUIStandalonePreset/./node_modules/core-js/library/modules/_to-object.js","webpack://SwaggerUIStandalonePreset/./node_modules/core-js/library/modules/_object-pie.js","webpack://SwaggerUIStandalonePreset/./node_modules/react/lib/reactProdInvariant.js","webpack://SwaggerUIStandalonePreset/./node_modules/node-libs-browser/node_modules/buffer/index.js","webpack://SwaggerUIStandalonePreset/./node_modules/lodash/_Symbol.js","webpack://SwaggerUILine truncated
|
||||
@@ -0,0 +1,4 @@
|
||||
.swagger-ui{color:#3b4151;
|
||||
/*! normalize.css v7.0.0 | MIT License | github.com/necolas/normalize.css */font-family:sans-serif}.swagger-ui html{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%;line-height:1.15}.swagger-ui body{margin:0}.swagger-ui article,.swagger-ui aside,.swagger-ui footer,.swagger-ui header,.swagger-ui nav,.swagger-ui section{display:block}.swagger-ui h1{font-size:2em;margin:.67em 0}.swagger-ui figcaption,.swagger-ui figure,.swagger-ui main{display:block}.swagger-ui figure{margin:1em 40px}.swagger-ui hr{box-sizing:content-box;height:0;overflow:visible}.swagger-ui pre{font-family:monospace,monospace;font-size:1em}.swagger-ui a{-webkit-text-decoration-skip:objects;background-color:transparent}.swagger-ui abbr[title]{border-bottom:none;text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted}.swagger-ui b,.swagger-ui strong{font-weight:inherit;font-weight:bolder}.swagger-ui code,.swagger-ui kbd,.swagger-ui samp{font-family:monospace,monospace;font-size:1em}.swagger-ui dfn{font-style:italic}.swagger-ui mark{background-color:#ff0;color:#000}.swagger-ui small{font-size:80%}.swagger-ui sub,.swagger-ui sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}.swagger-ui sub{bottom:-.25em}.swagger-ui sup{top:-.5em}.swagger-ui audio,.swagger-ui video{display:inline-block}.swagger-ui audio:not([controls]){display:none;height:0}.swagger-ui img{border-style:none}.swagger-ui svg:not(:root){overflow:hidden}.swagger-ui button,.swagger-ui input,.swagger-ui optgroup,.swagger-ui select,.swagger-ui textarea{font-family:sans-serif;font-size:100%;line-height:1.15;margin:0}.swagger-ui button,.swagger-ui input{overflow:visible}.swagger-ui button,.swagger-ui select{text-transform:none}.swagger-ui [type=reset],.swagger-ui [type=submit],.swagger-ui button,.swagger-ui html [type=button]{-webkit-appearance:button}.swagger-ui [type=button]::-moz-focus-inner,.swagger-ui [type=reset]::-moz-focus-inner,.swagger-ui [type=submit]::-moz-focus-inner,.swagger-ui button::-moz-focus-inner{border-style:none;padding:0}.swagger-ui [type=button]:-moz-focusring,.swagger-ui [type=reset]:-moz-focusring,.swagger-ui [type=submit]:-moz-focusring,.swagger-ui button:-moz-focusring{outline:1px dotted ButtonText}.swagger-ui fieldset{padding:.35em .75em .625em}.swagger-ui legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}.swagger-ui progress{display:inline-block;vertical-align:baseline}.swagger-ui textarea{overflow:auto}.swagger-ui [type=checkbox],.swagger-ui [type=radio]{box-sizing:border-box;padding:0}.swagger-ui [type=number]::-webkit-inner-spin-button,.swagger-ui [type=number]::-webkit-outer-spin-button{height:auto}.swagger-ui [type=search]{-webkit-appearance:textfield;outline-offset:-2px}.swagger-ui [type=search]::-webkit-search-cancel-button,.swagger-ui [type=search]::-webkit-search-decoration{-webkit-appearance:none}.swagger-ui ::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}.swagger-ui details,.swagger-ui menu{display:block}.swagger-ui summary{display:list-item}.swagger-ui canvas{display:inline-block}.swagger-ui [hidden],.swagger-ui template{display:none}.swagger-ui .debug *{outline:1px solid gold}.swagger-ui .debug-white *{outline:1px solid #fff}.swagger-ui .debug-black *{outline:1px solid #000}.swagger-ui .debug-grid{background:transparent url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAYAAADED76LAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyhpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMTExIDc5LjE1ODMyNSwgMjAxNS8wOS8xMC0wMToxMDoyMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6MTRDOTY4N0U2N0VFMTFFNjg2MzZDQjkwNkQ4MjgwMEIiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6MTRDOTY4N0Q2N0VFMTFFNjg2MzZDQjkwNkQ4MjgwMEIiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTUgKE1hY2ludG9zaCkiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo3NjcyQkQ3NjY3QzUxMUU2QjJCQ0UyNDA4MTAwMjE3MSIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo3NjcyQkQ3NzY3QzUxMUU2QjJCQ0UyNDA4MTAwMjE3MSIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PsBS+GMAAAAjSURBVHjaYvz//z8DLsD4gcGXiYEAGBIKGBne//fFpwAgwAB98AaF2pjlUQAAAABJRU5ErkJggg==) repeat 0 0}.swagger-ui .debug-grid-16{background:transparent url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyhpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eLine truncated
|
||||
|
||||
/*# sourceMappingURL=swagger-ui.css.map*/
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"sources":["webpack:///./src/style/main.scss","webpack:///./node_modules/tachyons-sass/scss/_normalize.scss","webpack:///./src/style/_type.scss","webpack:///main.scss","webpack:///./node_modules/tachyons-sass/scss/_debug-children.scss","webpack:///./node_modules/tachyons-sass/scss/_debug-grid.scss","webpack:///./node_modules/tachyons-sass/scss/_box-sizing.scss","webpack:///./node_modules/tachyons-sass/scss/_aspect-ratios.scss","webpack:///./node_modules/tachyons-sass/scss/_images.scss","webpack:///./node_modules/tachyons-sass/scss/_background-size.scss","webpack:///./node_modules/tachyons-sass/scss/_background-position.scss","webpack:///./node_modules/tachyons-sass/scss/_outlines.scss","webpack:///./node_modules/tachyons-sass/scss/_borders.scss","webpack:///./node_modules/tachyons-sass/scss/_border-colors.scss","webpack:///./node_modules/tachyons-sass/scss/_border-radius.scss","webpack:///./node_modules/tachyons-sass/scss/_border-style.scss","webpack:///./node_modules/tachyons-sass/scss/_border-widths.scss","webpack:///./node_modules/tachyons-sass/scss/_box-shadow.scss","webpack:///./node_modules/tachyons-sass/scss/_code.scss","webpack:///./node_modules/tachyons-sass/scss/_coordinates.scss","webpack:///./node_modules/tachyons-sass/scss/_clears.scss","webpack:///./node_modules/tachyons-sass/scss/_flexbox.scss","webpack:///./node_modules/tachyons-sass/scss/_display.scss","webpack:///./node_modules/tachyons-sass/scss/_floats.scss","webpack:///./node_modules/tachyons-sass/scss/_font-family.scss","webpack:///./node_modules/tachyons-sass/scss/_font-style.scss","webpack:///./node_modules/tachyons-sass/scss/_font-weight.scss","webpack:///./node_modules/tachyons-sass/scss/_forms.scss","webpack:///./node_modules/tachyons-sass/scss/_heights.scss","webpack:///./node_modules/tachyons-sass/scss/_letter-spacing.scss","webpack:///./node_modules/tachyons-sass/scss/_line-height.scss","webpack:///./node_modules/tachyons-sass/scss/_links.scss","webpack:///./node_modules/tachyons-sass/scss/_lists.scss","webpack:///./node_modules/tachyons-sass/scss/_max-widths.scss","webpack:///./node_modules/tachyons-sass/scss/_widths.scss","webpack:///./node_modules/tachyons-sass/scss/_overflow.scss","webpack:///./node_modules/tachyons-sass/scss/_position.scss","webpack:///./node_modules/tachyons-sass/scss/_opacity.scss","webpack:///./node_modules/tachyons-sass/scss/_rotations.scss","webpack:///./node_modules/tachyons-sass/scss/_skins.scss","webpack:///./node_modules/tachyons-sass/scss/_skins-pseudo.scss","webpack:///./node_modules/tachyons-sass/scss/_spacing.scss","webpack:///./node_modules/tachyons-sass/scss/_variables.scss","webpack:///./node_modules/tachyons-sass/scss/_negative-margins.scss","webpack:///./node_modules/tachyons-sass/scss/_tables.scss","webpack:///./node_modules/tachyons-sass/scss/_text-decoration.scss","webpack:///./node_modules/tachyons-sass/scss/_text-align.scss","webpack:///./node_modules/tachyons-sass/scss/_text-transform.scss","webpack:///./node_modules/tachyons-sass/scss/_type-scale.scss","webpack:///./node_modules/tachyons-sass/scss/_typography.scss","webpack:///./node_modules/tachyons-sass/scss/_utilities.scss","webpack:///./node_modules/tachyons-sass/scss/_visibility.scss","webpack:///./node_modules/tachyons-sass/scss/_white-space.scss","webpack:///./node_modules/tachyons-sass/scss/_vertical-align.scss","webpack:///./node_modules/tachyons-sass/scss/_hovers.scss","webpack:///./node_modules/tachyons-sass/scss/_z-index.scss","webpack:///./node_modules/tachyons-sass/scss/_nested.scss","webpack:///./src/style/_layout.scss","webpack:///./src/style/_variables.scss","webpack:///./src/style/_buttons.scss","webpack:///./src/style/_mixins.scss","webpack:///./src/style/_form.scss","webpack:///./src/style/_modal.scss","webpack:///./src/style/_models.scss","webpack:///./src/style/_servers.scss","webpack:///./src/style/_table.scss","webpack:///./src/style/_topbar.scss","webpack:///./src/style/_information.scss","webpack:///./src/style/_authorize.scss","webpack:///./src/style/_errors.scss","webpack:///./src/style/_split-pane-mode.scss","webpack:///./src/style/_markdown.scss"],"names":[],"mappings":"AAAA;ECOA,4ECLI,sBAAuB,CAEvB,aCmhCJ,CHvhCA,iBCmBE,gBAAiB,CACjB,yBAA0B,CAC1B,6BE2gCF,CHhiCA,iBCgCE,QEogCF,CHpiCA,gHC6CE,aEggCF,CH7iCA,eCsDE,aAAc,CACd,cE2/BF,CHljCA,2DCqEE,aEo/BF,CHzjCA,mBC6EE,eEg/BF,CH7jCA,eCsFE,sBAAuB,CACvB,QAAS,CACT,gBE8+BF,CHtkCA,gBCiGE,+BAAiC,CACjC,aE2+BF,CH7kCA,cC8GE,4BAA6B,CAC7B,oCEq+BF,CHplCA,wBCwHE,kBAAmB,CACnB,yBAA0B,CAC1B,wCAAiC,CAAjC,gCEm+BF,CH7lCA,iCCmIE,mBAAoB,CASpB,kBEs9BF,CHlmCA,kDCuJE,+BAAiC,CACjC,aEw9BF,CHhnCA,gBCgKE,iBEo9BF,CHpnCA,iBCwKE,qBAAsB,CACtB,UEg9BF,CHznCA,kBCiLE,aE48BF,CH7nCA,gCC2LE,aAAc,CACd,aAAc,CACd,iBAAkB,CAClB,uBEu8BF,CHroCA,gBCkME,aEu8BF,CHzoCA,gBCsME,SEu8BF,CH7oCA,oCCkNE,oBEg8BF,CHlpCA,kCC0NE,YAAa,CACb,QE47BF,CHvpCA,gBCmOE,iBEw7BF,CH3pCA,2BC2OE,eEo7BF,CH/pCA,kGC2PE,sBAAuB,CACvB,cAAe,CACf,gBAAiB,CACjB,QEg7BF,CH9qCA,qCCwQE,gBE46BF,CHprCA,sCCkRE,mBEw6BF,CH1rCA,qGC+RE,yBEm6BF,CLine truncated
|
||||
@@ -0,0 +1,2 @@
|
||||
!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.SwaggerUICore=t():e.SwaggerUICore=t()}(this,(function(){return(()=>{var e={66419:(e,t,r)=>{e.exports=r(27698)},41511:(e,t,r)=>{e.exports=r(83363)},11128:(e,t,r)=>{e.exports=r(57784)},54103:(e,t,r)=>{e.exports=r(28196)},77766:(e,t,r)=>{e.exports=r(8065)},72119:(e,t,r)=>{e.exports=r(57448)},10062:(e,t,r)=>{e.exports=r(29455)},44494:(e,t,r)=>{e.exports=r(69743)},20116:(e,t,r)=>{e.exports=r(11955)},62462:(e,t,r)=>{e.exports=r(96064)},94473:(e,t,r)=>{e.exports=r(61577)},78914:(e,t,r)=>{e.exports=r(46279)},78580:(e,t,r)=>{e.exports=r(33778)},81643:(e,t,r)=>{e.exports=r(19373)},69301:(e,t,r)=>{e.exports=r(73819)},23054:(e,t,r)=>{e.exports=r(11022)},2991:(e,t,r)=>{e.exports=r(61798)},32366:(e,t,r)=>{e.exports=r(52527)},39291:(e,t,r)=>{e.exports=r(36857)},3649:(e,t,r)=>{e.exports=r(82073)},77149:(e,t,r)=>{e.exports=r(45286)},47302:(e,t,r)=>{e.exports=r(62856)},92762:(e,t,r)=>{e.exports=r(2348)},29828:(e,t,r)=>{e.exports=r(35178)},25843:(e,t,r)=>{e.exports=r(76361)},89400:(e,t,r)=>{e.exports=r(71815)},59340:(e,t,r)=>{e.exports=r(8933)},39392:(e,t,r)=>{e.exports=r(15868)},51942:(e,t,r)=>{e.exports=r(63383)},63978:(e,t,r)=>{e.exports=r(41910)},26295:(e,t,r)=>{e.exports=r(86209)},86902:(e,t,r)=>{e.exports=r(23059)},20455:(e,t,r)=>{e.exports=r(47795)},93476:(e,t,r)=>{e.exports=r(27460)},33032:(e,t,r)=>{e.exports=r(27989)},94435:(e,t,r)=>{e.exports=r(73926)},39969:(e,t,r)=>{e.exports=r(57641)},52424:(e,t,r)=>{e.exports=r(72010)},53592:(e,t,r)=>{e.exports=r(27385)},78363:(e,t,r)=>{e.exports=r(81522)},19996:(e,t,r)=>{e.exports=r(32209)},51445:(e,t,r)=>{e.exports=r(14122)},30699:(e,t,r)=>{e.exports=r(44442)},28834:(e,t,r)=>{e.exports=r(57152)},95683:(e,t,r)=>{e.exports=r(69447)},95238:(e,t,r)=>{e.exports=r(81493)},90:(e,t,r)=>{e.exports=r(86672)},12088:(e,t,r)=>{e.exports=r(60269)},189:(e,t,r)=>{e.exports=r(76094)},32752:(e,t,r)=>{e.exports=r(70573)},44341:(e,t,r)=>{e.exports=r(73685)},58377:(e,t,r)=>{e.exports=r(27533)},13038:(e,t,r)=>{e.exports=r(39057)},63263:(e,t,r)=>{e.exports=r(84710)},24889:(e,t,r)=>{e.exports=r(74303)},89356:(e,t,r)=>{e.exports=r(93799)},79542:(e,t,r)=>{e.exports=r(55122)},69798:(e,t,r)=>{e.exports=r(29531)},13535:(e,t,r)=>{e.exports=r(10856)},83863:(e,t,r)=>{e.exports=r(31524)},51446:(e,t,r)=>{e.exports=r(86600)},23882:(e,t,r)=>{e.exports=r(9759)},34243:e=>{e.exports=function(e,t){(null==t||t>e.length)&&(t=e.length);for(var r=0,n=new Array(t);r<t;r++)n[r]=e[r];return n},e.exports.__esModule=!0,e.exports.default=e.exports},57726:(e,t,r)=>{var n=r(78363);e.exports=function(e){if(n(e))return e},e.exports.__esModule=!0,e.exports.default=e.exports},46868:(e,t,r)=>{var n=r(78363),a=r(34243);e.exports=function(e){if(n(e))return a(e)},e.exports.__esModule=!0,e.exports.default=e.exports},5824:e=>{e.exports=function(e){if(void 0===e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return e},e.exports.__esModule=!0,e.exports.default=e.exports},51161:(e,t,r)=>{var n=r(69798);function a(e,t,r,a,o,s,i){try{var l=e[s](i),c=l.value}catch(e){return void r(e)}l.done?t(c):n.resolve(c).then(a,o)}e.exports=function(e){return function(){var t=this,r=arguments;return new n((function(n,o){var s=e.apply(t,r);function i(e){a(s,n,o,i,l,"next",e)}function l(e){a(s,n,o,i,l,"throw",e)}i(void 0)}))}},e.exports.__esModule=!0,e.exports.default=e.exports},26394:e=>{e.exports=function(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")},e.exports.__esModule=!0,e.exports.default=e.exports},74003:(e,t,r)=>{var n=r(13535),a=r(51445),o=r(5613),s=r(8647);function i(t,r,l){return s()?(e.exports=i=n,e.exports.__esModule=!0,e.exports.default=e.exports):(e.exports=i=function(e,t,r){var n=[null];n.push.apply(n,t);var s=new(a(Function).apply(e,n));return r&&o(s,r.prototype),s},e.exports.__esModule=!0,e.exports.default=e.exports),i.apply(null,arguments)}e.exports=i,e.exports.__esModule=!0,e.exports.default=e.exports},69198:(e,t,r)=>{var n=r(44341);function a(e,t){for(var r=0;r<t.length;r++){var a=t[r];a.enumerable=a.enumerable||!1,a.configurable=!0,"value"in a&&(a.writable=!0),n(e,a.key,a)}}e.exports=function(e,t,r){return t&&a(e.prototype,t),r&&a(e,r),n(e,"prototype",{writable:!1}),e},e.exports.__esModule=!0,e.exports.default=e.exports},86418:(e,t,r)=>{var n=r(51446),a=r(19996),o=r(78363),s=r(79299);e.exports=function(e,t){var r=void 0!==n&&a(e)||e["@@iterator"];if(!r){if(o(e)||(r=s(e))||t&&e&&"number"==typeof e.length){r&&(e=r);var i=0,l=function(){};return{s:l,n:function(){return i>=e.length?{done:!0}:{done:!1,value:e[i++]}},e:function(e){throw e},f:l}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var c,u=!0,p=!1;return{s:function(){r=r.call(e)},n:function(){var e=r.next();return u=e.done,e},e:functioLine truncated
|
||||
//# sourceMappingURL=swagger-ui.js.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"sources":["webpack://SwaggerUICore/webpack/universalModuleDefinition","webpack://SwaggerUICore/webpack/bootstrap","webpack://SwaggerUICore/external \"react\"","webpack://SwaggerUICore/external \"immutable\"","webpack://SwaggerUICore/./node_modules/@babel/runtime-corejs2/helpers/defineProperty.js","webpack://SwaggerUICore/./src/core/utils.js","webpack://SwaggerUICore/./node_modules/@babel/runtime-corejs2/helpers/classCallCheck.js","webpack://SwaggerUICore/./node_modules/@babel/runtime-corejs2/helpers/createClass.js","webpack://SwaggerUICore/./node_modules/@babel/runtime-corejs2/helpers/possibleConstructorReturn.js","webpack://SwaggerUICore/./node_modules/@babel/runtime-corejs2/helpers/getPrototypeOf.js","webpack://SwaggerUICore/./node_modules/@babel/runtime-corejs2/helpers/inherits.js","webpack://SwaggerUICore/./node_modules/@babel/runtime-corejs2/helpers/assertThisInitialized.js","webpack://SwaggerUICore/external \"prop-types\"","webpack://SwaggerUICore/external \"reselect\"","webpack://SwaggerUICore/./node_modules/@babel/runtime-corejs2/helpers/toConsumableArray.js","webpack://SwaggerUICore/./node_modules/@babel/runtime-corejs2/helpers/slicedToArray.js","webpack://SwaggerUICore/./node_modules/@babel/runtime-corejs2/core-js/array/is-array.js","webpack://SwaggerUICore/./node_modules/@babel/runtime-corejs2/core-js/object/assign.js","webpack://SwaggerUICore/./src/core/window.js","webpack://SwaggerUICore/./node_modules/@babel/runtime-corejs2/core-js/object/keys.js","webpack://SwaggerUICore/external \"react-immutable-proptypes\"","webpack://SwaggerUICore/./node_modules/@babel/runtime-corejs2/helpers/extends.js","webpack://SwaggerUICore/./node_modules/core-js/library/modules/_core.js","webpack://SwaggerUICore/./src/core/plugins/oas3/helpers.jsx","webpack://SwaggerUICore/./node_modules/@babel/runtime-corejs2/helpers/typeof.js","webpack://SwaggerUICore/./node_modules/@babel/runtime-corejs2/core-js/json/stringify.js","webpack://SwaggerUICore/./src/core/plugins/spec/actions.js","webpack://SwaggerUICore/./node_modules/core-js/library/modules/_export.js","webpack://SwaggerUICore/./node_modules/core-js/modules/_wks.js","webpack://SwaggerUICore/./node_modules/core-js/library/modules/_global.js","webpack://SwaggerUICore/./node_modules/core-js/library/modules/_wks.js","webpack://SwaggerUICore/./node_modules/core-js/modules/_export.js","webpack://SwaggerUICore/./node_modules/core-js/modules/_global.js","webpack://SwaggerUICore/./node_modules/core-js/modules/_string-html.js","webpack://SwaggerUICore/./node_modules/lodash/isArray.js","webpack://SwaggerUICore/./node_modules/@babel/runtime-corejs2/helpers/objectWithoutProperties.js","webpack://SwaggerUICore/./src/core/plugins/err/actions.js","webpack://SwaggerUICore/./node_modules/core-js/modules/_an-object.js","webpack://SwaggerUICore/./node_modules/core-js/library/modules/_object-dp.js","webpack://SwaggerUICore/./node_modules/core-js/library/modules/_an-object.js","webpack://SwaggerUICore/./node_modules/core-js/library/modules/_is-object.js","webpack://SwaggerUICore/./node_modules/core-js/library/modules/_descriptors.js","webpack://SwaggerUICore/./node_modules/lodash/_root.js","webpack://SwaggerUICore/external \"swagger-client\"","webpack://SwaggerUICore/./node_modules/lodash/isObject.js","webpack://SwaggerUICore/./node_modules/@babel/runtime-corejs2/core-js/object/define-property.js","webpack://SwaggerUICore/external \"classnames\"","webpack://SwaggerUICore/./node_modules/@babel/runtime-corejs2/core-js/object/get-own-property-symbols.js","webpack://SwaggerUICore/./src/core/plugins/oas3/actions.js","webpack://SwaggerUICore/./node_modules/core-js/library/modules/_ctx.js","webpack://SwaggerUICore/./node_modules/lodash/isObjectLike.js","webpack://SwaggerUICore/./src/core/plugins/spec/selectors.js","webpack://SwaggerUICore/./node_modules/@babel/runtime-corejs2/core-js/object/get-own-property-descriptors.js","webpack://SwaggerUICore/./node_modules/@babel/runtime-corejs2/core-js/object/get-own-property-descriptor.js","webpack://SwaggerUICore/./src/core/plugins/auth/actions.js","webpack://SwaggerUICore/./node_modules/core-js/modules/_core.js","webpack://SwaggerUICore/./node_modules/core-js/modules/_defined.js","webpack://SwaggerUICore/./node_modules/core-js/modules/_to-length.js","webpack://SwaggerUICore/./node_modules/core-js/library/modules/_to-iobject.js","webpack://SwaggerUICore/./node_modules/core-js/library/modules/_hide.js","webpack://SwaggerUICore/./node_modules/lodash/toString.js","webpack://SwaggerUICore/./src/core/plugins/layout/actions.js","webpack://SwaggerUICore/external \"deep-extend\"","webpack://SwaggerUICore/./node_modules/core-js/modules/_hide.js","webpack://SwaggerUICore/./node_modules/core-js/library/modules/_has.js","webpack://SwaggerUICore/./node_modules/core-js/library/modules/_fails.js","webpack://SwaggerUICore/./node_modules/lodash/_baseGetTag.js","webpack://SwaggerUICore/./node_modules/lodash/_getNative.js","webpack://SwaggerUICore/./node_moLine truncated
|
||||
File diff suppressed because it is too large.
Load diff
+1058
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,94 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
_ "nodemaster/routers"
|
||||
|
||||
beego "github.com/beego/beego/v2/server/web"
|
||||
"github.com/beego/beego/v2/core/logs"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func init() {
|
||||
_, file, _, _ := runtime.Caller(0)
|
||||
apppath, _ := filepath.Abs(filepath.Dir(filepath.Join(file, ".."+string(filepath.Separator))))
|
||||
beego.TestBeegoInit(apppath)
|
||||
}
|
||||
|
||||
func TestGetNode(t *testing.T) {
|
||||
r, _ := http.NewRequest("GET", "/v1/node", nil)
|
||||
w := httptest.NewRecorder()
|
||||
beego.BeeApp.Handlers.ServeHTTP(w, r)
|
||||
|
||||
logs.Info("testing", "TestGetNode", "Code[%d]\n%s", w.Code, w.Body.String())
|
||||
|
||||
Convey("GET /v1/node", t, func() {
|
||||
Convey("Status Code Should Be 200", func() {
|
||||
So(w.Code, ShouldEqual, 200)
|
||||
})
|
||||
Convey("The Result Should Not Be Empty", func() {
|
||||
So(w.Body.Len(), ShouldBeGreaterThan, 0)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetNodes(t *testing.T) {
|
||||
r, _ := http.NewRequest("GET", "/v1/nodes", nil)
|
||||
w := httptest.NewRecorder()
|
||||
beego.BeeApp.Handlers.ServeHTTP(w, r)
|
||||
|
||||
logs.Info("testing", "TestGetNodes", "Code[%d]\n%s", w.Code, w.Body.String())
|
||||
|
||||
Convey("GET /v1/nodes", t, func() {
|
||||
Convey("Status Code Should Be 200", func() {
|
||||
So(w.Code, ShouldEqual, 200)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetServices(t *testing.T) {
|
||||
r, _ := http.NewRequest("GET", "/v1/services", nil)
|
||||
w := httptest.NewRecorder()
|
||||
beego.BeeApp.Handlers.ServeHTTP(w, r)
|
||||
|
||||
logs.Info("testing", "TestGetServices", "Code[%d]\n%s", w.Code, w.Body.String())
|
||||
|
||||
Convey("GET /v1/services", t, func() {
|
||||
Convey("Status Code Should Be 200", func() {
|
||||
So(w.Code, ShouldEqual, 200)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetNodeBackupTargets(t *testing.T) {
|
||||
r, _ := http.NewRequest("GET", "/v1/node/backup/targets", nil)
|
||||
w := httptest.NewRecorder()
|
||||
beego.BeeApp.Handlers.ServeHTTP(w, r)
|
||||
|
||||
logs.Info("testing", "TestGetNodeBackupTargets", "Code[%d]\n%s", w.Code, w.Body.String())
|
||||
|
||||
Convey("GET /v1/node/backup/targets", t, func() {
|
||||
Convey("Status Code Should Be 200", func() {
|
||||
So(w.Code, ShouldEqual, 200)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetNodeBackupProgress(t *testing.T) {
|
||||
r, _ := http.NewRequest("GET", "/v1/node/backup/progress", nil)
|
||||
w := httptest.NewRecorder()
|
||||
beego.BeeApp.Handlers.ServeHTTP(w, r)
|
||||
|
||||
logs.Info("testing", "TestGetNodeBackupProgress", "Code[%d]\n%s", w.Code, w.Body.String())
|
||||
|
||||
Convey("GET /v1/node/backup/progress", t, func() {
|
||||
Convey("Status Code Should Be 200", func() {
|
||||
So(w.Code, ShouldEqual, 200)
|
||||
})
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user