mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-26 21:47:20 +00:00
fb7dd955ed
Normalization keeps a disk's last-known temperature when the current observation did not collect one (a disk in standby, a host agent past its reporting lease) and marks it non-available. Several readers still took PhysicalDisk.Temperature at face value. The AI chat context listed a standby disk as needing attention for heat. Patrol raised health issues and triage flags from the retained value. The AI query and disk-health tools handed it to the model as the disk's temperature. Performance reports and the reporting runtime snapshot tabulated it as measured, and the disk temperature charts padded a series out to now with it. Each of them now goes through diskinventory.TemperatureCollected. AI surfaces keep a retained value as context, labelled last known with its reason. Reports and the charts leave it out. monitor.go's private copy of the predicate is replaced by the shared helper.
1083 lines
36 KiB
Go
1083 lines
36 KiB
Go
package ai
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/agentexec"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/truenas"
|
|
unifiedresources "github.com/rcourtman/pulse-go-rewrite/internal/unifiedresources"
|
|
"github.com/rcourtman/pulse-go-rewrite/pkg/diskinventory"
|
|
)
|
|
|
|
func TestBuildUnifiedResourceContext_NilProvider(t *testing.T) {
|
|
s := &Service{}
|
|
if got := s.buildUnifiedResourceContext(); got != "" {
|
|
t.Errorf("expected empty context, got %q", got)
|
|
}
|
|
}
|
|
|
|
func TestBuildUnifiedResourceContext_FullContext(t *testing.T) {
|
|
nodeWithAgent := unifiedresources.Resource{
|
|
ID: "node-1",
|
|
Name: "pve-node",
|
|
Type: unifiedresources.ResourceTypeAgent,
|
|
Status: unifiedresources.StatusOnline,
|
|
Identity: unifiedresources.ResourceIdentity{
|
|
ClusterName: "cluster-a",
|
|
},
|
|
Metrics: &unifiedresources.ResourceMetrics{
|
|
CPU: &unifiedresources.MetricValue{Percent: 12.3},
|
|
Memory: &unifiedresources.MetricValue{Percent: 45.6},
|
|
},
|
|
Proxmox: &unifiedresources.ProxmoxData{
|
|
NodeName: "pve-node",
|
|
ClusterName: "cluster-a",
|
|
},
|
|
}
|
|
nodeNoAgent := unifiedresources.Resource{
|
|
ID: "node-2",
|
|
Name: "minipc",
|
|
Type: unifiedresources.ResourceTypeAgent,
|
|
Status: unifiedresources.StatusWarning,
|
|
Proxmox: &unifiedresources.ProxmoxData{NodeName: "minipc"},
|
|
}
|
|
dockerNode := unifiedresources.Resource{
|
|
ID: "dock-node",
|
|
Name: "dock-node",
|
|
Type: unifiedresources.ResourceTypeAgent,
|
|
Status: unifiedresources.StatusOnline,
|
|
Docker: &unifiedresources.DockerData{Hostname: "dock-node"},
|
|
}
|
|
host := unifiedresources.Resource{
|
|
ID: "host-1",
|
|
Name: "barehost",
|
|
Type: unifiedresources.ResourceTypeAgent,
|
|
Status: unifiedresources.StatusOnline,
|
|
Identity: unifiedresources.ResourceIdentity{
|
|
IPAddresses: []string{"192.168.1.10"},
|
|
},
|
|
Metrics: &unifiedresources.ResourceMetrics{
|
|
CPU: &unifiedresources.MetricValue{Percent: 5},
|
|
Memory: &unifiedresources.MetricValue{Percent: 10},
|
|
},
|
|
Agent: &unifiedresources.AgentData{
|
|
Hostname: "barehost",
|
|
},
|
|
}
|
|
dockerHost := unifiedresources.Resource{
|
|
ID: "docker-1",
|
|
Name: "dockhost",
|
|
Type: unifiedresources.ResourceTypeAgent,
|
|
Status: unifiedresources.StatusOnline,
|
|
Docker: &unifiedresources.DockerData{Hostname: "dockhost"},
|
|
}
|
|
|
|
nodeWithAgentID := nodeWithAgent.ID
|
|
dockerHostID := dockerHost.ID
|
|
unknownParentID := "unknown-parent"
|
|
vm := unifiedresources.Resource{
|
|
ID: "vm-100",
|
|
Name: "web-vm",
|
|
Type: unifiedresources.ResourceTypeVM,
|
|
Tags: []string{"database"},
|
|
ParentID: &nodeWithAgentID,
|
|
Status: unifiedresources.StatusOnline,
|
|
Identity: unifiedresources.ResourceIdentity{
|
|
IPAddresses: []string{"10.0.0.1", "10.0.0.2", "10.0.0.3"},
|
|
},
|
|
Metrics: &unifiedresources.ResourceMetrics{
|
|
CPU: &unifiedresources.MetricValue{Percent: 65.4},
|
|
Memory: &unifiedresources.MetricValue{Percent: 70.2},
|
|
},
|
|
Proxmox: &unifiedresources.ProxmoxData{
|
|
VMID: 100,
|
|
},
|
|
}
|
|
ct := unifiedresources.Resource{
|
|
ID: "ct-200",
|
|
Name: "db-ct",
|
|
Type: unifiedresources.ResourceTypeSystemContainer,
|
|
Tags: []string{"database"},
|
|
ParentID: &nodeWithAgentID,
|
|
Status: unifiedresources.StatusOffline,
|
|
Proxmox: &unifiedresources.ProxmoxData{
|
|
VMID: 200,
|
|
},
|
|
}
|
|
dockerContainer := unifiedresources.Resource{
|
|
ID: "dock-300",
|
|
Name: "redis",
|
|
Type: unifiedresources.ResourceTypeAppContainer,
|
|
Tags: []string{"database"},
|
|
ParentID: &dockerHostID,
|
|
Status: unifiedresources.StatusOnline,
|
|
Metrics: &unifiedresources.ResourceMetrics{
|
|
Disk: &unifiedresources.MetricValue{Percent: 70},
|
|
},
|
|
}
|
|
dockerStopped := unifiedresources.Resource{
|
|
ID: "dock-301",
|
|
Name: "cache",
|
|
Type: unifiedresources.ResourceTypeAppContainer,
|
|
Tags: []string{"database"},
|
|
ParentID: &dockerHostID,
|
|
Status: unifiedresources.StatusOffline,
|
|
}
|
|
unknownParent := unifiedresources.Resource{
|
|
ID: "vm-999",
|
|
Name: "mystery",
|
|
Type: unifiedresources.ResourceTypeVM,
|
|
Tags: []string{"database"},
|
|
ParentID: &unknownParentID,
|
|
Status: unifiedresources.StatusOnline,
|
|
Proxmox: &unifiedresources.ProxmoxData{
|
|
VMID: 999,
|
|
},
|
|
}
|
|
orphan := unifiedresources.Resource{
|
|
ID: "orphan-1",
|
|
Name: "orphan",
|
|
Type: unifiedresources.ResourceTypeSystemContainer,
|
|
Tags: []string{"database"},
|
|
Status: unifiedresources.StatusOnline,
|
|
Identity: unifiedresources.ResourceIdentity{
|
|
IPAddresses: []string{"172.16.0.5"},
|
|
},
|
|
}
|
|
|
|
infrastructure := []unifiedresources.Resource{nodeWithAgent, nodeNoAgent, host, dockerHost, dockerNode}
|
|
workloads := []unifiedresources.Resource{vm, ct, dockerContainer, dockerStopped, unknownParent, orphan}
|
|
all := append(append([]unifiedresources.Resource{}, infrastructure...), workloads...)
|
|
|
|
stats := unifiedresources.ResourceStats{
|
|
Total: len(all),
|
|
ByType: map[unifiedresources.ResourceType]int{
|
|
unifiedresources.ResourceTypeAgent: 5,
|
|
unifiedresources.ResourceTypeVM: 2,
|
|
unifiedresources.ResourceTypeSystemContainer: 2,
|
|
unifiedresources.ResourceTypeAppContainer: 2,
|
|
},
|
|
ByStatus: map[unifiedresources.ResourceStatus]int{
|
|
unifiedresources.StatusOnline: 7,
|
|
unifiedresources.StatusWarning: 1,
|
|
unifiedresources.StatusOffline: 3,
|
|
},
|
|
BySource: map[unifiedresources.DataSource]int{
|
|
unifiedresources.SourceProxmox: 4,
|
|
unifiedresources.SourceAgent: 1,
|
|
unifiedresources.SourceDocker: 2,
|
|
},
|
|
}
|
|
|
|
mockURP := &mockUnifiedResourceProvider{
|
|
getStatsFunc: func() unifiedresources.ResourceStats {
|
|
return stats
|
|
},
|
|
getInfrastructureFunc: func() []unifiedresources.Resource {
|
|
return infrastructure
|
|
},
|
|
getWorkloadsFunc: func() []unifiedresources.Resource {
|
|
return workloads
|
|
},
|
|
getAllFunc: func() []unifiedresources.Resource {
|
|
return all
|
|
},
|
|
getTopCPUFunc: func(limit int, types []unifiedresources.ResourceType) []unifiedresources.Resource {
|
|
return []unifiedresources.Resource{vm}
|
|
},
|
|
getTopMemoryFunc: func(limit int, types []unifiedresources.ResourceType) []unifiedresources.Resource {
|
|
return []unifiedresources.Resource{host}
|
|
},
|
|
getTopDiskFunc: func(limit int, types []unifiedresources.ResourceType) []unifiedresources.Resource {
|
|
return []unifiedresources.Resource{dockerContainer}
|
|
},
|
|
}
|
|
|
|
s := &Service{
|
|
unifiedResourceProvider: mockURP,
|
|
alertProvider: &resourceContextAlertProvider{
|
|
active: []AlertInfo{
|
|
{
|
|
ResourceID: vm.ID,
|
|
ResourceName: vm.Name,
|
|
Message: "CPU high",
|
|
Level: "critical",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
s.agentServer = &mockAgentServer{
|
|
agents: []agentexec.ConnectedAgent{
|
|
{AgentID: "agent-1", Hostname: "pve-node"},
|
|
},
|
|
}
|
|
|
|
got := s.buildUnifiedResourceContext()
|
|
if got == "" {
|
|
t.Fatal("expected non-empty context")
|
|
}
|
|
|
|
assertContains := func(substr string) {
|
|
t.Helper()
|
|
if !strings.Contains(got, substr) {
|
|
t.Fatalf("expected context to contain %q", substr)
|
|
}
|
|
}
|
|
|
|
assertContains("## Unified Infrastructure View")
|
|
assertContains("Total resources: 11 (Infrastructure: 5, Workloads: 6)")
|
|
assertContains("Data Governance")
|
|
expectedSensitivity := strings.Join(unifiedresources.ResourcePolicySensitivitySummaryFromCounts(map[unifiedresources.ResourceSensitivity]int{
|
|
unifiedresources.ResourceSensitivityPublic: 0,
|
|
unifiedresources.ResourceSensitivityInternal: 5,
|
|
unifiedresources.ResourceSensitivitySensitive: 6,
|
|
unifiedresources.ResourceSensitivityRestricted: 0,
|
|
}), ", ")
|
|
assertContains("Sensitivity: " + expectedSensitivity)
|
|
expectedRouting := strings.Join(unifiedresources.ResourcePolicyRoutingSummaryFromCounts(map[unifiedresources.ResourceRoutingScope]int{
|
|
unifiedresources.ResourceRoutingScopeCloudSummary: 5,
|
|
unifiedresources.ResourceRoutingScopeLocalFirst: 6,
|
|
unifiedresources.ResourceRoutingScopeLocalOnly: 0,
|
|
}), ", ")
|
|
assertContains("Routing: " + expectedRouting)
|
|
assertContains("Policy Redaction Hints")
|
|
assertContains("Redactions in use: Hostname, IP Address, Platform ID, Alias")
|
|
if count := strings.Count(got, "### Policy Redaction Hints"); count != 1 {
|
|
t.Fatalf("expected one policy redaction hints section, got %d in %q", count, got)
|
|
}
|
|
assertContains("Proxmox VE Nodes")
|
|
assertContains("HAS AGENT")
|
|
assertContains("NO AGENT")
|
|
assertContains("cluster: cluster-a")
|
|
assertContains("Standalone Hosts")
|
|
assertContains("192.168.1.10")
|
|
assertContains("Docker/Podman Hosts")
|
|
assertContains("1/2 containers running")
|
|
assertContains("Workloads (VMs & Containers)")
|
|
assertContains("On pve-node")
|
|
assertContains("On unresolved parent resource")
|
|
assertContains("Other workloads")
|
|
assertContains("virtual machine resource; status online; linked to parent resource; redacted for cloud summary")
|
|
assertContains("IPs " + unifiedresources.ResourcePolicyRedactedLabel)
|
|
assertContains("Resources with Active Alerts")
|
|
assertContains("CPU high")
|
|
assertContains("Infrastructure Summary")
|
|
assertContains("Resources with alerts: 1")
|
|
assertContains("Average utilization by type")
|
|
assertContains("Top CPU Consumers")
|
|
assertContains("virtual machine resource; status online; linked to parent resource; redacted for cloud summary** (vm): 65.4%")
|
|
assertContains("Top Memory Consumers")
|
|
assertContains("Top Disk Usage")
|
|
assertContains("application container resource; status online; linked to parent resource; redacted for cloud summary** (app-container): 70.0%")
|
|
if strings.Contains(got, "web-vm") {
|
|
t.Fatalf("expected sensitive VM name to be redacted, got %q", got)
|
|
}
|
|
if strings.Contains(got, "10.0.0.1") || strings.Contains(got, "10.0.0.2") {
|
|
t.Fatalf("expected sensitive workload IPs to be redacted, got %q", got)
|
|
}
|
|
}
|
|
|
|
func TestBuildUnifiedResourceContextUsesAISafeSummaryForLocalOnlyResources(t *testing.T) {
|
|
restricted := unifiedresources.Resource{
|
|
ID: "mail-1",
|
|
Name: "customer-mail-gateway",
|
|
Type: unifiedresources.ResourceTypePMG,
|
|
Status: unifiedresources.StatusWarning,
|
|
PMG: &unifiedresources.PMGData{
|
|
Hostname: "mail-gateway.internal",
|
|
},
|
|
}
|
|
unifiedresources.RefreshCanonicalMetadata(&restricted)
|
|
|
|
stats := unifiedresources.ResourceStats{
|
|
Total: 1,
|
|
ByType: map[unifiedresources.ResourceType]int{
|
|
unifiedresources.ResourceTypePMG: 1,
|
|
},
|
|
ByStatus: map[unifiedresources.ResourceStatus]int{
|
|
unifiedresources.StatusWarning: 1,
|
|
},
|
|
}
|
|
|
|
mockURP := &mockUnifiedResourceProvider{
|
|
getStatsFunc: func() unifiedresources.ResourceStats { return stats },
|
|
getInfrastructureFunc: func() []unifiedresources.Resource {
|
|
return []unifiedresources.Resource{restricted}
|
|
},
|
|
getAllFunc: func() []unifiedresources.Resource {
|
|
return []unifiedresources.Resource{restricted}
|
|
},
|
|
}
|
|
|
|
s := &Service{unifiedResourceProvider: mockURP}
|
|
got := s.buildUnifiedResourceContext()
|
|
|
|
if !strings.Contains(got, "mail gateway resource; status warning; local-only context") {
|
|
t.Fatalf("expected AI-safe summary in context, got %q", got)
|
|
}
|
|
if strings.Contains(got, "customer-mail-gateway") {
|
|
t.Fatalf("expected raw restricted resource name to be hidden, got %q", got)
|
|
}
|
|
}
|
|
|
|
func TestBuildUnifiedResourceContextForExternalModelOmitsLocalOnlyResourceDetails(t *testing.T) {
|
|
restricted := unifiedresources.Resource{
|
|
ID: "mail-1",
|
|
Name: "customer-mail-gateway",
|
|
Type: unifiedresources.ResourceTypePMG,
|
|
Status: unifiedresources.StatusWarning,
|
|
PMG: &unifiedresources.PMGData{
|
|
Hostname: "mail-gateway.internal",
|
|
},
|
|
}
|
|
publicNode := unifiedresources.Resource{
|
|
ID: "public-1",
|
|
Name: "public-node",
|
|
Type: unifiedresources.ResourceTypeAgent,
|
|
Status: unifiedresources.StatusOnline,
|
|
Tags: []string{"public"},
|
|
}
|
|
resources := unifiedresources.RefreshCanonicalMetadataSlice([]unifiedresources.Resource{restricted, publicNode})
|
|
|
|
stats := unifiedresources.ResourceStats{
|
|
Total: 2,
|
|
ByType: map[unifiedresources.ResourceType]int{
|
|
unifiedresources.ResourceTypePMG: 1,
|
|
unifiedresources.ResourceTypeAgent: 1,
|
|
},
|
|
ByStatus: map[unifiedresources.ResourceStatus]int{
|
|
unifiedresources.StatusWarning: 1,
|
|
unifiedresources.StatusOnline: 1,
|
|
},
|
|
}
|
|
|
|
mockURP := &mockUnifiedResourceProvider{
|
|
getStatsFunc: func() unifiedresources.ResourceStats { return stats },
|
|
getInfrastructureFunc: func() []unifiedresources.Resource {
|
|
return resources
|
|
},
|
|
getAllFunc: func() []unifiedresources.Resource {
|
|
return resources
|
|
},
|
|
getTopCPUFunc: func(limit int, types []unifiedresources.ResourceType) []unifiedresources.Resource {
|
|
return []unifiedresources.Resource{restricted}
|
|
},
|
|
}
|
|
|
|
s := &Service{
|
|
unifiedResourceProvider: mockURP,
|
|
alertProvider: &resourceContextAlertProvider{
|
|
active: []AlertInfo{
|
|
{
|
|
ResourceID: restricted.ID,
|
|
ResourceName: restricted.Name,
|
|
Message: "customer-mail-gateway at mail-gateway.internal is degraded",
|
|
Level: "warning",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
got := s.buildUnifiedResourceContextForModel("openai:gpt-4o")
|
|
|
|
for _, raw := range []string{"customer-mail-gateway", "mail-gateway.internal", "mail gateway resource; status warning; local-only context"} {
|
|
if strings.Contains(got, raw) {
|
|
t.Fatalf("expected external context to omit local-only detail %q, got %q", raw, got)
|
|
}
|
|
}
|
|
if !strings.Contains(got, "Local-only resources: 1") {
|
|
t.Fatalf("expected aggregate local-only count, got %q", got)
|
|
}
|
|
if !strings.Contains(got, "External model handling: 1 local-only resources are represented only in aggregate and omitted from detailed context.") {
|
|
t.Fatalf("expected external model handling note, got %q", got)
|
|
}
|
|
if !strings.Contains(got, "1 alerts on local-only resources omitted from external model context by policy.") {
|
|
t.Fatalf("expected local-only alert omission note, got %q", got)
|
|
}
|
|
if strings.Contains(got, "Top CPU Consumers") {
|
|
t.Fatalf("expected local-only top consumers to be omitted, got %q", got)
|
|
}
|
|
}
|
|
|
|
func TestBuildUnifiedResourceContextUsesAISafeSummaryForSensitiveResources(t *testing.T) {
|
|
sensitiveVM := unifiedresources.Resource{
|
|
ID: "prod-west:101",
|
|
Name: "finance-db",
|
|
Type: unifiedresources.ResourceTypeVM,
|
|
Tags: []string{"database"},
|
|
Status: unifiedresources.StatusOnline,
|
|
Identity: unifiedresources.ResourceIdentity{
|
|
ClusterName: "prod-west",
|
|
IPAddresses: []string{"10.10.0.5"},
|
|
},
|
|
Proxmox: &unifiedresources.ProxmoxData{
|
|
NodeName: "pve-prod",
|
|
ClusterName: "prod-west",
|
|
VMID: 101,
|
|
},
|
|
}
|
|
unifiedresources.RefreshCanonicalMetadata(&sensitiveVM)
|
|
|
|
stats := unifiedresources.ResourceStats{
|
|
Total: 1,
|
|
ByType: map[unifiedresources.ResourceType]int{
|
|
unifiedresources.ResourceTypeVM: 1,
|
|
},
|
|
ByStatus: map[unifiedresources.ResourceStatus]int{
|
|
unifiedresources.StatusOnline: 1,
|
|
},
|
|
}
|
|
|
|
mockURP := &mockUnifiedResourceProvider{
|
|
getStatsFunc: func() unifiedresources.ResourceStats { return stats },
|
|
getWorkloadsFunc: func() []unifiedresources.Resource {
|
|
return []unifiedresources.Resource{sensitiveVM}
|
|
},
|
|
getAllFunc: func() []unifiedresources.Resource {
|
|
return []unifiedresources.Resource{sensitiveVM}
|
|
},
|
|
getTopCPUFunc: func(limit int, types []unifiedresources.ResourceType) []unifiedresources.Resource {
|
|
return []unifiedresources.Resource{sensitiveVM}
|
|
},
|
|
}
|
|
|
|
s := &Service{unifiedResourceProvider: mockURP}
|
|
got := s.buildUnifiedResourceContext()
|
|
|
|
if !strings.Contains(got, "virtual machine resource; status online; redacted for cloud summary") {
|
|
t.Fatalf("expected AI-safe summary for sensitive resource, got %q", got)
|
|
}
|
|
if !strings.Contains(got, "IPs "+unifiedresources.ResourcePolicyRedactedLabel) {
|
|
t.Fatalf("expected sensitive IP redaction marker, got %q", got)
|
|
}
|
|
if strings.Contains(got, "finance-db") {
|
|
t.Fatalf("expected sensitive resource name to be hidden, got %q", got)
|
|
}
|
|
if strings.Contains(got, "10.10.0.5") {
|
|
t.Fatalf("expected sensitive IP to be hidden, got %q", got)
|
|
}
|
|
if strings.Contains(got, "prod-west") {
|
|
t.Fatalf("expected sensitive cluster name to be hidden, got %q", got)
|
|
}
|
|
}
|
|
|
|
func TestBuildUnifiedResourceContextRedactsSensitiveAlertMessageText(t *testing.T) {
|
|
sensitiveVM := unifiedresources.Resource{
|
|
ID: "prod-west:101",
|
|
Name: "finance-db",
|
|
Type: unifiedresources.ResourceTypeVM,
|
|
Tags: []string{"database"},
|
|
Status: unifiedresources.StatusWarning,
|
|
Identity: unifiedresources.ResourceIdentity{
|
|
Hostnames: []string{"finance-db.internal"},
|
|
IPAddresses: []string{"10.10.0.5"},
|
|
},
|
|
Canonical: &unifiedresources.CanonicalIdentity{
|
|
PlatformID: "prod-west:101",
|
|
Aliases: []string{"finance-db-primary"},
|
|
},
|
|
}
|
|
unifiedresources.RefreshCanonicalMetadata(&sensitiveVM)
|
|
|
|
stats := unifiedresources.ResourceStats{
|
|
Total: 1,
|
|
ByType: map[unifiedresources.ResourceType]int{
|
|
unifiedresources.ResourceTypeVM: 1,
|
|
},
|
|
ByStatus: map[unifiedresources.ResourceStatus]int{
|
|
unifiedresources.StatusWarning: 1,
|
|
},
|
|
}
|
|
|
|
mockURP := &mockUnifiedResourceProvider{
|
|
getStatsFunc: func() unifiedresources.ResourceStats { return stats },
|
|
getWorkloadsFunc: func() []unifiedresources.Resource {
|
|
return []unifiedresources.Resource{sensitiveVM}
|
|
},
|
|
getAllFunc: func() []unifiedresources.Resource {
|
|
return []unifiedresources.Resource{sensitiveVM}
|
|
},
|
|
}
|
|
|
|
s := &Service{
|
|
unifiedResourceProvider: mockURP,
|
|
alertProvider: &resourceContextAlertProvider{
|
|
active: []AlertInfo{
|
|
{
|
|
ResourceID: sensitiveVM.ID,
|
|
ResourceName: sensitiveVM.Name,
|
|
Message: "finance-db at finance-db.internal / 10.10.0.5 on prod-west:101 is degraded",
|
|
Level: "critical",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
got := s.buildUnifiedResourceContextForModel("anthropic:claude-3-5-sonnet-latest")
|
|
for _, raw := range []string{"finance-db", "finance-db.internal", "10.10.0.5", "prod-west:101"} {
|
|
if strings.Contains(got, raw) {
|
|
t.Fatalf("expected sensitive alert text to redact %q, got %q", raw, got)
|
|
}
|
|
}
|
|
if !strings.Contains(got, unifiedresources.ResourcePolicyRedactedLabel) {
|
|
t.Fatalf("expected redaction marker in alert context, got %q", got)
|
|
}
|
|
}
|
|
|
|
func TestBuildUnifiedResourceContextUsesAISafeSummaryForPathRedactedResources(t *testing.T) {
|
|
storage := unifiedresources.Resource{
|
|
ID: "storage-1",
|
|
Name: "backup-volume",
|
|
Type: unifiedresources.ResourceTypeStorage,
|
|
Status: unifiedresources.StatusOnline,
|
|
Storage: &unifiedresources.StorageMeta{
|
|
Path: "/mnt/pve/backups",
|
|
},
|
|
}
|
|
unifiedresources.RefreshCanonicalMetadata(&storage)
|
|
|
|
stats := unifiedresources.ResourceStats{
|
|
Total: 1,
|
|
ByType: map[unifiedresources.ResourceType]int{
|
|
unifiedresources.ResourceTypeStorage: 1,
|
|
},
|
|
ByStatus: map[unifiedresources.ResourceStatus]int{
|
|
unifiedresources.StatusOnline: 1,
|
|
},
|
|
}
|
|
|
|
mockURP := &mockUnifiedResourceProvider{
|
|
getStatsFunc: func() unifiedresources.ResourceStats { return stats },
|
|
getInfrastructureFunc: func() []unifiedresources.Resource {
|
|
return []unifiedresources.Resource{storage}
|
|
},
|
|
getAllFunc: func() []unifiedresources.Resource {
|
|
return []unifiedresources.Resource{storage}
|
|
},
|
|
}
|
|
|
|
s := &Service{unifiedResourceProvider: mockURP}
|
|
got := s.buildUnifiedResourceContext()
|
|
|
|
if !strings.Contains(got, "storage resource; status online; redacted for cloud summary") {
|
|
t.Fatalf("expected AI-safe summary for path-redacted resource, got %q", got)
|
|
}
|
|
if strings.Contains(got, "backup-volume") {
|
|
t.Fatalf("expected path-redacted resource name to be hidden, got %q", got)
|
|
}
|
|
}
|
|
|
|
func TestResourcePolicyHelpersShareAISafeSummaryDecision(t *testing.T) {
|
|
policy := &unifiedresources.ResourcePolicy{
|
|
Sensitivity: unifiedresources.ResourceSensitivitySensitive,
|
|
Routing: unifiedresources.ResourceRoutingPolicy{
|
|
Scope: unifiedresources.ResourceRoutingScopeLocalFirst,
|
|
Redact: []unifiedresources.ResourceRedactionHint{
|
|
unifiedresources.ResourceRedactionHostname,
|
|
unifiedresources.ResourceRedactionIPAddress,
|
|
},
|
|
},
|
|
}
|
|
|
|
if !unifiedresources.ResourcePolicyUsesAISafeSummary("virtual machine resource; status online; redacted for cloud summary", policy) {
|
|
t.Fatal("expected AI-safe summary helper to honor shared policy rules")
|
|
}
|
|
if !unifiedresources.ResourcePolicyRedacts(policy, unifiedresources.ResourceRedactionHostname) {
|
|
t.Fatal("expected hostname redaction helper to honor shared policy rules")
|
|
}
|
|
if unifiedresources.ResourcePolicyRedacts(policy, unifiedresources.ResourceRedactionAlias) {
|
|
t.Fatal("did not expect alias redaction helper to match")
|
|
}
|
|
}
|
|
|
|
func TestUnifiedResourceDisplayNameUsesSharedHelper(t *testing.T) {
|
|
if got := unifiedresources.ResourceDisplayName(unifiedresources.Resource{Name: " node-a ", ID: "id-a"}); got != "node-a" {
|
|
t.Fatalf("ResourceDisplayName() with name = %q, want node-a", got)
|
|
}
|
|
if got := unifiedresources.ResourceDisplayName(unifiedresources.Resource{Name: " ", ID: " id-b "}); got != "id-b" {
|
|
t.Fatalf("ResourceDisplayName() with fallback ID = %q, want id-b", got)
|
|
}
|
|
}
|
|
|
|
func TestBuildUnifiedResourceContext_UnifiedPath(t *testing.T) {
|
|
clusterID := "k8s-cluster-1"
|
|
k8sCluster := unifiedresources.Resource{
|
|
ID: clusterID,
|
|
Name: "prod-cluster",
|
|
Type: unifiedresources.ResourceTypeK8sCluster,
|
|
Status: unifiedresources.StatusOnline,
|
|
Identity: unifiedresources.ResourceIdentity{
|
|
ClusterName: "prod-cluster",
|
|
},
|
|
Kubernetes: &unifiedresources.K8sData{
|
|
ClusterID: "prod-cluster",
|
|
ClusterName: "prod-cluster",
|
|
},
|
|
}
|
|
k8sNode := unifiedresources.Resource{
|
|
ID: "k8s-node-1",
|
|
Name: "worker-1",
|
|
Type: unifiedresources.ResourceTypeK8sNode,
|
|
Status: unifiedresources.StatusWarning,
|
|
ParentID: &clusterID,
|
|
Identity: unifiedresources.ResourceIdentity{
|
|
ClusterName: "prod-cluster",
|
|
},
|
|
Metrics: &unifiedresources.ResourceMetrics{
|
|
CPU: &unifiedresources.MetricValue{Percent: 91},
|
|
Memory: &unifiedresources.MetricValue{Percent: 77},
|
|
},
|
|
Kubernetes: &unifiedresources.K8sData{
|
|
ClusterID: "prod-cluster",
|
|
ClusterName: "prod-cluster",
|
|
},
|
|
}
|
|
|
|
all := []unifiedresources.Resource{k8sCluster, k8sNode}
|
|
stats := unifiedresources.ResourceStats{
|
|
Total: len(all),
|
|
ByType: map[unifiedresources.ResourceType]int{
|
|
unifiedresources.ResourceTypeK8sCluster: 1,
|
|
unifiedresources.ResourceTypeK8sNode: 1,
|
|
},
|
|
ByStatus: map[unifiedresources.ResourceStatus]int{
|
|
unifiedresources.StatusOnline: 1,
|
|
unifiedresources.StatusWarning: 1,
|
|
},
|
|
BySource: map[unifiedresources.DataSource]int{
|
|
unifiedresources.SourceK8s: 2,
|
|
},
|
|
}
|
|
|
|
unifiedProvider := &mockUnifiedResourceProvider{
|
|
getStatsFunc: func() unifiedresources.ResourceStats { return stats },
|
|
getInfrastructureFunc: func() []unifiedresources.Resource {
|
|
return all
|
|
},
|
|
getAllFunc: func() []unifiedresources.Resource {
|
|
return all
|
|
},
|
|
getTopCPUFunc: func(limit int, types []unifiedresources.ResourceType) []unifiedresources.Resource {
|
|
return []unifiedresources.Resource{k8sNode}
|
|
},
|
|
}
|
|
|
|
s := &Service{
|
|
unifiedResourceProvider: unifiedProvider,
|
|
alertProvider: &resourceContextAlertProvider{
|
|
active: []AlertInfo{
|
|
{
|
|
ResourceID: k8sNode.ID,
|
|
ResourceName: "wrong-fallback-name",
|
|
Message: "Node not ready",
|
|
Level: "warning",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
got := s.buildUnifiedResourceContext()
|
|
if got == "" {
|
|
t.Fatal("expected non-empty unified context")
|
|
}
|
|
|
|
assertContains := func(substr string) {
|
|
t.Helper()
|
|
if !strings.Contains(got, substr) {
|
|
t.Fatalf("expected context to contain %q", substr)
|
|
}
|
|
}
|
|
|
|
assertContains("Total resources: 2 (Infrastructure: 2, Workloads: 0)")
|
|
assertContains("Kubernetes")
|
|
assertContains("prod-cluster")
|
|
assertContains("worker-1")
|
|
assertContains("Resources with Active Alerts")
|
|
assertContains("Node not ready")
|
|
}
|
|
|
|
func TestBuildUnifiedResourceContextIncludesTrueNASResources(t *testing.T) {
|
|
previous := truenas.IsFeatureEnabled()
|
|
truenas.SetFeatureEnabled(true)
|
|
t.Cleanup(func() {
|
|
truenas.SetFeatureEnabled(previous)
|
|
})
|
|
|
|
registry := unifiedresources.NewRegistry(unifiedresources.NewMemoryStore())
|
|
records := truenas.NewDefaultProvider().Records()
|
|
if len(records) == 0 {
|
|
t.Fatal("expected truenas fixture records")
|
|
}
|
|
registry.IngestRecords(unifiedresources.SourceTrueNAS, records)
|
|
|
|
adapter := unifiedresources.NewUnifiedAIAdapter(registry)
|
|
s := &Service{}
|
|
s.SetUnifiedResourceProvider(adapter)
|
|
|
|
got := s.buildUnifiedResourceContext()
|
|
if got == "" {
|
|
t.Fatal("expected non-empty context")
|
|
}
|
|
if !strings.Contains(got, "TrueNAS Systems") {
|
|
t.Fatalf("expected context to include TrueNAS section, got %q", got)
|
|
}
|
|
if !strings.Contains(got, "### Storage") {
|
|
t.Fatalf("expected context to include storage section, got %q", got)
|
|
}
|
|
if !strings.Contains(got, "Storage Pools") {
|
|
t.Fatalf("expected context to include storage pools section, got %q", got)
|
|
}
|
|
if !strings.Contains(got, "Physical Disks Needing Attention") {
|
|
t.Fatalf("expected context to include disk attention section, got %q", got)
|
|
}
|
|
if strings.Contains(got, "tank/apps") {
|
|
t.Fatalf("expected storage pool section to exclude dataset names, got %q", got)
|
|
}
|
|
if !strings.Contains(got, "agent resource; status warning; sources truenas") {
|
|
t.Fatalf("expected context to include governed truenas summary, got %q", got)
|
|
}
|
|
if strings.Contains(got, "truenas-main") {
|
|
t.Fatalf("expected raw truenas host name to be redacted, got %q", got)
|
|
}
|
|
}
|
|
|
|
// A retained disk temperature (a disk in standby, a host agent past its
|
|
// reporting lease) is not current heat: it must not put a disk on the
|
|
// attention list, and a disk listed for another reason shows it as last known.
|
|
func TestBuildUnifiedResourceContextJudgesDiskHeatOnCollectedTemperature(t *testing.T) {
|
|
disk := func(id string, status unifiedresources.ResourceStatus, temperature int, collection diskinventory.FieldStatus) unifiedresources.Resource {
|
|
return unifiedresources.Resource{
|
|
ID: id, Name: id, Type: unifiedresources.ResourceTypePhysicalDisk, Status: status, ParentName: "tower",
|
|
PhysicalDisk: &unifiedresources.PhysicalDiskMeta{
|
|
Health: "PASSED", Wearout: -1, Temperature: temperature,
|
|
Collection: &diskinventory.CollectionStatus{Temperature: collection},
|
|
},
|
|
}
|
|
}
|
|
disks := []unifiedresources.Resource{
|
|
disk("standby-disk", unifiedresources.StatusOnline, 58, diskinventory.Unavailable("smartctl", "disk is in standby")),
|
|
disk("silent-disk", unifiedresources.StatusOffline, 52, diskinventory.Unavailable("smartctl", "host agent stopped reporting")),
|
|
disk("hot-disk", unifiedresources.StatusOnline, 55, diskinventory.Available("smartctl")),
|
|
}
|
|
s := &Service{unifiedResourceProvider: &mockUnifiedResourceProvider{
|
|
getStatsFunc: func() unifiedresources.ResourceStats {
|
|
return unifiedresources.ResourceStats{Total: len(disks), ByType: map[unifiedresources.ResourceType]int{unifiedresources.ResourceTypePhysicalDisk: len(disks)}}
|
|
},
|
|
getByTypeFunc: func(resourceType unifiedresources.ResourceType) []unifiedresources.Resource {
|
|
if resourceType == unifiedresources.ResourceTypePhysicalDisk {
|
|
return disks
|
|
}
|
|
return nil
|
|
},
|
|
}}
|
|
|
|
got := s.buildUnifiedResourceContext()
|
|
_, attention, found := strings.Cut(got, "**Physical Disks Needing Attention:**")
|
|
if !found {
|
|
t.Fatalf("expected disk attention section, got %q", got)
|
|
}
|
|
attention, _, _ = strings.Cut(attention, "\n\n")
|
|
if entries := strings.Count(attention, "\n- "); entries != 2 {
|
|
t.Fatalf("attention entries = %d, want the offline and the collected-hot disk only: %q", entries, attention)
|
|
}
|
|
if strings.Contains(attention, "58C") {
|
|
t.Fatalf("a standby disk's retained 58C must not flag it for heat: %q", attention)
|
|
}
|
|
if !strings.Contains(attention, "(PASSED), Temp: last known 52C (host agent stopped reporting) [offline]") {
|
|
t.Fatalf("expected the offline disk's retained temperature as last known: %q", attention)
|
|
}
|
|
if !strings.Contains(attention, "(PASSED), Temp: 55C [online]") {
|
|
t.Fatalf("expected the collected 55C disk on the attention list: %q", attention)
|
|
}
|
|
}
|
|
|
|
func TestBuildUnifiedResourceContext_TruncatesLargeContext(t *testing.T) {
|
|
largeName := strings.Repeat("a", 60000)
|
|
|
|
node := unifiedresources.Resource{
|
|
ID: "node-1",
|
|
Name: largeName,
|
|
Type: unifiedresources.ResourceTypeAgent,
|
|
Status: unifiedresources.StatusOnline,
|
|
}
|
|
|
|
stats := unifiedresources.ResourceStats{
|
|
Total: 1,
|
|
ByType: map[unifiedresources.ResourceType]int{
|
|
unifiedresources.ResourceTypeAgent: 1,
|
|
},
|
|
}
|
|
|
|
mockURP := &mockUnifiedResourceProvider{
|
|
getStatsFunc: func() unifiedresources.ResourceStats {
|
|
return stats
|
|
},
|
|
getInfrastructureFunc: func() []unifiedresources.Resource {
|
|
return []unifiedresources.Resource{node}
|
|
},
|
|
getWorkloadsFunc: func() []unifiedresources.Resource {
|
|
return nil
|
|
},
|
|
getAllFunc: func() []unifiedresources.Resource {
|
|
return []unifiedresources.Resource{node}
|
|
},
|
|
}
|
|
|
|
s := &Service{unifiedResourceProvider: mockURP}
|
|
got := s.buildUnifiedResourceContext()
|
|
if !strings.Contains(got, "[... Context truncated ...]") {
|
|
t.Fatal("expected context to be truncated")
|
|
}
|
|
if len(got) <= 50000 {
|
|
t.Fatalf("expected truncated context length > 50000, got %d", len(got))
|
|
}
|
|
}
|
|
|
|
func TestResourceContextUnifiedProvider_NilRegistry(t *testing.T) {
|
|
adapter := unifiedresources.NewUnifiedAIAdapter(nil)
|
|
|
|
if got := adapter.GetAll(); len(got) != 0 {
|
|
t.Fatalf("GetAll() len = %d, want 0", len(got))
|
|
}
|
|
if got := adapter.GetInfrastructure(); len(got) != 0 {
|
|
t.Fatalf("GetInfrastructure() len = %d, want 0", len(got))
|
|
}
|
|
if got := adapter.GetWorkloads(); len(got) != 0 {
|
|
t.Fatalf("GetWorkloads() len = %d, want 0", len(got))
|
|
}
|
|
if got := adapter.GetByType(unifiedresources.ResourceTypeAgent); len(got) != 0 {
|
|
t.Fatalf("GetByType(host) len = %d, want 0", len(got))
|
|
}
|
|
if got := adapter.GetTopByCPU(3, nil); len(got) != 0 {
|
|
t.Fatalf("GetTopByCPU() len = %d, want 0", len(got))
|
|
}
|
|
if got := adapter.GetRelated("missing"); len(got) != 0 {
|
|
t.Fatalf("GetRelated(missing) len = %d, want 0", len(got))
|
|
}
|
|
if got := adapter.FindContainerHost("missing"); got != "" {
|
|
t.Fatalf("FindContainerHost(missing) = %q, want empty", got)
|
|
}
|
|
if stats := adapter.GetStats(); stats.Total != 0 {
|
|
t.Fatalf("GetStats().Total = %d, want 0", stats.Total)
|
|
}
|
|
}
|
|
|
|
func TestResourceContextUnifiedProvider_ResourceCounts(t *testing.T) {
|
|
registry := unifiedresources.NewRegistry(nil)
|
|
now := time.Now().UTC()
|
|
|
|
registry.IngestRecords(unifiedresources.SourceAgent, []unifiedresources.IngestRecord{
|
|
{
|
|
SourceID: "host-1",
|
|
Resource: unifiedresources.Resource{
|
|
Type: unifiedresources.ResourceTypeAgent,
|
|
Name: "host-1",
|
|
Status: unifiedresources.StatusOnline,
|
|
LastSeen: now,
|
|
},
|
|
},
|
|
})
|
|
registry.IngestRecords(unifiedresources.SourceProxmox, []unifiedresources.IngestRecord{
|
|
{
|
|
SourceID: "vm-1",
|
|
Resource: unifiedresources.Resource{
|
|
Type: unifiedresources.ResourceTypeVM,
|
|
Name: "vm-1",
|
|
Status: unifiedresources.StatusOnline,
|
|
LastSeen: now,
|
|
},
|
|
},
|
|
{
|
|
SourceID: "ct-1",
|
|
Resource: unifiedresources.Resource{
|
|
Type: unifiedresources.ResourceTypeSystemContainer,
|
|
Name: "ct-1",
|
|
Status: unifiedresources.StatusOnline,
|
|
LastSeen: now,
|
|
},
|
|
},
|
|
})
|
|
unified := unifiedresources.NewUnifiedAIAdapter(registry)
|
|
|
|
if got, want := len(unified.GetAll()), 3; got != want {
|
|
t.Fatalf("GetAll() count mismatch: got=%d want=%d", got, want)
|
|
}
|
|
}
|
|
|
|
func TestResourceContextUnifiedProvider_InfrastructureWorkloadSplit(t *testing.T) {
|
|
registry := unifiedresources.NewRegistry(nil)
|
|
now := time.Now().UTC()
|
|
|
|
registry.IngestRecords(unifiedresources.SourceAgent, []unifiedresources.IngestRecord{
|
|
{
|
|
SourceID: "host-1",
|
|
Resource: unifiedresources.Resource{
|
|
Type: unifiedresources.ResourceTypeAgent,
|
|
Name: "host-1",
|
|
Status: unifiedresources.StatusOnline,
|
|
LastSeen: now,
|
|
},
|
|
},
|
|
})
|
|
registry.IngestRecords(unifiedresources.SourceProxmox, []unifiedresources.IngestRecord{
|
|
{
|
|
SourceID: "vm-1",
|
|
Resource: unifiedresources.Resource{
|
|
Type: unifiedresources.ResourceTypeVM,
|
|
Name: "vm-1",
|
|
Status: unifiedresources.StatusOnline,
|
|
LastSeen: now,
|
|
},
|
|
},
|
|
{
|
|
SourceID: "ct-1",
|
|
Resource: unifiedresources.Resource{
|
|
Type: unifiedresources.ResourceTypeSystemContainer,
|
|
Name: "ct-1",
|
|
Status: unifiedresources.StatusOnline,
|
|
LastSeen: now,
|
|
},
|
|
},
|
|
})
|
|
unified := unifiedresources.NewUnifiedAIAdapter(registry)
|
|
|
|
if got, want := len(unified.GetInfrastructure()), 1; got != want {
|
|
t.Fatalf("GetInfrastructure() count mismatch: got=%d want=%d", got, want)
|
|
}
|
|
if got, want := len(unified.GetWorkloads()), 2; got != want {
|
|
t.Fatalf("GetWorkloads() count mismatch: got=%d want=%d", got, want)
|
|
}
|
|
}
|
|
|
|
func TestResourceContextUnifiedProvider_TopCPU(t *testing.T) {
|
|
registry := unifiedresources.NewRegistry(nil)
|
|
now := time.Now().UTC()
|
|
registry.IngestRecords(unifiedresources.SourceAgent, []unifiedresources.IngestRecord{
|
|
{
|
|
SourceID: "host-low",
|
|
Resource: unifiedresources.Resource{
|
|
Type: unifiedresources.ResourceTypeAgent,
|
|
Name: "host-low",
|
|
Status: unifiedresources.StatusOnline,
|
|
LastSeen: now,
|
|
Metrics: &unifiedresources.ResourceMetrics{
|
|
CPU: &unifiedresources.MetricValue{Percent: 25},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
registry.IngestRecords(unifiedresources.SourceProxmox, []unifiedresources.IngestRecord{
|
|
{
|
|
SourceID: "vm-high",
|
|
Resource: unifiedresources.Resource{
|
|
Type: unifiedresources.ResourceTypeVM,
|
|
Name: "vm-high",
|
|
Status: unifiedresources.StatusOnline,
|
|
LastSeen: now,
|
|
Metrics: &unifiedresources.ResourceMetrics{
|
|
CPU: &unifiedresources.MetricValue{Percent: 92},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
registry.IngestRecords(unifiedresources.SourceDocker, []unifiedresources.IngestRecord{
|
|
{
|
|
SourceID: "ct-mid",
|
|
Resource: unifiedresources.Resource{
|
|
Type: unifiedresources.ResourceTypeAppContainer,
|
|
Name: "ct-mid",
|
|
Status: unifiedresources.StatusOnline,
|
|
LastSeen: now,
|
|
Metrics: &unifiedresources.ResourceMetrics{
|
|
CPU: &unifiedresources.MetricValue{Percent: 61},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
|
|
unified := unifiedresources.NewUnifiedAIAdapter(registry)
|
|
top := unified.GetTopByCPU(2, []unifiedresources.ResourceType{
|
|
unifiedresources.ResourceTypeAgent,
|
|
unifiedresources.ResourceTypeVM,
|
|
unifiedresources.ResourceTypeAppContainer,
|
|
})
|
|
if len(top) != 2 {
|
|
t.Fatalf("GetTopByCPU() len = %d, want 2", len(top))
|
|
}
|
|
if top[0].Name != "vm-high" || top[1].Name != "ct-mid" {
|
|
t.Fatalf("unexpected top CPU ordering: got [%s, %s]", top[0].Name, top[1].Name)
|
|
}
|
|
}
|
|
|
|
func TestResourceContextUnifiedProvider_FindContainerHost(t *testing.T) {
|
|
registry := unifiedresources.NewRegistry(nil)
|
|
now := time.Now().UTC()
|
|
registry.IngestRecords(unifiedresources.SourceDocker, []unifiedresources.IngestRecord{
|
|
{
|
|
SourceID: "docker-host-1",
|
|
Resource: unifiedresources.Resource{
|
|
Type: unifiedresources.ResourceTypeAgent,
|
|
Name: "docker-node-1",
|
|
Status: unifiedresources.StatusOnline,
|
|
LastSeen: now,
|
|
},
|
|
},
|
|
{
|
|
SourceID: "container-1",
|
|
ParentSourceID: "docker-host-1",
|
|
Resource: unifiedresources.Resource{
|
|
Type: unifiedresources.ResourceTypeAppContainer,
|
|
Name: "web",
|
|
Status: unifiedresources.StatusOnline,
|
|
LastSeen: now,
|
|
Docker: &unifiedresources.DockerData{ContainerID: "abc123"},
|
|
},
|
|
},
|
|
})
|
|
|
|
unified := unifiedresources.NewUnifiedAIAdapter(registry)
|
|
if got := unified.FindContainerHost("web"); got != "docker-node-1" {
|
|
t.Fatalf("FindContainerHost(web) = %q, want docker-node-1", got)
|
|
}
|
|
if got := unified.FindContainerHost("abc123"); got != "docker-node-1" {
|
|
t.Fatalf("FindContainerHost(abc123) = %q, want docker-node-1", got)
|
|
}
|
|
}
|
|
|
|
type resourceContextAlertProvider struct {
|
|
active []AlertInfo
|
|
history []ResolvedAlertInfo
|
|
}
|
|
|
|
func (m *resourceContextAlertProvider) GetActiveAlerts() []AlertInfo {
|
|
return m.active
|
|
}
|
|
|
|
func (m *resourceContextAlertProvider) GetRecentlyResolved(minutes int) []ResolvedAlertInfo {
|
|
return m.history
|
|
}
|
|
|
|
func (m *resourceContextAlertProvider) GetAlertsByResource(resourceID string) []AlertInfo {
|
|
out := make([]AlertInfo, 0)
|
|
for _, alert := range m.active {
|
|
if alert.ResourceID == resourceID {
|
|
out = append(out, alert)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func (m *resourceContextAlertProvider) GetAlertHistory(resourceID string, limit int) []ResolvedAlertInfo {
|
|
out := make([]ResolvedAlertInfo, 0)
|
|
for _, alert := range m.history {
|
|
if alert.ResourceID == resourceID {
|
|
out = append(out, alert)
|
|
}
|
|
if limit > 0 && len(out) >= limit {
|
|
break
|
|
}
|
|
}
|
|
return out
|
|
}
|