From 8f475cbf583ef00a20f19e3fc100249a08ef0eee Mon Sep 17 00:00:00 2001 From: rcourtman Date: Tue, 14 Jul 2026 12:00:57 +0100 Subject: [PATCH] Fold runtime-key Docker URL metadata into the stable guest key URLs saved through the resource drawer historically landed in the docker store under the runtime container key, which any stable record (including an intentionally empty cleared one) outranks in the unified customUrl projection, and which orphans on container recreation. On report ingest, copy those records into the stable app-container guest key when it is missing (cleared links stay cleared), healing saves stranded before the drawer moved to the stable identity. Also read the runtime key before its copy-if-missing container-name snapshot so the freshest write wins among the docker-store fallbacks. Refs #1556 --- .../monitoring/docker_metadata_migration.go | 37 +++++ internal/monitoring/monitor.go | 11 +- internal/monitoring/monitor_docker_test.go | 132 ++++++++++++++++++ 3 files changed, 177 insertions(+), 3 deletions(-) diff --git a/internal/monitoring/docker_metadata_migration.go b/internal/monitoring/docker_metadata_migration.go index 4a8207257..955061c8b 100644 --- a/internal/monitoring/docker_metadata_migration.go +++ b/internal/monitoring/docker_metadata_migration.go @@ -245,6 +245,43 @@ func (m *Monitor) migrateCurrentDockerContainerMetadataToStableIdentities( break } } + + // URLs saved through the resource drawer historically landed in the + // docker store under the runtime container key, which any stable + // record outranks in the unified customUrl projection. Fold them + // into the stable guest key so those saves surface in the tables + // and survive container recreation. Runtime key first: it is where + // writes landed, so it is fresher than its copy-if-missing snapshot + // under the container-name key. + if m.guestMetadataStore != nil && m.dockerMetadataStore != nil && + m.guestMetadataStore.Get(stableKey) == nil { + source := m.dockerMetadataStore.Get(dockerContainerRuntimeMetadataKey(hostID, containerID)) + if source == nil { + source = m.dockerMetadataStore.Get(dockerContainerNameMetadataKey(hostID, containerName)) + } + if source != nil { + clone := &config.GuestMetadata{ + CustomURL: source.CustomURL, + Description: source.Description, + LastKnownName: containerName, + LastKnownType: "app-container", + } + if len(source.Tags) > 0 { + clone.Tags = append([]string(nil), source.Tags...) + } + if len(source.Notes) > 0 { + clone.Notes = append([]string(nil), source.Notes...) + } + if err := m.guestMetadataStore.Set(stableKey, clone); err != nil { + log.Warn(). + Err(err). + Str("dockerHostID", hostID). + Str("containerName", container.Name). + Str("containerID", container.ID). + Msg("Failed to migrate docker metadata to stable app-container guest key") + } + } + } } } } diff --git a/internal/monitoring/monitor.go b/internal/monitoring/monitor.go index 680451608..4af1dcdfd 100644 --- a/internal/monitoring/monitor.go +++ b/internal/monitoring/monitor.go @@ -5232,14 +5232,19 @@ func (m *Monitor) dockerAppContainerCustomURL( } if m.dockerMetadataStore != nil { + // The runtime key is where drawer saves used to land, so it is always at + // least as fresh as the stable name key, which only receives + // copy-if-missing snapshots of it. Prefer the runtime record while it + // exists; the stable name key covers recreated containers whose old + // runtime key no longer matches. + if meta := m.dockerMetadataStore.Get(dockerContainerRuntimeMetadataKey(hostID, containerID)); meta != nil { + return strings.TrimSpace(meta.CustomURL), true + } if stableKey := dockerContainerNameMetadataKey(hostID, resource.Name); stableKey != "" { if meta := m.dockerMetadataStore.Get(stableKey); meta != nil { return strings.TrimSpace(meta.CustomURL), true } } - if meta := m.dockerMetadataStore.Get(dockerContainerRuntimeMetadataKey(hostID, containerID)); meta != nil { - return strings.TrimSpace(meta.CustomURL), true - } } return "", false diff --git a/internal/monitoring/monitor_docker_test.go b/internal/monitoring/monitor_docker_test.go index 38362773a..acfa56f9d 100644 --- a/internal/monitoring/monitor_docker_test.go +++ b/internal/monitoring/monitor_docker_test.go @@ -508,6 +508,138 @@ func TestApplyDockerReportMigratesGuestMetadataToStableContainerName(t *testing. } } +func TestApplyDockerReportFoldsRuntimeKeyDockerMetadataIntoStableGuestKey(t *testing.T) { + monitor := newTestMonitor(t) + + baseTimestamp := time.Now().UTC() + report := agentsdocker.Report{ + Agent: agentsdocker.AgentInfo{ + ID: "agent-runtime-fold", + Version: "1.0.0", + IntervalSeconds: 30, + }, + Host: agentsdocker.HostInfo{ + Hostname: "docker-host-runtime-fold", + MachineID: "machine-runtime-fold", + }, + Containers: []agentsdocker.Container{ + {ID: "container-wud", Name: "/wud"}, + }, + Timestamp: baseTimestamp, + } + + host, err := monitor.ApplyDockerReport(report, nil) + if err != nil { + t.Fatalf("first ApplyDockerReport failed: %v", err) + } + + // A URL saved through the resource drawer historically landed on the + // runtime container key in the docker store. + if err := monitor.dockerMetadataStore.Set(host.ID+":container:container-wud", &config.DockerMetadata{ + CustomURL: "https://wud.internal", + }); err != nil { + t.Fatalf("seed docker metadata: %v", err) + } + + report.Timestamp = baseTimestamp.Add(30 * time.Second) + if _, err := monitor.ApplyDockerReport(report, nil); err != nil { + t.Fatalf("second ApplyDockerReport failed: %v", err) + } + + stableKey := dockerAppContainerMetadataKey(host.ID, "wud") + stableMeta := monitor.guestMetadataStore.Get(stableKey) + if stableMeta == nil { + t.Fatalf("expected runtime docker metadata folded into stable guest key %q", stableKey) + } + if stableMeta.CustomURL != "https://wud.internal" { + t.Fatalf("stable guest metadata URL = %q, want https://wud.internal", stableMeta.CustomURL) + } + + resources := []unifiedresources.Resource{ + { + ID: dockerAppContainerLegacyResourceID(host.ID, "container-wud"), + Type: unifiedresources.ResourceTypeAppContainer, + Name: "wud", + Docker: &unifiedresources.DockerData{ + HostSourceID: host.ID, + ContainerID: "container-wud", + }, + }, + } + got := monitor.applyDockerMetadataToUnifiedResources(resources) + if got[0].CustomURL != "https://wud.internal" { + t.Fatalf("projected CustomURL = %q, want https://wud.internal", got[0].CustomURL) + } +} + +func TestApplyDockerReportKeepsClearedStableGuestKeyOverRuntimeDockerMetadata(t *testing.T) { + monitor := newTestMonitor(t) + + baseTimestamp := time.Now().UTC() + report := agentsdocker.Report{ + Agent: agentsdocker.AgentInfo{ + ID: "agent-cleared-stable", + Version: "1.0.0", + IntervalSeconds: 30, + }, + Host: agentsdocker.HostInfo{ + Hostname: "docker-host-cleared-stable", + MachineID: "machine-cleared-stable", + }, + Containers: []agentsdocker.Container{ + {ID: "container-cleared", Name: "cleared"}, + }, + Timestamp: baseTimestamp, + } + + host, err := monitor.ApplyDockerReport(report, nil) + if err != nil { + t.Fatalf("first ApplyDockerReport failed: %v", err) + } + + // A user cleared the link: the stable guest key intentionally holds an + // empty record. A stale runtime-key docker record must not overwrite or + // outrank it. + stableKey := dockerAppContainerMetadataKey(host.ID, "cleared") + if err := monitor.guestMetadataStore.Set(stableKey, &config.GuestMetadata{ + CustomURL: "", + LastKnownName: "cleared", + LastKnownType: "app-container", + }); err != nil { + t.Fatalf("seed cleared guest metadata: %v", err) + } + if err := monitor.dockerMetadataStore.Set(host.ID+":container:container-cleared", &config.DockerMetadata{ + CustomURL: "https://stale.internal", + }); err != nil { + t.Fatalf("seed docker metadata: %v", err) + } + + report.Timestamp = baseTimestamp.Add(30 * time.Second) + if _, err := monitor.ApplyDockerReport(report, nil); err != nil { + t.Fatalf("second ApplyDockerReport failed: %v", err) + } + + if meta := monitor.guestMetadataStore.Get(stableKey); meta == nil || meta.CustomURL != "" { + t.Fatalf("cleared stable guest metadata was overwritten: %#v", meta) + } + + resources := []unifiedresources.Resource{ + { + ID: dockerAppContainerLegacyResourceID(host.ID, "container-cleared"), + Type: unifiedresources.ResourceTypeAppContainer, + Name: "cleared", + Docker: &unifiedresources.DockerData{ + HostSourceID: host.ID, + ContainerID: "container-cleared", + }, + }, + } + got := monitor.applyDockerMetadataToUnifiedResources(resources) + if got[0].CustomURL != "" { + t.Fatalf("projected CustomURL = %q, want empty (cleared)", got[0].CustomURL) + } +} + func TestApplyDockerReportSkipsMetadataMigrationForAmbiguousContainerNames(t *testing.T) { monitor := newTestMonitor(t)