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
- Variables
- func IsBlank(img image.Image, ratio float64) bool
- func ResolveRef(base *url.URL, raw string) (*url.URL, error)
- func ValidatePath(p string) error
- type AppSettings
- type Browser
- type CDPShooter
- type CardMeta
- type Config
- type Deps
- type Eviction
- type FileEntry
- type FileStore
- func (f *FileStore) Exists(app, deploymentID string) bool
- func (f *FileStore) List() ([]FileEntry, error)
- func (f *FileStore) Path(app, deploymentID string) (string, error)
- func (f *FileStore) Remove(app, deploymentID string) error
- func (f *FileStore) RemoveApp(app string) error
- func (f *FileStore) RemoveEntry(e FileEntry) error
- func (f *FileStore) Write(app, deploymentID string, data []byte) error
- type ImageState
- type Manager
- func (m *Manager) BrowserImage(ctx context.Context) ImageState
- func (m *Manager) Capture(ctx context.Context, app string) error
- func (m *Manager) Capturing(app string) bool
- func (m *Manager) Config() Config
- func (m *Manager) DeleteApp(ctx context.Context, app string) error
- func (m *Manager) List(ctx context.Context, app string) ([]Record, error)
- func (m *Manager) NotifyReady(app, image, runningImageID string)
- func (m *Manager) OpenImage(ctx context.Context, app, deploymentID string) (string, Record, error)
- func (m *Manager) Prune(ctx context.Context, app string, all bool) (PruneResult, error)
- func (m *Manager) SaveSettings(ctx context.Context, app string, patch SettingsPatch) (AppSettings, error)
- func (m *Manager) Settings(ctx context.Context, app string) (AppSettings, error)
- func (m *Manager) Start(ctx context.Context)
- func (m *Manager) Status(ctx context.Context, app string) (Summary, error)
- func (m *Manager) Storage(ctx context.Context, app string) (appUsage, total Usage, err error)
- func (m *Manager) Sweep(ctx context.Context)
- type MetaFetcher
- type MetaTarget
- type Mode
- type Option
- type PageMeta
- type PageResult
- type PruneResult
- type Record
- type Resolver
- type RetentionPolicy
- type Runner
- type SQLStore
- func (s *SQLStore) DeletePreviewRecords(ctx context.Context, ids []string) error
- func (s *SQLStore) DeletePreviewSettings(ctx context.Context, app string) error
- func (s *SQLStore) GetPreviewRecord(ctx context.Context, deploymentID string) (*Record, error)
- func (s *SQLStore) GetPreviewSettings(ctx context.Context, app string) (AppSettings, error)
- func (s *SQLStore) GetPreviewState(ctx context.Context, key string) (string, error)
- func (s *SQLStore) LatestServingDeployment(ctx context.Context, service, image string) (string, error)
- func (s *SQLStore) LatestSucceededDeployment(ctx context.Context, service, image string) (string, error)
- func (s *SQLStore) ListAllPreviewRecords(ctx context.Context) ([]Record, error)
- func (s *SQLStore) ListPreviewRecords(ctx context.Context, app string) ([]Record, error)
- func (s *SQLStore) SavePreviewSettings(ctx context.Context, a AppSettings) error
- func (s *SQLStore) SetPreviewState(ctx context.Context, key, value string) error
- func (s *SQLStore) TouchPreviewViewed(ctx context.Context, deploymentID string, at time.Time) error
- func (s *SQLStore) UpsertPreviewRecord(ctx context.Context, r Record) error
- type SettingsPatch
- type Shooter
- type ShotRequest
- type ShotResult
- type SkipError
- type Store
- type Summary
- type Target
- type Thumb
- type Usage
Constants ¶
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.
const ( EvictKeepLimit = "keep_limit" EvictTTL = "ttl" EvictBudget = "budget" )
Eviction reasons.
const ( StatusOK = "ok" StatusSkipped = "skipped" StatusFailed = "failed" )
Record statuses.
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.
const ( SourceScreenshot = "screenshot" SourceOGImage = "og_image" SourceCard = "card" )
Record sources: where a preview's pixels came from.
const ( DefaultPath = "/" MaxPathLen = 512 MaxWaitMS = 10000 )
Bounds on the per-app capture options.
const DefaultImage = "docker.io/chromedp/headless-shell:151.0.7922.109"
DefaultImage is the pinned capture browser; override with APP_PREVIEW_IMAGE.
Variables ¶
var ErrBadRef = errors.New("preview: unusable reference")
ErrBadRef means a reference is not a fetchable http(s) URL.
var ErrBlankImage = errors.New("preview: capture is blank")
ErrBlankImage means the capture is a single flat color.
var ErrDisabled = errors.New("preview: disabled")
ErrDisabled means previews are switched off, globally or for the app.
var ErrInvalid = errors.New("preview: invalid setting")
ErrInvalid wraps every settings validation failure.
var ErrNotFound = errors.New("preview: not found")
ErrNotFound means no preview exists for the requested deployment.
var ErrNotHTML = errors.New("preview: page is not HTML")
ErrNotHTML means the page answered with something other than HTML.
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 ¶
IsBlank reports whether one color (quantized to 4 bits per channel) covers at least ratio of a sampled grid of img.
func ResolveRef ¶
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 ¶
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.
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 ¶
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 FileStore ¶
type FileStore struct {
// contains filtered or unexported fields
}
FileStore keeps thumbnails at <root>/<app>/<deployment>.jpg.
func NewFileStore ¶
NewFileStore returns a store rooted at root, which is created on first write.
func (*FileStore) Path ¶
Path returns the file path for a deployment, rejecting names that could escape the root.
func (*FileStore) RemoveEntry ¶
RemoveEntry deletes a file found by List, plus its app directory if empty.
type ImageState ¶
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 (*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) NotifyReady ¶
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 ¶
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 ¶
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) Start ¶
Start runs a startup sweep and then one on every SweepInterval until ctx ends. It is the only long-lived goroutine.
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.
Preview modes, cheapest first.
type Option ¶
type Option func(*Manager)
Option adjusts a Manager, mainly for tests.
func WithHostProbes ¶
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.
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 ¶
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 ¶
DisplayTitle prefers og:title over the document title.
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 ¶
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.
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 ¶
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 ¶
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 (*SQLStore) DeletePreviewRecords ¶
DeletePreviewRecords removes records by deployment ID.
func (*SQLStore) DeletePreviewSettings ¶
DeletePreviewSettings removes an app's settings, returning it to the default off state.
func (*SQLStore) GetPreviewRecord ¶
GetPreviewRecord returns one record, or nil when absent.
func (*SQLStore) GetPreviewSettings ¶
GetPreviewSettings returns the stored settings, or the zero value when none exist.
func (*SQLStore) GetPreviewState ¶
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 ¶
ListAllPreviewRecords lists every record, newest first.
func (*SQLStore) ListPreviewRecords ¶
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 ¶
SetPreviewState writes one state value.
func (*SQLStore) TouchPreviewViewed ¶
TouchPreviewViewed stamps the last time a preview image was served.
type SettingsPatch ¶
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 ¶
ShotRequest is one page to screenshot.
type ShotResult ¶
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 ¶
SkipError tells the Manager a capture must not run, recording Reason against DeploymentID (empty means nothing is recorded).
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 Thumb ¶
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.