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 }