Files
troubleshoot/pkg/collect/cluster_resources_test.go
ada mancini 73017ec48e feat: collect CertificateSigningRequests in clusterResources collector (#1964)
* Add .worktrees to .gitignore

Prevent worktree directories from being tracked in the repository.

* feat: collect CertificateSigningRequests in clusterResources collector

Add support for collecting CertificateSigningRequests (CSRs) from the
certificates.k8s.io/v1 API in the clusterResources collector.

Changes:
- Added certificateSigningRequests() helper function in cluster_resources.go
  following the existing pattern for other cluster-scoped resources
- Integrated CSR collection into the Collect() method between
  volumeAttachments and configMaps
- Added CLUSTER_RESOURCES_CERTIFICATE_SIGNING_REQUESTS constant
- Implemented fail-safe error handling for permission denied scenarios
  (e.g., managed clusters like EKS that may deny CSR access)

Testing:
- Added Test_CertificateSigningRequests() with table-driven tests for
  single and multiple CSR collection scenarios
- Added Test_CertificateSigningRequests_PermissionDenied() to verify
  fail-safe behavior when API access is forbidden
- All existing tests pass with no regressions

CSRs are saved to: cluster-resources/certificatesigningrequests.json
Errors are saved to: cluster-resources/certificatesigningrequests-errors.json

* style: run make fmt to align constant declarations

Formatting changes only - realigned constant declarations for
consistent spacing.

* fix: add .worktrees as separate line in .gitignore

The /support-bundle directory should remain ignored (for built
binaries), and /.worktrees/ should be added as a separate line.
2026-01-21 14:18:53 -05:00

779 lines
23 KiB
Go

package collect
import (
"context"
"encoding/json"
"fmt"
"os"
"reflect"
"testing"
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
"github.com/replicatedhq/troubleshoot/pkg/client/troubleshootclientset/scheme"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
certificatesv1 "k8s.io/api/certificates/v1"
v1 "k8s.io/api/coordination/v1"
corev1 "k8s.io/api/core/v1"
policyv1 "k8s.io/api/policy/v1"
policyv1beta1 "k8s.io/api/policy/v1beta1"
storagev1 "k8s.io/api/storage/v1"
apixfake "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/fake"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/intstr"
fakediscovery "k8s.io/client-go/discovery/fake"
testdynamicclient "k8s.io/client-go/dynamic/fake"
"k8s.io/client-go/kubernetes"
testclient "k8s.io/client-go/kubernetes/fake"
k8stesting "k8s.io/client-go/testing"
"sigs.k8s.io/yaml"
)
func init() {
apixfake.AddToScheme(scheme.Scheme)
}
func Test_ConfigMaps(t *testing.T) {
tests := []struct {
name string
configMapNames []string
namespaces []string
}{
{
name: "single namespace",
configMapNames: []string{"default"},
namespaces: []string{"default"},
},
{
name: "multiple namespaces",
configMapNames: []string{"default"},
namespaces: []string{"default", "test"},
},
{
name: "multiple in different namespaces",
configMapNames: []string{"default", "test-cm"},
namespaces: []string{"default", "test"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
client := testclient.NewSimpleClientset()
ctx := context.Background()
err := createConfigMaps(client, tt.configMapNames, tt.namespaces)
assert.NoError(t, err)
configMaps, _ := configMaps(ctx, client, tt.namespaces)
assert.Equal(t, len(tt.namespaces), len(configMaps))
for _, ns := range tt.namespaces {
assert.NotEmpty(t, configMaps[ns+".json"])
var configmapList corev1.ConfigMapList
err := json.Unmarshal(configMaps[ns+".json"], &configmapList)
assert.NoError(t, err)
// Ensure the ConfigMap names match those in the list
assert.Equal(t, len(configmapList.Items), len(tt.configMapNames))
for _, cm := range configmapList.Items {
assert.Contains(t, tt.configMapNames, cm.ObjectMeta.Name)
}
}
})
}
}
func createConfigMaps(client kubernetes.Interface, configMapNames []string, namespaces []string) error {
for _, ns := range namespaces {
for _, cmName := range configMapNames {
_, err := client.CoreV1().ConfigMaps(ns).Create(context.Background(), &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: cmName,
},
}, metav1.CreateOptions{})
if err != nil {
return err
}
}
}
return nil
}
func Test_VolumeAttachments(t *testing.T) {
tests := []struct {
name string
volumeAttachmentNames []string
}{
{
name: "single volume attachment",
volumeAttachmentNames: []string{"default"},
},
{
name: "multiple volume attachments",
volumeAttachmentNames: []string{"default", "test"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
client := testclient.NewSimpleClientset()
ctx := context.Background()
err := createTestVolumeAttachments(client, tt.volumeAttachmentNames)
assert.NoError(t, err)
volumeAttachments, _ := volumeAttachments(ctx, client)
assert.NotEmpty(t, volumeAttachments)
var volumeAttachmentList storagev1.VolumeAttachmentList
err = json.Unmarshal(volumeAttachments, &volumeAttachmentList)
assert.NoError(t, err)
// Ensure the VolumeAttachment names match those in the list
assert.Equal(t, len(volumeAttachmentList.Items), len(tt.volumeAttachmentNames))
for _, va := range volumeAttachmentList.Items {
assert.Contains(t, tt.volumeAttachmentNames, va.ObjectMeta.Name)
}
})
}
}
func createTestVolumeAttachments(client kubernetes.Interface, volumeAttachmentNames []string) error {
for _, vaName := range volumeAttachmentNames {
_, err := client.StorageV1().VolumeAttachments().Create(context.Background(), &storagev1.VolumeAttachment{
ObjectMeta: metav1.ObjectMeta{
Name: vaName,
},
}, metav1.CreateOptions{})
if err != nil {
return err
}
}
return nil
}
func Test_Leases(t *testing.T) {
tests := []struct {
name string
leaseNames []string
namespaces []string
}{
{
name: "single namespace",
leaseNames: []string{"default"},
namespaces: []string{"default"},
},
{
name: "multiple namespaces",
leaseNames: []string{"default"},
namespaces: []string{"default", "test"},
},
{
name: "multiple in different namespaces",
leaseNames: []string{"default", "test-lease"},
namespaces: []string{"default", "test"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
client := testclient.NewSimpleClientset()
ctx := context.Background()
err := createTestLeases(client, tt.leaseNames, tt.namespaces)
assert.NoError(t, err)
leases, _ := leases(ctx, client, tt.namespaces)
assert.Equal(t, len(tt.namespaces), len(leases))
for _, ns := range tt.namespaces {
assert.NotEmpty(t, leases[ns+".json"])
var leaseList v1.LeaseList
err := json.Unmarshal(leases[ns+".json"], &leaseList)
assert.NoError(t, err)
// Ensure the Lease names match those in the list
assert.Equal(t, len(leaseList.Items), len(tt.leaseNames))
for _, lease := range leaseList.Items {
assert.Contains(t, tt.leaseNames, lease.ObjectMeta.Name)
}
}
})
}
}
func createTestLeases(client kubernetes.Interface, leaseNames []string, namespaces []string) error {
for _, ns := range namespaces {
for _, leaseName := range leaseNames {
_, err := client.CoordinationV1().Leases(ns).Create(context.Background(), &v1.Lease{
ObjectMeta: metav1.ObjectMeta{
Name: leaseName,
},
}, metav1.CreateOptions{})
if err != nil {
return err
}
}
}
return nil
}
func Test_ServiceAccounts(t *testing.T) {
tests := []struct {
name string
serviceAccountNames []string
namespaces []string
}{
{
name: "single namespace",
serviceAccountNames: []string{"default"},
namespaces: []string{"default"},
},
{
name: "multiple namespaces",
serviceAccountNames: []string{"default"},
namespaces: []string{"default", "test"},
},
{
name: "multiple in different namespaces",
serviceAccountNames: []string{"default", "test-sa"},
namespaces: []string{"default", "test"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
client := testclient.NewSimpleClientset()
ctx := context.Background()
err := createTestServiceAccounts(client, tt.serviceAccountNames, tt.namespaces)
assert.NoError(t, err)
servicesAccounts, _ := serviceAccounts(ctx, client, tt.namespaces)
assert.Equal(t, len(tt.namespaces), len(servicesAccounts))
for _, ns := range tt.namespaces {
assert.NotEmpty(t, servicesAccounts[ns+".json"])
var serviceAccountList corev1.ServiceAccountList
err := json.Unmarshal(servicesAccounts[ns+".json"], &serviceAccountList)
assert.NoError(t, err)
// Ensure the ServiceAccount names match those in the list
assert.Equal(t, len(serviceAccountList.Items), len(tt.serviceAccountNames))
for _, sa := range serviceAccountList.Items {
assert.Contains(t, tt.serviceAccountNames, sa.ObjectMeta.Name)
}
}
})
}
}
func createTestServiceAccounts(client kubernetes.Interface, serviceAccountNames []string, namespaces []string) error {
for _, ns := range namespaces {
for _, saName := range serviceAccountNames {
_, err := client.CoreV1().ServiceAccounts(ns).Create(context.Background(), &corev1.ServiceAccount{
ObjectMeta: metav1.ObjectMeta{
Name: saName,
},
}, metav1.CreateOptions{})
if err != nil {
return err
}
}
}
return nil
}
func Test_SelectCRDVersionByPriority(t *testing.T) {
assert.Equal(t, "v1alpha3", selectCRDVersionByPriority([]string{"v1alpha2", "v1alpha3"}))
assert.Equal(t, "v1alpha3", selectCRDVersionByPriority([]string{"v1alpha3", "v1alpha2"}))
assert.Equal(t, "v1", selectCRDVersionByPriority([]string{"v1alpha2", "v1alpha3", "v1"}))
assert.Equal(t, "v1", selectCRDVersionByPriority([]string{"v1", "v1alpha2", "v1alpha3"}))
}
func TestClusterResources_Merge(t *testing.T) {
tests := []struct {
name string
Collectors []troubleshootv1beta2.Collect
want *CollectClusterResources
}{
{
name: "single cluster resources collector with multiple unique namespaces",
Collectors: []troubleshootv1beta2.Collect{
{
ClusterResources: &troubleshootv1beta2.ClusterResources{
CollectorMeta: troubleshootv1beta2.CollectorMeta{
CollectorName: "collectorname",
},
Namespaces: []string{"hello", "hello2"},
},
},
},
want: &CollectClusterResources{
Collector: &troubleshootv1beta2.ClusterResources{
CollectorMeta: troubleshootv1beta2.CollectorMeta{
CollectorName: "collectorname",
},
Namespaces: []string{"hello", "hello2"},
},
},
},
{
name: "multiple cluster resources collectors with unique namespaces",
Collectors: []troubleshootv1beta2.Collect{
{
ClusterResources: &troubleshootv1beta2.ClusterResources{
CollectorMeta: troubleshootv1beta2.CollectorMeta{
CollectorName: "collectorname",
},
Namespaces: []string{"hello"},
},
},
{
ClusterResources: &troubleshootv1beta2.ClusterResources{
CollectorMeta: troubleshootv1beta2.CollectorMeta{
CollectorName: "collectorname",
},
Namespaces: []string{"hello2"},
},
},
},
want: &CollectClusterResources{
Collector: &troubleshootv1beta2.ClusterResources{
CollectorMeta: troubleshootv1beta2.CollectorMeta{
CollectorName: "collectorname",
},
Namespaces: []string{"hello", "hello2"},
},
},
},
{
name: "multiple cluster resources collectors with duplicate namespaces",
Collectors: []troubleshootv1beta2.Collect{
{
ClusterResources: &troubleshootv1beta2.ClusterResources{
CollectorMeta: troubleshootv1beta2.CollectorMeta{
CollectorName: "collectorname",
},
Namespaces: []string{"hello"},
},
},
{
ClusterResources: &troubleshootv1beta2.ClusterResources{
CollectorMeta: troubleshootv1beta2.CollectorMeta{
CollectorName: "collectorname",
},
Namespaces: []string{"hello2"},
},
},
{
ClusterResources: &troubleshootv1beta2.ClusterResources{
CollectorMeta: troubleshootv1beta2.CollectorMeta{
CollectorName: "collectorname",
},
Namespaces: []string{"hello"},
},
},
},
want: &CollectClusterResources{
Collector: &troubleshootv1beta2.ClusterResources{
CollectorMeta: troubleshootv1beta2.CollectorMeta{
CollectorName: "collectorname",
},
Namespaces: []string{"hello", "hello2"},
},
},
},
{
name: "multiple cluster resource collectors with a empty string namespace provided",
Collectors: []troubleshootv1beta2.Collect{
{
ClusterResources: &troubleshootv1beta2.ClusterResources{
CollectorMeta: troubleshootv1beta2.CollectorMeta{
CollectorName: "collectorname",
},
Namespaces: []string{"hello"},
},
},
{
ClusterResources: &troubleshootv1beta2.ClusterResources{
CollectorMeta: troubleshootv1beta2.CollectorMeta{
CollectorName: "collectorname",
},
Namespaces: []string{"hello2"},
},
},
{
ClusterResources: &troubleshootv1beta2.ClusterResources{
CollectorMeta: troubleshootv1beta2.CollectorMeta{
CollectorName: "collectorname",
},
Namespaces: []string{""},
},
},
},
want: &CollectClusterResources{
Collector: &troubleshootv1beta2.ClusterResources{
CollectorMeta: troubleshootv1beta2.CollectorMeta{
CollectorName: "collectorname",
},
Namespaces: nil,
},
},
},
{
name: "multiple cluster resource collectors with a nil namespace provided",
Collectors: []troubleshootv1beta2.Collect{
{
ClusterResources: &troubleshootv1beta2.ClusterResources{
CollectorMeta: troubleshootv1beta2.CollectorMeta{
CollectorName: "collectorname",
},
Namespaces: []string{"hello"},
},
},
{
ClusterResources: &troubleshootv1beta2.ClusterResources{
CollectorMeta: troubleshootv1beta2.CollectorMeta{
CollectorName: "collectorname",
},
Namespaces: []string{"hello2"},
},
},
{
ClusterResources: &troubleshootv1beta2.ClusterResources{
CollectorMeta: troubleshootv1beta2.CollectorMeta{
CollectorName: "collectorname",
},
Namespaces: nil,
},
},
},
want: &CollectClusterResources{
Collector: &troubleshootv1beta2.ClusterResources{
CollectorMeta: troubleshootv1beta2.CollectorMeta{
CollectorName: "collectorname",
},
Namespaces: nil,
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
req := require.New(t)
var mergedCollectors []Collector
allCollectors := make(map[reflect.Type][]Collector)
collectorType := reflect.TypeOf(CollectClusterResources{})
for _, collector := range tt.Collectors {
collectorInterface, _ := GetCollector(&collector, "", "", nil, nil, nil)
if mergeCollector, ok := collectorInterface.(MergeableCollector); ok {
allCollectors[collectorType] = append(allCollectors[collectorType], mergeCollector)
}
}
for _, collectors := range allCollectors {
if mergeCollector, ok := collectors[0].(MergeableCollector); ok {
mergedCollectors, _ = mergeCollector.Merge(collectors)
}
}
clusterResourceCollector, _ := mergedCollectors[0].(*CollectClusterResources)
req.EqualValues(tt.want, clusterResourceCollector)
})
}
}
func TestCollectClusterResources_CustomResource(t *testing.T) {
ctx := context.Background()
// Register supportbundle troubleshoot CRD
dat, err := os.ReadFile("../../config/crds/troubleshoot.sh_supportbundles.yaml")
require.NoError(t, err)
obj, _, err := scheme.Codecs.UniversalDeserializer().Decode(dat, nil, nil)
require.NoError(t, err)
apixClient := apixfake.NewSimpleClientset(obj)
// Create a CR
sbObject := troubleshootv1beta2.SupportBundle{
ObjectMeta: metav1.ObjectMeta{
Name: "supportbundle",
Namespace: "default",
},
TypeMeta: metav1.TypeMeta{
Kind: "SupportBundle",
APIVersion: "troubleshoot.sh/v1beta2",
},
Spec: troubleshootv1beta2.SupportBundleSpec{
Collectors: []*troubleshootv1beta2.Collect{
{
ClusterResources: &troubleshootv1beta2.ClusterResources{},
},
},
},
}
dynamicClient := testdynamicclient.NewSimpleDynamicClient(scheme.Scheme, &sbObject)
// Fetch the CR from cluster
res, errs := crsV1(ctx, dynamicClient, apixClient.ApiextensionsV1(), []string{"default"})
assert.Empty(t, errs)
require.Equal(t, 2, len(res))
assert.Equal(t, fromJSON(t, res["supportbundles.troubleshoot.sh/default.json"]), sbObject)
assert.Equal(t, fromYAML(t, res["supportbundles.troubleshoot.sh/default.yaml"]), sbObject)
}
func fromYAML(t *testing.T, dat []byte) troubleshootv1beta2.SupportBundle {
sb := []troubleshootv1beta2.SupportBundle{}
err := yaml.Unmarshal(dat, &sb)
require.NoError(t, err)
require.Equal(t, 1, len(sb))
return sb[0]
}
func fromJSON(t *testing.T, dat []byte) troubleshootv1beta2.SupportBundle {
sb := []troubleshootv1beta2.SupportBundle{}
err := json.Unmarshal(dat, &sb)
require.NoError(t, err)
require.Equal(t, 1, len(sb))
return sb[0]
}
func Test_getPodDisruptionBudgets(t *testing.T) {
tests := []struct {
name string
pdbNames []string
namespaces []string
}{
{
name: "single namespace",
pdbNames: []string{"test-pdb"},
namespaces: []string{"default"},
},
{
name: "multiple namespaces",
pdbNames: []string{"test-pdb"},
namespaces: []string{"default", "test"},
},
{
name: "multiple pdbs in different namespaces",
pdbNames: []string{"test-pdb", "another-pdb"},
namespaces: []string{"default", "test"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
client := testclient.NewClientset()
ctx := context.Background()
err := createTestPodDisruptionBudgets(client, tt.pdbNames, tt.namespaces)
assert.NoError(t, err)
fakeDiscovery, ok := client.Discovery().(*fakediscovery.FakeDiscovery)
if !ok {
t.Fatalf("could not convert Discovery() to *FakeDiscovery")
}
fakeDiscovery.Resources = []*metav1.APIResourceList{
{
GroupVersion: "policy/v1",
APIResources: []metav1.APIResource{
{
Kind: "PodDisruptionBudget",
},
},
},
}
pdbs, errors := getPodDisruptionBudgets(ctx, client, tt.namespaces)
assert.Empty(t, errors)
assert.Equal(t, len(tt.namespaces), len(pdbs))
for _, ns := range tt.namespaces {
assert.NotEmpty(t, pdbs[ns+".json"])
var pdbList policyv1.PodDisruptionBudgetList
err := json.Unmarshal(pdbs[ns+".json"], &pdbList)
assert.NoError(t, err)
assert.Equal(t, len(tt.pdbNames), len(pdbList.Items))
for _, pdb := range pdbList.Items {
assert.Contains(t, tt.pdbNames, pdb.ObjectMeta.Name)
}
}
})
}
}
func Test_getPodDisruptionBudgets_v1beta1(t *testing.T) {
tests := []struct {
name string
pdbNames []string
namespaces []string
}{
{
name: "single namespace v1beta1",
pdbNames: []string{"test-pdb-beta"},
namespaces: []string{"default"},
},
{
name: "multiple namespaces v1beta1",
pdbNames: []string{"test-pdb-beta"},
namespaces: []string{"default", "test"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
client := testclient.NewClientset()
ctx := context.Background()
err := createTestPodDisruptionBudgetsV1beta1(client, tt.pdbNames, tt.namespaces)
assert.NoError(t, err)
fakeDiscovery, ok := client.Discovery().(*fakediscovery.FakeDiscovery)
if !ok {
t.Fatalf("could not convert Discovery() to *FakeDiscovery")
}
// Mock discovery to only have v1beta1 PodDisruptionBudget
fakeDiscovery.Resources = []*metav1.APIResourceList{
{
GroupVersion: "policy/v1beta1",
APIResources: []metav1.APIResource{
{
Kind: "PodDisruptionBudget",
},
},
},
}
pdbs, errors := getPodDisruptionBudgets(ctx, client, tt.namespaces)
assert.Empty(t, errors)
assert.Equal(t, len(tt.namespaces), len(pdbs))
for _, ns := range tt.namespaces {
assert.NotEmpty(t, pdbs[ns+".json"])
var pdbList policyv1beta1.PodDisruptionBudgetList
err := json.Unmarshal(pdbs[ns+".json"], &pdbList)
assert.NoError(t, err)
assert.Equal(t, len(tt.pdbNames), len(pdbList.Items))
for _, pdb := range pdbList.Items {
assert.Contains(t, tt.pdbNames, pdb.ObjectMeta.Name)
}
}
})
}
}
func createTestPodDisruptionBudgets(client kubernetes.Interface, pdbNames []string, namespaces []string) error {
for _, ns := range namespaces {
for _, pdbName := range pdbNames {
minAvailable := intstr.FromInt32(1)
_, err := client.PolicyV1().PodDisruptionBudgets(ns).Create(context.Background(), &policyv1.PodDisruptionBudget{
ObjectMeta: metav1.ObjectMeta{
Name: pdbName,
},
Spec: policyv1.PodDisruptionBudgetSpec{
MinAvailable: &minAvailable,
Selector: &metav1.LabelSelector{
MatchLabels: map[string]string{
"app": "test-app",
},
},
},
}, metav1.CreateOptions{})
if err != nil {
return err
}
}
}
return nil
}
func createTestPodDisruptionBudgetsV1beta1(client kubernetes.Interface, pdbNames []string, namespaces []string) error {
for _, ns := range namespaces {
for _, pdbName := range pdbNames {
minAvailable := intstr.FromInt32(1)
_, err := client.PolicyV1beta1().PodDisruptionBudgets(ns).Create(context.Background(), &policyv1beta1.PodDisruptionBudget{
ObjectMeta: metav1.ObjectMeta{
Name: pdbName,
},
Spec: policyv1beta1.PodDisruptionBudgetSpec{
MinAvailable: &minAvailable,
Selector: &metav1.LabelSelector{
MatchLabels: map[string]string{
"app": "test-app",
},
},
},
}, metav1.CreateOptions{})
if err != nil {
return err
}
}
}
return nil
}
func Test_CertificateSigningRequests(t *testing.T) {
tests := []struct {
name string
csrNames []string
}{
{
name: "single certificate signing request",
csrNames: []string{"test-csr"},
},
{
name: "multiple certificate signing requests",
csrNames: []string{"test-csr-1", "test-csr-2", "test-csr-3"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
client := testclient.NewSimpleClientset()
ctx := context.Background()
err := createTestCertificateSigningRequests(client, tt.csrNames)
assert.NoError(t, err)
csrs, csrErrors := certificateSigningRequests(ctx, client)
assert.Empty(t, csrErrors)
assert.NotEmpty(t, csrs)
var csrList certificatesv1.CertificateSigningRequestList
err = json.Unmarshal(csrs, &csrList)
assert.NoError(t, err)
assert.Equal(t, len(tt.csrNames), len(csrList.Items))
for _, csr := range csrList.Items {
assert.Contains(t, tt.csrNames, csr.ObjectMeta.Name)
}
})
}
}
func Test_CertificateSigningRequests_PermissionDenied(t *testing.T) {
client := testclient.NewSimpleClientset()
ctx := context.Background()
// Add a reactor to simulate permission denied error
client.PrependReactor("list", "certificatesigningrequests", func(action k8stesting.Action) (handled bool, ret runtime.Object, err error) {
return true, nil, fmt.Errorf("certificatesigningrequests.certificates.k8s.io is forbidden: User \"system:serviceaccount:default:default\" cannot list resource \"certificatesigningrequests\" in API group \"certificates.k8s.io\" at the cluster scope")
})
csrs, csrErrors := certificateSigningRequests(ctx, client)
// Verify fail-safe behavior: returns nil data + error string (not panic)
assert.Nil(t, csrs)
assert.NotEmpty(t, csrErrors)
assert.Len(t, csrErrors, 1)
// Verify the error is captured as a string
assert.IsType(t, "", csrErrors[0])
assert.Contains(t, csrErrors[0], "forbidden")
}
func createTestCertificateSigningRequests(client kubernetes.Interface, csrNames []string) error {
for _, csrName := range csrNames {
_, err := client.CertificatesV1().CertificateSigningRequests().Create(context.Background(), &certificatesv1.CertificateSigningRequest{
ObjectMeta: metav1.ObjectMeta{
Name: csrName,
},
Spec: certificatesv1.CertificateSigningRequestSpec{
Request: []byte("-----BEGIN CERTIFICATE REQUEST-----\ntest\n-----END CERTIFICATE REQUEST-----"),
SignerName: "kubernetes.io/kube-apiserver-client",
Usages: []certificatesv1.KeyUsage{certificatesv1.UsageClientAuth},
},
}, metav1.CreateOptions{})
if err != nil {
return err
}
}
return nil
}