fleet

package
v1.26.1 Latest Latest
Warning

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

Go to latest
Published: Jul 13, 2026 License: Apache-2.0 Imports: 9 Imported by: 0

Documentation

Overview

internal/fleet/changelog.go

Package fleet provides the runtime-image discovery primitives for go-code.

It defines the core data carriers (RuntimeImage, Target, Filter), the Probe interface, a Registry for mapping URL schemes to Probe drivers, and a pure Diff function for comparing pinned source images against discovered runtime images.

Package fleet imports internal/polyglot/pinned for the PinnedImage type. All driver packages (fleet/docker, fleet/ssh, …) depend on fleet, not the other way around.

Index

Constants

This section is empty.

Variables

DiffStatusPriority maps DiffStatus to a sort-order integer used by Diff() for stable per-target output. Lower number = higher priority (appears first).

External code wanting a DIFFERENT priority order (e.g. LLM-summary presentation) MUST declare its own table rather than mutating this one; see SummaryStatusPriority.

View Source
var ErrInvalidTarget = errors.New("fleet: invalid target")

ErrInvalidTarget is returned by ParseTarget when the input cannot be parsed into a valid Target.

View Source
var ErrSchemeUnknown = errors.New("fleet: no probe registered for scheme")

ErrSchemeUnknown is returned by Registry.Get when no probe is registered.

View Source
var SummaryStatusPriority = map[DiffStatus]int{
	DiffTagDrift:    0,
	DiffDigestDrift: 1,
	DiffUnresolved:  2,
	DiffOnlyRuntime: 3,
	DiffOnlySource:  4,
	DiffMatch:       5,
}

SummaryStatusPriority is the order used to rank entries when summarising fleet drift for an LLM prompt (cmd/go-code Phase 7). DELIBERATELY differs from DiffStatusPriority: OnlyRuntime ranks above OnlySource here because a running unpinned container is more actionable in incident triage than a pinned image with no running instance.

Lower number = higher priority (appears first). The zero value for unknown statuses sorts last — callers should provide explicit fallback (e.g. 99) for statuses not in this map.

Functions

func IsValidFilter

func IsValidFilter(s string) bool

IsValidFilter reports whether s is acceptable as a filter value passed to Filter.Service. Allowed chars: [a-zA-Z0-9._-]. Empty string is allowed (means "no filter").

This is the canonical implementation; internal/fleet/docker and internal/fleet/ssh both delegate to this. The character set is intentionally ASCII-only — Docker container and Compose service names are always ASCII, and a tighter allow-list reduces injection surface.

func MatchesFilter

func MatchesFilter(s string, r RuntimeImage) bool

MatchesFilter reports whether a runtime image matches the requested service filter, using the documented priority order:

  1. Exact Container name match
  2. com.docker.compose.service label value (carried in RuntimeImage.Service)

Empty filter (s == "") always matches.

Both comparisons are case-sensitive.

func ParseImageRef

func ParseImageRef(ref string) (image, tag, digest, invalidDigestReason string)

ParseImageRef splits a Docker image reference into (image, tag, digest, invalidDigestReason).

Rules:

  • If ref contains '@': right side is the digest candidate. If it does NOT begin with "sha256:", digest is set to "" and invalidDigestReason describes why. Otherwise digest = the full "sha256:..." string.
  • On the remaining ref (or whole if no '@'): find the LAST ':'. If the substring after it does NOT contain '/', that is the tag. Otherwise the whole thing is the image (the colon is part of a registry host:port like "localhost:5000/foo").
  • If both tag and digest are empty after the above: tag = "latest".

invalidDigestReason is "" when digest is valid or absent.

Driver call sites (fleet/docker, fleet/ssh) ignore invalidDigestReason, silently dropping invalid digests — matching the prior silent-drop behaviour. The polyglot/pinned parser surfaces it via PinnedImage.Unresolved instead.

Types

type Changelog

type Changelog struct {
	Repo      string            `json:"repo"`                // "minio/minio"
	Base      string            `json:"base"`                // "26.4.25" (actually-resolved tag)
	Head      string            `json:"head"`                // "26.5.3"
	Status    string            `json:"status"`              // "ahead" / "behind" / "diverged" / "identical" / "unresolved"
	Commits   []ChangelogCommit `json:"commits,omitempty"`   // capped at 20, head→base order
	Truncated bool              `json:"truncated,omitempty"` // true if upstream had >20 commits
	Resolved  bool              `json:"resolved"`            // false if mapping or compare failed
	Reason    string            `json:"reason,omitempty"`    // when Resolved=false
	URL       string            `json:"url,omitempty"`       // GitHub web URL for the compare view
}

