mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-07-10 00:14:38 +00:00
Project Kubernetes agent versions onto node rows
This commit is contained in:
parent
e52df68d30
commit
7440208163
10 changed files with 221 additions and 4 deletions
|
|
@ -241,7 +241,11 @@ must not grow this notice unless a concrete guest or monitored system row
|
|||
actually carries a Pulse agent identity. On vSphere specifically, stale-agent
|
||||
copy belongs to correlated in-guest VM agents and must not describe ESXi hosts
|
||||
as Pulse-agent update targets just because phase-1 host resources use the
|
||||
canonical `agent` resource type.
|
||||
canonical `agent` resource type. Kubernetes is cluster-agent-backed: canonical
|
||||
`k8s-node` rows may be pure Kubernetes API rows rather than merged `agent`
|
||||
rows, so the unified-resource owner must project the cluster agent identity and
|
||||
cluster-scoped agent version onto those node rows before the shared stale-agent
|
||||
collector can decide whether the node inventory is gated by an older agent.
|
||||
Kubernetes policy inventory follows that same primitive boundary while the
|
||||
unified-resource owner supplies NetworkPolicy policy type and rule-count
|
||||
columns, PodDisruptionBudget budget and observed health columns, ResourceQuota
|
||||
|
|
|
|||
|
|
@ -491,6 +491,13 @@ AI-only summary payloads, or page-local heuristics.
|
|||
Resource consumers must also use the API-owned agent update target when
|
||||
comparing resource-carried agent versions; the running app build version is
|
||||
not a resource freshness contract.
|
||||
Kubernetes node rows are cluster-agent-backed for this purpose: even when a
|
||||
canonical `k8s-node` row is a pure Kubernetes API projection with no merged
|
||||
`agent` facet, `internal/unifiedresources/adapters.go` must carry the
|
||||
cluster `AgentID` and cluster-scoped `AgentVersion` on the row's
|
||||
Kubernetes facet so platform consumers can scope stale-agent notices and
|
||||
update-command links from typed resource evidence instead of rebuilding
|
||||
ownership from the parent cluster row.
|
||||
`internal/unifiedresources/top_level_systems.go`
|
||||
Explicit linked-host correlation is canonical here: when Kubernetes node
|
||||
ingest has a resolved backing host agent, the registry must merge that node
|
||||
|
|
|
|||
|
|
@ -121,6 +121,8 @@ export function KubernetesPageSurface() {
|
|||
missingLabel="Kubernetes nodes, workloads, services, storage, configuration, and events"
|
||||
actionHref={outdatedAgentUpdatePath()}
|
||||
actionLabel="Open agent upgrade commands"
|
||||
subjectSingular="node"
|
||||
subjectPlural="nodes"
|
||||
/>
|
||||
<Show when={activeTab() === 'overview'}>
|
||||
<KubernetesOverview model={model} />
|
||||
|
|
|
|||
|
|
@ -240,6 +240,50 @@ describe('KubernetesPageSurface contract', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('surfaces stale cluster-backed Kubernetes node rows without agent metadata', () => {
|
||||
mockVersionInfo.mockReturnValue({
|
||||
version: 'v6.0.0-rc.6',
|
||||
agentUpdateTargetVersion: 'v6.0.0-rc.6',
|
||||
});
|
||||
setResources([
|
||||
makeResource({
|
||||
id: 'cluster-1',
|
||||
name: 'prod-cluster',
|
||||
type: 'k8s-cluster',
|
||||
kubernetes: {
|
||||
agentId: 'agent-k8s-cluster',
|
||||
agentVersion: 'v5.1.34',
|
||||
clusterId: 'cluster-1',
|
||||
clusterName: 'prod-cluster',
|
||||
},
|
||||
}),
|
||||
makeResource({
|
||||
id: 'k8s-node:worker-1',
|
||||
name: 'worker-1',
|
||||
type: 'k8s-node',
|
||||
agent: undefined,
|
||||
kubernetes: {
|
||||
agentId: 'agent-k8s-cluster',
|
||||
agentVersion: 'v5.1.34',
|
||||
clusterId: 'cluster-1',
|
||||
clusterName: 'prod-cluster',
|
||||
nodeName: 'worker-1',
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
renderSurface();
|
||||
|
||||
const notice = screen.getByTestId('platform-outdated-agent-notice');
|
||||
expect(notice).toHaveTextContent('worker-1 is running an older Pulse agent (v5.1.34).');
|
||||
expect(notice).toHaveTextContent('for this node');
|
||||
expect(notice).toHaveTextContent('Kubernetes nodes, workloads, services, storage');
|
||||
expect(screen.getByRole('link', { name: 'Open agent upgrade commands' })).toHaveAttribute(
|
||||
'href',
|
||||
'/settings/infrastructure?agentUpdates=1&agents=agent%3Aagent-k8s-cluster',
|
||||
);
|
||||
});
|
||||
|
||||
it('groups workload API tables under the Workloads tab', () => {
|
||||
mockPathname.mockReturnValue('/kubernetes/workloads');
|
||||
setResources([
|
||||
|
|
|
|||
|
|
@ -85,6 +85,29 @@ describe('hostAgentVersion', () => {
|
|||
).toBe('v6.0.0-rc.5');
|
||||
});
|
||||
|
||||
it('reads the Kubernetes cluster-agent version projected onto node rows', () => {
|
||||
expect(
|
||||
hostAgentVersion(
|
||||
host({
|
||||
type: 'k8s-node',
|
||||
kubernetes: { agentVersion: 'v6.0.0-rc.5' } as Resource['kubernetes'],
|
||||
}),
|
||||
),
|
||||
).toBe('v6.0.0-rc.5');
|
||||
});
|
||||
|
||||
it('prefers Kubernetes cluster-agent version for Kubernetes platform rows', () => {
|
||||
expect(
|
||||
hostAgentVersion(
|
||||
host({
|
||||
type: 'k8s-node',
|
||||
agent: { agentVersion: 'v6.0.0-rc.6' } as Resource['agent'],
|
||||
kubernetes: { agentVersion: 'v6.0.0-rc.5' } as Resource['kubernetes'],
|
||||
}),
|
||||
),
|
||||
).toBe('v6.0.0-rc.5');
|
||||
});
|
||||
|
||||
it('returns undefined when no version is reported', () => {
|
||||
expect(hostAgentVersion(host({}))).toBeUndefined();
|
||||
});
|
||||
|
|
@ -95,7 +118,22 @@ describe('hostAgentVersion', () => {
|
|||
).toBe('agent:agent-delly');
|
||||
expect(
|
||||
hostAgentConnectionID(
|
||||
host({ kubernetes: { agentId: 'agent:agent-k8s' } as Resource['kubernetes'] }),
|
||||
host({
|
||||
type: 'k8s-node',
|
||||
kubernetes: { agentId: 'agent:agent-k8s' } as Resource['kubernetes'],
|
||||
}),
|
||||
),
|
||||
).toBe('agent:agent-k8s');
|
||||
});
|
||||
|
||||
it('prefers Kubernetes cluster-agent IDs for Kubernetes platform rows', () => {
|
||||
expect(
|
||||
hostAgentConnectionID(
|
||||
host({
|
||||
type: 'k8s-node',
|
||||
agent: { agentId: 'agent-host' } as Resource['agent'],
|
||||
kubernetes: { agentId: 'agent-k8s' } as Resource['kubernetes'],
|
||||
}),
|
||||
),
|
||||
).toBe('agent:agent-k8s');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -22,13 +22,37 @@ import {
|
|||
// second case by comparing each host's reported agent version against the
|
||||
// server that is rendering the page.
|
||||
|
||||
const isKubernetesPlatformRow = (host: Resource): boolean =>
|
||||
host.type === 'k8s-node' ||
|
||||
host.type === 'k8s-cluster' ||
|
||||
host.platformType === 'kubernetes' ||
|
||||
host.sources?.includes('kubernetes') === true;
|
||||
|
||||
// The agent version reported for a host. Docker hosts carry it on the docker
|
||||
// meta; plain host agents carry it on the agent meta.
|
||||
// meta; Kubernetes platform rows carry the cluster agent version on the
|
||||
// kubernetes meta; plain host agents carry it on the agent meta.
|
||||
export function hostAgentVersion(host: Resource): string | undefined {
|
||||
return host.docker?.agentVersion?.trim() || host.agent?.agentVersion?.trim() || undefined;
|
||||
const kubernetesAgentVersion = host.kubernetes?.agentVersion?.trim();
|
||||
if (kubernetesAgentVersion && isKubernetesPlatformRow(host)) {
|
||||
return kubernetesAgentVersion;
|
||||
}
|
||||
return (
|
||||
host.docker?.agentVersion?.trim() ||
|
||||
host.agent?.agentVersion?.trim() ||
|
||||
kubernetesAgentVersion ||
|
||||
undefined
|
||||
);
|
||||
}
|
||||
|
||||
export function hostAgentConnectionID(host: Resource): string | undefined {
|
||||
if (isKubernetesPlatformRow(host)) {
|
||||
const kubernetesAgentId = host.kubernetes?.agentId?.trim();
|
||||
if (kubernetesAgentId) {
|
||||
return kubernetesAgentId.startsWith('agent:')
|
||||
? kubernetesAgentId
|
||||
: `agent:${kubernetesAgentId}`;
|
||||
}
|
||||
}
|
||||
const agentId =
|
||||
host.agent?.agentId?.trim() ||
|
||||
host.kubernetes?.agentId?.trim() ||
|
||||
|
|
|
|||
|
|
@ -903,6 +903,7 @@ export interface ResourceKubernetesPodContainerStatus {
|
|||
export interface ResourceKubernetesMeta {
|
||||
clusterId?: string;
|
||||
agentId?: string;
|
||||
agentVersion?: string;
|
||||
clusterName?: string;
|
||||
resourceUid?: string;
|
||||
resourceKind?: string;
|
||||
|
|
|
|||
|
|
@ -1759,6 +1759,69 @@ func TestResourceListIncludesKubernetesPods(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestResourceListProjectsKubernetesClusterAgentVersionOntoNode(t *testing.T) {
|
||||
now := time.Now().UTC()
|
||||
snapshot := models.StateSnapshot{
|
||||
KubernetesClusters: []models.KubernetesCluster{
|
||||
{
|
||||
ID: "cluster-1",
|
||||
AgentID: "agent-1",
|
||||
Name: "prod-k8s",
|
||||
Context: "prod",
|
||||
Status: "online",
|
||||
LastSeen: now,
|
||||
Version: "1.31.2",
|
||||
AgentVersion: "5.1.34",
|
||||
Nodes: []models.KubernetesNode{
|
||||
{
|
||||
UID: "node-1",
|
||||
Name: "worker-1",
|
||||
Ready: true,
|
||||
OSImage: "Ubuntu 24.04",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
cfg := &config.Config{DataPath: t.TempDir()}
|
||||
h := NewResourceHandlers(cfg)
|
||||
h.SetStateProvider(resourceStateProvider{snapshot: snapshot})
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/resources?type=k8s-node", nil)
|
||||
h.HandleListResources(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d, body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
|
||||
var resp ResourcesResponse
|
||||
if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil {
|
||||
t.Fatalf("decode response: %v", err)
|
||||
}
|
||||
if len(resp.Data) != 1 {
|
||||
t.Fatalf("expected 1 kubernetes node resource, got %d", len(resp.Data))
|
||||
}
|
||||
|
||||
resource := resp.Data[0]
|
||||
if resource.Type != unified.ResourceTypeK8sNode {
|
||||
t.Fatalf("resource type = %q, want %q", resource.Type, unified.ResourceTypeK8sNode)
|
||||
}
|
||||
if resource.Agent != nil {
|
||||
t.Fatalf("expected pure k8s-node row without agent metadata, got %+v", resource.Agent)
|
||||
}
|
||||
if resource.Kubernetes == nil {
|
||||
t.Fatalf("expected kubernetes metadata")
|
||||
}
|
||||
if resource.Kubernetes.AgentID != "agent-1" {
|
||||
t.Fatalf("kubernetes agentID = %q, want agent-1", resource.Kubernetes.AgentID)
|
||||
}
|
||||
if resource.Kubernetes.AgentVersion != "5.1.34" {
|
||||
t.Fatalf("kubernetes agentVersion = %q, want 5.1.34", resource.Kubernetes.AgentVersion)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResourceListFiltersCanonicalKubernetesNamespace(t *testing.T) {
|
||||
now := time.Now().UTC()
|
||||
snapshot := models.StateSnapshot{
|
||||
|
|
|
|||
|
|
@ -2421,6 +2421,7 @@ func resourceFromKubernetesNode(cluster models.KubernetesCluster, node models.Ku
|
|||
ClusterID: cluster.ID,
|
||||
ClusterName: clusterName,
|
||||
AgentID: cluster.AgentID,
|
||||
AgentVersion: cluster.AgentVersion,
|
||||
Context: cluster.Context,
|
||||
Server: cluster.Server,
|
||||
Version: cluster.Version,
|
||||
|
|
|
|||
|
|
@ -715,6 +715,39 @@ func TestResourceFromHostSMARTDiskFallsBackToFilesystemSize(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestResourceFromKubernetesNodeProjectsClusterAgentVersion(t *testing.T) {
|
||||
cluster := models.KubernetesCluster{
|
||||
ID: "cluster-1",
|
||||
AgentID: "agent-k8s-1",
|
||||
Name: "prod",
|
||||
DisplayName: "Production",
|
||||
LastSeen: time.Now().UTC(),
|
||||
AgentVersion: "5.1.34",
|
||||
}
|
||||
node := models.KubernetesNode{
|
||||
UID: "node-1",
|
||||
Name: "worker-1",
|
||||
Ready: true,
|
||||
}
|
||||
|
||||
resource, _ := resourceFromKubernetesNode(cluster, node, nil, nil)
|
||||
if resource.Type != ResourceTypeK8sNode {
|
||||
t.Fatalf("resource type = %q, want %q", resource.Type, ResourceTypeK8sNode)
|
||||
}
|
||||
if resource.Agent != nil {
|
||||
t.Fatalf("expected pure k8s-node adapter row without agent facet, got %+v", resource.Agent)
|
||||
}
|
||||
if resource.Kubernetes == nil {
|
||||
t.Fatalf("expected kubernetes facet")
|
||||
}
|
||||
if resource.Kubernetes.AgentID != "agent-k8s-1" {
|
||||
t.Fatalf("agent id = %q, want agent-k8s-1", resource.Kubernetes.AgentID)
|
||||
}
|
||||
if resource.Kubernetes.AgentVersion != "5.1.34" {
|
||||
t.Fatalf("agent version = %q, want 5.1.34", resource.Kubernetes.AgentVersion)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResourceFromKubernetesDeployment_PopulatesMetricsUnderMockMode(t *testing.T) {
|
||||
mockruntime.SetEnabled(true)
|
||||
t.Cleanup(func() { mockruntime.SetEnabled(false) })
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue