forked from tangled.org/core
this repo has no description
1package knotserver 2 3import ( 4 "context" 5 "encoding/json" 6 "fmt" 7 "log/slog" 8 "net/http" 9 "path/filepath" 10 "strings" 11 12 securejoin "github.com/cyphar/filepath-securejoin" 13 "github.com/go-chi/chi/v5" 14 "github.com/go-chi/chi/v5/middleware" 15 "tangled.sh/tangled.sh/core/api/tangled" 16 "tangled.sh/tangled.sh/core/knotserver/config" 17 "tangled.sh/tangled.sh/core/knotserver/db" 18 "tangled.sh/tangled.sh/core/knotserver/git" 19 "tangled.sh/tangled.sh/core/notifier" 20 "tangled.sh/tangled.sh/core/rbac" 21 "tangled.sh/tangled.sh/core/workflow" 22) 23 24type InternalHandle struct { 25 db *db.DB 26 c *config.Config 27 e *rbac.Enforcer 28 l *slog.Logger 29 n *notifier.Notifier 30} 31 32func (h *InternalHandle) PushAllowed(w http.ResponseWriter, r *http.Request) { 33 user := r.URL.Query().Get("user") 34 repo := r.URL.Query().Get("repo") 35 36 if user == "" || repo == "" { 37 w.WriteHeader(http.StatusBadRequest) 38 return 39 } 40 41 ok, err := h.e.IsPushAllowed(user, rbac.ThisServer, repo) 42 if err != nil || !ok { 43 w.WriteHeader(http.StatusForbidden) 44 return 45 } 46 47 w.WriteHeader(http.StatusNoContent) 48 return 49} 50 51func (h *InternalHandle) InternalKeys(w http.ResponseWriter, r *http.Request) { 52 keys, err := h.db.GetAllPublicKeys() 53 if err != nil { 54 writeError(w, err.Error(), http.StatusInternalServerError) 55 return 56 } 57 58 data := make([]map[string]interface{}, 0) 59 for _, key := range keys { 60 j := key.JSON() 61 data = append(data, j) 62 } 63 writeJSON(w, data) 64 return 65} 66 67type PushOptions struct { 68 skipCi bool 69} 70 71func (h *InternalHandle) PostReceiveHook(w http.ResponseWriter, r *http.Request) { 72 l := h.l.With("handler", "PostReceiveHook") 73 74 gitAbsoluteDir := r.Header.Get("X-Git-Dir") 75 gitRelativeDir, err := filepath.Rel(h.c.Repo.ScanPath, gitAbsoluteDir) 76 if err != nil { 77 l.Error("failed to calculate relative git dir", "scanPath", h.c.Repo.ScanPath, "gitAbsoluteDir", gitAbsoluteDir) 78 return 79 } 80 81 parts := strings.SplitN(gitRelativeDir, "/", 2) 82 if len(parts) != 2 { 83 l.Error("invalid git dir", "gitRelativeDir", gitRelativeDir) 84 return 85 } 86 repoDid := parts[0] 87 repoName := parts[1] 88 89 gitUserDid := r.Header.Get("X-Git-User-Did") 90 91 lines, err := git.ParsePostReceive(r.Body) 92 if err != nil { 93 l.Error("failed to parse post-receive payload", "err", err) 94 // non-fatal 95 } 96 97 // extract any push options 98 pushOptionsRaw := r.Header.Values("X-Git-Push-Option") 99 pushOptions := PushOptions{} 100 for _, option := range pushOptionsRaw { 101 if option == "skip-ci" || option == "ci-skip" { 102 pushOptions.skipCi = true 103 } 104 } 105 106 for _, line := range lines { 107 err := h.insertRefUpdate(line, gitUserDid, repoDid, repoName) 108 if err != nil { 109 l.Error("failed to insert op", "err", err, "line", line, "did", gitUserDid, "repo", gitRelativeDir) 110 // non-fatal 111 } 112 113 err = h.triggerPipeline(line, gitUserDid, repoDid, repoName, pushOptions) 114 if err != nil { 115 l.Error("failed to trigger pipeline", "err", err, "line", line, "did", gitUserDid, "repo", gitRelativeDir) 116 // non-fatal 117 } 118 } 119} 120 121func (h *InternalHandle) insertRefUpdate(line git.PostReceiveLine, gitUserDid, repoDid, repoName string) error { 122 didSlashRepo, err := securejoin.SecureJoin(repoDid, repoName) 123 if err != nil { 124 return err 125 } 126 127 repoPath, err := securejoin.SecureJoin(h.c.Repo.ScanPath, didSlashRepo) 128 if err != nil { 129 return err 130 } 131 132 gr, err := git.Open(repoPath, line.Ref) 133 if err != nil { 134 return fmt.Errorf("failed to open git repo at ref %s: %w", line.Ref, err) 135 } 136 137 meta := gr.RefUpdateMeta(line) 138 139 metaRecord := meta.AsRecord() 140 141 refUpdate := tangled.GitRefUpdate{ 142 OldSha: line.OldSha.String(), 143 NewSha: line.NewSha.String(), 144 Ref: line.Ref, 145 CommitterDid: gitUserDid, 146 RepoDid: repoDid, 147 RepoName: repoName, 148 Meta: &metaRecord, 149 } 150 eventJson, err := json.Marshal(refUpdate) 151 if err != nil { 152 return err 153 } 154 155 event := db.Event{ 156 Rkey: TID(), 157 Nsid: tangled.GitRefUpdateNSID, 158 EventJson: string(eventJson), 159 } 160 161 return h.db.InsertEvent(event, h.n) 162} 163 164func (h *InternalHandle) triggerPipeline(line git.PostReceiveLine, gitUserDid, repoDid, repoName string, pushOptions PushOptions) error { 165 if pushOptions.skipCi { 166 return nil 167 } 168 169 didSlashRepo, err := securejoin.SecureJoin(repoDid, repoName) 170 if err != nil { 171 return err 172 } 173 174 repoPath, err := securejoin.SecureJoin(h.c.Repo.ScanPath, didSlashRepo) 175 if err != nil { 176 return err 177 } 178 179 gr, err := git.Open(repoPath, line.Ref) 180 if err != nil { 181 return err 182 } 183 184 workflowDir, err := gr.FileTree(context.Background(), workflow.WorkflowDir) 185 if err != nil { 186 return err 187 } 188 189 var pipeline workflow.Pipeline 190 for _, e := range workflowDir { 191 if !e.IsFile { 192 continue 193 } 194 195 fpath := filepath.Join(workflow.WorkflowDir, e.Name) 196 contents, err := gr.RawContent(fpath) 197 if err != nil { 198 continue 199 } 200 201 wf, err := workflow.FromFile(e.Name, contents) 202 if err != nil { 203 // TODO: log here, respond to client that is pushing 204 h.l.Error("failed to parse workflow", "err", err, "path", fpath) 205 continue 206 } 207 208 pipeline = append(pipeline, wf) 209 } 210 211 trigger := tangled.Pipeline_PushTriggerData{ 212 Ref: line.Ref, 213 OldSha: line.OldSha.String(), 214 NewSha: line.NewSha.String(), 215 } 216 217 compiler := workflow.Compiler{ 218 Trigger: tangled.Pipeline_TriggerMetadata{ 219 Kind: string(workflow.TriggerKindPush), 220 Push: &trigger, 221 Repo: &tangled.Pipeline_TriggerRepo{ 222 Did: repoDid, 223 Knot: h.c.Server.Hostname, 224 Repo: repoName, 225 }, 226 }, 227 } 228 229 // TODO: send the diagnostics back to the user here via stderr 230 cp := compiler.Compile(pipeline) 231 eventJson, err := json.Marshal(cp) 232 if err != nil { 233 return err 234 } 235 236 // do not run empty pipelines 237 if cp.Workflows == nil { 238 return nil 239 } 240 241 event := db.Event{ 242 Rkey: TID(), 243 Nsid: tangled.PipelineNSID, 244 EventJson: string(eventJson), 245 } 246 247 return h.db.InsertEvent(event, h.n) 248} 249 250func Internal(ctx context.Context, c *config.Config, db *db.DB, e *rbac.Enforcer, l *slog.Logger, n *notifier.Notifier) http.Handler { 251 r := chi.NewRouter() 252 253 h := InternalHandle{ 254 db, 255 c, 256 e, 257 l, 258 n, 259 } 260 261 r.Get("/push-allowed", h.PushAllowed) 262 r.Get("/keys", h.InternalKeys) 263 r.Post("/hooks/post-receive", h.PostReceiveHook) 264 r.Mount("/debug", middleware.Profiler()) 265 266 return r 267}