Changelog holds the result of correlating two upstream image tags against a public GitHub repository via the Compare API.

Populated by internal/fleet/upstream.Enrich for DiffTagDrift rows. Attached to ImageDiff.Changelog (nil for non-TagDrift rows).

type ChangelogCommit

type ChangelogCommit struct {
	SHA     string `json:"sha"`
	Author  string `json:"author"`
	Date    string `json:"date"`    // ISO-8601, from author date
	Subject string `json:"subject"` // first line of commit message
}

ChangelogCommit is one commit entry in a Changelog.

type DiffStatus

type DiffStatus string

DiffStatus enumerates the possible diff outcomes for one image.

const (
	DiffMatch       DiffStatus = "Match"
	DiffTagDrift    DiffStatus = "TagDrift"
	DiffDigestDrift DiffStatus = "DigestDrift"
	DiffOnlySource  DiffStatus = "OnlySource"  // pinned in repo, not running anywhere
	DiffOnlyRuntime DiffStatus = "OnlyRuntime" // running but not pinned in repo
	DiffUnresolved  DiffStatus = "Unresolved"  // pinned image had Unresolved set; cannot compare
)

type Filter

type Filter struct {
	Service string            // matches container name OR com.docker.compose.service label OR plain compose service
	Labels  map[string]string // exact label-value match; reserved for v2 — drivers may ignore for MVP
}

Filter narrows what a probe returns. Empty fields mean no constraint.

type ImageDiff

type ImageDiff struct {
	Image       string // the registry+repo identifier this row is about
	Pinned      *pinned.PinnedImage
	Runtime     *RuntimeImage
	Status      DiffStatus
	Explanation string     // one-line human-readable summary
	Changelog   *Changelog `json:"changelog,omitempty"` // populated for DiffTagDrift rows when upstream enrichment succeeds
}

ImageDiff is one row of the per-target report. Either Pinned or Runtime (or both) is non-nil — never both nil.

func Diff

func Diff(pinnedImgs []pinned.PinnedImage, runtimeImgs []RuntimeImage) []ImageDiff

Diff computes per-image diffs between pinned (source) and runtime (probe) sets.

Matching: by Image string (registry+repo, no tag, no digest). Case-sensitive.

Semantics per image group:

  • Pinned only, no Runtime → DiffOnlySource (Pinned set, Runtime nil)
  • Runtime only, no Pinned → DiffOnlyRuntime (Pinned nil, Runtime set)
  • Pinned.Unresolved != "" → DiffUnresolved (Pinned set, Runtime set if available)
  • Both present, same Tag, same Digest (or both Digest=="") → DiffMatch
  • Both present, same Tag, different Digest (both non-empty) → DiffDigestDrift
  • Both present, different Tag → DiffTagDrift

One-side-has-digest, other-does-not: treated as DiffMatch (no evidence of drift; runtime probes may not surface digests).

When the same Image appears multiple times in either input (e.g. two compose services referencing nginx), items are sorted within each Image group before pairing:

  • Pinned: by Service, then Source, then Line
  • Runtime: by Container, then StartedAt

Surplus on either side gets OnlySource / OnlyRuntime rows.

Output is sorted: by Status priority (TagDrift > DigestDrift > Unresolved > OnlySource > OnlyRuntime > Match), then by Image asc, then by Pinned.Service / Runtime.Container — deterministic.

type Probe

type Probe interface {
	Scheme() string
	List(ctx context.Context, t Target, f Filter) ([]RuntimeImage, error)
}

Probe is the runtime-image discovery primitive. One implementation per Scheme.

type Registry

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

Registry maps a target scheme to its Probe driver.

func NewRegistry

func NewRegistry() *Registry

NewRegistry creates an empty Registry.

func (*Registry) Get

func (r *Registry) Get(t Target) (Probe, error)

Get returns the Probe for t.Scheme. Error if no probe is registered. Error wraps a sentinel ErrSchemeUnknown that callers can errors.Is against.

func (*Registry) Has

func (r *Registry) Has(scheme string) bool

Has reports whether a probe is registered for the given scheme.

func (*Registry) Register

func (r *Registry) Register(p Probe)

Register adds p under p.Scheme(). Overwrites silently if scheme already present (last-wins) — caller can detect via Has().

type RuntimeImage

type RuntimeImage struct {
	Container string    // container name (preferred) or short id
	Image     string    // registry+repo, no tag, no digest
	Tag       string    // resolved tag; "" if image lacks one
	Digest    string    // sha256:...; "" if not surfaced by probe
	State     string    // "running" / "exited" / "restarting" / etc — probe-driver canonical
	StartedAt time.Time // zero if unknown
	Service   string    // com.docker.compose.service label value if present, else ""
}

