From cdc779030fb3943673e6361c8cb684bb37017897 Mon Sep 17 00:00:00 2001 From: Alex Gustafsson Date: Sun, 12 Jan 2025 11:59:24 +0100 Subject: [PATCH] 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 | 14 +++++-- internal/platform/docker/platform.go | 56 ++++++++++++++++++++++++++- internal/platform/docker/resources.go | 11 +++++- internal/worker/worker.go | 5 ++- web/graph.tsx | 3 ++ 5 files changed, 81 insertions(+), 8 deletions(-) diff --git a/cmd/cupdate/main.go b/cmd/cupdate/main.go index 5d7c408..8932233 100644 --- a/cmd/cupdate/main.go +++ b/cmd/cupdate/main.go @@ -295,7 +295,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) @@ -316,6 +315,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", @@ -328,8 +330,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 { @@ -347,8 +349,12 @@ func main() { } if childNode != nil { - resource := (*childNode).(kubernetes.Resource) - tags = append(tags, kubernetes.TagName(resource.Kind())) + switch resource := (*childNode).(type) { + case kubernetes.Resource: + 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 452a4c1..509e0bf 100644 --- a/internal/platform/docker/platform.go +++ b/internal/platform/docker/platform.go @@ -194,7 +194,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, }, @@ -203,7 +215,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 @@ -214,6 +265,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 a159e7e..d0c512d 100644 --- a/web/graph.tsx +++ b/web/graph.tsx @@ -46,6 +46,9 @@ const titles: Record | undefined> = { }, docker: { container: 'Container', + 'swarm/task': 'Task', + 'swarm/service': 'Service', + 'swarm/namespace': 'Namespace', }, }