174 lines
4.9 KiB
Go
174 lines
4.9 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
|
|
"git.ipao.vip/rogee/go-sip/internal/configread"
|
|
"git.ipao.vip/rogee/go-sip/internal/contract"
|
|
)
|
|
|
|
type snapshots struct {
|
|
dispatcherID string
|
|
tenantID int64
|
|
sip, providers, quota []byte
|
|
tasks map[string][]byte
|
|
}
|
|
|
|
func validateResource(raw []byte, resource, dispatcherID string) error {
|
|
if err := contract.ValidateCurrent("config-read", raw); err != nil {
|
|
// Existing contract diagnostics report field paths without private values.
|
|
return fmt.Errorf("%s: %w", resource, err)
|
|
}
|
|
var identity struct {
|
|
Resource string `json:"resource"`
|
|
DispatcherID string `json:"dispatcher_id"`
|
|
}
|
|
if err := json.Unmarshal(raw, &identity); err != nil {
|
|
return errors.New("invalid resource JSON")
|
|
}
|
|
if identity.Resource != resource || identity.DispatcherID != dispatcherID {
|
|
return fmt.Errorf("%s has wrong owner or resource", resource)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func loadSnapshots(dir, id string) (snapshots, error) {
|
|
d := snapshots{dispatcherID: id, tasks: make(map[string][]byte)}
|
|
read := func(name, resource string) ([]byte, error) {
|
|
path := filepath.Join(dir, name)
|
|
info, err := os.Stat(path)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("read %s: %w", name, err)
|
|
}
|
|
if !info.Mode().IsRegular() || info.Mode().Perm()&0077 != 0 {
|
|
return nil, fmt.Errorf("%s must be a private regular file", name)
|
|
}
|
|
raw, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("read %s: %w", name, err)
|
|
}
|
|
if err = validateResource(raw, resource, id); err != nil {
|
|
return nil, err
|
|
}
|
|
return raw, nil
|
|
}
|
|
var err error
|
|
if d.sip, err = read("sip.json", "sip_config"); err != nil {
|
|
return d, err
|
|
}
|
|
if d.providers, err = read("providers.json", "ai_providers"); err != nil {
|
|
return d, err
|
|
}
|
|
if d.quota, err = read("quota.json", "tenant_quota"); err != nil {
|
|
return d, err
|
|
}
|
|
var q configread.Quota
|
|
if err = json.Unmarshal(d.quota, &q); err != nil {
|
|
return d, errors.New("invalid quota")
|
|
}
|
|
d.tenantID = q.TenantID
|
|
files, err := filepath.Glob(filepath.Join(dir, "tasks", "*.json"))
|
|
if err != nil {
|
|
return d, err
|
|
}
|
|
if len(files) == 0 {
|
|
return d, errors.New("at least one task is required")
|
|
}
|
|
for _, path := range files {
|
|
raw, err := read(filepath.Join("tasks", filepath.Base(path)), "task_config")
|
|
if err != nil {
|
|
return d, err
|
|
}
|
|
var task configread.Task
|
|
if err = json.Unmarshal(raw, &task); err != nil {
|
|
return d, errors.New("invalid task snapshot")
|
|
}
|
|
if filepath.Base(path) != task.TaskID+".json" || task.TenantID != d.tenantID {
|
|
return d, errors.New("task filename or tenant mismatch")
|
|
}
|
|
d.tasks[task.TaskID] = raw
|
|
}
|
|
if err = d.validateReferences(); err != nil {
|
|
return d, err
|
|
}
|
|
return d, nil
|
|
}
|
|
|
|
func (d snapshots) validateReferences() error {
|
|
var sip struct {
|
|
Trunks []struct {
|
|
ID string `json:"trunk_id"`
|
|
} `json:"trunks"`
|
|
}
|
|
var providers struct {
|
|
Providers []configread.Provider `json:"providers"`
|
|
}
|
|
if err := json.Unmarshal(d.sip, &sip); err != nil {
|
|
return errors.New("invalid SIP snapshot")
|
|
}
|
|
if err := json.Unmarshal(d.providers, &providers); err != nil {
|
|
return errors.New("invalid providers snapshot")
|
|
}
|
|
trunks := make(map[string]bool)
|
|
for _, t := range sip.Trunks {
|
|
if trunks[t.ID] {
|
|
return errors.New("duplicate trunk")
|
|
}
|
|
trunks[t.ID] = true
|
|
}
|
|
codes := make(map[string]bool)
|
|
for _, p := range providers.Providers {
|
|
if codes[p.Code] {
|
|
return errors.New("duplicate provider code")
|
|
}
|
|
codes[p.Code] = true
|
|
}
|
|
for _, raw := range d.tasks {
|
|
var task configread.Task
|
|
if err := json.Unmarshal(raw, &task); err != nil {
|
|
return errors.New("invalid task snapshot")
|
|
}
|
|
for _, t := range task.AllowedTrunks {
|
|
if !trunks[t.TrunkID] {
|
|
return errors.New("task references missing trunk")
|
|
}
|
|
}
|
|
for _, ref := range []string{task.Agent.ASR.ProviderRef, task.Agent.LLM.ProviderRef, task.Agent.TTS.ProviderRef} {
|
|
if ref != "" && !codes[ref] {
|
|
return errors.New("task references missing provider")
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (d snapshots) page(after string) ([]byte, error) {
|
|
if after != "" && after != "complete" {
|
|
return nil, errors.New("unknown discovery cursor")
|
|
}
|
|
tasks := []configread.DiscoveredTask{}
|
|
if after == "" {
|
|
for _, raw := range d.tasks {
|
|
var task configread.Task
|
|
if err := json.Unmarshal(raw, &task); err != nil {
|
|
return nil, errors.New("invalid task snapshot")
|
|
}
|
|
tasks = append(tasks, configread.DiscoveredTask{TaskID: task.TaskID, TenantID: task.TenantID, Status: task.Status, TaskRevision: task.TaskRevision})
|
|
}
|
|
}
|
|
sort.Slice(tasks, func(i, j int) bool { return tasks[i].TaskID < tasks[j].TaskID })
|
|
raw, err := json.Marshal(configread.TaskPage{DispatcherID: d.dispatcherID, Cursor: "complete", Tasks: tasks})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err = contract.ValidateCurrent("task-discovery", raw); err != nil {
|
|
return nil, errors.New("invalid discovery response")
|
|
}
|
|
return raw, nil
|
|
}
|