RuntimeImage is one container image discovered by a Probe on a target host.

type SiblingDriftRow

type SiblingDriftRow struct {
	Image    string           `json:"image"` // registry+repo, no tag/digest
	Variants []SiblingVariant `json:"variants"`
}

SiblingDriftRow groups same-Image runtime entries across targets whose tag or digest differs. Populated by SiblingDiff over a slice of TargetReports (one per probed host).

func SiblingDiff

func SiblingDiff(reports []TargetReportLike) []SiblingDriftRow

SiblingDiff returns one SiblingDriftRow per Image that appears with a differing (Tag, Digest) tuple across two or more targets.

Inputs: the per-target reports produced by per-host Diff(). Output: deterministic order — by Image asc, then by Variant.Target asc. Empty result (nil slice) when fewer than 2 targets, or when no Image shows cross-host drift.

Rules:

  • For each Image observed in any target's Diffs[].Runtime (regardless of Status — Match, OnlyRuntime, TagDrift, etc.), collect (Target, Tag, Digest, ...).
  • If two or more variants for the same Image differ in (Tag, Digest), emit one row.
  • Variants WITHOUT a Runtime (e.g. OnlySource rows) are skipped — only actually-running containers count for sibling-drift purposes.
  • When Digest is empty on one side and non-empty on the other, treat as drift only if Tag also differs. (Same-tag/empty-digest-vs-real-digest is the "we don't have full info" case, not actionable drift.)
  • When the same Image appears multiple times in one target (multiple containers), one representative variant is chosen (first by Container name ascending).

type SiblingVariant

type SiblingVariant struct {
	Target    string `json:"target"` // the TargetReport.Target string (e.g. "ssh://host-b")
	Tag       string `json:"tag"`
	Digest    string `json:"digest,omitempty"`
	Container string `json:"container,omitempty"` // container name on that target
	State     string `json:"state,omitempty"`
}

SiblingVariant is one row inside a SiblingDriftRow — a single host's view of the image.

type Target

type Target struct {
	Raw    string // original input, kept for diagnostics
	Scheme string // "local" | "docker" | "ssh" (case-folded to lowercase)
	Host   string // ssh: hostname or alias as understood by ~/.ssh/config. docker/local: ""
	User   string // ssh-only; "" means default per ~/.ssh/config
	Port   int    // ssh-only; 0 means default per ~/.ssh/config
}

Target identifies WHERE a probe should look. Scheme dispatch happens in Registry.Get.

func ParseTarget

func ParseTarget(raw string) (Target, error)

ParseTarget normalises a target URL into a Target.

Accepted forms:

""                            → {Scheme:"local"}
"local://"                    → {Scheme:"local"}
"docker://"                   → {Scheme:"docker"}
"ssh://<host>"                → {Scheme:"ssh", Host:"<host>"}
"ssh://<user>@<host>"         → {Scheme:"ssh", User:"<user>", Host:"<host>"}
"ssh://<host>:<port>"         → {Scheme:"ssh", Host:"<host>", Port:<port>}
"ssh://<user>@<host>:<port>"

Anything else returns an error wrapping ErrInvalidTarget.

Rules:

  • Host MUST be non-empty for ssh.
  • Host MUST be empty for local/docker (MVP: docker is local-socket only).
  • Port MUST be in 1..65535 if specified.
  • Scheme is case-folded to lowercase in the result.

Target.Raw is always set to the original input.

type TargetReportLike

type TargetReportLike interface {
	TargetStr() string
	DiffsList() []ImageDiff
}

TargetReportLike is the minimal view of a per-host probe report that SiblingDiff needs. cmd/go-code's TargetReport implements this interface via TargetStr() and DiffsList() methods so that internal/fleet stays import-free of cmd/.

Directories

Path Synopsis
Package docker provides a fleet.Probe implementation that discovers running containers via the local Docker Engine unix socket using raw HTTP/1.1.
Package docker provides a fleet.Probe implementation that discovers running containers via the local Docker Engine unix socket using raw HTTP/1.1.
Package ssh provides a fleet.Probe implementation that discovers running containers on a remote host via the system `ssh` binary and `docker ps`.
Package ssh provides a fleet.Probe implementation that discovers running containers on a remote host via the system `ssh` binary and `docker ps`.
internal/fleet/upstream/client.go
internal/fleet/upstream/client.go

Jump to

Keyboard shortcuts

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