181 lines
5.9 KiB
Go
181 lines
5.9 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"net/url"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type dockerContainerSummary struct {
|
|
ID string `json:"Id"`
|
|
Names []string `json:"Names"`
|
|
Image string `json:"Image"`
|
|
State string `json:"State"`
|
|
Status string `json:"Status"`
|
|
Labels map[string]string `json:"Labels"`
|
|
}
|
|
|
|
type dockerSkillService struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
Image string `json:"image"`
|
|
State string `json:"state"`
|
|
Status string `json:"status"`
|
|
Runtime string `json:"runtime,omitempty"`
|
|
WorkerName string `json:"worker_name,omitempty"`
|
|
Labels map[string]string `json:"labels,omitempty"`
|
|
}
|
|
|
|
type dockerControllerStatus struct {
|
|
Enabled bool `json:"enabled"`
|
|
Available bool `json:"available"`
|
|
Socket string `json:"socket"`
|
|
LabelKey string `json:"label_key"`
|
|
LabelValue string `json:"label_value"`
|
|
Error string `json:"error,omitempty"`
|
|
Services []dockerSkillService `json:"services"`
|
|
}
|
|
|
|
type dockerSkillController struct {
|
|
app *app
|
|
enabled bool
|
|
socket string
|
|
labelKey string
|
|
labelValue string
|
|
client *http.Client
|
|
}
|
|
|
|
func newDockerSkillController(a *app) *dockerSkillController {
|
|
socket := env("JARVIS_DOCKER_SOCKET", "/var/run/docker.sock")
|
|
tr := &http.Transport{DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
|
return (&net.Dialer{Timeout: 3 * time.Second}).DialContext(ctx, "unix", socket)
|
|
}}
|
|
return &dockerSkillController{
|
|
app: a,
|
|
enabled: envBool("JARVIS_DOCKER_CONTROLLER_ENABLED", false),
|
|
socket: socket,
|
|
labelKey: strings.TrimSpace(env("JARVIS_DOCKER_SKILL_LABEL", "com.jarvis.skill-service")),
|
|
labelValue: strings.TrimSpace(env("JARVIS_DOCKER_SKILL_LABEL_VALUE", "true")),
|
|
client: &http.Client{Transport: tr, Timeout: 8 * time.Second},
|
|
}
|
|
}
|
|
|
|
func (d *dockerSkillController) Status(ctx context.Context) dockerControllerStatus {
|
|
st := dockerControllerStatus{Enabled: d.enabled, Socket: d.socket, LabelKey: d.labelKey, LabelValue: d.labelValue}
|
|
if d == nil || !d.enabled {
|
|
return st
|
|
}
|
|
list, err := d.list(ctx)
|
|
if err != nil {
|
|
st.Error = err.Error()
|
|
return st
|
|
}
|
|
st.Available = true
|
|
st.Services = list
|
|
return st
|
|
}
|
|
|
|
func (d *dockerSkillController) list(ctx context.Context) ([]dockerSkillService, error) {
|
|
if d == nil || !d.enabled {
|
|
return nil, errors.New("Docker-Controller deaktiviert")
|
|
}
|
|
filters := map[string][]string{"label": {d.labelKey + "=" + d.labelValue}}
|
|
fb, _ := json.Marshal(filters)
|
|
endpoint := "http://docker/containers/json?all=1&filters=" + url.QueryEscape(string(fb))
|
|
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
|
|
resp, err := d.client.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("docker.sock nicht erreichbar: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
raw, _ := io.ReadAll(io.LimitReader(resp.Body, 2<<20))
|
|
if resp.StatusCode != 200 {
|
|
return nil, fmt.Errorf("Docker API HTTP %d: %s", resp.StatusCode, clipSkillString(string(raw), 800))
|
|
}
|
|
var cs []dockerContainerSummary
|
|
if err := json.Unmarshal(raw, &cs); err != nil {
|
|
return nil, err
|
|
}
|
|
out := make([]dockerSkillService, 0, len(cs))
|
|
for _, c := range cs {
|
|
if c.Labels[d.labelKey] != d.labelValue {
|
|
continue
|
|
}
|
|
name := ""
|
|
if len(c.Names) > 0 {
|
|
name = strings.TrimPrefix(c.Names[0], "/")
|
|
}
|
|
out = append(out, dockerSkillService{ID: c.ID, Name: name, Image: c.Image, State: c.State, Status: c.Status, Runtime: c.Labels["com.jarvis.skill-runtime"], WorkerName: c.Labels["com.jarvis.worker-name"], Labels: map[string]string{d.labelKey: c.Labels[d.labelKey], "com.jarvis.skill-runtime": c.Labels["com.jarvis.skill-runtime"], "com.jarvis.worker-name": c.Labels["com.jarvis.worker-name"]}})
|
|
}
|
|
sort.Slice(out, func(i, j int) bool { return out[i].Name < out[j].Name })
|
|
return out, nil
|
|
}
|
|
|
|
func (d *dockerSkillController) control(ctx context.Context, idv, action string) error {
|
|
if d == nil || !d.enabled {
|
|
return errors.New("Docker-Controller deaktiviert")
|
|
}
|
|
services, err := d.list(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var selected *dockerSkillService
|
|
for i := range services {
|
|
if services[i].ID == idv {
|
|
selected = &services[i]
|
|
break
|
|
}
|
|
}
|
|
if selected == nil {
|
|
return errors.New("Container ist nicht für JARVIS Skill Control freigegeben")
|
|
}
|
|
var path string
|
|
switch action {
|
|
case "start":
|
|
path = "/containers/" + url.PathEscape(idv) + "/start"
|
|
case "stop":
|
|
path = "/containers/" + url.PathEscape(idv) + "/stop?t=10"
|
|
case "restart":
|
|
path = "/containers/" + url.PathEscape(idv) + "/restart?t=10"
|
|
default:
|
|
return errors.New("unbekannte Docker-Aktion")
|
|
}
|
|
req, _ := http.NewRequestWithContext(ctx, http.MethodPost, "http://docker"+path, bytes.NewReader(nil))
|
|
started := time.Now()
|
|
resp, err := d.client.Do(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer resp.Body.Close()
|
|
raw, _ := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
|
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
|
return fmt.Errorf("Docker API HTTP %d: %s", resp.StatusCode, clipSkillString(strings.TrimSpace(string(raw)), 600))
|
|
}
|
|
if d.app != nil {
|
|
d.app.traceTimed(ctx, "docker", "skill_service_control", "output", map[string]any{"container_id": idv, "container": selected.Name, "action": action}, started)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (d *dockerSkillController) handleList(w http.ResponseWriter, r *http.Request) {
|
|
writeJSON(w, 200, d.Status(r.Context()))
|
|
}
|
|
func (d *dockerSkillController) handleControl(w http.ResponseWriter, r *http.Request) {
|
|
action := r.PathValue("action")
|
|
idv := r.PathValue("id")
|
|
if err := d.control(r.Context(), idv, action); err != nil {
|
|
writeJSON(w, 400, map[string]any{"ok": false, "error": err.Error()})
|
|
return
|
|
}
|
|
writeJSON(w, 200, map[string]any{"ok": true, "action": action, "id": idv})
|
|
}
|