From 4ca3afba84dc2ea472e4920c9dd2f74dd093cec4 Mon Sep 17 00:00:00 2001 From: guoyuqi Date: Wed, 4 Nov 2026 18:29:24 +0800 Subject: [PATCH] fix(docker): check image update --- agent/app/service/container_image_check_update.go | 13 ++++++++++++- agent/utils/dockerhub/client.go | 14 ++++++++++---- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/agent/app/service/container_image_check_update.go b/agent/app/service/container_image_check_update.go index df4e53085..a30535f02 100644 --- a/agent/app/service/container_image_check_update.go +++ b/agent/app/service/container_image_check_update.go @@ -299,8 +299,19 @@ func checkContainerImageUpdate(ctx context.Context, cli imageCheckUpdateDockerCl record.LatestVersion = remote.target.Tag record.TargetImage = "docker.io/" + repository + ":" + remote.target.Tag record.TargetImageID = remote.target.ConfigDigest + sameImage := inspected.ID == remote.target.ConfigDigest + if inspected.Descriptor != nil { + // The containerd store uses the pulled index/manifest as its image ID, + // not the config digest. Persist the ID that pulling this tag will produce + // so post-upgrade reconciliation uses the same identity as Docker. + if remote.target.ReferenceDigest == "" { + return record, fmt.Errorf("Docker Hub manifest response is missing its digest") + } + record.TargetImageID = remote.target.ReferenceDigest + sameImage = inspected.ID == remote.target.ReferenceDigest || inspected.ID == remote.target.Digest + } // Both versioned and floating tags can be republished with new image content. - record.HasUpdate = remote.target.Tag != tag || remote.target.ConfigDigest != inspected.ID + record.HasUpdate = remote.target.Tag != tag || !sameImage return record, nil } return record, fmt.Errorf("no compatible image for %s/%s", platform.OS, platform.Architecture) diff --git a/agent/utils/dockerhub/client.go b/agent/utils/dockerhub/client.go index 2d66a49f1..c7a28245b 100644 --- a/agent/utils/dockerhub/client.go +++ b/agent/utils/dockerhub/client.go @@ -42,7 +42,12 @@ type Client struct { } type Platform struct{ OS, Architecture, Variant string } -type Target struct{ Tag, Digest, ConfigDigest string } +type Target struct { + Tag string + Digest string // Platform-specific manifest digest. + ReferenceDigest string // Tag target: an image index or a single-platform manifest. + ConfigDigest string // Image configuration digest used by the classic Docker image store. +} func New(ctx context.Context, repository, username, password string, httpClient *http.Client) (*Client, error) { if httpClient == nil { @@ -151,7 +156,8 @@ func (c *Client) Resolve(ctx context.Context, tag string, platform Platform) (Ta if err != nil { return Target{}, false, err } - digest := headers.Get("Docker-Content-Digest") + referenceDigest := headers.Get("Docker-Content-Digest") + digest := referenceDigest if len(m.Manifests) > 0 { digest = "" for _, item := range m.Manifests { @@ -171,7 +177,7 @@ func (c *Client) Resolve(ctx context.Context, tag string, platform Platform) (Ta if m.Config.Digest == "" { return Target{}, false, fmt.Errorf("unsupported Docker Hub manifest") } - // Validate single-platform manifests too; the config digest is the Docker Image ID. + // Validate single-platform manifests too; classic Docker image IDs use the config digest. var config struct { OS string `json:"os"` Architecture string `json:"architecture"` @@ -183,7 +189,7 @@ func (c *Client) Resolve(ctx context.Context, tag string, platform Platform) (Ta if config.OS != platform.OS || config.Architecture != platform.Architecture || (platform.Variant != "" && config.Variant != "" && config.Variant != platform.Variant) { return Target{}, false, nil } - return Target{Tag: tag, Digest: digest, ConfigDigest: m.Config.Digest}, true, nil + return Target{Tag: tag, Digest: digest, ReferenceDigest: referenceDigest, ConfigDigest: m.Config.Digest}, true, nil } var versionPattern = regexp.MustCompile(`^v?(\d+)\.(\d+)(?:\.(\d+))?(-[a-zA-Z][a-zA-Z0-9_.-]*)?$`)