imgregistry

package
v1.0.37 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 8, 2026 License: MIT Imports: 11 Imported by: 0

Documentation

Overview

Package imgregistry is a minimal Docker Registry v2 HTTP client used to browse the fleet's image registry and resolve tags/digests for the upgrade-picker. Deliberately not named "registry" — the codebase already has jobs.Registry (job-kind registry) and a same-named package here would collide in every import block needing both.

Index

Constants

View Source
const TagsCacheTTL = 5 * time.Minute

TagsCacheTTL bounds how long a CachingClient serves a repo's Tags() result before re-resolving it. Registry contents only change on push (a human or CI action), never spontaneously, so a few minutes of staleness is harmless — an operator who just pushed a new tag and immediately checks the UI sees it within this window, not never. 5 minutes is picked to make the common case (browsing the list, clicking into a couple of repos, going back) free after the first view, while still bounding how long a since-deleted tag would linger in the UI.

Variables

View Source
var ErrNotFound = errors.New("not found")

ErrNotFound wraps a registry's genuine 404 response (NAME_UNKNOWN / MANIFEST_UNKNOWN) — the registry answered and said this repo/tag doesn't exist.

View Source
var ErrUnreachable = errors.New("registry unreachable")

ErrUnreachable wraps a transport failure, a non-404 non-2xx response, or a response body this client cannot decode — anything that means "we could not get a straight answer from the registry," as distinct from ErrNotFound ("the registry answered and said no"). Callers must not collapse the two: an operator told "not found" for a registry that is simply down gets the exact misleading signal this package exists to prevent.

Functions

func IsDigestRef added in v1.0.35

func IsDigestRef(ref string) bool

IsDigestRef reports whether ref is digest-form (matches digestRe) rather than a tag. Delete requires this on top of ValidRef: a tag-form ref must never reach the registry's delete endpoint, since deleting by tag removes the manifest for every tag that currently shares that digest.

func ValidRef

func ValidRef(ref string) bool

ValidRef reports whether ref is an acceptable manifest reference: either a tag (tagRe) or a content digest (digestRe). Callers must validate ref before passing it to Client.Manifest, which interpolates it unescaped into a registry request path — an unvalidated ref (e.g. containing "../" or "/") would let a caller steer that request to an arbitrary path on the registry host.

func ValidRepoName

func ValidRepoName(repo string) bool

ValidRepoName reports whether repo is an acceptable Docker Registry v2 repository name: one or more "/"-separated components, each matching repoComponentRe. Registry v2 repo names legally contain "/" (e.g. "iotready/engine"), unlike the DNS-label-style names (host/template/slug) validated elsewhere by render.ValidName — so this validator is intentionally separate, not a reuse of that one. Shared by internal/api and internal/ui so the two edges cannot drift into accepting different repo shapes.

Types

type Auth

type Auth struct {
	Mode     string // "none" (default) or "basic"
	Username string
	Password string
}

Auth configures how HTTPClient authenticates to the registry.

type CachingClient added in v1.0.33

type CachingClient struct {
	// contains filtered or unexported fields
}

CachingClient wraps a Client and caches its ResolveTags() listings for ttl, since resolving a repo's tags is the one call expensive enough to matter — for the fleet's "engine" repo, ~1000 manifest GETs plus 737 config-blob GETs, ~21s on a cold cache. Tags() is served from that same entry, so the two can never disagree about a repo. The list page (internal/ui/handlers_registry.go) fires this once per catalog repo, concurrently, on every single page view — without a cache that's the full per-repo cost paid ~26 times on every visit. With it, only the first visit (or the first visit after ttl) pays that cost; every revisit inside the window is instant.

It is a decorator, not a change to HTTPClient itself: HTTPClient stays a pure client with no caching semantics of its own, the cache is independently testable against a fake inner Client, and a caller that wants uncached behavior (e.g. a future admin "force refresh" action, or a test) can simply hold the inner Client instead.

Catalog, TagCount, and Manifest are passed straight through uncached: Catalog and TagCount are already cheap (a single, unresolved tags/catalog list call), and Manifest is one lookup for one digest — caching it would buy little while adding another thing that could go stale.

