preview

package
v0.8.3 Latest Latest
Warning

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

Go to latest
Published: Oct 24, 2026 License: Apache-2.0 Imports: 37 Imported by: 0

Documentation

Overview

Package preview captures a thumbnail of each deployed release with a short-lived browser container and keeps them within bounded storage.

Index

Constants

View Source
const (
	EnvEnabled       = "APP_PREVIEW_ENABLED"
	EnvImage         = "APP_PREVIEW_IMAGE"
	EnvTimeout       = "APP_PREVIEW_TIMEOUT"
	EnvPullTimeout   = "APP_PREVIEW_PULL_TIMEOUT"
	EnvMemoryMB      = "APP_PREVIEW_MEMORY_MB"
	EnvCPUs          = "APP_PREVIEW_CPUS"
	EnvViewport      = "APP_PREVIEW_VIEWPORT"
	EnvThumbWidth    = "APP_PREVIEW_THUMB_WIDTH"
	EnvQuality       = "APP_PREVIEW_QUALITY"
	EnvMaxThumbKB    = "APP_PREVIEW_MAX_THUMB_KB"
	EnvBlankRatio    = "APP_PREVIEW_BLANK_RATIO"
	EnvKeepPerApp    = "APP_PREVIEW_KEEP_PER_APP"
	EnvTTLDays       = "APP_PREVIEW_TTL_DAYS"
	EnvMaxTotalMB    = "APP_PREVIEW_MAX_TOTAL_MB"
	EnvImageTTLDays  = "APP_PREVIEW_IMAGE_TTL_DAYS"
	EnvMinFreeRAMMB  = "APP_PREVIEW_MIN_FREE_RAM_MB"
	EnvMinFreeDiskMB = "APP_PREVIEW_MIN_FREE_DISK_MB"
	EnvQueueDepth    = "APP_PREVIEW_QUEUE_DEPTH"
	EnvSweepInterval = "APP_PREVIEW_SWEEP_INTERVAL"
	EnvDefaultMode   = "APP_PREVIEW_DEFAULT_MODE"
	EnvThumbHeight   = "APP_PREVIEW_THUMB_HEIGHT"
	EnvMetaTimeout   = "APP_PREVIEW_META_TIMEOUT"
	EnvMetaMaxHTMLKB = "APP_PREVIEW_META_MAX_HTML_KB"
	EnvMetaMaxImgKB  = "APP_PREVIEW_META_MAX_IMAGE_KB"
	EnvMetaRedirects = "APP_PREVIEW_META_MAX_REDIRECTS"
	EnvMetaMinImgPx  = "APP_PREVIEW_META_MIN_IMAGE_PX"
)

Environment variables ConfigFromEnv reads.

View Source
const (
	EvictKeepLimit = "keep_limit"
	EvictTTL       = "ttl"
	EvictBudget    = "budget"
)

Eviction reasons.

View Source
const (
	StatusOK      = "ok"
	StatusSkipped = "skipped"
	StatusFailed  = "failed"
)

Record statuses.

View Source
const (
	ReasonLowRAM        = "low_ram"
	ReasonLowDisk       = "low_disk"
	ReasonImageUnusable = "browser_image_unavailable"
	ReasonNoAppNetwork  = "no_app_network"
	ReasonRemoteNode    = "remote_node"
	ReasonUnreachable   = "unreachable"
	ReasonHTTPStatus    = "http_status"
	ReasonAuthWall      = "auth_wall"
	ReasonBlankImage    = "blank_image"
	ReasonBadImage      = "bad_image"
	ReasonTimeout       = "timeout"
	ReasonNotHTML       = "not_html"
	ReasonRedirect      = "redirect"
	ReasonCaptureFailed = "capture_failed"
)

Skip and failure reason codes stored on a Record.

View Source
const (
	SourceScreenshot = "screenshot"
	SourceOGImage    = "og_image"
	SourceCard       = "card"
)

Record sources: where a preview's pixels came from.

View Source
const (
	DefaultPath = "/"
	MaxPathLen  = 512
	MaxWaitMS   = 10000
)

Bounds on the per-app capture options.

View Source
const DefaultImage = "docker.io/chromedp/headless-shell:151.0.7922.109"

DefaultImage is the pinned capture browser; override with APP_PREVIEW_IMAGE.

Variables

View Source
var ErrBadRef = errors.New("preview: unusable reference")

ErrBadRef means a reference is not a fetchable http(s) URL.

View Source
var ErrBlankImage = errors.New("preview: capture is blank")

ErrBlankImage means the capture is a single flat color.

View Source
var ErrDisabled = errors.New("preview: disabled")

ErrDisabled means previews are switched off, globally or for the app.

View Source
var ErrInvalid = errors.New("preview: invalid setting")

ErrInvalid wraps every settings validation failure.

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

ErrNotFound means no preview exists for the requested deployment.

View Source
var ErrNotHTML = errors.New("preview: page is not HTML")

ErrNotHTML means the page answered with something other than HTML.

View Source
var ErrTinyImage = errors.New("preview: image is too small")

ErrTinyImage means a fetched image is too small to be a useful thumbnail.

Functions

func IsBlank

func IsBlank(img image.Image, ratio float64) bool

IsBlank reports whether one color (quantized to 4 bits per channel) covers at least ratio of a sampled grid of img.

func ResolveRef

func ResolveRef(base *url.URL, raw string) (*url.URL, error)

ResolveRef resolves raw against base and accepts only absolute http(s) URLs without credentials. A literal internal IP is refused here; DNS names that resolve internally are refused by the guarded dialer.

func ValidatePath

func ValidatePath(p string) error

ValidatePath rejects anything that is not an absolute path on the app itself.

Types

type AppSettings

type AppSettings struct {
	App string
	// Mode is the preview tier. Enabled is derived from it (Mode != off) and
	// kept for callers written before tiers existed.
	Mode    Mode
	Enabled bool
	Path    string
	WaitMS  int
}

AppSettings is one app's opt-in and capture options.

func (*AppSettings) Normalize

func (s *AppSettings) Normalize(def Mode, configured bool)

Normalize fills Mode and Enabled from each other. A zero Mode with Enabled unset takes def when the app was never configured, and off otherwise.

func (AppSettings) Validate

func (s AppSettings) Validate() error

Validate checks settings before they are stored.

type Browser

type Browser interface {
	// Endpoint is the host address of the browser's debugging port.
	Endpoint() string
	Close()
}

Browser is a running capture browser.

type CDPShooter

type CDPShooter struct{}

CDPShooter speaks the Chrome DevTools Protocol over a websocket.

func (CDPShooter) Shoot

func (CDPShooter) Shoot(ctx context.Context, addr string, req ShotRequest) (*ShotResult, error)

Shoot loads req.URL, waits for the page to settle and returns a PNG of the viewport plus the main document's final HTTP status.

type CardMeta

type CardMeta struct {
	Title       string `json:"title,omitempty"`
	Description string `json:"description,omitempty"`
	ThemeColor  string `json:"theme_color,omitempty"`
}

CardMeta is the page metadata a card preview is composed from in the UI.

type Config

type Config struct {
	Enabled       bool
	Image         string
	Timeout       time.Duration
	PullTimeout   time.Duration
	MemoryMB      int64
	CPUs          float64
	ViewportW     int
	ViewportH     int
	ThumbWidth    int
	Quality       int
	MaxThumbKB    int
	BlankRatio    float64
	KeepPerApp    int
	TTL           time.Duration
	MaxTotalBytes int64
	ImageTTL      time.Duration
	MinFreeRAM    int64
	MinFreeDisk   int64
	QueueDepth    int
	SweepInterval time.Duration

	DefaultMode  Mode
	ThumbHeight  int
	MetaTimeout  time.Duration
	MetaMaxHTML  int64
	MetaMaxImage int64
	MetaRedirect int
	MetaMinImgPx int
}

Config holds every preview threshold. ConfigFromEnv fills defaults, so a Manager never sees a zero value it has to interpret.

func ConfigFromEnv

func ConfigFromEnv(lookup func(string) (string, bool), logger *slog.Logger) Config

ConfigFromEnv reads Config from lookup (os.LookupEnv in production). Unset or malformed values fall back to the defaults.

type Deps

