129 lines
3.9 KiB
Go
129 lines
3.9 KiB
Go
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
|
|
}
|