initial commit

This commit is contained in:
yves committed 2026-07-27 14:41:04 +02:00
commit 5c01fbdd5f
37 files changed
+6082

No files matched your search

+69
View File
@@ -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
View File
Whitespace-only changes.
+10
View File
@@ -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:*
+352
View File
@@ -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()
}
+159
View File
@@ -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()
}
+397
View File
@@ -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()
}
+35
View File
@@ -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
)
+81
View File
@@ -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=
+125
View File
@@ -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)
}
+298
View File
@@ -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
View File
@@ -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
}
+59
View File
@@ -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")
}
+59
View File
@@ -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
View File
@@ -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
}
+148
View File
@@ -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()
}
+316
View File
@@ -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
}
+93
View File
@@ -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
}
+99
View File
@@ -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"`
}
+370
View File
@@ -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})
}
+51
View File
@@ -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

+60
View File
@@ -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>
+79
View File
@@ -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>
+3
View File
@@ -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
+1
View File
@@ -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
+2
View File
@@ -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
+3
View File
@@ -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
+3
View File
@@ -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
+4
View File
@@ -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*/
+1
View File
@@ -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
+2
View File
@@ -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
+1
View File
@@ -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
+1462
View File
File diff suppressed because it is too large. Load diff
+1058
View File
File diff suppressed because it is too large. Load diff
+94
View File
@@ -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)
})
})
}