Pulse/internal/kubernetesagent/agent_inventory_test.go
rcourtman b707512e38 Clear all errcheck and gofmt violations so make lint gates on real findings
golangci-lint run ./... failed on ~190 pre-existing errcheck violations and
5 unformatted files, burying any new regression in noise. Fix all of them:

- Test files that hand-rolled mock-mode set/restore (vmware, truenas, and
  friends) now use the canonical setMockModeForTest/testutil.SetMockMode
  helper instead of drift copies that ignored SetEnabled errors.
- internal/mock and internal/monitoring tests get package-local
  mustSetEnabled/mustSetMockEnabled/mustSetMonitorMockMode helpers that
  fail the test on toggle errors.
- pkg/auth/sqlite_manager.go, pkg/metrics/store.go, pkg/server/server.go:
  rollbacks in defers use the explicit-discard idiom, migration renames and
  rollup commits log failures, the hosted reaper goroutine logs an error
  exit, shutdown mock-disable logs failures.
- Remaining test sites check errors with t.Fatalf/t.Errorf or explicitly
  discard best-effort calls (restore-chmods, handler-closure unmarshals)
  per existing repo style.
- gofmt: internal/api/maintenance_verification.go, internal/ai/demo.go and
  three findings test files.

Only dupl findings remain (44 pre-existing production-code duplication
pairs) — those need real refactors, not mechanical fixes.

Full test suites pass for every touched package.
2026-06-09 21:42:21 +01:00

286 lines
11 KiB
Go

package kubernetesagent
import (
"context"
"errors"
"testing"
"time"
appsv1 "k8s.io/api/apps/v1"
autoscalingv2 "k8s.io/api/autoscaling/v2"
corev1 "k8s.io/api/core/v1"
policyv1 "k8s.io/api/policy/v1"
rbacv1 "k8s.io/api/rbac/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) {
minAvailable := intstr.FromInt(1)
clientset := fake.NewSimpleClientset(
&corev1.ResourceQuota{
ObjectMeta: metav1.ObjectMeta{UID: "quota-uid", Namespace: "apps", Name: "apps-quota"},
Spec: corev1.ResourceQuotaSpec{
Hard: corev1.ResourceList{corev1.ResourcePods: k8sresource.MustParse("10")},
},
Status: corev1.ResourceQuotaStatus{
Hard: corev1.ResourceList{corev1.ResourcePods: k8sresource.MustParse("10")},
Used: corev1.ResourceList{corev1.ResourcePods: k8sresource.MustParse("3")},
},
},
&corev1.LimitRange{
ObjectMeta: metav1.ObjectMeta{UID: "limits-uid", Namespace: "apps", Name: "apps-limits"},
Spec: corev1.LimitRangeSpec{
Limits: []corev1.LimitRangeItem{{Type: corev1.LimitTypeContainer}},
},
},
&policyv1.PodDisruptionBudget{
ObjectMeta: metav1.ObjectMeta{UID: "pdb-uid", Namespace: "apps", Name: "api-pdb"},
Spec: policyv1.PodDisruptionBudgetSpec{MinAvailable: &minAvailable},
Status: policyv1.PodDisruptionBudgetStatus{
DesiredHealthy: 1,
CurrentHealthy: 1,
DisruptionsAllowed: 1,
ExpectedPods: 2,
},
},
&autoscalingv2.HorizontalPodAutoscaler{
ObjectMeta: metav1.ObjectMeta{UID: "hpa-uid", Namespace: "apps", Name: "api-hpa"},
Spec: autoscalingv2.HorizontalPodAutoscalerSpec{
ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{Kind: "Deployment", Name: "api"},
MinReplicas: int32Ptr(2),
MaxReplicas: 10,
Metrics: []autoscalingv2.MetricSpec{{
Type: autoscalingv2.ResourceMetricSourceType,
Resource: &autoscalingv2.ResourceMetricSource{
Name: corev1.ResourceCPU,
Target: autoscalingv2.MetricTarget{
Type: autoscalingv2.UtilizationMetricType,
AverageUtilization: int32Ptr(70),
},
},
}},
},
Status: autoscalingv2.HorizontalPodAutoscalerStatus{CurrentReplicas: 2, DesiredReplicas: 3},
},
)
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()
if err := metav1.AddMetaToScheme(metadataScheme); err != nil {
t.Fatalf("add meta to scheme: %v", err)
}
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].Name != "api-secret" || !secrets[0].MetadataOnly || secrets[0].Type != "" || len(secrets[0].DataKeys) != 0 || secrets[0].Immutable {
t.Fatalf("unexpected secrets: %+v", secrets)
}
quotas, err := a.collectResourceQuotas(context.Background())
if err != nil {
t.Fatalf("collectResourceQuotas: %v", err)
}
if len(quotas) != 1 || quotas[0].Hard["pods"] != "10" || quotas[0].Used["pods"] != "3" {
t.Fatalf("unexpected quotas: %+v", quotas)
}
limitRanges, err := a.collectLimitRanges(context.Background())
if err != nil {
t.Fatalf("collectLimitRanges: %v", err)
}
if len(limitRanges) != 1 || len(limitRanges[0].LimitTypes) != 1 || limitRanges[0].LimitTypes[0] != "Container" {
t.Fatalf("unexpected limit ranges: %+v", limitRanges)
}
budgets, err := a.collectPodDisruptionBudgets(context.Background())
if err != nil {
t.Fatalf("collectPodDisruptionBudgets: %v", err)
}
if len(budgets) != 1 || budgets[0].MinAvailable != "1" || budgets[0].ExpectedPods != 2 || budgets[0].DisruptionsAllowed != 1 {
t.Fatalf("unexpected pod disruption budgets: %+v", budgets)
}
autoscalers, err := a.collectHorizontalPodAutoscalers(context.Background())
if err != nil {
t.Fatalf("collectHorizontalPodAutoscalers: %v", err)
}
if len(autoscalers) != 1 || autoscalers[0].TargetName != "api" || autoscalers[0].MinReplicas != 2 || autoscalers[0].MaxReplicas != 10 || autoscalers[0].MetricTypes[0] != "Resource:cpu" {
t.Fatalf("unexpected autoscalers: %+v", autoscalers)
}
}
// TestCollectRBACInventoryReportsSummaryCountsOnly verifies that the four
// RBAC collectors expose rule / subject counts and subject Kinds, but never
// surface individual subject names or full PolicyRule contents from the
// underlying k8s API objects.
func TestCollectRBACInventoryReportsSummaryCountsOnly(t *testing.T) {
clientset := fake.NewSimpleClientset(
&rbacv1.Role{
ObjectMeta: metav1.ObjectMeta{UID: "role-uid", Namespace: "apps", Name: "api-runtime"},
Rules: []rbacv1.PolicyRule{
{APIGroups: []string{""}, Resources: []string{"pods"}, Verbs: []string{"get", "list"}},
{APIGroups: []string{""}, Resources: []string{"services"}, Verbs: []string{"get"}},
},
},
&rbacv1.ClusterRole{
ObjectMeta: metav1.ObjectMeta{UID: "crole-uid", Name: "platform-monitoring"},
Rules: []rbacv1.PolicyRule{
{APIGroups: []string{""}, Resources: []string{"nodes"}, Verbs: []string{"get", "list", "watch"}},
},
AggregationRule: &rbacv1.AggregationRule{
ClusterRoleSelectors: []metav1.LabelSelector{
{MatchLabels: map[string]string{"rbac.authorization.k8s.io/aggregate-to-admin": "true"}},
},
},
},
&rbacv1.RoleBinding{
ObjectMeta: metav1.ObjectMeta{UID: "rb-uid", Namespace: "apps", Name: "api-runtime"},
RoleRef: rbacv1.RoleRef{Kind: "Role", Name: "api-runtime"},
Subjects: []rbacv1.Subject{
{Kind: "ServiceAccount", Name: "checkout", Namespace: "apps"},
{Kind: "Group", Name: "team-checkout"},
},
},
&rbacv1.ClusterRoleBinding{
ObjectMeta: metav1.ObjectMeta{UID: "crb-uid", Name: "platform-monitoring"},
RoleRef: rbacv1.RoleRef{Kind: "ClusterRole", Name: "platform-monitoring"},
Subjects: []rbacv1.Subject{
{Kind: "User", Name: "alice@example.test"},
{Kind: "Group", Name: "ops-oncall"},
{Kind: "ServiceAccount", Name: "metrics", Namespace: "monitoring"},
},
},
)
a := &Agent{kubeClient: clientset}
roles, err := a.collectRoles(context.Background())
if err != nil {
t.Fatalf("collectRoles: %v", err)
}
if len(roles) != 1 || roles[0].Name != "api-runtime" || roles[0].RuleCount != 2 {
t.Fatalf("unexpected role inventory: %+v", roles)
}
clusterRoles, err := a.collectClusterRoles(context.Background())
if err != nil {
t.Fatalf("collectClusterRoles: %v", err)
}
if len(clusterRoles) != 1 || clusterRoles[0].RuleCount != 1 {
t.Fatalf("unexpected clusterrole inventory: %+v", clusterRoles)
}
if clusterRoles[0].AggregationLabels["rbac.authorization.k8s.io/aggregate-to-admin"] != "true" {
t.Fatalf("expected aggregation label, got %+v", clusterRoles[0].AggregationLabels)
}
roleBindings, err := a.collectRoleBindings(context.Background())
if err != nil {
t.Fatalf("collectRoleBindings: %v", err)
}
if len(roleBindings) != 1 || roleBindings[0].RoleKind != "Role" || roleBindings[0].SubjectCount != 2 {
t.Fatalf("unexpected rolebinding inventory: %+v", roleBindings)
}
// Subject Kinds are reported (sorted, deduplicated); individual subject
// names must not leak through the agent contract.
gotKinds := roleBindings[0].SubjectKinds
if len(gotKinds) != 2 || gotKinds[0] != "Group" || gotKinds[1] != "ServiceAccount" {
t.Fatalf("expected sorted subject kinds [Group, ServiceAccount], got %+v", gotKinds)
}
clusterRoleBindings, err := a.collectClusterRoleBindings(context.Background())
if err != nil {
t.Fatalf("collectClusterRoleBindings: %v", err)
}
if len(clusterRoleBindings) != 1 || clusterRoleBindings[0].SubjectCount != 3 {
t.Fatalf("unexpected clusterrolebinding inventory: %+v", clusterRoleBindings)
}
if len(clusterRoleBindings[0].SubjectKinds) != 3 {
t.Fatalf("expected three subject kinds, got %+v", clusterRoleBindings[0].SubjectKinds)
}
}
func TestCollectDeploymentInventoryPreservesAPIMetadata(t *testing.T) {
replicas := int32(4)
createdAt := metav1.NewTime(time.Date(2026, 5, 24, 15, 0, 0, 0, time.UTC))
clientset := fake.NewSimpleClientset(&appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
UID: "deployment-uid-1",
Namespace: "services",
Name: "checkout-api",
CreationTimestamp: createdAt,
Labels: map[string]string{"app": "checkout"},
},
Spec: appsv1.DeploymentSpec{Replicas: &replicas},
Status: appsv1.DeploymentStatus{
ObservedGeneration: 12,
UpdatedReplicas: 3,
ReadyReplicas: 2,
AvailableReplicas: 2,
},
})
a := &Agent{kubeClient: clientset}
deployments, err := a.collectDeployments(context.Background())
if err != nil {
t.Fatalf("collectDeployments: %v", err)
}
if len(deployments) != 1 {
t.Fatalf("expected one deployment, got %+v", deployments)
}
deployment := deployments[0]
if deployment.UID != "deployment-uid-1" || deployment.Name != "checkout-api" || deployment.Namespace != "services" {
t.Fatalf("deployment identity metadata not preserved: %+v", deployment)
}
if !deployment.CreatedAt.Equal(createdAt.Time) || deployment.ObservedGeneration != 12 {
t.Fatalf("deployment API metadata not preserved: %+v", deployment)
}
}
func int32Ptr(value int32) *int32 {
return &value
}