From 5f8f607c2779ae9f120a4b0a617e3ab386bb15d0 Mon Sep 17 00:00:00 2001 From: Sean Sullivan Date: Tue, 9 Oct 2018 18:59:24 -0700 Subject: [PATCH] kubectl: copy pod utils into util/podutils package --- pkg/kubectl/BUILD | 2 +- pkg/kubectl/polymorphichelpers/BUILD | 4 +- .../attachablepodforobject.go | 4 +- .../polymorphichelpers/helpers_test.go | 10 +- .../polymorphichelpers/logsforobject.go | 4 +- pkg/kubectl/rolling_updater.go | 4 +- pkg/kubectl/util/BUILD | 1 + pkg/kubectl/util/podutils/BUILD | 27 +++ pkg/kubectl/util/podutils/podutils.go | 190 ++++++++++++++++++ 9 files changed, 232 insertions(+), 14 deletions(-) create mode 100644 pkg/kubectl/util/podutils/BUILD create mode 100644 pkg/kubectl/util/podutils/podutils.go diff --git a/pkg/kubectl/BUILD b/pkg/kubectl/BUILD index ca4c7c21e3..f68f57b8d8 100644 --- a/pkg/kubectl/BUILD +++ b/pkg/kubectl/BUILD @@ -109,7 +109,6 @@ go_library( ], importpath = "k8s.io/kubernetes/pkg/kubectl", deps = [ - "//pkg/api/v1/pod:go_default_library", "//pkg/apis/core:go_default_library", "//pkg/apis/core/v1:go_default_library", "//pkg/controller/deployment/util:go_default_library", @@ -118,6 +117,7 @@ go_library( "//pkg/kubectl/scheme:go_default_library", "//pkg/kubectl/util:go_default_library", "//pkg/kubectl/util/hash:go_default_library", + "//pkg/kubectl/util/podutils:go_default_library", "//pkg/kubectl/util/slice:go_default_library", "//pkg/printers:go_default_library", "//pkg/printers/internalversion:go_default_library", diff --git a/pkg/kubectl/polymorphichelpers/BUILD b/pkg/kubectl/polymorphichelpers/BUILD index 48c44d8979..07bd70b7ef 100644 --- a/pkg/kubectl/polymorphichelpers/BUILD +++ b/pkg/kubectl/polymorphichelpers/BUILD @@ -22,9 +22,9 @@ go_library( importpath = "k8s.io/kubernetes/pkg/kubectl/polymorphichelpers", visibility = ["//visibility:public"], deps = [ - "//pkg/controller:go_default_library", "//pkg/kubectl:go_default_library", "//pkg/kubectl/scheme:go_default_library", + "//pkg/kubectl/util/podutils:go_default_library", "//staging/src/k8s.io/api/apps/v1:go_default_library", "//staging/src/k8s.io/api/apps/v1beta1:go_default_library", "//staging/src/k8s.io/api/apps/v1beta2:go_default_library", @@ -63,7 +63,7 @@ go_test( ], embed = [":go_default_library"], deps = [ - "//pkg/controller:go_default_library", + "//pkg/kubectl/util/podutils:go_default_library", "//staging/src/k8s.io/api/apps/v1:go_default_library", "//staging/src/k8s.io/api/apps/v1beta1:go_default_library", "//staging/src/k8s.io/api/apps/v1beta2:go_default_library", diff --git a/pkg/kubectl/polymorphichelpers/attachablepodforobject.go b/pkg/kubectl/polymorphichelpers/attachablepodforobject.go index bb4e366228..52a4d05384 100644 --- a/pkg/kubectl/polymorphichelpers/attachablepodforobject.go +++ b/pkg/kubectl/polymorphichelpers/attachablepodforobject.go @@ -25,7 +25,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/cli-runtime/pkg/genericclioptions" corev1client "k8s.io/client-go/kubernetes/typed/core/v1" - "k8s.io/kubernetes/pkg/controller" + "k8s.io/kubernetes/pkg/kubectl/util/podutils" ) // attachablePodForObject returns the pod to which to attach given an object. @@ -48,7 +48,7 @@ func attachablePodForObject(restClientGetter genericclioptions.RESTClientGetter, if err != nil { return nil, fmt.Errorf("cannot attach to %T: %v", object, err) } - sortBy := func(pods []*corev1.Pod) sort.Interface { return sort.Reverse(controller.ActivePods(pods)) } + sortBy := func(pods []*corev1.Pod) sort.Interface { return sort.Reverse(podutils.ActivePods(pods)) } pod, _, err := GetFirstPod(clientset, namespace, selector.String(), timeout, sortBy) return pod, err } diff --git a/pkg/kubectl/polymorphichelpers/helpers_test.go b/pkg/kubectl/polymorphichelpers/helpers_test.go index 91e582f8aa..2aba02e6db 100644 --- a/pkg/kubectl/polymorphichelpers/helpers_test.go +++ b/pkg/kubectl/polymorphichelpers/helpers_test.go @@ -29,7 +29,7 @@ import ( "k8s.io/apimachinery/pkg/watch" fakeexternal "k8s.io/client-go/kubernetes/fake" testcore "k8s.io/client-go/testing" - "k8s.io/kubernetes/pkg/controller" + "k8s.io/kubernetes/pkg/kubectl/util/podutils" ) func TestGetFirstPod(t *testing.T) { @@ -48,7 +48,7 @@ func TestGetFirstPod(t *testing.T) { { name: "kubectl logs - two ready pods", podList: newPodList(2, -1, -1, labelSet), - sortBy: func(pods []*corev1.Pod) sort.Interface { return controller.ByLogging(pods) }, + sortBy: func(pods []*corev1.Pod) sort.Interface { return podutils.ByLogging(pods) }, expected: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "pod-1", @@ -70,7 +70,7 @@ func TestGetFirstPod(t *testing.T) { { name: "kubectl logs - one unhealthy, one healthy", podList: newPodList(2, -1, 1, labelSet), - sortBy: func(pods []*corev1.Pod) sort.Interface { return controller.ByLogging(pods) }, + sortBy: func(pods []*corev1.Pod) sort.Interface { return podutils.ByLogging(pods) }, expected: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "pod-2", @@ -93,7 +93,7 @@ func TestGetFirstPod(t *testing.T) { { name: "kubectl attach - two ready pods", podList: newPodList(2, -1, -1, labelSet), - sortBy: func(pods []*corev1.Pod) sort.Interface { return sort.Reverse(controller.ActivePods(pods)) }, + sortBy: func(pods []*corev1.Pod) sort.Interface { return sort.Reverse(podutils.ActivePods(pods)) }, expected: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "pod-1", @@ -136,7 +136,7 @@ func TestGetFirstPod(t *testing.T) { }, }, }, - sortBy: func(pods []*corev1.Pod) sort.Interface { return sort.Reverse(controller.ActivePods(pods)) }, + sortBy: func(pods []*corev1.Pod) sort.Interface { return sort.Reverse(podutils.ActivePods(pods)) }, expected: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "pod-1", diff --git a/pkg/kubectl/polymorphichelpers/logsforobject.go b/pkg/kubectl/polymorphichelpers/logsforobject.go index 069469abd3..85472a135e 100644 --- a/pkg/kubectl/polymorphichelpers/logsforobject.go +++ b/pkg/kubectl/polymorphichelpers/logsforobject.go @@ -29,7 +29,7 @@ import ( "k8s.io/cli-runtime/pkg/genericclioptions" corev1client "k8s.io/client-go/kubernetes/typed/core/v1" "k8s.io/client-go/rest" - "k8s.io/kubernetes/pkg/controller" + "k8s.io/kubernetes/pkg/kubectl/util/podutils" ) func logsForObject(restClientGetter genericclioptions.RESTClientGetter, object, options runtime.Object, timeout time.Duration, allContainers bool) ([]*rest.Request, error) { @@ -99,7 +99,7 @@ func logsForObjectWithClient(clientset corev1client.CoreV1Interface, object, opt return nil, fmt.Errorf("cannot get the logs from %T: %v", object, err) } - sortBy := func(pods []*v1.Pod) sort.Interface { return controller.ByLogging(pods) } + sortBy := func(pods []*v1.Pod) sort.Interface { return podutils.ByLogging(pods) } pod, numPods, err := GetFirstPod(clientset, namespace, selector.String(), timeout, sortBy) if err != nil { return nil, err diff --git a/pkg/kubectl/rolling_updater.go b/pkg/kubectl/rolling_updater.go index 1c750614bb..7b019d7c97 100644 --- a/pkg/kubectl/rolling_updater.go +++ b/pkg/kubectl/rolling_updater.go @@ -35,9 +35,9 @@ import ( scaleclient "k8s.io/client-go/scale" "k8s.io/client-go/util/integer" "k8s.io/client-go/util/retry" - podutil "k8s.io/kubernetes/pkg/api/v1/pod" deploymentutil "k8s.io/kubernetes/pkg/controller/deployment/util" "k8s.io/kubernetes/pkg/kubectl/util" + "k8s.io/kubernetes/pkg/kubectl/util/podutils" ) func newInt32Ptr(val int) *int32 { @@ -443,7 +443,7 @@ func (r *RollingUpdater) readyPods(oldRc, newRc *corev1.ReplicationController, m if v1Pod.DeletionTimestamp != nil { continue } - if !podutil.IsPodAvailable(&v1Pod, minReadySeconds, r.nowFn()) { + if !podutils.IsPodAvailable(&v1Pod, minReadySeconds, r.nowFn()) { continue } switch controller.Name { diff --git a/pkg/kubectl/util/BUILD b/pkg/kubectl/util/BUILD index 794e6e8a7d..f2d073ddb4 100644 --- a/pkg/kubectl/util/BUILD +++ b/pkg/kubectl/util/BUILD @@ -69,6 +69,7 @@ filegroup( "//pkg/kubectl/util/hash:all-srcs", "//pkg/kubectl/util/i18n:all-srcs", "//pkg/kubectl/util/logs:all-srcs", + "//pkg/kubectl/util/podutils:all-srcs", "//pkg/kubectl/util/slice:all-srcs", "//pkg/kubectl/util/term:all-srcs", ], diff --git a/pkg/kubectl/util/podutils/BUILD b/pkg/kubectl/util/podutils/BUILD new file mode 100644 index 0000000000..51522abeca --- /dev/null +++ b/pkg/kubectl/util/podutils/BUILD @@ -0,0 +1,27 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "go_default_library", + srcs = ["podutils.go"], + importpath = "k8s.io/kubernetes/pkg/kubectl/util/podutils", + visibility = ["//visibility:public"], + deps = [ + "//staging/src/k8s.io/api/core/v1:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", + "//staging/src/k8s.io/client-go/util/integer:go_default_library", + ], +) + +filegroup( + name = "package-srcs", + srcs = glob(["**"]), + tags = ["automanaged"], + visibility = ["//visibility:private"], +) + +filegroup( + name = "all-srcs", + srcs = [":package-srcs"], + tags = ["automanaged"], + visibility = ["//visibility:public"], +) diff --git a/pkg/kubectl/util/podutils/podutils.go b/pkg/kubectl/util/podutils/podutils.go new file mode 100644 index 0000000000..2da66ef9c3 --- /dev/null +++ b/pkg/kubectl/util/podutils/podutils.go @@ -0,0 +1,190 @@ +/* +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 podutils + +import ( + "time" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/util/integer" +) + +// IsPodAvailable returns true if a pod is available; false otherwise. +// Precondition for an available pod is that it must be ready. On top +// of that, there are two cases when a pod can be considered available: +// 1. minReadySeconds == 0, or +// 2. LastTransitionTime (is set) + minReadySeconds < current time +func IsPodAvailable(pod *corev1.Pod, minReadySeconds int32, now metav1.Time) bool { + if !IsPodReady(pod) { + return false + } + + c := getPodReadyCondition(pod.Status) + minReadySecondsDuration := time.Duration(minReadySeconds) * time.Second + if minReadySeconds == 0 || !c.LastTransitionTime.IsZero() && c.LastTransitionTime.Add(minReadySecondsDuration).Before(now.Time) { + return true + } + return false +} + +// IsPodReady returns true if a pod is ready; false otherwise. +func IsPodReady(pod *corev1.Pod) bool { + return isPodReadyConditionTrue(pod.Status) +} + +// IsPodReadyConditionTrue returns true if a pod is ready; false otherwise. +func isPodReadyConditionTrue(status corev1.PodStatus) bool { + condition := getPodReadyCondition(status) + return condition != nil && condition.Status == corev1.ConditionTrue +} + +// GetPodReadyCondition extracts the pod ready condition from the given status and returns that. +// Returns nil if the condition is not present. +func getPodReadyCondition(status corev1.PodStatus) *corev1.PodCondition { + _, condition := getPodCondition(&status, corev1.PodReady) + return condition +} + +// GetPodCondition extracts the provided condition from the given status and returns that. +// Returns nil and -1 if the condition is not present, and the index of the located condition. +func getPodCondition(status *corev1.PodStatus, conditionType corev1.PodConditionType) (int, *corev1.PodCondition) { + if status == nil { + return -1, nil + } + return getPodConditionFromList(status.Conditions, conditionType) +} + +// GetPodConditionFromList extracts the provided condition from the given list of condition and +// returns the index of the condition and the condition. Returns -1 and nil if the condition is not present. +func getPodConditionFromList(conditions []corev1.PodCondition, conditionType corev1.PodConditionType) (int, *corev1.PodCondition) { + if conditions == nil { + return -1, nil + } + for i := range conditions { + if conditions[i].Type == conditionType { + return i, &conditions[i] + } + } + return -1, nil +} + +// ByLogging allows custom sorting of pods so the best one can be picked for getting its logs. +type ByLogging []*corev1.Pod + +func (s ByLogging) Len() int { return len(s) } +func (s ByLogging) Swap(i, j int) { s[i], s[j] = s[j], s[i] } + +func (s ByLogging) Less(i, j int) bool { + // 1. assigned < unassigned + if s[i].Spec.NodeName != s[j].Spec.NodeName && (len(s[i].Spec.NodeName) == 0 || len(s[j].Spec.NodeName) == 0) { + return len(s[i].Spec.NodeName) > 0 + } + // 2. PodRunning < PodUnknown < PodPending + m := map[corev1.PodPhase]int{corev1.PodRunning: 0, corev1.PodUnknown: 1, corev1.PodPending: 2} + if m[s[i].Status.Phase] != m[s[j].Status.Phase] { + return m[s[i].Status.Phase] < m[s[j].Status.Phase] + } + // 3. ready < not ready + if IsPodReady(s[i]) != IsPodReady(s[j]) { + return IsPodReady(s[i]) + } + // TODO: take availability into account when we push minReadySeconds information from deployment into pods, + // see https://github.com/kubernetes/kubernetes/issues/22065 + // 4. Been ready for more time < less time < empty time + if IsPodReady(s[i]) && IsPodReady(s[j]) && !podReadyTime(s[i]).Equal(podReadyTime(s[j])) { + return afterOrZero(podReadyTime(s[j]), podReadyTime(s[i])) + } + // 5. Pods with containers with higher restart counts < lower restart counts + if maxContainerRestarts(s[i]) != maxContainerRestarts(s[j]) { + return maxContainerRestarts(s[i]) > maxContainerRestarts(s[j]) + } + // 6. older pods < newer pods < empty timestamp pods + if !s[i].CreationTimestamp.Equal(&s[j].CreationTimestamp) { + return afterOrZero(&s[j].CreationTimestamp, &s[i].CreationTimestamp) + } + return false +} + +// ActivePods type allows custom sorting of pods so a controller can pick the best ones to delete. +type ActivePods []*corev1.Pod + +func (s ActivePods) Len() int { return len(s) } +func (s ActivePods) Swap(i, j int) { s[i], s[j] = s[j], s[i] } + +func (s ActivePods) Less(i, j int) bool { + // 1. Unassigned < assigned + // If only one of the pods is unassigned, the unassigned one is smaller + if s[i].Spec.NodeName != s[j].Spec.NodeName && (len(s[i].Spec.NodeName) == 0 || len(s[j].Spec.NodeName) == 0) { + return len(s[i].Spec.NodeName) == 0 + } + // 2. PodPending < PodUnknown < PodRunning + m := map[corev1.PodPhase]int{corev1.PodPending: 0, corev1.PodUnknown: 1, corev1.PodRunning: 2} + if m[s[i].Status.Phase] != m[s[j].Status.Phase] { + return m[s[i].Status.Phase] < m[s[j].Status.Phase] + } + // 3. Not ready < ready + // If only one of the pods is not ready, the not ready one is smaller + if IsPodReady(s[i]) != IsPodReady(s[j]) { + return !IsPodReady(s[i]) + } + // TODO: take availability into account when we push minReadySeconds information from deployment into pods, + // see https://github.com/kubernetes/kubernetes/issues/22065 + // 4. Been ready for empty time < less time < more time + // If both pods are ready, the latest ready one is smaller + if IsPodReady(s[i]) && IsPodReady(s[j]) && !podReadyTime(s[i]).Equal(podReadyTime(s[j])) { + return afterOrZero(podReadyTime(s[i]), podReadyTime(s[j])) + } + // 5. Pods with containers with higher restart counts < lower restart counts + if maxContainerRestarts(s[i]) != maxContainerRestarts(s[j]) { + return maxContainerRestarts(s[i]) > maxContainerRestarts(s[j]) + } + // 6. Empty creation time pods < newer pods < older pods + if !s[i].CreationTimestamp.Equal(&s[j].CreationTimestamp) { + return afterOrZero(&s[i].CreationTimestamp, &s[j].CreationTimestamp) + } + return false +} + +// afterOrZero checks if time t1 is after time t2; if one of them +// is zero, the zero time is seen as after non-zero time. +func afterOrZero(t1, t2 *metav1.Time) bool { + if t1.Time.IsZero() || t2.Time.IsZero() { + return t1.Time.IsZero() + } + return t1.After(t2.Time) +} + +func podReadyTime(pod *corev1.Pod) *metav1.Time { + if IsPodReady(pod) { + for _, c := range pod.Status.Conditions { + // we only care about pod ready conditions + if c.Type == corev1.PodReady && c.Status == corev1.ConditionTrue { + return &c.LastTransitionTime + } + } + } + return &metav1.Time{} +} + +func maxContainerRestarts(pod *corev1.Pod) int { + maxRestarts := 0 + for _, c := range pod.Status.ContainerStatuses { + maxRestarts = integer.IntMax(maxRestarts, int(c.RestartCount)) + } + return maxRestarts +}