type Deps struct {
	Store    Store
	Resolver Resolver
	Runner   Runner
	// DataDir is the platform data directory; thumbnails live in DataDir/previews.
	DataDir string
	// Namespace prefixes the labels on capture containers (the brand short name).
	Namespace string
	Logger    *slog.Logger
}

Deps are the collaborators a Manager needs.

type Eviction

type Eviction struct {
	Record Record
	Reason string
}

Eviction is one record PlanEviction wants deleted and why.

func PlanEviction

func PlanEviction(records []Record, protected map[string]bool, now time.Time, p RetentionPolicy) []Eviction

PlanEviction picks the records to delete. Protected deployments (the current production release of each app) are never evicted and do not count against KeepPerApp. Order of rules: per-app keep limit, TTL, then the global byte budget by least recently used.

type FileEntry

type FileEntry struct {
	App          string
	DeploymentID string
	Size         int64
}

FileEntry is one thumbnail file found on disk.

type FileStore

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

FileStore keeps thumbnails at <root>/<app>/<deployment>.jpg.

func NewFileStore

func NewFileStore(root string) *FileStore

NewFileStore returns a store rooted at root, which is created on first write.

func (*FileStore) Exists

func (f *FileStore) Exists(app, deploymentID string) bool

Exists reports whether the thumbnail file is present.

func (*FileStore) List

func (f *FileStore) List() ([]FileEntry, error)

List returns every thumbnail file on disk. Stray temp files are removed.

func (*FileStore) Path

func (f *FileStore) Path(app, deploymentID string) (string, error)

Path returns the file path for a deployment, rejecting names that could escape the root.

func (*FileStore) Remove

func (f *FileStore) Remove(app, deploymentID string) error

Remove deletes one thumbnail; a missing file is not an error.

func (*FileStore) RemoveApp

func (f *FileStore) RemoveApp(app string) error

RemoveApp deletes every thumbnail of app.

func (*FileStore) RemoveEntry

func (f *FileStore) RemoveEntry(e FileEntry) error

RemoveEntry deletes a file found by List, plus its app directory if empty.

func (*FileStore) Write

func (f *FileStore) Write(app, deploymentID string, data []byte) error

Write stores data atomically.

type ImageState

type ImageState struct {
	Ref      string
	ID       string
	LastUsed time.Time
}

ImageState is what the platform knows about the browser image it pulled.

type Manager

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

Manager captures, stores and prunes deploy preview thumbnails. It holds no goroutine while idle: a worker exists only while jobs are queued, plus one coarse sweep ticker after Start.

func New

func New(cfg Config, deps Deps, opts ...Option) *Manager

New builds a Manager. Nothing runs until a job is queued or Start is called.

func (*Manager) BrowserImage

func (m *Manager) BrowserImage(ctx context.Context) ImageState

BrowserImage returns the tracked browser image, zero when none was pulled by this platform.

func (*Manager) Capture

func (m *Manager) Capture(ctx context.Context, app string) error

Capture queues a manual recapture of app's current release.

func (*Manager) Capturing

func (m *Manager) Capturing(app string) bool

Capturing reports whether a job for app is queued or running.

func (*Manager) Config

func (m *Manager) Config() Config

Config returns the effective configuration.

func (*Manager) DeleteApp

func (m *Manager) DeleteApp(ctx context.Context, app string) error

DeleteApp removes every preview of a deleted app.

func (*Manager) List

func (m *Manager) List(ctx context.Context, app string) ([]Record, error)

List returns app's records, newest first.

func (*Manager) NotifyReady

func (m *Manager) NotifyReady(app, image, runningImageID string)

NotifyReady is called each time an app's release is observed serving. It is cheap after the first call per release (image tag plus the running image ID, so a rebuilt mutable tag counts as a new release) and never blocks.

func (*Manager) OpenImage

func (m *Manager) OpenImage(ctx context.Context, app, deploymentID string) (string, Record, error)

OpenImage returns the file path of an app's stored thumbnail. It marks the preview as viewed, at most once per viewedTouchAfter.

func (*Manager) Prune

func (m *Manager) Prune(ctx context.Context, app string, all bool) (PruneResult, error)

Prune applies retention to app now. With all set it removes every preview of the app, including the current release's.

func (*Manager) SaveSettings

func (m *Manager) SaveSettings(ctx context.Context, app string, patch SettingsPatch) (AppSettings, error)

SaveSettings applies patch to app's settings and stores the result.

func (*Manager) Settings

func (m *Manager) Settings(ctx context.Context, app string) (AppSettings, error)

Settings returns app's settings, defaulting to off with path "/".

func (*Manager) Start

func (m *Manager) Start(ctx context.Context)

Start runs a startup sweep and then one on every SweepInterval until ctx ends. It is the only long-lived goroutine.

func (*Manager) Status

func (m *Manager) Status(ctx context.Context, app string) (Summary, error)

Status assembles the app-level summary.

func (*Manager) Storage

func (m *Manager) Storage(ctx context.Context, app string) (appUsage, total Usage, err error)

Storage returns app's usage, and the global usage across all apps.

func (*Manager) Sweep

func (m *Manager) Sweep(ctx context.Context)

Sweep runs every cleanup pass: retention, deleted apps, orphan files, orphan capture containers and the idle browser image.

type MetaFetcher

type MetaFetcher interface {
	FetchPage(ctx context.Context, t MetaTarget, path string) (*PageResult, error)
	FetchImage(ctx context.Context, t MetaTarget, base *url.URL, ref string) ([]byte, error)
}

MetaFetcher fetches a page and its image for the metadata tier.

type MetaTarget

type MetaTarget struct {
	DeploymentID string
	Image        string
	Dial         string
	Host         string
	Port         int
	Domains      []string
}

MetaTarget is where the metadata tier fetches an app's page: the app's published loopback port, with Host/Port/Domains naming its origins.

type Mode

type Mode string

Mode is how much a per-app preview may cost.

const (
	ModeOff        Mode = "off"
	ModeMetadata   Mode = "metadata"
	ModeScreenshot Mode = "screenshot"
)

Preview modes, cheapest first.

func ParseMode

func ParseMode(s string) (Mode, error)

ParseMode validates a mode string.

type Option

type Option func(*Manager)

Option adjusts a Manager, mainly for tests.

func WithClock

func WithClock(now func() time.Time) Option

WithClock overrides the time source.

func WithHostProbes

func WithHostProbes(mem func() (int64, int64, error), disk func(string) (int64, error)) Option

WithHostProbes overrides the free RAM and free disk readings.

func WithMetaFetcher

func WithMetaFetcher(f MetaFetcher) Option

WithMetaFetcher overrides the metadata tier's HTTP client, mainly for tests.

func WithShooter

func WithShooter(s Shooter) Option

WithShooter overrides the browser driver.

type PageMeta

type PageMeta struct {
	Title        string
	OGTitle      string
	Description  string
	ThemeColor   string
	OGImage      string
	TwitterImage string
	Icon         string
}

PageMeta is what the metadata tier reads from a page's head. URL fields are raw and unresolved; use ResolveRef before fetching any of them.

func ParseMeta

func ParseMeta(r io.Reader) PageMeta

ParseMeta tokenizes HTML from r and collects the head metadata it knows. It stops at the first body element or when r is exhausted.

func (PageMeta) DisplayTitle

func (m PageMeta) DisplayTitle() string

DisplayTitle prefers og:title over the document title.

func (PageMeta) ImageRef

func (m PageMeta) ImageRef() string

ImageRef is the raw image reference to try: og:image first, then twitter:image.

type PageResult

type PageResult struct {
	HTTPStatus int
	// Base is the final page URL, on the app's own origin.
	Base *url.URL
	Meta PageMeta
}

PageResult is a fetched page's outcome.

type PruneResult

type PruneResult struct {
	Removed    int
	FreedBytes int64
}

PruneResult reports what a prune removed.

type Record

type Record struct {
	DeploymentID string
	App          string
	Path         string
	Bytes        int64
	Width        int
	Height       int
	Status       string
	Reason       string
	Detail       string
	HTTPStatus   int
	Source       string
	Meta         string
	CapturedAt   time.Time
	LastViewedAt time.Time
}

Record is one deployment's preview outcome. Only rows with HasFile own a file.

func (Record) HasFile

func (r Record) HasFile() bool

