// Package sessions — FleetClient calls the SandboxFleet controller REST API // to assign sessions to fleet pods. This is the primary sandbox path; // the raw-k8s SandboxManager is the fallback when the fleet controller // is unavailable (e.g. during fleet rollout). package sessions import ( "bytes" "context" "encoding/json" "fmt" "io" "net/http" "os" "time" ) const ( defaultFleetControllerURL = "http://tool-definition-controller.poimen.svc.cluster.local:8788" ) // FleetClient calls the SandboxFleet controller REST API. type FleetClient struct { baseURL string client *http.Client } // FleetAssignResult is returned by the controller assign endpoint. type FleetAssignResult struct { PodSvcDns string `json:"podSvcDns"` PodName string `json:"podName"` } // NewFleetClient creates a FleetClient from env or default URL. // FLEET_CONTROLLER_URL — default: defaultFleetControllerURL func NewFleetClient() *FleetClient { url := os.Getenv("FLEET_CONTROLLER_URL") if url == "" { url = defaultFleetControllerURL } return &FleetClient{ baseURL: url, client: &http.Client{Timeout: 35 * time.Second}, } } // Assign asks the fleet controller to assign sessionID to a fleet pod. // Returns the pod's ClusterIP service DNS and pod name. func (f *FleetClient) Assign(ctx context.Context, sessionID string) (*FleetAssignResult, error) { req, err := http.NewRequestWithContext(ctx, http.MethodPost, f.baseURL+"/sessions/"+sessionID+"/assign", nil) if err != nil { return nil, fmt.Errorf("build request: %w", err) } req.Header.Set("Content-Type", "application/json") resp, err := f.client.Do(req) if err != nil { return nil, fmt.Errorf("fleet controller unreachable: %w", err) } defer resp.Body.Close() if resp.StatusCode >= 300 { body, _ := io.ReadAll(io.LimitReader(resp.Body, 512)) return nil, fmt.Errorf("fleet controller returned %d: %s", resp.StatusCode, body) } var result FleetAssignResult if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { return nil, fmt.Errorf("decode: %w", err) } if result.PodSvcDns == "" { return nil, fmt.Errorf("fleet controller returned empty podSvcDns") } return &result, nil } // Release notifies the fleet controller that a session has ended. // Cleans up the per-session ServiceAccount (audit trail SA). // Non-fatal: fleet controller handles this async. func (f *FleetClient) Release(ctx context.Context, sessionID string) error { req, err := http.NewRequestWithContext(ctx, http.MethodDelete, f.baseURL+"/sessions/"+sessionID, bytes.NewReader(nil)) if err != nil { return fmt.Errorf("build request: %w", err) } resp, err := f.client.Do(req) if err != nil { return fmt.Errorf("fleet controller unreachable: %w", err) } defer resp.Body.Close() if resp.StatusCode >= 300 { body, _ := io.ReadAll(io.LimitReader(resp.Body, 512)) return fmt.Errorf("fleet controller returned %d: %s", resp.StatusCode, body) } return nil }