mirror of
https://tangled.org/evan.jarrett.net/at-container-registry
synced 2026-08-29 04:06:58 +00:00
general appview bugfixes
This commit is contained in:
@@ -74,7 +74,7 @@ jetstream:
|
||||
relay_endpoints:
|
||||
- https://relay1.us-east.bsky.network
|
||||
- https://relay1.us-west.bsky.network
|
||||
- https://zlay.waow.tech
|
||||
- https://relay.waow.tech
|
||||
# JWT authentication settings.
|
||||
auth:
|
||||
# RSA private key for signing registry JWTs issued to Docker clients.
|
||||
|
||||
@@ -333,9 +333,10 @@ func GetAvailableHolds(db DBTX, userDID string) ([]AvailableHold, error) {
|
||||
c.permissions
|
||||
FROM hold_captain_records h
|
||||
LEFT JOIN hold_crew_members c ON h.hold_did = c.hold_did AND c.member_did = ?1
|
||||
WHERE h.allow_all_crew = 1
|
||||
WHERE (h.successor IS NULL OR h.successor = '')
|
||||
AND (h.allow_all_crew = 1
|
||||
OR h.owner_did = ?1
|
||||
OR c.member_did IS NOT NULL
|
||||
OR c.member_did IS NOT NULL)
|
||||
ORDER BY
|
||||
CASE
|
||||
WHEN h.owner_did = ?1 THEN 0
|
||||
|
||||
@@ -2022,6 +2022,32 @@ func (h *HoldDIDDB) UpdateManifestHoldDID(did, oldHoldDID, newHoldDID string) (i
|
||||
return UpdateManifestHoldDID(h.db, did, oldHoldDID, newHoldDID)
|
||||
}
|
||||
|
||||
// GetDistinctManifestHoldDIDs returns all distinct hold DIDs referenced by a user's manifests.
|
||||
func GetDistinctManifestHoldDIDs(db DBTX, did string) ([]string, error) {
|
||||
rows, err := db.Query(`
|
||||
SELECT DISTINCT hold_endpoint FROM manifests
|
||||
WHERE did = ? AND hold_endpoint != ''
|
||||
`, did)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var holds []string
|
||||
for rows.Next() {
|
||||
var h string
|
||||
if err := rows.Scan(&h); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
holds = append(holds, h)
|
||||
}
|
||||
return holds, rows.Err()
|
||||
}
|
||||
|
||||
// GetDistinctManifestHoldDIDs wraps the package-level function.
|
||||
func (h *HoldDIDDB) GetDistinctManifestHoldDIDs(did string) ([]string, error) {
|
||||
return GetDistinctManifestHoldDIDs(h.db, did)
|
||||
}
|
||||
|
||||
// IsManifestReferenced checks if a digest is a child of any manifest list for the user.
|
||||
// Implements storage.ManifestReferenceChecker.
|
||||
func (h *HoldDIDDB) IsManifestReferenced(did, digest string) (bool, error) {
|
||||
|
||||
@@ -594,28 +594,36 @@ func (h *RepositoryTagsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request
|
||||
ociClient = user.OciClient
|
||||
}
|
||||
|
||||
// Resolve viewer's default hold for per-entry badges
|
||||
var viewerDefaultHold string
|
||||
if viewerDID != "" {
|
||||
viewerDefaultHold = db.GetUserHoldDID(h.ReadOnlyDB, viewerDID)
|
||||
}
|
||||
|
||||
data := struct {
|
||||
Owner *db.User
|
||||
Repository *db.Repository
|
||||
Entries []db.ManifestEntry
|
||||
IsOwner bool
|
||||
ScanBatchParams []template.HTML
|
||||
RegistryURL string
|
||||
OciClient string
|
||||
HasMore bool
|
||||
NextOffset int
|
||||
IsFirstPage bool
|
||||
Owner *db.User
|
||||
Repository *db.Repository
|
||||
Entries []db.ManifestEntry
|
||||
IsOwner bool
|
||||
ScanBatchParams []template.HTML
|
||||
RegistryURL string
|
||||
OciClient string
|
||||
HasMore bool
|
||||
NextOffset int
|
||||
IsFirstPage bool
|
||||
ViewerDefaultHold string
|
||||
}{
|
||||
Owner: owner,
|
||||
Repository: &db.Repository{Name: repository},
|
||||
Entries: entries,
|
||||
IsOwner: isOwner,
|
||||
ScanBatchParams: scanBatchParams,
|
||||
RegistryURL: h.RegistryURL,
|
||||
OciClient: ociClient,
|
||||
HasMore: hasMore,
|
||||
NextOffset: offset + pageSize,
|
||||
IsFirstPage: isFirstPage,
|
||||
Owner: owner,
|
||||
Repository: &db.Repository{Name: repository},
|
||||
Entries: entries,
|
||||
IsOwner: isOwner,
|
||||
ScanBatchParams: scanBatchParams,
|
||||
RegistryURL: h.RegistryURL,
|
||||
OciClient: ociClient,
|
||||
HasMore: hasMore,
|
||||
NextOffset: offset + pageSize,
|
||||
IsFirstPage: isFirstPage,
|
||||
ViewerDefaultHold: viewerDefaultHold,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
|
||||
@@ -66,20 +66,15 @@ func NewProcessor(database db.DBTX, useCache bool, statsCache *StatsCache) *Proc
|
||||
// EnsureUser resolves and upserts a user by DID
|
||||
// Uses cache if enabled (Worker), queries DB if cache disabled (Backfill)
|
||||
func (p *Processor) EnsureUser(ctx context.Context, did string) error {
|
||||
// Check cache first (if enabled)
|
||||
// Check cache first (if enabled) — within a single backfill run,
|
||||
// a user's identity won't change, so the cache hit is safe.
|
||||
if p.useCache && p.userCache != nil {
|
||||
if _, ok := p.userCache.cache[did]; ok {
|
||||
// User in cache - just update last seen timestamp
|
||||
return db.UpdateUserLastSeen(p.db, did)
|
||||
}
|
||||
} else if !p.useCache {
|
||||
// No cache - check if user already exists in DB
|
||||
existingUser, err := db.GetUserByDID(p.db, did)
|
||||
if err == nil && existingUser != nil {
|
||||
// User exists - just update last seen timestamp
|
||||
return db.UpdateUserLastSeen(p.db, did)
|
||||
}
|
||||
}
|
||||
// No cache early-return: always re-resolve identity so stale handles
|
||||
// (e.g., user changed handle between backfill runs) get corrected.
|
||||
|
||||
// Resolve DID to get handle and PDS endpoint
|
||||
resolvedDID, handle, pdsEndpoint, err := atproto.ResolveIdentity(ctx, did)
|
||||
|
||||
@@ -40,6 +40,7 @@ type PushWebhookEvent struct {
|
||||
type HoldDIDLookup interface {
|
||||
GetLatestHoldDIDForRepo(did, repository string) (string, error)
|
||||
UpdateManifestHoldDID(did, oldHoldDID, newHoldDID string) (int64, error)
|
||||
GetDistinctManifestHoldDIDs(did string) ([]string, error)
|
||||
}
|
||||
|
||||
// RegistryContext bundles all the context needed for registry operations
|
||||
|
||||
@@ -21,6 +21,13 @@ func (m *mockHoldDIDLookup) UpdateManifestHoldDID(did, oldHoldDID, newHoldDID st
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func (m *mockHoldDIDLookup) GetDistinctManifestHoldDIDs(did string) ([]string, error) {
|
||||
if m.holdDID != "" {
|
||||
return []string{m.holdDID}, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func TestRegistryContext_Fields(t *testing.T) {
|
||||
// Create a sample RegistryContext
|
||||
ctx := &RegistryContext{
|
||||
|
||||
@@ -17,13 +17,13 @@ import (
|
||||
var drainLocks sync.Map
|
||||
|
||||
// MigrateManifestsForSuccessor rewrites manifest records and profile
|
||||
// when a user's defaultHold has a successor. Best-effort, runs in background.
|
||||
// when any of the user's holds have a successor. Best-effort, runs in background.
|
||||
//
|
||||
// Steps:
|
||||
// 1. Get user's sailor profile — check if defaultHold has a successor
|
||||
// 2. Update profile.DefaultHold from oldHold → newHold
|
||||
// 3. Walk all io.atcr.manifest records, rewrite holdDid from oldHold → newHold
|
||||
// 4. Update appview's local manifests table to match
|
||||
// 1. Get user's sailor profile
|
||||
// 2. Collect candidate holds: profile.DefaultHold + distinct holds from manifests DB
|
||||
// 3. For each hold with a successor: rewrite PDS manifest records and local DB
|
||||
// 4. If profile.DefaultHold itself had a successor, update the profile too
|
||||
func MigrateManifestsForSuccessor(
|
||||
ctx context.Context,
|
||||
client *atproto.Client,
|
||||
@@ -43,35 +43,64 @@ func MigrateManifestsForSuccessor(
|
||||
slog.Debug("Drain: failed to get profile", "component", "storage/drain", "did", did, "error", err)
|
||||
return
|
||||
}
|
||||
if profile == nil || profile.DefaultHold == "" {
|
||||
|
||||
// 2. Collect candidate holds to check for successors.
|
||||
// Start with profile.DefaultHold, then add any distinct holds from the DB.
|
||||
candidates := make(map[string]bool)
|
||||
if profile != nil && profile.DefaultHold != "" {
|
||||
candidates[profile.DefaultHold] = true
|
||||
}
|
||||
if db != nil {
|
||||
manifestHolds, err := db.GetDistinctManifestHoldDIDs(did)
|
||||
if err != nil {
|
||||
slog.Warn("Drain: failed to get distinct manifest holds", "component", "storage/drain", "did", did, "error", err)
|
||||
}
|
||||
for _, h := range manifestHolds {
|
||||
candidates[h] = true
|
||||
}
|
||||
}
|
||||
|
||||
if len(candidates) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// 2. Check if their defaultHold has a successor
|
||||
oldHold := profile.DefaultHold
|
||||
captain, err := authorizer.GetCaptainRecord(ctx, oldHold)
|
||||
if err != nil {
|
||||
slog.Debug("Drain: failed to get captain record", "component", "storage/drain", "did", did, "hold", oldHold, "error", err)
|
||||
return
|
||||
}
|
||||
if captain == nil || captain.Successor == "" {
|
||||
return // No successor — nothing to drain
|
||||
}
|
||||
newHold := captain.Successor
|
||||
// 3. Check each candidate for a successor and drain if found
|
||||
for oldHold := range candidates {
|
||||
captain, err := authorizer.GetCaptainRecord(ctx, oldHold)
|
||||
if err != nil {
|
||||
slog.Debug("Drain: failed to get captain record", "component", "storage/drain", "did", did, "hold", oldHold, "error", err)
|
||||
continue
|
||||
}
|
||||
if captain == nil || captain.Successor == "" {
|
||||
continue
|
||||
}
|
||||
newHold := captain.Successor
|
||||
|
||||
slog.Info("Starting hold drain", "component", "storage/drain", "did", did, "from", oldHold, "to", newHold)
|
||||
slog.Info("Starting hold drain", "component", "storage/drain", "did", did, "from", oldHold, "to", newHold)
|
||||
|
||||
// 3. Update profile.DefaultHold
|
||||
profile.DefaultHold = newHold
|
||||
profile.UpdatedAt = time.Now()
|
||||
if err := UpdateProfile(ctx, client, profile); err != nil {
|
||||
slog.Warn("Drain: failed to update profile", "component", "storage/drain", "did", did, "error", err)
|
||||
// Continue — manifest rewrite is still valuable even if profile update fails
|
||||
} else {
|
||||
slog.Info("Drain: updated profile defaultHold", "component", "storage/drain", "did", did, "newHold", newHold)
|
||||
drainHold(ctx, client, db, did, oldHold, newHold)
|
||||
|
||||
// 4. If profile.DefaultHold pointed to this old hold, update it
|
||||
if profile != nil && profile.DefaultHold == oldHold {
|
||||
profile.DefaultHold = newHold
|
||||
profile.UpdatedAt = time.Now()
|
||||
if err := UpdateProfile(ctx, client, profile); err != nil {
|
||||
slog.Warn("Drain: failed to update profile", "component", "storage/drain", "did", did, "error", err)
|
||||
} else {
|
||||
slog.Info("Drain: updated profile defaultHold", "component", "storage/drain", "did", did, "newHold", newHold)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Walk manifest records, rewrite holdDid
|
||||
// drainHold rewrites all PDS manifest records and local DB rows from oldHold to newHold.
|
||||
func drainHold(
|
||||
ctx context.Context,
|
||||
client *atproto.Client,
|
||||
db HoldDIDLookup,
|
||||
did, oldHold, newHold string,
|
||||
) {
|
||||
// Walk PDS manifest records, rewrite holdDid
|
||||
cursor := ""
|
||||
rewritten := 0
|
||||
for {
|
||||
@@ -126,7 +155,7 @@ func MigrateManifestsForSuccessor(
|
||||
cursor = nextCursor
|
||||
}
|
||||
|
||||
// 5. Update appview's local manifests table
|
||||
// Update appview's local manifests table
|
||||
if db != nil {
|
||||
dbUpdated, err := db.UpdateManifestHoldDID(did, oldHold, newHold)
|
||||
if err != nil {
|
||||
|
||||
@@ -37,6 +37,13 @@ func (m *mockDatabase) UpdateManifestHoldDID(did, oldHoldDID, newHoldDID string)
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func (m *mockDatabase) GetDistinctManifestHoldDIDs(did string) ([]string, error) {
|
||||
if m.holdDID != "" {
|
||||
return []string{m.holdDID}, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func TestNewRoutingRepository(t *testing.T) {
|
||||
ctx := &RegistryContext{
|
||||
DID: "did:plc:test123",
|
||||
|
||||
@@ -18,6 +18,9 @@
|
||||
{{ icon "shield-check" "size-3" }} Attested
|
||||
</button>
|
||||
{{ end }}
|
||||
{{ if and .ViewerDefaultHold .Entry.HoldEndpoint (ne .Entry.HoldEndpoint .ViewerDefaultHold) }}
|
||||
<span class="badge badge-xs badge-soft badge-warning" title="{{ .Entry.HoldEndpoint }}">{{ icon "hard-drive" "size-3" }} {{ displayHoldDID .Entry.HoldEndpoint }}</span>
|
||||
{{ end }}
|
||||
</div>
|
||||
<div class="flex items-center gap-2 shrink-0">
|
||||
<span class="text-base-content text-sm flex items-center gap-1" title="{{ .Entry.CreatedAt.Format "2006-01-02T15:04:05Z07:00" }}">{{ icon "history" "size-4" }}{{ timeAgoShort .Entry.CreatedAt }}</span>
|
||||
@@ -160,7 +163,7 @@
|
||||
<div class="card bg-base-100 shadow-sm border border-base-300">
|
||||
<div class="divide-y divide-base-200" id="tags-list">
|
||||
{{ range .Entries }}
|
||||
{{ template "artifact-entry-markup" (dict "Entry" . "OwnerDID" $.Owner.DID "OwnerHandle" $.Owner.Handle "RepoName" $.Repository.Name "RegistryURL" $.RegistryURL "OciClient" $.OciClient "IsOwner" $.IsOwner) }}
|
||||
{{ template "artifact-entry-markup" (dict "Entry" . "OwnerDID" $.Owner.DID "OwnerHandle" $.Owner.Handle "RepoName" $.Repository.Name "RegistryURL" $.RegistryURL "OciClient" $.OciClient "IsOwner" $.IsOwner "ViewerDefaultHold" $.ViewerDefaultHold) }}
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ template "load-more-button" . }}
|
||||
@@ -175,7 +178,7 @@
|
||||
|
||||
{{ define "repo-tags-page" }}
|
||||
{{ range .Entries }}
|
||||
{{ template "artifact-entry-markup" (dict "Entry" . "OwnerDID" $.Owner.DID "OwnerHandle" $.Owner.Handle "RepoName" $.Repository.Name "RegistryURL" $.RegistryURL "OciClient" $.OciClient "IsOwner" $.IsOwner) }}
|
||||
{{ template "artifact-entry-markup" (dict "Entry" . "OwnerDID" $.Owner.DID "OwnerHandle" $.Owner.Handle "RepoName" $.Repository.Name "RegistryURL" $.RegistryURL "OciClient" $.OciClient "IsOwner" $.IsOwner "ViewerDefaultHold" $.ViewerDefaultHold) }}
|
||||
{{ end }}
|
||||
{{ template "load-more-button" . }}
|
||||
{{ template "scan-batch-triggers" . }}
|
||||
|
||||
@@ -34,7 +34,6 @@ var KnownRelays = []KnownRelay{
|
||||
{Name: "Xero", URL: "https://relay.xero.systems"},
|
||||
{Name: "Feeds Blue", URL: "https://relay.feeds.blue"},
|
||||
{Name: "Waow", URL: "https://relay.waow.tech"},
|
||||
{Name: "Zlay", URL: "https://zlay.waow.tech"},
|
||||
{Name: "Bassh", URL: "https://relay.bas.sh"},
|
||||
}
|
||||
|
||||
|
||||
+12
-1
@@ -103,6 +103,7 @@ func (a *RemoteHoldAuthorizer) cleanupRecentDenials() {
|
||||
// 3. Update cache
|
||||
func (a *RemoteHoldAuthorizer) GetCaptainRecord(ctx context.Context, holdDID string) (*atproto.CaptainRecord, error) {
|
||||
// Try cache first
|
||||
var staleCached *captainRecordWithMeta
|
||||
if a.db != nil {
|
||||
cached, err := a.getCachedCaptainRecord(holdDID)
|
||||
if err == nil && cached != nil {
|
||||
@@ -110,13 +111,23 @@ func (a *RemoteHoldAuthorizer) GetCaptainRecord(ctx context.Context, holdDID str
|
||||
if time.Since(cached.UpdatedAt) < a.cacheTTL {
|
||||
return cached.CaptainRecord, nil
|
||||
}
|
||||
// Cache expired - continue to fetch fresh data
|
||||
// Cache expired - keep as fallback in case XRPC fetch fails
|
||||
staleCached = cached
|
||||
}
|
||||
}
|
||||
|
||||
// Cache miss or expired - query XRPC endpoint
|
||||
record, err := a.fetchCaptainRecordFromXRPC(ctx, holdDID)
|
||||
if err != nil {
|
||||
// If the hold is unreachable but we have stale cache, use it.
|
||||
// Successor fields don't change once set, so stale data is safe.
|
||||
if staleCached != nil {
|
||||
slog.Warn("Captain record fetch failed, using stale cache",
|
||||
"holdDID", holdDID,
|
||||
"cache_age", time.Since(staleCached.UpdatedAt),
|
||||
"error", err)
|
||||
return staleCached.CaptainRecord, nil
|
||||
}
|
||||
slog.Error("Captain record fetch failed",
|
||||
"holdDID", holdDID,
|
||||
"denial_reason", "captain_record_fetch_failed",
|
||||
|
||||
Reference in New Issue
Block a user