package main import ( "context" "encoding/json" "fmt" "log" "net/http" "os" "git.cin.su/m/Assistant/adapters/postgres" secretcontracts "git.cin.su/m/Assistant/contracts/secrets" "git.cin.su/m/Assistant/modules/work/application" "git.cin.su/m/Assistant/platform/secrets" "git.cin.su/m/Assistant/runtime/health" ) func main() { ctx := context.Background() password, err := resolveDatabasePassword() if err != nil { log.Fatal(err) } repository, err := postgres.OpenWork(ctx, postgres.ContinuityConfig{Host: os.Getenv("POSTGRES_HOST"), Port: os.Getenv("POSTGRES_PORT"), Database: os.Getenv("POSTGRES_DATABASE"), User: os.Getenv("WORK_POSTGRES_USER"), Password: password}) if err != nil { log.Fatal(err) } defer repository.Close() mux := http.NewServeMux() mux.Handle("/healthz", health.Handler{Runtime: "work-graph-runtime", SemanticGeneration: 1, ArchitectureGeneration: 1}) mux.Handle("/readyz", health.ReadinessHandler{Runtime: "work-graph-runtime", Probes: []health.Probe{{Name: "work-postgres", Check: func(ctx context.Context) error { return repository.Ping(ctx) }}}}) mux.HandleFunc("POST /v1/work-graphs/schedule", func(w http.ResponseWriter, r *http.Request) { var request application.GraphScheduleRequest if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 4<<20)).Decode(&request); err != nil { http.Error(w, "invalid graph schedule request", http.StatusBadRequest) return } graph, ready, err := application.AdmitAndSchedule(r.Context(), repository, request) if err != nil { http.Error(w, err.Error(), http.StatusUnprocessableEntity) return } writeJSON(w, map[string]any{"graph": graph, "scheduled_nodes": ready}) }) mux.HandleFunc("POST /v1/work-graphs/complete", func(w http.ResponseWriter, r *http.Request) { var request application.GraphCompleteRequest if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 64<<10)).Decode(&request); err != nil { http.Error(w, "invalid graph completion request", http.StatusBadRequest) return } graph, err := application.CompleteGraphNode(r.Context(), repository, request.GraphID, request.Revision, request.NodeID, request.Succeeded) if err != nil { http.Error(w, err.Error(), http.StatusUnprocessableEntity) return } writeJSON(w, graph) }) addr := os.Getenv("LISTEN_ADDR") if addr == "" { addr = ":8088" } log.Printf("work-graph-runtime listening on %s", addr) log.Fatal(http.ListenAndServe(addr, mux)) } func writeJSON(w http.ResponseWriter, value any) { w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(value) } func resolveDatabasePassword() (string, error) { if _, ok := os.LookupEnv("WORK_POSTGRES_PASSWORD"); ok { return "", fmt.Errorf("WORK_POSTGRES_PASSWORD direct environment secret is forbidden") } path, ref := os.Getenv("WORK_POSTGRES_PASSWORD_FILE"), os.Getenv("WORK_POSTGRES_CREDENTIAL_REF") if path == "" || ref == "" { return "", fmt.Errorf("WORK_POSTGRES_PASSWORD_FILE and WORK_POSTGRES_CREDENTIAL_REF are required") } root := os.Getenv("SECRET_BACKEND_ROOT") if root == "" { root = "/run/secrets" } resolver, err := secrets.NewFileResolver(root) if err != nil { return "", err } return resolver.ResolveFile(secretcontracts.SecretFileRef{CredentialRef: secretcontracts.CredentialRef(ref), Purpose: secretcontracts.SecretPurposeDatabasePassword, Consumer: secretcontracts.RuntimeConsumerWorkWorker, Path: path}) }