mirror of
https://tangled.org/evan.jarrett.net/at-container-registry
synced 2026-08-30 04:37:06 +00:00
1. Multiple registry domains + per-user domain preference
The biggest feature. The appview can serve several registry domains (e.g. buoy.cr, atcr.io),
and users can now pick which one shows up in their pull/push commands.
- Lexicon/record: adds registryDomain (and documents ociClient)
to the sailor profile (lexicons/.../profile.json, pkg/atproto/lexicon.go).
- DB: new registry_domain column on users (schema.sql + migration 0027),
with GetUserByDID/Handle reads, UpdateUserRegistryDomain writer,
and Jetstream caching it on profile updates (writes unconditionally so clearing propagates).
- UI/handlers: new UpdateRegistryDomainHandler + /api/profile/registry-domain route,
a <select> in the user settings panel (only shown when >1 domain configured), and resolveRegistryURL()
which falls back to the primary domain if the user's pref is stale/removed. Tests added for all of it.
2. default_hold_did removed → first managed_holds entry is the default
Consolidates two overlapping config fields into one. ServerConfig.DefaultHoldDID is gone;
PrimaryHoldDID() now returns managed_holds[0]. managed_holds is now REQUIRED.
Updated in config, validation, server wiring, test harness, example YAML, and the deploy template.
3. Admin long-running operations → generic background-job framework
New pkg/hold/admin/jobs.go introduces a reusable startJob/jobRegistry pattern
(a detached context.Background() job + a /admin/api/jobs/{key}/status polling endpoint).
This replaces the bespoke scan-backfill goroutine state machine, and now also wraps crew tier remap and crew import
all three previously looped synchronously on the request context and got 504'd/cancelled mid-run by the reverse proxy.
Forms switched from POST-redirect to htmx fragments (job_progress.html, job_result.html, crew_import_results.html)
the old crew_import_results.html page and scan_backfill_progress.html partial were deleted.
This is also captured as a new rule in CLAUDE.md.
4. Cascade-delete manifest on last-tag deletion
DeleteTagHandler now, after removing the last tag pointing to a digest, cascade-deletes the manifest itself
(PDS + DB + hold blob purge) — but only if it's not a child of a manifest list (multi-arch parent).
New GetTagDigest and ShouldCascadeDeleteManifest queries back it, plus cascade_delete_test.go.
Also switches tag rkey computation to the atproto.RepositoryTagToRKey helper.
5. Billing simplification
Drops the OwnerBadge config option (hold-owner supporter badge).
The user-profile template no longer special-cases an "owner" badge value (only "Captain").
Example tiers renamed to the nautical scheme (deckhand/bosun/quartermaster).
6. Build/deploy: go generate always runs via Make
make generate is now a phony target that always runs go generate ./... (regenerating cbor_gen, icon sprites, etc.),
and build-trixie depends on it. The deploy tooling (provision.go/update.go)
drops its own runGenerate calls since the Makefile handles it.
7. New cmd/firehose-tap tool (untracked)
A standalone CLI that subscribes to a com.atproto.sync.subscribeRepos endpoint and pretty-prints events,
with emphasis on Sync 1.1 compliance fields (per-op prev CIDs, commit prevData) and a --validate CI mode.
Fits with the recent "more sync1.1 compliant" commit.
203 lines
6.2 KiB
Go
203 lines
6.2 KiB
Go
package db
|
|
|
|
import "time"
|
|
|
|
// User represents a user in the system
|
|
type User struct {
|
|
DID string
|
|
Handle string
|
|
PDSEndpoint string
|
|
Avatar string
|
|
DefaultHoldDID string
|
|
OciClient string
|
|
RegistryDomain string
|
|
LastSeen time.Time
|
|
}
|
|
|
|
// Manifest represents an OCI manifest stored in the cache
|
|
type Manifest struct {
|
|
ID int64
|
|
DID string
|
|
Repository string
|
|
Digest string
|
|
HoldEndpoint string
|
|
SchemaVersion int
|
|
MediaType string
|
|
ConfigDigest string
|
|
ConfigSize int64
|
|
ArtifactType string // container-image, helm-chart, unknown
|
|
SubjectDigest string // digest of the parent manifest (for attestations/referrers)
|
|
CreatedAt time.Time
|
|
// Annotations removed - now stored in repository_annotations table
|
|
}
|
|
|
|
// Layer represents a layer in a manifest
|
|
type Layer struct {
|
|
ManifestID int64
|
|
Digest string
|
|
Size int64
|
|
MediaType string
|
|
LayerIndex int
|
|
Annotations map[string]string // JSON-encoded layer annotations (e.g. in-toto predicate type)
|
|
}
|
|
|
|
// ManifestReference represents a reference to a manifest in a manifest list/index
|
|
type ManifestReference struct {
|
|
ManifestID int64
|
|
Digest string
|
|
Size int64
|
|
MediaType string
|
|
PlatformArchitecture string
|
|
PlatformOS string
|
|
PlatformVariant string
|
|
PlatformOSVersion string
|
|
IsAttestation bool // true if vnd.docker.reference.type = "attestation-manifest"
|
|
ReferenceIndex int
|
|
}
|
|
|
|
// Tag represents a tag pointing to a manifest
|
|
type Tag struct {
|
|
ID int64
|
|
DID string
|
|
Repository string
|
|
Tag string
|
|
Digest string
|
|
CreatedAt time.Time
|
|
}
|
|
|
|
// Repository represents an aggregated view of a user's repository
|
|
type Repository struct {
|
|
Name string
|
|
TagCount int
|
|
ManifestCount int
|
|
LastPush time.Time
|
|
Tags []Tag
|
|
Manifests []Manifest
|
|
Title string
|
|
Description string
|
|
SourceURL string
|
|
DocumentationURL string
|
|
Licenses string
|
|
IconURL string
|
|
ReadmeURL string
|
|
Version string
|
|
}
|
|
|
|
// RepositoryStats represents statistics for a repository
|
|
type RepositoryStats struct {
|
|
DID string `json:"did"`
|
|
Repository string `json:"repository"`
|
|
StarCount int `json:"star_count"` // Calculated from stars table, not stored
|
|
PullCount int `json:"pull_count"`
|
|
LastPull *time.Time `json:"last_pull,omitempty"`
|
|
PushCount int `json:"push_count"`
|
|
LastPush *time.Time `json:"last_push,omitempty"`
|
|
}
|
|
|
|
// DailyStats represents daily pull/push statistics for a repository
|
|
type DailyStats struct {
|
|
DID string `json:"did"`
|
|
Repository string `json:"repository"`
|
|
Date string `json:"date"`
|
|
PullCount int `json:"pull_count"`
|
|
PushCount int `json:"push_count"`
|
|
}
|
|
|
|
// RepositoryWithStats combines repository data with statistics
|
|
type RepositoryWithStats struct {
|
|
Repository
|
|
Stats RepositoryStats
|
|
}
|
|
|
|
// RepoCardData contains all data needed to render a repository card
|
|
type RepoCardData struct {
|
|
OwnerHandle string
|
|
OwnerAvatarURL string // Owner's profile avatar URL (fallback when no repo icon)
|
|
Repository string
|
|
Title string
|
|
Description string
|
|
IconURL string
|
|
StarCount int
|
|
PullCount int
|
|
IsStarred bool // Whether the current user has starred this repository
|
|
ArtifactType string // container-image, helm-chart, unknown
|
|
Tag string // Latest tag name (e.g., "latest", "v1.0.0")
|
|
Digest string // Latest manifest digest (sha256:...)
|
|
LastUpdated time.Time // When the repository was last pushed to
|
|
RegistryURL string // Registry URL for docker commands (e.g., "atcr.io" or "127.0.0.1:5000")
|
|
OciClient string // Preferred OCI client for pull commands (e.g., "docker", "podman")
|
|
}
|
|
|
|
// SetRegistryURL sets the RegistryURL field on all cards in the slice
|
|
func SetRegistryURL(cards []RepoCardData, registryURL string) {
|
|
for i := range cards {
|
|
cards[i].RegistryURL = registryURL
|
|
}
|
|
}
|
|
|
|
// SetOciClient sets the OciClient field on all cards in the slice
|
|
func SetOciClient(cards []RepoCardData, ociClient string) {
|
|
for i := range cards {
|
|
cards[i].OciClient = ociClient
|
|
}
|
|
}
|
|
|
|
// PlatformInfo represents platform information (OS/Architecture)
|
|
type PlatformInfo struct {
|
|
OS string
|
|
Architecture string
|
|
Variant string
|
|
OSVersion string
|
|
Digest string // child platform manifest digest (for manifest lists)
|
|
HoldEndpoint string // hold endpoint for this platform manifest
|
|
CompressedSize int64 // sum of layer sizes (compressed)
|
|
}
|
|
|
|
// TagWithPlatforms extends Tag with platform information
|
|
type TagWithPlatforms struct {
|
|
Tag
|
|
HoldEndpoint string // hold endpoint from the tag's own manifest
|
|
Platforms []PlatformInfo
|
|
IsMultiArch bool
|
|
HasAttestations bool // true if manifest list contains attestation references
|
|
ArtifactType string // container-image, helm-chart, unknown
|
|
CompressedSize int64 // sum of layer sizes for single-arch tags
|
|
}
|
|
|
|
// ManifestWithMetadata extends Manifest with tags and platform information
|
|
type ManifestWithMetadata struct {
|
|
Manifest
|
|
Tags []string
|
|
Platforms []PlatformInfo
|
|
PlatformCount int
|
|
IsManifestList bool
|
|
HasAttestations bool // true if manifest list contains attestation references
|
|
Reachable bool // Whether the hold endpoint is reachable
|
|
Pending bool // Whether health check is still in progress
|
|
// Note: ArtifactType is available via embedded Manifest struct
|
|
}
|
|
|
|
// ManifestEntry is a unified view model for the tags tab.
|
|
// Every entry is a manifest — labeled by tag name or digest.
|
|
type ManifestEntry struct {
|
|
Label string // tag name, or digest if untagged
|
|
Digest string // manifest digest
|
|
IsTagged bool
|
|
CreatedAt time.Time
|
|
HoldEndpoint string
|
|
Platforms []PlatformInfo
|
|
IsMultiArch bool
|
|
HasAttestations bool
|
|
ArtifactType string
|
|
CompressedSize int64 // for single-arch
|
|
}
|
|
|
|
// AttestationDetail represents an attestation manifest and its layers
|
|
type AttestationDetail struct {
|
|
Digest string
|
|
MediaType string // attestation manifest media type
|
|
Size int64
|
|
HoldEndpoint string // hold DID/URL where blobs are stored
|
|
Layers []Layer
|
|
}
|