diff --git a/frontend-modern/src/components/Docker/UpdateBadge.tsx b/frontend-modern/src/components/Docker/UpdateBadge.tsx index f72d35024..902205365 100644 --- a/frontend-modern/src/components/Docker/UpdateBadge.tsx +++ b/frontend-modern/src/components/Docker/UpdateBadge.tsx @@ -58,11 +58,20 @@ export const UpdateBadge: Component = (props) => { export const UpdateIcon: Component<{ updateStatus?: DockerContainerUpdateStatus }> = (props) => { const hasUpdate = () => props.updateStatus?.updateAvailable === true; + const getTooltip = () => { + if (!props.updateStatus) return 'Image update available'; + + const current = props.updateStatus.currentDigest?.slice(0, 12) || 'unknown'; + const latest = props.updateStatus.latestDigest?.slice(0, 12) || 'unknown'; + + return `Update available\nCurrent: ${current}...\nLatest: ${latest}...`; + }; + return ( diff --git a/internal/alerts/alerts.go b/internal/alerts/alerts.go index 4ca3e75a2..a317ad0d2 100644 --- a/internal/alerts/alerts.go +++ b/internal/alerts/alerts.go @@ -4491,6 +4491,14 @@ func (m *Manager) cleanupDockerContainerAlerts(host models.DockerHost, seen map[ } } } + // Cleanup update tracking for removed containers + for resourceID := range m.dockerUpdateFirstSeen { + if strings.HasPrefix(resourceID, prefix) { + if _, exists := seen[resourceID]; !exists { + delete(m.dockerUpdateFirstSeen, resourceID) + } + } + } m.mu.Unlock() for _, alertID := range toClear { @@ -4523,6 +4531,11 @@ func (m *Manager) clearDockerHostContainerAlerts(hostID string) { delete(m.dockerLastExitCode, resourceID) } } + for resourceID := range m.dockerUpdateFirstSeen { + if strings.HasPrefix(resourceID, prefix) { + delete(m.dockerUpdateFirstSeen, resourceID) + } + } m.mu.Unlock() for _, alertID := range toClear { diff --git a/internal/dockeragent/registry.go b/internal/dockeragent/registry.go index 38a1ff586..c1c0c7268 100644 --- a/internal/dockeragent/registry.go +++ b/internal/dockeragent/registry.go @@ -272,41 +272,51 @@ func (r *RegistryChecker) getAuthToken(ctx context.Context, registry, repository // Docker Hub requires auth token even for public images if registry == "registry-1.docker.io" { tokenURL := fmt.Sprintf("https://auth.docker.io/token?service=registry.docker.io&scope=repository:%s:pull", repository) + return r.fetchAuthToken(ctx, tokenURL) + } - req, err := http.NewRequestWithContext(ctx, http.MethodGet, tokenURL, nil) - if err != nil { - return "", err - } - - resp, err := r.httpClient.Do(req) - if err != nil { - return "", err - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return "", fmt.Errorf("token request failed: %d", resp.StatusCode) - } - - body, err := io.ReadAll(resp.Body) - if err != nil { - return "", err - } - - var tokenResp struct { - Token string `json:"token"` - } - if err := json.Unmarshal(body, &tokenResp); err != nil { - return "", err - } - - return tokenResp.Token, nil + // GitHub Container Registry (ghcr.io) requires auth token for public images + if registry == "ghcr.io" { + tokenURL := fmt.Sprintf("https://ghcr.io/token?service=ghcr.io&scope=repository:%s:pull", repository) + return r.fetchAuthToken(ctx, tokenURL) } // For other registries, try anonymous access first return "", nil } +// fetchAuthToken fetches an auth token from a token endpoint. +func (r *RegistryChecker) fetchAuthToken(ctx context.Context, tokenURL string) (string, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, tokenURL, nil) + if err != nil { + return "", err + } + + resp, err := r.httpClient.Do(req) + if err != nil { + return "", err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("token request failed: %d", resp.StatusCode) + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return "", err + } + + var tokenResp struct { + Token string `json:"token"` + } + if err := json.Unmarshal(body, &tokenResp); err != nil { + return "", err + } + + return tokenResp.Token, nil +} + func (r *RegistryChecker) getCached(key string) *cacheEntry { r.cache.mu.RLock() defer r.cache.mu.RUnlock()