Initial commit: DSH Web Manager
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
)
|
||||
|
||||
// Reconcile checks persisted identities against Docker without deleting unknown
|
||||
// containers. Orphans require explicit administrator review, never blind adoption.
|
||||
func (a *API) Reconcile(ctx context.Context) error {
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
filters, _ := json.Marshal(map[string][]string{"label": {"dsh-mgr.instance"}})
|
||||
var containers []struct {
|
||||
ID string
|
||||
State string
|
||||
Labels map[string]string
|
||||
}
|
||||
if err := a.docker.Do(ctx, "GET", "/containers/json?all=1&filters="+url.QueryEscape(string(filters)), nil, &containers); err != nil {
|
||||
return err
|
||||
}
|
||||
rows, err := a.s.DB.QueryContext(ctx, "SELECT id,uid,container_id,status FROM instances")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
type record struct{ id, uid, container, status string }
|
||||
var records []record
|
||||
for rows.Next() {
|
||||
var r record
|
||||
if err = rows.Scan(&r.id, &r.uid, &r.container, &r.status); err != nil {
|
||||
break
|
||||
}
|
||||
records = append(records, r)
|
||||
}
|
||||
if err == nil {
|
||||
err = rows.Err()
|
||||
}
|
||||
rows.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
known := make(map[string]record)
|
||||
for _, r := range records {
|
||||
known[r.id] = r
|
||||
}
|
||||
matched := make(map[string]bool)
|
||||
panel := os.Getenv("PANEL_CONTAINER_ID")
|
||||
var networkErrors []error
|
||||
for _, c := range containers {
|
||||
id := c.Labels["dsh-mgr.instance"]
|
||||
r, ok := known[id]
|
||||
if !ok || r.container != c.ID || r.uid != c.Labels["dsh-mgr.owner"] {
|
||||
a.s.AddAudit("", "reconcile.orphan", c.ID, "container identity requires administrator review")
|
||||
continue
|
||||
}
|
||||
matched[id] = true
|
||||
if panel != "" {
|
||||
if networkErr := a.reconcilePanelNetwork(ctx, id, panel); networkErr != nil {
|
||||
if _, err = a.s.DB.ExecContext(ctx, "UPDATE instances SET status='error' WHERE id=?", id); err != nil {
|
||||
return err
|
||||
}
|
||||
a.s.AddAudit(r.uid, "reconcile.network_error", id, networkErr.Error())
|
||||
networkErrors = append(networkErrors, fmt.Errorf("instance %s: %w", id, networkErr))
|
||||
continue
|
||||
}
|
||||
}
|
||||
if r.status != "error" {
|
||||
if _, err = a.s.DB.ExecContext(ctx, "UPDATE instances SET status=? WHERE id=?", c.State, id); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, r := range records {
|
||||
if r.container == "" || matched[r.id] || r.status == "error" {
|
||||
continue
|
||||
}
|
||||
if _, err = a.s.DB.ExecContext(ctx, "UPDATE instances SET status='error' WHERE id=?", r.id); err != nil {
|
||||
return err
|
||||
}
|
||||
a.s.AddAudit(r.uid, "reconcile.missing", r.id, "recorded container is absent or identity mismatch")
|
||||
}
|
||||
if ctx.Err() != nil {
|
||||
return fmt.Errorf("reconcile: %w", ctx.Err())
|
||||
}
|
||||
return errors.Join(networkErrors...)
|
||||
}
|
||||
|
||||
func (a *API) reconcilePanelNetwork(ctx context.Context, id, panel string) error {
|
||||
name := runtimeNetwork(id)
|
||||
var network struct {
|
||||
ID, Name, Driver string
|
||||
Labels map[string]string
|
||||
}
|
||||
if err := a.docker.Do(ctx, "GET", "/networks/"+url.PathEscape(name), nil, &network); err != nil {
|
||||
return fmt.Errorf("inspect instance network: %w", err)
|
||||
}
|
||||
if network.ID == "" || network.Name != name || network.Driver != "bridge" || network.Labels["dsh-mgr.instance"] != id {
|
||||
return fmt.Errorf("instance network identity mismatch")
|
||||
}
|
||||
container, err := a.docker.Inspect(ctx, panel)
|
||||
if err != nil {
|
||||
return fmt.Errorf("inspect panel: %w", err)
|
||||
}
|
||||
if container.ID == "" {
|
||||
return fmt.Errorf("panel container identity missing")
|
||||
}
|
||||
for _, endpoint := range container.NetworkSettings.Networks {
|
||||
if endpoint.NetworkID == network.ID {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if err := a.docker.Do(ctx, "POST", "/networks/"+url.PathEscape(network.ID)+"/connect", map[string]string{"Container": container.ID}, nil); err != nil {
|
||||
// Another reconciliation may have connected the panel after inspection.
|
||||
if current, inspectErr := a.docker.Inspect(ctx, container.ID); inspectErr == nil && current.ID == container.ID {
|
||||
for _, endpoint := range current.NetworkSettings.Networks {
|
||||
if endpoint.NetworkID == network.ID {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("connect panel network: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user