diff --git a/.gitignore b/.gitignore index fee41d57..0a142a4b 100644 --- a/.gitignore +++ b/.gitignore @@ -55,4 +55,5 @@ sbom/ /troubleshoot-test cmd/troubleshoot/troubleshoot cmd/*/troubleshoot -/support-bundle \ No newline at end of file +/support-bundle +/.worktrees/ diff --git a/pkg/collect/cluster_resources.go b/pkg/collect/cluster_resources.go index 7a30024b..c0b7b593 100644 --- a/pkg/collect/cluster_resources.go +++ b/pkg/collect/cluster_resources.go @@ -393,6 +393,11 @@ func (c *CollectClusterResources) Collect(progressChan chan<- interface{}) (Coll output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, fmt.Sprintf("%s.json", constants.CLUSTER_RESOURCES_VOLUME_ATTACHMENTS)), bytes.NewBuffer(volumeAttachments)) output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, fmt.Sprintf("%s-errors.json", constants.CLUSTER_RESOURCES_VOLUME_ATTACHMENTS)), marshalErrors(volumeAttachmentsErrors)) + // Certificate Signing Requests + csrs, csrsErrors := certificateSigningRequests(ctx, client) + output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, fmt.Sprintf("%s.json", constants.CLUSTER_RESOURCES_CERTIFICATE_SIGNING_REQUESTS)), bytes.NewBuffer(csrs)) + output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, fmt.Sprintf("%s-errors.json", constants.CLUSTER_RESOURCES_CERTIFICATE_SIGNING_REQUESTS)), marshalErrors(csrsErrors)) + // ConfigMaps configMaps, configMapsErrors := configMaps(ctx, client, namespaceNames) for k, v := range configMaps { @@ -2129,6 +2134,32 @@ func volumeAttachments(ctx context.Context, client kubernetes.Interface) ([]byte return b, nil } +func certificateSigningRequests(ctx context.Context, client kubernetes.Interface) ([]byte, []string) { + csrs, err := client.CertificatesV1().CertificateSigningRequests().List(ctx, metav1.ListOptions{}) + if err != nil { + return nil, []string{err.Error()} + } + + gvk, err := apiutil.GVKForObject(csrs, scheme.Scheme) + if err == nil { + csrs.GetObjectKind().SetGroupVersionKind(gvk) + } + + for i, o := range csrs.Items { + gvk, err := apiutil.GVKForObject(&o, scheme.Scheme) + if err == nil { + csrs.Items[i].GetObjectKind().SetGroupVersionKind(gvk) + } + } + + b, err := json.MarshalIndent(csrs, "", " ") + if err != nil { + return nil, []string{err.Error()} + } + + return b, nil +} + func configMaps(ctx context.Context, client kubernetes.Interface, namespaces []string) (map[string][]byte, map[string]string) { configmapByNamespace := make(map[string][]byte) errorsByNamespace := make(map[string]string) diff --git a/pkg/collect/cluster_resources_test.go b/pkg/collect/cluster_resources_test.go index bc0b94cb..400bf359 100644 --- a/pkg/collect/cluster_resources_test.go +++ b/pkg/collect/cluster_resources_test.go @@ -3,6 +3,7 @@ package collect import ( "context" "encoding/json" + "fmt" "os" "reflect" "testing" @@ -11,6 +12,7 @@ import ( "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" @@ -18,11 +20,13 @@ import ( 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" ) @@ -697,3 +701,78 @@ func createTestPodDisruptionBudgetsV1beta1(client kubernetes.Interface, pdbNames } 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 +} diff --git a/pkg/constants/constants.go b/pkg/constants/constants.go index cb664a2e..a33b2bc1 100644 --- a/pkg/constants/constants.go +++ b/pkg/constants/constants.go @@ -23,45 +23,46 @@ const ( ANALYSIS_FILENAME = "analysis.json" // Cluster Resources Collector Directories - CLUSTER_RESOURCES_DIR = "cluster-resources" - CLUSTER_RESOURCES_NAMESPACES = "namespaces" - CLUSTER_RESOURCES_AUTH_CANI = "auth-cani-list" - CLUSTER_RESOURCES_PODS = "pods" - CLUSTER_RESOURCES_PODS_LOGS = "pods/logs" - CLUSTER_RESOURCES_POD_DISRUPTION_BUDGETS = "pod-disruption-budgets" - CLUSTER_RESOURCES_SERVICES = "services" - CLUSTER_RESOURCES_DEPLOYMENTS = "deployments" - CLUSTER_RESOURCES_REPLICASETS = "replicasets" - CLUSTER_RESOURCES_STATEFULSETS = "statefulsets" - CLUSTER_RESOURCES_DAEMONSETS = "daemonsets" - CLUSTER_RESOURCES_JOBS = "jobs" - CLUSTER_RESOURCES_CRONJOBS = "cronjobs" - CLUSTER_RESOURCES_INGRESS = "ingress" - CLUSTER_RESOURCES_NETWORK_POLICY = "network-policy" - CLUSTER_RESOURCES_RESOURCE_QUOTA = "resource-quota" - CLUSTER_RESOURCES_STORAGE_CLASS = "storage-classes" - CLUSTER_RESOURCES_CUSTOM_RESOURCE_DEFINITIONS = "custom-resource-definitions" - CLUSTER_RESOURCES_CUSTOM_RESOURCES = "custom-resources" - CLUSTER_RESOURCES_IMAGE_PULL_SECRETS = "image-pull-secrets" // nolint:gosec - CLUSTER_RESOURCES_NODES = "nodes" - CLUSTER_RESOURCES_GROUPS = "groups" - CLUSTER_RESOURCES_RESOURCES = "resources" - CLUSTER_RESOURCES_LIMITRANGES = "limitranges" - CLUSTER_RESOURCES_EVENTS = "events" - CLUSTER_RESOURCES_PVS = "pvs" - CLUSTER_RESOURCES_PVCS = "pvcs" - CLUSTER_RESOURCES_ROLES = "roles" - CLUSTER_RESOURCES_ROLE_BINDINGS = "rolebindings" - CLUSTER_RESOURCES_CLUSTER_ROLES = "clusterroles" - CLUSTER_RESOURCES_CLUSTER_ROLE_BINDINGS = "clusterrolebindings" - CLUSTER_RESOURCES_PRIORITY_CLASS = "priorityclasses" - CLUSTER_RESOURCES_ENDPOINTS = "endpoints" - CLUSTER_RESOURCES_ENDPOINTSLICES = "endpointslices" - CLUSTER_RESOURCES_SERVICE_ACCOUNTS = "serviceaccounts" - CLUSTER_RESOURCES_LEASES = "leases" - CLUSTER_RESOURCES_VOLUME_ATTACHMENTS = "volumeattachments" - CLUSTER_RESOURCES_CONFIGMAPS = "configmaps" - CLUSTER_RESOURCES_REPLICATED_LICENSE = "license.json" + CLUSTER_RESOURCES_DIR = "cluster-resources" + CLUSTER_RESOURCES_NAMESPACES = "namespaces" + CLUSTER_RESOURCES_AUTH_CANI = "auth-cani-list" + CLUSTER_RESOURCES_PODS = "pods" + CLUSTER_RESOURCES_PODS_LOGS = "pods/logs" + CLUSTER_RESOURCES_POD_DISRUPTION_BUDGETS = "pod-disruption-budgets" + CLUSTER_RESOURCES_SERVICES = "services" + CLUSTER_RESOURCES_DEPLOYMENTS = "deployments" + CLUSTER_RESOURCES_REPLICASETS = "replicasets" + CLUSTER_RESOURCES_STATEFULSETS = "statefulsets" + CLUSTER_RESOURCES_DAEMONSETS = "daemonsets" + CLUSTER_RESOURCES_JOBS = "jobs" + CLUSTER_RESOURCES_CRONJOBS = "cronjobs" + CLUSTER_RESOURCES_INGRESS = "ingress" + CLUSTER_RESOURCES_NETWORK_POLICY = "network-policy" + CLUSTER_RESOURCES_RESOURCE_QUOTA = "resource-quota" + CLUSTER_RESOURCES_STORAGE_CLASS = "storage-classes" + CLUSTER_RESOURCES_CUSTOM_RESOURCE_DEFINITIONS = "custom-resource-definitions" + CLUSTER_RESOURCES_CUSTOM_RESOURCES = "custom-resources" + CLUSTER_RESOURCES_IMAGE_PULL_SECRETS = "image-pull-secrets" // nolint:gosec + CLUSTER_RESOURCES_NODES = "nodes" + CLUSTER_RESOURCES_GROUPS = "groups" + CLUSTER_RESOURCES_RESOURCES = "resources" + CLUSTER_RESOURCES_LIMITRANGES = "limitranges" + CLUSTER_RESOURCES_EVENTS = "events" + CLUSTER_RESOURCES_PVS = "pvs" + CLUSTER_RESOURCES_PVCS = "pvcs" + CLUSTER_RESOURCES_ROLES = "roles" + CLUSTER_RESOURCES_ROLE_BINDINGS = "rolebindings" + CLUSTER_RESOURCES_CLUSTER_ROLES = "clusterroles" + CLUSTER_RESOURCES_CLUSTER_ROLE_BINDINGS = "clusterrolebindings" + CLUSTER_RESOURCES_PRIORITY_CLASS = "priorityclasses" + CLUSTER_RESOURCES_ENDPOINTS = "endpoints" + CLUSTER_RESOURCES_ENDPOINTSLICES = "endpointslices" + CLUSTER_RESOURCES_SERVICE_ACCOUNTS = "serviceaccounts" + CLUSTER_RESOURCES_LEASES = "leases" + CLUSTER_RESOURCES_VOLUME_ATTACHMENTS = "volumeattachments" + CLUSTER_RESOURCES_CONFIGMAPS = "configmaps" + CLUSTER_RESOURCES_REPLICATED_LICENSE = "license.json" + CLUSTER_RESOURCES_CERTIFICATE_SIGNING_REQUESTS = "certificatesigningrequests" // SelfSubjectRulesReview evaluation responses SELFSUBJECTRULESREVIEW_ERROR_AUTHORIZATION_WEBHOOK_UNSUPPORTED = "webhook authorizer does not support user rule resolution"