Only a successful ResolveTags() listing is cached (see ResolveTags below) — a transient registry blip must not poison every viewer's list page for the rest of the TTL.

func NewCachingClient added in v1.0.33

func NewCachingClient(inner Client, ttl time.Duration) *CachingClient

NewCachingClient wraps inner in a Tags()-caching decorator with the given ttl (TagsCacheTTL in production; tests pass a short one to observe expiry).

func (*CachingClient) Catalog added in v1.0.33

func (c *CachingClient) Catalog(ctx context.Context) ([]string, error)

func (*CachingClient) Delete added in v1.0.35

func (c *CachingClient) Delete(ctx context.Context, repo, digest string) error

Delete delegates to the inner Client and, only on success, invalidates repo's cached Tags() entry — a stale listing after a delete would let a subsequent run reason from a tag that no longer exists on the registry. A failed delete leaves the cache untouched, since nothing was actually removed.

func (*CachingClient) Manifest added in v1.0.33

func (c *CachingClient) Manifest(ctx context.Context, repo, ref string) (Manifest, error)

func (*CachingClient) ResolveTags added in v1.0.35

func (c *CachingClient) ResolveTags(ctx context.Context, repo string) (TagListing, error)

ResolveTags returns repo's cached listing if present and not yet expired, otherwise resolves it via the inner Client and, only on success, stores the result. Safe for concurrent use — the list page fans this out across ~26 goroutines on a single page load.

func (*CachingClient) TagCount added in v1.0.33

func (c *CachingClient) TagCount(ctx context.Context, repo string) (int, error)

func (*CachingClient) Tags added in v1.0.33

func (c *CachingClient) Tags(ctx context.Context, repo string) ([]TagGroup, error)

Tags returns repo's cached TagGroups if present and not yet expired, otherwise resolves them via the inner Client and, only on success, stores the result. Safe for concurrent use — the list page fans this out across ~26 goroutines on a single page load.

type Client

type Client interface {
	Catalog(ctx context.Context) ([]string, error)
	Tags(ctx context.Context, repo string) ([]TagGroup, error)
	// ResolveTags is Tags plus the evidence of what it could not resolve.
	// Prefer it over Tags anywhere a partial listing would be acted on.
	ResolveTags(ctx context.Context, repo string) (TagListing, error)
	TagCount(ctx context.Context, repo string) (int, error)
	Manifest(ctx context.Context, repo, ref string) (Manifest, error)

	// Delete removes the manifest at repo/digest. digest must be
	// digest-form (sha256:...), never a tag: deleting by tag would remove
	// the manifest for every tag that currently shares that digest.
	Delete(ctx context.Context, repo, digest string) error
}

Client is the surface this package exposes: read-only browsing plus the one mutating call, Delete, that a prune job needs to reclaim registry storage.

type DroppedTag added in v1.0.35

type DroppedTag struct {
	Tag string

	// Vanished is true ONLY when, after resolution failed, a fresh read of
	// /v2/<repo>/tags/list no longer listed Tag. That is the registry's one
	// exact statement about a tag's existence, and the ONLY basis on which a
	// caller may treat a drop as benign.
	//
	// It is a field rather than a sentinel wrapped into Err on purpose. A 404
	// from GET/HEAD /manifests/<ref> is content-negotiated: distribution
	// answers 404 MANIFEST_UNKNOWN when its stored media type matches no entry
	// in the request's Accept set, byte-identical to "this tag does not
	// exist". So no manifest error, however classified, can carry this
	// meaning — and if the decision were read back out of Err via
	// errors.Is(..., ErrNotFound), any future edit that wrapped a manifest
	// error's cause with %w instead of %v would silently reclassify an
	// unreadable tag as a deleted one. That mistake ends in deleting a live
	// image, and it would leave every test green. The flag cannot be set by
	// accident: exactly one code path assigns it, from the tags/list read.
	Vanished bool

	Err error
}

DroppedTag is one tag that a listing could not resolve, and why.

type HTTPClient

type HTTPClient struct {
	BaseURL string // e.g. "http://100.64.0.23:5000", no trailing slash
	Auth    Auth
	HTTP    *http.Client
}

HTTPClient implements Client against a real Registry v2 server.

func NewHTTPClient

