163 lines
5.8 KiB
Go
163 lines
5.8 KiB
Go
package main
|
|
|
|
import (
|
|
"crypto/subtle"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"git.ipao.vip/rogee/go-sip/internal/configread"
|
|
"git.ipao.vip/rogee/go-sip/internal/contract"
|
|
)
|
|
|
|
type dataset struct {
|
|
dispatcherID string
|
|
tenantID int64
|
|
sip []byte
|
|
providers []byte
|
|
quota []byte
|
|
tasks map[string][]byte
|
|
discovery []configread.DiscoveredTask
|
|
}
|
|
|
|
func loadDataset(dir, dispatcherID string) (dataset, error) {
|
|
if dispatcherID == "" {
|
|
return dataset{}, errors.New("mock dispatcher ID is required")
|
|
}
|
|
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 SaaS test snapshot %s: %w", name, err)
|
|
}
|
|
if !info.Mode().IsRegular() || info.Mode().Perm()&0077 != 0 {
|
|
return nil, fmt.Errorf("SaaS test snapshot %s must be a private regular file", name)
|
|
}
|
|
body, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("read SaaS test snapshot %s: %w", name, err)
|
|
}
|
|
if err := contract.ValidateCurrent("config-read", body); err != nil {
|
|
// Schema errors can contain credential values; never include their text.
|
|
return nil, fmt.Errorf("SaaS test snapshot %s violates the current contract", name)
|
|
}
|
|
var owner struct {
|
|
DispatcherID string `json:"dispatcher_id"`
|
|
Resource string `json:"resource"`
|
|
}
|
|
if err := json.Unmarshal(body, &owner); err != nil || owner.DispatcherID != dispatcherID || owner.Resource != resource {
|
|
return nil, fmt.Errorf("SaaS test snapshot %s has a wrong owner or resource", name)
|
|
}
|
|
return body, nil
|
|
}
|
|
data := dataset{dispatcherID: dispatcherID, tasks: make(map[string][]byte)}
|
|
var err error
|
|
if data.sip, err = read("sip.json", "sip_config"); err != nil {
|
|
return dataset{}, err
|
|
}
|
|
if data.providers, err = read("providers.json", "ai_providers"); err != nil {
|
|
return dataset{}, err
|
|
}
|
|
if data.quota, err = read("quota.json", "tenant_quota"); err != nil {
|
|
return dataset{}, err
|
|
}
|
|
var quota configread.Quota
|
|
if err = json.Unmarshal(data.quota, "a); err != nil || quota.TenantID <= 0 {
|
|
return dataset{}, errors.New("SaaS test quota has no tenant ID")
|
|
}
|
|
data.tenantID = quota.TenantID
|
|
files, err := filepath.Glob(filepath.Join(dir, "tasks", "*.json"))
|
|
if err != nil || len(files) == 0 || len(files) > 6 {
|
|
return dataset{}, errors.New("SaaS test dataset must contain one to six task snapshots")
|
|
}
|
|
for _, path := range files {
|
|
body, err := read(filepath.Join("tasks", filepath.Base(path)), "task_config")
|
|
if err != nil {
|
|
return dataset{}, err
|
|
}
|
|
var task configread.Task
|
|
if err := json.Unmarshal(body, &task); err != nil || task.TaskID == "" || filepath.Base(path) != task.TaskID+".json" || task.TenantID != data.tenantID {
|
|
return dataset{}, errors.New("SaaS test task has an invalid ID or tenant")
|
|
}
|
|
if _, found := data.tasks[task.TaskID]; found {
|
|
return dataset{}, errors.New("duplicate SaaS test task ID")
|
|
}
|
|
data.tasks[task.TaskID] = body
|
|
data.discovery = append(data.discovery, configread.DiscoveredTask{
|
|
TaskID: task.TaskID, TenantID: task.TenantID, TaskRevision: task.TaskRevision, Status: task.Status,
|
|
})
|
|
}
|
|
sort.Slice(data.discovery, func(i, j int) bool { return data.discovery[i].TaskID < data.discovery[j].TaskID })
|
|
page, err := json.Marshal(configread.TaskPage{DispatcherID: dispatcherID, Cursor: "mock-complete", Tasks: data.discovery})
|
|
if err != nil {
|
|
return dataset{}, errors.New("encode SaaS test task discovery")
|
|
}
|
|
if err := contract.ValidateCurrent("task-discovery", page); err != nil {
|
|
return dataset{}, errors.New("SaaS test task discovery violates the current contract")
|
|
}
|
|
return data, nil
|
|
}
|
|
|
|
func (d dataset) handler(secret string) http.Handler {
|
|
mux := http.NewServeMux()
|
|
const prefix = "/internal/v1/dispatcher/"
|
|
write := func(w http.ResponseWriter, body []byte) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusOK)
|
|
_, _ = w.Write(body)
|
|
}
|
|
mux.HandleFunc("GET "+prefix+"sip", func(w http.ResponseWriter, _ *http.Request) { write(w, d.sip) })
|
|
mux.HandleFunc("GET "+prefix+"ai-providers", func(w http.ResponseWriter, _ *http.Request) { write(w, d.providers) })
|
|
mux.HandleFunc("GET "+prefix+"task/{task_id}", func(w http.ResponseWriter, r *http.Request) {
|
|
body, exists := d.tasks[r.PathValue("task_id")]
|
|
if !exists {
|
|
http.Error(w, "unknown task", http.StatusNotFound)
|
|
return
|
|
}
|
|
write(w, body)
|
|
})
|
|
mux.HandleFunc("GET "+prefix+"tenant/{tenant_id}/quota", func(w http.ResponseWriter, r *http.Request) {
|
|
id, err := strconv.ParseInt(r.PathValue("tenant_id"), 10, 64)
|
|
if err != nil || id != d.tenantID {
|
|
http.Error(w, "unknown tenant", http.StatusNotFound)
|
|
return
|
|
}
|
|
write(w, d.quota)
|
|
})
|
|
mux.HandleFunc("GET "+prefix+"tasks", func(w http.ResponseWriter, r *http.Request) {
|
|
params := r.URL.Query()["after"]
|
|
if len(params) > 1 || (len(params) == 1 && params[0] != "" && params[0] != "mock-complete") {
|
|
http.Error(w, "unknown task discovery cursor", http.StatusBadRequest)
|
|
return
|
|
}
|
|
tasks := d.discovery
|
|
if len(params) == 1 && params[0] == "mock-complete" {
|
|
tasks = []configread.DiscoveredTask{}
|
|
}
|
|
body, err := json.Marshal(configread.TaskPage{DispatcherID: d.dispatcherID, Cursor: "mock-complete", Tasks: tasks})
|
|
if err != nil {
|
|
http.Error(w, "encode task discovery", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
write(w, body)
|
|
})
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if !strings.HasPrefix(r.URL.Path, prefix) {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
if subtle.ConstantTimeCompare([]byte(r.Header.Get("X-DISPATCHER-id")), []byte(d.dispatcherID)) != 1 ||
|
|
subtle.ConstantTimeCompare([]byte(r.Header.Get("X-DISPATCHER-SECRET-KEY")), []byte(secret)) != 1 {
|
|
http.Error(w, "unknown dispatcher", http.StatusForbidden)
|
|
return
|
|
}
|
|
mux.ServeHTTP(w, r)
|
|
})
|
|
}
|