HasFile reports whether the record has a thumbnail on disk. Card records are composed in the UI from Meta and store no file.

func (Record) LastUsed

func (r Record) LastUsed() time.Time

LastUsed is the recency LRU eviction orders by.

type Resolver

type Resolver interface {
	// Resolve returns the capture target for app's release currently
	// serving image ("" means whatever is current), or a *SkipError.
	Resolve(ctx context.Context, app, image string) (Target, error)
	// ResolveMeta is Resolve for the metadata tier: it needs no Docker
	// network, only a reachable published port.
	ResolveMeta(ctx context.Context, app, image string) (MetaTarget, error)
	// CurrentDeployment returns the deployment serving production, or "".
	CurrentDeployment(ctx context.Context, app string) (string, error)
	AppExists(ctx context.Context, app string) (bool, error)
}

Resolver maps an app to its live release and Docker network.

type RetentionPolicy

type RetentionPolicy struct {
	KeepPerApp    int
	TTL           time.Duration
	MaxTotalBytes int64
}

RetentionPolicy bounds how many previews are kept.

type Runner

type Runner interface {
	EnsureImageID(ctx context.Context, ref string) (id string, pulled bool, err error)
	StartBrowser(ctx context.Context, spec docker.BrowserSpec) (Browser, error)
	ListContainersByLabel(ctx context.Context, label string) ([]docker.LabeledContainer, error)
	Remove(ctx context.Context, id string, force bool) error
	ImageInUse(ctx context.Context, imageID string) (bool, error)
	RemoveImageByID(ctx context.Context, imageID string) error
}

Runner is the Docker surface previews use.

func NewDockerRunner

func NewDockerRunner(c *docker.Client) Runner

NewDockerRunner returns a Runner backed by the real Docker client.

type SQLStore

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

SQLStore implements Store over the control plane database.

func NewSQLStore

func NewSQLStore(db *store.DB) *SQLStore

NewSQLStore wraps db.

func (*SQLStore) DeletePreviewRecords

func (s *SQLStore) DeletePreviewRecords(ctx context.Context, ids []string) error

DeletePreviewRecords removes records by deployment ID.

func (*SQLStore) DeletePreviewSettings

func (s *SQLStore) DeletePreviewSettings(ctx context.Context, app string) error

DeletePreviewSettings removes an app's settings, returning it to the default off state.

func (*SQLStore) GetPreviewRecord

func (s *SQLStore) GetPreviewRecord(ctx context.Context, deploymentID string) (*Record, error)

GetPreviewRecord returns one record, or nil when absent.

func (*SQLStore) GetPreviewSettings

func (s *SQLStore) GetPreviewSettings(ctx context.Context, app string) (AppSettings, error)

GetPreviewSettings returns the stored settings, or the zero value when none exist.

func (*SQLStore) GetPreviewState

func (s *SQLStore) GetPreviewState(ctx context.Context, key string) (string, error)

GetPreviewState reads one state value, "" when unset.

func (*SQLStore) LatestServingDeployment

func (s *SQLStore) LatestServingDeployment(ctx context.Context, service, image string) (string, error)

LatestServingDeployment is LatestSucceededDeployment restricted to attempts the controller has observed serving, so a preview is never filed under a release that has not replaced the old one yet.

func (*SQLStore) LatestSucceededDeployment

func (s *SQLStore) LatestSucceededDeployment(ctx context.Context, service, image string) (string, error)

LatestSucceededDeployment returns the newest succeeded deploy attempt of service, restricted to image when non-empty. "" means none.

func (*SQLStore) ListAllPreviewRecords

func (s *SQLStore) ListAllPreviewRecords(ctx context.Context) ([]Record, error)

ListAllPreviewRecords lists every record, newest first.

func (*SQLStore) ListPreviewRecords

func (s *SQLStore) ListPreviewRecords(ctx context.Context, app string) ([]Record, error)

ListPreviewRecords lists one app's records, newest first.

func (*SQLStore) SavePreviewSettings

func (s *SQLStore) SavePreviewSettings(ctx context.Context, a AppSettings) error

SavePreviewSettings upserts settings. The legacy enabled column stays true only for screenshot mode, so an older binary reading it behaves the same.