func NewHTTPClient(baseURL string, auth Auth) *HTTPClient

NewHTTPClient builds an HTTPClient with a sane default timeout.

func (*HTTPClient) Catalog

func (c *HTTPClient) Catalog(ctx context.Context) ([]string, error)

Catalog lists every repository in the registry, following the Link response header across pages rather than requesting one unbounded page.

func (*HTTPClient) Delete added in v1.0.35

func (c *HTTPClient) Delete(ctx context.Context, repo, digest string) error

Delete issues Registry v2 DELETE /v2/<repo>/manifests/<digest>. digest must be digest-form (validated by ValidRef + IsDigestRef); a tag-form or otherwise invalid ref is rejected before any request is constructed — deleting by tag would remove the manifest for every tag sharing that digest. Success is any 2xx (the spec documents 202); a genuine 404 is ErrNotFound, anything else non-2xx or a transport failure is ErrUnreachable — the two must never collapse into each other.

func (*HTTPClient) Manifest

func (c *HTTPClient) Manifest(ctx context.Context, repo, ref string) (Manifest, error)

Manifest fetches a single manifest by tag or digest and returns its canonical digest (from Docker-Content-Digest) plus size/layer info.

func (*HTTPClient) ResolveTags added in v1.0.35

func (c *HTTPClient) ResolveTags(ctx context.Context, repo string) (TagListing, error)

ResolveTags lists every tag in repo, resolves each to its manifest digest, and groups tags that share a digest into one TagGroup. Each unique digest's config blob is fetched once (not once per tag) for its Created timestamp. Manifest and blob-created lookups both run with bounded concurrency (tagResolveConcurrency), not sequentially. Groups are sorted newest-Created-first.

A tag whose manifest cannot be resolved is skipped rather than failing the whole call — and reported in TagListing.Dropped, so a caller that cannot act on a partial view can tell "this repo has one tag" from "this repo has two tags and we could only see one".

func (*HTTPClient) TagCount added in v1.0.33

func (c *HTTPClient) TagCount(ctx context.Context, repo string) (int, error)

TagCount returns how many tags repo has, without resolving any of them. Deliberately NOT implemented as len(Tags(...)): Tags resolves every tag's manifest and every unique digest's config blob (~21s for the fleet's "engine" repo), while the repo-list page calls this once per repo on a single page load. One tags-list call — paginated, but nothing more — is the whole point.

Note this counts tags, not unique digests, so it can exceed len(Tags(...)) when several tags share a digest. That is the intended meaning for a "how many tags does this repo have" column.

func (*HTTPClient) Tags

func (c *HTTPClient) Tags(ctx context.Context, repo string) ([]TagGroup, error)

Tags is ResolveTags' groups-only view, for callers that only render a listing. Anything that DELETES must use ResolveTags and check Dropped.

type Layer

type Layer struct {
	Digest string
	Size   int64
}

Layer is one entry in a manifest's layer list.

type Manifest

type Manifest struct {
	Digest       string // from Docker-Content-Digest, the canonical content digest
	ConfigDigest string
	Layers       []Layer
	Size         int64 // config size + sum of all layer sizes
}

Manifest is the resolved shape of a single tag/digest reference.

type TagGroup

type TagGroup struct {
	Digest  string
	Tags    []string
	Created time.Time
	Size    int64
}

TagGroup is every tag that resolves to the same content digest, with the digest's creation time (from its config blob) and total size.

type TagListing added in v1.0.35

type TagListing struct {
	Groups  []TagGroup
	Dropped []DroppedTag
}

TagListing is a repo's resolved tag groups PLUS the tags that could not be resolved and are therefore absent from those groups.

The two travel together on purpose. Resolution drops a tag whose manifest fetch failed and carries on (one exotic media type must not hide a whole repo behind a single error), which is right for browsing and catastrophic for anything that deletes: a dropped "latest" leaves its bare-hex alias alone in its group, reclassifying it from "this is latest's own digest" to "orphan". A caller that must not reason from a partial view checks Dropped; a caller that just wants to render a list ignores it, or uses Tags. Crucially, Dropped is derived from the SAME resolution pass as Groups, so no two calls, layers or caches can disagree about whether a given listing was complete.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL