be3bfd0513
F1: cap the image-status cache TTL at 5m (was 24h) — the cache is keyed by the
LOCAL imageID, which doesn't change when upstream pushes a new image under the
same tag, so the 24h TTL hid new images from both the badge and the auto-update
daemon; a short TTL re-resolves the remote digest within the poll window.
F2: document that the update->rollback guard map is in-memory (restart implication).
F3: skip auto-update for an unnamed container when rollback is on (the endpoint+name
keyed guard can't record it, so it would loop) — pure skipUnnamedForRollback + test.
F4: wrap the pre-update ContainerInspect in context.WithTimeout(endpointTimeout).
F5: document Reload() does not interrupt an in-flight tick.
F6: floor auto-heal CheckInterval at 1s (mirrors auto-update) + test.
F7: wontfix — migration is currently correct; namespace rework is out of scope.
F8: correct the misleading SSRF/AllowList comment (no filter is applied).
F9: front auto-heal interval floor + test; dedup STALE_TIME; fix invalidation comment.
Also refresh three stale '24h/long-lived cache' comments to match the 5m TTL.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
85 lines
4.0 KiB
Go
85 lines
4.0 KiB
Go
package containers
|
|
|
|
import (
|
|
"net/http"
|
|
|
|
"github.com/portainer/portainer/api/docker/images"
|
|
"github.com/portainer/portainer/api/http/middlewares"
|
|
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
|
"github.com/portainer/portainer/pkg/libhttp/request"
|
|
"github.com/portainer/portainer/pkg/libhttp/response"
|
|
|
|
"github.com/rs/zerolog/log"
|
|
)
|
|
|
|
// imageStatusResponse is the body returned by the image status endpoint.
|
|
type imageStatusResponse struct {
|
|
// Status of the running container image. One of:
|
|
// "outdated", "updated", "skipped", "processing", "preparing", "error".
|
|
Status string `json:"Status"`
|
|
// Message holds an optional human-readable detail, typically the detection error.
|
|
Message string `json:"Message,omitempty"`
|
|
}
|
|
|
|
// @id ContainerImageStatus
|
|
// @summary Fetch the image status of a container
|
|
// @description Detect whether a newer image is available for the running container by
|
|
// @description comparing the local image digest against the remote registry digest.
|
|
// @description This is a read-only operation: it never pulls or recreates anything.
|
|
// @description **Access policy**: authenticated
|
|
// @tags docker
|
|
// @security ApiKeyAuth
|
|
// @security jwt
|
|
// @produce json
|
|
// @param id path int true "Environment identifier"
|
|
// @param containerId path string true "Container identifier"
|
|
// @param nodeName query string false "Node name for a Swarm/agent endpoint"
|
|
// @description Engine-level issues (container not found, registry unreachable, auth
|
|
// @description failure, ...) are not treated as API errors: they degrade gracefully to a
|
|
// @description 200 response carrying a "skipped" or "error" status. HTTP errors are only
|
|
// @description returned for request/authorization problems.
|
|
// @success 200 {object} imageStatusResponse "Image status (also returned with a skipped/error status for engine-level issues)"
|
|
// @failure 400 "Invalid request: missing container identifier"
|
|
// @failure 403 "Permission denied to access the environment"
|
|
// @failure 404 "Environment not found"
|
|
// @router /docker/{id}/containers/{containerId}/image_status [get]
|
|
func (handler *Handler) imageStatus(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
|
containerID, err := request.RetrieveRouteVariableValue(r, "containerId")
|
|
if err != nil {
|
|
return httperror.BadRequest("Invalid containerId", err)
|
|
}
|
|
|
|
// nodeName is optional and only relevant for Swarm/agent endpoints.
|
|
nodeName, _ := request.RetrieveQueryParameter(r, "nodeName", true)
|
|
|
|
endpoint, err := middlewares.FetchEndpoint(r)
|
|
if err != nil {
|
|
return httperror.NotFound("Unable to find an environment on request context", err)
|
|
}
|
|
|
|
if err := handler.bouncer.AuthorizedEndpointOperation(r, endpoint); err != nil {
|
|
return httperror.Forbidden("Permission denied to access environment", err)
|
|
}
|
|
|
|
// The detection engine (zlib/CE) routes outbound registry calls through the
|
|
// RegistryClient, which honors the encrypted credential store. It caches results
|
|
// briefly and skips digest-pinned/local-only images. Note: the outbound registry
|
|
// HEAD (RemoteDigest -> docker.GetDigest) is NOT run through an SSRF/AllowList
|
|
// filter; this mirrors upstream ContainersImageStatus behaviour.
|
|
digestClient := images.NewClientWithRegistry(images.NewRegistryClient(handler.dataStore), handler.dockerClientFactory)
|
|
|
|
status, err := digestClient.ContainerImageStatus(r.Context(), containerID, endpoint, nodeName)
|
|
if err != nil {
|
|
// A detection failure (registry unreachable, auth failure, ...) is not an API
|
|
// failure: degrade gracefully with a 200 + "error" status so the UI can render a
|
|
// neutral badge instead of surfacing a hard error. The raw error is logged
|
|
// server-side only; the response carries a generic message to avoid leaking
|
|
// registry URLs or credential details to the client.
|
|
log.Warn().Err(err).Str("containerId", containerID).Msg("unable to determine container image status")
|
|
|
|
return response.JSON(w, &imageStatusResponse{Status: string(images.Error), Message: "unable to determine image status"})
|
|
}
|
|
|
|
return response.JSON(w, &imageStatusResponse{Status: string(status)})
|
|
}
|