Skip to content

feat(api): add read-only /v1/containers endpoint - #1700

Merged
nicholas-fedor merged 11 commits into
nicholas-fedor:mainfrom
barnabasbusa:feat/http-api-containers
May 29, 2026
Merged

feat(api): add read-only /v1/containers endpoint#1700
nicholas-fedor merged 11 commits into
nicholas-fedor:mainfrom
barnabasbusa:feat/http-api-containers

Conversation

@barnabasbusa

@barnabasbusa barnabasbusa commented May 29, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds an optional, read-only 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 — container name
  • image — image reference with tag
  • image_id — local image config ID
  • running_digest — the registry manifest digest the running image was pulled from (from the image's RepoDigests), directly comparable to a registry's Docker-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

  • New pkg/api/containers handler (+ test)
  • Wire-up in internal/api behind the new flag, listing via the existing container client + filter and reading ImageInfo().RepoDigests
  • Flag + config plumbing (internal/flags, pkg/types, cmd/root.go)
  • Docs: HTTP API + arguments pages

Notes

  • Token-authenticated and GET-only, same as /v1/metrics.
  • Coexists with --http-api-update and --http-api-metrics.

Test plan

  • go build ./...
  • go test ./pkg/api/containers/...
  • Manual: curl -H "Authorization: Bearer <token>" localhost:8080/v1/containers

Summary by CodeRabbit

  • New Features

    • Added a read-only GET /v1/containers HTTP API that lists watched containers with name, image, image ID, and registry-derived digest; response includes containers, count, timestamp, and api_version.
    • Added a CLI flag (--http-api-containers) and environment variable to enable the containers API.
  • Documentation

    • Documented the new endpoint, response fields, and how to enable the option.
  • Tests

    • Added unit and integration tests covering success, empty results, auth failures, handler errors, and edge cases.

Review Change Stack

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.
@coderabbitai

coderabbitai Bot commented May 29, 2026

Copy link
Copy Markdown
Contributor

Walkthrough

Adds a new read-only HTTP endpoint /v1/containers to list watched containers and their image identity. The feature is enabled via --http-api-containers / WATCHTOWER_HTTP_API_CONTAINERS, threaded through RunConfig into API options, implemented as a handler package, covered by tests, and documented.

Changes

HTTP API Containers Endpoint

Layer / File(s) Summary
Configuration & Flag Wiring
internal/flags/flags.go, pkg/types/config.go, cmd/root.go
Registers --http-api-containers (WATCHTOWER_HTTP_API_CONTAINERS), adds RunConfig.EnableContainersAPI, reads the flag during run, and passes the value into api.SetupAndStartAPI via api.Options.
Containers API Handler Implementation
pkg/api/containers/containers.go, pkg/api/containers/doc.go
Implements the /v1/containers handler with Status model, ListFunc callback, and Handle that returns JSON {containers, count, timestamp, api_version} and handles list/encode errors.
API Integration & Registration
internal/api/api.go
Imports the containers package, extends api.Options with EnableContainersAPI, and conditionally registers a GET /v1/containers endpoint in SetupAndStartAPI, mapping container data and extracting digest from RepoDigests when present.
Tests & Documentation
pkg/api/containers/containers_test.go, docs/advanced-features/http-api/index.md, docs/configuration/arguments/index.md
Adds Ginkgo and unit tests for success, empty/multiple results, auth failures, content-type, error propagation, digest behavior, and documents the endpoint, response schema, and CLI/environment flag.

Sequence Diagram

sequenceDiagram
  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}
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(api): add read-only /v1/containers endpoint' directly and specifically describes the main change: adding a new read-only HTTP API endpoint for containers.
Docstring Coverage ✅ Passed Docstring coverage is 88.89% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ 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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codacy-production

codacy-production Bot commented May 29, 2026

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 41 complexity · 34 duplication

Metric Results
Complexity 41
Duplication 34

View in Codacy

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
pkg/api/containers/containers.go (1)

85-90: 💤 Low value

Encoding 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).Encode fails, 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 win

Digest parsing inconsistency: use strings.Cut for compatibility.

