Monorepo for Tangled tangled.org
766
fork

Configure Feed

Select the types of activity you want to include in your feed.

knotmirror,spindle,appview: propagate repo rename across services #275

open opened by oyster.cafe targeting master from lt/repo-rename-by-rkey
Labels

None yet.

assignee

None yet.

Participants 1
AT URI
at://did:plc:3fwecdnvtcscjnrx2p4n7alz/sh.tangled.repo.pull/3mjm6w2jhfa22
+411 -18
Diff #0
+279
appview/db/repos_rename_test.go
··· 1 + package db 2 + 3 + import ( 4 + "context" 5 + "database/sql" 6 + "errors" 7 + "path/filepath" 8 + "testing" 9 + 10 + "tangled.org/core/appview/models" 11 + "tangled.org/core/orm" 12 + ) 13 + 14 + func newTestDB(t *testing.T) *DB { 15 + t.Helper() 16 + path := filepath.Join(t.TempDir(), "test.db") 17 + d, err := Make(context.Background(), path) 18 + if err != nil { 19 + t.Fatalf("Make: %v", err) 20 + } 21 + t.Cleanup(func() { d.Close() }) 22 + return d 23 + } 24 + 25 + func seedRepo(t *testing.T, d *DB, did, knot, name, rkey, repoDid string) *models.Repo { 26 + t.Helper() 27 + tx, err := d.Begin() 28 + if err != nil { 29 + t.Fatalf("Begin: %v", err) 30 + } 31 + repo := &models.Repo{ 32 + Did: did, 33 + Name: name, 34 + Knot: knot, 35 + Rkey: rkey, 36 + RepoDid: repoDid, 37 + } 38 + if err := AddRepo(tx, repo); err != nil { 39 + t.Fatalf("AddRepo: %v", err) 40 + } 41 + if err := tx.Commit(); err != nil { 42 + t.Fatalf("Commit: %v", err) 43 + } 44 + return repo 45 + } 46 + 47 + func TestRenameRepo_HappyPath(t *testing.T) { 48 + d := newTestDB(t) 49 + seedRepo(t, d, "did:plc:akshay", "knot.example", "foo", "foo", "did:plc:repo1") 50 + 51 + tx, err := d.Begin() 52 + if err != nil { 53 + t.Fatalf("Begin: %v", err) 54 + } 55 + defer tx.Rollback() 56 + 57 + if err := RenameRepo(tx, "did:plc:akshay", "foo", "bar", "Bar"); err != nil { 58 + t.Fatalf("RenameRepo: %v", err) 59 + } 60 + if err := tx.Commit(); err != nil { 61 + t.Fatalf("Commit: %v", err) 62 + } 63 + 64 + got, err := GetRepoByDid(d, "did:plc:repo1") 65 + if err != nil { 66 + t.Fatalf("GetRepoByDid: %v", err) 67 + } 68 + if got.Rkey != "bar" { 69 + t.Errorf("rkey = %q, want %q", got.Rkey, "bar") 70 + } 71 + if got.Name != "Bar" { 72 + t.Errorf("name = %q, want %q", got.Name, "Bar") 73 + } 74 + } 75 + 76 + func TestUpdateRepoDisplayName_HappyPath(t *testing.T) { 77 + d := newTestDB(t) 78 + seedRepo(t, d, "did:plc:akshay", "knot.example", "foo", "foo", "did:plc:repo1") 79 + 80 + if err := UpdateRepoDisplayName(d, "did:plc:akshay", "foo", "Foo"); err != nil { 81 + t.Fatalf("UpdateRepoDisplayName: %v", err) 82 + } 83 + 84 + got, err := GetRepoByDid(d, "did:plc:repo1") 85 + if err != nil { 86 + t.Fatalf("GetRepoByDid: %v", err) 87 + } 88 + if got.Name != "Foo" { 89 + t.Errorf("name = %q, want %q", got.Name, "Foo") 90 + } 91 + if got.Rkey != "foo" { 92 + t.Errorf("rkey should be unchanged but got %q, want %q", got.Rkey, "foo") 93 + } 94 + } 95 + 96 + func TestRecordAndLookupRepoRename(t *testing.T) { 97 + d := newTestDB(t) 98 + seedRepo(t, d, "did:plc:akshay", "knot.example", "bar", "rkey1", "did:plc:repo1") 99 + 100 + if err := RecordRepoRename(d, "did:plc:akshay", "foo", "did:plc:repo1"); err != nil { 101 + t.Fatalf("RecordRepoRename: %v", err) 102 + } 103 + 104 + repo, err := LookupRepoRename(d, "did:plc:akshay", "foo") 105 + if err != nil { 106 + t.Fatalf("LookupRepoRename: %v", err) 107 + } 108 + if repo.RepoDid != "did:plc:repo1" { 109 + t.Errorf("repoDid = %q, want %q", repo.RepoDid, "did:plc:repo1") 110 + } 111 + if repo.Name != "bar" { 112 + t.Errorf("name = %q, want %q", repo.Name, "bar") 113 + } 114 + } 115 + 116 + func TestLookupRepoRename_MultipleOldNamesResolveToCurrent(t *testing.T) { 117 + d := newTestDB(t) 118 + seedRepo(t, d, "did:plc:akshay", "knot.example", "baz", "baz", "did:plc:repo1") 119 + 120 + if err := RecordRepoRename(d, "did:plc:akshay", "foo", "did:plc:repo1"); err != nil { 121 + t.Fatalf("record foo: %v", err) 122 + } 123 + if err := RecordRepoRename(d, "did:plc:akshay", "bar", "did:plc:repo1"); err != nil { 124 + t.Fatalf("record bar: %v", err) 125 + } 126 + 127 + for _, oldName := range []string{"foo", "bar"} { 128 + repo, err := LookupRepoRename(d, "did:plc:akshay", oldName) 129 + if err != nil { 130 + t.Fatalf("lookup %q: %v", oldName, err) 131 + } 132 + if repo.Name != "baz" { 133 + t.Errorf("lookup %q: name = %q, want %q", oldName, repo.Name, "baz") 134 + } 135 + } 136 + } 137 + 138 + func TestRecordRepoRename_UpsertRefreshesTarget(t *testing.T) { 139 + d := newTestDB(t) 140 + seedRepo(t, d, "did:plc:akshay", "knot.example", "current", "rkey1", "did:plc:repo1") 141 + seedRepo(t, d, "did:plc:akshay", "knot.example", "other", "rkey2", "did:plc:repo2") 142 + 143 + if err := RecordRepoRename(d, "did:plc:akshay", "shared", "did:plc:repo1"); err != nil { 144 + t.Fatalf("first record: %v", err) 145 + } 146 + if err := RecordRepoRename(d, "did:plc:akshay", "shared", "did:plc:repo2"); err != nil { 147 + t.Fatalf("second record: %v", err) 148 + } 149 + 150 + repo, err := LookupRepoRename(d, "did:plc:akshay", "shared") 151 + if err != nil { 152 + t.Fatalf("LookupRepoRename: %v", err) 153 + } 154 + if repo.RepoDid != "did:plc:repo2" { 155 + t.Errorf("latest record should win: repoDid = %q, want %q", repo.RepoDid, "did:plc:repo2") 156 + } 157 + } 158 + 159 + func TestLookupRepoRename_StaleSelfHeal(t *testing.T) { 160 + d := newTestDB(t) 161 + 162 + if err := RecordRepoRename(d, "did:plc:akshay", "foo", "did:plc:ghost"); err != nil { 163 + t.Fatalf("RecordRepoRename: %v", err) 164 + } 165 + 166 + _, err := LookupRepoRename(d, "did:plc:akshay", "foo") 167 + if !errors.Is(err, sql.ErrNoRows) { 168 + t.Errorf("target should be gone and fall through to 404: err = %v, want sql.ErrNoRows", err) 169 + } 170 + } 171 + 172 + func TestLookupRepoRename_NoRow(t *testing.T) { 173 + d := newTestDB(t) 174 + 175 + _, err := LookupRepoRename(d, "did:plc:akshay", "nothing") 176 + if !errors.Is(err, sql.ErrNoRows) { 177 + t.Errorf("err = %v, want sql.ErrNoRows", err) 178 + } 179 + } 180 + 181 + func TestDuplicateRkeyUnderSameDID_Rejected(t *testing.T) { 182 + d := newTestDB(t) 183 + seedRepo(t, d, "did:plc:akshay", "knot.example", "myrepo", "myrepo", "did:plc:repo1") 184 + 185 + tx, err := d.Begin() 186 + if err != nil { 187 + t.Fatalf("Begin: %v", err) 188 + } 189 + defer tx.Rollback() 190 + 191 + err = AddRepo(tx, &models.Repo{ 192 + Did: "did:plc:akshay", 193 + Name: "myrepo", 194 + Knot: "knot.example", 195 + Rkey: "myrepo", 196 + RepoDid: "did:plc:repo2", 197 + }) 198 + if err == nil { 199 + t.Fatal("expected unique violation for duplicate (did, rkey), got nil") 200 + } 201 + if !orm.IsUniqueViolation(err) { 202 + t.Errorf("err = %v, want unique violation", err) 203 + } 204 + } 205 + 206 + func TestRenameRepo_OldRkeyRowGone(t *testing.T) { 207 + d := newTestDB(t) 208 + seedRepo(t, d, "did:plc:akshay", "knot.example", "old", "old", "did:plc:repo1") 209 + 210 + tx, err := d.Begin() 211 + if err != nil { 212 + t.Fatalf("Begin: %v", err) 213 + } 214 + defer tx.Rollback() 215 + 216 + if err := RenameRepo(tx, "did:plc:akshay", "old", "new", "New"); err != nil { 217 + t.Fatalf("RenameRepo: %v", err) 218 + } 219 + if err := tx.Commit(); err != nil { 220 + t.Fatalf("Commit: %v", err) 221 + } 222 + 223 + got, err := GetRepoByDid(d, "did:plc:repo1") 224 + if err != nil { 225 + t.Fatalf("GetRepoByDid: %v", err) 226 + } 227 + if got.Rkey != "new" { 228 + t.Errorf("rkey = %q, want %q", got.Rkey, "new") 229 + } 230 + 231 + var dummy int 232 + err = d.QueryRow(`select 1 from repos where did = ? and rkey = ?`, "did:plc:akshay", "old").Scan(&dummy) 233 + if !errors.Is(err, sql.ErrNoRows) { 234 + t.Errorf("old rkey row should be gone, got err = %v", err) 235 + } 236 + } 237 + 238 + func TestRenameRepo_PipelineRenamed(t *testing.T) { 239 + d := newTestDB(t) 240 + seedRepo(t, d, "did:plc:akshay", "knot.example", "old", "old", "did:plc:repo1") 241 + 242 + if _, err := d.Exec( 243 + `insert into triggers (kind) values (?)`, "push", 244 + ); err != nil { 245 + t.Fatalf("seed trigger: %v", err) 246 + } 247 + if _, err := d.Exec( 248 + `insert into pipelines (rkey, knot, repo_owner, repo_name, sha, trigger_id, repo_did) 249 + values (?, ?, ?, ?, ?, ?, ?)`, 250 + "pipe1", "knot.example", "did:plc:akshay", "old", 251 + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", 1, "did:plc:repo1", 252 + ); err != nil { 253 + t.Fatalf("seed pipeline: %v", err) 254 + } 255 + 256 + tx, err := d.Begin() 257 + if err != nil { 258 + t.Fatalf("Begin: %v", err) 259 + } 260 + defer tx.Rollback() 261 + 262 + if err := RenameRepo(tx, "did:plc:akshay", "old", "new", "New"); err != nil { 263 + t.Fatalf("RenameRepo: %v", err) 264 + } 265 + if err := tx.Commit(); err != nil { 266 + t.Fatalf("Commit: %v", err) 267 + } 268 + 269 + var repoName string 270 + if err := d.QueryRow( 271 + `select repo_name from pipelines where repo_owner = ? and rkey = ?`, 272 + "did:plc:akshay", "pipe1", 273 + ).Scan(&repoName); err != nil { 274 + t.Fatalf("query pipeline: %v", err) 275 + } 276 + if repoName != "new" { 277 + t.Errorf("pipeline repo_name = %q, want %q", repoName, "new") 278 + } 279 + }
+50
appview/repo/rename_test.go
··· 1 + package repo 2 + 3 + import ( 4 + "strings" 5 + "testing" 6 + ) 7 + 8 + func TestValidateRenameInput(t *testing.T) { 9 + cases := []struct { 10 + name string 11 + currentName string 12 + raw string 13 + wantName string 14 + wantErrSub string 15 + }{ 16 + {"happy path", "foo", "bar", "bar", ""}, 17 + {"trims surrounding whitespace", "foo", " bar ", "bar", ""}, 18 + {"strips .git suffix", "foo", "bar.git", "bar", ""}, 19 + {"empty after trim", "foo", " ", "", "cannot be empty"}, 20 + {"raw empty", "foo", "", "", "cannot be empty"}, 21 + {"path traversal slash", "foo", "../bar", "", "invalid path"}, 22 + {"invalid character", "foo", "ba r", "", "alphanumeric"}, 23 + {"same name as current", "foo", "foo", "", "matches the current name"}, 24 + {"case-only diff is not a no-op", "foo", "Foo", "Foo", ""}, 25 + {"strip-git collides with current", "foo", "foo.git", "", "matches the current name"}, 26 + } 27 + for _, tc := range cases { 28 + t.Run(tc.name, func(t *testing.T) { 29 + got, err := validateRenameInput(tc.currentName, tc.raw) 30 + if tc.wantErrSub == "" { 31 + if err != nil { 32 + t.Fatalf("err = %v, want nil", err) 33 + } 34 + if got != tc.wantName { 35 + t.Errorf("name = %q, want %q", got, tc.wantName) 36 + } 37 + return 38 + } 39 + if err == nil { 40 + t.Fatalf("err = nil, want error containing %q", tc.wantErrSub) 41 + } 42 + if got != "" { 43 + t.Errorf("name = %q, want empty on error", got) 44 + } 45 + if !strings.Contains(err.Error(), tc.wantErrSub) { 46 + t.Errorf("err = %q, want substring %q", err.Error(), tc.wantErrSub) 47 + } 48 + }) 49 + } 50 + }
+62 -10
spindle/ingester.go
··· 5 5 "encoding/json" 6 6 "errors" 7 7 "fmt" 8 + "strings" 8 9 "time" 9 10 10 11 "tangled.org/core/api/tangled" ··· 154 155 } 155 156 156 157 domain := s.cfg.Server.Hostname 158 + rkey := e.Commit.RKey 157 159 158 160 // no spindle configured for this repo 159 161 if record.Spindle == nil { 160 - l.Info("no spindle configured", "name", record.Name) 162 + l.Info("no spindle configured", "rkey", rkey) 161 163 return nil 162 164 } 163 165 164 166 // this repo did not want this spindle 165 167 if *record.Spindle != domain { 166 - l.Info("different spindle configured", "name", record.Name, "spindle", *record.Spindle, "domain", domain) 168 + l.Info("different spindle configured", "rkey", rkey, "spindle", *record.Spindle, "domain", domain) 167 169 return nil 168 170 } 169 171 170 172 // add this repo to the watch list 171 - if err := s.db.AddRepo(record.Knot, did, record.Name); err != nil { 173 + if err := s.db.AddRepo(record.Knot, did, rkey); err != nil { 172 174 l.Error("failed to add repo", "error", err) 173 175 return fmt.Errorf("failed to add repo: %w", err) 174 176 } 175 177 176 - didSlashRepo, err := securejoin.SecureJoin(did, record.Name) 178 + didSlashRepo, err := securejoin.SecureJoin(did, rkey) 177 179 if err != nil { 178 180 return err 179 181 } ··· 228 230 var rbacResource string 229 231 var ownerDid string 230 232 switch { 231 - case record.Repo != nil: 232 - repoAt, parseErr := syntax.ParseATURI(*record.Repo) 233 + case strings.HasPrefix(record.Repo, "did:"): 234 + resolvedOwner, repoName, lookupErr := s.resolveRepoDid(ctx, e.Did, record.Repo) 235 + if lookupErr != nil { 236 + return fmt.Errorf("unknown repo DID %s: %w", record.Repo, lookupErr) 237 + } 238 + ownerDid = resolvedOwner 239 + rbacResource, _ = securejoin.SecureJoin(ownerDid, repoName) 240 + 241 + case strings.Contains(record.Repo, "/"): 242 + repoAt, parseErr := syntax.ParseATURI(record.Repo) 233 243 if parseErr != nil { 234 - l.Info("rejecting record, invalid repoAt", "repoAt", *record.Repo) 244 + l.Info("rejecting record, invalid repoAt", "repoAt", record.Repo) 235 245 return nil 236 246 } 237 247 ··· 249 259 return getErr 250 260 } 251 261 252 - repo := resp.Value.Val.(*tangled.Repo) 253 - rbacResource, _ = securejoin.SecureJoin(owner.DID.String(), repo.Name) 262 + if _, ok := resp.Value.Val.(*tangled.Repo); !ok { 263 + return fmt.Errorf("record at %s is not a tangled.Repo", repoAt) 264 + } 265 + rbacResource, _ = securejoin.SecureJoin(owner.DID.String(), repoAt.RecordKey().String()) 254 266 ownerDid = owner.DID.String() 255 267 256 268 default: 257 - l.Info("rejecting collaborator record without repo at-uri (spindle RBAC keyed by owner/name)") 269 + l.Info("rejecting collaborator record with unrecognized repo format", "repo", record.Repo) 258 270 return nil 259 271 } 260 272 ··· 272 284 return nil 273 285 } 274 286 287 + func (s *Spindle) resolveRepoDid(ctx context.Context, ownerDid string, repoDid string) (string, string, error) { 288 + owner, resolveErr := s.res.ResolveIdent(ctx, ownerDid) 289 + if resolveErr != nil || owner.Handle.IsInvalidHandle() { 290 + return "", "", fmt.Errorf("failed to resolve owner %s: %w", ownerDid, resolveErr) 291 + } 292 + 293 + xrpcc := xrpc.Client{ 294 + Host: owner.PDSEndpoint(), 295 + } 296 + 297 + cursor := "" 298 + for { 299 + resp, listErr := comatproto.RepoListRecords(ctx, &xrpcc, tangled.RepoNSID, cursor, 100, ownerDid, false) 300 + if listErr != nil { 301 + return "", "", fmt.Errorf("failed to list repo records for %s: %w", ownerDid, listErr) 302 + } 303 + 304 + for _, r := range resp.Records { 305 + if r == nil { 306 + continue 307 + } 308 + repo, ok := r.Value.Val.(*tangled.Repo) 309 + if !ok { 310 + continue 311 + } 312 + if repo.RepoDid != nil && *repo.RepoDid == repoDid { 313 + rkey := r.Uri[strings.LastIndex(r.Uri, "/")+1:] 314 + return ownerDid, rkey, nil 315 + } 316 + } 317 + 318 + if resp.Cursor == nil || *resp.Cursor == "" { 319 + break 320 + } 321 + cursor = *resp.Cursor 322 + } 323 + 324 + return "", "", fmt.Errorf("repo DID %s not found in records for %s", repoDid, ownerDid) 325 + } 326 + 275 327 func (s *Spindle) fetchAndAddCollaborators(ctx context.Context, owner *identity.Identity, didSlashRepo string) error { 276 328 l := s.l.With("component", "ingester", "handler", "fetchAndAddCollaborators") 277 329
+5 -2
spindle/xrpc/add_secret.go
··· 61 61 return 62 62 } 63 63 64 - repo := resp.Value.Val.(*tangled.Repo) 65 - didPath, err := securejoin.SecureJoin(ident.DID.String(), repo.Name) 64 + if _, ok := resp.Value.Val.(*tangled.Repo); !ok { 65 + fail(xrpcerr.RepoNotFoundError) 66 + return 67 + } 68 + didPath, err := securejoin.SecureJoin(ident.DID.String(), repoAt.RecordKey().String()) 66 69 if err != nil { 67 70 fail(xrpcerr.GenericError(err)) 68 71 return
+5 -2
spindle/xrpc/list_secrets.go
··· 56 56 return 57 57 } 58 58 59 - repo := resp.Value.Val.(*tangled.Repo) 60 - didPath, err := securejoin.SecureJoin(ident.DID.String(), repo.Name) 59 + if _, ok := resp.Value.Val.(*tangled.Repo); !ok { 60 + fail(xrpcerr.RepoNotFoundError) 61 + return 62 + } 63 + didPath, err := securejoin.SecureJoin(ident.DID.String(), repoAt.RecordKey().String()) 61 64 if err != nil { 62 65 fail(xrpcerr.GenericError(err)) 63 66 return
+5 -2
spindle/xrpc/pipeline_cancel_pipeline.go
··· 66 66 return 67 67 } 68 68 69 - repo := resp.Value.Val.(*tangled.Repo) 70 - didSlashRepo, err := securejoin.SecureJoin(ident.DID.String(), repo.Name) 69 + if _, ok := resp.Value.Val.(*tangled.Repo); !ok { 70 + fail(xrpcerr.RepoNotFoundError) 71 + return 72 + } 73 + didSlashRepo, err := securejoin.SecureJoin(ident.DID.String(), repoAt.RecordKey().String()) 71 74 if err != nil { 72 75 fail(xrpcerr.GenericError(err)) 73 76 return
+5 -2
spindle/xrpc/remove_secret.go
··· 55 55 return 56 56 } 57 57 58 - repo := resp.Value.Val.(*tangled.Repo) 59 - didPath, err := securejoin.SecureJoin(ident.DID.String(), repo.Name) 58 + if _, ok := resp.Value.Val.(*tangled.Repo); !ok { 59 + fail(xrpcerr.RepoNotFoundError) 60 + return 61 + } 62 + didPath, err := securejoin.SecureJoin(ident.DID.String(), repoAt.RecordKey().String()) 60 63 if err != nil { 61 64 fail(xrpcerr.GenericError(err)) 62 65 return

History

1 round 0 comments
sign up or login to add to the discussion
oyster.cafe submitted #0
1 commit
expand
knotmirror,spindle,appview: propagate repo rename across services
failed to check merge status: invalid xrpc request
expand 0 comments