/* Copyright 2014 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package describe import ( "bytes" "fmt" "maps" "reflect" "strings" "testing" "time" "github.com/google/go-cmp/cmp" "github.com/lithammer/dedent" "github.com/stretchr/testify/assert" appsv1 "k8s.io/api/apps/v1" autoscalingv1 "k8s.io/api/autoscaling/v1" autoscalingv2 "k8s.io/api/autoscaling/v2" batchv1 "k8s.io/api/batch/v1" coordinationv1 "k8s.io/api/coordination/v1" corev1 "k8s.io/api/core/v1" discoveryv1 "k8s.io/api/discovery/v1" discoveryv1beta1 "k8s.io/api/discovery/v1beta1" networkingv1 "k8s.io/api/networking/v1" networkingv1beta1 "k8s.io/api/networking/v1beta1" policyv1 "k8s.io/api/policy/v1" policyv1beta1 "k8s.io/api/policy/v1beta1" schedulingv1 "k8s.io/api/scheduling/v1" storagev1 "k8s.io/api/storage/v1" storagev1beta1 "k8s.io/api/storage/v1beta1" apiequality "k8s.io/apimachinery/pkg/api/equality" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/intstr" "k8s.io/client-go/kubernetes" "k8s.io/client-go/kubernetes/fake" "k8s.io/utils/ptr" ) type describeClient struct { T *testing.T Namespace string Err error kubernetes.Interface } func TestDescribePod(t *testing.T) { deletionTimestamp := metav1.Time{Time: time.Now().UTC().AddDate(-10, 0, 0)} gracePeriod := int64(1234) condition1 := corev1.PodConditionType("condition1") condition2 := corev1.PodConditionType("condition2") runningPod := corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", DeletionTimestamp: &deletionTimestamp, DeletionGracePeriodSeconds: &gracePeriod, }, Spec: corev1.PodSpec{ ReadinessGates: []corev1.PodReadinessGate{ { ConditionType: condition1, }, { ConditionType: condition2, }, }, }, Status: corev1.PodStatus{ Phase: corev1.PodRunning, Conditions: []corev1.PodCondition{ { Type: condition1, Status: corev1.ConditionTrue, }, }, }, } tests := []struct { name string namespace string phase corev1.PodPhase wantOutput []string }{ { name: "foo", namespace: "bar", phase: "Running", wantOutput: []string{"bar", "Status:", "Terminating (lasts 10y)", "Termination Grace Period", "1234s"}, }, { name: "pod1", namespace: "ns1", phase: "Pending", wantOutput: []string{"pod1", "ns1", "Terminating (lasts 10y)", "Termination Grace Period", "1234s"}, }, { name: "pod2", namespace: "ns2", phase: "Succeeded", wantOutput: []string{"pod2", "ns2", "Succeeded"}, }, { name: "pod3", namespace: "ns3", phase: "Failed", wantOutput: []string{"pod3", "ns3", "Failed"}, }, } for i, test := range tests { pod := runningPod.DeepCopy() pod.Name, pod.Namespace, pod.Status.Phase = test.name, test.namespace, test.phase fake := fake.NewSimpleClientset(pod) c := &describeClient{T: t, Namespace: pod.Namespace, Interface: fake} d := PodDescriber{c} out, err := d.Describe(pod.Namespace, pod.Name, DescriberSettings{ShowEvents: true}) if err != nil { t.Errorf("case %d: unexpected error: %v", i, err) } for _, wantStr := range test.wantOutput { if !strings.Contains(out, wantStr) { t.Errorf("case %d didn't contain want(%s): unexpected out:\n%s", i, wantStr, out) } } } } func TestDescribePodServiceAccount(t *testing.T) { fake := fake.NewSimpleClientset(&corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", }, Spec: corev1.PodSpec{ ServiceAccountName: "fooaccount", }, }) c := &describeClient{T: t, Namespace: "foo", Interface: fake} d := PodDescriber{c} out, err := d.Describe("foo", "bar", DescriberSettings{ShowEvents: true}) if err != nil { t.Errorf("unexpected error: %v", err) } if !strings.Contains(out, "Service Account:") { t.Errorf("unexpected out: %s", out) } if !strings.Contains(out, "fooaccount") { t.Errorf("unexpected out: %s", out) } } func TestDescribePodEphemeralContainers(t *testing.T) { fake := fake.NewSimpleClientset(&corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", }, Spec: corev1.PodSpec{ EphemeralContainers: []corev1.EphemeralContainer{ { EphemeralContainerCommon: corev1.EphemeralContainerCommon{ Name: "debugger", Image: "busybox", }, }, }, }, Status: corev1.PodStatus{ EphemeralContainerStatuses: []corev1.ContainerStatus{ { Name: "debugger", State: corev1.ContainerState{ Running: &corev1.ContainerStateRunning{ StartedAt: metav1.NewTime(time.Now()), }, }, Ready: false, RestartCount: 0, }, }, }, }) c := &describeClient{T: t, Namespace: "foo", Interface: fake} d := PodDescriber{c} out, err := d.Describe("foo", "bar", DescriberSettings{ShowEvents: true}) if err != nil { t.Errorf("unexpected error: %v", err) } if !strings.Contains(out, "debugger:") { t.Errorf("unexpected out: %s", out) } if !strings.Contains(out, "busybox") { t.Errorf("unexpected out: %s", out) } } func TestDescribePodNode(t *testing.T) { fake := fake.NewSimpleClientset(&corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", }, Spec: corev1.PodSpec{ NodeName: "all-in-one", }, Status: corev1.PodStatus{ HostIP: "127.0.0.1", NominatedNodeName: "nodeA", }, }) c := &describeClient{T: t, Namespace: "foo", Interface: fake} d := PodDescriber{c} out, err := d.Describe("foo", "bar", DescriberSettings{ShowEvents: true}) if err != nil { t.Errorf("unexpected error: %v", err) } if !strings.Contains(out, "all-in-one/127.0.0.1") { t.Errorf("unexpected out: %s", out) } if !strings.Contains(out, "nodeA") { t.Errorf("unexpected out: %s", out) } } func TestDescribePodTolerations(t *testing.T) { fake := fake.NewSimpleClientset(&corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", }, Spec: corev1.PodSpec{ Tolerations: []corev1.Toleration{ {Operator: corev1.TolerationOpExists}, {Effect: corev1.TaintEffectNoSchedule, Operator: corev1.TolerationOpExists}, {Key: "key0", Operator: corev1.TolerationOpExists}, {Key: "key1", Value: "value1"}, {Key: "key2", Operator: corev1.TolerationOpEqual, Value: "value2", Effect: corev1.TaintEffectNoSchedule}, {Key: "key3", Value: "value3", Effect: corev1.TaintEffectNoExecute, TolerationSeconds: &[]int64{300}[0]}, {Key: "key4", Effect: corev1.TaintEffectNoExecute, TolerationSeconds: &[]int64{60}[0]}, }, }, }) c := &describeClient{T: t, Namespace: "foo", Interface: fake} d := PodDescriber{c} out, err := d.Describe("foo", "bar", DescriberSettings{}) if err != nil { t.Errorf("unexpected error: %v", err) } if !strings.Contains(out, " op=Exists\n") || !strings.Contains(out, ":NoSchedule op=Exists\n") || !strings.Contains(out, "key0 op=Exists\n") || !strings.Contains(out, "key1=value1\n") || !strings.Contains(out, "key2=value2:NoSchedule\n") || !strings.Contains(out, "key3=value3:NoExecute for 300s\n") || !strings.Contains(out, "key4:NoExecute for 60s\n") || !strings.Contains(out, "Tolerations:") { t.Errorf("unexpected out:\n%s", out) } } func TestDescribePodVolumes(t *testing.T) { pod := &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", }, Spec: corev1.PodSpec{ Volumes: []corev1.Volume{ { Name: "image", VolumeSource: corev1.VolumeSource{Image: &corev1.ImageVolumeSource{Reference: "image", PullPolicy: corev1.PullIfNotPresent}}, }, }, }, } expected := dedent.Dedent(` Name: bar Namespace: foo Node: Labels: Annotations: Status: IP: IPs: Containers: Volumes: image: Type: Image (a container image or OCI artifact) Reference: image PullPolicy: IfNotPresent QoS Class: BestEffort Node-Selectors: Tolerations: Events: `)[1:] fakeClient := fake.NewSimpleClientset(pod) c := &describeClient{T: t, Namespace: "foo", Interface: fakeClient} d := PodDescriber{c} out, err := d.Describe("foo", "bar", DescriberSettings{ShowEvents: true}) if err != nil { t.Errorf("unexpected error: %v", err) } assert.Equal(t, expected, out) } func TestDescribeTopologySpreadConstraints(t *testing.T) { fake := fake.NewSimpleClientset(&corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", }, Spec: corev1.PodSpec{ TopologySpreadConstraints: []corev1.TopologySpreadConstraint{ { MaxSkew: 3, TopologyKey: "topology.kubernetes.io/test1", WhenUnsatisfiable: "DoNotSchedule", LabelSelector: &metav1.LabelSelector{MatchLabels: map[string]string{"key1": "val1", "key2": "val2"}}, }, { MaxSkew: 1, TopologyKey: "topology.kubernetes.io/test2", WhenUnsatisfiable: "ScheduleAnyway", }, }, }, }) c := &describeClient{T: t, Namespace: "foo", Interface: fake} d := PodDescriber{c} out, err := d.Describe("foo", "bar", DescriberSettings{}) if err != nil { t.Errorf("unexpected error: %v", err) } if !strings.Contains(out, "topology.kubernetes.io/test1:DoNotSchedule when max skew 3 is exceeded for selector key1=val1,key2=val2\n") || !strings.Contains(out, "topology.kubernetes.io/test2:ScheduleAnyway when max skew 1 is exceeded\n") { t.Errorf("unexpected out:\n%s", out) } } func TestDescribeSecret(t *testing.T) { testCases := []struct { description string data map[string][]byte // secret key -> secret in bytes expected []string }{ { description: "alphabetical ordering", data: map[string][]byte{ "username": []byte("YWRtaW4="), "password": []byte("MWYyZDFlMmU2N2Rm"), }, expected: []string{"password", "username"}, }, { description: "uppercase takes precedence", data: map[string][]byte{ "text": []byte("a3ViZXJuZXRlcwo="), "Text": []byte("dGhpcyBpcyBhIHRlc3QK"), "tExt": []byte("d2VpcmQgY2FzaW5nCg=="), }, expected: []string{"Text", "tExt", "text"}, }, { description: "numbers take precedence", data: map[string][]byte{ "key_1": []byte("c29tZV9zZWNyZXQK"), "1_key": []byte("c29tZV90ZXh0Cg=="), }, expected: []string{"1_key", "key_1"}, }, } for _, testCase := range testCases { t.Run(testCase.description, func(t *testing.T) { secret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", }, Data: testCase.data, } fake := fake.NewSimpleClientset(secret) c := &describeClient{T: t, Namespace: "foo", Interface: fake} d := SecretDescriber{c} out, err := d.Describe("foo", "bar", DescriberSettings{}) if err != nil { t.Errorf("unexpected error: %v", err) } for value := range maps.Values(testCase.data) { if strings.Contains(out, string(value)) { t.Errorf("sensitive data should not be shown, unexpected out: %s", out) } } expectedOut := `Name: bar Namespace: foo Labels: Annotations: Type: Data ====` for _, expectedKey := range testCase.expected { expectedOut = fmt.Sprintf("%s\n%s: %d bytes", expectedOut, expectedKey, len(testCase.data[expectedKey])) } expectedOut = fmt.Sprintf("%s\n", expectedOut) assert.Equal(t, expectedOut, out) }) } } func TestDescribeNamespace(t *testing.T) { exampleNamespaceName := "example" testCases := []struct { name string namespace *corev1.Namespace expect []string }{ { name: "no quotas or limit ranges", namespace: &corev1.Namespace{ ObjectMeta: metav1.ObjectMeta{ Name: exampleNamespaceName, }, Status: corev1.NamespaceStatus{ Phase: corev1.NamespaceActive, }, }, expect: []string{ "Name", exampleNamespaceName, "Status", string(corev1.NamespaceActive), "No resource quota", "No LimitRange resource.", }, }, { name: "has conditions", namespace: &corev1.Namespace{ ObjectMeta: metav1.ObjectMeta{ Name: exampleNamespaceName, }, Status: corev1.NamespaceStatus{ Phase: corev1.NamespaceTerminating, Conditions: []corev1.NamespaceCondition{ { LastTransitionTime: metav1.NewTime(time.Date(2014, time.January, 15, 0, 0, 0, 0, time.UTC)), Message: "example message", Reason: "example reason", Status: corev1.ConditionTrue, Type: corev1.NamespaceDeletionContentFailure, }, }, }, }, expect: []string{ "Name", exampleNamespaceName, "Status", string(corev1.NamespaceTerminating), "Conditions", "Type", string(corev1.NamespaceDeletionContentFailure), "Status", string(corev1.ConditionTrue), "Reason", "example reason", "Message", "example message", "No resource quota", "No LimitRange resource.", }, }, } for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { fake := fake.NewSimpleClientset(testCase.namespace) c := &describeClient{T: t, Namespace: "", Interface: fake} d := NamespaceDescriber{c} out, err := d.Describe("", testCase.namespace.Name, DescriberSettings{ShowEvents: true}) if err != nil { t.Errorf("unexpected error: %v", err) } for _, expected := range testCase.expect { if !strings.Contains(out, expected) { t.Errorf("expected to find %q in output: %q", expected, out) } } }) } } func TestDescribePodPriority(t *testing.T) { priority := int32(1000) fake := fake.NewSimpleClientset(&corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", }, Spec: corev1.PodSpec{ PriorityClassName: "high-priority", Priority: &priority, }, }) c := &describeClient{T: t, Namespace: "", Interface: fake} d := PodDescriber{c} out, err := d.Describe("", "bar", DescriberSettings{ShowEvents: true}) if err != nil { t.Errorf("unexpected error: %v", err) } if !strings.Contains(out, "high-priority") || !strings.Contains(out, "1000") { t.Errorf("unexpected out: %s", out) } } func TestDescribePodRuntimeClass(t *testing.T) { runtimeClassNames := []string{"test1", ""} testCases := []struct { name string pod *corev1.Pod expect []string unexpect []string }{ { name: "test1", pod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", }, Spec: corev1.PodSpec{ RuntimeClassName: &runtimeClassNames[0], }, }, expect: []string{ "Name", "bar", "Runtime Class Name", "test1", }, unexpect: []string{}, }, { name: "test2", pod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", }, Spec: corev1.PodSpec{ RuntimeClassName: &runtimeClassNames[1], }, }, expect: []string{ "Name", "bar", }, unexpect: []string{ "Runtime Class Name", }, }, { name: "test3", pod: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", }, Spec: corev1.PodSpec{}, }, expect: []string{ "Name", "bar", }, unexpect: []string{ "Runtime Class Name", }, }, } for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { fake := fake.NewSimpleClientset(testCase.pod) c := &describeClient{T: t, Interface: fake} d := PodDescriber{c} out, err := d.Describe("", "bar", DescriberSettings{ShowEvents: true}) if err != nil { t.Errorf("unexpected error: %v", err) } for _, expected := range testCase.expect { if !strings.Contains(out, expected) { t.Errorf("expected to find %q in output: %q", expected, out) } } for _, unexpected := range testCase.unexpect { if strings.Contains(out, unexpected) { t.Errorf("unexpected to find %q in output: %q", unexpected, out) } } }) } } func TestDescribePriorityClass(t *testing.T) { preemptLowerPriority := corev1.PreemptLowerPriority preemptNever := corev1.PreemptNever testCases := []struct { name string priorityClass *schedulingv1.PriorityClass expect []string }{ { name: "test1", priorityClass: &schedulingv1.PriorityClass{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", }, Value: 10, GlobalDefault: false, PreemptionPolicy: &preemptLowerPriority, Description: "test1", }, expect: []string{ "Name", "bar", "Value", "10", "GlobalDefault", "false", "PreemptionPolicy", "PreemptLowerPriority", "Description", "test1", "Annotations", "", }, }, { name: "test2", priorityClass: &schedulingv1.PriorityClass{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", }, Value: 100, GlobalDefault: true, PreemptionPolicy: &preemptNever, Description: "test2", }, expect: []string{ "Name", "bar", "Value", "100", "GlobalDefault", "true", "PreemptionPolicy", "Never", "Description", "test2", "Annotations", "", }, }, } for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { fake := fake.NewSimpleClientset(testCase.priorityClass) c := &describeClient{T: t, Interface: fake} d := PriorityClassDescriber{c} out, err := d.Describe("", "bar", DescriberSettings{ShowEvents: true}) if err != nil { t.Errorf("unexpected error: %v", err) } for _, expected := range testCase.expect { if !strings.Contains(out, expected) { t.Errorf("expected to find %q in output: %q", expected, out) } } }) } } func TestDescribeConfigMap(t *testing.T) { fake := fake.NewSimpleClientset(&corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ Name: "mycm", Namespace: "foo", }, Data: map[string]string{ "key1": "value1", "key2": "value2", }, BinaryData: map[string][]byte{ "binarykey1": {0xFF, 0xFE, 0xFD, 0xFC, 0xFB}, "binarykey2": {0xFF, 0xFE, 0xFD, 0xFC, 0xFB, 0xFA}, }, }) c := &describeClient{T: t, Namespace: "foo", Interface: fake} d := ConfigMapDescriber{c} out, err := d.Describe("foo", "mycm", DescriberSettings{ShowEvents: true}) if err != nil { t.Errorf("unexpected error: %v", err) } if !strings.Contains(out, "foo") || !strings.Contains(out, "mycm") { t.Errorf("unexpected out: %s", out) } if !strings.Contains(out, "key1") || !strings.Contains(out, "value1") || !strings.Contains(out, "key2") || !strings.Contains(out, "value2") { t.Errorf("unexpected out: %s", out) } if !strings.Contains(out, "binarykey1") || !strings.Contains(out, "5 bytes") || !strings.Contains(out, "binarykey2") || !strings.Contains(out, "6 bytes") { t.Errorf("unexpected out: %s", out) } } func TestDescribeLimitRange(t *testing.T) { fake := fake.NewSimpleClientset(&corev1.LimitRange{ ObjectMeta: metav1.ObjectMeta{ Name: "mylr", Namespace: "foo", }, Spec: corev1.LimitRangeSpec{ Limits: []corev1.LimitRangeItem{ { Type: corev1.LimitTypePod, Max: getResourceList("100m", "10000Mi"), Min: getResourceList("5m", "100Mi"), MaxLimitRequestRatio: getResourceList("10", ""), }, { Type: corev1.LimitTypeContainer, Max: getResourceList("100m", "10000Mi"), Min: getResourceList("5m", "100Mi"), Default: getResourceList("50m", "500Mi"), DefaultRequest: getResourceList("10m", "200Mi"), MaxLimitRequestRatio: getResourceList("10", ""), }, { Type: corev1.LimitTypePersistentVolumeClaim, Max: getStorageResourceList("10Gi"), Min: getStorageResourceList("5Gi"), }, }, }, }) c := &describeClient{T: t, Namespace: "foo", Interface: fake} d := LimitRangeDescriber{c} out, err := d.Describe("foo", "mylr", DescriberSettings{ShowEvents: true}) if err != nil { t.Errorf("unexpected error: %v", err) } checks := []string{"foo", "mylr", "Pod", "cpu", "5m", "100m", "memory", "100Mi", "10000Mi", "10", "Container", "cpu", "10m", "50m", "200Mi", "500Mi", "PersistentVolumeClaim", "storage", "5Gi", "10Gi"} for _, check := range checks { if !strings.Contains(out, check) { t.Errorf("unexpected out: %s", out) } } } func getStorageResourceList(storage string) corev1.ResourceList { res := corev1.ResourceList{} if storage != "" { res[corev1.ResourceStorage] = resource.MustParse(storage) } return res } func getResourceList(cpu, memory string) corev1.ResourceList { res := corev1.ResourceList{} if cpu != "" { res[corev1.ResourceCPU] = resource.MustParse(cpu) } if memory != "" { res[corev1.ResourceMemory] = resource.MustParse(memory) } return res } func TestDescribeService(t *testing.T) { singleStack := corev1.IPFamilyPolicySingleStack preferClose := corev1.ServiceTrafficDistributionPreferClose testCases := []struct { name string service *corev1.Service endpointSlices []*discoveryv1.EndpointSlice expected string }{ { name: "test1", service: &corev1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", }, Spec: corev1.ServiceSpec{ Type: corev1.ServiceTypeLoadBalancer, Ports: []corev1.ServicePort{{ Name: "port-tcp", Port: 8080, Protocol: corev1.ProtocolTCP, TargetPort: intstr.FromInt32(9527), NodePort: 31111, }}, Selector: map[string]string{"blah": "heh"}, ClusterIP: "1.2.3.4", IPFamilies: []corev1.IPFamily{corev1.IPv4Protocol}, LoadBalancerIP: "5.6.7.8", SessionAffinity: corev1.ServiceAffinityNone, ExternalTrafficPolicy: corev1.ServiceExternalTrafficPolicyLocal, InternalTrafficPolicy: ptr.To(corev1.ServiceInternalTrafficPolicyCluster), HealthCheckNodePort: 32222, }, Status: corev1.ServiceStatus{ LoadBalancer: corev1.LoadBalancerStatus{ Ingress: []corev1.LoadBalancerIngress{ { IP: "5.6.7.8", IPMode: ptr.To(corev1.LoadBalancerIPModeVIP), }, }, }, }, }, endpointSlices: []*discoveryv1.EndpointSlice{{ ObjectMeta: metav1.ObjectMeta{ Name: "bar-abcde", Namespace: "foo", Labels: map[string]string{ "kubernetes.io/service-name": "bar", }, }, Endpoints: []discoveryv1.Endpoint{ {Addresses: []string{"10.244.0.1"}, Conditions: discoveryv1.EndpointConditions{Ready: ptr.To(true)}}, {Addresses: []string{"10.244.0.2"}, Conditions: discoveryv1.EndpointConditions{Ready: ptr.To(true)}}, {Addresses: []string{"10.244.0.3"}, Conditions: discoveryv1.EndpointConditions{Ready: ptr.To(true)}}, }, Ports: []discoveryv1.EndpointPort{{ Name: ptr.To("port-tcp"), Port: ptr.To[int32](9527), Protocol: ptr.To(corev1.ProtocolTCP), }}, }}, expected: dedent.Dedent(` Name: bar Namespace: foo Labels: Annotations: Selector: blah=heh Type: LoadBalancer IP Families: IPv4 IP: 1.2.3.4 IPs: Desired LoadBalancer IP: 5.6.7.8 LoadBalancer Ingress: 5.6.7.8 (VIP) Port: port-tcp 8080/TCP TargetPort: 9527/TCP NodePort: port-tcp 31111/TCP Endpoints: 10.244.0.1:9527,10.244.0.2:9527,10.244.0.3:9527 Session Affinity: None External Traffic Policy: Local Internal Traffic Policy: Cluster HealthCheck NodePort: 32222 Events: `)[1:], }, { name: "test2", service: &corev1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", }, Spec: corev1.ServiceSpec{ Type: corev1.ServiceTypeLoadBalancer, Ports: []corev1.ServicePort{{ Name: "port-tcp", Port: 8080, Protocol: corev1.ProtocolTCP, TargetPort: intstr.FromString("targetPort"), NodePort: 31111, }}, Selector: map[string]string{"blah": "heh"}, ClusterIP: "1.2.3.4", IPFamilies: []corev1.IPFamily{corev1.IPv4Protocol}, LoadBalancerIP: "5.6.7.8", SessionAffinity: corev1.ServiceAffinityNone, ExternalTrafficPolicy: corev1.ServiceExternalTrafficPolicyLocal, InternalTrafficPolicy: ptr.To(corev1.ServiceInternalTrafficPolicyLocal), HealthCheckNodePort: 32222, }, Status: corev1.ServiceStatus{ LoadBalancer: corev1.LoadBalancerStatus{ Ingress: []corev1.LoadBalancerIngress{ { IP: "5.6.7.8", }, }, }, }, }, endpointSlices: []*discoveryv1.EndpointSlice{ { ObjectMeta: metav1.ObjectMeta{ Name: "bar-12345", Namespace: "foo", Labels: map[string]string{ "kubernetes.io/service-name": "bar", }, }, Endpoints: []discoveryv1.Endpoint{ {Addresses: []string{"10.244.0.1"}, Conditions: discoveryv1.EndpointConditions{Ready: ptr.To(true)}}, {Addresses: []string{"10.244.0.2"}, Conditions: discoveryv1.EndpointConditions{Ready: ptr.To(true)}}, }, Ports: []discoveryv1.EndpointPort{{ Name: ptr.To("port-tcp"), Port: ptr.To[int32](9527), Protocol: ptr.To(corev1.ProtocolUDP), }}, }, { ObjectMeta: metav1.ObjectMeta{ Name: "bar-54321", Namespace: "foo", Labels: map[string]string{ "kubernetes.io/service-name": "bar", }, }, Endpoints: []discoveryv1.Endpoint{ {Addresses: []string{"10.244.0.3"}, Conditions: discoveryv1.EndpointConditions{Ready: ptr.To(false)}}, {Addresses: []string{"10.244.0.4"}, Conditions: discoveryv1.EndpointConditions{Ready: ptr.To(true)}}, {Addresses: []string{"10.244.0.5"}, Conditions: discoveryv1.EndpointConditions{Ready: ptr.To(true)}}, }, Ports: []discoveryv1.EndpointPort{{ Name: ptr.To("port-tcp"), Port: ptr.To[int32](9527), Protocol: ptr.To(corev1.ProtocolUDP), }}, }, }, expected: dedent.Dedent(` Name: bar Namespace: foo Labels: Annotations: Selector: blah=heh Type: LoadBalancer IP Families: IPv4 IP: 1.2.3.4 IPs: Desired LoadBalancer IP: 5.6.7.8 LoadBalancer Ingress: 5.6.7.8 Port: port-tcp 8080/TCP TargetPort: targetPort/TCP NodePort: port-tcp 31111/TCP Endpoints: 10.244.0.1:9527,10.244.0.2:9527,10.244.0.4:9527 + 1 more... Session Affinity: None External Traffic Policy: Local Internal Traffic Policy: Local HealthCheck NodePort: 32222 Events: `)[1:], }, { name: "test-ready-field-empty", service: &corev1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", }, Spec: corev1.ServiceSpec{ Type: corev1.ServiceTypeLoadBalancer, Ports: []corev1.ServicePort{{ Name: "port-tcp", Port: 8080, Protocol: corev1.ProtocolTCP, TargetPort: intstr.FromInt32(9527), NodePort: 31111, }}, Selector: map[string]string{"blah": "heh"}, ClusterIP: "1.2.3.4", IPFamilies: []corev1.IPFamily{corev1.IPv4Protocol}, LoadBalancerIP: "5.6.7.8", SessionAffinity: corev1.ServiceAffinityNone, ExternalTrafficPolicy: corev1.ServiceExternalTrafficPolicyLocal, InternalTrafficPolicy: ptr.To(corev1.ServiceInternalTrafficPolicyCluster), HealthCheckNodePort: 32222, }, }, endpointSlices: []*discoveryv1.EndpointSlice{{ ObjectMeta: metav1.ObjectMeta{ Name: "bar-abcdef", Namespace: "foo", Labels: map[string]string{ "kubernetes.io/service-name": "bar", }, }, Endpoints: []discoveryv1.Endpoint{ {Addresses: []string{"10.244.0.1"}}, }, Ports: []discoveryv1.EndpointPort{{ Name: ptr.To("port-tcp"), Port: ptr.To[int32](9527), Protocol: ptr.To(corev1.ProtocolTCP), }}, }}, expected: dedent.Dedent(` Name: bar Namespace: foo Labels: Annotations: Selector: blah=heh Type: LoadBalancer IP Families: IPv4 IP: 1.2.3.4 IPs: Desired LoadBalancer IP: 5.6.7.8 Port: port-tcp 8080/TCP TargetPort: 9527/TCP NodePort: port-tcp 31111/TCP Endpoints: 10.244.0.1:9527 Session Affinity: None External Traffic Policy: Local Internal Traffic Policy: Cluster HealthCheck NodePort: 32222 Events: `)[1:], }, { name: "test-ServiceIPFamily", service: &corev1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", }, Spec: corev1.ServiceSpec{ Type: corev1.ServiceTypeLoadBalancer, Ports: []corev1.ServicePort{{ Name: "port-tcp", Port: 8080, Protocol: corev1.ProtocolTCP, TargetPort: intstr.FromString("targetPort"), NodePort: 31111, }}, Selector: map[string]string{"blah": "heh"}, ClusterIP: "1.2.3.4", IPFamilies: []corev1.IPFamily{corev1.IPv4Protocol}, LoadBalancerIP: "5.6.7.8", SessionAffinity: corev1.ServiceAffinityNone, ExternalTrafficPolicy: corev1.ServiceExternalTrafficPolicyLocal, HealthCheckNodePort: 32222, }, }, endpointSlices: []*discoveryv1.EndpointSlice{{ ObjectMeta: metav1.ObjectMeta{ Name: "bar-123ab", Namespace: "foo", Labels: map[string]string{ "kubernetes.io/service-name": "bar", }, }, Endpoints: []discoveryv1.Endpoint{ {Addresses: []string{"10.244.0.1"}, Conditions: discoveryv1.EndpointConditions{Ready: ptr.To(true)}}, }, Ports: []discoveryv1.EndpointPort{{ Name: ptr.To("port-tcp"), Port: ptr.To[int32](9527), Protocol: ptr.To(corev1.ProtocolTCP), }}, }}, expected: dedent.Dedent(` Name: bar Namespace: foo Labels: Annotations: Selector: blah=heh Type: LoadBalancer IP Families: IPv4 IP: 1.2.3.4 IPs: Desired LoadBalancer IP: 5.6.7.8 Port: port-tcp 8080/TCP TargetPort: targetPort/TCP NodePort: port-tcp 31111/TCP Endpoints: 10.244.0.1:9527 Session Affinity: None External Traffic Policy: Local HealthCheck NodePort: 32222 Events: `)[1:], }, { name: "test-ServiceIPFamilyPolicy+ClusterIPs", service: &corev1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", }, Spec: corev1.ServiceSpec{ Type: corev1.ServiceTypeLoadBalancer, Ports: []corev1.ServicePort{{ Name: "port-tcp", Port: 8080, Protocol: corev1.ProtocolTCP, TargetPort: intstr.FromString("targetPort"), NodePort: 31111, }}, Selector: map[string]string{"blah": "heh"}, ClusterIP: "1.2.3.4", IPFamilies: []corev1.IPFamily{corev1.IPv4Protocol}, IPFamilyPolicy: &singleStack, ClusterIPs: []string{"1.2.3.4"}, LoadBalancerIP: "5.6.7.8", SessionAffinity: corev1.ServiceAffinityNone, ExternalTrafficPolicy: corev1.ServiceExternalTrafficPolicyLocal, HealthCheckNodePort: 32222, }, }, expected: dedent.Dedent(` Name: bar Namespace: foo Labels: Annotations: Selector: blah=heh Type: LoadBalancer IP Family Policy: SingleStack IP Families: IPv4 IP: 1.2.3.4 IPs: 1.2.3.4 Desired LoadBalancer IP: 5.6.7.8 Port: port-tcp 8080/TCP TargetPort: targetPort/TCP NodePort: port-tcp 31111/TCP Endpoints: Session Affinity: None External Traffic Policy: Local HealthCheck NodePort: 32222 Events: `)[1:], }, { name: "test-TrafficDistribution", service: &corev1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", }, Spec: corev1.ServiceSpec{ Type: corev1.ServiceTypeLoadBalancer, Ports: []corev1.ServicePort{{ Name: "port-tcp", Port: 8080, Protocol: corev1.ProtocolTCP, TargetPort: intstr.FromString("targetPort"), NodePort: 31111, }}, Selector: map[string]string{"blah": "heh"}, ClusterIP: "1.2.3.4", IPFamilies: []corev1.IPFamily{corev1.IPv4Protocol}, LoadBalancerIP: "5.6.7.8", SessionAffinity: corev1.ServiceAffinityNone, ExternalTrafficPolicy: corev1.ServiceExternalTrafficPolicyLocal, TrafficDistribution: &preferClose, HealthCheckNodePort: 32222, }, }, expected: dedent.Dedent(` Name: bar Namespace: foo Labels: Annotations: Selector: blah=heh Type: LoadBalancer IP Families: IPv4 IP: 1.2.3.4 IPs: Desired LoadBalancer IP: 5.6.7.8 Port: port-tcp 8080/TCP TargetPort: targetPort/TCP NodePort: port-tcp 31111/TCP Endpoints: Session Affinity: None External Traffic Policy: Local HealthCheck NodePort: 32222 Traffic Distribution: PreferClose Events: `)[1:], }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { objects := []runtime.Object{tc.service} for i := range tc.endpointSlices { objects = append(objects, tc.endpointSlices[i]) } fakeClient := fake.NewSimpleClientset(objects...) c := &describeClient{T: t, Namespace: "foo", Interface: fakeClient} d := ServiceDescriber{c} out, err := d.Describe("foo", "bar", DescriberSettings{ShowEvents: true}) if err != nil { t.Errorf("unexpected error: %v", err) } assert.Equal(t, tc.expected, out) }) } } func TestPodDescribeResultsSorted(t *testing.T) { // Arrange fake := fake.NewSimpleClientset( &corev1.EventList{ Items: []corev1.Event{ { ObjectMeta: metav1.ObjectMeta{Name: "one"}, Source: corev1.EventSource{Component: "kubelet"}, Message: "Item 1", FirstTimestamp: metav1.NewTime(time.Date(2014, time.January, 15, 0, 0, 0, 0, time.UTC)), LastTimestamp: metav1.NewTime(time.Date(2014, time.January, 15, 0, 0, 0, 0, time.UTC)), Count: 1, Type: corev1.EventTypeNormal, }, { ObjectMeta: metav1.ObjectMeta{Name: "two"}, Source: corev1.EventSource{Component: "scheduler"}, Message: "Item 2", FirstTimestamp: metav1.NewTime(time.Date(1987, time.June, 17, 0, 0, 0, 0, time.UTC)), LastTimestamp: metav1.NewTime(time.Date(1987, time.June, 17, 0, 0, 0, 0, time.UTC)), Count: 1, Type: corev1.EventTypeNormal, }, { ObjectMeta: metav1.ObjectMeta{Name: "three"}, Source: corev1.EventSource{Component: "kubelet"}, Message: "Item 3", FirstTimestamp: metav1.NewTime(time.Date(2002, time.December, 25, 0, 0, 0, 0, time.UTC)), LastTimestamp: metav1.NewTime(time.Date(2002, time.December, 25, 0, 0, 0, 0, time.UTC)), Count: 1, Type: corev1.EventTypeNormal, }, }, }, &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Namespace: "foo", Name: "bar"}}, ) c := &describeClient{T: t, Namespace: "foo", Interface: fake} d := PodDescriber{c} // Act out, err := d.Describe("foo", "bar", DescriberSettings{ShowEvents: true}) // Assert if err != nil { t.Errorf("unexpected error: %v", err) } VerifyDatesInOrder(out, "\n" /* rowDelimiter */, "\t" /* columnDelimiter */, t) } // VerifyDatesInOrder checks the start of each line for a RFC1123Z date // and posts error if all subsequent dates are not equal or increasing func VerifyDatesInOrder( resultToTest, rowDelimiter, columnDelimiter string, t *testing.T) { lines := strings.Split(resultToTest, rowDelimiter) var previousTime time.Time for _, str := range lines { columns := strings.Split(str, columnDelimiter) if len(columns) > 0 { currentTime, err := time.Parse(time.RFC1123Z, columns[0]) if err == nil { if previousTime.After(currentTime) { t.Errorf( "Output is not sorted by time. %s should be listed after %s. Complete output: %s", previousTime.Format(time.RFC1123Z), currentTime.Format(time.RFC1123Z), resultToTest) } previousTime = currentTime } } } } func TestDescribeResources(t *testing.T) { testCases := []struct { resources *corev1.ResourceRequirements expectedElements map[string]int }{ { resources: &corev1.ResourceRequirements{}, expectedElements: map[string]int{}, }, { resources: &corev1.ResourceRequirements{ Requests: corev1.ResourceList{ corev1.ResourceCPU: resource.MustParse("1000"), corev1.ResourceMemory: resource.MustParse("100Mi"), }, Limits: corev1.ResourceList{ corev1.ResourceCPU: resource.MustParse("1000"), corev1.ResourceMemory: resource.MustParse("100Mi"), }, }, expectedElements: map[string]int{"cpu": 2, "memory": 2, "Requests": 1, "Limits": 1, "1k": 2, "100Mi": 2}, }, { resources: &corev1.ResourceRequirements{ Limits: corev1.ResourceList{ corev1.ResourceCPU: resource.MustParse("1000"), corev1.ResourceMemory: resource.MustParse("100Mi"), }, }, expectedElements: map[string]int{"cpu": 1, "memory": 1, "Limits": 1, "1k": 1, "100Mi": 1}, }, { resources: &corev1.ResourceRequirements{ Requests: corev1.ResourceList{ corev1.ResourceCPU: resource.MustParse("1000"), corev1.ResourceMemory: resource.MustParse("100Mi"), }, }, expectedElements: map[string]int{"cpu": 1, "memory": 1, "Requests": 1, "1k": 1, "100Mi": 1}, }, } for i, testCase := range testCases { t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { out := new(bytes.Buffer) writer := NewPrefixWriter(out) describeResources(testCase.resources, writer, LEVEL_1) output := out.String() gotElements := make(map[string]int) for key, val := range testCase.expectedElements { count := strings.Count(output, key) if count == 0 { t.Errorf("expected to find %q in output: %q", val, output) continue } gotElements[key] = count } if !reflect.DeepEqual(gotElements, testCase.expectedElements) { t.Errorf("Expected %v, got %v in output string: %q", testCase.expectedElements, gotElements, output) } }) } } func TestDescribeContainers(t *testing.T) { trueVal := true testCases := []struct { container corev1.Container status corev1.ContainerStatus expectedElements []string }{ // Running state. { container: corev1.Container{Name: "test", Image: "image"}, status: corev1.ContainerStatus{ Name: "test", State: corev1.ContainerState{ Running: &corev1.ContainerStateRunning{ StartedAt: metav1.NewTime(time.Now()), }, }, Ready: true, RestartCount: 7, }, expectedElements: []string{"test", "State", "Running", "Ready", "True", "Restart Count", "7", "Image", "image", "Started"}, }, // Waiting state. { container: corev1.Container{Name: "test", Image: "image"}, status: corev1.ContainerStatus{ Name: "test", State: corev1.ContainerState{ Waiting: &corev1.ContainerStateWaiting{ Reason: "potato", }, }, Ready: true, RestartCount: 7, }, expectedElements: []string{"test", "State", "Waiting", "Ready", "True", "Restart Count", "7", "Image", "image", "Reason", "potato"}, }, // Terminated state. { container: corev1.Container{Name: "test", Image: "image"}, status: corev1.ContainerStatus{ Name: "test", State: corev1.ContainerState{ Terminated: &corev1.ContainerStateTerminated{ StartedAt: metav1.NewTime(time.Now()), FinishedAt: metav1.NewTime(time.Now()), Reason: "potato", ExitCode: 2, }, }, Ready: true, RestartCount: 7, }, expectedElements: []string{"test", "State", "Terminated", "Ready", "True", "Restart Count", "7", "Image", "image", "Reason", "potato", "Started", "Finished", "Exit Code", "2"}, }, // Last Terminated { container: corev1.Container{Name: "test", Image: "image"}, status: corev1.ContainerStatus{ Name: "test", State: corev1.ContainerState{ Running: &corev1.ContainerStateRunning{ StartedAt: metav1.NewTime(time.Now()), }, }, LastTerminationState: corev1.ContainerState{ Terminated: &corev1.ContainerStateTerminated{ StartedAt: metav1.NewTime(time.Now().Add(time.Second * 3)), FinishedAt: metav1.NewTime(time.Now()), Reason: "crashing", ExitCode: 3, }, }, Ready: true, RestartCount: 7, }, expectedElements: []string{"test", "State", "Terminated", "Ready", "True", "Restart Count", "7", "Image", "image", "Started", "Finished", "Exit Code", "2", "crashing", "3"}, }, // No state defaults to waiting. { container: corev1.Container{Name: "test", Image: "image"}, status: corev1.ContainerStatus{ Name: "test", Ready: true, RestartCount: 7, }, expectedElements: []string{"test", "State", "Waiting", "Ready", "True", "Restart Count", "7", "Image", "image"}, }, // Env { container: corev1.Container{Name: "test", Image: "image", Env: []corev1.EnvVar{{Name: "envname", Value: "xyz"}}, EnvFrom: []corev1.EnvFromSource{{ConfigMapRef: &corev1.ConfigMapEnvSource{LocalObjectReference: corev1.LocalObjectReference{Name: "a123"}}}}}, status: corev1.ContainerStatus{ Name: "test", Ready: true, RestartCount: 7, }, expectedElements: []string{"test", "State", "Waiting", "Ready", "True", "Restart Count", "7", "Image", "image", "envname", "xyz", "a123\tConfigMap\tOptional: false"}, }, { container: corev1.Container{Name: "test", Image: "image", Env: []corev1.EnvVar{{Name: "envname", Value: "xyz"}}, EnvFrom: []corev1.EnvFromSource{{Prefix: "p_", ConfigMapRef: &corev1.ConfigMapEnvSource{LocalObjectReference: corev1.LocalObjectReference{Name: "a123"}}}}}, status: corev1.ContainerStatus{ Name: "test", Ready: true, RestartCount: 7, }, expectedElements: []string{"test", "State", "Waiting", "Ready", "True", "Restart Count", "7", "Image", "image", "envname", "xyz", "a123\tConfigMap with prefix 'p_'\tOptional: false"}, }, { container: corev1.Container{Name: "test", Image: "image", Env: []corev1.EnvVar{{Name: "envname", Value: "xyz"}}, EnvFrom: []corev1.EnvFromSource{{ConfigMapRef: &corev1.ConfigMapEnvSource{Optional: &trueVal, LocalObjectReference: corev1.LocalObjectReference{Name: "a123"}}}}}, status: corev1.ContainerStatus{ Name: "test", Ready: true, RestartCount: 7, }, expectedElements: []string{"test", "State", "Waiting", "Ready", "True", "Restart Count", "7", "Image", "image", "envname", "xyz", "a123\tConfigMap\tOptional: true"}, }, { container: corev1.Container{Name: "test", Image: "image", Env: []corev1.EnvVar{{Name: "envname", Value: "xyz"}}, EnvFrom: []corev1.EnvFromSource{{SecretRef: &corev1.SecretEnvSource{LocalObjectReference: corev1.LocalObjectReference{Name: "a123"}, Optional: &trueVal}}}}, status: corev1.ContainerStatus{ Name: "test", Ready: true, RestartCount: 7, }, expectedElements: []string{"test", "State", "Waiting", "Ready", "True", "Restart Count", "7", "Image", "image", "envname", "xyz", "a123\tSecret\tOptional: true"}, }, { container: corev1.Container{Name: "test", Image: "image", Env: []corev1.EnvVar{{Name: "envname", Value: "xyz"}}, EnvFrom: []corev1.EnvFromSource{{Prefix: "p_", SecretRef: &corev1.SecretEnvSource{LocalObjectReference: corev1.LocalObjectReference{Name: "a123"}}}}}, status: corev1.ContainerStatus{ Name: "test", Ready: true, RestartCount: 7, }, expectedElements: []string{"test", "State", "Waiting", "Ready", "True", "Restart Count", "7", "Image", "image", "envname", "xyz", "a123\tSecret with prefix 'p_'\tOptional: false"}, }, // Command { container: corev1.Container{Name: "test", Image: "image", Command: []string{"sleep", "1000"}}, status: corev1.ContainerStatus{ Name: "test", Ready: true, RestartCount: 7, }, expectedElements: []string{"test", "State", "Waiting", "Ready", "True", "Restart Count", "7", "Image", "image", "sleep", "1000"}, }, // Command with newline { container: corev1.Container{Name: "test", Image: "image", Command: []string{"sleep", "1000\n2000"}}, status: corev1.ContainerStatus{ Name: "test", Ready: true, RestartCount: 7, }, expectedElements: []string{"1000\n 2000"}, }, // Args { container: corev1.Container{Name: "test", Image: "image", Args: []string{"time", "1000"}}, status: corev1.ContainerStatus{ Name: "test", Ready: true, RestartCount: 7, }, expectedElements: []string{"test", "State", "Waiting", "Ready", "True", "Restart Count", "7", "Image", "image", "time", "1000"}, }, // Args with newline { container: corev1.Container{Name: "test", Image: "image", Args: []string{"time", "1000\n2000"}}, status: corev1.ContainerStatus{ Name: "test", Ready: true, RestartCount: 7, }, expectedElements: []string{"1000\n 2000"}, }, // Using limits. { container: corev1.Container{ Name: "test", Image: "image", Resources: corev1.ResourceRequirements{ Limits: corev1.ResourceList{ corev1.ResourceName(corev1.ResourceCPU): resource.MustParse("1000"), corev1.ResourceName(corev1.ResourceMemory): resource.MustParse("4G"), corev1.ResourceName(corev1.ResourceStorage): resource.MustParse("20G"), }, }, }, status: corev1.ContainerStatus{ Name: "test", Ready: true, RestartCount: 7, }, expectedElements: []string{"cpu", "1k", "memory", "4G", "storage", "20G"}, }, // Using requests. { container: corev1.Container{ Name: "test", Image: "image", Resources: corev1.ResourceRequirements{ Requests: corev1.ResourceList{ corev1.ResourceName(corev1.ResourceCPU): resource.MustParse("1000"), corev1.ResourceName(corev1.ResourceMemory): resource.MustParse("4G"), corev1.ResourceName(corev1.ResourceStorage): resource.MustParse("20G"), }, }, }, expectedElements: []string{"cpu", "1k", "memory", "4G", "storage", "20G"}, }, // volumeMounts read/write { container: corev1.Container{ Name: "test", Image: "image", VolumeMounts: []corev1.VolumeMount{ { Name: "mounted-volume", MountPath: "/opt/", }, }, }, expectedElements: []string{"mounted-volume", "/opt/", "(rw)"}, }, // volumeMounts readonly { container: corev1.Container{ Name: "test", Image: "image", VolumeMounts: []corev1.VolumeMount{ { Name: "mounted-volume", MountPath: "/opt/", ReadOnly: true, }, }, }, expectedElements: []string{"Mounts", "mounted-volume", "/opt/", "(ro)"}, }, // volumeMounts subPath { container: corev1.Container{ Name: "test", Image: "image", VolumeMounts: []corev1.VolumeMount{ { Name: "mounted-volume", MountPath: "/opt/", SubPath: "foo", }, }, }, expectedElements: []string{"Mounts", "mounted-volume", "/opt/", "(rw,path=\"foo\")"}, }, // volumeDevices { container: corev1.Container{ Name: "test", Image: "image", VolumeDevices: []corev1.VolumeDevice{ { Name: "volume-device", DevicePath: "/dev/xvda", }, }, }, expectedElements: []string{"Devices", "volume-device", "/dev/xvda"}, }, } for i, testCase := range testCases { t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { out := new(bytes.Buffer) pod := corev1.Pod{ Spec: corev1.PodSpec{ Containers: []corev1.Container{testCase.container}, }, Status: corev1.PodStatus{ ContainerStatuses: []corev1.ContainerStatus{testCase.status}, }, } writer := NewPrefixWriter(out) describeContainers("Containers", pod.Spec.Containers, pod.Status.ContainerStatuses, EnvValueRetriever(&pod), writer, "") output := out.String() for _, expected := range testCase.expectedElements { if !strings.Contains(output, expected) { t.Errorf("Test case %d: expected to find %q in output: %q", i, expected, output) } } }) } } func TestDescribers(t *testing.T) { first := &corev1.Event{} second := &corev1.Pod{} var third *corev1.Pod testErr := fmt.Errorf("test") d := Describers{} d.Add( func(e *corev1.Event, p *corev1.Pod) (string, error) { if e != first { t.Errorf("first argument not equal: %#v", e) } if p != second { t.Errorf("second argument not equal: %#v", p) } return "test", testErr }, ) if out, err := d.DescribeObject(first, second); out != "test" || err != testErr { t.Errorf("unexpected result: %s %v", out, err) } if out, err := d.DescribeObject(first, second, third); out != "" || err == nil { t.Errorf("unexpected result: %s %v", out, err) } else { if noDescriber, ok := err.(ErrNoDescriber); ok { if !reflect.DeepEqual(noDescriber.Types, []string{"*v1.Event", "*v1.Pod", "*v1.Pod"}) { t.Errorf("unexpected describer: %v", err) } } else { t.Errorf("unexpected error type: %v", err) } } d.Add( func(e *corev1.Event) (string, error) { if e != first { t.Errorf("first argument not equal: %#v", e) } return "simpler", testErr }, ) if out, err := d.DescribeObject(first); out != "simpler" || err != testErr { t.Errorf("unexpected result: %s %v", out, err) } } func TestDefaultDescribers(t *testing.T) { out, err := DefaultObjectDescriber.DescribeObject(&corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "foo"}}) if err != nil { t.Fatalf("unexpected error: %v", err) } if !strings.Contains(out, "foo") { t.Errorf("missing Pod `foo` in output: %s", out) } out, err = DefaultObjectDescriber.DescribeObject(&corev1.Service{ObjectMeta: metav1.ObjectMeta{Name: "foo"}}) if err != nil { t.Fatalf("unexpected error: %v", err) } if !strings.Contains(out, "foo") { t.Errorf("missing Service `foo` in output: %s", out) } out, err = DefaultObjectDescriber.DescribeObject(&corev1.ReplicationController{ ObjectMeta: metav1.ObjectMeta{Name: "foo"}, Spec: corev1.ReplicationControllerSpec{Replicas: ptr.To[int32](1)}, }) if err != nil { t.Fatalf("unexpected error: %v", err) } if !strings.Contains(out, "foo") { t.Errorf("missing Replication Controller `foo` in output: %s", out) } out, err = DefaultObjectDescriber.DescribeObject(&corev1.Node{ObjectMeta: metav1.ObjectMeta{Name: "foo"}}) if err != nil { t.Fatalf("unexpected error: %v", err) } if !strings.Contains(out, "foo") { t.Errorf("missing Node `foo` output: %s", out) } out, err = DefaultObjectDescriber.DescribeObject(&appsv1.StatefulSet{ ObjectMeta: metav1.ObjectMeta{Name: "foo"}, Spec: appsv1.StatefulSetSpec{Replicas: ptr.To[int32](1)}, }) if err != nil { t.Fatalf("unexpected error: %v", err) } if !strings.Contains(out, "foo") { t.Errorf("missing StatefulSet `foo` in output: %s", out) } } func TestGetPodsTotalRequests(t *testing.T) { testCases := []struct { name string pods *corev1.PodList expectedReqs map[corev1.ResourceName]resource.Quantity }{ { name: "test1", pods: &corev1.PodList{ Items: []corev1.Pod{ { Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Resources: corev1.ResourceRequirements{ Requests: corev1.ResourceList{ corev1.ResourceName(corev1.ResourceCPU): resource.MustParse("1"), corev1.ResourceName(corev1.ResourceMemory): resource.MustParse("300Mi"), corev1.ResourceName(corev1.ResourceStorage): resource.MustParse("1G"), }, }, }, { Resources: corev1.ResourceRequirements{ Requests: corev1.ResourceList{ corev1.ResourceName(corev1.ResourceCPU): resource.MustParse("90m"), corev1.ResourceName(corev1.ResourceMemory): resource.MustParse("120Mi"), corev1.ResourceName(corev1.ResourceStorage): resource.MustParse("200M"), }, }, }, }, }, }, { Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Resources: corev1.ResourceRequirements{ Requests: corev1.ResourceList{ corev1.ResourceName(corev1.ResourceCPU): resource.MustParse("60m"), corev1.ResourceName(corev1.ResourceMemory): resource.MustParse("43Mi"), corev1.ResourceName(corev1.ResourceStorage): resource.MustParse("500M"), }, }, }, { Resources: corev1.ResourceRequirements{ Requests: corev1.ResourceList{ corev1.ResourceName(corev1.ResourceCPU): resource.MustParse("34m"), corev1.ResourceName(corev1.ResourceMemory): resource.MustParse("83Mi"), corev1.ResourceName(corev1.ResourceStorage): resource.MustParse("700M"), }, }, }, }, }, }, }, }, expectedReqs: map[corev1.ResourceName]resource.Quantity{ corev1.ResourceName(corev1.ResourceCPU): resource.MustParse("1.184"), corev1.ResourceName(corev1.ResourceMemory): resource.MustParse("546Mi"), corev1.ResourceName(corev1.ResourceStorage): resource.MustParse("2.4G"), }, }, } for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { reqs, _ := getPodsTotalRequestsAndLimits(testCase.pods) if !apiequality.Semantic.DeepEqual(reqs, testCase.expectedReqs) { t.Errorf("Expected %v, got %v", testCase.expectedReqs, reqs) } }) } } func TestPersistentVolumeDescriber(t *testing.T) { block := corev1.PersistentVolumeBlock file := corev1.PersistentVolumeFilesystem foo := "glusterfsendpointname" deletionTimestamp := metav1.Time{Time: time.Now().UTC().AddDate(-10, 0, 0)} testCases := []struct { name string plugin string pv *corev1.PersistentVolume expectedElements []string unexpectedElements []string }{ { name: "test0", plugin: "hostpath", pv: &corev1.PersistentVolume{ ObjectMeta: metav1.ObjectMeta{Name: "bar"}, Spec: corev1.PersistentVolumeSpec{ PersistentVolumeSource: corev1.PersistentVolumeSource{ HostPath: &corev1.HostPathVolumeSource{Type: new(corev1.HostPathType)}, }, }, }, unexpectedElements: []string{"VolumeMode", "Filesystem"}, }, { name: "test1", plugin: "gce", pv: &corev1.PersistentVolume{ ObjectMeta: metav1.ObjectMeta{Name: "bar"}, Spec: corev1.PersistentVolumeSpec{ PersistentVolumeSource: corev1.PersistentVolumeSource{ GCEPersistentDisk: &corev1.GCEPersistentDiskVolumeSource{}, }, VolumeMode: &file, }, }, expectedElements: []string{"VolumeMode", "Filesystem"}, }, { name: "test2", plugin: "ebs", pv: &corev1.PersistentVolume{ ObjectMeta: metav1.ObjectMeta{Name: "bar"}, Spec: corev1.PersistentVolumeSpec{ PersistentVolumeSource: corev1.PersistentVolumeSource{ AWSElasticBlockStore: &corev1.AWSElasticBlockStoreVolumeSource{}, }, }, }, unexpectedElements: []string{"VolumeMode", "Filesystem"}, }, { name: "test3", plugin: "nfs", pv: &corev1.PersistentVolume{ ObjectMeta: metav1.ObjectMeta{Name: "bar"}, Spec: corev1.PersistentVolumeSpec{ PersistentVolumeSource: corev1.PersistentVolumeSource{ NFS: &corev1.NFSVolumeSource{}, }, }, }, unexpectedElements: []string{"VolumeMode", "Filesystem"}, }, { name: "test4", plugin: "iscsi", pv: &corev1.PersistentVolume{ ObjectMeta: metav1.ObjectMeta{Name: "bar"}, Spec: corev1.PersistentVolumeSpec{ PersistentVolumeSource: corev1.PersistentVolumeSource{ ISCSI: &corev1.ISCSIPersistentVolumeSource{}, }, VolumeMode: &block, }, }, expectedElements: []string{"VolumeMode", "Block"}, }, { name: "test5", plugin: "gluster", pv: &corev1.PersistentVolume{ ObjectMeta: metav1.ObjectMeta{Name: "bar"}, Spec: corev1.PersistentVolumeSpec{ PersistentVolumeSource: corev1.PersistentVolumeSource{ Glusterfs: &corev1.GlusterfsPersistentVolumeSource{}, }, }, }, expectedElements: []string{"EndpointsNamespace", ""}, unexpectedElements: []string{"VolumeMode", "Filesystem"}, }, { name: "test6", plugin: "rbd", pv: &corev1.PersistentVolume{ ObjectMeta: metav1.ObjectMeta{Name: "bar"}, Spec: corev1.PersistentVolumeSpec{ PersistentVolumeSource: corev1.PersistentVolumeSource{ RBD: &corev1.RBDPersistentVolumeSource{}, }, }, }, unexpectedElements: []string{"VolumeMode", "Filesystem"}, }, { name: "test7", plugin: "quobyte", pv: &corev1.PersistentVolume{ ObjectMeta: metav1.ObjectMeta{Name: "bar"}, Spec: corev1.PersistentVolumeSpec{ PersistentVolumeSource: corev1.PersistentVolumeSource{ Quobyte: &corev1.QuobyteVolumeSource{}, }, }, }, unexpectedElements: []string{"VolumeMode", "Filesystem"}, }, { name: "test8", plugin: "cinder", pv: &corev1.PersistentVolume{ ObjectMeta: metav1.ObjectMeta{Name: "bar"}, Spec: corev1.PersistentVolumeSpec{ PersistentVolumeSource: corev1.PersistentVolumeSource{ Cinder: &corev1.CinderPersistentVolumeSource{}, }, }, }, unexpectedElements: []string{"VolumeMode", "Filesystem"}, }, { name: "test9", plugin: "fc", pv: &corev1.PersistentVolume{ ObjectMeta: metav1.ObjectMeta{Name: "bar"}, Spec: corev1.PersistentVolumeSpec{ PersistentVolumeSource: corev1.PersistentVolumeSource{ FC: &corev1.FCVolumeSource{}, }, VolumeMode: &block, }, }, expectedElements: []string{"VolumeMode", "Block"}, }, { name: "test10", plugin: "local", pv: &corev1.PersistentVolume{ ObjectMeta: metav1.ObjectMeta{Name: "bar"}, Spec: corev1.PersistentVolumeSpec{ PersistentVolumeSource: corev1.PersistentVolumeSource{ Local: &corev1.LocalVolumeSource{}, }, }, }, expectedElements: []string{"Node Affinity: "}, unexpectedElements: []string{"Required Terms", "Term "}, }, { name: "test11", plugin: "local", pv: &corev1.PersistentVolume{ ObjectMeta: metav1.ObjectMeta{Name: "bar"}, Spec: corev1.PersistentVolumeSpec{ PersistentVolumeSource: corev1.PersistentVolumeSource{ Local: &corev1.LocalVolumeSource{}, }, NodeAffinity: &corev1.VolumeNodeAffinity{}, }, }, expectedElements: []string{"Node Affinity: "}, unexpectedElements: []string{"Required Terms", "Term "}, }, { name: "test12", plugin: "local", pv: &corev1.PersistentVolume{ ObjectMeta: metav1.ObjectMeta{Name: "bar"}, Spec: corev1.PersistentVolumeSpec{ PersistentVolumeSource: corev1.PersistentVolumeSource{ Local: &corev1.LocalVolumeSource{}, }, NodeAffinity: &corev1.VolumeNodeAffinity{ Required: &corev1.NodeSelector{}, }, }, }, expectedElements: []string{"Node Affinity", "Required Terms: "}, unexpectedElements: []string{"Term "}, }, { name: "test13", plugin: "local", pv: &corev1.PersistentVolume{ ObjectMeta: metav1.ObjectMeta{Name: "bar"}, Spec: corev1.PersistentVolumeSpec{ PersistentVolumeSource: corev1.PersistentVolumeSource{ Local: &corev1.LocalVolumeSource{}, }, NodeAffinity: &corev1.VolumeNodeAffinity{ Required: &corev1.NodeSelector{ NodeSelectorTerms: []corev1.NodeSelectorTerm{ { MatchExpressions: []corev1.NodeSelectorRequirement{}, }, { MatchExpressions: []corev1.NodeSelectorRequirement{}, }, }, }, }, }, }, expectedElements: []string{"Node Affinity", "Required Terms", "Term 0", "Term 1"}, }, { name: "test14", plugin: "local", pv: &corev1.PersistentVolume{ ObjectMeta: metav1.ObjectMeta{Name: "bar"}, Spec: corev1.PersistentVolumeSpec{ PersistentVolumeSource: corev1.PersistentVolumeSource{ Local: &corev1.LocalVolumeSource{}, }, NodeAffinity: &corev1.VolumeNodeAffinity{ Required: &corev1.NodeSelector{ NodeSelectorTerms: []corev1.NodeSelectorTerm{ { MatchExpressions: []corev1.NodeSelectorRequirement{ { Key: "foo", Operator: "In", Values: []string{"val1", "val2"}, }, { Key: "foo", Operator: "Exists", }, }, }, }, }, }, }, }, expectedElements: []string{"Node Affinity", "Required Terms", "Term 0", "foo in [val1, val2]", "foo exists"}, }, { name: "test15", plugin: "local", pv: &corev1.PersistentVolume{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", DeletionTimestamp: &deletionTimestamp, }, Spec: corev1.PersistentVolumeSpec{ PersistentVolumeSource: corev1.PersistentVolumeSource{ Local: &corev1.LocalVolumeSource{}, }, }, }, expectedElements: []string{"Terminating (lasts 10y)"}, }, { name: "test16", plugin: "local", pv: &corev1.PersistentVolume{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", GenerateName: "test-GenerateName", UID: "test-UID", CreationTimestamp: metav1.Time{Time: time.Now()}, DeletionTimestamp: &metav1.Time{Time: time.Now()}, DeletionGracePeriodSeconds: new(int64), Labels: map[string]string{"label1": "label1", "label2": "label2", "label3": "label3"}, Annotations: map[string]string{"annotation1": "annotation1", "annotation2": "annotation2", "annotation3": "annotation3"}, }, Spec: corev1.PersistentVolumeSpec{ PersistentVolumeSource: corev1.PersistentVolumeSource{ Local: &corev1.LocalVolumeSource{}, }, NodeAffinity: &corev1.VolumeNodeAffinity{ Required: &corev1.NodeSelector{ NodeSelectorTerms: []corev1.NodeSelectorTerm{ { MatchExpressions: []corev1.NodeSelectorRequirement{ { Key: "foo", Operator: "In", Values: []string{"val1", "val2"}, }, { Key: "foo", Operator: "Exists", }, }, }, }, }, }, }, }, expectedElements: []string{"Node Affinity", "Required Terms", "Term 0", "foo in [val1, val2]", "foo exists"}, }, { name: "test17", plugin: "local", pv: &corev1.PersistentVolume{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", GenerateName: "test-GenerateName", UID: "test-UID", CreationTimestamp: metav1.Time{Time: time.Now()}, DeletionTimestamp: &metav1.Time{Time: time.Now()}, DeletionGracePeriodSeconds: new(int64), Labels: map[string]string{"label1": "label1", "label2": "label2", "label3": "label3"}, Annotations: map[string]string{"annotation1": "annotation1", "annotation2": "annotation2", "annotation3": "annotation3"}, }, Spec: corev1.PersistentVolumeSpec{ PersistentVolumeSource: corev1.PersistentVolumeSource{ CSI: &corev1.CSIPersistentVolumeSource{ Driver: "drive", VolumeHandle: "handler", ReadOnly: true, VolumeAttributes: map[string]string{ "Attribute1": "Value1", "Attribute2": "Value2", "Attribute3": "Value3", }, }, }, }, }, expectedElements: []string{"Driver", "VolumeHandle", "ReadOnly", "VolumeAttributes"}, }, { name: "test19", plugin: "gluster", pv: &corev1.PersistentVolume{ ObjectMeta: metav1.ObjectMeta{Name: "bar"}, Spec: corev1.PersistentVolumeSpec{ PersistentVolumeSource: corev1.PersistentVolumeSource{ Glusterfs: &corev1.GlusterfsPersistentVolumeSource{ EndpointsNamespace: &foo, }, }, }, }, expectedElements: []string{"EndpointsNamespace", "glusterfsendpointname"}, unexpectedElements: []string{"VolumeMode", "Filesystem"}, }, } for _, test := range testCases { t.Run(test.name, func(t *testing.T) { fake := fake.NewSimpleClientset(test.pv) c := PersistentVolumeDescriber{fake} str, err := c.Describe("foo", "bar", DescriberSettings{ShowEvents: true}) if err != nil { t.Errorf("Unexpected error for test %s: %v", test.plugin, err) } if str == "" { t.Errorf("Unexpected empty string for test %s. Expected PV Describer output", test.plugin) } for _, expected := range test.expectedElements { if !strings.Contains(str, expected) { t.Errorf("expected to find %q in output: %q", expected, str) } } for _, unexpected := range test.unexpectedElements { if strings.Contains(str, unexpected) { t.Errorf("unexpected to find %q in output: %q", unexpected, str) } } }) } } func TestPersistentVolumeClaimDescriber(t *testing.T) { block := corev1.PersistentVolumeBlock file := corev1.PersistentVolumeFilesystem goldClassName := "gold" now := time.Now() deletionTimestamp := metav1.Time{Time: time.Now().UTC().AddDate(-10, 0, 0)} snapshotAPIGroup := "snapshot.storage.k8s.io" defaultDescriberSettings := &DescriberSettings{ShowEvents: true} testCases := []struct { name string pvc *corev1.PersistentVolumeClaim describerSettings *DescriberSettings expectedElements []string unexpectedElements []string }{ { name: "default", pvc: &corev1.PersistentVolumeClaim{ ObjectMeta: metav1.ObjectMeta{Namespace: "foo", Name: "bar"}, Spec: corev1.PersistentVolumeClaimSpec{ VolumeName: "volume1", StorageClassName: &goldClassName, }, Status: corev1.PersistentVolumeClaimStatus{ Phase: corev1.ClaimBound, }, }, expectedElements: []string{"Events"}, unexpectedElements: []string{"VolumeMode", "Filesystem"}, }, { name: "filesystem", pvc: &corev1.PersistentVolumeClaim{ ObjectMeta: metav1.ObjectMeta{Namespace: "foo", Name: "bar"}, Spec: corev1.PersistentVolumeClaimSpec{ VolumeName: "volume2", StorageClassName: &goldClassName, VolumeMode: &file, }, Status: corev1.PersistentVolumeClaimStatus{ Phase: corev1.ClaimBound, }, }, expectedElements: []string{"VolumeMode", "Filesystem"}, }, { name: "block", pvc: &corev1.PersistentVolumeClaim{ ObjectMeta: metav1.ObjectMeta{Namespace: "foo", Name: "bar"}, Spec: corev1.PersistentVolumeClaimSpec{ VolumeName: "volume3", StorageClassName: &goldClassName, VolumeMode: &block, }, Status: corev1.PersistentVolumeClaimStatus{ Phase: corev1.ClaimBound, }, }, expectedElements: []string{"VolumeMode", "Block"}, }, // Tests for Status.Condition. { name: "condition-type", pvc: &corev1.PersistentVolumeClaim{ ObjectMeta: metav1.ObjectMeta{Namespace: "foo", Name: "bar"}, Spec: corev1.PersistentVolumeClaimSpec{ VolumeName: "volume4", StorageClassName: &goldClassName, }, Status: corev1.PersistentVolumeClaimStatus{ Conditions: []corev1.PersistentVolumeClaimCondition{ {Type: corev1.PersistentVolumeClaimResizing}, }, }, }, expectedElements: []string{"Conditions", "Type", "Resizing"}, }, { name: "condition-status", pvc: &corev1.PersistentVolumeClaim{ ObjectMeta: metav1.ObjectMeta{Namespace: "foo", Name: "bar"}, Spec: corev1.PersistentVolumeClaimSpec{ VolumeName: "volume5", StorageClassName: &goldClassName, }, Status: corev1.PersistentVolumeClaimStatus{ Conditions: []corev1.PersistentVolumeClaimCondition{ {Status: corev1.ConditionTrue}, }, }, }, expectedElements: []string{"Conditions", "Status", "True"}, }, { name: "condition-last-probe-time", pvc: &corev1.PersistentVolumeClaim{ ObjectMeta: metav1.ObjectMeta{Namespace: "foo", Name: "bar"}, Spec: corev1.PersistentVolumeClaimSpec{ VolumeName: "volume6", StorageClassName: &goldClassName, }, Status: corev1.PersistentVolumeClaimStatus{ Conditions: []corev1.PersistentVolumeClaimCondition{ {LastProbeTime: metav1.Time{Time: now}}, }, }, }, expectedElements: []string{"Conditions", "LastProbeTime", now.Format(time.RFC1123Z)}, }, { name: "condition-last-transition-time", pvc: &corev1.PersistentVolumeClaim{ ObjectMeta: metav1.ObjectMeta{Namespace: "foo", Name: "bar"}, Spec: corev1.PersistentVolumeClaimSpec{ VolumeName: "volume7", StorageClassName: &goldClassName, }, Status: corev1.PersistentVolumeClaimStatus{ Conditions: []corev1.PersistentVolumeClaimCondition{ {LastTransitionTime: metav1.Time{Time: now}}, }, }, }, expectedElements: []string{"Conditions", "LastTransitionTime", now.Format(time.RFC1123Z)}, }, { name: "condition-reason", pvc: &corev1.PersistentVolumeClaim{ ObjectMeta: metav1.ObjectMeta{Namespace: "foo", Name: "bar"}, Spec: corev1.PersistentVolumeClaimSpec{ VolumeName: "volume8", StorageClassName: &goldClassName, }, Status: corev1.PersistentVolumeClaimStatus{ Conditions: []corev1.PersistentVolumeClaimCondition{ {Reason: "OfflineResize"}, }, }, }, expectedElements: []string{"Conditions", "Reason", "OfflineResize"}, }, { name: "condition-message", pvc: &corev1.PersistentVolumeClaim{ ObjectMeta: metav1.ObjectMeta{Namespace: "foo", Name: "bar"}, Spec: corev1.PersistentVolumeClaimSpec{ VolumeName: "volume9", StorageClassName: &goldClassName, }, Status: corev1.PersistentVolumeClaimStatus{ Conditions: []corev1.PersistentVolumeClaimCondition{ {Message: "User request resize"}, }, }, }, expectedElements: []string{"Conditions", "Message", "User request resize"}, }, { name: "deletion-timestamp", pvc: &corev1.PersistentVolumeClaim{ ObjectMeta: metav1.ObjectMeta{ Namespace: "foo", Name: "bar", DeletionTimestamp: &deletionTimestamp, }, Spec: corev1.PersistentVolumeClaimSpec{ VolumeName: "volume10", StorageClassName: &goldClassName, }, Status: corev1.PersistentVolumeClaimStatus{}, }, expectedElements: []string{"Terminating (lasts 10y)"}, }, { name: "pvc-datasource", pvc: &corev1.PersistentVolumeClaim{ ObjectMeta: metav1.ObjectMeta{ Namespace: "foo", Name: "bar", }, Spec: corev1.PersistentVolumeClaimSpec{ VolumeName: "volume10", StorageClassName: &goldClassName, DataSource: &corev1.TypedLocalObjectReference{ Name: "srcpvc", Kind: "PersistentVolumeClaim", }, }, Status: corev1.PersistentVolumeClaimStatus{}, }, expectedElements: []string{"\nDataSource:\n Kind: PersistentVolumeClaim\n Name: srcpvc"}, }, { name: "snapshot-datasource", pvc: &corev1.PersistentVolumeClaim{ ObjectMeta: metav1.ObjectMeta{ Namespace: "foo", Name: "bar", }, Spec: corev1.PersistentVolumeClaimSpec{ VolumeName: "volume10", StorageClassName: &goldClassName, DataSource: &corev1.TypedLocalObjectReference{ Name: "src-snapshot", Kind: "VolumeSnapshot", APIGroup: &snapshotAPIGroup, }, }, Status: corev1.PersistentVolumeClaimStatus{}, }, expectedElements: []string{"DataSource:\n APIGroup: snapshot.storage.k8s.io\n Kind: VolumeSnapshot\n Name: src-snapshot\n"}, }, { name: "no-show-events", pvc: &corev1.PersistentVolumeClaim{ ObjectMeta: metav1.ObjectMeta{Namespace: "foo", Name: "bar"}, Spec: corev1.PersistentVolumeClaimSpec{ VolumeName: "volume1", StorageClassName: &goldClassName, }, Status: corev1.PersistentVolumeClaimStatus{ Phase: corev1.ClaimBound, }, }, unexpectedElements: []string{"Events"}, describerSettings: &DescriberSettings{ShowEvents: false}, }, } for _, test := range testCases { t.Run(test.name, func(t *testing.T) { fake := fake.NewSimpleClientset(test.pvc) c := PersistentVolumeClaimDescriber{fake} var describerSettings DescriberSettings if test.describerSettings != nil { describerSettings = *test.describerSettings } else { describerSettings = *defaultDescriberSettings } str, err := c.Describe("foo", "bar", describerSettings) if err != nil { t.Errorf("Unexpected error for test %s: %v", test.name, err) } if str == "" { t.Errorf("Unexpected empty string for test %s. Expected PVC Describer output", test.name) } for _, expected := range test.expectedElements { if !strings.Contains(str, expected) { t.Errorf("expected to find %q in output: %q", expected, str) } } for _, unexpected := range test.unexpectedElements { if strings.Contains(str, unexpected) { t.Errorf("unexpected to find %q in output: %q", unexpected, str) } } }) } } func TestGetPodsForPVC(t *testing.T) { goldClassName := "gold" testCases := []struct { name string pvc *corev1.PersistentVolumeClaim requiredObjects []runtime.Object expectedPods []string }{ { name: "pvc-unused", pvc: &corev1.PersistentVolumeClaim{ ObjectMeta: metav1.ObjectMeta{Namespace: "ns", Name: "pvc-name"}, Spec: corev1.PersistentVolumeClaimSpec{ VolumeName: "volume1", StorageClassName: &goldClassName, }, Status: corev1.PersistentVolumeClaimStatus{ Phase: corev1.ClaimBound, }, }, expectedPods: []string{}, }, { name: "pvc-in-pods-volumes-list", pvc: &corev1.PersistentVolumeClaim{ ObjectMeta: metav1.ObjectMeta{Namespace: "ns", Name: "pvc-name"}, Spec: corev1.PersistentVolumeClaimSpec{ VolumeName: "volume1", StorageClassName: &goldClassName, }, Status: corev1.PersistentVolumeClaimStatus{ Phase: corev1.ClaimBound, }, }, requiredObjects: []runtime.Object{ &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{Namespace: "ns", Name: "pod-name"}, Spec: corev1.PodSpec{ Volumes: []corev1.Volume{ { Name: "volume", VolumeSource: corev1.VolumeSource{ PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ ClaimName: "pvc-name", }, }, }, }, }, }, }, expectedPods: []string{"pod-name"}, }, { name: "pvc-owned-by-pod", pvc: &corev1.PersistentVolumeClaim{ ObjectMeta: metav1.ObjectMeta{ Namespace: "ns", Name: "pvc-name", OwnerReferences: []metav1.OwnerReference{ { Kind: "Pod", Name: "pod-name", UID: "pod-uid", }, }, }, Spec: corev1.PersistentVolumeClaimSpec{ VolumeName: "volume1", StorageClassName: &goldClassName, }, Status: corev1.PersistentVolumeClaimStatus{ Phase: corev1.ClaimBound, }, }, requiredObjects: []runtime.Object{ &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{Namespace: "ns", Name: "pod-name", UID: "pod-uid"}, }, }, expectedPods: []string{"pod-name"}, }, } for _, test := range testCases { t.Run(test.name, func(t *testing.T) { var objects []runtime.Object objects = append(objects, test.requiredObjects...) objects = append(objects, test.pvc) fake := fake.NewSimpleClientset(objects...) pods, err := getPodsForPVC(fake.CoreV1().Pods(test.pvc.ObjectMeta.Namespace), test.pvc, DescriberSettings{}) if err != nil { t.Errorf("Unexpected error for test %s: %v", test.name, err) } for _, expectedPod := range test.expectedPods { foundPod := false for _, pod := range pods { if pod.Name == expectedPod { foundPod = true break } } if !foundPod { t.Errorf("Expected pod %s, but it was not returned: %v", expectedPod, pods) } } if len(test.expectedPods) != len(pods) { t.Errorf("Expected %d pods, but got %d pods", len(test.expectedPods), len(pods)) } }) } } func TestDescribeDeployment(t *testing.T) { labels := map[string]string{"k8s-app": "bar"} testCases := []struct { name string objects []runtime.Object expects []string }{ { name: "deployment with two mounted volumes", objects: []runtime.Object{ &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", Labels: labels, UID: "00000000-0000-0000-0000-000000000001", CreationTimestamp: metav1.NewTime(time.Date(2021, time.Month(1), 1, 0, 0, 0, 0, time.UTC)), }, Spec: appsv1.DeploymentSpec{ Replicas: ptr.To[int32](1), Selector: &metav1.LabelSelector{ MatchLabels: labels, }, Template: corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", Labels: labels, }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Image: "mytest-image:latest", VolumeMounts: []corev1.VolumeMount{ { Name: "vol-foo", MountPath: "/tmp/vol-foo", }, { Name: "vol-bar", MountPath: "/tmp/vol-bar", }, }, }, }, Volumes: []corev1.Volume{ { Name: "vol-foo", VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{}}, }, { Name: "vol-bar", VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{}}, }, }, }, }, }, }, &appsv1.ReplicaSet{ ObjectMeta: metav1.ObjectMeta{ Name: "bar-001", Namespace: "foo", Labels: labels, OwnerReferences: []metav1.OwnerReference{ { Controller: ptr.To(true), UID: "00000000-0000-0000-0000-000000000001", }, }, }, Spec: appsv1.ReplicaSetSpec{ Replicas: ptr.To[int32](1), Selector: &metav1.LabelSelector{ MatchLabels: labels, }, Template: corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", Labels: labels, }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Image: "mytest-image:latest", VolumeMounts: []corev1.VolumeMount{ { Name: "vol-foo", MountPath: "/tmp/vol-foo", }, { Name: "vol-bar", MountPath: "/tmp/vol-bar", }, }, }, }, Volumes: []corev1.Volume{ { Name: "vol-foo", VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{}}, }, { Name: "vol-bar", VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{}}, }, }, }, }, }, Status: appsv1.ReplicaSetStatus{ Replicas: 1, ReadyReplicas: 1, AvailableReplicas: 1, }, }, }, expects: []string{ "Name: bar\nNamespace: foo", "CreationTimestamp: Fri, 01 Jan 2021 00:00:00 +0000", "Labels: k8s-app=bar", "Selector: k8s-app=bar", "Replicas: 1 desired | 0 updated | 0 total | 0 available | 0 unavailable", "Image: mytest-image:latest", "Mounts:\n /tmp/vol-bar from vol-bar (rw)\n /tmp/vol-foo from vol-foo (rw)", "OldReplicaSets: ", "NewReplicaSet: bar-001 (1/1 replicas created)", "Events: ", "Node-Selectors: ", "Tolerations: ", }, }, { name: "deployment during the process of rolling out", objects: []runtime.Object{ &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", Labels: labels, UID: "00000000-0000-0000-0000-000000000001", CreationTimestamp: metav1.NewTime(time.Date(2021, time.Month(1), 1, 0, 0, 0, 0, time.UTC)), }, Spec: appsv1.DeploymentSpec{ Replicas: ptr.To[int32](2), Selector: &metav1.LabelSelector{ MatchLabels: labels, }, Template: corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", Labels: labels, }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Image: "mytest-image:v2.0", VolumeMounts: []corev1.VolumeMount{ { Name: "vol-foo", MountPath: "/tmp/vol-foo", }, { Name: "vol-bar", MountPath: "/tmp/vol-bar", }, }, }, }, Volumes: []corev1.Volume{ { Name: "vol-foo", VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{}}, }, { Name: "vol-bar", VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{}}, }, }, }, }, }, Status: appsv1.DeploymentStatus{ Replicas: 3, UpdatedReplicas: 1, AvailableReplicas: 2, UnavailableReplicas: 1, }, }, &appsv1.ReplicaSet{ ObjectMeta: metav1.ObjectMeta{ Name: "bar-001", Namespace: "foo", Labels: labels, UID: "00000000-0000-0000-0000-000000000001", OwnerReferences: []metav1.OwnerReference{ { Controller: ptr.To(true), UID: "00000000-0000-0000-0000-000000000001", }, }, }, Spec: appsv1.ReplicaSetSpec{ Replicas: ptr.To[int32](2), Selector: &metav1.LabelSelector{ MatchLabels: labels, }, Template: corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", Labels: labels, }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Image: "mytest-image:v1.0", VolumeMounts: []corev1.VolumeMount{ { Name: "vol-foo", MountPath: "/tmp/vol-foo", }, { Name: "vol-bar", MountPath: "/tmp/vol-bar", }, }, }, }, Volumes: []corev1.Volume{ { Name: "vol-foo", VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{}}, }, { Name: "vol-bar", VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{}}, }, }, }, }, }, Status: appsv1.ReplicaSetStatus{ Replicas: 2, ReadyReplicas: 2, AvailableReplicas: 2, }, }, &appsv1.ReplicaSet{ ObjectMeta: metav1.ObjectMeta{ Name: "bar-002", Namespace: "foo", Labels: labels, UID: "00000000-0000-0000-0000-000000000002", OwnerReferences: []metav1.OwnerReference{ { Controller: ptr.To(true), UID: "00000000-0000-0000-0000-000000000001", }, }, }, Spec: appsv1.ReplicaSetSpec{ Replicas: ptr.To[int32](1), Selector: &metav1.LabelSelector{ MatchLabels: labels, }, Template: corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", Labels: labels, }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Image: "mytest-image:v2.0", VolumeMounts: []corev1.VolumeMount{ { Name: "vol-foo", MountPath: "/tmp/vol-foo", }, { Name: "vol-bar", MountPath: "/tmp/vol-bar", }, }, }, }, Volumes: []corev1.Volume{ { Name: "vol-foo", VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{}}, }, { Name: "vol-bar", VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{}}, }, }, }, }, }, Status: appsv1.ReplicaSetStatus{ Replicas: 1, ReadyReplicas: 0, AvailableReplicas: 1, }, }, &corev1.Event{ ObjectMeta: metav1.ObjectMeta{ Name: "bar-000", Namespace: "foo", }, InvolvedObject: corev1.ObjectReference{ APIVersion: "apps/v1", Kind: "Deployment", Name: "bar", Namespace: "foo", UID: "00000000-0000-0000-0000-000000000001", }, Type: corev1.EventTypeNormal, Reason: "ScalingReplicaSet", Message: "Scaled up replica set bar-002 from 0 to 1", ReportingController: "deployment-controller", EventTime: metav1.NewMicroTime(time.Now().Add(-20 * time.Minute)), Series: &corev1.EventSeries{ Count: 3, LastObservedTime: metav1.NewMicroTime(time.Now().Add(-12 * time.Minute)), }, }, &corev1.Event{ ObjectMeta: metav1.ObjectMeta{ Name: "bar-001", Namespace: "foo", }, InvolvedObject: corev1.ObjectReference{ APIVersion: "apps/v1", Kind: "Deployment", Name: "bar", Namespace: "foo", UID: "00000000-0000-0000-0000-000000000001", }, Type: corev1.EventTypeNormal, Reason: "ScalingReplicaSet", Message: "Scaled up replica set bar-001 from 0 to 2", Source: corev1.EventSource{ Component: "deployment-controller", }, FirstTimestamp: metav1.NewTime(time.Now().Add(-10 * time.Minute)), }, &corev1.Event{ ObjectMeta: metav1.ObjectMeta{ Name: "bar-002", Namespace: "foo", }, InvolvedObject: corev1.ObjectReference{ APIVersion: "apps/v1", Kind: "Deployment", Name: "bar", Namespace: "foo", UID: "00000000-0000-0000-0000-000000000001", }, Type: corev1.EventTypeNormal, Reason: "ScalingReplicaSet", Message: "Scaled up replica set bar-002 from 0 to 1", Source: corev1.EventSource{ Component: "deployment-controller", }, FirstTimestamp: metav1.NewTime(time.Now().Add(-2 * time.Minute)), }, &corev1.Event{ ObjectMeta: metav1.ObjectMeta{ Name: "bar-003", Namespace: "foo", }, InvolvedObject: corev1.ObjectReference{ APIVersion: "apps/v1", Kind: "Deployment", Name: "bar", Namespace: "foo", UID: "00000000-0000-0000-0000-000000000001", }, Type: corev1.EventTypeNormal, Reason: "ScalingReplicaSet", Message: "Scaled down replica set bar-002 from 2 to 1", ReportingController: "deployment-controller", EventTime: metav1.NewMicroTime(time.Now().Add(-1 * time.Minute)), }, }, expects: []string{ "Replicas: 2 desired | 1 updated | 3 total | 2 available | 1 unavailable", "Image: mytest-image:v2.0", "OldReplicaSets: bar-001 (2/2 replicas created)", "NewReplicaSet: bar-002 (1/1 replicas created)", "Events:\n", "Normal ScalingReplicaSet 12m (x3 over 20m) deployment-controller Scaled up replica set bar-002 from 0 to 1", "Normal ScalingReplicaSet 10m deployment-controller Scaled up replica set bar-001 from 0 to 2", "Normal ScalingReplicaSet 2m deployment-controller Scaled up replica set bar-002 from 0 to 1", "Normal ScalingReplicaSet 60s deployment-controller Scaled down replica set bar-002 from 2 to 1", }, }, { name: "deployment after successful rollout", objects: []runtime.Object{ &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", Labels: labels, UID: "00000000-0000-0000-0000-000000000001", CreationTimestamp: metav1.NewTime(time.Date(2021, time.Month(1), 1, 0, 0, 0, 0, time.UTC)), }, Spec: appsv1.DeploymentSpec{ Replicas: ptr.To[int32](2), Selector: &metav1.LabelSelector{ MatchLabels: labels, }, Template: corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", Labels: labels, }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Image: "mytest-image:v2.0", VolumeMounts: []corev1.VolumeMount{ { Name: "vol-foo", MountPath: "/tmp/vol-foo", }, { Name: "vol-bar", MountPath: "/tmp/vol-bar", }, }, }, }, Volumes: []corev1.Volume{ { Name: "vol-foo", VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{}}, }, { Name: "vol-bar", VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{}}, }, }, }, }, }, Status: appsv1.DeploymentStatus{ Replicas: 2, UpdatedReplicas: 2, AvailableReplicas: 2, UnavailableReplicas: 0, }, }, &appsv1.ReplicaSet{ ObjectMeta: metav1.ObjectMeta{ Name: "bar-001", Namespace: "foo", Labels: labels, UID: "00000000-0000-0000-0000-000000000001", OwnerReferences: []metav1.OwnerReference{ { Controller: ptr.To(true), UID: "00000000-0000-0000-0000-000000000001", }, }, }, Spec: appsv1.ReplicaSetSpec{ Replicas: ptr.To[int32](0), Selector: &metav1.LabelSelector{ MatchLabels: labels, }, Template: corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", Labels: labels, }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Image: "mytest-image:v1.0", VolumeMounts: []corev1.VolumeMount{ { Name: "vol-foo", MountPath: "/tmp/vol-foo", }, { Name: "vol-bar", MountPath: "/tmp/vol-bar", }, }, }, }, Volumes: []corev1.Volume{ { Name: "vol-foo", VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{}}, }, { Name: "vol-bar", VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{}}, }, }, }, }, }, Status: appsv1.ReplicaSetStatus{ Replicas: 0, ReadyReplicas: 0, AvailableReplicas: 0, }, }, &appsv1.ReplicaSet{ ObjectMeta: metav1.ObjectMeta{ Name: "bar-002", Namespace: "foo", Labels: labels, UID: "00000000-0000-0000-0000-000000000002", OwnerReferences: []metav1.OwnerReference{ { Controller: ptr.To(true), UID: "00000000-0000-0000-0000-000000000001", }, }, }, Spec: appsv1.ReplicaSetSpec{ Replicas: ptr.To[int32](2), Selector: &metav1.LabelSelector{ MatchLabels: labels, }, Template: corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", Labels: labels, }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Image: "mytest-image:v2.0", VolumeMounts: []corev1.VolumeMount{ { Name: "vol-foo", MountPath: "/tmp/vol-foo", }, { Name: "vol-bar", MountPath: "/tmp/vol-bar", }, }, }, }, Volumes: []corev1.Volume{ { Name: "vol-foo", VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{}}, }, { Name: "vol-bar", VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{}}, }, }, }, }, }, Status: appsv1.ReplicaSetStatus{ Replicas: 2, ReadyReplicas: 2, AvailableReplicas: 2, }, }, &corev1.Event{ ObjectMeta: metav1.ObjectMeta{ Name: "bar-000", Namespace: "foo", }, InvolvedObject: corev1.ObjectReference{ APIVersion: "apps/v1", Kind: "Deployment", Name: "bar", Namespace: "foo", UID: "00000000-0000-0000-0000-000000000001", }, Type: corev1.EventTypeNormal, Reason: "ScalingReplicaSet", Message: "Scaled up replica set bar-002 from 0 to 1", ReportingController: "deployment-controller", EventTime: metav1.NewMicroTime(time.Now().Add(-20 * time.Minute)), Series: &corev1.EventSeries{ Count: 3, LastObservedTime: metav1.NewMicroTime(time.Now().Add(-12 * time.Minute)), }, }, &corev1.Event{ ObjectMeta: metav1.ObjectMeta{ Name: "bar-001", Namespace: "foo", }, InvolvedObject: corev1.ObjectReference{ APIVersion: "apps/v1", Kind: "Deployment", Name: "bar", Namespace: "foo", UID: "00000000-0000-0000-0000-000000000001", }, Type: corev1.EventTypeNormal, Reason: "ScalingReplicaSet", Message: "Scaled up replica set bar-001 from 0 to 2", Source: corev1.EventSource{ Component: "deployment-controller", }, FirstTimestamp: metav1.NewTime(time.Now().Add(-10 * time.Minute)), }, &corev1.Event{ ObjectMeta: metav1.ObjectMeta{ Name: "bar-002", Namespace: "foo", }, InvolvedObject: corev1.ObjectReference{ APIVersion: "apps/v1", Kind: "Deployment", Name: "bar", Namespace: "foo", UID: "00000000-0000-0000-0000-000000000001", }, Type: corev1.EventTypeNormal, Reason: "ScalingReplicaSet", Message: "Scaled up replica set bar-002 from 0 to 1", Source: corev1.EventSource{ Component: "deployment-controller", }, FirstTimestamp: metav1.NewTime(time.Now().Add(-2 * time.Minute)), }, &corev1.Event{ ObjectMeta: metav1.ObjectMeta{ Name: "bar-003", Namespace: "foo", }, InvolvedObject: corev1.ObjectReference{ APIVersion: "apps/v1", Kind: "Deployment", Name: "bar", Namespace: "foo", UID: "00000000-0000-0000-0000-000000000001", }, Type: corev1.EventTypeNormal, Reason: "ScalingReplicaSet", Message: "Scaled down replica set bar-002 from 2 to 1", ReportingController: "deployment-controller", EventTime: metav1.NewMicroTime(time.Now().Add(-1 * time.Minute)), }, &corev1.Event{ ObjectMeta: metav1.ObjectMeta{ Name: "bar-004", Namespace: "foo", }, InvolvedObject: corev1.ObjectReference{ APIVersion: "apps/v1", Kind: "Deployment", Name: "bar", Namespace: "foo", UID: "00000000-0000-0000-0000-000000000001", }, Type: corev1.EventTypeNormal, Reason: "ScalingReplicaSet", Message: "Scaled up replica set bar-002 from 0 to 2", ReportingController: "deployment-controller", EventTime: metav1.NewMicroTime(time.Now().Add(-15 * time.Second)), }, &corev1.Event{ ObjectMeta: metav1.ObjectMeta{ Name: "bar-005", Namespace: "foo", }, InvolvedObject: corev1.ObjectReference{ APIVersion: "apps/v1", Kind: "Deployment", Name: "bar", Namespace: "foo", UID: "00000000-0000-0000-0000-000000000001", }, Type: corev1.EventTypeNormal, Reason: "ScalingReplicaSet", Message: "Scaled down replica set bar-001 from 2 to 0", ReportingController: "deployment-controller", EventTime: metav1.NewMicroTime(time.Now().Add(-3 * time.Second)), }, }, expects: []string{ "Replicas: 2 desired | 2 updated | 2 total | 2 available | 0 unavailable", "Image: mytest-image:v2.0", "OldReplicaSets: bar-001 (0/0 replicas created)", "NewReplicaSet: bar-002 (2/2 replicas created)", "Events:\n", "Normal ScalingReplicaSet 12m (x3 over 20m) deployment-controller Scaled up replica set bar-002 from 0 to 1", "Normal ScalingReplicaSet 10m deployment-controller Scaled up replica set bar-001 from 0 to 2", "Normal ScalingReplicaSet 2m deployment-controller Scaled up replica set bar-002 from 0 to 1", "Normal ScalingReplicaSet 60s deployment-controller Scaled down replica set bar-002 from 2 to 1", "Normal ScalingReplicaSet 15s deployment-controller Scaled up replica set bar-002 from 0 to 2", "Normal ScalingReplicaSet 3s deployment-controller Scaled down replica set bar-001 from 2 to 0", }, }, } for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { fakeClient := fake.NewSimpleClientset(testCase.objects...) d := DeploymentDescriber{fakeClient} 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 \"%s\" in:\n %s", expect, out) } } }) } } func TestDescribeJob(t *testing.T) { indexedCompletion := batchv1.IndexedCompletion cases := map[string]struct { job *batchv1.Job wantElements []string dontWantElements []string }{ "empty job": { job: &batchv1.Job{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", }, Spec: batchv1.JobSpec{}, }, dontWantElements: []string{"Completed Indexes:", "Suspend:", "Backoff Limit:", "TTL Seconds After Finished:"}, }, "no completed indexes": { job: &batchv1.Job{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", }, Spec: batchv1.JobSpec{ CompletionMode: &indexedCompletion, }, }, wantElements: []string{"Completed Indexes: "}, }, "few completed indexes": { job: &batchv1.Job{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", }, Spec: batchv1.JobSpec{ CompletionMode: &indexedCompletion, }, Status: batchv1.JobStatus{ CompletedIndexes: "0-5,7,9,10,12,13,15,16,18,20,21,23,24,26,27,29,30,32", }, }, wantElements: []string{"Completed Indexes: 0-5,7,9,10,12,13,15,16,18,20,21,23,24,26,27,29,30,32"}, }, "too many completed indexes": { job: &batchv1.Job{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", }, Spec: batchv1.JobSpec{ CompletionMode: &indexedCompletion, }, Status: batchv1.JobStatus{ CompletedIndexes: "0-5,7,9,10,12,13,15,16,18,20,21,23,24,26,27,29,30,32-34,36,37", }, }, wantElements: []string{"Completed Indexes: 0-5,7,9,10,12,13,15,16,18,20,21,23,24,26,27,29,30,32-34,..."}, }, "suspend set to true": { job: &batchv1.Job{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", }, Spec: batchv1.JobSpec{ Suspend: ptr.To(true), TTLSecondsAfterFinished: ptr.To[int32](123), BackoffLimit: ptr.To[int32](1), }, }, wantElements: []string{ "Suspend: true", "TTL Seconds After Finished: 123", "Backoff Limit: 1", }, }, "suspend set to false": { job: &batchv1.Job{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", }, Spec: batchv1.JobSpec{ Suspend: ptr.To(false), }, }, wantElements: []string{"Suspend: false"}, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { client := &describeClient{ T: t, Namespace: tc.job.Namespace, Interface: fake.NewSimpleClientset(tc.job), } describer := JobDescriber{Interface: client} out, err := describer.Describe(tc.job.Namespace, tc.job.Name, DescriberSettings{ShowEvents: true}) if err != nil { t.Fatalf("unexpected error describing object: %v", err) } for _, expected := range tc.wantElements { if !strings.Contains(out, expected) { t.Errorf("expected to find %q in output:\n %s", expected, out) } } for _, unexpected := range tc.dontWantElements { if strings.Contains(out, unexpected) { t.Errorf("unexpected to find %q in output:\n %s", unexpected, out) } } }) } } func TestDescribeIngress(t *testing.T) { ingresClassName := "test" backendV1beta1 := networkingv1beta1.IngressBackend{ ServiceName: "default-backend", ServicePort: intstr.FromInt32(80), } v1beta1 := fake.NewSimpleClientset(&networkingv1beta1.Ingress{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Labels: map[string]string{ "id1": "app1", "id2": "app2", }, Namespace: "foo", }, Spec: networkingv1beta1.IngressSpec{ IngressClassName: &ingresClassName, Rules: []networkingv1beta1.IngressRule{ { Host: "foo.bar.com", IngressRuleValue: networkingv1beta1.IngressRuleValue{ HTTP: &networkingv1beta1.HTTPIngressRuleValue{ Paths: []networkingv1beta1.HTTPIngressPath{ { Path: "/foo", Backend: backendV1beta1, }, }, }, }, }, }, }, }) backendV1 := networkingv1.IngressBackend{ Service: &networkingv1.IngressServiceBackend{ Name: "default-backend", Port: networkingv1.ServiceBackendPort{ Number: 80, }, }, } netv1 := fake.NewSimpleClientset(&networkingv1.Ingress{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", }, Spec: networkingv1.IngressSpec{ IngressClassName: &ingresClassName, Rules: []networkingv1.IngressRule{ { Host: "foo.bar.com", IngressRuleValue: networkingv1.IngressRuleValue{ HTTP: &networkingv1.HTTPIngressRuleValue{ Paths: []networkingv1.HTTPIngressPath{ { Path: "/foo", Backend: backendV1, }, }, }, }, }, }, }, }) backendResource := networkingv1.IngressBackend{ Resource: &corev1.TypedLocalObjectReference{ APIGroup: ptr.To("example.com"), Kind: "foo", Name: "bar", }, } backendResourceNoAPIGroup := networkingv1.IngressBackend{ Resource: &corev1.TypedLocalObjectReference{ Kind: "foo", Name: "bar", }, } tests := map[string]struct { input *fake.Clientset output string }{ "IngressRule.HTTP.Paths.Backend.Service v1beta1": { input: v1beta1, output: `Name: bar Labels: id1=app1 id2=app2 Namespace: foo Address: Ingress Class: test Default backend: Rules: Host Path Backends ---- ---- -------- foo.bar.com /foo default-backend:80 () Annotations: Events: ` + "\n", }, "IngressRule.HTTP.Paths.Backend.Service v1": { input: netv1, output: `Name: bar Labels: Namespace: foo Address: Ingress Class: test Default backend: Rules: Host Path Backends ---- ---- -------- foo.bar.com /foo default-backend:80 () Annotations: Events: ` + "\n", }, "IngressRule.HTTP.Paths.Backend.Resource v1": { input: fake.NewSimpleClientset(&networkingv1.Ingress{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", }, Spec: networkingv1.IngressSpec{ IngressClassName: &ingresClassName, Rules: []networkingv1.IngressRule{ { Host: "foo.bar.com", IngressRuleValue: networkingv1.IngressRuleValue{ HTTP: &networkingv1.HTTPIngressRuleValue{ Paths: []networkingv1.HTTPIngressPath{ { Path: "/foo", Backend: backendResource, }, }, }, }, }, }, }, }), output: `Name: bar Labels: Namespace: foo Address: Ingress Class: test Default backend: Rules: Host Path Backends ---- ---- -------- foo.bar.com /foo APIGroup: example.com, Kind: foo, Name: bar Annotations: Events: ` + "\n", }, "IngressRule.HTTP.Paths.Backend.Resource v1 Without APIGroup": { input: fake.NewSimpleClientset(&networkingv1.Ingress{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", }, Spec: networkingv1.IngressSpec{ IngressClassName: &ingresClassName, Rules: []networkingv1.IngressRule{ { Host: "foo.bar.com", IngressRuleValue: networkingv1.IngressRuleValue{ HTTP: &networkingv1.HTTPIngressRuleValue{ Paths: []networkingv1.HTTPIngressPath{ { Path: "/foo", Backend: backendResourceNoAPIGroup, }, }, }, }, }, }, }, }), output: `Name: bar Labels: Namespace: foo Address: Ingress Class: test Default backend: Rules: Host Path Backends ---- ---- -------- foo.bar.com /foo APIGroup: , Kind: foo, Name: bar Annotations: Events: ` + "\n", }, "Spec.DefaultBackend.Service & IngressRule.HTTP.Paths.Backend.Service v1": { input: fake.NewSimpleClientset(&networkingv1.Ingress{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", }, Spec: networkingv1.IngressSpec{ DefaultBackend: &backendV1, IngressClassName: &ingresClassName, Rules: []networkingv1.IngressRule{ { Host: "foo.bar.com", IngressRuleValue: networkingv1.IngressRuleValue{ HTTP: &networkingv1.HTTPIngressRuleValue{ Paths: []networkingv1.HTTPIngressPath{ { Path: "/foo", Backend: backendV1, }, }, }, }, }, }, }, }), output: `Name: bar Labels: Namespace: foo Address: Ingress Class: test Default backend: default-backend:80 () Rules: Host Path Backends ---- ---- -------- foo.bar.com /foo default-backend:80 () Annotations: Events: ` + "\n", }, "Spec.DefaultBackend.Resource & IngressRule.HTTP.Paths.Backend.Resource v1": { input: fake.NewSimpleClientset(&networkingv1.Ingress{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", }, Spec: networkingv1.IngressSpec{ DefaultBackend: &backendResource, IngressClassName: &ingresClassName, Rules: []networkingv1.IngressRule{ { Host: "foo.bar.com", IngressRuleValue: networkingv1.IngressRuleValue{ HTTP: &networkingv1.HTTPIngressRuleValue{ Paths: []networkingv1.HTTPIngressPath{ { Path: "/foo", Backend: backendResource, }, }, }, }, }, }, }, }), output: `Name: bar Labels: Namespace: foo Address: Ingress Class: test Default backend: APIGroup: example.com, Kind: foo, Name: bar Rules: Host Path Backends ---- ---- -------- foo.bar.com /foo APIGroup: example.com, Kind: foo, Name: bar Annotations: Events: ` + "\n", }, "Spec.DefaultBackend.Resource & IngressRule.HTTP.Paths.Backend.Service v1": { input: fake.NewSimpleClientset(&networkingv1.Ingress{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", }, Spec: networkingv1.IngressSpec{ DefaultBackend: &backendResource, IngressClassName: &ingresClassName, Rules: []networkingv1.IngressRule{ { Host: "foo.bar.com", IngressRuleValue: networkingv1.IngressRuleValue{ HTTP: &networkingv1.HTTPIngressRuleValue{ Paths: []networkingv1.HTTPIngressPath{ { Path: "/foo", Backend: backendV1, }, }, }, }, }, }, }, }), output: `Name: bar Labels: Namespace: foo Address: Ingress Class: test Default backend: APIGroup: example.com, Kind: foo, Name: bar Rules: Host Path Backends ---- ---- -------- foo.bar.com /foo default-backend:80 () Annotations: Events: ` + "\n", }, "DefaultBackend": { input: fake.NewSimpleClientset(&networkingv1.Ingress{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", }, Spec: networkingv1.IngressSpec{ DefaultBackend: &backendV1, IngressClassName: &ingresClassName, }, }), output: `Name: bar Labels: Namespace: foo Address: Ingress Class: test Default backend: default-backend:80 () Rules: Host Path Backends ---- ---- -------- * * default-backend:80 () Annotations: Events: `, }, "EmptyBackend": { input: fake.NewSimpleClientset(&networkingv1.Ingress{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", }, Spec: networkingv1.IngressSpec{ IngressClassName: &ingresClassName, }, }), output: `Name: bar Labels: Namespace: foo Address: Ingress Class: test Default backend: Rules: Host Path Backends ---- ---- -------- * * Annotations: Events: `, }, "EmptyIngressClassName": { input: fake.NewSimpleClientset(&networkingv1.Ingress{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", }, Spec: networkingv1.IngressSpec{ DefaultBackend: &backendV1, }, }), output: `Name: bar Labels: Namespace: foo Address: Ingress Class: Default backend: default-backend:80 () Rules: Host Path Backends ---- ---- -------- * * default-backend:80 () Annotations: Events: `, }, } for name, test := range tests { t.Run(name, func(t *testing.T) { c := &describeClient{T: t, Namespace: "foo", Interface: test.input} i := IngressDescriber{c} out, err := i.Describe("foo", "bar", DescriberSettings{ShowEvents: true}) if err != nil { t.Errorf("unexpected error: %v", err) } if out != test.output { t.Log(out) t.Log(test.output) t.Errorf("expected: \n%q\n but got output: \n%q\n", test.output, out) } }) } } func TestDescribeIngressV1(t *testing.T) { ingresClassName := "test" defaultBackend := networkingv1.IngressBackend{ Service: &networkingv1.IngressServiceBackend{ Name: "default-backend", Port: networkingv1.ServiceBackendPort{ Number: 80, }, }, } fakeClient := fake.NewSimpleClientset(&networkingv1.Ingress{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Labels: map[string]string{ "id1": "app1", "id2": "app2", }, Namespace: "foo", }, Spec: networkingv1.IngressSpec{ IngressClassName: &ingresClassName, Rules: []networkingv1.IngressRule{ { Host: "foo.bar.com", IngressRuleValue: networkingv1.IngressRuleValue{ HTTP: &networkingv1.HTTPIngressRuleValue{ Paths: []networkingv1.HTTPIngressPath{ { Path: "/foo", Backend: defaultBackend, }, }, }, }, }, }, }, }) i := IngressDescriber{fakeClient} out, err := i.Describe("foo", "bar", DescriberSettings{ShowEvents: true}) if err != nil { t.Errorf("unexpected error: %v", err) } if !strings.Contains(out, "bar") || !strings.Contains(out, "foo") || !strings.Contains(out, "foo.bar.com") || !strings.Contains(out, "/foo") || !strings.Contains(out, "app1") || !strings.Contains(out, "app2") { t.Errorf("unexpected out: %s", out) } } func TestDescribeStorageClass(t *testing.T) { reclaimPolicy := corev1.PersistentVolumeReclaimRetain bindingMode := storagev1.VolumeBindingMode("bindingmode") f := fake.NewSimpleClientset(&storagev1.StorageClass{ ObjectMeta: metav1.ObjectMeta{ Name: "foo", ResourceVersion: "4", Annotations: map[string]string{ "name": "foo", }, }, Provisioner: "my-provisioner", Parameters: map[string]string{ "param1": "value1", "param2": "value2", }, ReclaimPolicy: &reclaimPolicy, VolumeBindingMode: &bindingMode, AllowedTopologies: []corev1.TopologySelectorTerm{ { MatchLabelExpressions: []corev1.TopologySelectorLabelRequirement{ { Key: "failure-domain.beta.kubernetes.io/zone", Values: []string{"zone1"}, }, { Key: "kubernetes.io/hostname", Values: []string{"node1"}, }, }, }, { MatchLabelExpressions: []corev1.TopologySelectorLabelRequirement{ { Key: "failure-domain.beta.kubernetes.io/zone", Values: []string{"zone2"}, }, { Key: "kubernetes.io/hostname", Values: []string{"node2"}, }, }, }, }, }) s := StorageClassDescriber{f} out, err := s.Describe("", "foo", DescriberSettings{ShowEvents: true}) if err != nil { t.Errorf("unexpected error: %v", err) } if !strings.Contains(out, "foo") || !strings.Contains(out, "my-provisioner") || !strings.Contains(out, "param1") || !strings.Contains(out, "param2") || !strings.Contains(out, "value1") || !strings.Contains(out, "value2") || !strings.Contains(out, "Retain") || !strings.Contains(out, "bindingmode") || !strings.Contains(out, "failure-domain.beta.kubernetes.io/zone") || !strings.Contains(out, "zone1") || !strings.Contains(out, "kubernetes.io/hostname") || !strings.Contains(out, "node1") || !strings.Contains(out, "zone2") || !strings.Contains(out, "node2") { t.Errorf("unexpected out: %s", out) } } func TestDescribeVolumeAttributesClass(t *testing.T) { expectedOut := `Name: foo Annotations: name=bar DriverName: my-driver Parameters: param1=value1,param2=value2 Events: ` f := fake.NewSimpleClientset(&storagev1beta1.VolumeAttributesClass{ ObjectMeta: metav1.ObjectMeta{ Name: "foo", ResourceVersion: "4", Annotations: map[string]string{ "name": "bar", }, }, DriverName: "my-driver", Parameters: map[string]string{ "param1": "value1", "param2": "value2", }, }) s := VolumeAttributesClassDescriber{f} out, err := s.Describe("", "foo", DescriberSettings{ShowEvents: true}) if err != nil { t.Errorf("unexpected error: %v", err) } if out != expectedOut { t.Errorf("expected:\n %s\n but got output:\n %s diff:\n%s", expectedOut, out, cmp.Diff(out, expectedOut)) } } func TestDescribeCSINode(t *testing.T) { limit := ptr.To[int32](2) f := fake.NewSimpleClientset(&storagev1.CSINode{ ObjectMeta: metav1.ObjectMeta{Name: "foo"}, Spec: storagev1.CSINodeSpec{ Drivers: []storagev1.CSINodeDriver{ { Name: "driver1", NodeID: "node1", }, { Name: "driver2", NodeID: "node2", Allocatable: &storagev1.VolumeNodeResources{Count: limit}, }, }, }, }) s := CSINodeDescriber{f} out, err := s.Describe("", "foo", DescriberSettings{ShowEvents: true}) if err != nil { t.Errorf("unexpected error: %v", err) } if !strings.Contains(out, "foo") || !strings.Contains(out, "driver1") || !strings.Contains(out, "node1") || !strings.Contains(out, "driver2") || !strings.Contains(out, "node2") { t.Errorf("unexpected out: %s", out) } } func TestDescribePodDisruptionBudgetV1beta1(t *testing.T) { minAvailable := intstr.FromInt32(22) f := fake.NewSimpleClientset(&policyv1beta1.PodDisruptionBudget{ ObjectMeta: metav1.ObjectMeta{ Namespace: "ns1", Name: "pdb1", CreationTimestamp: metav1.Time{Time: time.Now().Add(1.9e9)}, }, Spec: policyv1beta1.PodDisruptionBudgetSpec{ MinAvailable: &minAvailable, }, Status: policyv1beta1.PodDisruptionBudgetStatus{ DisruptionsAllowed: 5, }, }) s := PodDisruptionBudgetDescriber{f} out, err := s.Describe("ns1", "pdb1", DescriberSettings{ShowEvents: true}) if err != nil { t.Errorf("unexpected error: %v", err) } if !strings.Contains(out, "pdb1") || !strings.Contains(out, "ns1") || !strings.Contains(out, "22") || !strings.Contains(out, "5") { t.Errorf("unexpected out: %s", out) } } func TestDescribePodDisruptionBudgetV1(t *testing.T) { minAvailable := intstr.FromInt32(22) f := fake.NewSimpleClientset(&policyv1.PodDisruptionBudget{ ObjectMeta: metav1.ObjectMeta{ Namespace: "ns1", Name: "pdb1", CreationTimestamp: metav1.Time{Time: time.Now().Add(1.9e9)}, }, Spec: policyv1.PodDisruptionBudgetSpec{ MinAvailable: &minAvailable, }, Status: policyv1.PodDisruptionBudgetStatus{ DisruptionsAllowed: 5, }, }) s := PodDisruptionBudgetDescriber{f} out, err := s.Describe("ns1", "pdb1", DescriberSettings{ShowEvents: true}) if err != nil { t.Errorf("unexpected error: %v", err) } if !strings.Contains(out, "pdb1") || !strings.Contains(out, "ns1") || !strings.Contains(out, "22") || !strings.Contains(out, "5") { t.Errorf("unexpected out: %s", out) } } func TestDescribeHorizontalPodAutoscaler(t *testing.T) { minReplicasVal := int32(2) targetUtilizationVal := int32(80) currentUtilizationVal := int32(50) maxSelectPolicy := autoscalingv2.MaxChangePolicySelect metricLabelSelector, err := metav1.ParseToLabelSelector("label=value") if err != nil { t.Errorf("unable to parse label selector: %v", err) } testsv2 := []struct { name string hpa autoscalingv2.HorizontalPodAutoscaler }{ { "minReplicas unset", autoscalingv2.HorizontalPodAutoscaler{ Spec: autoscalingv2.HorizontalPodAutoscalerSpec{ ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{ Name: "some-rc", Kind: "ReplicationController", }, MaxReplicas: 10, }, Status: autoscalingv2.HorizontalPodAutoscalerStatus{ CurrentReplicas: 4, DesiredReplicas: 5, }, }, }, { "external source type, target average value (no current)", autoscalingv2.HorizontalPodAutoscaler{ Spec: autoscalingv2.HorizontalPodAutoscalerSpec{ ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{ Name: "some-rc", Kind: "ReplicationController", }, MinReplicas: &minReplicasVal, MaxReplicas: 10, Metrics: []autoscalingv2.MetricSpec{ { Type: autoscalingv2.ExternalMetricSourceType, External: &autoscalingv2.ExternalMetricSource{ Metric: autoscalingv2.MetricIdentifier{ Name: "some-external-metric", Selector: metricLabelSelector, }, Target: autoscalingv2.MetricTarget{ Type: autoscalingv2.AverageValueMetricType, AverageValue: resource.NewMilliQuantity(100, resource.DecimalSI), }, }, }, }, }, Status: autoscalingv2.HorizontalPodAutoscalerStatus{ CurrentReplicas: 4, DesiredReplicas: 5, }, }, }, { "external source type, target average value (with current)", autoscalingv2.HorizontalPodAutoscaler{ Spec: autoscalingv2.HorizontalPodAutoscalerSpec{ ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{ Name: "some-rc", Kind: "ReplicationController", }, MinReplicas: &minReplicasVal, MaxReplicas: 10, Metrics: []autoscalingv2.MetricSpec{ { Type: autoscalingv2.ExternalMetricSourceType, External: &autoscalingv2.ExternalMetricSource{ Metric: autoscalingv2.MetricIdentifier{ Name: "some-external-metric", Selector: metricLabelSelector, }, Target: autoscalingv2.MetricTarget{ Type: autoscalingv2.AverageValueMetricType, AverageValue: resource.NewMilliQuantity(100, resource.DecimalSI), }, }, }, }, }, Status: autoscalingv2.HorizontalPodAutoscalerStatus{ CurrentReplicas: 4, DesiredReplicas: 5, CurrentMetrics: []autoscalingv2.MetricStatus{ { Type: autoscalingv2.ExternalMetricSourceType, External: &autoscalingv2.ExternalMetricStatus{ Metric: autoscalingv2.MetricIdentifier{ Name: "some-external-metric", Selector: metricLabelSelector, }, Current: autoscalingv2.MetricValueStatus{ AverageValue: resource.NewMilliQuantity(50, resource.DecimalSI), }, }, }, }, }, }, }, { "external source type, target value (no current)", autoscalingv2.HorizontalPodAutoscaler{ Spec: autoscalingv2.HorizontalPodAutoscalerSpec{ ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{ Name: "some-rc", Kind: "ReplicationController", }, MinReplicas: &minReplicasVal, MaxReplicas: 10, Metrics: []autoscalingv2.MetricSpec{ { Type: autoscalingv2.ExternalMetricSourceType, External: &autoscalingv2.ExternalMetricSource{ Metric: autoscalingv2.MetricIdentifier{ Name: "some-external-metric", Selector: metricLabelSelector, }, Target: autoscalingv2.MetricTarget{ Type: autoscalingv2.ValueMetricType, Value: resource.NewMilliQuantity(100, resource.DecimalSI), }, }, }, }, }, Status: autoscalingv2.HorizontalPodAutoscalerStatus{ CurrentReplicas: 4, DesiredReplicas: 5, }, }, }, { "external source type, target value (with current)", autoscalingv2.HorizontalPodAutoscaler{ Spec: autoscalingv2.HorizontalPodAutoscalerSpec{ ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{ Name: "some-rc", Kind: "ReplicationController", }, MinReplicas: &minReplicasVal, MaxReplicas: 10, Metrics: []autoscalingv2.MetricSpec{ { Type: autoscalingv2.ExternalMetricSourceType, External: &autoscalingv2.ExternalMetricSource{ Metric: autoscalingv2.MetricIdentifier{ Name: "some-external-metric", Selector: metricLabelSelector, }, Target: autoscalingv2.MetricTarget{ Type: autoscalingv2.ValueMetricType, Value: resource.NewMilliQuantity(100, resource.DecimalSI), }, }, }, }, }, Status: autoscalingv2.HorizontalPodAutoscalerStatus{ CurrentReplicas: 4, DesiredReplicas: 5, CurrentMetrics: []autoscalingv2.MetricStatus{ { Type: autoscalingv2.ExternalMetricSourceType, External: &autoscalingv2.ExternalMetricStatus{ Metric: autoscalingv2.MetricIdentifier{ Name: "some-external-metric", Selector: metricLabelSelector, }, Current: autoscalingv2.MetricValueStatus{ Value: resource.NewMilliQuantity(50, resource.DecimalSI), }, }, }, }, }, }, }, { "pods source type (no current)", autoscalingv2.HorizontalPodAutoscaler{ Spec: autoscalingv2.HorizontalPodAutoscalerSpec{ ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{ Name: "some-rc", Kind: "ReplicationController", }, MinReplicas: &minReplicasVal, MaxReplicas: 10, Metrics: []autoscalingv2.MetricSpec{ { Type: autoscalingv2.PodsMetricSourceType, Pods: &autoscalingv2.PodsMetricSource{ Metric: autoscalingv2.MetricIdentifier{ Name: "some-pods-metric", }, Target: autoscalingv2.MetricTarget{ Type: autoscalingv2.AverageValueMetricType, AverageValue: resource.NewMilliQuantity(100, resource.DecimalSI), }, }, }, }, }, Status: autoscalingv2.HorizontalPodAutoscalerStatus{ CurrentReplicas: 4, DesiredReplicas: 5, }, }, }, { "pods source type (with current)", autoscalingv2.HorizontalPodAutoscaler{ Spec: autoscalingv2.HorizontalPodAutoscalerSpec{ ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{ Name: "some-rc", Kind: "ReplicationController", }, MinReplicas: &minReplicasVal, MaxReplicas: 10, Metrics: []autoscalingv2.MetricSpec{ { Type: autoscalingv2.PodsMetricSourceType, Pods: &autoscalingv2.PodsMetricSource{ Metric: autoscalingv2.MetricIdentifier{ Name: "some-pods-metric", }, Target: autoscalingv2.MetricTarget{ Type: autoscalingv2.AverageValueMetricType, AverageValue: resource.NewMilliQuantity(100, resource.DecimalSI), }, }, }, }, }, Status: autoscalingv2.HorizontalPodAutoscalerStatus{ CurrentReplicas: 4, DesiredReplicas: 5, CurrentMetrics: []autoscalingv2.MetricStatus{ { Type: autoscalingv2.PodsMetricSourceType, Pods: &autoscalingv2.PodsMetricStatus{ Metric: autoscalingv2.MetricIdentifier{ Name: "some-pods-metric", }, Current: autoscalingv2.MetricValueStatus{ AverageValue: resource.NewMilliQuantity(50, resource.DecimalSI), }, }, }, }, }, }, }, { "object source type target average value (no current)", autoscalingv2.HorizontalPodAutoscaler{ Spec: autoscalingv2.HorizontalPodAutoscalerSpec{ ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{ Name: "some-rc", Kind: "ReplicationController", }, MinReplicas: &minReplicasVal, MaxReplicas: 10, Metrics: []autoscalingv2.MetricSpec{ { Type: autoscalingv2.ObjectMetricSourceType, Object: &autoscalingv2.ObjectMetricSource{ DescribedObject: autoscalingv2.CrossVersionObjectReference{ Name: "some-service", Kind: "Service", }, Metric: autoscalingv2.MetricIdentifier{ Name: "some-service-metric", }, Target: autoscalingv2.MetricTarget{ Type: autoscalingv2.AverageValueMetricType, AverageValue: resource.NewMilliQuantity(100, resource.DecimalSI), }, }, }, }, }, Status: autoscalingv2.HorizontalPodAutoscalerStatus{ CurrentReplicas: 4, DesiredReplicas: 5, }, }, }, { "object source type target average value (with current)", autoscalingv2.HorizontalPodAutoscaler{ Spec: autoscalingv2.HorizontalPodAutoscalerSpec{ ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{ Name: "some-rc", Kind: "ReplicationController", }, MinReplicas: &minReplicasVal, MaxReplicas: 10, Metrics: []autoscalingv2.MetricSpec{ { Type: autoscalingv2.ObjectMetricSourceType, Object: &autoscalingv2.ObjectMetricSource{ DescribedObject: autoscalingv2.CrossVersionObjectReference{ Name: "some-service", Kind: "Service", }, Metric: autoscalingv2.MetricIdentifier{ Name: "some-service-metric", }, Target: autoscalingv2.MetricTarget{ Type: autoscalingv2.AverageValueMetricType, AverageValue: resource.NewMilliQuantity(100, resource.DecimalSI), }, }, }, }, }, Status: autoscalingv2.HorizontalPodAutoscalerStatus{ CurrentReplicas: 4, DesiredReplicas: 5, CurrentMetrics: []autoscalingv2.MetricStatus{ { Type: autoscalingv2.ObjectMetricSourceType, Object: &autoscalingv2.ObjectMetricStatus{ DescribedObject: autoscalingv2.CrossVersionObjectReference{ Name: "some-service", Kind: "Service", }, Metric: autoscalingv2.MetricIdentifier{ Name: "some-service-metric", }, Current: autoscalingv2.MetricValueStatus{ AverageValue: resource.NewMilliQuantity(50, resource.DecimalSI), }, }, }, }, }, }, }, { "object source type target value (no current)", autoscalingv2.HorizontalPodAutoscaler{ Spec: autoscalingv2.HorizontalPodAutoscalerSpec{ ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{ Name: "some-rc", Kind: "ReplicationController", }, MinReplicas: &minReplicasVal, MaxReplicas: 10, Metrics: []autoscalingv2.MetricSpec{ { Type: autoscalingv2.ObjectMetricSourceType, Object: &autoscalingv2.ObjectMetricSource{ DescribedObject: autoscalingv2.CrossVersionObjectReference{ Name: "some-service", Kind: "Service", }, Metric: autoscalingv2.MetricIdentifier{ Name: "some-service-metric", }, Target: autoscalingv2.MetricTarget{ Type: autoscalingv2.ValueMetricType, Value: resource.NewMilliQuantity(100, resource.DecimalSI), }, }, }, }, }, Status: autoscalingv2.HorizontalPodAutoscalerStatus{ CurrentReplicas: 4, DesiredReplicas: 5, }, }, }, { "object source type target value (with current)", autoscalingv2.HorizontalPodAutoscaler{ Spec: autoscalingv2.HorizontalPodAutoscalerSpec{ ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{ Name: "some-rc", Kind: "ReplicationController", }, MinReplicas: &minReplicasVal, MaxReplicas: 10, Metrics: []autoscalingv2.MetricSpec{ { Type: autoscalingv2.ObjectMetricSourceType, Object: &autoscalingv2.ObjectMetricSource{ DescribedObject: autoscalingv2.CrossVersionObjectReference{ Name: "some-service", Kind: "Service", }, Metric: autoscalingv2.MetricIdentifier{ Name: "some-service-metric", }, Target: autoscalingv2.MetricTarget{ Type: autoscalingv2.ValueMetricType, Value: resource.NewMilliQuantity(100, resource.DecimalSI), }, }, }, }, }, Status: autoscalingv2.HorizontalPodAutoscalerStatus{ CurrentReplicas: 4, DesiredReplicas: 5, CurrentMetrics: []autoscalingv2.MetricStatus{ { Type: autoscalingv2.ObjectMetricSourceType, Object: &autoscalingv2.ObjectMetricStatus{ DescribedObject: autoscalingv2.CrossVersionObjectReference{ Name: "some-service", Kind: "Service", }, Metric: autoscalingv2.MetricIdentifier{ Name: "some-service-metric", }, Current: autoscalingv2.MetricValueStatus{ Value: resource.NewMilliQuantity(50, resource.DecimalSI), }, }, }, }, }, }, }, { "resource source type, target average value (no current)", autoscalingv2.HorizontalPodAutoscaler{ Spec: autoscalingv2.HorizontalPodAutoscalerSpec{ ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{ Name: "some-rc", Kind: "ReplicationController", }, MinReplicas: &minReplicasVal, MaxReplicas: 10, Metrics: []autoscalingv2.MetricSpec{ { Type: autoscalingv2.ResourceMetricSourceType, Resource: &autoscalingv2.ResourceMetricSource{ Name: corev1.ResourceCPU, Target: autoscalingv2.MetricTarget{ Type: autoscalingv2.AverageValueMetricType, AverageValue: resource.NewMilliQuantity(100, resource.DecimalSI), }, }, }, }, }, Status: autoscalingv2.HorizontalPodAutoscalerStatus{ CurrentReplicas: 4, DesiredReplicas: 5, }, }, }, { "resource source type, target average value (with current)", autoscalingv2.HorizontalPodAutoscaler{ Spec: autoscalingv2.HorizontalPodAutoscalerSpec{ ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{ Name: "some-rc", Kind: "ReplicationController", }, MinReplicas: &minReplicasVal, MaxReplicas: 10, Metrics: []autoscalingv2.MetricSpec{ { Type: autoscalingv2.ResourceMetricSourceType, Resource: &autoscalingv2.ResourceMetricSource{ Name: corev1.ResourceCPU, Target: autoscalingv2.MetricTarget{ Type: autoscalingv2.AverageValueMetricType, AverageValue: resource.NewMilliQuantity(100, resource.DecimalSI), }, }, }, }, }, Status: autoscalingv2.HorizontalPodAutoscalerStatus{ CurrentReplicas: 4, DesiredReplicas: 5, CurrentMetrics: []autoscalingv2.MetricStatus{ { Type: autoscalingv2.ResourceMetricSourceType, Resource: &autoscalingv2.ResourceMetricStatus{ Name: corev1.ResourceCPU, Current: autoscalingv2.MetricValueStatus{ AverageValue: resource.NewMilliQuantity(50, resource.DecimalSI), }, }, }, }, }, }, }, { "resource source type, target utilization (no current)", autoscalingv2.HorizontalPodAutoscaler{ Spec: autoscalingv2.HorizontalPodAutoscalerSpec{ ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{ Name: "some-rc", Kind: "ReplicationController", }, MinReplicas: &minReplicasVal, MaxReplicas: 10, Metrics: []autoscalingv2.MetricSpec{ { Type: autoscalingv2.ResourceMetricSourceType, Resource: &autoscalingv2.ResourceMetricSource{ Name: corev1.ResourceCPU, Target: autoscalingv2.MetricTarget{ Type: autoscalingv2.UtilizationMetricType, AverageUtilization: &targetUtilizationVal, }, }, }, }, }, Status: autoscalingv2.HorizontalPodAutoscalerStatus{ CurrentReplicas: 4, DesiredReplicas: 5, }, }, }, { "resource source type, target utilization (with current)", autoscalingv2.HorizontalPodAutoscaler{ Spec: autoscalingv2.HorizontalPodAutoscalerSpec{ ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{ Name: "some-rc", Kind: "ReplicationController", }, MinReplicas: &minReplicasVal, MaxReplicas: 10, Metrics: []autoscalingv2.MetricSpec{ { Type: autoscalingv2.ResourceMetricSourceType, Resource: &autoscalingv2.ResourceMetricSource{ Name: corev1.ResourceCPU, Target: autoscalingv2.MetricTarget{ Type: autoscalingv2.UtilizationMetricType, AverageUtilization: &targetUtilizationVal, }, }, }, }, }, Status: autoscalingv2.HorizontalPodAutoscalerStatus{ CurrentReplicas: 4, DesiredReplicas: 5, CurrentMetrics: []autoscalingv2.MetricStatus{ { Type: autoscalingv2.ResourceMetricSourceType, Resource: &autoscalingv2.ResourceMetricStatus{ Name: corev1.ResourceCPU, Current: autoscalingv2.MetricValueStatus{ AverageUtilization: ¤tUtilizationVal, AverageValue: resource.NewMilliQuantity(40, resource.DecimalSI), }, }, }, }, }, }, }, { "container resource source type, target average value (no current)", autoscalingv2.HorizontalPodAutoscaler{ Spec: autoscalingv2.HorizontalPodAutoscalerSpec{ ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{ Name: "some-rc", Kind: "ReplicationController", }, MinReplicas: &minReplicasVal, MaxReplicas: 10, Metrics: []autoscalingv2.MetricSpec{ { Type: autoscalingv2.ContainerResourceMetricSourceType, ContainerResource: &autoscalingv2.ContainerResourceMetricSource{ Name: corev1.ResourceCPU, Container: "application", Target: autoscalingv2.MetricTarget{ Type: autoscalingv2.AverageValueMetricType, AverageValue: resource.NewMilliQuantity(100, resource.DecimalSI), }, }, }, }, }, Status: autoscalingv2.HorizontalPodAutoscalerStatus{ CurrentReplicas: 4, DesiredReplicas: 5, }, }, }, { "container resource source type, target average value (with current)", autoscalingv2.HorizontalPodAutoscaler{ Spec: autoscalingv2.HorizontalPodAutoscalerSpec{ ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{ Name: "some-rc", Kind: "ReplicationController", }, MinReplicas: &minReplicasVal, MaxReplicas: 10, Metrics: []autoscalingv2.MetricSpec{ { Type: autoscalingv2.ContainerResourceMetricSourceType, ContainerResource: &autoscalingv2.ContainerResourceMetricSource{ Name: corev1.ResourceCPU, Container: "application", Target: autoscalingv2.MetricTarget{ Type: autoscalingv2.AverageValueMetricType, AverageValue: resource.NewMilliQuantity(100, resource.DecimalSI), }, }, }, }, }, Status: autoscalingv2.HorizontalPodAutoscalerStatus{ CurrentReplicas: 4, DesiredReplicas: 5, CurrentMetrics: []autoscalingv2.MetricStatus{ { Type: autoscalingv2.ContainerResourceMetricSourceType, ContainerResource: &autoscalingv2.ContainerResourceMetricStatus{ Name: corev1.ResourceCPU, Container: "application", Current: autoscalingv2.MetricValueStatus{ AverageValue: resource.NewMilliQuantity(50, resource.DecimalSI), }, }, }, }, }, }, }, { "container resource source type, target utilization (no current)", autoscalingv2.HorizontalPodAutoscaler{ Spec: autoscalingv2.HorizontalPodAutoscalerSpec{ ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{ Name: "some-rc", Kind: "ReplicationController", }, MinReplicas: &minReplicasVal, MaxReplicas: 10, Metrics: []autoscalingv2.MetricSpec{ { Type: autoscalingv2.ContainerResourceMetricSourceType, ContainerResource: &autoscalingv2.ContainerResourceMetricSource{ Name: corev1.ResourceCPU, Container: "application", Target: autoscalingv2.MetricTarget{ Type: autoscalingv2.UtilizationMetricType, AverageUtilization: &targetUtilizationVal, }, }, }, }, }, Status: autoscalingv2.HorizontalPodAutoscalerStatus{ CurrentReplicas: 4, DesiredReplicas: 5, }, }, }, { "container resource source type, target utilization (with current)", autoscalingv2.HorizontalPodAutoscaler{ Spec: autoscalingv2.HorizontalPodAutoscalerSpec{ ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{ Name: "some-rc", Kind: "ReplicationController", }, MinReplicas: &minReplicasVal, MaxReplicas: 10, Metrics: []autoscalingv2.MetricSpec{ { Type: autoscalingv2.ContainerResourceMetricSourceType, ContainerResource: &autoscalingv2.ContainerResourceMetricSource{ Name: corev1.ResourceCPU, Container: "application", Target: autoscalingv2.MetricTarget{ Type: autoscalingv2.UtilizationMetricType, AverageUtilization: &targetUtilizationVal, }, }, }, }, }, Status: autoscalingv2.HorizontalPodAutoscalerStatus{ CurrentReplicas: 4, DesiredReplicas: 5, CurrentMetrics: []autoscalingv2.MetricStatus{ { Type: autoscalingv2.ContainerResourceMetricSourceType, ContainerResource: &autoscalingv2.ContainerResourceMetricStatus{ Name: corev1.ResourceCPU, Container: "application", Current: autoscalingv2.MetricValueStatus{ AverageUtilization: ¤tUtilizationVal, AverageValue: resource.NewMilliQuantity(40, resource.DecimalSI), }, }, }, }, }, }, }, { "multiple metrics", autoscalingv2.HorizontalPodAutoscaler{ Spec: autoscalingv2.HorizontalPodAutoscalerSpec{ ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{ Name: "some-rc", Kind: "ReplicationController", }, MinReplicas: &minReplicasVal, MaxReplicas: 10, Metrics: []autoscalingv2.MetricSpec{ { Type: autoscalingv2.PodsMetricSourceType, Pods: &autoscalingv2.PodsMetricSource{ Metric: autoscalingv2.MetricIdentifier{ Name: "some-pods-metric", }, Target: autoscalingv2.MetricTarget{ Type: autoscalingv2.AverageValueMetricType, AverageValue: resource.NewMilliQuantity(100, resource.DecimalSI), }, }, }, { Type: autoscalingv2.ResourceMetricSourceType, Resource: &autoscalingv2.ResourceMetricSource{ Name: corev1.ResourceCPU, Target: autoscalingv2.MetricTarget{ Type: autoscalingv2.UtilizationMetricType, AverageUtilization: &targetUtilizationVal, }, }, }, { Type: autoscalingv2.PodsMetricSourceType, Pods: &autoscalingv2.PodsMetricSource{ Metric: autoscalingv2.MetricIdentifier{ Name: "other-pods-metric", }, Target: autoscalingv2.MetricTarget{ Type: autoscalingv2.AverageValueMetricType, AverageValue: resource.NewMilliQuantity(400, resource.DecimalSI), }, }, }, }, }, Status: autoscalingv2.HorizontalPodAutoscalerStatus{ CurrentReplicas: 4, DesiredReplicas: 5, CurrentMetrics: []autoscalingv2.MetricStatus{ { Type: autoscalingv2.PodsMetricSourceType, Pods: &autoscalingv2.PodsMetricStatus{ Metric: autoscalingv2.MetricIdentifier{ Name: "some-pods-metric", }, Current: autoscalingv2.MetricValueStatus{ AverageValue: resource.NewMilliQuantity(50, resource.DecimalSI), }, }, }, { Type: autoscalingv2.ResourceMetricSourceType, Resource: &autoscalingv2.ResourceMetricStatus{ Name: corev1.ResourceCPU, Current: autoscalingv2.MetricValueStatus{ AverageUtilization: ¤tUtilizationVal, AverageValue: resource.NewMilliQuantity(40, resource.DecimalSI), }, }, }, }, }, }, }, { "scale up behavior specified", autoscalingv2.HorizontalPodAutoscaler{ Spec: autoscalingv2.HorizontalPodAutoscalerSpec{ ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{ Name: "behavior-target", Kind: "Deployment", }, MinReplicas: &minReplicasVal, MaxReplicas: 10, Metrics: []autoscalingv2.MetricSpec{ { Type: autoscalingv2.ResourceMetricSourceType, Resource: &autoscalingv2.ResourceMetricSource{ Name: corev1.ResourceCPU, Target: autoscalingv2.MetricTarget{ Type: autoscalingv2.UtilizationMetricType, AverageUtilization: &targetUtilizationVal, }, }, }, }, Behavior: &autoscalingv2.HorizontalPodAutoscalerBehavior{ ScaleUp: &autoscalingv2.HPAScalingRules{ StabilizationWindowSeconds: ptr.To[int32](30), SelectPolicy: &maxSelectPolicy, Policies: []autoscalingv2.HPAScalingPolicy{ {Type: autoscalingv2.PodsScalingPolicy, Value: 10, PeriodSeconds: 10}, {Type: autoscalingv2.PercentScalingPolicy, Value: 10, PeriodSeconds: 10}, }, }, }, }, }, }, { "scale down behavior specified", autoscalingv2.HorizontalPodAutoscaler{ Spec: autoscalingv2.HorizontalPodAutoscalerSpec{ ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{ Name: "behavior-target", Kind: "Deployment", }, MinReplicas: &minReplicasVal, MaxReplicas: 10, Metrics: []autoscalingv2.MetricSpec{ { Type: autoscalingv2.ResourceMetricSourceType, Resource: &autoscalingv2.ResourceMetricSource{ Name: corev1.ResourceCPU, Target: autoscalingv2.MetricTarget{ Type: autoscalingv2.UtilizationMetricType, AverageUtilization: &targetUtilizationVal, }, }, }, }, Behavior: &autoscalingv2.HorizontalPodAutoscalerBehavior{ ScaleDown: &autoscalingv2.HPAScalingRules{ StabilizationWindowSeconds: ptr.To[int32](30), Policies: []autoscalingv2.HPAScalingPolicy{ {Type: autoscalingv2.PodsScalingPolicy, Value: 10, PeriodSeconds: 10}, {Type: autoscalingv2.PercentScalingPolicy, Value: 10, PeriodSeconds: 10}, }, }, }, }, }, }, } for _, test := range testsv2 { t.Run(test.name, func(t *testing.T) { test.hpa.ObjectMeta = metav1.ObjectMeta{ Name: "bar", Namespace: "foo", } fake := fake.NewSimpleClientset(&test.hpa) desc := HorizontalPodAutoscalerDescriber{fake} str, err := desc.Describe("foo", "bar", DescriberSettings{ShowEvents: true}) if err != nil { t.Errorf("Unexpected error for test %s: %v", test.name, err) } if str == "" { t.Errorf("Unexpected empty string for test %s. Expected HPA Describer output", test.name) } t.Logf("Description for %q:\n%s", test.name, str) }) } testsV1 := []struct { name string hpa autoscalingv1.HorizontalPodAutoscaler }{ { "minReplicas unset", autoscalingv1.HorizontalPodAutoscaler{ Spec: autoscalingv1.HorizontalPodAutoscalerSpec{ ScaleTargetRef: autoscalingv1.CrossVersionObjectReference{ Name: "some-rc", Kind: "ReplicationController", }, MaxReplicas: 10, }, Status: autoscalingv1.HorizontalPodAutoscalerStatus{ CurrentReplicas: 4, DesiredReplicas: 5, }, }, }, { "minReplicas set", autoscalingv1.HorizontalPodAutoscaler{ Spec: autoscalingv1.HorizontalPodAutoscalerSpec{ ScaleTargetRef: autoscalingv1.CrossVersionObjectReference{ Name: "some-rc", Kind: "ReplicationController", }, MinReplicas: &minReplicasVal, MaxReplicas: 10, }, Status: autoscalingv1.HorizontalPodAutoscalerStatus{ CurrentReplicas: 4, DesiredReplicas: 5, }, }, }, { "with target no current", autoscalingv1.HorizontalPodAutoscaler{ Spec: autoscalingv1.HorizontalPodAutoscalerSpec{ ScaleTargetRef: autoscalingv1.CrossVersionObjectReference{ Name: "some-rc", Kind: "ReplicationController", }, MinReplicas: &minReplicasVal, MaxReplicas: 10, TargetCPUUtilizationPercentage: &targetUtilizationVal, }, Status: autoscalingv1.HorizontalPodAutoscalerStatus{ CurrentReplicas: 4, DesiredReplicas: 5, }, }, }, { "with target and current", autoscalingv1.HorizontalPodAutoscaler{ Spec: autoscalingv1.HorizontalPodAutoscalerSpec{ ScaleTargetRef: autoscalingv1.CrossVersionObjectReference{ Name: "some-rc", Kind: "ReplicationController", }, MinReplicas: &minReplicasVal, MaxReplicas: 10, TargetCPUUtilizationPercentage: &targetUtilizationVal, }, Status: autoscalingv1.HorizontalPodAutoscalerStatus{ CurrentReplicas: 4, DesiredReplicas: 5, CurrentCPUUtilizationPercentage: ¤tUtilizationVal, }, }, }, } for _, test := range testsV1 { t.Run(test.name, func(t *testing.T) { test.hpa.ObjectMeta = metav1.ObjectMeta{ Name: "bar", Namespace: "foo", } fake := fake.NewSimpleClientset(&test.hpa) desc := HorizontalPodAutoscalerDescriber{fake} str, err := desc.Describe("foo", "bar", DescriberSettings{ShowEvents: true}) if err != nil { t.Errorf("Unexpected error for test %s: %v", test.name, err) } if str == "" { t.Errorf("Unexpected empty string for test %s. Expected HPA Describer output", test.name) } t.Logf("Description for %q:\n%s", test.name, str) }) } } func TestDescribeEvents(t *testing.T) { events := &corev1.EventList{ Items: []corev1.Event{ { ObjectMeta: metav1.ObjectMeta{ Name: "event-1", Namespace: "foo", }, Source: corev1.EventSource{Component: "kubelet"}, Message: "Item 1", FirstTimestamp: metav1.NewTime(time.Date(2014, time.January, 15, 0, 0, 0, 0, time.UTC)), LastTimestamp: metav1.NewTime(time.Date(2014, time.January, 15, 0, 0, 0, 0, time.UTC)), Count: 1, Type: corev1.EventTypeNormal, }, { ObjectMeta: metav1.ObjectMeta{ Name: "event-2", Namespace: "foo", }, Source: corev1.EventSource{Component: "kubelet"}, Message: "Item 1", EventTime: metav1.NewMicroTime(time.Date(2014, time.January, 15, 0, 0, 0, 0, time.UTC)), Series: &corev1.EventSeries{ Count: 1, LastObservedTime: metav1.NewMicroTime(time.Date(2014, time.January, 15, 0, 0, 0, 0, time.UTC)), }, Type: corev1.EventTypeNormal, }, }, } m := map[string]ResourceDescriber{ "DaemonSetDescriber": &DaemonSetDescriber{ fake.NewSimpleClientset(&appsv1.DaemonSet{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", }, }, events), }, "DeploymentDescriber": &DeploymentDescriber{ fake.NewSimpleClientset(&appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", }, Spec: appsv1.DeploymentSpec{ Replicas: ptr.To[int32](1), Selector: &metav1.LabelSelector{}, }, }, events), }, "EndpointsDescriber": &EndpointsDescriber{ fake.NewSimpleClientset(&corev1.Endpoints{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", }, }, events), }, "EndpointSliceDescriber": &EndpointSliceDescriber{ fake.NewSimpleClientset(&discoveryv1beta1.EndpointSlice{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", }, }, events), }, "JobDescriber": &JobDescriber{ fake.NewSimpleClientset(&batchv1.Job{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", }, }, events), }, "IngressDescriber": &IngressDescriber{ fake.NewSimpleClientset(&networkingv1beta1.Ingress{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", }, }, events), }, "NodeDescriber": &NodeDescriber{ fake.NewSimpleClientset(&corev1.Node{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", }, }, events), }, "PersistentVolumeDescriber": &PersistentVolumeDescriber{ fake.NewSimpleClientset(&corev1.PersistentVolume{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", }, }, events), }, "PodDescriber": &PodDescriber{ fake.NewSimpleClientset(&corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", }, }, events), }, "ReplicaSetDescriber": &ReplicaSetDescriber{ fake.NewSimpleClientset(&appsv1.ReplicaSet{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", }, Spec: appsv1.ReplicaSetSpec{ Replicas: ptr.To[int32](1), }, }, events), }, "ReplicationControllerDescriber": &ReplicationControllerDescriber{ fake.NewSimpleClientset(&corev1.ReplicationController{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", }, Spec: corev1.ReplicationControllerSpec{ Replicas: ptr.To[int32](1), }, }, events), }, "Service": &ServiceDescriber{ fake.NewSimpleClientset(&corev1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", }, }, events), }, "StorageClass": &StorageClassDescriber{ fake.NewSimpleClientset(&storagev1.StorageClass{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", }, }, events), }, "HorizontalPodAutoscaler": &HorizontalPodAutoscalerDescriber{ fake.NewSimpleClientset(&autoscalingv2.HorizontalPodAutoscaler{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", }, }, events), }, "ConfigMap": &ConfigMapDescriber{ fake.NewSimpleClientset(&corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", }, }, events), }, } for name, d := range m { t.Run(name, func(t *testing.T) { out, err := d.Describe("foo", "bar", DescriberSettings{ShowEvents: true}) if err != nil { t.Errorf("unexpected error for %q: %v", name, err) } if !strings.Contains(out, "bar") { t.Errorf("unexpected out for %q: %s", name, out) } if !strings.Contains(out, "Events:") { t.Errorf("events not found for %q when ShowEvents=true: %s", name, out) } out, err = d.Describe("foo", "bar", DescriberSettings{ShowEvents: false}) if err != nil { t.Errorf("unexpected error for %q: %s", name, err) } if !strings.Contains(out, "bar") { t.Errorf("unexpected out for %q: %s", name, out) } if strings.Contains(out, "Events:") { t.Errorf("events found for %q when ShowEvents=false: %s", name, out) } }) } } func TestPrintLabelsMultiline(t *testing.T) { key := "MaxLenAnnotation" value := strings.Repeat("a", maxAnnotationLen-len(key)-2) testCases := []struct { annotations map[string]string expectPrint string }{ { annotations: map[string]string{"col1": "asd", "COL2": "zxc"}, expectPrint: "Annotations:\tCOL2: zxc\n\tcol1: asd\n", }, { annotations: map[string]string{"MaxLenAnnotation": value}, expectPrint: fmt.Sprintf("Annotations:\t%s: %s\n", key, value), }, { annotations: map[string]string{"MaxLenAnnotation": value + "1"}, expectPrint: fmt.Sprintf("Annotations:\t%s:\n\t %s\n", key, value+"1"), }, { annotations: map[string]string{"MaxLenAnnotation": value + value}, expectPrint: fmt.Sprintf("Annotations:\t%s:\n\t %s\n", key, strings.Repeat("a", maxAnnotationLen-2)+"..."), }, { annotations: map[string]string{"key": "value\nwith\nnewlines\n"}, expectPrint: "Annotations:\tkey:\n\t value\n\t with\n\t newlines\n", }, { annotations: map[string]string{}, expectPrint: "Annotations:\t\n", }, } for i, testCase := range testCases { t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { out := new(bytes.Buffer) writer := NewPrefixWriter(out) printAnnotationsMultiline(writer, "Annotations", testCase.annotations) output := out.String() if output != testCase.expectPrint { t.Errorf("Test case %d: expected to match:\n%q\nin output:\n%q", i, testCase.expectPrint, output) } }) } } func TestDescribeUnstructuredContent(t *testing.T) { testCases := []struct { expected string unexpected string }{ { expected: `API Version: v1 Dummy - Dummy: present dummy-dummy@dummy: present dummy/dummy: present dummy2: present Dummy Dummy: present Items: Item Bool: true Item Int: 42 Kind: Test Metadata: Creation Timestamp: 2017-04-01T00:00:00Z Name: MyName Namespace: MyNamespace Resource Version: 123 UID: 00000000-0000-0000-0000-000000000001 Status: ok URL: http://localhost `, }, { unexpected: "\nDummy 1:\tpresent\n", }, { unexpected: "Dummy 1", }, { unexpected: "Dummy 3", }, { unexpected: "Dummy3", }, { unexpected: "dummy3", }, { unexpected: "dummy 3", }, } out := new(bytes.Buffer) w := NewPrefixWriter(out) obj := &unstructured.Unstructured{ Object: map[string]interface{}{ "apiVersion": "v1", "kind": "Test", "dummyDummy": "present", "dummy/dummy": "present", "dummy-dummy@dummy": "present", "dummy-dummy": "present", "dummy1": "present", "dummy2": "present", "metadata": map[string]interface{}{ "name": "MyName", "namespace": "MyNamespace", "creationTimestamp": "2017-04-01T00:00:00Z", "resourceVersion": 123, "uid": "00000000-0000-0000-0000-000000000001", "dummy3": "present", }, "items": []interface{}{ map[string]interface{}{ "itemBool": true, "itemInt": 42, }, }, "url": "http://localhost", "status": "ok", }, } printUnstructuredContent(w, LEVEL_0, obj.UnstructuredContent(), "", ".dummy1", ".metadata.dummy3") output := out.String() for _, test := range testCases { if len(test.expected) > 0 { if !strings.Contains(output, test.expected) { t.Errorf("Expected to find %q in: %q", test.expected, output) } } if len(test.unexpected) > 0 { if strings.Contains(output, test.unexpected) { t.Errorf("Didn't expect to find %q in: %q", test.unexpected, output) } } } } func TestDescribeResourceQuota(t *testing.T) { fake := fake.NewSimpleClientset(&corev1.ResourceQuota{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", }, Status: corev1.ResourceQuotaStatus{ Hard: corev1.ResourceList{ corev1.ResourceName(corev1.ResourceCPU): resource.MustParse("1"), corev1.ResourceName(corev1.ResourceLimitsCPU): resource.MustParse("2"), corev1.ResourceName(corev1.ResourceLimitsMemory): resource.MustParse("2G"), corev1.ResourceName(corev1.ResourceMemory): resource.MustParse("1G"), corev1.ResourceName(corev1.ResourceRequestsCPU): resource.MustParse("1"), corev1.ResourceName(corev1.ResourceRequestsMemory): resource.MustParse("1G"), }, Used: corev1.ResourceList{ corev1.ResourceName(corev1.ResourceCPU): resource.MustParse("0"), corev1.ResourceName(corev1.ResourceLimitsCPU): resource.MustParse("0"), corev1.ResourceName(corev1.ResourceLimitsMemory): resource.MustParse("0G"), corev1.ResourceName(corev1.ResourceMemory): resource.MustParse("0G"), corev1.ResourceName(corev1.ResourceRequestsCPU): resource.MustParse("0"), corev1.ResourceName(corev1.ResourceRequestsMemory): resource.MustParse("1000Ki"), }, }, }) c := &describeClient{T: t, Namespace: "foo", Interface: fake} d := ResourceQuotaDescriber{c} out, err := d.Describe("foo", "bar", DescriberSettings{ShowEvents: true}) if err != nil { t.Errorf("unexpected error: %v", err) } expectedOut := []string{"bar", "foo", "limits.cpu", "2", "limits.memory", "2G", "requests.cpu", "1", "requests.memory", "1024k", "1G"} for _, expected := range expectedOut { if !strings.Contains(out, expected) { t.Errorf("expected to find %q in output: %q", expected, out) } } } func TestDescribeIngressClass(t *testing.T) { expectedOut := `Name: example-class Labels: Annotations: Controller: example.com/controller Parameters: APIGroup: v1 Kind: ConfigMap Name: example-parameters` + "\n" tests := map[string]struct { input *fake.Clientset output string }{ "basic IngressClass (v1beta1)": { input: fake.NewSimpleClientset(&networkingv1beta1.IngressClass{ ObjectMeta: metav1.ObjectMeta{ Name: "example-class", }, Spec: networkingv1beta1.IngressClassSpec{ Controller: "example.com/controller", Parameters: &networkingv1beta1.IngressClassParametersReference{ APIGroup: ptr.To("v1"), Kind: "ConfigMap", Name: "example-parameters", }, }, }), output: expectedOut, }, "basic IngressClass (v1)": { input: fake.NewSimpleClientset(&networkingv1.IngressClass{ ObjectMeta: metav1.ObjectMeta{ Name: "example-class", }, Spec: networkingv1.IngressClassSpec{ Controller: "example.com/controller", Parameters: &networkingv1.IngressClassParametersReference{ APIGroup: ptr.To("v1"), Kind: "ConfigMap", Name: "example-parameters", }, }, }), output: expectedOut, }, } for name, test := range tests { t.Run(name, func(t *testing.T) { c := &describeClient{T: t, Namespace: "foo", Interface: test.input} d := IngressClassDescriber{c} out, err := d.Describe("", "example-class", DescriberSettings{}) if err != nil { t.Errorf("unexpected error: %v", err) } if out != expectedOut { t.Log(out) t.Errorf("expected : %q\n but got output:\n %q", test.output, out) } }) } } func TestDescribeNetworkPolicies(t *testing.T) { expectedTime, err := time.Parse("2006-01-02 15:04:05 Z0700 MST", "2017-06-04 21:45:56 -0700 PDT") if err != nil { t.Errorf("unable to parse time %q error: %s", "2017-06-04 21:45:56 -0700 PDT", err) } expectedOut := `Name: network-policy-1 Namespace: default Created on: 2017-06-04 21:45:56 -0700 PDT Labels: Annotations: Spec: PodSelector: foo in (bar1,bar2),foo2 notin (bar1,bar2),id1=app1,id2=app2 Allowing ingress traffic: To Port: 80/TCP To Port: 82/TCP From: NamespaceSelector: id=ns1,id2=ns2 PodSelector: id=pod1,id2=pod2 From: PodSelector: id=app2,id2=app3 From: NamespaceSelector: id=app2,id2=app3 From: NamespaceSelector: foo in (bar1,bar2),id=app2,id2=app3 From: IPBlock: CIDR: 192.168.0.0/16 Except: 192.168.3.0/24, 192.168.4.0/24 ---------- To Port: (traffic allowed to all ports) From: (traffic not restricted by source) Allowing egress traffic: To Port: 80/TCP To Port: 82/TCP To: NamespaceSelector: id=ns1,id2=ns2 PodSelector: id=pod1,id2=pod2 To: PodSelector: id=app2,id2=app3 To: NamespaceSelector: id=app2,id2=app3 To: NamespaceSelector: foo in (bar1,bar2),id=app2,id2=app3 To: IPBlock: CIDR: 192.168.0.0/16 Except: 192.168.3.0/24, 192.168.4.0/24 ---------- To Port: (traffic allowed to all ports) To: (traffic not restricted by destination) Policy Types: Ingress, Egress ` port80 := intstr.FromInt32(80) port82 := intstr.FromInt32(82) protoTCP := corev1.ProtocolTCP versionedFake := fake.NewSimpleClientset(&networkingv1.NetworkPolicy{ ObjectMeta: metav1.ObjectMeta{ Name: "network-policy-1", Namespace: "default", CreationTimestamp: metav1.NewTime(expectedTime), }, Spec: networkingv1.NetworkPolicySpec{ PodSelector: metav1.LabelSelector{ MatchLabels: map[string]string{ "id1": "app1", "id2": "app2", }, MatchExpressions: []metav1.LabelSelectorRequirement{ {Key: "foo", Operator: "In", Values: []string{"bar1", "bar2"}}, {Key: "foo2", Operator: "NotIn", Values: []string{"bar1", "bar2"}}, }, }, Ingress: []networkingv1.NetworkPolicyIngressRule{ { Ports: []networkingv1.NetworkPolicyPort{ {Port: &port80}, {Port: &port82, Protocol: &protoTCP}, }, From: []networkingv1.NetworkPolicyPeer{ { PodSelector: &metav1.LabelSelector{ MatchLabels: map[string]string{ "id": "pod1", "id2": "pod2", }, }, NamespaceSelector: &metav1.LabelSelector{ MatchLabels: map[string]string{ "id": "ns1", "id2": "ns2", }, }, }, { PodSelector: &metav1.LabelSelector{ MatchLabels: map[string]string{ "id": "app2", "id2": "app3", }, }, }, { NamespaceSelector: &metav1.LabelSelector{ MatchLabels: map[string]string{ "id": "app2", "id2": "app3", }, }, }, { NamespaceSelector: &metav1.LabelSelector{ MatchLabels: map[string]string{ "id": "app2", "id2": "app3", }, MatchExpressions: []metav1.LabelSelectorRequirement{ {Key: "foo", Operator: "In", Values: []string{"bar1", "bar2"}}, }, }, }, { IPBlock: &networkingv1.IPBlock{ CIDR: "192.168.0.0/16", Except: []string{"192.168.3.0/24", "192.168.4.0/24"}, }, }, }, }, {}, }, Egress: []networkingv1.NetworkPolicyEgressRule{ { Ports: []networkingv1.NetworkPolicyPort{ {Port: &port80}, {Port: &port82, Protocol: &protoTCP}, }, To: []networkingv1.NetworkPolicyPeer{ { PodSelector: &metav1.LabelSelector{ MatchLabels: map[string]string{ "id": "pod1", "id2": "pod2", }, }, NamespaceSelector: &metav1.LabelSelector{ MatchLabels: map[string]string{ "id": "ns1", "id2": "ns2", }, }, }, { PodSelector: &metav1.LabelSelector{ MatchLabels: map[string]string{ "id": "app2", "id2": "app3", }, }, }, { NamespaceSelector: &metav1.LabelSelector{ MatchLabels: map[string]string{ "id": "app2", "id2": "app3", }, }, }, { NamespaceSelector: &metav1.LabelSelector{ MatchLabels: map[string]string{ "id": "app2", "id2": "app3", }, MatchExpressions: []metav1.LabelSelectorRequirement{ {Key: "foo", Operator: "In", Values: []string{"bar1", "bar2"}}, }, }, }, { IPBlock: &networkingv1.IPBlock{ CIDR: "192.168.0.0/16", Except: []string{"192.168.3.0/24", "192.168.4.0/24"}, }, }, }, }, {}, }, PolicyTypes: []networkingv1.PolicyType{networkingv1.PolicyTypeIngress, networkingv1.PolicyTypeEgress}, }, }) d := NetworkPolicyDescriber{versionedFake} out, err := d.Describe("default", "network-policy-1", DescriberSettings{}) if err != nil { t.Errorf("unexpected error: %s", err) } if out != expectedOut { t.Errorf("want:\n%s\ngot:\n%s", expectedOut, out) } } func TestDescribeIngressNetworkPolicies(t *testing.T) { expectedTime, err := time.Parse("2006-01-02 15:04:05 Z0700 MST", "2017-06-04 21:45:56 -0700 PDT") if err != nil { t.Errorf("unable to parse time %q error: %s", "2017-06-04 21:45:56 -0700 PDT", err) } expectedOut := `Name: network-policy-1 Namespace: default Created on: 2017-06-04 21:45:56 -0700 PDT Labels: Annotations: Spec: PodSelector: foo in (bar1,bar2),foo2 notin (bar1,bar2),id1=app1,id2=app2 Allowing ingress traffic: To Port: 80/TCP To Port: 82/TCP From: NamespaceSelector: id=ns1,id2=ns2 PodSelector: id=pod1,id2=pod2 From: PodSelector: id=app2,id2=app3 From: NamespaceSelector: id=app2,id2=app3 From: NamespaceSelector: foo in (bar1,bar2),id=app2,id2=app3 From: IPBlock: CIDR: 192.168.0.0/16 Except: 192.168.3.0/24, 192.168.4.0/24 ---------- To Port: (traffic allowed to all ports) From: (traffic not restricted by source) Not affecting egress traffic Policy Types: Ingress ` port80 := intstr.FromInt32(80) port82 := intstr.FromInt32(82) protoTCP := corev1.ProtocolTCP versionedFake := fake.NewSimpleClientset(&networkingv1.NetworkPolicy{ ObjectMeta: metav1.ObjectMeta{ Name: "network-policy-1", Namespace: "default", CreationTimestamp: metav1.NewTime(expectedTime), }, Spec: networkingv1.NetworkPolicySpec{ PodSelector: metav1.LabelSelector{ MatchLabels: map[string]string{ "id1": "app1", "id2": "app2", }, MatchExpressions: []metav1.LabelSelectorRequirement{ {Key: "foo", Operator: "In", Values: []string{"bar1", "bar2"}}, {Key: "foo2", Operator: "NotIn", Values: []string{"bar1", "bar2"}}, }, }, Ingress: []networkingv1.NetworkPolicyIngressRule{ { Ports: []networkingv1.NetworkPolicyPort{ {Port: &port80}, {Port: &port82, Protocol: &protoTCP}, }, From: []networkingv1.NetworkPolicyPeer{ { PodSelector: &metav1.LabelSelector{ MatchLabels: map[string]string{ "id": "pod1", "id2": "pod2", }, }, NamespaceSelector: &metav1.LabelSelector{ MatchLabels: map[string]string{ "id": "ns1", "id2": "ns2", }, }, }, { PodSelector: &metav1.LabelSelector{ MatchLabels: map[string]string{ "id": "app2", "id2": "app3", }, }, }, { NamespaceSelector: &metav1.LabelSelector{ MatchLabels: map[string]string{ "id": "app2", "id2": "app3", }, }, }, { NamespaceSelector: &metav1.LabelSelector{ MatchLabels: map[string]string{ "id": "app2", "id2": "app3", }, MatchExpressions: []metav1.LabelSelectorRequirement{ {Key: "foo", Operator: "In", Values: []string{"bar1", "bar2"}}, }, }, }, { IPBlock: &networkingv1.IPBlock{ CIDR: "192.168.0.0/16", Except: []string{"192.168.3.0/24", "192.168.4.0/24"}, }, }, }, }, {}, }, PolicyTypes: []networkingv1.PolicyType{networkingv1.PolicyTypeIngress}, }, }) d := NetworkPolicyDescriber{versionedFake} out, err := d.Describe("default", "network-policy-1", DescriberSettings{}) if err != nil { t.Errorf("unexpected error: %s", err) } if out != expectedOut { t.Errorf("want:\n%s\ngot:\n%s", expectedOut, out) } } func TestDescribeIsolatedEgressNetworkPolicies(t *testing.T) { expectedTime, err := time.Parse("2006-01-02 15:04:05 Z0700 MST", "2017-06-04 21:45:56 -0700 PDT") if err != nil { t.Errorf("unable to parse time %q error: %s", "2017-06-04 21:45:56 -0700 PDT", err) } expectedOut := `Name: network-policy-1 Namespace: default Created on: 2017-06-04 21:45:56 -0700 PDT Labels: Annotations: Spec: PodSelector: foo in (bar1,bar2),foo2 notin (bar1,bar2),id1=app1,id2=app2 Allowing ingress traffic: To Port: 80/TCP To Port: 82/TCP From: NamespaceSelector: id=ns1,id2=ns2 PodSelector: id=pod1,id2=pod2 From: PodSelector: id=app2,id2=app3 From: NamespaceSelector: id=app2,id2=app3 From: NamespaceSelector: foo in (bar1,bar2),id=app2,id2=app3 From: IPBlock: CIDR: 192.168.0.0/16 Except: 192.168.3.0/24, 192.168.4.0/24 ---------- To Port: (traffic allowed to all ports) From: (traffic not restricted by source) Allowing egress traffic: (Selected pods are isolated for egress connectivity) Policy Types: Ingress, Egress ` port80 := intstr.FromInt32(80) port82 := intstr.FromInt32(82) protoTCP := corev1.ProtocolTCP versionedFake := fake.NewSimpleClientset(&networkingv1.NetworkPolicy{ ObjectMeta: metav1.ObjectMeta{ Name: "network-policy-1", Namespace: "default", CreationTimestamp: metav1.NewTime(expectedTime), }, Spec: networkingv1.NetworkPolicySpec{ PodSelector: metav1.LabelSelector{ MatchLabels: map[string]string{ "id1": "app1", "id2": "app2", }, MatchExpressions: []metav1.LabelSelectorRequirement{ {Key: "foo", Operator: "In", Values: []string{"bar1", "bar2"}}, {Key: "foo2", Operator: "NotIn", Values: []string{"bar1", "bar2"}}, }, }, Ingress: []networkingv1.NetworkPolicyIngressRule{ { Ports: []networkingv1.NetworkPolicyPort{ {Port: &port80}, {Port: &port82, Protocol: &protoTCP}, }, From: []networkingv1.NetworkPolicyPeer{ { PodSelector: &metav1.LabelSelector{ MatchLabels: map[string]string{ "id": "pod1", "id2": "pod2", }, }, NamespaceSelector: &metav1.LabelSelector{ MatchLabels: map[string]string{ "id": "ns1", "id2": "ns2", }, }, }, { PodSelector: &metav1.LabelSelector{ MatchLabels: map[string]string{ "id": "app2", "id2": "app3", }, }, }, { NamespaceSelector: &metav1.LabelSelector{ MatchLabels: map[string]string{ "id": "app2", "id2": "app3", }, }, }, { NamespaceSelector: &metav1.LabelSelector{ MatchLabels: map[string]string{ "id": "app2", "id2": "app3", }, MatchExpressions: []metav1.LabelSelectorRequirement{ {Key: "foo", Operator: "In", Values: []string{"bar1", "bar2"}}, }, }, }, { IPBlock: &networkingv1.IPBlock{ CIDR: "192.168.0.0/16", Except: []string{"192.168.3.0/24", "192.168.4.0/24"}, }, }, }, }, {}, }, PolicyTypes: []networkingv1.PolicyType{networkingv1.PolicyTypeIngress, networkingv1.PolicyTypeEgress}, }, }) d := NetworkPolicyDescriber{versionedFake} out, err := d.Describe("default", "network-policy-1", DescriberSettings{}) if err != nil { t.Errorf("unexpected error: %s", err) } if out != expectedOut { t.Errorf("want:\n%s\ngot:\n%s", expectedOut, out) } } func TestDescribeNetworkPoliciesWithPortRange(t *testing.T) { expectedTime, err := time.Parse("2006-01-02 15:04:05 Z0700 MST", "2017-06-04 21:45:56 -0700 PDT") if err != nil { t.Errorf("unable to parse time %q error: %s", "2017-06-04 21:45:56 -0700 PDT", err) } expectedOut := `Name: network-policy-1 Namespace: default Created on: 2017-06-04 21:45:56 -0700 PDT Labels: Annotations: Spec: PodSelector: foo in (bar1,bar2),foo2 notin (bar1,bar2),id1=app1,id2=app2 Allowing ingress traffic: To Port Range: 80-82/TCP From: NamespaceSelector: id=ns1,id2=ns2 PodSelector: id=pod1,id2=pod2 From: PodSelector: id=app2,id2=app3 From: NamespaceSelector: id=app2,id2=app3 From: NamespaceSelector: foo in (bar1,bar2),id=app2,id2=app3 From: IPBlock: CIDR: 192.168.0.0/16 Except: 192.168.3.0/24, 192.168.4.0/24 ---------- To Port: (traffic allowed to all ports) From: (traffic not restricted by source) Allowing egress traffic: To Port Range: 80-82/TCP To: NamespaceSelector: id=ns1,id2=ns2 PodSelector: id=pod1,id2=pod2 To: PodSelector: id=app2,id2=app3 To: NamespaceSelector: id=app2,id2=app3 To: NamespaceSelector: foo in (bar1,bar2),id=app2,id2=app3 To: IPBlock: CIDR: 192.168.0.0/16 Except: 192.168.3.0/24, 192.168.4.0/24 ---------- To Port: (traffic allowed to all ports) To: (traffic not restricted by destination) Policy Types: Ingress, Egress ` port80 := intstr.FromInt(80) port82 := int32(82) protoTCP := corev1.ProtocolTCP versionedFake := fake.NewSimpleClientset(&networkingv1.NetworkPolicy{ ObjectMeta: metav1.ObjectMeta{ Name: "network-policy-1", Namespace: "default", CreationTimestamp: metav1.NewTime(expectedTime), }, Spec: networkingv1.NetworkPolicySpec{ PodSelector: metav1.LabelSelector{ MatchLabels: map[string]string{ "id1": "app1", "id2": "app2", }, MatchExpressions: []metav1.LabelSelectorRequirement{ {Key: "foo", Operator: "In", Values: []string{"bar1", "bar2"}}, {Key: "foo2", Operator: "NotIn", Values: []string{"bar1", "bar2"}}, }, }, Ingress: []networkingv1.NetworkPolicyIngressRule{ { Ports: []networkingv1.NetworkPolicyPort{ {Port: &port80, EndPort: &port82, Protocol: &protoTCP}, }, From: []networkingv1.NetworkPolicyPeer{ { PodSelector: &metav1.LabelSelector{ MatchLabels: map[string]string{ "id": "pod1", "id2": "pod2", }, }, NamespaceSelector: &metav1.LabelSelector{ MatchLabels: map[string]string{ "id": "ns1", "id2": "ns2", }, }, }, { PodSelector: &metav1.LabelSelector{ MatchLabels: map[string]string{ "id": "app2", "id2": "app3", }, }, }, { NamespaceSelector: &metav1.LabelSelector{ MatchLabels: map[string]string{ "id": "app2", "id2": "app3", }, }, }, { NamespaceSelector: &metav1.LabelSelector{ MatchLabels: map[string]string{ "id": "app2", "id2": "app3", }, MatchExpressions: []metav1.LabelSelectorRequirement{ {Key: "foo", Operator: "In", Values: []string{"bar1", "bar2"}}, }, }, }, { IPBlock: &networkingv1.IPBlock{ CIDR: "192.168.0.0/16", Except: []string{"192.168.3.0/24", "192.168.4.0/24"}, }, }, }, }, {}, }, Egress: []networkingv1.NetworkPolicyEgressRule{ { Ports: []networkingv1.NetworkPolicyPort{ {Port: &port80, EndPort: &port82, Protocol: &protoTCP}, }, To: []networkingv1.NetworkPolicyPeer{ { PodSelector: &metav1.LabelSelector{ MatchLabels: map[string]string{ "id": "pod1", "id2": "pod2", }, }, NamespaceSelector: &metav1.LabelSelector{ MatchLabels: map[string]string{ "id": "ns1", "id2": "ns2", }, }, }, { PodSelector: &metav1.LabelSelector{ MatchLabels: map[string]string{ "id": "app2", "id2": "app3", }, }, }, { NamespaceSelector: &metav1.LabelSelector{ MatchLabels: map[string]string{ "id": "app2", "id2": "app3", }, }, }, { NamespaceSelector: &metav1.LabelSelector{ MatchLabels: map[string]string{ "id": "app2", "id2": "app3", }, MatchExpressions: []metav1.LabelSelectorRequirement{ {Key: "foo", Operator: "In", Values: []string{"bar1", "bar2"}}, }, }, }, { IPBlock: &networkingv1.IPBlock{ CIDR: "192.168.0.0/16", Except: []string{"192.168.3.0/24", "192.168.4.0/24"}, }, }, }, }, {}, }, PolicyTypes: []networkingv1.PolicyType{networkingv1.PolicyTypeIngress, networkingv1.PolicyTypeEgress}, }, }) d := NetworkPolicyDescriber{versionedFake} out, err := d.Describe("default", "network-policy-1", DescriberSettings{}) if err != nil { t.Errorf("unexpected error: %s", err) } if out != expectedOut { t.Errorf("want:\n%s\ngot:\n%s", expectedOut, out) } } func TestDescribeServiceAccount(t *testing.T) { fake := fake.NewSimpleClientset(&corev1.ServiceAccount{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", }, Secrets: []corev1.ObjectReference{ { Name: "test-objectref", }, }, ImagePullSecrets: []corev1.LocalObjectReference{ { Name: "test-local-ref", }, }, }) c := &describeClient{T: t, Namespace: "foo", Interface: fake} d := ServiceAccountDescriber{c} out, err := d.Describe("foo", "bar", DescriberSettings{ShowEvents: true}) if err != nil { t.Errorf("unexpected error: %v", err) } expectedOut := `Name: bar Namespace: foo Labels: Annotations: Image pull secrets: test-local-ref (not found) Mountable secrets: test-objectref (not found) Tokens: Events: ` + "\n" if out != expectedOut { t.Errorf("expected : %q\n but got output:\n %q", expectedOut, out) } } func getHugePageResourceList(pageSize, value string) corev1.ResourceList { res := corev1.ResourceList{} if pageSize != "" && value != "" { res[corev1.ResourceName(corev1.ResourceHugePagesPrefix+pageSize)] = resource.MustParse(value) } return res } // mergeResourceLists will merge resoure lists. When two lists have the same resourece, the value from // the last list will be present in the result func mergeResourceLists(resourceLists ...corev1.ResourceList) corev1.ResourceList { result := corev1.ResourceList{} for _, rl := range resourceLists { for resource, quantity := range rl { result[resource] = quantity } } return result } func TestDescribeNode(t *testing.T) { holderIdentity := "holder" nodeCapacity := mergeResourceLists( getHugePageResourceList("2Mi", "4Gi"), getResourceList("8", "24Gi"), getHugePageResourceList("1Gi", "0"), ) nodeAllocatable := mergeResourceLists( getHugePageResourceList("2Mi", "2Gi"), getResourceList("4", "12Gi"), getHugePageResourceList("1Gi", "0"), ) fake := fake.NewSimpleClientset( &corev1.Node{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", UID: "uid", }, Spec: corev1.NodeSpec{ Unschedulable: true, }, Status: corev1.NodeStatus{ Capacity: nodeCapacity, Allocatable: nodeAllocatable, }, }, &coordinationv1.Lease{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: corev1.NamespaceNodeLease, }, Spec: coordinationv1.LeaseSpec{ HolderIdentity: &holderIdentity, AcquireTime: &metav1.MicroTime{Time: time.Now().Add(-time.Hour)}, RenewTime: &metav1.MicroTime{Time: time.Now()}, }, }, &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "pod-with-resources", Namespace: "foo", }, TypeMeta: metav1.TypeMeta{ Kind: "Pod", }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "cpu-mem", Image: "image:latest", Resources: corev1.ResourceRequirements{ Requests: getResourceList("1", "1Gi"), Limits: getResourceList("2", "2Gi"), }, }, { Name: "hugepages", Image: "image:latest", Resources: corev1.ResourceRequirements{ Requests: getHugePageResourceList("2Mi", "512Mi"), Limits: getHugePageResourceList("2Mi", "512Mi"), }, }, }, }, Status: corev1.PodStatus{ Phase: corev1.PodRunning, }, }, &corev1.EventList{ Items: []corev1.Event{ { ObjectMeta: metav1.ObjectMeta{ Name: "event-1", Namespace: "default", }, InvolvedObject: corev1.ObjectReference{ Kind: "Node", Name: "bar", UID: "bar", }, Message: "Node bar status is now: NodeHasNoDiskPressure", FirstTimestamp: metav1.NewTime(time.Date(2014, time.January, 15, 0, 0, 0, 0, time.UTC)), LastTimestamp: metav1.NewTime(time.Date(2014, time.January, 15, 0, 0, 0, 0, time.UTC)), Count: 1, Type: corev1.EventTypeNormal, }, { ObjectMeta: metav1.ObjectMeta{ Name: "event-2", Namespace: "default", }, InvolvedObject: corev1.ObjectReference{ Kind: "Node", Name: "bar", UID: "0ceac5fb-a393-49d7-b04f-9ea5f18de5e9", }, Message: "Node bar status is now: NodeReady", FirstTimestamp: metav1.NewTime(time.Date(2014, time.January, 15, 0, 0, 0, 0, time.UTC)), LastTimestamp: metav1.NewTime(time.Date(2014, time.January, 15, 0, 0, 0, 0, time.UTC)), Count: 2, Type: corev1.EventTypeNormal, }, }, }, ) c := &describeClient{T: t, Namespace: "foo", Interface: fake} d := NodeDescriber{c} out, err := d.Describe("foo", "bar", DescriberSettings{ShowEvents: true}) if err != nil { t.Errorf("unexpected error: %v", err) } expectedOut := []string{"Unschedulable", "true", "holder", `Allocated resources: (Total limits may be over 100 percent, i.e., overcommitted.) Resource Requests Limits -------- -------- ------ cpu 1 (25%) 2 (50%) memory 1Gi (8%) 2Gi (16%) ephemeral-storage 0 (0%) 0 (0%) hugepages-1Gi 0 (0%) 0 (0%) hugepages-2Mi 512Mi (25%) 512Mi (25%)`, `Node bar status is now: NodeHasNoDiskPressure`, `Node bar status is now: NodeReady`} for _, expected := range expectedOut { if !strings.Contains(out, expected) { t.Errorf("expected to find %q in output: %q", expected, out) } } } func TestDescribeNodeWithSidecar(t *testing.T) { holderIdentity := "holder" nodeCapacity := mergeResourceLists( getHugePageResourceList("2Mi", "4Gi"), getResourceList("8", "24Gi"), getHugePageResourceList("1Gi", "0"), ) nodeAllocatable := mergeResourceLists( getHugePageResourceList("2Mi", "2Gi"), getResourceList("4", "12Gi"), getHugePageResourceList("1Gi", "0"), ) restartPolicy := corev1.ContainerRestartPolicyAlways fake := fake.NewSimpleClientset( &corev1.Node{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", UID: "uid", }, Spec: corev1.NodeSpec{ Unschedulable: true, }, Status: corev1.NodeStatus{ Capacity: nodeCapacity, Allocatable: nodeAllocatable, }, }, &coordinationv1.Lease{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: corev1.NamespaceNodeLease, }, Spec: coordinationv1.LeaseSpec{ HolderIdentity: &holderIdentity, AcquireTime: &metav1.MicroTime{Time: time.Now().Add(-time.Hour)}, RenewTime: &metav1.MicroTime{Time: time.Now()}, }, }, &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "pod-with-resources", Namespace: "foo", }, TypeMeta: metav1.TypeMeta{ Kind: "Pod", }, Spec: corev1.PodSpec{ InitContainers: []corev1.Container{ // sidecar, should sum into the total resources { Name: "init-container-1", RestartPolicy: &restartPolicy, Resources: corev1.ResourceRequirements{ Requests: getResourceList("1", "1Gi"), }, }, // non-sidecar { Name: "init-container-2", Resources: corev1.ResourceRequirements{ Requests: getResourceList("1", "1Gi"), }, }, }, Containers: []corev1.Container{ { Name: "cpu-mem", Image: "image:latest", Resources: corev1.ResourceRequirements{ Requests: getResourceList("1", "1Gi"), Limits: getResourceList("2", "2Gi"), }, }, { Name: "hugepages", Image: "image:latest", Resources: corev1.ResourceRequirements{ Requests: getHugePageResourceList("2Mi", "512Mi"), Limits: getHugePageResourceList("2Mi", "512Mi"), }, }, }, }, Status: corev1.PodStatus{ Phase: corev1.PodRunning, }, }, &corev1.EventList{ Items: []corev1.Event{ { ObjectMeta: metav1.ObjectMeta{ Name: "event-1", Namespace: "default", }, InvolvedObject: corev1.ObjectReference{ Kind: "Node", Name: "bar", UID: "bar", }, Message: "Node bar status is now: NodeHasNoDiskPressure", FirstTimestamp: metav1.NewTime(time.Date(2014, time.January, 15, 0, 0, 0, 0, time.UTC)), LastTimestamp: metav1.NewTime(time.Date(2014, time.January, 15, 0, 0, 0, 0, time.UTC)), Count: 1, Type: corev1.EventTypeNormal, }, { ObjectMeta: metav1.ObjectMeta{ Name: "event-2", Namespace: "default", }, InvolvedObject: corev1.ObjectReference{ Kind: "Node", Name: "bar", UID: "0ceac5fb-a393-49d7-b04f-9ea5f18de5e9", }, Message: "Node bar status is now: NodeReady", FirstTimestamp: metav1.NewTime(time.Date(2014, time.January, 15, 0, 0, 0, 0, time.UTC)), LastTimestamp: metav1.NewTime(time.Date(2014, time.January, 15, 0, 0, 0, 0, time.UTC)), Count: 2, Type: corev1.EventTypeNormal, }, }, }, ) c := &describeClient{T: t, Namespace: "foo", Interface: fake} d := NodeDescriber{c} out, err := d.Describe("foo", "bar", DescriberSettings{ShowEvents: true}) if err != nil { t.Errorf("unexpected error: %v", err) } expectedOut := []string{"Unschedulable", "true", "holder", `Allocated resources: (Total limits may be over 100 percent, i.e., overcommitted.) Resource Requests Limits -------- -------- ------ cpu 2 (50%) 2 (50%) memory 2Gi (16%) 2Gi (16%) ephemeral-storage 0 (0%) 0 (0%) hugepages-1Gi 0 (0%) 0 (0%) hugepages-2Mi 512Mi (25%) 512Mi (25%)`, `Node bar status is now: NodeHasNoDiskPressure`, `Node bar status is now: NodeReady`} for _, expected := range expectedOut { if !strings.Contains(out, expected) { t.Errorf("expected to find %s in output: %s", expected, out) } } } func TestDescribeStatefulSet(t *testing.T) { var partition int32 = 2672 var replicas int32 = 1 fake := fake.NewSimpleClientset(&appsv1.StatefulSet{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", }, Spec: appsv1.StatefulSetSpec{ Replicas: &replicas, Selector: &metav1.LabelSelector{}, Template: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ {Image: "mytest-image:latest"}, }, }, }, UpdateStrategy: appsv1.StatefulSetUpdateStrategy{ Type: appsv1.RollingUpdateStatefulSetStrategyType, RollingUpdate: &appsv1.RollingUpdateStatefulSetStrategy{ Partition: &partition, }, }, }, }) d := StatefulSetDescriber{fake} out, err := d.Describe("foo", "bar", DescriberSettings{ShowEvents: true}) if err != nil { t.Errorf("unexpected error: %v", err) } expectedOutputs := []string{ "bar", "foo", "Containers:", "mytest-image:latest", "Update Strategy", "RollingUpdate", "Partition", "2672", } for _, o := range expectedOutputs { if !strings.Contains(out, o) { t.Errorf("unexpected out: %s", out) break } } } func TestDescribeDaemonSet(t *testing.T) { fake := fake.NewSimpleClientset(&appsv1.DaemonSet{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", }, Spec: appsv1.DaemonSetSpec{ Selector: &metav1.LabelSelector{ MatchLabels: map[string]string{"node-role.kubernetes.io/control-plane": "true"}, }, Template: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ {Image: "mytest-image:latest"}, }, }, }, }, }) d := DaemonSetDescriber{fake} out, err := d.Describe("foo", "bar", DescriberSettings{ShowEvents: true}) if err != nil { t.Errorf("unexpected error: %v", err) } expectedOutputs := []string{ "bar", "foo", "Containers:", "mytest-image:latest", "Selector", "node-role.kubernetes.io/control-plane=true", } for _, o := range expectedOutputs { if !strings.Contains(out, o) { t.Errorf("unexpected out: %s", out) break } } } func TestDescribeEndpointSlice(t *testing.T) { protocolTCP := corev1.ProtocolTCP port80 := int32(80) testcases := map[string]struct { input *fake.Clientset output string }{ "EndpointSlices v1beta1": { input: fake.NewSimpleClientset(&discoveryv1beta1.EndpointSlice{ ObjectMeta: metav1.ObjectMeta{ Name: "foo.123", Namespace: "bar", }, AddressType: discoveryv1beta1.AddressTypeIPv4, Endpoints: []discoveryv1beta1.Endpoint{ { Addresses: []string{"1.2.3.4", "1.2.3.5"}, Conditions: discoveryv1beta1.EndpointConditions{Ready: ptr.To(true)}, TargetRef: &corev1.ObjectReference{Kind: "Pod", Name: "test-123"}, Topology: map[string]string{ "topology.kubernetes.io/zone": "us-central1-a", "topology.kubernetes.io/region": "us-central1", }, }, { Addresses: []string{"1.2.3.6", "1.2.3.7"}, Conditions: discoveryv1beta1.EndpointConditions{Ready: ptr.To(true)}, TargetRef: &corev1.ObjectReference{Kind: "Pod", Name: "test-124"}, Topology: map[string]string{ "topology.kubernetes.io/zone": "us-central1-b", "topology.kubernetes.io/region": "us-central1", }, }, }, Ports: []discoveryv1beta1.EndpointPort{ { Protocol: &protocolTCP, Port: &port80, }, }, }), output: `Name: foo.123 Namespace: bar Labels: Annotations: AddressType: IPv4 Ports: Name Port Protocol ---- ---- -------- 80 TCP Endpoints: - Addresses: 1.2.3.4,1.2.3.5 Conditions: Ready: true Hostname: TargetRef: Pod/test-123 Topology: topology.kubernetes.io/region=us-central1 topology.kubernetes.io/zone=us-central1-a - Addresses: 1.2.3.6,1.2.3.7 Conditions: Ready: true Hostname: TargetRef: Pod/test-124 Topology: topology.kubernetes.io/region=us-central1 topology.kubernetes.io/zone=us-central1-b Events: ` + "\n", }, "EndpointSlices v1": { input: fake.NewSimpleClientset(&discoveryv1.EndpointSlice{ ObjectMeta: metav1.ObjectMeta{ Name: "foo.123", Namespace: "bar", }, AddressType: discoveryv1.AddressTypeIPv4, Endpoints: []discoveryv1.Endpoint{ { Addresses: []string{"1.2.3.4", "1.2.3.5"}, Conditions: discoveryv1.EndpointConditions{Ready: ptr.To(true)}, TargetRef: &corev1.ObjectReference{Kind: "Pod", Name: "test-123"}, Zone: ptr.To("us-central1-a"), NodeName: ptr.To("node-1"), }, { Addresses: []string{"1.2.3.6", "1.2.3.7"}, Conditions: discoveryv1.EndpointConditions{Ready: ptr.To(true)}, TargetRef: &corev1.ObjectReference{Kind: "Pod", Name: "test-124"}, NodeName: ptr.To("node-2"), }, }, Ports: []discoveryv1.EndpointPort{ { Protocol: &protocolTCP, Port: &port80, }, }, }), output: `Name: foo.123 Namespace: bar Labels: Annotations: AddressType: IPv4 Ports: Name Port Protocol ---- ---- -------- 80 TCP Endpoints: - Addresses: 1.2.3.4, 1.2.3.5 Conditions: Ready: true Hostname: TargetRef: Pod/test-123 NodeName: node-1 Zone: us-central1-a - Addresses: 1.2.3.6, 1.2.3.7 Conditions: Ready: true Hostname: TargetRef: Pod/test-124 NodeName: node-2 Zone: Events: ` + "\n", }, } for name, tc := range testcases { t.Run(name, func(t *testing.T) { c := &describeClient{T: t, Namespace: "foo", Interface: tc.input} d := EndpointSliceDescriber{c} out, err := d.Describe("bar", "foo.123", DescriberSettings{ShowEvents: true}) if err != nil { t.Errorf("unexpected error: %v", err) } if out != tc.output { t.Log(out) t.Errorf("expected :\n%s\nbut got output:\n%s", tc.output, out) } }) } } func TestDescribeServiceCIDR(t *testing.T) { testcases := map[string]struct { input *fake.Clientset output string }{ "ServiceCIDR v1beta1": { input: fake.NewSimpleClientset(&networkingv1beta1.ServiceCIDR{ ObjectMeta: metav1.ObjectMeta{ Name: "foo.123", }, Spec: networkingv1beta1.ServiceCIDRSpec{ CIDRs: []string{"10.1.0.0/16", "fd00:1:1::/64"}, }, }), output: `Name: foo.123 Labels: Annotations: CIDRs: 10.1.0.0/16, fd00:1:1::/64 Events: ` + "\n", }, "ServiceCIDR v1beta1 IPv4": { input: fake.NewSimpleClientset(&networkingv1beta1.ServiceCIDR{ ObjectMeta: metav1.ObjectMeta{ Name: "foo.123", }, Spec: networkingv1beta1.ServiceCIDRSpec{ CIDRs: []string{"10.1.0.0/16"}, }, }), output: `Name: foo.123 Labels: Annotations: CIDRs: 10.1.0.0/16 Events: ` + "\n", }, "ServiceCIDR v1beta1 IPv6": { input: fake.NewSimpleClientset(&networkingv1beta1.ServiceCIDR{ ObjectMeta: metav1.ObjectMeta{ Name: "foo.123", }, Spec: networkingv1beta1.ServiceCIDRSpec{ CIDRs: []string{"fd00:1:1::/64"}, }, }), output: `Name: foo.123 Labels: Annotations: CIDRs: fd00:1:1::/64 Events: ` + "\n", }, "ServiceCIDR v1": { input: fake.NewSimpleClientset(&networkingv1.ServiceCIDR{ ObjectMeta: metav1.ObjectMeta{ Name: "foo.123", }, Spec: networkingv1.ServiceCIDRSpec{ CIDRs: []string{"10.1.0.0/16", "fd00:1:1::/64"}, }, }), output: `Name: foo.123 Labels: Annotations: CIDRs: 10.1.0.0/16, fd00:1:1::/64 Events: ` + "\n", }, "ServiceCIDR v1 IPv4": { input: fake.NewSimpleClientset(&networkingv1.ServiceCIDR{ ObjectMeta: metav1.ObjectMeta{ Name: "foo.123", }, Spec: networkingv1.ServiceCIDRSpec{ CIDRs: []string{"10.1.0.0/16"}, }, }), output: `Name: foo.123 Labels: Annotations: CIDRs: 10.1.0.0/16 Events: ` + "\n", }, "ServiceCIDR v1 IPv6": { input: fake.NewSimpleClientset(&networkingv1.ServiceCIDR{ ObjectMeta: metav1.ObjectMeta{ Name: "foo.123", }, Spec: networkingv1.ServiceCIDRSpec{ CIDRs: []string{"fd00:1:1::/64"}, }, }), output: `Name: foo.123 Labels: Annotations: CIDRs: fd00:1:1::/64 Events: ` + "\n", }, } for name, tc := range testcases { t.Run(name, func(t *testing.T) { c := &describeClient{T: t, Namespace: "foo", Interface: tc.input} d := ServiceCIDRDescriber{c} out, err := d.Describe("bar", "foo.123", DescriberSettings{ShowEvents: true}) if err != nil { t.Errorf("unexpected error: %v", err) } if out != tc.output { t.Errorf("expected :\n%s\nbut got output:\n%s diff:\n%s", tc.output, out, cmp.Diff(tc.output, out)) } }) } } func TestDescribeIPAddress(t *testing.T) { testcases := map[string]struct { input *fake.Clientset output string }{ "IPAddress v1beta1": { input: fake.NewSimpleClientset(&networkingv1beta1.IPAddress{ ObjectMeta: metav1.ObjectMeta{ Name: "foo.123", }, Spec: networkingv1beta1.IPAddressSpec{ ParentRef: &networkingv1beta1.ParentReference{ Group: "mygroup", Resource: "myresource", Namespace: "mynamespace", Name: "myname", }, }, }), output: `Name: foo.123 Labels: Annotations: Parent Reference: Group: mygroup Resource: myresource Namespace: mynamespace Name: myname Events: ` + "\n", }, "IPAddress v1": { input: fake.NewSimpleClientset(&networkingv1.IPAddress{ ObjectMeta: metav1.ObjectMeta{ Name: "foo.123", }, Spec: networkingv1.IPAddressSpec{ ParentRef: &networkingv1.ParentReference{ Group: "mygroup", Resource: "myresource", Namespace: "mynamespace", Name: "myname", }, }, }), output: `Name: foo.123 Labels: Annotations: Parent Reference: Group: mygroup Resource: myresource Namespace: mynamespace Name: myname Events: ` + "\n", }, } for name, tc := range testcases { t.Run(name, func(t *testing.T) { c := &describeClient{T: t, Namespace: "foo", Interface: tc.input} d := IPAddressDescriber{c} out, err := d.Describe("bar", "foo.123", DescriberSettings{ShowEvents: true}) if err != nil { t.Errorf("unexpected error: %v", err) } if out != tc.output { t.Errorf("expected :\n%s\nbut got output:\n%s diff:\n%s", tc.output, out, cmp.Diff(tc.output, out)) } }) } } func TestControllerRef(t *testing.T) { var replicas int32 = 1 f := fake.NewSimpleClientset( &corev1.ReplicationController{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", UID: "123456", }, TypeMeta: metav1.TypeMeta{ Kind: "ReplicationController", }, Spec: corev1.ReplicationControllerSpec{ Replicas: &replicas, Selector: map[string]string{"abc": "xyz"}, Template: &corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ {Image: "mytest-image:latest"}, }, }, }, }, }, &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "barpod", Namespace: "foo", Labels: map[string]string{"abc": "xyz"}, OwnerReferences: []metav1.OwnerReference{{Name: "bar", UID: "123456", Controller: ptr.To(true)}}, }, TypeMeta: metav1.TypeMeta{ Kind: "Pod", }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ {Image: "mytest-image:latest"}, }, }, Status: corev1.PodStatus{ Phase: corev1.PodRunning, }, }, &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "orphan", Namespace: "foo", Labels: map[string]string{"abc": "xyz"}, }, TypeMeta: metav1.TypeMeta{ Kind: "Pod", }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ {Image: "mytest-image:latest"}, }, }, Status: corev1.PodStatus{ Phase: corev1.PodRunning, }, }, &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "buzpod", Namespace: "foo", Labels: map[string]string{"abc": "xyz"}, OwnerReferences: []metav1.OwnerReference{{Name: "buz", UID: "654321", Controller: ptr.To(true)}}, }, TypeMeta: metav1.TypeMeta{ Kind: "Pod", }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ {Image: "mytest-image:latest"}, }, }, Status: corev1.PodStatus{ Phase: corev1.PodRunning, }, }) d := ReplicationControllerDescriber{f} out, err := d.Describe("foo", "bar", DescriberSettings{ShowEvents: false}) if err != nil { t.Errorf("unexpected error: %v", err) } if !strings.Contains(out, "1 Running") { t.Errorf("unexpected out: %s", out) } } func TestDescribeTerminalEscape(t *testing.T) { fake := fake.NewSimpleClientset(&corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ Name: "mycm", Namespace: "foo", Annotations: map[string]string{"annotation1": "terminal escape: \x1b"}, }, }) c := &describeClient{T: t, Namespace: "foo", Interface: fake} d := ConfigMapDescriber{c} out, err := d.Describe("foo", "mycm", DescriberSettings{ShowEvents: true}) if err != nil { t.Errorf("unexpected error: %v", err) } if strings.Contains(out, "\x1b") || !strings.Contains(out, "^[") { t.Errorf("unexpected out: %s", out) } } func TestDescribeSeccompProfile(t *testing.T) { testLocalhostProfiles := []string{"lauseafoodpod", "tikkamasalaconatiner", "dropshotephemeral"} testCases := []struct { name string pod *corev1.Pod expect []string }{ { name: "podLocalhostSeccomp", pod: &corev1.Pod{ Spec: corev1.PodSpec{ SecurityContext: &corev1.PodSecurityContext{ SeccompProfile: &corev1.SeccompProfile{ Type: corev1.SeccompProfileTypeLocalhost, LocalhostProfile: &testLocalhostProfiles[0], }, }, }, }, expect: []string{ "SeccompProfile", "Localhost", "LocalhostProfile", testLocalhostProfiles[0], }, }, { name: "podOther", pod: &corev1.Pod{ Spec: corev1.PodSpec{ SecurityContext: &corev1.PodSecurityContext{ SeccompProfile: &corev1.SeccompProfile{ Type: corev1.SeccompProfileTypeRuntimeDefault, }, }, }, }, expect: []string{ "SeccompProfile", "RuntimeDefault", }, }, { name: "containerLocalhostSeccomp", pod: &corev1.Pod{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ { SecurityContext: &corev1.SecurityContext{ SeccompProfile: &corev1.SeccompProfile{ Type: corev1.SeccompProfileTypeLocalhost, LocalhostProfile: &testLocalhostProfiles[1], }, }, }, }, }, }, expect: []string{ "SeccompProfile", "Localhost", "LocalhostProfile", testLocalhostProfiles[1], }, }, { name: "containerOther", pod: &corev1.Pod{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ { SecurityContext: &corev1.SecurityContext{ SeccompProfile: &corev1.SeccompProfile{ Type: corev1.SeccompProfileTypeUnconfined, }, }, }, }, }, }, expect: []string{ "SeccompProfile", "Unconfined", }, }, { name: "ephemeralLocalhostSeccomp", pod: &corev1.Pod{ Spec: corev1.PodSpec{ EphemeralContainers: []corev1.EphemeralContainer{ { EphemeralContainerCommon: corev1.EphemeralContainerCommon{ SecurityContext: &corev1.SecurityContext{ SeccompProfile: &corev1.SeccompProfile{ Type: corev1.SeccompProfileTypeLocalhost, LocalhostProfile: &testLocalhostProfiles[2], }, }, }, }, }, }, }, expect: []string{ "SeccompProfile", "Localhost", "LocalhostProfile", testLocalhostProfiles[2], }, }, { name: "ephemeralOther", pod: &corev1.Pod{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ { SecurityContext: &corev1.SecurityContext{ SeccompProfile: &corev1.SeccompProfile{ Type: corev1.SeccompProfileTypeUnconfined, }, }, }, }, }, }, expect: []string{ "SeccompProfile", "Unconfined", }, }, } for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { fake := fake.NewSimpleClientset(testCase.pod) c := &describeClient{T: t, Interface: fake} d := PodDescriber{c} out, err := d.Describe("", "", DescriberSettings{ShowEvents: true}) if err != nil { t.Errorf("unexpected error: %v", err) } for _, expected := range testCase.expect { if !strings.Contains(out, expected) { t.Errorf("expected to find %q in output: %q", expected, out) } } }) } } func TestDescribeProjectedVolumesOptionalSecret(t *testing.T) { fake := fake.NewSimpleClientset(&corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "bar", Namespace: "foo", }, Spec: corev1.PodSpec{ Volumes: []corev1.Volume{ { Name: "optional-secret", VolumeSource: corev1.VolumeSource{ Projected: &corev1.ProjectedVolumeSource{ Sources: []corev1.VolumeProjection{ { Secret: &corev1.SecretProjection{ LocalObjectReference: corev1.LocalObjectReference{ Name: "optional-secret", }, Optional: ptr.To(true), }, }, }, }, }, }, }, }, }) c := &describeClient{T: t, Namespace: "foo", Interface: fake} d := PodDescriber{c} out, err := d.Describe("foo", "bar", DescriberSettings{ShowEvents: true}) if err != nil { t.Errorf("unexpected error: %v", err) } expectedOut := "SecretName: optional-secret\n Optional: true" if !strings.Contains(out, expectedOut) { t.Errorf("expected to find %q in output: %q", expectedOut, out) } }