normalizeRepoDigest uses strings.LastIndex to extract the digest portion after @, but the existing digest comparison logic in pkg/registry/digest/digest.go:872-918 uses strings.Cut(digest, "@") to parse the same format. Using Cut is 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

📥 Commits

Reviewing files that changed from the base of the PR and between 8f1dd3c and acb5baa.

📒 Files selected for processing (8)
  • cmd/root.go
  • docs/advanced-features/http-api/index.md
  • docs/configuration/arguments/index.md
  • internal/api/api.go
  • internal/flags/flags.go
  • pkg/api/containers/containers.go
  • pkg/api/containers/containers_test.go
  • pkg/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 |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

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.

Suggested change
| [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.

Comment on lines +766 to +777
### 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"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

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

codecov Bot commented May 29, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 42.64706% with 39 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
internal/api/api.go 0.00% 32 Missing and 1 partial ⚠️
pkg/api/containers/containers.go 80.00% 4 Missing and 2 partials ⚠️

❌ 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.

Impacted file tree graph

@@            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     
Files with missing lines Coverage Δ
internal/flags/flags.go 86.29% <100.00%> (+0.08%) ⬆️
pkg/api/containers/containers.go 80.00% <80.00%> (ø)
internal/api/api.go 39.32% <0.00%> (-23.18%) ⬇️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

- 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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
pkg/api/containers/containers_test.go (1)

300-399: ⚡ Quick win

Bind Gomega to t in the standalone tests instead of relying on the global Ginkgo fail handler.

TestHandleReturns500OnListError, TestHandleReturnsEmptyRunningDigestForLocalImages, TestNewHandlerSetsCorrectPath, and TestHandlerStartsDebugLogging are plain go test functions but use the package-global gomega.Expect. The only fail handler registered is ginkgo.Fail (set in TestContainers). If an assertion fails here, ginkgo.Fail is invoked outside a Ginkgo spec, producing a panic/misattributed failure rather than a clean t failure — and because all of these run with t.Parallel(), the handler may not even be registered when they execute. Use a t-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

📥 Commits

Reviewing files that changed from the base of the PR and between acb5baa and cf367cb.

📒 Files selected for processing (4)
  • internal/api/api.go
  • pkg/api/containers/containers.go
  • pkg/api/containers/containers_test.go
  • pkg/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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between cf367cb and ddba439.

📒 Files selected for processing (3)
  • docs/advanced-features/http-api/index.md
  • internal/api/api.go
  • pkg/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

Comment thread docs/advanced-features/http-api/index.md
- 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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 win

Validate opts.Client before enabling the containers endpoint.

Line 179 captures opts.Client, and Line 189/191 dereferences it without a guard. If EnableContainersAPI is 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 win

Bind Gomega to *testing.T in the standalone Test... functions.

These TestHandleReturns500OnListError, TestHandleReturnsEmptyDigestForLocalImages, TestNewHandlerSetsCorrectPath, and TestHandlerStartsDebugLogging use gomega.Expect(...) with t.Parallel() but don’t bind a fail handler themselves; they rely on TestContainers calling gomega.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 to g.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

📥 Commits

Reviewing files that changed from the base of the PR and between ddba439 and 11e2fbd.

📒 Files selected for processing (5)
  • docs/advanced-features/http-api/index.md
  • internal/api/api.go
  • pkg/api/containers/containers.go
  • pkg/api/containers/containers_test.go
  • pkg/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

@nicholas-fedor

Copy link
Copy Markdown
Owner

Thank you for submitting the feature request / PR.
I have reviewed and made some modifications, including opting for the digest field, rather than running_digest.

Testing shows functionality to be working, as expected.
As this is based upon the context of Watchtower's configuration, this includes scope-based filtering, as shown below.

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-test

Request:

curl -X GET -H "Authorization: Bearer test" localhost:8080/v1/containers

Response:

{
    "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.

@nicholas-fedor
nicholas-fedor merged commit bb267c8 into nicholas-fedor:main May 29, 2026
14 of 15 checks passed
@nicholas-fedor

Copy link
Copy Markdown
Owner

@all-contributors add @barnabasbusa for code and documentation

@allcontributors

Copy link
Copy Markdown
Contributor

@nicholas-fedor

I've put up a pull request to add @barnabasbusa! 🎉

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants