This repository has no description
1package xrpc
2
3import (
4 "context"
5 _ "embed"
6 "encoding/json"
7 "errors"
8 "log/slog"
9 "net/http"
10
11 "github.com/bluesky-social/indigo/atproto/syntax"
12 "github.com/go-chi/chi/v5"
13
14 "tangled.org/core/api/tangled"
15 "tangled.org/core/idresolver"
16 "tangled.org/core/notifier"
17 "tangled.org/core/rbac"
18 "tangled.org/core/spindle/config"
19 "tangled.org/core/spindle/db"
20 "tangled.org/core/spindle/models"
21 "tangled.org/core/spindle/secrets"
22 xrpcerr "tangled.org/core/xrpc/errors"
23 "tangled.org/core/xrpc/serviceauth"
24)
25
26const ActorDid = serviceauth.ActorDid
27
28var ErrNoMatchingWorkflows = errors.New("no workflows to run")
29
30// this is to break an import cycle. spindle imports this package for Xrpc,
31// so this package can't import *spindle.Spindle back.
32type PipelineTrigger interface {
33 TriggerManual(ctx context.Context, repoDid syntax.DID, sha, ref string, workflows []string, sourceRepo syntax.DID, pull PullContext, inputs []*tangled.Pipeline_Pair) (syntax.ATURI, error)
34}
35
36type PullContext struct {
37 IsPullRequest bool
38 Pull syntax.ATURI
39 SourceBranch string
40 TargetBranch string
41}
42
43type Xrpc struct {
44 Logger *slog.Logger
45 Db *db.DB
46 Enforcer *rbac.Enforcer
47 Engines map[string]models.Engine
48 Config *config.Config
49 Resolver *idresolver.Resolver
50 Vault secrets.Manager
51 Notifier *notifier.Notifier
52 ServiceAuth *serviceauth.ServiceAuth
53 Trigger PipelineTrigger
54}
55
56func (x *Xrpc) Router() http.Handler {
57 r := chi.NewRouter()
58 r.Use(x.cors)
59
60 r.Group(func(r chi.Router) {
61 r.Use(x.ServiceAuth.VerifyServiceAuth)
62
63 r.Post("/"+tangled.RepoAddSecretNSID, x.AddSecret)
64 r.Post("/"+tangled.RepoRemoveSecretNSID, x.RemoveSecret)
65 r.Get("/"+tangled.RepoListSecretsNSID, x.ListSecrets)
66 r.Post("/"+tangled.CiCancelPipelineNSID, x.CancelPipeline)
67 r.Post("/"+tangled.CiTriggerPipelineNSID, x.TriggerPipeline)
68 })
69
70 // service query endpoints (no auth required)
71 r.Get("/"+tangled.OwnerNSID, x.Owner)
72 r.Get("/"+tangled.CiSubscribePipelineLogsNSID, x.HandleCiSubscribePipelineLogs)
73 r.Get("/"+tangled.CiQueryPipelinesNSID, x.HandleCiQueryPipelines)
74 r.Get("/"+tangled.CiGetPipelineNSID, x.HandleCiGetPipeline)
75
76 return r
77}
78
79// cors allows the browser origin to call these xrpc endpoints directly. auth is
80// via service-auth bearer tokens, not cookies, so a wildcard origin is safe.
81func (x *Xrpc) cors(next http.Handler) http.Handler {
82 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
83 w.Header().Set("Access-Control-Allow-Origin", "*")
84 w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
85 w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
86 w.Header().Set("Access-Control-Max-Age", "86400")
87 if r.Method == http.MethodOptions {
88 w.WriteHeader(http.StatusNoContent)
89 return
90 }
91 next.ServeHTTP(w, r)
92 })
93}
94
95// this is slightly different from http_util::write_error to follow the spec:
96//
97// the json object returned must include an "error" and a "message"
98func writeError(w http.ResponseWriter, e xrpcerr.XrpcError, status int) {
99 w.Header().Set("Content-Type", "application/json")
100 w.WriteHeader(status)
101 json.NewEncoder(w).Encode(e)
102}
103
104func writeJson(w http.ResponseWriter, status int, response any) error {
105 w.Header().Set("Content-Type", "application/json")
106 w.WriteHeader(status)
107 return json.NewEncoder(w).Encode(response)
108}