From aa17de7110441a27d1dd0a99dad92805be8a6d10 Mon Sep 17 00:00:00 2001 From: Alex Gustafsson Date: Sun, 12 Jan 2025 11:28:05 +0100 Subject: [PATCH 01/12] Ensure no duplicate tags are created --- internal/worker/worker.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/worker/worker.go b/internal/worker/worker.go index 5faca1f..57b304a 100644 --- a/internal/worker/worker.go +++ b/internal/worker/worker.go @@ -112,9 +112,9 @@ func (w *Worker) ProcessRawImage(ctx context.Context, reference oci.Reference) e // Add some basic tags if data.LatestReference != nil { if data.ImageReference.String() == data.LatestReference.String() { - data.Tags = append(data.Tags, "up-to-date") + data.InsertTag("up-to-date") } else { - data.Tags = append(data.Tags, "outdated") + data.InsertTag("outdated") } // Add tags based on version diff @@ -123,7 +123,7 @@ func (w *Worker) ProcessRawImage(ctx context.Context, reference oci.Reference) e if currentVersion != nil && currentVersionErr == nil && newVersion != nil && newVersionErr == nil { diff := currentVersion.Diff(newVersion) if diff != "" { - data.Tags = append(data.Tags, diff) + data.InsertTag(diff) } versionDiffSortable = semver.PackInt64(newVersion) - semver.PackInt64(currentVersion) From 020c4d66849d8209134d8e4b18297eb514186ccc Mon Sep 17 00:00:00 2001 From: Alex Gustafsson Date: Sun, 12 Jan 2025 11:28:19 +0100 Subject: [PATCH 02/12] Add Kubernetes namespaces as tags --- internal/worker/worker.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/internal/worker/worker.go b/internal/worker/worker.go index 57b304a..da6f666 100644 --- a/internal/worker/worker.go +++ b/internal/worker/worker.go @@ -8,6 +8,7 @@ import ( "github.com/AlexGustafsson/cupdate/internal/httputil" "github.com/AlexGustafsson/cupdate/internal/models" "github.com/AlexGustafsson/cupdate/internal/oci" + "github.com/AlexGustafsson/cupdate/internal/platform/kubernetes" "github.com/AlexGustafsson/cupdate/internal/semver" "github.com/AlexGustafsson/cupdate/internal/store" "github.com/AlexGustafsson/cupdate/internal/workflow/imageworkflow" @@ -130,6 +131,18 @@ func (w *Worker) ProcessRawImage(ctx context.Context, reference oci.Reference) e } } + // Add Kubernetes namespace and Docker stack tags + for _, node := range image.Graph.Nodes { + switch node.Domain { + case "kubernetes": + if node.Type == kubernetes.ResourceKindCoreV1Namespace { + data.InsertTag(node.Name) + } + case "docker": + // Not implemented + } + } + result := models.Image{ Reference: data.ImageReference.String(), Created: data.Created, From fc61c03b4c814ba9dfd72eb08af153182e94b845 Mon Sep 17 00:00:00 2001 From: Alex Gustafsson Date: Sun, 12 Jan 2025 11:59:24 +0100 Subject: [PATCH 03/12] Improve support for Docker Swarm - Identify tags even when Docker Swarm uses a digest image - Graph Docker Swarm tasks, services and namespaces - Add Docker Swarm namespace as a tag --- cmd/cupdate/main.go | 18 ++++++--- internal/platform/docker/platform.go | 56 ++++++++++++++++++++++++++- internal/platform/docker/resources.go | 11 +++++- internal/worker/worker.go | 5 ++- web/graph.tsx | 3 ++ 5 files changed, 83 insertions(+), 10 deletions(-) diff --git a/cmd/cupdate/main.go b/cmd/cupdate/main.go index 5c4e5a9..412c0c0 100644 --- a/cmd/cupdate/main.go +++ b/cmd/cupdate/main.go @@ -303,7 +303,6 @@ func main() { edges := subgraph.Edges() nodes := subgraph.Nodes() - // TODO: Rewrite to be more generic (to include Docker?) var namespaceNode *platform.Node mappedNodes := make(map[string]models.GraphNode) @@ -324,6 +323,9 @@ func main() { Type: string(n.Kind()), Name: n.Name(), } + if node.Type() == "docker/"+docker.ResourceKindSwarmNamespace { + namespaceNode = &node + } case platform.ImageNode: mappedNodes[node.ID()] = models.GraphNode{ Domain: "oci", @@ -336,8 +338,8 @@ func main() { } tags := []string{} + // Set tags for resources - // TODO: Handle for docker as well? if namespaceNode != nil { children := edges[(*namespaceNode).ID()] for childID, isParent := range children { @@ -355,10 +357,14 @@ func main() { } if childNode != nil { - resource := (*childNode).(kubernetes.Resource) - kind := resource.Kind() - if kind.IsSupported() { - tags = append(tags, kubernetes.TagName(resource.Kind())) + switch resource := (*childNode).(type) { + case kubernetes.Resource: + kind := resource.Kind() + if kind.IsSupported() { + tags = append(tags, kubernetes.TagName(resource.Kind())) + } + case docker.Resource: + tags = append(tags, docker.TagName(resource.Kind())) } } } diff --git a/internal/platform/docker/platform.go b/internal/platform/docker/platform.go index 61404bc..d1a56f8 100644 --- a/internal/platform/docker/platform.go +++ b/internal/platform/docker/platform.go @@ -217,7 +217,19 @@ func (p *Platform) Graph(ctx context.Context) (*graph.Graph[platform.Node], erro continue } - graph.InsertTree( + // Docker Swarm has a preference for digested names, even when started with + // a manifest referencing a tag, try to resolve the reference + if !reference.HasTag && reference.HasDigest { + r, _, ok := strings.Cut(container.Image, "@") + if ok { + ref, err := oci.ParseReference(r) + if err == nil { + reference = ref + } + } + } + + tree := []platform.Node{ platform.ImageNode{ Reference: reference, }, @@ -226,7 +238,46 @@ func (p *Platform) Graph(ctx context.Context) (*graph.Graph[platform.Node], erro id: fmt.Sprintf("docker/containers/%s", container.ID), name: container.Name(), }, - ) + } + + // Add graph nodes for Docker Swarm, if available + if container.Labels != nil { + if taskID, ok := container.Labels["com.docker.swarm.task.id"]; ok { + taskName, ok := container.Labels["com.docker.swarm.task.name"] + if !ok { + taskName = taskID + } + + tree = append(tree, resource{ + kind: ResourceKindSwarmTask, + id: fmt.Sprintf("docker/swarm/task/%s", taskID), + name: taskName, + }) + } + + if serviceID, ok := container.Labels["com.docker.swarm.service.id"]; ok { + serviceName, ok := container.Labels["com.docker.swarm.service.name"] + if !ok { + serviceName = serviceID + } + + tree = append(tree, resource{ + kind: ResourceKindSwarmService, + id: fmt.Sprintf("docker/swarm/service/%s", serviceID), + name: serviceName, + }) + } + + if namespace, ok := container.Labels["com.docker.stack.namespace"]; ok { + tree = append(tree, resource{ + kind: ResourceKindSwarmNamespace, + id: fmt.Sprintf("docker/swarm/namespace/%s", namespace), + name: namespace, + }) + } + } + + graph.InsertTree(tree...) } return graph, nil @@ -237,6 +288,7 @@ type Container struct { Names []string Image string ImageID string + Labels map[string]string // ... other ignored fields } diff --git a/internal/platform/docker/resources.go b/internal/platform/docker/resources.go index 0382969..43ef82b 100644 --- a/internal/platform/docker/resources.go +++ b/internal/platform/docker/resources.go @@ -9,7 +9,10 @@ import ( type ResourceKind string const ( - ResourceKindContainer = "container" + ResourceKindContainer = "container" + ResourceKindSwarmTask = "swarm/task" + ResourceKindSwarmService = "swarm/service" + ResourceKindSwarmNamespace = "swarm/namespace" ) type Resource interface { @@ -49,6 +52,12 @@ func TagName(kind ResourceKind) string { switch kind { case ResourceKindContainer: return "container" + case ResourceKindSwarmTask: + return "task" + case ResourceKindSwarmService: + return "service" + case ResourceKindSwarmNamespace: + return "namespace" default: // Panic as missing entries would be a programming issue, not runtime // bug diff --git a/internal/worker/worker.go b/internal/worker/worker.go index da6f666..14bf56a 100644 --- a/internal/worker/worker.go +++ b/internal/worker/worker.go @@ -8,6 +8,7 @@ import ( "github.com/AlexGustafsson/cupdate/internal/httputil" "github.com/AlexGustafsson/cupdate/internal/models" "github.com/AlexGustafsson/cupdate/internal/oci" + "github.com/AlexGustafsson/cupdate/internal/platform/docker" "github.com/AlexGustafsson/cupdate/internal/platform/kubernetes" "github.com/AlexGustafsson/cupdate/internal/semver" "github.com/AlexGustafsson/cupdate/internal/store" @@ -139,7 +140,9 @@ func (w *Worker) ProcessRawImage(ctx context.Context, reference oci.Reference) e data.InsertTag(node.Name) } case "docker": - // Not implemented + if node.Type == docker.ResourceKindSwarmNamespace { + data.InsertTag(node.Name) + } } } diff --git a/web/graph.tsx b/web/graph.tsx index 3f33d71..f248729 100644 --- a/web/graph.tsx +++ b/web/graph.tsx @@ -47,6 +47,9 @@ const titles: Record | undefined> = { }, docker: { container: 'Container', + 'swarm/task': 'Task', + 'swarm/service': 'Service', + 'swarm/namespace': 'Namespace', }, } From 1ac2fbbef0a30a06e44258f96c8b40e20e7294a3 Mon Sep 17 00:00:00 2001 From: Alex Gustafsson Date: Sun, 12 Jan 2025 12:10:26 +0100 Subject: [PATCH 04/12] Namespace namespace tags --- internal/worker/worker.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/worker/worker.go b/internal/worker/worker.go index 14bf56a..191c271 100644 --- a/internal/worker/worker.go +++ b/internal/worker/worker.go @@ -137,11 +137,11 @@ func (w *Worker) ProcessRawImage(ctx context.Context, reference oci.Reference) e switch node.Domain { case "kubernetes": if node.Type == kubernetes.ResourceKindCoreV1Namespace { - data.InsertTag(node.Name) + data.InsertTag("namespace:" + node.Name) } case "docker": if node.Type == docker.ResourceKindSwarmNamespace { - data.InsertTag(node.Name) + data.InsertTag("namespace:" + node.Name) } } } From e54ebcce4a1f98e436e9dd112208a362e51fdda1 Mon Sep 17 00:00:00 2001 From: Alex Gustafsson Date: Sun, 12 Jan 2025 12:18:14 +0100 Subject: [PATCH 05/12] Sort tags lexically, non-prefixed first With namespaces tags added, the list of tags can be quite long. Make the order deterministic and prioritize the built-in tags. Place prefixed tags such as "namespace:monitoring" at the end of the list. --- web/components/TagSelect.tsx | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/web/components/TagSelect.tsx b/web/components/TagSelect.tsx index 47ed0a3..d1ee952 100644 --- a/web/components/TagSelect.tsx +++ b/web/components/TagSelect.tsx @@ -12,6 +12,17 @@ const IOS = [ 'iPod', ].includes(navigator.platform) +/** Sort tags lexically, putting prefixed tags last. */ +function sortTags(a: Tag, b: Tag): number { + if (a.name.includes(':') === b.name.includes(':')) { + return a.name.localeCompare(b.name) + } else if (a.name.includes(':')) { + return 1 + } else { + return -1 + } +} + export function TagSelect({ tags, filter, @@ -39,7 +50,7 @@ export function TagSelect({ ) } > - {tags.map((x) => ( + {tags.toSorted(sortTags).map((x) => ( @@ -97,7 +108,7 @@ export function TagSelect({ {isOpen && (
- {tags.map((x) => ( + {tags.toSorted(sortTags).map((x) => (