125 lines
4.3 KiB
Go
125 lines
4.3 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"flag"
|
|
"fmt"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"git.cin.su/m/Assistant/adapters/agentbridge"
|
|
"git.cin.su/m/Assistant/adapters/postgres"
|
|
secretcontracts "git.cin.su/m/Assistant/contracts/secrets"
|
|
"git.cin.su/m/Assistant/contracts/work"
|
|
"git.cin.su/m/Assistant/modules/work/application"
|
|
"git.cin.su/m/Assistant/modules/work/ports"
|
|
platformsecrets "git.cin.su/m/Assistant/platform/secrets"
|
|
)
|
|
|
|
func main() {
|
|
workerID := flag.String("worker-id", "agent-worker-1", "durable worker identity")
|
|
statusAddr := flag.String("status-addr", ":8090", "internal status HTTP address")
|
|
staleAfter := flag.Duration("stale-after", 10*time.Minute, "age after which a claimed item is recovered")
|
|
leaseTTL := flag.Duration("lease-ttl", 15*time.Minute, "executor lease duration")
|
|
pollInterval := flag.Duration("poll-interval", 5*time.Second, "database-owned idle wake-up interval")
|
|
once := flag.Bool("once", false, "run one cycle and exit")
|
|
flag.Parse()
|
|
|
|
password, err := resolveDatabasePassword("WORK_POSTGRES_PASSWORD", "WORK_POSTGRES_CREDENTIAL_REF", secretcontracts.RuntimeConsumerWorkWorker)
|
|
if err != nil {
|
|
fmt.Fprintln(os.Stderr, err)
|
|
os.Exit(1)
|
|
}
|
|
repository, err := postgres.OpenWork(context.Background(), 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 {
|
|
fmt.Fprintln(os.Stderr, err)
|
|
os.Exit(1)
|
|
}
|
|
defer repository.Close()
|
|
var executor ports.AgentExecutor
|
|
if bridgeURL, bridgeConfigured := loadProtectedBridgeURL(); bridgeConfigured {
|
|
executor, err = agentbridge.NewHTTPExecutor(bridgeURL, &http.Client{Timeout: *leaseTTL})
|
|
if err != nil {
|
|
fmt.Fprintln(os.Stderr, err)
|
|
os.Exit(1)
|
|
}
|
|
} else {
|
|
fmt.Fprintln(os.Stderr, "protected agent execution bridge URL is not configured; dispatch claims are disabled; recovery-only mode")
|
|
}
|
|
go func() {
|
|
if serveErr := http.ListenAndServe(*statusAddr, workerStatusHandler(executor != nil)); serveErr != nil {
|
|
fmt.Fprintln(os.Stderr, "agent worker status server:", serveErr)
|
|
}
|
|
}()
|
|
ctx := context.Background()
|
|
for {
|
|
var result application.WorkerCycleResult
|
|
var cycleErr error
|
|
if executor == nil {
|
|
result, cycleErr = application.RecoverOnlyWorkerCycle(ctx, repository, time.Now().UTC(), *staleAfter)
|
|
} else {
|
|
result, cycleErr = application.ExecuteWorkerCycleWithExecutor(ctx, repository, executor, *workerID, work.AgentFencingToken(1), time.Now().UTC(), *staleAfter, *leaseTTL)
|
|
}
|
|
if cycleErr != nil {
|
|
fmt.Fprintln(os.Stderr, cycleErr)
|
|
if *once {
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
if cycleErr == nil && (result.Completed || result.RecoveredClaims > 0) {
|
|
_ = json.NewEncoder(os.Stdout).Encode(result)
|
|
}
|
|
if *once {
|
|
return
|
|
}
|
|
if err := repository.WaitForDispatch(ctx, *pollInterval); err != nil {
|
|
fmt.Fprintln(os.Stderr, err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
}
|
|
|
|
func loadProtectedBridgeURL() (string, bool) {
|
|
return loadProtectedBridgeURLFrom("/run/secrets", "/run/secrets/agent_execution_bridge_url")
|
|
}
|
|
|
|
func loadProtectedBridgeURLFrom(root, path string) (string, bool) {
|
|
clean := filepath.Clean(path)
|
|
cleanRoot := filepath.Clean(root)
|
|
if clean != cleanRoot && !strings.HasPrefix(clean, cleanRoot+string(os.PathSeparator)) {
|
|
return "", false
|
|
}
|
|
payload, err := os.ReadFile(clean)
|
|
if err != nil {
|
|
return "", false
|
|
}
|
|
value := strings.TrimSpace(string(payload))
|
|
if value == "" || strings.ContainsAny(value, "\r\n") {
|
|
return "", false
|
|
}
|
|
return value, true
|
|
}
|
|
|
|
func resolveDatabasePassword(name, refName string, consumer secretcontracts.RuntimeConsumer) (string, error) {
|
|
if _, ok := os.LookupEnv(name); ok {
|
|
return "", fmt.Errorf("%s direct environment secret is forbidden", name)
|
|
}
|
|
path, ref := os.Getenv(name+"_FILE"), os.Getenv(refName)
|
|
if path == "" || ref == "" {
|
|
return "", fmt.Errorf("%s_FILE and %s are required", name, refName)
|
|
}
|
|
root := os.Getenv("SECRET_BACKEND_ROOT")
|
|
if root == "" {
|
|
root = "/run/secrets"
|
|
}
|
|
resolver, err := platformsecrets.NewFileResolver(root)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return resolver.ResolveFile(secretcontracts.SecretFileRef{CredentialRef: secretcontracts.CredentialRef(ref), Purpose: secretcontracts.SecretPurposeDatabasePassword, Consumer: consumer, Path: path})
|
|
}
|