Monorepo for Tangled tangled.org

appview: update state, ingester, middleware, and resolver for repo DID #1140

open opened by oyster.cafe targeting master from oyster.cafe/tangled-core: master
Labels

None yet.

assignee

None yet.

Participants 2
AT URI
at://did:plc:3fwecdnvtcscjnrx2p4n7alz/sh.tangled.repo.pull/3mgprvt2eon22
+295 -131
Diff #1
+78 -18
appview/ingester.go
··· 2 2 3 3 import ( 4 4 "context" 5 + "database/sql" 5 6 "encoding/json" 7 + "errors" 6 8 "fmt" 7 9 "log/slog" 8 10 "maps" ··· 116 118 return err 117 119 } 118 120 119 - subjectUri, err = syntax.ParseATURI(record.Subject) 120 - if err != nil { 121 - l.Error("invalid record", "err", err) 122 - return err 121 + star := &models.Star{ 122 + Did: did, 123 + Rkey: e.Commit.RKey, 123 124 } 124 - err = db.AddStar(i.Db, &models.Star{ 125 - Did: did, 126 - RepoAt: subjectUri, 127 - Rkey: e.Commit.RKey, 128 - }) 125 + 126 + switch { 127 + case record.SubjectDid != nil: 128 + star.SubjectDid = *record.SubjectDid 129 + repo, repoErr := db.GetRepo(i.Db, orm.FilterEq("repo_did", *record.SubjectDid)) 130 + if repoErr == nil { 131 + subjectUri = repo.RepoAt() 132 + star.RepoAt = subjectUri 133 + } 134 + case record.Subject != nil: 135 + subjectUri, err = syntax.ParseATURI(*record.Subject) 136 + if err != nil { 137 + l.Error("invalid record", "err", err) 138 + return err 139 + } 140 + star.RepoAt = subjectUri 141 + repo, repoErr := db.GetRepoByAtUri(i.Db, subjectUri.String()) 142 + if repoErr == nil && repo.RepoDid != "" { 143 + star.SubjectDid = repo.RepoDid 144 + if enqErr := db.EnqueuePdsRewrite(i.Db, did, repo.RepoDid, tangled.FeedStarNSID, e.Commit.RKey, *record.Subject); enqErr != nil { 145 + l.Warn("failed to enqueue PDS rewrite for star", "err", enqErr, "did", did, "repoDid", repo.RepoDid) 146 + } 147 + } 148 + default: 149 + l.Error("star record has neither subject nor subjectDid") 150 + return fmt.Errorf("star record has neither subject nor subjectDid") 151 + } 152 + err = db.AddStar(i.Db, star) 129 153 case jmodels.CommitOperationDelete: 130 154 err = db.DeleteStarByRkey(i.Db, did, e.Commit.RKey) 131 155 } ··· 220 244 return err 221 245 } 222 246 223 - repoAt, err := syntax.ParseATURI(record.Repo) 224 - if err != nil { 225 - return err 247 + var repo *models.Repo 248 + if record.RepoDid != nil && *record.RepoDid != "" { 249 + repo, err = db.GetRepoByDid(i.Db, *record.RepoDid) 250 + if err != nil && !errors.Is(err, sql.ErrNoRows) { 251 + return fmt.Errorf("failed to look up repo by DID %s: %w", *record.RepoDid, err) 252 + } 226 253 } 227 - 228 - repo, err := db.GetRepoByAtUri(i.Db, repoAt.String()) 229 - if err != nil { 230 - return err 254 + if repo == nil && record.Repo != nil { 255 + repoAt, parseErr := syntax.ParseATURI(*record.Repo) 256 + if parseErr != nil { 257 + return parseErr 258 + } 259 + repo, err = db.GetRepoByAtUri(i.Db, repoAt.String()) 260 + if err != nil { 261 + return err 262 + } 263 + } 264 + if repo == nil { 265 + return fmt.Errorf("artifact record has neither valid repoDid nor repo field") 231 266 } 232 267 233 - ok, err := i.Enforcer.E.Enforce(did, repo.Knot, repo.DidSlashRepo(), "repo:push") 268 + ok, err := i.Enforcer.E.Enforce(did, repo.Knot, repo.RepoIdentifier(), "repo:push") 234 269 if err != nil || !ok { 235 270 return err 236 271 } 237 272 273 + repoDid := repo.RepoDid 274 + if repoDid == "" && record.RepoDid != nil { 275 + repoDid = *record.RepoDid 276 + } 277 + if repoDid != "" && (record.RepoDid == nil || *record.RepoDid == "") && record.Repo != nil { 278 + if enqErr := db.EnqueuePdsRewrite(i.Db, did, repoDid, tangled.RepoArtifactNSID, e.Commit.RKey, *record.Repo); enqErr != nil { 279 + l.Warn("failed to enqueue PDS rewrite for artifact", "err", enqErr, "did", did, "repoDid", repoDid) 280 + } 281 + } 282 + 238 283 createdAt, err := time.Parse(time.RFC3339, record.CreatedAt) 239 284 if err != nil { 240 285 createdAt = time.Now() ··· 243 288 artifact := models.Artifact{ 244 289 Did: did, 245 290 Rkey: e.Commit.RKey, 246 - RepoAt: repoAt, 291 + RepoAt: repo.RepoAt(), 292 + RepoDid: repoDid, 247 293 Tag: plumbing.Hash(record.Tag), 248 294 CreatedAt: createdAt, 249 295 BlobCid: cid.Cid(record.Artifact.Ref), ··· 822 868 823 869 issue := models.IssueFromRecord(did, rkey, record) 824 870 871 + if issue.RepoDid == "" && issue.RepoAt == "" { 872 + return fmt.Errorf("issue record has neither repo nor repoDid") 873 + } 874 + 825 875 if err := i.Validator.ValidateIssue(&issue); err != nil { 826 876 return fmt.Errorf("failed to validate issue: %w", err) 827 877 } 828 878 879 + if issue.RepoDid == "" && record.Repo != nil { 880 + repo, repoErr := db.GetRepoByAtUri(i.Db, *record.Repo) 881 + if repoErr == nil && repo.RepoDid != "" { 882 + issue.RepoDid = repo.RepoDid 883 + if enqErr := db.EnqueuePdsRewrite(i.Db, did, repo.RepoDid, tangled.RepoIssueNSID, rkey, *record.Repo); enqErr != nil { 884 + l.Warn("failed to enqueue PDS rewrite for issue", "err", enqErr, "did", did, "repoDid", repo.RepoDid) 885 + } 886 + } 887 + } 888 + 829 889 tx, err := ddb.BeginTx(ctx, nil) 830 890 if err != nil { 831 891 l.Error("failed to begin transaction", "err", err)
+9 -5
appview/middleware/middleware.go
··· 17 17 "tangled.org/core/appview/pages" 18 18 "tangled.org/core/appview/pagination" 19 19 "tangled.org/core/appview/reporesolver" 20 + "tangled.org/core/appview/state/userutil" 20 21 "tangled.org/core/idresolver" 21 22 "tangled.org/core/orm" 22 23 "tangled.org/core/rbac" ··· 161 162 return 162 163 } 163 164 164 - ok, err := mw.enforcer.E.Enforce(actor.Active.Did, f.Knot, f.DidSlashRepo(), requiredPerm) 165 + ok, err := mw.enforcer.E.Enforce(actor.Active.Did, f.Knot, f.RepoIdentifier(), requiredPerm) 165 166 if err != nil || !ok { 166 - log.Printf("%s does not have perms of a %s in repo %s", actor.Active.Did, requiredPerm, f.DidSlashRepo()) 167 + log.Printf("%s does not have perms of a %s in repo %s", actor.Active.Did, requiredPerm, f.RepoIdentifier()) 167 168 http.Error(w, "Forbiden", http.StatusUnauthorized) 168 169 return 169 170 } ··· 188 189 189 190 id, err := mw.idResolver.ResolveIdent(req.Context(), didOrHandle) 190 191 if err != nil { 191 - // invalid did or handle 192 192 log.Printf("failed to resolve did/handle '%s': %s\n", didOrHandle, err) 193 193 mw.pages.Error404(w) 194 194 return ··· 334 334 335 335 if r.Header.Get("User-Agent") == "Go-http-client/1.1" { 336 336 if r.URL.Query().Get("go-get") == "1" { 337 + modulePath := userutil.FlattenDid(fullName) 338 + if strings.Contains(modulePath, ":") { 339 + modulePath = userutil.FlattenDid(f.Did) + "/" + f.Name 340 + } 337 341 html := fmt.Sprintf( 338 342 `<meta name="go-import" content="tangled.sh/%s git https://tangled.sh/%s"/> 339 343 <meta name="go-import" content="tangled.org/%s git https://tangled.org/%s"/>`, 340 - fullName, fullName, 341 - fullName, fullName, 344 + modulePath, fullName, 345 + modulePath, fullName, 342 346 ) 343 347 w.Header().Set("Content-Type", "text/html") 344 348 w.Write([]byte(html))
+20 -6
appview/reporesolver/resolver.go
··· 36 36 37 37 // NOTE: this... should not even be here. the entire package will be removed in future refactor 38 38 func GetBaseRepoPath(r *http.Request, repo *models.Repo) string { 39 + if repo.RepoDid != "" { 40 + return repo.RepoDid 41 + } 39 42 var ( 40 43 user = chi.URLParam(r, "user") 41 44 name = chi.URLParam(r, "repo") 42 45 ) 43 46 if user == "" || name == "" { 44 - return repo.DidSlashRepo() 47 + return repo.RepoIdentifier() 45 48 } 46 49 return path.Join(user, name) 47 50 } ··· 77 80 roles := repoinfo.RolesInRepo{} 78 81 if user != nil && user.Active != nil { 79 82 isStarred = db.GetStarStatus(rr.execer, user.Active.Did, repoAt) 80 - roles.Roles = rr.enforcer.GetPermissionsInRepo(user.Active.Did, repo.Knot, repo.DidSlashRepo()) 83 + roles.Roles = rr.enforcer.GetPermissionsInRepo(user.Active.Did, repo.Knot, repo.RepoIdentifier()) 81 84 } 82 85 83 86 stats := repo.RepoStats 84 87 if stats == nil { 85 - starCount, err := db.GetStarCount(rr.execer, repoAt) 86 - if err != nil { 88 + var starCount int 89 + var starErr error 90 + if repo.RepoDid != "" { 91 + starCount, starErr = db.GetStarCountByRepoDid(rr.execer, repo.RepoDid, repoAt) 92 + } else { 93 + starCount, starErr = db.GetStarCount(rr.execer, repoAt) 94 + } 95 + if starErr != nil { 87 96 log.Println("failed to get star count for ", repoAt) 88 97 } 89 98 issueCount, err := db.GetIssueCount(rr.execer, repoAt) ··· 104 113 var sourceRepo *models.Repo 105 114 var err error 106 115 if repo.Source != "" { 107 - sourceRepo, err = db.GetRepoByAtUri(rr.execer, repo.Source) 116 + if strings.HasPrefix(repo.Source, "did:") { 117 + sourceRepo, err = db.GetRepoByDid(rr.execer, repo.Source) 118 + } else { 119 + sourceRepo, err = db.GetRepoByAtUri(rr.execer, repo.Source) 120 + } 108 121 if err != nil { 109 - log.Println("failed to get repo by at uri", err) 122 + log.Println("failed to get source repo", err) 110 123 } 111 124 } 112 125 ··· 114 127 // this is basically a models.Repo 115 128 OwnerDid: ownerId.DID.String(), 116 129 OwnerHandle: ownerId.Handle.String(), 130 + RepoDid: repo.RepoDid, 117 131 Name: repo.Name, 118 132 Rkey: repo.Rkey, 119 133 Description: repo.Description,
+7 -20
appview/state/git_http.go
··· 12 12 ) 13 13 14 14 func (s *State) InfoRefs(w http.ResponseWriter, r *http.Request) { 15 - user := r.Context().Value("resolvedId").(identity.Identity) 16 15 repo := r.Context().Value("repo").(*models.Repo) 17 16 18 17 scheme := "https" ··· 20 19 scheme = "http" 21 20 } 22 21 23 - targetURL := fmt.Sprintf("%s://%s/%s/%s/info/refs?%s", scheme, repo.Knot, user.DID, repo.Name, r.URL.RawQuery) 22 + targetURL := fmt.Sprintf("%s://%s/%s/info/refs?%s", scheme, repo.Knot, repo.RepoIdentifier(), r.URL.RawQuery) 24 23 s.proxyRequest(w, r, targetURL) 25 24 26 25 } 27 26 28 27 func (s *State) UploadArchive(w http.ResponseWriter, r *http.Request) { 29 - user, ok := r.Context().Value("resolvedId").(identity.Identity) 30 - if !ok { 31 - http.Error(w, "failed to resolve user", http.StatusInternalServerError) 32 - return 33 - } 34 28 repo := r.Context().Value("repo").(*models.Repo) 35 29 36 30 scheme := "https" ··· 38 32 scheme = "http" 39 33 } 40 34 41 - targetURL := fmt.Sprintf("%s://%s/%s/%s/git-upload-archive?%s", scheme, repo.Knot, user.DID, repo.Name, r.URL.RawQuery) 35 + targetURL := fmt.Sprintf("%s://%s/%s/git-upload-archive?%s", scheme, repo.Knot, repo.RepoIdentifier(), r.URL.RawQuery) 42 36 s.proxyRequest(w, r, targetURL) 43 37 } 44 38 45 39 func (s *State) UploadPack(w http.ResponseWriter, r *http.Request) { 46 - user, ok := r.Context().Value("resolvedId").(identity.Identity) 47 - if !ok { 48 - http.Error(w, "failed to resolve user", http.StatusInternalServerError) 49 - return 50 - } 51 40 repo := r.Context().Value("repo").(*models.Repo) 52 41 53 42 scheme := "https" ··· 55 44 scheme = "http" 56 45 } 57 46 58 - targetURL := fmt.Sprintf("%s://%s/%s/%s/git-upload-pack?%s", scheme, repo.Knot, user.DID, repo.Name, r.URL.RawQuery) 47 + targetURL := fmt.Sprintf("%s://%s/%s/git-upload-pack?%s", scheme, repo.Knot, repo.RepoIdentifier(), r.URL.RawQuery) 59 48 s.proxyRequest(w, r, targetURL) 60 49 } 61 50 62 51 func (s *State) ReceivePack(w http.ResponseWriter, r *http.Request) { 63 - user, ok := r.Context().Value("resolvedId").(identity.Identity) 64 - if !ok { 65 - http.Error(w, "failed to resolve user", http.StatusInternalServerError) 66 - return 67 - } 68 52 repo := r.Context().Value("repo").(*models.Repo) 69 53 70 54 scheme := "https" ··· 72 56 scheme = "http" 73 57 } 74 58 75 - targetURL := fmt.Sprintf("%s://%s/%s/%s/git-receive-pack?%s", scheme, repo.Knot, user.DID, repo.Name, r.URL.RawQuery) 59 + targetURL := fmt.Sprintf("%s://%s/%s/git-receive-pack?%s", scheme, repo.Knot, repo.RepoIdentifier(), r.URL.RawQuery) 76 60 s.proxyRequest(w, r, targetURL) 77 61 } 78 62 ··· 90 74 proxyReq.Header = r.Header 91 75 92 76 repoOwnerHandle := chi.URLParam(r, "user") 77 + if id, ok := r.Context().Value("resolvedId").(identity.Identity); ok && !id.Handle.IsInvalidHandle() { 78 + repoOwnerHandle = id.Handle.String() 79 + } 93 80 proxyReq.Header.Add("x-tangled-repo-owner-handle", repoOwnerHandle) 94 81 95 82 // Execute request
+42 -36
appview/state/knotstream.go
··· 2 2 3 3 import ( 4 4 "context" 5 + "database/sql" 5 6 "encoding/json" 6 7 "errors" 7 8 "fmt" ··· 86 87 return err 87 88 } 88 89 90 + if record.RepoDid == nil || *record.RepoDid == "" { 91 + logger.Error("gitRefUpdate missing repoDid, skipping", "owner_did", record.OwnerDid, "repo_name", record.RepoName) 92 + return fmt.Errorf("gitRefUpdate missing repoDid") 93 + } 94 + 89 95 knownKnots, err := enforcer.GetKnotsForUser(record.CommitterDid) 90 96 if err != nil { 91 97 return err ··· 95 101 } 96 102 97 103 logger.Info("processing gitRefUpdate event", 98 - "repo_did", record.RepoDid, 99 - "repo_name", record.RepoName, 104 + "repo_did", *record.RepoDid, 100 105 "ref", record.Ref, 101 106 "old_sha", record.OldSha, 102 107 "new_sha", record.NewSha) 103 108 104 - // trigger webhook notifications first (before other ops that might fail) 105 109 var errWebhook error 106 - repos, err := db.GetRepos( 107 - d, 108 - 0, 109 - orm.FilterEq("did", record.RepoDid), 110 - orm.FilterEq("name", record.RepoName), 111 - ) 112 - if err != nil { 113 - errWebhook = fmt.Errorf("failed to lookup repo for webhooks: %w", err) 114 - } else if len(repos) == 1 { 110 + 111 + repo, lookupErr := db.GetRepoByDid(d, *record.RepoDid) 112 + if lookupErr != nil && !errors.Is(lookupErr, sql.ErrNoRows) { 113 + return fmt.Errorf("failed to look up repo by DID %s: %w", *record.RepoDid, lookupErr) 114 + } 115 + 116 + var repos []models.Repo 117 + if lookupErr == nil { 118 + repos = []models.Repo{*repo} 119 + } 120 + 121 + if errWebhook == nil && len(repos) == 1 { 115 122 notifier.Push(ctx, &repos[0], record.Ref, record.OldSha, record.NewSha, record.CommitterDid) 116 123 } 117 124 ··· 167 174 168 175 func updateRepoLanguages(d *db.DB, record tangled.GitRefUpdate) error { 169 176 if record.Meta == nil || record.Meta.LangBreakdown == nil || record.Meta.LangBreakdown.Inputs == nil { 170 - return fmt.Errorf("empty language data for repo: %s/%s", record.RepoDid, record.RepoName) 177 + return fmt.Errorf("empty language data for repo: %v/%s", record.OwnerDid, record.RepoName) 171 178 } 172 179 173 - repos, err := db.GetRepos( 174 - d, 175 - 0, 176 - orm.FilterEq("did", record.RepoDid), 177 - orm.FilterEq("name", record.RepoName), 178 - ) 179 - if err != nil { 180 - return fmt.Errorf("failed to look for repo in DB (%s/%s): %w", record.RepoDid, record.RepoName, err) 180 + if record.RepoDid == nil || *record.RepoDid == "" { 181 + return fmt.Errorf("gitRefUpdate missing repoDid for language update") 181 182 } 182 - if len(repos) != 1 { 183 - return fmt.Errorf("incorrect number of repos returned: %d (expected 1)", len(repos)) 183 + 184 + r, lookupErr := db.GetRepoByDid(d, *record.RepoDid) 185 + if lookupErr != nil { 186 + return fmt.Errorf("failed to look up repo by DID %s: %w", *record.RepoDid, lookupErr) 184 187 } 185 - repo := repos[0] 188 + repo := *r 186 189 187 190 ref := plumbing.ReferenceName(record.Ref) 188 191 if !ref.IsBranch() { ··· 197 200 198 201 langs = append(langs, models.RepoLanguage{ 199 202 RepoAt: repo.RepoAt(), 203 + RepoDid: repo.RepoDid, 200 204 Ref: ref.Short(), 201 205 IsDefaultRef: record.Meta.IsDefaultRef, 202 206 Language: l.Lang, ··· 235 239 return fmt.Errorf("empty repo: nsid %s, rkey %s", msg.Nsid, msg.Rkey) 236 240 } 237 241 238 - // does this repo have a spindle configured? 239 - repos, err := db.GetRepos( 240 - d, 241 - 0, 242 - orm.FilterEq("did", record.TriggerMetadata.Repo.Did), 243 - orm.FilterEq("name", record.TriggerMetadata.Repo.Repo), 244 - ) 245 - if err != nil { 246 - return fmt.Errorf("failed to look for repo in DB: nsid %s, rkey %s, %w", msg.Nsid, msg.Rkey, err) 242 + if record.TriggerMetadata.Repo.RepoDid == nil || *record.TriggerMetadata.Repo.RepoDid == "" { 243 + return fmt.Errorf("pipeline missing repoDid: nsid %s, rkey %s", msg.Nsid, msg.Rkey) 247 244 } 248 - if len(repos) != 1 { 249 - return fmt.Errorf("incorrect number of repos returned: %d (expected 1)", len(repos)) 245 + 246 + repo, lookupErr := db.GetRepoByDid(d, *record.TriggerMetadata.Repo.RepoDid) 247 + if lookupErr != nil { 248 + return fmt.Errorf("failed to look up repo by DID %s: %w", *record.TriggerMetadata.Repo.RepoDid, lookupErr) 250 249 } 250 + repos := []models.Repo{*repo} 251 251 if repos[0].Spindle == "" { 252 252 return fmt.Errorf("repo does not have a spindle configured yet: nsid %s, rkey %s", msg.Nsid, msg.Rkey) 253 253 } ··· 280 280 return fmt.Errorf("failed to add trigger entry: %w", err) 281 281 } 282 282 283 + repoName := "" 284 + if record.TriggerMetadata.Repo.Repo != nil { 285 + repoName = *record.TriggerMetadata.Repo.Repo 286 + } 287 + 283 288 pipeline := models.Pipeline{ 284 289 Rkey: msg.Rkey, 285 290 Knot: source.Key(), 286 291 RepoOwner: syntax.DID(record.TriggerMetadata.Repo.Did), 287 - RepoName: record.TriggerMetadata.Repo.Repo, 292 + RepoName: repoName, 293 + RepoDid: repos[0].RepoDid, 288 294 TriggerId: int(triggerId), 289 295 Sha: sha, 290 296 }
+28 -2
appview/state/router.go
··· 1 1 package state 2 2 3 3 import ( 4 + "context" 5 + "database/sql" 6 + "errors" 4 7 "net/http" 5 8 "strings" 6 9 7 10 "github.com/go-chi/chi/v5" 11 + "tangled.org/core/appview/db" 8 12 "tangled.org/core/appview/issues" 9 13 "tangled.org/core/appview/knots" 10 14 "tangled.org/core/appview/labels" ··· 45 49 if len(pathParts) > 0 { 46 50 firstPart := pathParts[0] 47 51 48 - // if using a DID or handle, just continue as per usual 49 - if userutil.IsDid(firstPart) || userutil.IsHandle(firstPart) { 52 + if userutil.IsDid(firstPart) { 53 + repo, err := db.GetRepoByDid(s.db, firstPart) 54 + switch { 55 + case err == nil: 56 + remaining := "" 57 + if len(pathParts) > 1 { 58 + remaining = "/" + pathParts[1] 59 + } 60 + rewritten := "/" + repo.Did + "/" + repo.Name + remaining 61 + r2 := r.Clone(r.Context()) 62 + r2.URL.Path = rewritten 63 + r2.URL.RawPath = rewritten 64 + ctx := context.WithValue(r2.Context(), "repoDidCanonical", true) 65 + userRouter.ServeHTTP(w, r2.WithContext(ctx)) 66 + case errors.Is(err, sql.ErrNoRows): 67 + userRouter.ServeHTTP(w, r) 68 + default: 69 + s.logger.Error("db error looking up repo DID", "repoDid", firstPart, "err", err) 70 + http.Error(w, "internal server error", http.StatusInternalServerError) 71 + } 72 + return 73 + } 74 + 75 + if userutil.IsHandle(firstPart) { 50 76 userRouter.ServeHTTP(w, r) 51 77 return 52 78 }
+18 -5
appview/state/star.go
··· 12 12 "tangled.org/core/appview/db" 13 13 "tangled.org/core/appview/models" 14 14 "tangled.org/core/appview/pages" 15 + "tangled.org/core/orm" 15 16 "tangled.org/core/tid" 16 17 ) 17 18 ··· 40 41 case http.MethodPost: 41 42 createdAt := time.Now().Format(time.RFC3339) 42 43 rkey := tid.TID() 44 + 45 + starRecord := &tangled.FeedStar{ 46 + CreatedAt: createdAt, 47 + } 48 + repo, err := db.GetRepo(s.db, orm.FilterEq("at_uri", subjectUri.String())) 49 + repoHasDid := err == nil && repo.RepoDid != "" 50 + if repoHasDid { 51 + starRecord.SubjectDid = &repo.RepoDid 52 + } else { 53 + s := subjectUri.String() 54 + starRecord.Subject = &s 55 + } 56 + 43 57 resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 44 58 Collection: tangled.FeedStarNSID, 45 59 Repo: currentUser.Active.Did, 46 60 Rkey: rkey, 47 - Record: &lexutil.LexiconTypeDecoder{ 48 - Val: &tangled.FeedStar{ 49 - Subject: subjectUri.String(), 50 - CreatedAt: createdAt, 51 - }}, 61 + Record: &lexutil.LexiconTypeDecoder{Val: starRecord}, 52 62 }) 53 63 if err != nil { 54 64 log.Println("failed to create atproto record", err) ··· 61 71 RepoAt: subjectUri, 62 72 Rkey: rkey, 63 73 } 74 + if repoHasDid { 75 + star.SubjectDid = repo.RepoDid 76 + } 64 77 65 78 err = db.AddStar(s.db, star) 66 79 if err != nil {
+92 -38
appview/state/state.go
··· 41 41 "github.com/bluesky-social/indigo/atproto/syntax" 42 42 lexutil "github.com/bluesky-social/indigo/lex/util" 43 43 "github.com/bluesky-social/indigo/xrpc" 44 - securejoin "github.com/cyphar/filepath-securejoin" 44 + 45 45 "github.com/go-chi/chi/v5" 46 46 "github.com/posthog/posthog-go" 47 47 ) ··· 432 432 return 433 433 } 434 434 435 - // create atproto record for this repo 436 435 rkey := tid.TID() 436 + 437 + client, err := s.oauth.ServiceClient( 438 + r, 439 + oauth.WithService(domain), 440 + oauth.WithLxm(tangled.RepoCreateNSID), 441 + oauth.WithDev(s.config.Core.Dev), 442 + ) 443 + if err != nil { 444 + l.Error("service auth failed", "err", err) 445 + s.pages.Notice(w, "repo", "Failed to reach knot server.") 446 + return 447 + } 448 + 449 + input := &tangled.RepoCreate_Input{ 450 + Rkey: rkey, 451 + Name: repoName, 452 + DefaultBranch: &defaultBranch, 453 + } 454 + if rd := strings.TrimSpace(r.FormValue("repo_did")); rd != "" { 455 + input.RepoDid = &rd 456 + } 457 + 458 + createResp, xe := tangled.RepoCreate( 459 + r.Context(), 460 + client, 461 + input, 462 + ) 463 + if err := xrpcclient.HandleXrpcErr(xe); err != nil { 464 + l.Error("xrpc error", "xe", xe) 465 + s.pages.Notice(w, "repo", err.Error()) 466 + return 467 + } 468 + 469 + var repoDid string 470 + if createResp != nil && createResp.RepoDid != nil { 471 + repoDid = *createResp.RepoDid 472 + } 473 + if repoDid == "" { 474 + l.Error("knot returned empty repo DID") 475 + s.pages.Notice(w, "repo", "Knot failed to mint a repo DID. The knot may need to be upgraded.") 476 + return 477 + } 478 + 437 479 repo := &models.Repo{ 438 480 Did: user.Active.Did, 439 481 Name: repoName, ··· 442 484 Description: description, 443 485 Created: time.Now(), 444 486 Labels: s.config.Label.DefaultLabelDefs, 487 + RepoDid: repoDid, 445 488 } 446 489 record := repo.AsRecord() 447 490 491 + cleanupKnot := func() { 492 + go func() { 493 + delays := []time.Duration{0, 2 * time.Second, 5 * time.Second} 494 + for attempt, delay := range delays { 495 + time.Sleep(delay) 496 + deleteClient, dErr := s.oauth.ServiceClient( 497 + r, 498 + oauth.WithService(domain), 499 + oauth.WithLxm(tangled.RepoDeleteNSID), 500 + oauth.WithDev(s.config.Core.Dev), 501 + ) 502 + if dErr != nil { 503 + l.Error("failed to create delete client for knot cleanup", "attempt", attempt+1, "err", dErr) 504 + continue 505 + } 506 + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) 507 + if dErr := tangled.RepoDelete(ctx, deleteClient, &tangled.RepoDelete_Input{ 508 + Did: user.Active.Did, 509 + Name: repoName, 510 + Rkey: rkey, 511 + }); dErr != nil { 512 + cancel() 513 + l.Error("failed to clean up repo on knot after rollback", "attempt", attempt+1, "err", dErr) 514 + continue 515 + } 516 + cancel() 517 + l.Info("successfully cleaned up repo on knot after rollback", "attempt", attempt+1) 518 + return 519 + } 520 + l.Error("exhausted retries for knot cleanup, repo may be orphaned", 521 + "did", user.Active.Did, "repo", repoName, "knot", domain) 522 + }() 523 + } 524 + 448 525 atpClient, err := s.oauth.AuthorizedClient(r) 449 526 if err != nil { 450 527 l.Info("PDS write failed", "err", err) 528 + cleanupKnot() 451 529 s.pages.Notice(w, "repo", "Failed to write record to PDS.") 452 530 return 453 531 } ··· 462 540 }) 463 541 if err != nil { 464 542 l.Info("PDS write failed", "err", err) 543 + cleanupKnot() 465 544 s.pages.Notice(w, "repo", "Failed to announce repository creation.") 466 545 return 467 546 } ··· 477 556 return 478 557 } 479 558 480 - // The rollback function reverts a few things on failure: 481 - // - the pending txn 482 - // - the ACLs 483 - // - the atproto record created 484 559 rollback := func() { 485 560 err1 := tx.Rollback() 486 561 err2 := s.enforcer.E.LoadPolicy() 487 562 err3 := rollbackRecord(context.Background(), aturi, atpClient) 488 563 489 - // ignore txn complete errors, this is okay 490 564 if errors.Is(err1, sql.ErrTxDone) { 491 565 err1 = nil 492 566 } 493 567 494 568 if errs := errors.Join(err1, err2, err3); errs != nil { 495 569 l.Error("failed to rollback changes", "errs", errs) 496 - return 497 570 } 498 - } 499 - defer rollback() 500 - 501 - client, err := s.oauth.ServiceClient( 502 - r, 503 - oauth.WithService(domain), 504 - oauth.WithLxm(tangled.RepoCreateNSID), 505 - oauth.WithDev(s.config.Core.Dev), 506 - ) 507 - if err != nil { 508 - l.Error("service auth failed", "err", err) 509 - s.pages.Notice(w, "repo", "Failed to reach PDS.") 510 - return 511 - } 512 571 513 - xe := tangled.RepoCreate( 514 - r.Context(), 515 - client, 516 - &tangled.RepoCreate_Input{ 517 - Rkey: rkey, 518 - }, 519 - ) 520 - if err := xrpcclient.HandleXrpcErr(xe); err != nil { 521 - l.Error("xrpc error", "xe", xe) 522 - s.pages.Notice(w, "repo", err.Error()) 523 - return 572 + if aturi != "" { 573 + cleanupKnot() 574 + } 524 575 } 576 + defer rollback() 525 577 526 578 err = db.AddRepo(tx, repo) 527 579 if err != nil { ··· 530 582 return 531 583 } 532 584 533 - // acls 534 - p, _ := securejoin.SecureJoin(user.Active.Did, repoName) 535 - err = s.enforcer.AddRepo(user.Active.Did, domain, p) 585 + rbacPath := repo.RepoIdentifier() 586 + err = s.enforcer.AddRepo(user.Active.Did, domain, rbacPath) 536 587 if err != nil { 537 588 l.Error("acl setup failed", "err", err) 538 589 s.pages.Notice(w, "repo", "Failed to set up repository permissions.") ··· 553 604 return 554 605 } 555 606 556 - // reset the ATURI because the transaction completed successfully 557 607 aturi = "" 558 608 559 609 s.notifier.NewRepo(r.Context(), repo) 560 - s.pages.HxLocation(w, fmt.Sprintf("/%s/%s", user.Active.Did, repoName)) 610 + if repoDid != "" { 611 + s.pages.HxLocation(w, fmt.Sprintf("/%s", repoDid)) 612 + } else { 613 + s.pages.HxLocation(w, fmt.Sprintf("/%s/%s", user.Active.Did, repoName)) 614 + } 561 615 } 562 616 } 563 617
+1 -1
appview/validator/label.go
··· 109 109 // validate permissions: only collaborators can apply labels currently 110 110 // 111 111 // TODO: introduce a repo:triage permission 112 - ok, err := v.enforcer.IsPushAllowed(labelOp.Did, repo.Knot, repo.DidSlashRepo()) 112 + ok, err := v.enforcer.IsPushAllowed(labelOp.Did, repo.Knot, repo.RepoIdentifier()) 113 113 if err != nil { 114 114 return fmt.Errorf("failed to enforce permissions: %w", err) 115 115 }

