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)