feat(api): add read-only /v1/containers endpoint - #1700
Conversation
Adds an optional GET /v1/containers HTTP API endpoint (enabled with --http-api-containers / WATCHTOWER_HTTP_API_CONTAINERS) that lists each watched container's current image identity: name, image, image_id, and running_digest (the registry manifest digest from RepoDigests, directly comparable to a registry's Docker-Content-Digest). This is the read-only counterpart to /v1/update, letting an external orchestrator see what each container is actually running and compare it against a registry without pulling any image layers. Addresses nicholas-fedor#979. Token-authenticated and GET-only, and coexists with --http-api-update and --http-api-metrics.
WalkthroughAdds a new read-only HTTP endpoint ChangesHTTP API Containers Endpoint
Sequence DiagramsequenceDiagram
participant CLI
participant RunConfig
participant APISetup
participant ContainersHandler
participant HTTPClient
participant ContainerStore
CLI->>RunConfig: set EnableContainersAPI
RunConfig->>APISetup: pass Options.EnableContainersAPI
APISetup->>ContainersHandler: register endpoint (when enabled)
HTTPClient->>ContainersHandler: GET /v1/containers (with auth)
ContainersHandler->>ContainerStore: list containers (with filter)
ContainerStore-->>ContainersHandler: []Status (Name, Image, ImageID, RepoDigests)
ContainersHandler->>ContainersHandler: set Digest from RepoDigests
ContainersHandler-->>HTTPClient: 200 JSON {containers,count,timestamp,api_version}
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Up to standards ✅🟢 Issues
|
| Metric | Results |
|---|---|
| Complexity | 41 |
| Duplication | 34 |
NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (2)
pkg/api/containers/containers.go (1)
85-90: 💤 Low valueEncoding error after headers sent cannot update response.
Lines 85-86 set headers and write HTTP 200 before attempting JSON encoding at line 88. If
json.NewEncoder(w).Encodefails, the error is only logged (line 89), but the client already received a 200 status and partial response. This is acceptable for a best-effort API, but consider whether the list result should be pre-encoded to catch serialization errors before committing the status code.♻️ Optional: pre-encode JSON to catch errors before writing headers
- w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - - if err := json.NewEncoder(w).Encode(response); err != nil { + encoded, err := json.Marshal(response) + if err != nil { logrus.WithError(err).Error("Failed to encode containers response") + http.Error(w, "failed to encode response", http.StatusInternalServerError) + return } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write(encoded)🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@pkg/api/containers/containers.go` around lines 85 - 90, The handler currently sets headers and writes status with w.Header().Set and w.WriteHeader(http.StatusOK) before calling json.NewEncoder(w).Encode(response), which can send a 200 even if encoding fails; change this by pre-encoding the response into a buffer (e.g., use bytes.Buffer or json.Marshal) and check for encoding errors first, and only after successful encoding write the Content-Type and StatusOK and copy the buffer to w; if encoding fails, log the error and write an appropriate error status (e.g., http.StatusInternalServerError) instead—look for the variables/function around json.NewEncoder(w).Encode(response) and the w.WriteHeader calls to implement this swap.internal/api/api.go (1)
87-96: ⚡ Quick winDigest parsing inconsistency: use
strings.Cutfor compatibility.
normalizeRepoDigestusesstrings.LastIndexto extract the digest portion after@, but the existing digest comparison logic inpkg/registry/digest/digest.go:872-918usesstrings.Cut(digest, "@")to parse the same format. UsingCutis more idiomatic, consistent with the codebase pattern, and handles edge cases like leading@more explicitly.♻️ Refactor to use `strings.Cut` for consistency
-func normalizeRepoDigest(repoDigest string) string { - if at := strings.LastIndex(repoDigest, "@"); at >= 0 { - return repoDigest[at+1:] - } - - return repoDigest +func normalizeRepoDigest(repoDigest string) string { + _, after, found := strings.Cut(repoDigest, "@") + if !found { + return repoDigest + } + return after }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@internal/api/api.go` around lines 87 - 96, normalizeRepoDigest currently uses strings.LastIndex to split on "@"; replace that with strings.Cut for consistency with the rest of the codebase. In normalizeRepoDigest, call _, after, found := strings.Cut(repoDigest, "@"); if found return after else return repoDigest so leading "@" cases yield the right-hand part and inputs without "@" remain unchanged; keep the function name normalizeRepoDigest unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/advanced-features/http-api/index.md`:
- Line 16: Update the broken Markdown link fragment in the table row that
currently uses the underscored fragment "[Containers](`#http_api_containers`)";
replace it with the hyphenated slug "[Containers](`#http-api-containers`)" so it
matches the generated heading slug (the heading "HTTP API Containers") and
resolves correctly.
In `@docs/configuration/arguments/index.md`:
- Around line 766-777: The link fragment in the "See the [HTTP API
documentation](...#http_api_containers)" anchor is using underscores; change the
fragment from `#http_api_containers` to `#http-api-containers` so it matches the
generated slug for the "HTTP API Containers" heading in the HTTP API doc; update
the anchor next to the "See the [HTTP API documentation]" text to use the
hyphenated fragment.
---
Nitpick comments:
In `@internal/api/api.go`:
- Around line 87-96: normalizeRepoDigest currently uses strings.LastIndex to
split on "@"; replace that with strings.Cut for consistency with the rest of the
codebase. In normalizeRepoDigest, call _, after, found :=
strings.Cut(repoDigest, "@"); if found return after else return repoDigest so
leading "@" cases yield the right-hand part and inputs without "@" remain
unchanged; keep the function name normalizeRepoDigest unchanged.
In `@pkg/api/containers/containers.go`:
- Around line 85-90: The handler currently sets headers and writes status with
w.Header().Set and w.WriteHeader(http.StatusOK) before calling
json.NewEncoder(w).Encode(response), which can send a 200 even if encoding
fails; change this by pre-encoding the response into a buffer (e.g., use
bytes.Buffer or json.Marshal) and check for encoding errors first, and only
after successful encoding write the Content-Type and StatusOK and copy the
buffer to w; if encoding fails, log the error and write an appropriate error
status (e.g., http.StatusInternalServerError) instead—look for the
variables/function around json.NewEncoder(w).Encode(response) and the
w.WriteHeader calls to implement this swap.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 19144806-ca17-40cb-a740-6bc76205a4ca
📒 Files selected for processing (8)
cmd/root.godocs/advanced-features/http-api/index.mddocs/configuration/arguments/index.mdinternal/api/api.gointernal/flags/flags.gopkg/api/containers/containers.gopkg/api/containers/containers_test.gopkg/types/config.go
| |:----------------------------------:|:----------:|:-------------:|:-------------------------------------------------------------------:|:--------------------------------------------------------------------:| | ||
| | [Update](#http_api_update) | `POST` | `/v1/update` | [`image`](#image_parameter_usage), [`async`](#asynchronous_updates) | Triggers container updates and returns JSON results of the operation | | ||
| | [Metrics](../metrics-api/index.md) | `GET` | `/v1/metrics` | | Exposes Prometheus-compatible metrics for monitoring and alerting | | ||
| | [Containers](#http_api_containers) | `GET` | `/v1/containers` | | Lists watched containers and their current running image digests | |
There was a problem hiding this comment.
Fix broken link fragment: use hyphens instead of underscores.
The link #http_api_containers won't match the heading at line 311 because Markdown slug generation converts spaces to hyphens, producing #http-api-containers. As per static analysis hints, markdownlint correctly flagged this (MD051).
🔗 Fix the link fragment
-| [Containers](`#http_api_containers`) | `GET` | `/v1/containers` | | Lists watched containers and their current running image digests |
+| [Containers](`#http-api-containers`) | `GET` | `/v1/containers` | | Lists watched containers and their current running image digests |📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| | [Containers](#http_api_containers) | `GET` | `/v1/containers` | | Lists watched containers and their current running image digests | | |
| | [Containers](`#http-api-containers`) | `GET` | `/v1/containers` | | Lists watched containers and their current running image digests | |
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)
[warning] 16-16: Link fragments should be valid
(MD051, link-fragments)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/advanced-features/http-api/index.md` at line 16, Update the broken
Markdown link fragment in the table row that currently uses the underscored
fragment "[Containers](`#http_api_containers`)"; replace it with the hyphenated
slug "[Containers](`#http-api-containers`)" so it matches the generated heading
slug (the heading "HTTP API Containers") and resolves correctly.
| ### HTTP API Containers | ||
|
|
||
| Enables a read-only endpoint that lists watched containers and their current running image digests. | ||
|
|
||
| ```text | ||
| Argument: --http-api-containers | ||
| Environment Variable: WATCHTOWER_HTTP_API_CONTAINERS | ||
| Type: Boolean | ||
| Default: false | ||
| ``` | ||
|
|
||
| !!! Note "See the [HTTP API documentation](../../advanced-features/http-api/index.md#http_api_containers) for details" |
There was a problem hiding this comment.
Fix broken link fragment: use hyphens instead of underscores.
Line 777 links to #http_api_containers, but the target heading in docs/advanced-features/http-api/index.md will generate the slug #http-api-containers (Markdown converts spaces to hyphens). Update the fragment to match.
🔗 Fix the link fragment
-!!! Note "See the [HTTP API documentation](../../advanced-features/http-api/index.md#http_api_containers) for details"
+!!! Note "See the [HTTP API documentation](../../advanced-features/http-api/index.md#http-api-containers) for details"🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/configuration/arguments/index.md` around lines 766 - 777, The link
fragment in the "See the [HTTP API documentation](...#http_api_containers)"
anchor is using underscores; change the fragment from `#http_api_containers` to
`#http-api-containers` so it matches the generated slug for the "HTTP API
Containers" heading in the HTTP API doc; update the anchor next to the "See the
[HTTP API documentation]" text to use the hyphenated fragment.
Codecov Report❌ Patch coverage is
❌ Your patch check has failed because the patch coverage (42.64%) is below the target coverage (70.00%). You can increase the patch coverage or adjust the target coverage. @@ Coverage Diff @@
## main #1700 +/- ##
==========================================
- Coverage 74.59% 74.37% -0.22%
==========================================
Files 60 61 +1
Lines 10052 10120 +68
==========================================
+ Hits 7498 7527 +29
- Misses 2286 2322 +36
- Partials 268 271 +3
🚀 New features to boost your workflow:
|
- Replace `normalizeRepoDigest` helper with inline `strings.Cut` call - Remove unused `normalizeRepoDigest` function
- Extract package-level godoc comment from containers.go into new doc.go file - Add component overview and usage example to package documentation
…ponseWriter - Use bytes.Buffer to encode JSON response before writing to http.ResponseWriter - Separate encoding and writing steps for better error handling
- Add comprehensive test suite with success, error, and edge case scenarios - Include tests for unauthorized access, server errors, and malformed responses - Update test container names and images to generic examples - Add timestamp field validation to response parsing
There was a problem hiding this comment.
🧹 Nitpick comments (1)
pkg/api/containers/containers_test.go (1)
300-399: ⚡ Quick winBind Gomega to
tin the standalone tests instead of relying on the global Ginkgo fail handler.
TestHandleReturns500OnListError,TestHandleReturnsEmptyRunningDigestForLocalImages,TestNewHandlerSetsCorrectPath, andTestHandlerStartsDebugLoggingare plaingo testfunctions but use the package-globalgomega.Expect. The only fail handler registered isginkgo.Fail(set inTestContainers). If an assertion fails here,ginkgo.Failis invoked outside a Ginkgo spec, producing a panic/misattributed failure rather than a cleantfailure — and because all of these run witht.Parallel(), the handler may not even be registered when they execute. Use at-scoped Gomega instance.💚 Example for one test (apply to all four standalone tests)
func TestHandleReturns500OnListError(t *testing.T) { t.Parallel() + g := gomega.NewWithT(t) expectedErr := errors.New("list error") handler := containersAPI.New(func(_ context.Context) ([]containersAPI.Status, error) { return nil, expectedErr }) rec := httptest.NewRecorder() req := httptest.NewRequestWithContext( context.Background(), http.MethodGet, "/v1/containers", nil, ) handler.Handle(rec, req) - gomega.Expect(rec.Code).To(gomega.Equal(http.StatusInternalServerError)) + g.Expect(rec.Code).To(gomega.Equal(http.StatusInternalServerError)) body, err := io.ReadAll(rec.Body) - gomega.Expect(err).ToNot(gomega.HaveOccurred()) - gomega.Expect(string(body)).To(gomega.ContainSubstring("failed to list containers")) + g.Expect(err).ToNot(gomega.HaveOccurred()) + g.Expect(string(body)).To(gomega.ContainSubstring("failed to list containers")) }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@pkg/api/containers/containers_test.go` around lines 300 - 399, The four standalone tests (TestHandleReturns500OnListError, TestHandleReturnsEmptyRunningDigestForLocalImages, TestNewHandlerSetsCorrectPath, TestHandlerStartsDebugLogging) use the package-global gomega.Expect which relies on a Ginkgo fail handler; change each to create a test-scoped Gomega with g := gomega.NewWithT(t) at the top of the test and replace all gomega.Expect(...) calls in that test with g.Expect(...), ensuring assertions now fail via t instead of the global Ginkgo handler.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@pkg/api/containers/containers_test.go`:
- Around line 300-399: The four standalone tests
(TestHandleReturns500OnListError,
TestHandleReturnsEmptyRunningDigestForLocalImages,
TestNewHandlerSetsCorrectPath, TestHandlerStartsDebugLogging) use the
package-global gomega.Expect which relies on a Ginkgo fail handler; change each
to create a test-scoped Gomega with g := gomega.NewWithT(t) at the top of the
test and replace all gomega.Expect(...) calls in that test with g.Expect(...),
ensuring assertions now fail via t instead of the global Ginkgo handler.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 0c813b62-7e97-4923-b334-f8248e4899ac
📒 Files selected for processing (4)
internal/api/api.gopkg/api/containers/containers.gopkg/api/containers/containers_test.gopkg/api/containers/doc.go
✅ Files skipped from review due to trivial changes (1)
- pkg/api/containers/doc.go
🚧 Files skipped from review as they are similar to previous changes (1)
- pkg/api/containers/containers.go
…response - Fix table formatting by aligning endpoint column headers with content - Update example container name from "beacon" to "nginx" for consistency with common use cases - Correct JSON indentation in containers endpoint response example - Standardize endpoint path formatting with consistent spacing
… test assertions - Check `strings.Cut` return value to avoid setting empty digest when format is unexpected - Add debug logging for malformed RepoDigest entries to aid troubleshooting - Rename test variables for clarity (e.g., `httpAPI` → `emptyHTTPAPI`, `multiHTTPAPI`) - Add explicit request and authorization header verification in container API tests - Standardize server variable naming across test cases for consistency
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/advanced-features/http-api/index.md`:
- Line 14: The Update endpoint link uses an underscored fragment
`[Update](`#http_api_update`)` which doesn't match Markdown's generated slug;
change the link to the hyphenated fragment `[Update](`#http-api-update`)` in the
table row (the visible link text "Update" and its fragment) so it correctly
resolves to the "HTTP API Update" section anchor.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: b63ce4ac-6c20-4ebd-9fba-4135521bf0c8
📒 Files selected for processing (3)
docs/advanced-features/http-api/index.mdinternal/api/api.gopkg/api/containers/containers_test.go
🚧 Files skipped from review as they are similar to previous changes (2)
- internal/api/api.go
- pkg/api/containers/containers_test.go
- Update `Status` struct field and JSON tag from `RunningDigest`/`running_digest` to `Digest`/`digest` - Adjust all test fixtures, assertions, and documentation to use the new field name - Simplify field description in comments and docs for clarity
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
internal/api/api.go (1)
178-191:⚠️ Potential issue | 🟠 Major | ⚡ Quick winValidate
opts.Clientbefore enabling the containers endpoint.Line 179 captures
opts.Client, and Line 189/191 dereferences it without a guard. IfEnableContainersAPIis set with a nil client, requests can panic at runtime. Please fail fast in setup (same pattern used for other endpoint dependencies).Suggested fix
var ( // errMissingRunUpdatesWithNotifications indicates RunUpdatesWithNotifications was not provided. errMissingRunUpdatesWithNotifications = errors.New("RunUpdatesWithNotifications must be provided when EnableUpdateAPI is set") @@ // errMissingDefaultMetrics indicates DefaultMetrics was not provided. errMissingDefaultMetrics = errors.New("DefaultMetrics must be provided when EnableUpdateAPI is set") + // errMissingClient indicates Client was not provided. + errMissingClient = errors.New("Client must be provided when EnableContainersAPI is set") ) @@ if opts.EnableContainersAPI { + if opts.Client == nil { + return errMissingClient + } + client := opts.Client filter := opts.Filter🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@internal/api/api.go` around lines 178 - 191, When EnableContainersAPI is true you must validate opts.Client before constructing the containers endpoint to avoid a nil dereference: check opts.Client (the variable captured as client) is non-nil before calling containersAPI.New (and before using client.ListContainers inside the handler) and fail fast in setup (return an error or log and exit) if it is nil, mirroring the same dependency checks used for other endpoints so the handler never captures a nil client.pkg/api/containers/containers_test.go (1)
317-416:⚠️ Potential issue | 🟠 Major | ⚡ Quick winBind Gomega to
*testing.Tin the standaloneTest...functions.These
TestHandleReturns500OnListError,TestHandleReturnsEmptyDigestForLocalImages,TestNewHandlerSetsCorrectPath, andTestHandlerStartsDebugLoggingusegomega.Expect(...)witht.Parallel()but don’t bind a fail handler themselves; they rely onTestContainerscallinggomega.RegisterFailHandler(ginkgo.Fail)in a separate parallel test, which is fragile with respect to test start order. Bind Gomega inside each standalone test (e.g.,g := gomega.NewWithT(t)) and switch tog.Expect(...).Suggested pattern
func TestHandleReturns500OnListError(t *testing.T) { t.Parallel() + g := gomega.NewWithT(t) @@ - gomega.Expect(rec.Code).To(gomega.Equal(http.StatusInternalServerError)) + g.Expect(rec.Code).To(gomega.Equal(http.StatusInternalServerError)) @@ - gomega.Expect(err).ToNot(gomega.HaveOccurred()) - gomega.Expect(string(body)).To(gomega.ContainSubstring("failed to list containers")) + g.Expect(err).ToNot(gomega.HaveOccurred()) + g.Expect(string(body)).To(gomega.ContainSubstring("failed to list containers")) }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@pkg/api/containers/containers_test.go` around lines 317 - 416, Each standalone test (TestHandleReturns500OnListError, TestHandleReturnsEmptyDigestForLocalImages, TestNewHandlerSetsCorrectPath, TestHandlerStartsDebugLogging) must bind Gomega to the local *testing.T to avoid relying on external registration; in each test call g := gomega.NewWithT(t) at the top and replace calls to gomega.Expect(...) with g.Expect(...), ensuring all assertions in those tests use the local g instance.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@internal/api/api.go`:
- Around line 178-191: When EnableContainersAPI is true you must validate
opts.Client before constructing the containers endpoint to avoid a nil
dereference: check opts.Client (the variable captured as client) is non-nil
before calling containersAPI.New (and before using client.ListContainers inside
the handler) and fail fast in setup (return an error or log and exit) if it is
nil, mirroring the same dependency checks used for other endpoints so the
handler never captures a nil client.
In `@pkg/api/containers/containers_test.go`:
- Around line 317-416: Each standalone test (TestHandleReturns500OnListError,
TestHandleReturnsEmptyDigestForLocalImages, TestNewHandlerSetsCorrectPath,
TestHandlerStartsDebugLogging) must bind Gomega to the local *testing.T to avoid
relying on external registration; in each test call g := gomega.NewWithT(t) at
the top and replace calls to gomega.Expect(...) with g.Expect(...), ensuring all
assertions in those tests use the local g instance.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 82566c4c-c1bc-44b6-ab05-2869e6e144ed
📒 Files selected for processing (5)
docs/advanced-features/http-api/index.mdinternal/api/api.gopkg/api/containers/containers.gopkg/api/containers/containers_test.gopkg/api/containers/doc.go
✅ Files skipped from review due to trivial changes (2)
- docs/advanced-features/http-api/index.md
- pkg/api/containers/doc.go
🚧 Files skipped from review as they are similar to previous changes (1)
- pkg/api/containers/containers.go
|
Thank you for submitting the feature request / PR. Testing shows functionality to be working, as expected. Test Configuration: services:
watchtower-test:
container_name: watchtower-test
image: watchtower:test
volumes:
- /var/run/docker.sock:/var/run/docker.sock
environment:
- WATCHTOWER_DEBUG=true
- WATCHTOWER_SCOPE=watchtower-test
- WATCHTOWER_HTTP_API_TOKEN=test
- WATCHTOWER_HTTP_API_CONTAINERS=true
networks:
- watchtower-test
ports:
- 8080:8080
restart: unless-stopped
labels:
- com.centurylinklabs.watchtower.scope=watchtower-test
networks:
watchtower-test:
name: watchtower-testRequest: curl -X GET -H "Authorization: Bearer test" localhost:8080/v1/containersResponse: {
"api_version": "v1",
"containers": [
{
"name": "watchtower-test",
"image": "watchtower:test",
"image_id": "sha256:9053a97e02420c1a617c05561aeb84232f825553831e149d341752cf1796c117",
"digest": "sha256:9053a97e02420c1a617c05561aeb84232f825553831e149d341752cf1796c117"
}
],
"count": 1,
"timestamp": "2026-05-29T18:51:55Z"
}I am accepting this PR, because the addition of this endpoint is consistent with Watchtower's purpose: Docker container lifecycle management (i.e. container updates). With that said, I do not recommend using Watchtower in a commercial or production environment. From what you described, I would recommend considering a solution that is specifically tailored to your use case and needs. If you have an interest in working on a such a project, then please feel free to let me know. |
|
@all-contributors add @barnabasbusa for code and documentation |
|
I've put up a pull request to add @barnabasbusa! 🎉 |
Summary
Adds an optional, read-only
GET /v1/containersHTTP API endpoint (enabled with--http-api-containers/WATCHTOWER_HTTP_API_CONTAINERS) that lists each watched container's current image identity:name— container nameimage— image reference with tagimage_id— local image config IDrunning_digest— the registry manifest digest the running image was pulled from (from the image'sRepoDigests), directly comparable to a registry'sDocker-Content-Digest. Empty for locally-built images with no registry reference.It's the read-only counterpart to
/v1/update: it lets an external orchestrator see what each container is actually running and compare it against a registry without pulling any image layers. Addresses the use case in #979.Motivation
We drive gated, sequential image rollouts across an Ethereum node fleet and need to know which container is on which digest — both to decide what to roll and to confirm a node recovered onto the new digest afterwards. Watchtower already inspects this information internally; this just exposes it read-only.
Changes
pkg/api/containershandler (+ test)internal/apibehind the new flag, listing via the existing container client + filter and readingImageInfo().RepoDigestsinternal/flags,pkg/types,cmd/root.go)Notes
/v1/metrics.--http-api-updateand--http-api-metrics.Test plan
go build ./...go test ./pkg/api/containers/...curl -H "Authorization: Bearer <token>" localhost:8080/v1/containersSummary by CodeRabbit
New Features
Documentation
Tests