History

14 rounds 2 comments
sign up or login to add to the discussion
1 commit
expand
appview: DID-based routing, state/handler/middleware updates
no conflicts, ready to merge
expand 0 comments
1 commit
expand
appview: DID-based routing, state/handler/middleware updates
expand 0 comments
1 commit
expand
appview: DID-based routing, state/handler/middleware updates
expand 2 comments

appview/state/state.go:631 won't this eventually redirect to /{owner}/{reponame}?

wdym by eventually? I was thinking to keep this in, so that we don't simply error out if someone does decide to be clever and link to their git repo by repoDID, we should render the page anyway to reward them for being clever instead of punishing heh

1 commit
expand
appview: DID-based routing, state/handler/middleware updates
expand 0 comments
1 commit
expand
appview: DID-based routing, state/handler/middleware updates
expand 0 comments
1 commit
expand
appview: update state, ingester, middleware, and resolver for repo DID
expand 0 comments
1 commit
expand
appview: update state, ingester, middleware, and resolver for repo DID
expand 0 comments
1 commit
expand
appview: update state, ingester, middleware, and resolver for repo DID
expand 0 comments
1 commit
expand
appview: update state, ingester, middleware, and resolver for repo DID
expand 0 comments
1 commit
expand
appview: update state, ingester, middleware, and resolver for repo DID
expand 0 comments
1 commit
expand
appview: update state, ingester, middleware, and resolver for repo DID
expand 0 comments
1 commit
expand
appview: update state, ingester, middleware, and resolver for repo DID
expand 0 comments
1 commit
expand
appview: update state, ingester, middleware, and resolver for repo DID
expand 0 comments
1 commit
expand
appview: update state, ingester, middleware, and resolver for repo DID
expand 0 comments