func (*SQLStore) SetPreviewState

func (s *SQLStore) SetPreviewState(ctx context.Context, key, value string) error

SetPreviewState writes one state value.

func (*SQLStore) TouchPreviewViewed

func (s *SQLStore) TouchPreviewViewed(ctx context.Context, deploymentID string, at time.Time) error

TouchPreviewViewed stamps the last time a preview image was served.

func (*SQLStore) UpsertPreviewRecord

func (s *SQLStore) UpsertPreviewRecord(ctx context.Context, r Record) error

UpsertPreviewRecord inserts or replaces a record, keeping its view time.

type SettingsPatch

type SettingsPatch struct {
	Mode    *string
	Enabled *bool
	Path    *string
	WaitMS  *int
}

SettingsPatch updates only the fields that are non-nil.

type Shooter

type Shooter interface {
	Shoot(ctx context.Context, addr string, req ShotRequest) (*ShotResult, error)
}

Shooter drives a running browser, reachable at addr, to screenshot a page.

type ShotRequest

type ShotRequest struct {
	URL      string
	Width    int
	Height   int
	Settle   time.Duration
	Deadline time.Time
}

ShotRequest is one page to screenshot.

type ShotResult

type ShotResult struct {
	HTTPStatus int
	FinalURL   string
	PNG        []byte
}

ShotResult is what the browser saw and captured.

func (*ShotResult) Classify

func (r *ShotResult) Classify(requestedPath string) (reason, detail string)

Classify says whether a browser result allows a thumbnail. A non-empty reason means skip. requestedPath is the path that was asked for: landing on a different login-looking path means an auth wall.

type SkipError

type SkipError struct {
	DeploymentID string
	Reason       string
	Detail       string
}

SkipError tells the Manager a capture must not run, recording Reason against DeploymentID (empty means nothing is recorded).

func (*SkipError) Error

func (e *SkipError) Error() string

type Store

type Store interface {
	GetPreviewSettings(ctx context.Context, app string) (AppSettings, error)
	SavePreviewSettings(ctx context.Context, s AppSettings) error
	DeletePreviewSettings(ctx context.Context, app string) error
	UpsertPreviewRecord(ctx context.Context, r Record) error
	GetPreviewRecord(ctx context.Context, deploymentID string) (*Record, error)
	ListPreviewRecords(ctx context.Context, app string) ([]Record, error)
	ListAllPreviewRecords(ctx context.Context) ([]Record, error)
	DeletePreviewRecords(ctx context.Context, deploymentIDs []string) error
	TouchPreviewViewed(ctx context.Context, deploymentID string, at time.Time) error
	GetPreviewState(ctx context.Context, key string) (string, error)
	SetPreviewState(ctx context.Context, key, value string) error
}

Store persists settings, records and the small image-tracking state.

type Summary

type Summary struct {
	Settings     AppSettings
	GlobalOn     bool
	Capturing    bool
	App, Total   Usage
	Image        string
	BrowserImage ImageState
	Latest       *Record
}

Summary is the app-level status shown in the UI and CLI.

type Target

type Target struct {
	DeploymentID string
	Image        string
	Network      string
	Host         string
	Port         int
}

Target is where to point the browser for an app's current release.

type Thumb

type Thumb struct {
	JPEG          []byte
	Width, Height int
}

Thumb is an encoded thumbnail.

func BuildThumb

func BuildThumb(pngData []byte, thumbWidth, quality, maxBytes int, blankRatio float64) (*Thumb, error)

BuildThumb validates a PNG capture and returns a JPEG thumbnail of width thumbWidth. It re-encodes once at lower quality when the result exceeds maxBytes, and rejects it if it is still too large.

func BuildThumbFit

func BuildThumbFit(data []byte, w, h, minPx, quality, maxBytes int, blankRatio float64) (*Thumb, error)

BuildThumbFit decodes a JPEG or PNG, scales it down to fit within w by h (never up) and centers it on a neutral w by h canvas. It rejects images under minPx on either side and single-color images.

type Usage

type Usage struct {
	Bytes int64
	Count int
}

Usage is the storage a set of previews occupies.

Jump to

Keyboard shortcuts

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