Add type-aware backup job forms
This commit is contained in:
@@ -37,6 +37,7 @@ func New(socket string, svc *service.Service, log *slog.Logger) *Server {
|
||||
mux.HandleFunc("DELETE /v1/secrets/{id}", s.deleteSecret)
|
||||
mux.HandleFunc("GET /v1/runs", s.runs)
|
||||
mux.HandleFunc("GET /v1/filesystem/directories", s.directories)
|
||||
mux.HandleFunc("GET /v1/workloads/{kind}", s.workloads)
|
||||
mux.HandleFunc("POST /v1/jobs/{id}/run", s.runJob)
|
||||
mux.HandleFunc("POST /v1/runs/{id}/cancel", s.cancelRun)
|
||||
mux.HandleFunc("POST /v1/repositories/{id}/test", s.testRepository)
|
||||
@@ -125,6 +126,16 @@ func (s *Server) directories(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
writeJSON(w, 200, items)
|
||||
}
|
||||
func (s *Server) workloads(w http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second)
|
||||
defer cancel()
|
||||
items, err := s.service.DiscoverWorkloads(ctx, model.JobType(r.PathValue("kind")))
|
||||
if err != nil {
|
||||
writeError(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, 200, items)
|
||||
}
|
||||
|
||||
func (s *Server) runJob(w http.ResponseWriter, r *http.Request) {
|
||||
run, err := s.service.EnqueueJob(r.PathValue("id"), 10)
|
||||
|
||||
@@ -74,6 +74,13 @@ func ValidateJob(j Job) error {
|
||||
return errors.New("source requires path or workloadId")
|
||||
}
|
||||
}
|
||||
if j.Type == JobDocker || j.Type == JobVM {
|
||||
for _, source := range j.Sources {
|
||||
if source.WorkloadID == "" {
|
||||
return errors.New("docker and vm jobs require workload selections")
|
||||
}
|
||||
}
|
||||
}
|
||||
if j.Compression != "auto" && j.Compression != "off" && j.Compression != "max" {
|
||||
return errors.New("compression must be auto, off, or max")
|
||||
}
|
||||
|
||||
@@ -34,3 +34,14 @@ func TestRepositoryCannotBeSharedByJobs(t *testing.T) {
|
||||
t.Fatal("shared repository accepted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDockerJobRequiresWorkloadSources(t *testing.T) {
|
||||
job := Job{SchemaVersion: 1, ID: "docker-job", Name: "Docker", Type: JobDocker, RepositoryID: "repo", Sources: []Source{{Path: "/mnt/user/appdata"}}, Consistency: Consistency{Mode: "live"}, Compression: "auto"}
|
||||
if err := ValidateJob(job); err == nil {
|
||||
t.Fatal("docker path source accepted without workload selection")
|
||||
}
|
||||
job.Sources = []Source{{WorkloadID: "plex"}}
|
||||
if err := ValidateJob(job); err != nil {
|
||||
t.Fatalf("valid docker workload rejected: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,12 @@ import (
|
||||
|
||||
type WorkloadManager struct{ RuntimeDir string }
|
||||
|
||||
type Workload struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
type Prepared struct {
|
||||
Sources []string
|
||||
Stopped []model.Source
|
||||
@@ -23,6 +29,49 @@ type Prepared struct {
|
||||
Kind model.JobType
|
||||
}
|
||||
|
||||
func (w *WorkloadManager) Discover(ctx context.Context, kind model.JobType) ([]Workload, error) {
|
||||
switch kind {
|
||||
case model.JobDocker:
|
||||
output, err := exec.CommandContext(ctx, "docker", "ps", "-a", "--format", "{{json .}}").Output()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("environment: list Docker containers: %w", err)
|
||||
}
|
||||
var result []Workload
|
||||
for _, line := range strings.Split(strings.TrimSpace(string(output)), "\n") {
|
||||
if strings.TrimSpace(line) == "" {
|
||||
continue
|
||||
}
|
||||
var item struct {
|
||||
ID string `json:"ID"`
|
||||
Names string `json:"Names"`
|
||||
Status string `json:"Status"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(line), &item); err != nil {
|
||||
return nil, fmt.Errorf("environment: decode Docker container list: %w", err)
|
||||
}
|
||||
result = append(result, Workload{ID: item.Names, Name: item.Names, Status: item.Status})
|
||||
}
|
||||
return result, nil
|
||||
case model.JobVM:
|
||||
output, err := exec.CommandContext(ctx, "virsh", "list", "--all", "--name").Output()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("environment: list virtual machines: %w", err)
|
||||
}
|
||||
var result []Workload
|
||||
for _, name := range strings.Split(strings.TrimSpace(string(output)), "\n") {
|
||||
name = strings.TrimSpace(name)
|
||||
if name == "" {
|
||||
continue
|
||||
}
|
||||
state, _ := exec.CommandContext(ctx, "virsh", "domstate", name).Output()
|
||||
result = append(result, Workload{ID: name, Name: name, Status: strings.TrimSpace(string(state))})
|
||||
}
|
||||
return result, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("validation: unsupported workload type %q", kind)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *WorkloadManager) Prepare(ctx context.Context, job model.Job) (Prepared, error) {
|
||||
prepared := Prepared{Kind: job.Type}
|
||||
for _, source := range job.Sources {
|
||||
|
||||
@@ -70,6 +70,10 @@ func (s *Service) BrowseDirectories(path string) ([]platform.DirectoryEntry, err
|
||||
return platform.BrowseDirectories(path)
|
||||
}
|
||||
|
||||
func (s *Service) DiscoverWorkloads(ctx context.Context, kind model.JobType) ([]platform.Workload, error) {
|
||||
return s.workloads.Discover(ctx, kind)
|
||||
}
|
||||
|
||||
func (s *Service) LastRun(jobID string) time.Time {
|
||||
var latest time.Time
|
||||
for _, run := range s.queue.Snapshot() {
|
||||
|
||||
Reference in New Issue
Block a user