8.4 KiB
Watchtower Update Flow
This document explains the end-to-end update flow in the Watchtower codebase, including the main function call chain, the key data shapes, and diagrams (Mermaid & PlantUML).
Quick Summary
- Trigger: CLI (
watchtowerstart / scheduler / HTTP API update) constructstypes.UpdateParamsand callsinternal/actions.Update. internal/actions.Updateorchestrates discovery, stale detection, lifecycle hooks, stopping/restarting containers, cleanup and reporting.- Image pull optimization uses a digest HEAD request (
pkg/registry/digest) and a token flow (pkg/registry/auth) with an in-memory token cache. - TLS for HEAD/token requests is secure-by-default and configurable via
--insecure-registry,--registry-ca, and--registry-ca-validate.
Call Chain (step-by-step)
-
CLI start / scheduler / HTTP API
- Entry points:
main()->cmd.Execute()-> Cobra commandRun/PreRun. cmd.PreRunreads flags and config, setsregistry.InsecureSkipVerifyandregistry.RegistryCABundle.
- Entry points:
-
Run update
cmd.runUpdatesWithNotificationsbuildstypes.UpdateParamsand callsinternal/actions.Update(client, updateParams).
-
Orchestration:
internal/actions.Update- If
params.LifecycleHooks->lifecycle.ExecutePreChecks(client, params) - Discover containers:
client.ListContainers(params.Filter) - For each container:
client.IsContainerStale(container, params)- calls
client.PullImage(ctx, container)unlesscontainer.IsNoPull(params)is truePullImageobtainstypes.ImagePullOptionsviapkg/registry.GetPullOptions(image)- tries digest optimization:
pkg/registry/digest.CompareDigest(container, opts.RegistryAuth)auth.GetToken(container, registryAuth)obtains a token:- sends GET to the challenge URL (
/v2/), inspectsWWW-Authenticate - for
Bearer: constructs auth URL withrealm,service, andscope(repository:<path>:pull) - checks in-memory cache (
auth.getCachedToken(cacheKey)) keyed by the auth URL - if missing, requests token from auth URL (Basic header if Docker cred present), parses
types.TokenResponseand callsauth.storeToken(cacheKey, token, ExpiresIn)
- sends GET to the challenge URL (
digest.GetDigest(manifestURL, token)performs an HTTPHEADusing a transport created bydigest.newTransport()- transport respects
registry.InsecureSkipVerifyand usesregistry.GetRegistryCertPool()when a CA bundle is provided
- transport respects
- If remote digest matches a local digest,
PullImageskips the pull
client.HasNewImage(ctx, container)compares local image ID with remote image ID
- calls
targetContainer.VerifyConfiguration()(fail/skip logic)- Mark scanned/skipped in
session.Progressand setcontainer.SetStale(stale)
- Sort containers:
sorter.SortByDependencies(containers) UpdateImplicitRestart(containers)setsLinkedToRestartingflags- Build
containersToUpdateand mark them for update inProgress - Update strategy:
- Rolling restart:
performRollingRestart(containersToUpdate, client, params)stopStaleContainer(c)->restartStaleContainer(c)per container
- Normal:
stopContainersInReversedOrder(...)->restartContainersInSortedOrder(...)stopStaleContainerrunslifecycle.ExecutePreUpdateCommandandclient.StopContainerrestartStaleContainermayclient.RenameContainer(watchtower self),client.StartContainerandlifecycle.ExecutePostUpdateCommand
- Rolling restart:
- If
params.Cleanup->cleanupImages(client, imageIDs)callsclient.RemoveImageByID - If
params.LifecycleHooks->lifecycle.ExecutePostChecks(client, params) - Return
progress.Report()(atypes.Reportimplemented fromsession.Progress)
- If
Key data shapes
-
types.UpdateParams(created incmd/runUpdatesWithNotifications)Filter(types.Filter)Cleanup boolNoRestart boolTimeout time.DurationMonitorOnly boolNoPull boolLifecycleHooks boolRollingRestart boolLabelPrecedence bool
-
container.Clientinterface (inpkg/container/client.go) — used byactions.UpdateListContainers(Filter) ([]types.Container, error)GetContainer(containerID) (types.Container, error)StopContainer(types.Container, time.Duration) errorStartContainer(types.Container) (types.ContainerID, error)RenameContainer(types.Container, string) errorIsContainerStale(types.Container, types.UpdateParams) (bool, types.ImageID, error)ExecuteCommand(containerID types.ContainerID, command string, timeout int) (SkipUpdate bool, err error)RemoveImageByID(types.ImageID) errorWarnOnHeadPullFailed(types.Container) bool
-
types.Containerinterface (inpkg/types/container.go) — methods used include:ID(), Name(), ImageName(), ImageID(), SafeImageID(), IsRunning(), IsRestarting()VerifyConfiguration() error,HasImageInfo() bool,ImageInfo() *types.ImageInspect- lifecycle hooks:
GetLifecyclePreUpdateCommand(), GetLifecyclePostUpdateCommand(), PreUpdateTimeout(), PostUpdateTimeout() - flags:
IsNoPull(UpdateParams), IsMonitorOnly(UpdateParams), ToRestart(), IsWatchtower()
-
session.Progressandsession.ContainerStatus(reporting)Progressis a mapmap[types.ContainerID]*ContainerStatusContainerStatusfields:containerID, containerName, imageName, oldImage, newImage, error, stateProgress.Report()returns atypes.Reportimplementation
-
types.TokenResponse(used bypkg/registry/auth) containsToken stringandExpiresIn int(seconds)
Diagrams
Mermaid sequence diagram (embedded):
sequenceDiagram
participant CLI as CLI / Scheduler / HTTP API
participant CMD as cmd
participant ACT as internal/actions.Update
participant CLIENT as container.Client (docker wrapper)
participant DIG as pkg/registry/digest
participant AUTH as pkg/registry/auth
participant REG as pkg/registry (TLS config)
participant DOCKER as Docker Engine
CLI->>CMD: trigger runUpdatesWithNotifications()
CMD->>ACT: Update(client, UpdateParams)
ACT->>CLIENT: ListContainers(filter)
loop per container
ACT->>CLIENT: IsContainerStale(container, params)
CLIENT->>CLIENT: PullImage (maybe)
CLIENT->>DIG: CompareDigest(container, registryAuth)
DIG->>AUTH: GetToken(challenge)
AUTH->>AUTH: getCachedToken / storeToken
DIG->>REG: newTransport() (uses --insecure-registry / --registry-ca)
DIG->>DOCKER: HEAD manifest with token
alt digest matches
CLIENT-->>ACT: no pull needed
else
CLIENT->>DOCKER: ImagePull(image)
end
CLIENT-->>ACT: HasNewImage -> stale/ newestImage
end
ACT->>ACT: SortByDependencies
ACT->>CLIENT: StopContainer / StartContainer (with lifecycle hooks)
ACT->>CLIENT: RemoveImageByID (cleanup)
ACT-->>CMD: progress.Report()
For reference, a PlantUML source for the same sequence is available in docs/diagrams/update-flow.puml.
Security & operational notes
- TLS: registry HEAD and token requests are secure-by-default. Use
--registry-cato add private CAs, and--registry-ca-validateto fail fast on bad bundles. Avoid--insecure-registryexcept for testing. - Token cache: tokens are cached per auth URL (realm+service+scope). Tokens with
ExpiresInare cached for that TTL. No persistent or distributed cache is provided. - Digest HEAD optimization avoids pulls and unnecessary rate consumption when possible. DockerHub/GHCR may rate-limit HEAD or behave differently; the code includes a
WarnOnAPIConsumptionheuristic.
Where to look in the code
- Orchestration:
internal/actions/update.go - CLI wiring:
cmd/root.go,internal/flags/flags.go - Container wrapper:
pkg/container/client.go,pkg/container/container.go - Digest & transport:
pkg/registry/digest/digest.go - Token & auth handling:
pkg/registry/auth/auth.go - TLS helpers:
pkg/registry/registry.go - Lifecycle hooks:
pkg/lifecycle/lifecycle.go - Session/reporting:
pkg/session/*,pkg/types/report.go
If you'd like, I can also open a branch and create a PR with these files, or convert the PlantUML into an SVG and add it to the docs site.
End of document.