diff --git a/pkg/describe/describe_test.go b/pkg/describe/describe_test.go index 824ab7be5..a036f8699 100644 --- a/pkg/describe/describe_test.go +++ b/pkg/describe/describe_test.go @@ -33,12 +33,14 @@ import ( autoscalingv1 "k8s.io/api/autoscaling/v1" autoscalingv2 "k8s.io/api/autoscaling/v2" batchv1 "k8s.io/api/batch/v1" + certificatesv1 "k8s.io/api/certificates/v1" coordinationv1 "k8s.io/api/coordination/v1" corev1 "k8s.io/api/core/v1" discoveryv1 "k8s.io/api/discovery/v1" networkingv1 "k8s.io/api/networking/v1" networkingv1beta1 "k8s.io/api/networking/v1beta1" policyv1 "k8s.io/api/policy/v1" + rbacv1 "k8s.io/api/rbac/v1" schedulingv1 "k8s.io/api/scheduling/v1" storagev1 "k8s.io/api/storage/v1" apiequality "k8s.io/apimachinery/pkg/api/equality" @@ -7254,3 +7256,686 @@ func TestSmartLabelFor(t *testing.T) { }) } } + +func TestDescribeCronJob(t *testing.T) { + testCases := []struct { + name string + cronJob *batchv1.CronJob + expects []string + }{ + { + name: "standard cronjob", + cronJob: &batchv1.CronJob{ + ObjectMeta: metav1.ObjectMeta{ + Name: "bar", + Namespace: "foo", + }, + Spec: batchv1.CronJobSpec{ + Schedule: "*/5 * * * *", + ConcurrencyPolicy: batchv1.ForbidConcurrent, + Suspend: ptr.To(false), + SuccessfulJobsHistoryLimit: ptr.To[int32](3), + FailedJobsHistoryLimit: ptr.To[int32](1), + StartingDeadlineSeconds: ptr.To[int64](200), + JobTemplate: batchv1.JobTemplateSpec{ + Spec: batchv1.JobSpec{ + Selector: &metav1.LabelSelector{}, + Parallelism: ptr.To[int32](4), + Completions: ptr.To[int32](5), + ActiveDeadlineSeconds: ptr.To[int64](400), + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{}, + }, + }, + }, + }, + }, + expects: []string{ + "Name: bar", + "Namespace: foo", + "Schedule: */5 * * * *", + "Concurrency Policy: Forbid", + "Suspend: False", + "Starting Deadline Seconds: 200", + "Successful Job History Limit: 3", + "Failed Job History Limit: 1", + "Starting Deadline Seconds: 200s", + "Parallelism: 4", + "Completions: 5", + "Active Deadline Seconds: 400s", + "Last Schedule Time: ", + "Active Jobs: ", + "Events: ", + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + fake := fake.NewClientset(testCase.cronJob) + c := &describeClient{T: t, Namespace: "foo", Interface: fake} + d := CronJobDescriber{c} + out, err := d.Describe("foo", "bar", DescriberSettings{ShowEvents: true}) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + for _, expect := range testCase.expects { + if !strings.Contains(out, expect) { + t.Errorf("expected to find %q in output: %q", expect, out) + } + } + }) + } +} + +func TestDescribeReplicaSet(t *testing.T) { + testCases := []struct { + name string + replicaSet *appsv1.ReplicaSet + pods []corev1.Pod + expects []string + }{ + { + name: "replica set with one pod and one condition", + replicaSet: &appsv1.ReplicaSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: "bar", + Namespace: "foo", + UID: "test-uid", + }, + Spec: appsv1.ReplicaSetSpec{ + Replicas: ptr.To[int32](1), + }, + Status: appsv1.ReplicaSetStatus{ + Replicas: 1, + ReadyReplicas: 1, + AvailableReplicas: 1, + Conditions: []appsv1.ReplicaSetCondition{ + { + Type: appsv1.ReplicaSetReplicaFailure, + Status: corev1.ConditionTrue, + Reason: "FailedCreate", + }, + }, + }, + }, + pods: []corev1.Pod{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "pod1", + Namespace: "foo", + OwnerReferences: []metav1.OwnerReference{ + { + UID: "test-uid", + Controller: ptr.To(true), + }, + }, + }, + Status: corev1.PodStatus{Phase: corev1.PodRunning}, + }, + }, + expects: []string{ + "Name: bar", + "Namespace: foo", + "Replicas: 1 current / 1 desired", + "Pods Status: 1 Running / 0 Waiting / 0 Succeeded / 0 Failed", + "Conditions:", + "Type Status Reason", + "---- ------ ------", + "ReplicaFailure True FailedCreate", + }, + }, + { + name: "replica set with multiple pods", + replicaSet: &appsv1.ReplicaSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: "bar", + Namespace: "foo", + UID: "test-uid", + }, + Spec: appsv1.ReplicaSetSpec{ + Replicas: ptr.To[int32](2), + }, + Status: appsv1.ReplicaSetStatus{ + Replicas: 2, + ReadyReplicas: 2, + AvailableReplicas: 2, + }, + }, + pods: []corev1.Pod{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "pod1", + Namespace: "foo", + OwnerReferences: []metav1.OwnerReference{ + { + UID: "test-uid", + Controller: ptr.To(true), + }, + }, + }, + Status: corev1.PodStatus{Phase: corev1.PodRunning}, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "pod2", + Namespace: "foo", + OwnerReferences: []metav1.OwnerReference{ + { + UID: "test-uid", + Controller: ptr.To(true), + }, + }, + }, + Status: corev1.PodStatus{Phase: corev1.PodRunning}, + }, + }, + expects: []string{ + "Name: bar", + "Namespace: foo", + "Replicas: 2 current / 2 desired", + "Pods Status: 2 Running / 0 Waiting / 0 Succeeded / 0 Failed", + }, + }, + { + name: "replica set with different pod statuses", + replicaSet: &appsv1.ReplicaSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: "bar", + Namespace: "foo", + UID: "test-uid", + }, + Spec: appsv1.ReplicaSetSpec{ + Replicas: ptr.To[int32](4), + }, + Status: appsv1.ReplicaSetStatus{ + Replicas: 4, + ReadyReplicas: 4, + AvailableReplicas: 4, + }, + }, + pods: []corev1.Pod{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "pod1", + Namespace: "foo", + OwnerReferences: []metav1.OwnerReference{ + { + UID: "test-uid", + Controller: ptr.To(true), + }, + }, + }, + Status: corev1.PodStatus{Phase: corev1.PodRunning}, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "pod2", + Namespace: "foo", + OwnerReferences: []metav1.OwnerReference{ + { + UID: "test-uid", + Controller: ptr.To(true), + }, + }, + }, + Status: corev1.PodStatus{Phase: corev1.PodPending}, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "pod3", + Namespace: "foo", + OwnerReferences: []metav1.OwnerReference{ + { + UID: "test-uid", + Controller: ptr.To(true), + }, + }, + }, + Status: corev1.PodStatus{Phase: corev1.PodSucceeded}, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "pod4", + Namespace: "foo", + OwnerReferences: []metav1.OwnerReference{ + { + UID: "test-uid", + Controller: ptr.To(true), + }, + }, + }, + Status: corev1.PodStatus{Phase: corev1.PodFailed}, + }, + }, + expects: []string{ + "Name: bar", + "Namespace: foo", + "Replicas: 4 current / 4 desired", + "Pods Status: 1 Running / 1 Waiting / 1 Succeeded / 1 Failed", + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + objects := []runtime.Object{testCase.replicaSet} + for _, pod := range testCase.pods { + objects = append(objects, &pod) + } + fake := fake.NewClientset(objects...) + c := &describeClient{T: t, Namespace: "foo", Interface: fake} + d := ReplicaSetDescriber{c} + out, err := d.Describe(testCase.replicaSet.Namespace, testCase.replicaSet.Name, DescriberSettings{ShowEvents: true}) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + for _, expect := range testCase.expects { + if !strings.Contains(out, expect) { + t.Errorf("expected to find %q in output: %q", expect, out) + } + } + }) + } +} + +func TestDescribeRole(t *testing.T) { + testCases := []struct { + name string + role *rbacv1.Role + expects []string + }{ + { + name: "role with resource rules", + role: &rbacv1.Role{ + ObjectMeta: metav1.ObjectMeta{ + Name: "bar", + Namespace: "foo", + }, + Rules: []rbacv1.PolicyRule{ + { + APIGroups: []string{""}, + Resources: []string{"pods"}, + Verbs: []string{"get", "watch", "list"}, + }, + }, + }, + expects: []string{ + "Name: bar", + "PolicyRule:", + "Resources Non-Resource URLs Resource Names Verbs", + "--------- ----------------- -------------- -----", + "pods [] [] [get watch list]", + }, + }, + { + name: "role with resource names", + role: &rbacv1.Role{ + ObjectMeta: metav1.ObjectMeta{ + Name: "bar", + Namespace: "foo", + }, + Rules: []rbacv1.PolicyRule{ + { + APIGroups: []string{""}, + Resources: []string{"configmaps"}, + ResourceNames: []string{"my-configmap"}, + Verbs: []string{"update", "get"}, + }, + }, + }, + expects: []string{ + "Name: bar", + "PolicyRule:", + "Resources Non-Resource URLs Resource Names Verbs", + "--------- ----------------- -------------- -----", + "configmaps [] [my-configmap] [update get]", + }, + }, + { + name: "non-resource urls", + role: &rbacv1.Role{ + ObjectMeta: metav1.ObjectMeta{ + Name: "bar", + Namespace: "foo", + }, + Rules: []rbacv1.PolicyRule{ + { + NonResourceURLs: []string{"/healthz", "/version"}, + Verbs: []string{"get"}, + }, + }, + }, + expects: []string{ + "Name: bar", + "PolicyRule:", + "Resources Non-Resource URLs Resource Names Verbs", + "--------- ----------------- -------------- -----", + " [/healthz] [] [get]", + " [/version] [] [get]", + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + fake := fake.NewClientset(testCase.role) + c := &describeClient{T: t, Namespace: "foo", Interface: fake} + d := RoleDescriber{c} + out, err := d.Describe("foo", "bar", DescriberSettings{}) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + for _, expect := range testCase.expects { + if !strings.Contains(out, expect) { + t.Errorf("expected to find %q in output: %q", expect, out) + } + } + }) + } +} + +func TestDescribeRoleBinding(t *testing.T) { + testCases := []struct { + name string + binding *rbacv1.RoleBinding + expects []string + }{ + { + name: "role binding with subjects", + binding: &rbacv1.RoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "bar", + Namespace: "foo", + }, + Subjects: []rbacv1.Subject{ + { + Kind: rbacv1.UserKind, + Name: "alice", + Namespace: "foo", + }, + }, + RoleRef: rbacv1.RoleRef{ + Kind: "Role", + Name: "my-role", + }, + }, + expects: []string{ + "Name: bar", + "Role:", + "Kind: Role", + "Name: my-role", + "Subjects:", + "Kind Name Namespace", + "---- ---- ---------", + "User alice foo", + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + fake := fake.NewClientset(testCase.binding) + c := &describeClient{T: t, Namespace: "foo", Interface: fake} + d := RoleBindingDescriber{c} + out, err := d.Describe("foo", "bar", DescriberSettings{}) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + for _, expect := range testCase.expects { + if !strings.Contains(out, expect) { + t.Errorf("expected to find %q in output: %q", expect, out) + } + } + }) + } +} + +func TestDescribeClusterRole(t *testing.T) { + testCases := []struct { + name string + role *rbacv1.ClusterRole + expects []string + }{ + { + name: "cluster role with resource rules", + role: &rbacv1.ClusterRole{ + ObjectMeta: metav1.ObjectMeta{ + Name: "bar", + }, + Rules: []rbacv1.PolicyRule{ + { + APIGroups: []string{""}, + Resources: []string{"secrets"}, + Verbs: []string{"get", "watch", "list"}, + }, + }, + }, + expects: []string{ + "Name: bar", + "PolicyRule:", + "Resources Non-Resource URLs Resource Names Verbs", + "--------- ----------------- -------------- -----", + "secrets [] [] [get watch list]", + }, + }, + { + name: "cluster role with resource names", + role: &rbacv1.ClusterRole{ + ObjectMeta: metav1.ObjectMeta{ + Name: "bar", + }, + Rules: []rbacv1.PolicyRule{ + { + APIGroups: []string{"rbac.authorization.k8s.io"}, + Resources: []string{"clusterroles"}, + ResourceNames: []string{"bind"}, + Verbs: []string{"delete", "get"}, + }, + }, + }, + expects: []string{ + "Name: bar", + "PolicyRule:", + "Resources Non-Resource URLs Resource Names Verbs", + "--------- ----------------- -------------- -----", + "clusterroles.rbac.authorization.k8s.io [] [bind] [delete get]", + }, + }, + { + name: "non-resource urls", + role: &rbacv1.ClusterRole{ + ObjectMeta: metav1.ObjectMeta{ + Name: "bar", + }, + Rules: []rbacv1.PolicyRule{ + { + NonResourceURLs: []string{"/metrics", "/logs"}, + Verbs: []string{"get"}, + }, + }, + }, + expects: []string{ + "Name: bar", + "PolicyRule:", + "Resources Non-Resource URLs Resource Names Verbs", + "--------- ----------------- -------------- -----", + " [/logs] [] [get]", + " [/metrics] [] [get]", + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + fake := fake.NewClientset(testCase.role) + c := &describeClient{T: t, Interface: fake} + d := ClusterRoleDescriber{c} + out, err := d.Describe("", "bar", DescriberSettings{}) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + for _, expect := range testCase.expects { + if !strings.Contains(out, expect) { + t.Errorf("expected to find %q in output: %q", expect, out) + } + } + }) + } +} + +func TestDescribeClusterRoleBinding(t *testing.T) { + testCases := []struct { + name string + binding *rbacv1.ClusterRoleBinding + expects []string + }{ + { + name: "cluster role binding with cluster-scoped subject", + binding: &rbacv1.ClusterRoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "bar", + }, + Subjects: []rbacv1.Subject{ + { + Kind: rbacv1.GroupKind, + Name: "dev-team", + }, + }, + RoleRef: rbacv1.RoleRef{ + Kind: "ClusterRole", + Name: "my-cluster-role", + }, + }, + expects: []string{ + "Name: bar", + "Role:", + "Kind: ClusterRole", + "Name: my-cluster-role", + "Subjects:", + "Kind Name Namespace", + "---- ---- ---------", + "Group dev-team ", + }, + }, + { + name: "cluster role binding with namespace-scoped subject", + binding: &rbacv1.ClusterRoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "bar", + }, + Subjects: []rbacv1.Subject{ + { + Kind: rbacv1.ServiceAccountKind, + Name: "sa-name", + Namespace: "sa-namespace", + }, + }, + RoleRef: rbacv1.RoleRef{ + Kind: "ClusterRole", + Name: "my-cluster-role", + }, + }, + expects: []string{ + "Name: bar", + "Role:", + "Kind: ClusterRole", + "Name: my-cluster-role", + "Subjects:", + "Kind Name Namespace", + "---- ---- ---------", + "ServiceAccount sa-name sa-namespace", + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + fake := fake.NewClientset(testCase.binding) + c := &describeClient{T: t, Interface: fake} + d := ClusterRoleBindingDescriber{c} + out, err := d.Describe("", "bar", DescriberSettings{}) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + for _, expect := range testCase.expects { + if !strings.Contains(out, expect) { + t.Errorf("expected to find %q in output: %q", expect, out) + } + } + }) + } +} + +func TestCertificateSigningRequestDescriber(t *testing.T) { + dummyCSRPEM := `-----BEGIN CERTIFICATE REQUEST----- +MIICVTCCAT0CAQAwEDEOMAwGA1UEAwwFYWxpY2UwggEiMA0GCSqGSIb3DQEBAQUA +A4IBDwAwggEKAoIBAQDPqga01w90b6SZEVvM11IONKSW2tOMtDE+ustAH9b8B8wb +iExQKse0zzGB4N9lKCWBkH8BpcNpJ6RzNNcs7SldejEy7xCCEFJw4/KPy8PrbZly +TcPI3i5PZ8UJPimUVHtZIfNePSo8A4/j5l+AnHO/K2sBi6A2QqOYizga+VH7sfr9 +c80azgPiNYFMm7eKOTFJZ/HCfcPQHWnyrk9vsL/cSL7WgsjtKulR8SIYwHAsJOh2 +REtJ6AX0FErmft1xiecFhrHFuY9op+XsiB8hku8kcGTJQ2TKl/lu9eBHWleZ/Y9R +vta3YxaXB07oS8FhmQeUcutNkCBxX3JNBO/9d2CBAgMBAAGgADANBgkqhkiG9w0B +AQsFAAOCAQEAv4R6Hh++vq3hwLfhYORe6WplwElE6AvtPB0v7OVwxbAE5FEaFerB +VIoiW44gRWsN/+be6D4IV+ZxIDI+JlHL8smc7nFVGo+uDj4QMeMHD29Bne+er1Nm +wULDTL6nWWGUM+fXVeULEhYDhZQ0am1BqMU+foc3wRm4/coPIkZfhG8o2OPmZN+D +aj7eYCTT+56driz9oVNawWF1ObeR4GOV6ba0aTqNm2JgT4fj6ePTD22b3cMZs81d +wzxacvSgqBb0HR10MjB5l2Vys7GOhWXnDadQaO09XCua1++3GWPOAAdoOtLiWuj4 +IY5vBsBP6cycvKRL1XZ43uF9e2zE2GiHBw== +-----END CERTIFICATE REQUEST-----` + + testCases := []struct { + name string + csr *certificatesv1.CertificateSigningRequest + expects []string + }{ + { + name: "basic certificate signing request", + csr: &certificatesv1.CertificateSigningRequest{ + ObjectMeta: metav1.ObjectMeta{ + Name: "bar", + }, + Spec: certificatesv1.CertificateSigningRequestSpec{ + SignerName: "kubernetes.io/kube-apiserver-client", + Request: []byte(dummyCSRPEM), + Usages: []certificatesv1.KeyUsage{certificatesv1.UsageClientAuth, certificatesv1.UsageDigitalSignature}, + Username: "alice", + Groups: []string{"system:authenticated"}, + }, + Status: certificatesv1.CertificateSigningRequestStatus{ + Conditions: []certificatesv1.CertificateSigningRequestCondition{ + { + Type: certificatesv1.CertificateApproved, + Reason: "Approved by admin", + Message: "This CSR was approved by the admin.", + }, + }, + }, + }, + expects: []string{ + "Name: bar", + "Signer: kubernetes.io/kube-apiserver-client", + "Requesting User: alice", + "Status: Approved", + "Common Name: alice", + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + fake := fake.NewClientset(testCase.csr) + c := &describeClient{T: t, Interface: fake} + d := CertificateSigningRequestDescriber{c} + out, err := d.Describe("", "bar", DescriberSettings{}) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + for _, expect := range testCase.expects { + if !strings.Contains(out, expect) { + t.Errorf("expected to find %q in output: %q", expect, out) + } + } + }) + } +}