mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-07-09 16:00:59 +00:00
Use metadata-only Kubernetes config inventory
This commit is contained in:
parent
120dd5353a
commit
49c9ca7656
22 changed files with 324 additions and 157 deletions
|
|
@ -173,7 +173,11 @@ alignment classes, or ad hoc report-category coercion. Docker / Podman native
|
|||
subsections now include engine storage usage, Swarm node inventory, and
|
||||
metadata-only Swarm secret/config inventory where the documented Docker APIs
|
||||
report those resources; Podman-only libpod pod inventory must not be
|
||||
represented until the collector has a libpod-native source.
|
||||
represented until the collector has a libpod-native source. Kubernetes config
|
||||
tables must preserve the same trust boundary for metadata-only ConfigMap and
|
||||
Secret rows: the shared table wording may indicate metadata-only inventory, but
|
||||
must not imply payload fields were read when the agent used Kubernetes
|
||||
metadata-only API responses.
|
||||
|
||||
1. `frontend-modern/src/components/Settings/APIAccessPanel.tsx` shared with `security-privacy`: the API Access settings intro is both a security/privacy token-management trust surface and a canonical settings-shell presentation boundary.
|
||||
The panel may own shell placement and local action layout, but
|
||||
|
|
|
|||
|
|
@ -63,6 +63,8 @@ truth for live infrastructure data.
|
|||
39. `internal/models/converters.go`
|
||||
40. `internal/models/deepcopy.go`
|
||||
41. `internal/mock/generator.go`
|
||||
42. `internal/kubernetesagent/agent.go`
|
||||
43. `pkg/agents/kubernetes/report.go`
|
||||
|
||||
## Shared Boundaries
|
||||
|
||||
|
|
@ -187,10 +189,15 @@ truth for live infrastructure data.
|
|||
PersistentVolumeClaims, StorageClasses, ConfigMaps, Secrets, ServiceAccounts,
|
||||
ResourceQuotas, LimitRanges, PodDisruptionBudgets,
|
||||
HorizontalPodAutoscalers, and Events as bounded best-effort inventory.
|
||||
Secret values must not be collected; only metadata, type, and key names are
|
||||
allowed. Monitoring must preserve those
|
||||
objects as native cluster inventory instead of flattening them into pods,
|
||||
deployments, or generic storage/configuration rows.
|
||||
ConfigMap and Secret payload values must not be collected for inventory.
|
||||
Current agents must prefer the Kubernetes metadata-only API path for
|
||||
ConfigMap and Secret inventory and mark those rows as metadata-only; older
|
||||
agent reports may still carry key names, but Secret values remain outside
|
||||
the report contract. Mock/demo Kubernetes ConfigMap and Secret inventory
|
||||
must mirror the current metadata-only trust boundary rather than seeding
|
||||
payload key names. Monitoring must preserve those objects as native cluster
|
||||
inventory instead of flattening them into pods, deployments, or generic
|
||||
storage/configuration rows.
|
||||
|
||||
## Forbidden Paths
|
||||
|
||||
|
|
|
|||
|
|
@ -3586,6 +3586,7 @@
|
|||
"internal/dockeragent/collect.go",
|
||||
"internal/dockeragent/docker_client.go",
|
||||
"internal/dockeragent/swarm.go",
|
||||
"internal/kubernetesagent/agent.go",
|
||||
"internal/mock/demo_scenarios.go",
|
||||
"internal/mock/fixture_graph.go",
|
||||
"internal/mock/generator.go",
|
||||
|
|
@ -3595,6 +3596,7 @@
|
|||
"internal/models/models.go",
|
||||
"internal/models/models_frontend.go",
|
||||
"pkg/agents/docker/report.go",
|
||||
"pkg/agents/kubernetes/report.go",
|
||||
"pkg/proxmox/ceph.go",
|
||||
"pkg/proxmox/zfs.go"
|
||||
],
|
||||
|
|
@ -3763,6 +3765,23 @@
|
|||
"internal/dockeragent/swarm_test.go"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "kubernetes-native-agent-runtime",
|
||||
"label": "kubernetes native agent inventory proof",
|
||||
"match_prefixes": [],
|
||||
"match_files": [
|
||||
"internal/kubernetesagent/agent.go",
|
||||
"pkg/agents/kubernetes/report.go"
|
||||
],
|
||||
"allow_same_subsystem_tests": false,
|
||||
"test_prefixes": [],
|
||||
"exact_files": [
|
||||
"internal/kubernetesagent/agent_inventory_test.go",
|
||||
"internal/monitoring/kubernetes_agents_test.go",
|
||||
"internal/unifiedresources/adapter_coverage_test.go",
|
||||
"internal/unifiedresources/kubernetes_registry_test.go"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "docker-runtime-report-model",
|
||||
"label": "docker runtime report model proof",
|
||||
|
|
@ -5047,6 +5066,7 @@
|
|||
"frontend-modern/src/features/infrastructure/InfrastructurePageSurface.tsx",
|
||||
"frontend-modern/src/features/infrastructure/useInfrastructurePageRouteState.ts",
|
||||
"frontend-modern/src/features/infrastructure/useInfrastructurePageState.ts",
|
||||
"frontend-modern/src/features/kubernetes/KubernetesInventoryTable.tsx",
|
||||
"frontend-modern/src/hooks/useUnifiedResources.ts",
|
||||
"frontend-modern/src/routing/resourceLinks.ts",
|
||||
"frontend-modern/src/types/resource.ts",
|
||||
|
|
@ -5174,6 +5194,7 @@
|
|||
"frontend-modern/src/features/infrastructure/InfrastructurePageSurface.tsx",
|
||||
"frontend-modern/src/features/infrastructure/useInfrastructurePageRouteState.ts",
|
||||
"frontend-modern/src/features/infrastructure/useInfrastructurePageState.ts",
|
||||
"frontend-modern/src/features/kubernetes/KubernetesInventoryTable.tsx",
|
||||
"frontend-modern/src/hooks/useUnifiedResources.ts",
|
||||
"frontend-modern/src/routing/resourceLinks.ts",
|
||||
"frontend-modern/src/types/resource.ts",
|
||||
|
|
@ -5201,6 +5222,7 @@
|
|||
"frontend-modern/src/features/docker/__tests__/DockerPageSurface.test.tsx",
|
||||
"frontend-modern/src/features/infrastructure/__tests__/infrastructurePageModel.test.ts",
|
||||
"frontend-modern/src/features/infrastructure/__tests__/InfrastructurePageSurface.guardrails.test.ts",
|
||||
"frontend-modern/src/features/kubernetes/__tests__/KubernetesInventoryTable.test.tsx",
|
||||
"frontend-modern/src/hooks/__tests__/useUnifiedResources.test.ts",
|
||||
"frontend-modern/src/routing/__tests__/resourceLinks.test.ts",
|
||||
"frontend-modern/src/stores/__tests__/websocket-unified.test.ts",
|
||||
|
|
|
|||
|
|
@ -164,7 +164,10 @@ inventory objects that flow through the shared resource decoder, such as
|
|||
ConfigMaps, Secrets, ServiceAccounts, ResourceQuotas, LimitRanges,
|
||||
PodDisruptionBudgets, and HorizontalPodAutoscalers, remain platform
|
||||
configuration, policy, or autoscaling evidence only and must not become
|
||||
storage/recovery ownership, restore scope, or secret material.
|
||||
storage/recovery ownership, restore scope, or secret material. If those
|
||||
ConfigMap or Secret rows are marked `metadataOnly`, storage and recovery may
|
||||
surface that trust state as context only; they must not infer payload
|
||||
availability, key names, or restore inputs from the row.
|
||||
Docker engine `/system/df` storage-usage buckets are host-level runtime
|
||||
capacity evidence for the Docker page and unified-resource Docker host facet;
|
||||
they are not storage/recovery resources, recovery-point sources, or restore
|
||||
|
|
|
|||
|
|
@ -123,6 +123,7 @@ cross-source deduplication.
|
|||
99. `frontend-modern/src/features/agents/AgentsPageSurface.tsx`
|
||||
100. `frontend-modern/src/features/agents/AgentsMachinesTable.tsx`
|
||||
101. `internal/platformsupport/manifest_generated.go`
|
||||
102. `frontend-modern/src/features/kubernetes/KubernetesInventoryTable.tsx`
|
||||
|
||||
## Shared Boundaries
|
||||
|
||||
|
|
@ -255,11 +256,13 @@ reintroduce false Swarm capability surfaces.
|
|||
Secrets, ServiceAccounts, ResourceQuotas, LimitRanges,
|
||||
PodDisruptionBudgets, HorizontalPodAutoscalers, and Events must preserve
|
||||
their API identity and source metadata through clone, merge, REST,
|
||||
websocket, and frontend decode paths. `k8s-secret` resources must expose
|
||||
metadata, type, and key names only; secret values are outside the unified
|
||||
resource contract. Because Secret names and key names can disclose intent,
|
||||
`k8s-secret` policy metadata must classify them as `restricted` with
|
||||
`local-only` routing.
|
||||
websocket, and frontend decode paths. `k8s-configmap` and `k8s-secret`
|
||||
resources must preserve the `metadataOnly` flag when the agent used the
|
||||
Kubernetes metadata-only API path; current agents must not fetch ConfigMap
|
||||
or Secret payload values merely to build inventory rows. Older reports may
|
||||
carry key names, but Secret values are outside the unified resource contract.
|
||||
Because Secret names and key names can disclose intent, `k8s-secret` policy
|
||||
metadata must classify them as `restricted` with `local-only` routing.
|
||||
2. Add typed accessors and views in `internal/unifiedresources/views.go`
|
||||
3. Add source ingestion/adaptation in the adapter layer only
|
||||
Frontend resource platform contracts in
|
||||
|
|
|
|||
|
|
@ -330,13 +330,17 @@ const KubernetesInventoryRow: Component<{
|
|||
textValue(
|
||||
props.resource.kubernetes?.phase ||
|
||||
(props.resource.type === 'k8s-configmap'
|
||||
? props.resource.kubernetes?.immutable
|
||||
? 'Immutable'
|
||||
: 'Mutable'
|
||||
? props.resource.kubernetes?.metadataOnly
|
||||
? 'Metadata-only'
|
||||
: props.resource.kubernetes?.immutable
|
||||
? 'Immutable'
|
||||
: 'Mutable'
|
||||
: undefined) ||
|
||||
(props.resource.type === 'k8s-secret'
|
||||
? props.resource.kubernetes?.secretType ||
|
||||
(props.resource.kubernetes?.immutable ? 'Immutable' : 'Mutable')
|
||||
? props.resource.kubernetes?.metadataOnly
|
||||
? 'Metadata-only'
|
||||
: props.resource.kubernetes?.secretType ||
|
||||
(props.resource.kubernetes?.immutable ? 'Immutable' : 'Mutable')
|
||||
: undefined) ||
|
||||
(props.resource.type === 'k8s-serviceaccount'
|
||||
? props.resource.kubernetes?.automountServiceAccountToken === false
|
||||
|
|
@ -346,7 +350,8 @@ const KubernetesInventoryRow: Component<{
|
|||
);
|
||||
const configDetail = () =>
|
||||
textValue(
|
||||
props.resource.kubernetes?.dataKeys?.join(', ') ||
|
||||
(props.resource.kubernetes?.metadataOnly ? 'Payload omitted' : undefined) ||
|
||||
props.resource.kubernetes?.dataKeys?.join(', ') ||
|
||||
props.resource.kubernetes?.binaryDataKeys?.join(', ') ||
|
||||
props.resource.kubernetes?.imagePullSecrets?.join(', ') ||
|
||||
(typeof props.resource.kubernetes?.secretCount === 'number'
|
||||
|
|
|
|||
|
|
@ -0,0 +1,69 @@
|
|||
import { cleanup, render, screen } from '@solidjs/testing-library';
|
||||
import { afterEach, describe, expect, it } from 'vitest';
|
||||
|
||||
import type { Resource } from '@/types/resource';
|
||||
import { KubernetesInventoryTable } from '../KubernetesInventoryTable';
|
||||
|
||||
const makeResource = ({
|
||||
id,
|
||||
type,
|
||||
...overrides
|
||||
}: Partial<Resource> & Pick<Resource, 'id' | 'type'>): Resource => ({
|
||||
id,
|
||||
name: id,
|
||||
displayName: id,
|
||||
platformId: 'cluster-1',
|
||||
platformType: 'kubernetes',
|
||||
sourceType: 'agent',
|
||||
sources: ['kubernetes'],
|
||||
status: 'online',
|
||||
type,
|
||||
lastSeen: 1_700_000_000_000,
|
||||
...overrides,
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
describe('KubernetesInventoryTable', () => {
|
||||
it('marks metadata-only config and secret rows without implying payload fields were read', () => {
|
||||
render(() => (
|
||||
<KubernetesInventoryTable
|
||||
resources={[
|
||||
makeResource({
|
||||
id: 'api-config',
|
||||
type: 'k8s-configmap',
|
||||
kubernetes: {
|
||||
namespace: 'apps',
|
||||
resourceKind: 'ConfigMap',
|
||||
dataKeys: ['app.yaml'],
|
||||
metadataOnly: true,
|
||||
},
|
||||
}),
|
||||
makeResource({
|
||||
id: 'api-secret',
|
||||
type: 'k8s-secret',
|
||||
kubernetes: {
|
||||
namespace: 'apps',
|
||||
resourceKind: 'Secret',
|
||||
dataKeys: ['token'],
|
||||
metadataOnly: true,
|
||||
},
|
||||
}),
|
||||
]}
|
||||
variant="config"
|
||||
emptyIcon={<span />}
|
||||
emptyTitle="No config"
|
||||
emptyDescription="No config"
|
||||
showToolbar={false}
|
||||
/>
|
||||
));
|
||||
|
||||
expect(screen.getAllByText('Metadata-only')).toHaveLength(2);
|
||||
expect(screen.getAllByText('Payload omitted')).toHaveLength(2);
|
||||
expect(screen.queryByText('app.yaml')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('token')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Mutable')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
@ -953,6 +953,7 @@ export interface ResourceKubernetesMeta {
|
|||
dataKeys?: string[];
|
||||
binaryDataKeys?: string[];
|
||||
immutable?: boolean;
|
||||
metadataOnly?: boolean;
|
||||
secretType?: string;
|
||||
automountServiceAccountToken?: boolean;
|
||||
secretCount?: number;
|
||||
|
|
|
|||
|
|
@ -36,7 +36,9 @@ import (
|
|||
k8sresource "k8s.io/apimachinery/pkg/api/resource"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/fields"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/metadata"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
)
|
||||
|
|
@ -76,8 +78,9 @@ type Agent struct {
|
|||
logger zerolog.Logger
|
||||
httpClient *http.Client
|
||||
|
||||
kubeClient kubernetes.Interface
|
||||
restCfg *rest.Config
|
||||
kubeClient kubernetes.Interface
|
||||
metadataClient metadata.Interface
|
||||
restCfg *rest.Config
|
||||
|
||||
agentID string
|
||||
agentVersion string
|
||||
|
|
@ -115,6 +118,11 @@ const (
|
|||
maxRecoveryResponseBodyBytes int64 = 8 * 1024 * 1024 // 8 MB (recovery APIs can be large)
|
||||
)
|
||||
|
||||
var (
|
||||
configMapsGVR = schema.GroupVersionResource{Version: "v1", Resource: "configmaps"}
|
||||
secretsGVR = schema.GroupVersionResource{Version: "v1", Resource: "secrets"}
|
||||
)
|
||||
|
||||
func New(cfg Config) (*Agent, error) {
|
||||
if cfg.Interval <= 0 {
|
||||
cfg.Interval = defaultInterval
|
||||
|
|
@ -162,6 +170,10 @@ func New(cfg Config) (*Agent, error) {
|
|||
if err != nil {
|
||||
return nil, fmt.Errorf("create kubernetes client: %w", err)
|
||||
}
|
||||
metadataClient, err := metadata.NewForConfig(restCfg)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create kubernetes metadata client: %w", err)
|
||||
}
|
||||
|
||||
agentVersion := strings.TrimSpace(cfg.AgentVersion)
|
||||
if agentVersion == "" {
|
||||
|
|
@ -202,6 +214,7 @@ func New(cfg Config) (*Agent, error) {
|
|||
logger: *logger,
|
||||
httpClient: httpClient,
|
||||
kubeClient: kubeClient,
|
||||
metadataClient: metadataClient,
|
||||
restCfg: restCfg,
|
||||
agentID: agentID,
|
||||
agentVersion: agentVersion,
|
||||
|
|
@ -1986,6 +1999,46 @@ func (a *Agent) inventoryNamespaces() []string {
|
|||
return namespaces
|
||||
}
|
||||
|
||||
func (a *Agent) collectPartialObjectMetadata(ctx context.Context, resource schema.GroupVersionResource, actionResource string) ([]metav1.PartialObjectMetadata, error) {
|
||||
if a.metadataClient == nil {
|
||||
return nil, fmt.Errorf("kubernetes metadata client unavailable")
|
||||
}
|
||||
|
||||
items := make([]metav1.PartialObjectMetadata, 0)
|
||||
for _, namespace := range a.inventoryNamespaces() {
|
||||
opts := metav1.ListOptions{Limit: listPageSize}
|
||||
for {
|
||||
var list *metav1.PartialObjectMetadataList
|
||||
action := "list " + actionResource + " metadata"
|
||||
if namespace != metav1.NamespaceAll {
|
||||
action = fmt.Sprintf("list %s metadata in namespace %q", actionResource, namespace)
|
||||
}
|
||||
err := a.runKubernetesCallWithRetry(ctx, action, func(callCtx context.Context) error {
|
||||
var err error
|
||||
list, err = a.metadataClient.Resource(resource).Namespace(namespace).List(callCtx, opts)
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, item := range list.Items {
|
||||
if !a.namespaceAllowed(item.Namespace) {
|
||||
continue
|
||||
}
|
||||
items = append(items, item)
|
||||
if len(items) >= maxInventoryItems {
|
||||
return items, nil
|
||||
}
|
||||
}
|
||||
if list.Continue == "" {
|
||||
break
|
||||
}
|
||||
opts.Continue = list.Continue
|
||||
}
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func copyKubernetesStringMap(src map[string]string) map[string]string {
|
||||
if len(src) == 0 {
|
||||
return nil
|
||||
|
|
@ -2858,50 +2911,20 @@ func (a *Agent) collectStorageClasses(ctx context.Context) ([]agentsk8s.StorageC
|
|||
}
|
||||
|
||||
func (a *Agent) collectConfigMaps(ctx context.Context) ([]agentsk8s.ConfigMap, error) {
|
||||
items := make([]agentsk8s.ConfigMap, 0)
|
||||
for _, namespace := range a.inventoryNamespaces() {
|
||||
opts := metav1.ListOptions{Limit: listPageSize}
|
||||
for {
|
||||
var list *corev1.ConfigMapList
|
||||
action := "list configmaps"
|
||||
if namespace != metav1.NamespaceAll {
|
||||
action = fmt.Sprintf("list configmaps in namespace %q", namespace)
|
||||
}
|
||||
err := a.runKubernetesCallWithRetry(ctx, action, func(callCtx context.Context) error {
|
||||
var err error
|
||||
list, err = a.kubeClient.CoreV1().ConfigMaps(namespace).List(callCtx, opts)
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, configMap := range list.Items {
|
||||
if !a.namespaceAllowed(configMap.Namespace) {
|
||||
continue
|
||||
}
|
||||
immutable := false
|
||||
if configMap.Immutable != nil {
|
||||
immutable = *configMap.Immutable
|
||||
}
|
||||
items = append(items, agentsk8s.ConfigMap{
|
||||
UID: string(configMap.UID),
|
||||
Name: configMap.Name,
|
||||
Namespace: configMap.Namespace,
|
||||
DataKeys: sortedStringKeys(configMap.Data),
|
||||
BinaryDataKeys: sortedStringKeys(configMap.BinaryData),
|
||||
Immutable: immutable,
|
||||
CreatedAt: configMap.CreationTimestamp.Time,
|
||||
Labels: copyKubernetesStringMap(configMap.Labels),
|
||||
})
|
||||
if len(items) >= maxInventoryItems {
|
||||
return items, nil
|
||||
}
|
||||
}
|
||||
if list.Continue == "" {
|
||||
break
|
||||
}
|
||||
opts.Continue = list.Continue
|
||||
}
|
||||
metadataItems, err := a.collectPartialObjectMetadata(ctx, configMapsGVR, "configmaps")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items := make([]agentsk8s.ConfigMap, 0, len(metadataItems))
|
||||
for _, configMap := range metadataItems {
|
||||
items = append(items, agentsk8s.ConfigMap{
|
||||
UID: string(configMap.UID),
|
||||
Name: configMap.Name,
|
||||
Namespace: configMap.Namespace,
|
||||
MetadataOnly: true,
|
||||
CreatedAt: configMap.CreationTimestamp.Time,
|
||||
Labels: copyKubernetesStringMap(configMap.Labels),
|
||||
})
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
|
@ -2952,50 +2975,20 @@ func (a *Agent) collectServiceAccounts(ctx context.Context) ([]agentsk8s.Service
|
|||
}
|
||||
|
||||
func (a *Agent) collectSecrets(ctx context.Context) ([]agentsk8s.Secret, error) {
|
||||
items := make([]agentsk8s.Secret, 0)
|
||||
for _, namespace := range a.inventoryNamespaces() {
|
||||
opts := metav1.ListOptions{Limit: listPageSize}
|
||||
for {
|
||||
var list *corev1.SecretList
|
||||
action := "list secrets"
|
||||
if namespace != metav1.NamespaceAll {
|
||||
action = fmt.Sprintf("list secrets in namespace %q", namespace)
|
||||
}
|
||||
err := a.runKubernetesCallWithRetry(ctx, action, func(callCtx context.Context) error {
|
||||
var err error
|
||||
list, err = a.kubeClient.CoreV1().Secrets(namespace).List(callCtx, opts)
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, secret := range list.Items {
|
||||
if !a.namespaceAllowed(secret.Namespace) {
|
||||
continue
|
||||
}
|
||||
immutable := false
|
||||
if secret.Immutable != nil {
|
||||
immutable = *secret.Immutable
|
||||
}
|
||||
items = append(items, agentsk8s.Secret{
|
||||
UID: string(secret.UID),
|
||||
Name: secret.Name,
|
||||
Namespace: secret.Namespace,
|
||||
Type: string(secret.Type),
|
||||
DataKeys: sortedStringKeys(secret.Data),
|
||||
Immutable: immutable,
|
||||
CreatedAt: secret.CreationTimestamp.Time,
|
||||
Labels: copyKubernetesStringMap(secret.Labels),
|
||||
})
|
||||
if len(items) >= maxInventoryItems {
|
||||
return items, nil
|
||||
}
|
||||
}
|
||||
if list.Continue == "" {
|
||||
break
|
||||
}
|
||||
opts.Continue = list.Continue
|
||||
}
|
||||
metadataItems, err := a.collectPartialObjectMetadata(ctx, secretsGVR, "secrets")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items := make([]agentsk8s.Secret, 0, len(metadataItems))
|
||||
for _, secret := range metadataItems {
|
||||
items = append(items, agentsk8s.Secret{
|
||||
UID: string(secret.UID),
|
||||
Name: secret.Name,
|
||||
Namespace: secret.Namespace,
|
||||
MetadataOnly: true,
|
||||
CreatedAt: secret.CreationTimestamp.Time,
|
||||
Labels: copyKubernetesStringMap(secret.Labels),
|
||||
})
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package kubernetesagent
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
autoscalingv2 "k8s.io/api/autoscaling/v2"
|
||||
|
|
@ -9,20 +10,16 @@ import (
|
|||
policyv1 "k8s.io/api/policy/v1"
|
||||
k8sresource "k8s.io/apimachinery/pkg/api/resource"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/util/intstr"
|
||||
"k8s.io/client-go/kubernetes/fake"
|
||||
metadatafake "k8s.io/client-go/metadata/fake"
|
||||
k8stesting "k8s.io/client-go/testing"
|
||||
)
|
||||
|
||||
func TestCollectNativePolicyConfigAndAutoscalingInventory(t *testing.T) {
|
||||
immutable := true
|
||||
minAvailable := intstr.FromInt(1)
|
||||
clientset := fake.NewSimpleClientset(
|
||||
&corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{UID: "secret-uid", Namespace: "apps", Name: "api-secret"},
|
||||
Type: corev1.SecretTypeOpaque,
|
||||
Immutable: &immutable,
|
||||
Data: map[string][]byte{"token": []byte("super-secret")},
|
||||
},
|
||||
&corev1.ResourceQuota{
|
||||
ObjectMeta: metav1.ObjectMeta{UID: "quota-uid", Namespace: "apps", Name: "apps-quota"},
|
||||
Spec: corev1.ResourceQuotaSpec{
|
||||
|
|
@ -69,13 +66,53 @@ func TestCollectNativePolicyConfigAndAutoscalingInventory(t *testing.T) {
|
|||
Status: autoscalingv2.HorizontalPodAutoscalerStatus{CurrentReplicas: 2, DesiredReplicas: 3},
|
||||
},
|
||||
)
|
||||
a := &Agent{kubeClient: clientset}
|
||||
clientset.PrependReactor("list", "configmaps", func(k8stesting.Action) (bool, runtime.Object, error) {
|
||||
return true, nil, errors.New("full configmap payload list should not be called")
|
||||
})
|
||||
clientset.PrependReactor("list", "secrets", func(k8stesting.Action) (bool, runtime.Object, error) {
|
||||
return true, nil, errors.New("full secret payload list should not be called")
|
||||
})
|
||||
|
||||
metadataScheme := metadatafake.NewTestScheme()
|
||||
metav1.AddMetaToScheme(metadataScheme)
|
||||
metadataClient := metadatafake.NewSimpleMetadataClient(
|
||||
metadataScheme,
|
||||
&metav1.PartialObjectMetadata{
|
||||
TypeMeta: metav1.TypeMeta{APIVersion: "v1", Kind: "ConfigMap"},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
UID: "configmap-uid",
|
||||
Namespace: "apps",
|
||||
Name: "api-config",
|
||||
CreationTimestamp: metav1.Now(),
|
||||
Labels: map[string]string{"app": "api"},
|
||||
},
|
||||
},
|
||||
&metav1.PartialObjectMetadata{
|
||||
TypeMeta: metav1.TypeMeta{APIVersion: "v1", Kind: "Secret"},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
UID: "secret-uid",
|
||||
Namespace: "apps",
|
||||
Name: "api-secret",
|
||||
CreationTimestamp: metav1.Now(),
|
||||
Labels: map[string]string{"app": "api"},
|
||||
},
|
||||
},
|
||||
)
|
||||
a := &Agent{kubeClient: clientset, metadataClient: metadataClient}
|
||||
|
||||
configMaps, err := a.collectConfigMaps(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("collectConfigMaps: %v", err)
|
||||
}
|
||||
if len(configMaps) != 1 || configMaps[0].Name != "api-config" || !configMaps[0].MetadataOnly || len(configMaps[0].DataKeys) != 0 || len(configMaps[0].BinaryDataKeys) != 0 {
|
||||
t.Fatalf("unexpected configmaps: %+v", configMaps)
|
||||
}
|
||||
|
||||
secrets, err := a.collectSecrets(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("collectSecrets: %v", err)
|
||||
}
|
||||
if len(secrets) != 1 || secrets[0].Type != string(corev1.SecretTypeOpaque) || len(secrets[0].DataKeys) != 1 || secrets[0].DataKeys[0] != "token" || !secrets[0].Immutable {
|
||||
if len(secrets) != 1 || secrets[0].Name != "api-secret" || !secrets[0].MetadataOnly || secrets[0].Type != "" || len(secrets[0].DataKeys) != 0 || secrets[0].Immutable {
|
||||
t.Fatalf("unexpected secrets: %+v", secrets)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -646,10 +646,10 @@ func applyDemoKubernetesNativeInventory(cluster *models.KubernetesCluster, now t
|
|||
{UID: cluster.ID + "-netpol-default-deny", Name: "default-deny", Namespace: "services", PolicyTypes: []string{"Ingress", "Egress"}, IngressRuleCount: 1, EgressRuleCount: 1, CreatedAt: createdAt, Labels: labels("default-deny")},
|
||||
}
|
||||
cluster.ConfigMaps = []models.KubernetesConfigMap{
|
||||
{UID: cluster.ID + "-cm-checkout-api", Name: "checkout-api-config", Namespace: "services", DataKeys: []string{"app.yaml", "feature-flags.yaml"}, CreatedAt: createdAt, Labels: labels("checkout-api")},
|
||||
{UID: cluster.ID + "-cm-checkout-api", Name: "checkout-api-config", Namespace: "services", MetadataOnly: true, CreatedAt: createdAt, Labels: labels("checkout-api")},
|
||||
}
|
||||
cluster.Secrets = []models.KubernetesSecret{
|
||||
{UID: cluster.ID + "-secret-checkout-api", Name: "checkout-api-runtime", Namespace: "services", Type: "Opaque", DataKeys: []string{"database-url", "session-key"}, CreatedAt: createdAt, Labels: labels("checkout-api")},
|
||||
{UID: cluster.ID + "-secret-checkout-api", Name: "checkout-api-runtime", Namespace: "services", MetadataOnly: true, CreatedAt: createdAt, Labels: labels("checkout-api")},
|
||||
}
|
||||
cluster.ServiceAccounts = []models.KubernetesServiceAccount{
|
||||
{UID: cluster.ID + "-sa-checkout-api", Name: "checkout-api", Namespace: "services", SecretCount: 1, ImagePullSecrets: []string{"registry-pull"}, CreatedAt: createdAt, Labels: labels("checkout-api")},
|
||||
|
|
|
|||
|
|
@ -372,7 +372,14 @@ func kubernetesNativeInventoryExists(graph FixtureGraph) bool {
|
|||
len(cluster.Events) == 0 {
|
||||
return false
|
||||
}
|
||||
if cluster.Secrets[0].Type == "" || len(cluster.Secrets[0].DataKeys) == 0 {
|
||||
if !cluster.ConfigMaps[0].MetadataOnly ||
|
||||
len(cluster.ConfigMaps[0].DataKeys) != 0 ||
|
||||
len(cluster.ConfigMaps[0].BinaryDataKeys) != 0 {
|
||||
return false
|
||||
}
|
||||
if !cluster.Secrets[0].MetadataOnly ||
|
||||
cluster.Secrets[0].Type != "" ||
|
||||
len(cluster.Secrets[0].DataKeys) != 0 {
|
||||
return false
|
||||
}
|
||||
if cluster.HorizontalPodAutoscalers[0].TargetKind == "" ||
|
||||
|
|
|
|||
|
|
@ -1598,6 +1598,7 @@ type KubernetesConfigMap struct {
|
|||
DataKeys []string `json:"dataKeys,omitempty"`
|
||||
BinaryDataKeys []string `json:"binaryDataKeys,omitempty"`
|
||||
Immutable bool `json:"immutable,omitempty"`
|
||||
MetadataOnly bool `json:"metadataOnly,omitempty"`
|
||||
CreatedAt time.Time `json:"createdAt,omitempty"`
|
||||
Labels map[string]string `json:"labels,omitempty"`
|
||||
}
|
||||
|
|
@ -1616,14 +1617,15 @@ func (c KubernetesConfigMap) NormalizeCollections() KubernetesConfigMap {
|
|||
}
|
||||
|
||||
type KubernetesSecret struct {
|
||||
UID string `json:"uid"`
|
||||
Name string `json:"name"`
|
||||
Namespace string `json:"namespace"`
|
||||
Type string `json:"type,omitempty"`
|
||||
DataKeys []string `json:"dataKeys,omitempty"`
|
||||
Immutable bool `json:"immutable,omitempty"`
|
||||
CreatedAt time.Time `json:"createdAt,omitempty"`
|
||||
Labels map[string]string `json:"labels,omitempty"`
|
||||
UID string `json:"uid"`
|
||||
Name string `json:"name"`
|
||||
Namespace string `json:"namespace"`
|
||||
Type string `json:"type,omitempty"`
|
||||
DataKeys []string `json:"dataKeys,omitempty"`
|
||||
Immutable bool `json:"immutable,omitempty"`
|
||||
MetadataOnly bool `json:"metadataOnly,omitempty"`
|
||||
CreatedAt time.Time `json:"createdAt,omitempty"`
|
||||
Labels map[string]string `json:"labels,omitempty"`
|
||||
}
|
||||
|
||||
func (s KubernetesSecret) NormalizeCollections() KubernetesSecret {
|
||||
|
|
|
|||
|
|
@ -639,6 +639,7 @@ func convertKubernetesConfigMaps(configMaps []agentsk8s.ConfigMap) []models.Kube
|
|||
DataKeys: append([]string(nil), configMap.DataKeys...),
|
||||
BinaryDataKeys: append([]string(nil), configMap.BinaryDataKeys...),
|
||||
Immutable: configMap.Immutable,
|
||||
MetadataOnly: configMap.MetadataOnly,
|
||||
CreatedAt: configMap.CreatedAt,
|
||||
Labels: cloneStringMap(configMap.Labels),
|
||||
}.NormalizeCollections())
|
||||
|
|
@ -653,14 +654,15 @@ func convertKubernetesSecrets(secrets []agentsk8s.Secret) []models.KubernetesSec
|
|||
out := make([]models.KubernetesSecret, 0, len(secrets))
|
||||
for _, secret := range secrets {
|
||||
out = append(out, models.KubernetesSecret{
|
||||
UID: strings.TrimSpace(secret.UID),
|
||||
Name: strings.TrimSpace(secret.Name),
|
||||
Namespace: strings.TrimSpace(secret.Namespace),
|
||||
Type: strings.TrimSpace(secret.Type),
|
||||
DataKeys: append([]string(nil), secret.DataKeys...),
|
||||
Immutable: secret.Immutable,
|
||||
CreatedAt: secret.CreatedAt,
|
||||
Labels: cloneStringMap(secret.Labels),
|
||||
UID: strings.TrimSpace(secret.UID),
|
||||
Name: strings.TrimSpace(secret.Name),
|
||||
Namespace: strings.TrimSpace(secret.Namespace),
|
||||
Type: strings.TrimSpace(secret.Type),
|
||||
DataKeys: append([]string(nil), secret.DataKeys...),
|
||||
Immutable: secret.Immutable,
|
||||
MetadataOnly: secret.MetadataOnly,
|
||||
CreatedAt: secret.CreatedAt,
|
||||
Labels: cloneStringMap(secret.Labels),
|
||||
}.NormalizeCollections())
|
||||
}
|
||||
return out
|
||||
|
|
|
|||
|
|
@ -129,8 +129,8 @@ func TestApplyKubernetesReportPreservesNativeAPIInventory(t *testing.T) {
|
|||
UID: "pvc-1", Name: "checkout-data", Namespace: "services", Phase: " Bound ", StorageClass: "fast", RequestedBytes: 10_000, CapacityBytes: 10_000, AccessModes: []string{"ReadWriteOnce"}, VolumeName: "pv-checkout", CreatedAt: now,
|
||||
}},
|
||||
StorageClasses: []agentsk8s.StorageClass{{UID: "sc-1", Name: "fast", Provisioner: "csi.example.test", ReclaimPolicy: "Delete", VolumeBindingMode: "WaitForFirstConsumer", AllowVolumeExpansion: &allowExpansion, ParameterKeys: []string{"type"}}},
|
||||
ConfigMaps: []agentsk8s.ConfigMap{{UID: "cm-1", Name: "checkout-config", Namespace: "services", DataKeys: []string{"app.yaml"}, BinaryDataKeys: []string{"logo.png"}, Immutable: true}},
|
||||
Secrets: []agentsk8s.Secret{{UID: "sec-1", Name: "checkout-secret", Namespace: "services", Type: "Opaque", DataKeys: []string{"token"}, Immutable: true}},
|
||||
ConfigMaps: []agentsk8s.ConfigMap{{UID: "cm-1", Name: "checkout-config", Namespace: "services", DataKeys: []string{"app.yaml"}, BinaryDataKeys: []string{"logo.png"}, Immutable: true, MetadataOnly: true}},
|
||||
Secrets: []agentsk8s.Secret{{UID: "sec-1", Name: "checkout-secret", Namespace: "services", Type: "Opaque", DataKeys: []string{"token"}, Immutable: true, MetadataOnly: true}},
|
||||
ServiceAccounts: []agentsk8s.ServiceAccount{{UID: "sa-1", Name: "checkout", Namespace: "services", AutomountServiceAccountToken: &automountToken, SecretCount: 1, ImagePullSecrets: []string{"pull-secret"}}},
|
||||
ResourceQuotas: []agentsk8s.ResourceQuota{{UID: "rq-1", Name: "services-quota", Namespace: "services", Hard: map[string]string{"pods": "10"}, Used: map[string]string{"pods": "3"}}},
|
||||
LimitRanges: []agentsk8s.LimitRange{{UID: "lr-1", Name: "services-limits", Namespace: "services", LimitTypes: []string{"Container"}}},
|
||||
|
|
@ -187,10 +187,10 @@ func TestApplyKubernetesReportPreservesNativeAPIInventory(t *testing.T) {
|
|||
if len(cluster.StorageClasses) != 1 || cluster.StorageClasses[0].Provisioner != "csi.example.test" || cluster.StorageClasses[0].AllowVolumeExpansion == nil || !*cluster.StorageClasses[0].AllowVolumeExpansion {
|
||||
t.Fatalf("expected storage class inventory, got %+v", cluster.StorageClasses)
|
||||
}
|
||||
if len(cluster.ConfigMaps) != 1 || cluster.ConfigMaps[0].DataKeys[0] != "app.yaml" || !cluster.ConfigMaps[0].Immutable {
|
||||
if len(cluster.ConfigMaps) != 1 || cluster.ConfigMaps[0].DataKeys[0] != "app.yaml" || !cluster.ConfigMaps[0].Immutable || !cluster.ConfigMaps[0].MetadataOnly {
|
||||
t.Fatalf("expected configmap inventory, got %+v", cluster.ConfigMaps)
|
||||
}
|
||||
if len(cluster.Secrets) != 1 || cluster.Secrets[0].Type != "Opaque" || cluster.Secrets[0].DataKeys[0] != "token" || !cluster.Secrets[0].Immutable {
|
||||
if len(cluster.Secrets) != 1 || cluster.Secrets[0].Type != "Opaque" || cluster.Secrets[0].DataKeys[0] != "token" || !cluster.Secrets[0].Immutable || !cluster.Secrets[0].MetadataOnly {
|
||||
t.Fatalf("expected secret metadata inventory, got %+v", cluster.Secrets)
|
||||
}
|
||||
if len(cluster.ServiceAccounts) != 1 || cluster.ServiceAccounts[0].SecretCount != 1 || cluster.ServiceAccounts[0].AutomountServiceAccountToken == nil || !*cluster.ServiceAccounts[0].AutomountServiceAccountToken {
|
||||
|
|
|
|||
|
|
@ -511,13 +511,13 @@ func TestKubernetesNativeInventoryAdapters(t *testing.T) {
|
|||
t.Fatalf("unexpected storage class resource: %+v", storageClass)
|
||||
}
|
||||
|
||||
configMap, _ := resourceFromKubernetesConfigMap(cluster, models.KubernetesConfigMap{Name: "checkout-config", Namespace: "services", DataKeys: []string{"app.yaml"}, BinaryDataKeys: []string{"logo.png"}, Immutable: true}, nil)
|
||||
if configMap.Type != ResourceTypeK8sConfigMap || configMap.Kubernetes == nil || configMap.Kubernetes.DataKeys[0] != "app.yaml" || !configMap.Kubernetes.Immutable {
|
||||
configMap, _ := resourceFromKubernetesConfigMap(cluster, models.KubernetesConfigMap{Name: "checkout-config", Namespace: "services", DataKeys: []string{"app.yaml"}, BinaryDataKeys: []string{"logo.png"}, Immutable: true, MetadataOnly: true}, nil)
|
||||
if configMap.Type != ResourceTypeK8sConfigMap || configMap.Kubernetes == nil || configMap.Kubernetes.DataKeys[0] != "app.yaml" || !configMap.Kubernetes.Immutable || !configMap.Kubernetes.MetadataOnly {
|
||||
t.Fatalf("unexpected configmap resource: %+v", configMap)
|
||||
}
|
||||
|
||||
secret, _ := resourceFromKubernetesSecret(cluster, models.KubernetesSecret{Name: "checkout-secret", Namespace: "services", Type: "Opaque", DataKeys: []string{"token"}, Immutable: true}, nil)
|
||||
if secret.Type != ResourceTypeK8sSecret || secret.Kubernetes == nil || secret.Kubernetes.SecretType != "Opaque" || secret.Kubernetes.DataKeys[0] != "token" || !secret.Kubernetes.Immutable {
|
||||
secret, _ := resourceFromKubernetesSecret(cluster, models.KubernetesSecret{Name: "checkout-secret", Namespace: "services", Type: "Opaque", DataKeys: []string{"token"}, Immutable: true, MetadataOnly: true}, nil)
|
||||
if secret.Type != ResourceTypeK8sSecret || secret.Kubernetes == nil || secret.Kubernetes.SecretType != "Opaque" || secret.Kubernetes.DataKeys[0] != "token" || !secret.Kubernetes.Immutable || !secret.Kubernetes.MetadataOnly {
|
||||
t.Fatalf("unexpected secret resource: %+v", secret)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2935,6 +2935,7 @@ func resourceFromKubernetesConfigMap(cluster models.KubernetesCluster, configMap
|
|||
data.DataKeys = append([]string(nil), configMap.DataKeys...)
|
||||
data.BinaryDataKeys = append([]string(nil), configMap.BinaryDataKeys...)
|
||||
data.Immutable = configMap.Immutable
|
||||
data.MetadataOnly = configMap.MetadataOnly
|
||||
data.Labels = labels
|
||||
data.CreatedAt = zeroTimeToPtr(configMap.CreatedAt)
|
||||
resource := Resource{
|
||||
|
|
@ -2960,6 +2961,7 @@ func resourceFromKubernetesSecret(cluster models.KubernetesCluster, secret model
|
|||
data.SecretType = secret.Type
|
||||
data.DataKeys = append([]string(nil), secret.DataKeys...)
|
||||
data.Immutable = secret.Immutable
|
||||
data.MetadataOnly = secret.MetadataOnly
|
||||
data.Labels = labels
|
||||
data.CreatedAt = zeroTimeToPtr(secret.CreatedAt)
|
||||
resource := Resource{
|
||||
|
|
|
|||
|
|
@ -121,13 +121,13 @@ func TestContractResourceType(t *testing.T) {
|
|||
want: ResourceTypeK8sStorageClass,
|
||||
},
|
||||
{
|
||||
name: "kubernetes configmap passthrough remains canonical",
|
||||
resource: Resource{Type: ResourceTypeK8sConfigMap, Kubernetes: &K8sData{DataKeys: []string{"app.yaml"}}},
|
||||
name: "kubernetes metadata-only configmap passthrough remains canonical",
|
||||
resource: Resource{Type: ResourceTypeK8sConfigMap, Kubernetes: &K8sData{MetadataOnly: true}},
|
||||
want: ResourceTypeK8sConfigMap,
|
||||
},
|
||||
{
|
||||
name: "kubernetes secret passthrough remains canonical",
|
||||
resource: Resource{Type: ResourceTypeK8sSecret, Kubernetes: &K8sData{SecretType: "Opaque", DataKeys: []string{"token"}}},
|
||||
name: "kubernetes metadata-only secret passthrough remains canonical",
|
||||
resource: Resource{Type: ResourceTypeK8sSecret, Kubernetes: &K8sData{MetadataOnly: true}},
|
||||
want: ResourceTypeK8sSecret,
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1579,6 +1579,7 @@ type K8sData struct {
|
|||
DataKeys []string `json:"dataKeys,omitempty"`
|
||||
BinaryDataKeys []string `json:"binaryDataKeys,omitempty"`
|
||||
Immutable bool `json:"immutable,omitempty"`
|
||||
MetadataOnly bool `json:"metadataOnly,omitempty"`
|
||||
SecretType string `json:"secretType,omitempty"`
|
||||
AutomountServiceAccountToken *bool `json:"automountServiceAccountToken,omitempty"`
|
||||
SecretCount int `json:"secretCount,omitempty"`
|
||||
|
|
|
|||
|
|
@ -328,7 +328,9 @@ type StorageClass struct {
|
|||
}
|
||||
|
||||
// ConfigMap represents Kubernetes ConfigMap metadata at report time.
|
||||
// Data values are intentionally omitted; only keys are reported.
|
||||
// ConfigMap payload values are intentionally omitted. Current agents prefer
|
||||
// Kubernetes metadata-only API responses and therefore do not need to receive
|
||||
// ConfigMap data values at all; older reports may include key names only.
|
||||
type ConfigMap struct {
|
||||
UID string `json:"uid"`
|
||||
Name string `json:"name"`
|
||||
|
|
@ -336,21 +338,25 @@ type ConfigMap struct {
|
|||
DataKeys []string `json:"dataKeys,omitempty"`
|
||||
BinaryDataKeys []string `json:"binaryDataKeys,omitempty"`
|
||||
Immutable bool `json:"immutable,omitempty"`
|
||||
MetadataOnly bool `json:"metadataOnly,omitempty"`
|
||||
CreatedAt time.Time `json:"createdAt,omitempty"`
|
||||
Labels map[string]string `json:"labels,omitempty"`
|
||||
}
|
||||
|
||||
// Secret represents Kubernetes Secret metadata at report time.
|
||||
// Secret values are intentionally omitted; only key names and type metadata are reported.
|
||||
// Secret payload values are intentionally omitted. Current agents use
|
||||
// Kubernetes metadata-only API responses for Secret inventory; older reports
|
||||
// may include type and key names, but never Secret values.
|
||||
type Secret struct {
|
||||
UID string `json:"uid"`
|
||||
Name string `json:"name"`
|
||||
Namespace string `json:"namespace"`
|
||||
Type string `json:"type,omitempty"`
|
||||
DataKeys []string `json:"dataKeys,omitempty"`
|
||||
Immutable bool `json:"immutable,omitempty"`
|
||||
CreatedAt time.Time `json:"createdAt,omitempty"`
|
||||
Labels map[string]string `json:"labels,omitempty"`
|
||||
UID string `json:"uid"`
|
||||
Name string `json:"name"`
|
||||
Namespace string `json:"namespace"`
|
||||
Type string `json:"type,omitempty"`
|
||||
DataKeys []string `json:"dataKeys,omitempty"`
|
||||
Immutable bool `json:"immutable,omitempty"`
|
||||
MetadataOnly bool `json:"metadataOnly,omitempty"`
|
||||
CreatedAt time.Time `json:"createdAt,omitempty"`
|
||||
Labels map[string]string `json:"labels,omitempty"`
|
||||
}
|
||||
|
||||
// ServiceAccount represents Kubernetes ServiceAccount metadata at report time.
|
||||
|
|
|
|||
|
|
@ -220,6 +220,7 @@ class CanonicalCompletionGuardTest(unittest.TestCase):
|
|||
"memory-source-runtime",
|
||||
"docker-collect-runtime",
|
||||
"docker-swarm-runtime",
|
||||
"kubernetes-native-agent-runtime",
|
||||
"docker-runtime-report-model",
|
||||
"proxmox-zfs-runtime",
|
||||
"proxmox-ceph-runtime",
|
||||
|
|
@ -771,6 +772,7 @@ class CanonicalCompletionGuardTest(unittest.TestCase):
|
|||
"frontend-modern/src/features/docker/__tests__/dockerPageModel.test.ts",
|
||||
"frontend-modern/src/features/infrastructure/__tests__/InfrastructurePageSurface.guardrails.test.ts",
|
||||
"frontend-modern/src/features/infrastructure/__tests__/infrastructurePageModel.test.ts",
|
||||
"frontend-modern/src/features/kubernetes/__tests__/KubernetesInventoryTable.test.tsx",
|
||||
"frontend-modern/src/hooks/__tests__/useUnifiedResources.test.ts",
|
||||
"frontend-modern/src/routing/__tests__/resourceLinks.test.ts",
|
||||
"frontend-modern/src/stores/__tests__/websocket-unified.test.ts",
|
||||
|
|
|
|||
|
|
@ -4810,6 +4810,7 @@ class SubsystemLookupTest(unittest.TestCase):
|
|||
"frontend-modern/src/features/docker/__tests__/dockerPageModel.test.ts",
|
||||
"frontend-modern/src/features/infrastructure/__tests__/InfrastructurePageSurface.guardrails.test.ts",
|
||||
"frontend-modern/src/features/infrastructure/__tests__/infrastructurePageModel.test.ts",
|
||||
"frontend-modern/src/features/kubernetes/__tests__/KubernetesInventoryTable.test.tsx",
|
||||
"frontend-modern/src/hooks/__tests__/useUnifiedResources.test.ts",
|
||||
"frontend-modern/src/routing/__tests__/resourceLinks.test.ts",
|
||||
"frontend-modern/src/stores/__tests__/websocket-unified.test.ts",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue