diff --git a/src/app/backend/resource/pod/common.go b/src/app/backend/resource/pod/common.go index e247a547fd29d7c655f3c81685fdc5bfdab56276..fdd981888ffe827597ab1f9fe6559a8eaf8fb300 100644 --- a/src/app/backend/resource/pod/common.go +++ b/src/app/backend/resource/pod/common.go @@ -15,12 +15,15 @@ package pod import ( + "fmt" + + v1 "k8s.io/api/core/v1" + "github.com/kubernetes/dashboard/src/app/backend/api" metricapi "github.com/kubernetes/dashboard/src/app/backend/integration/metric/api" "github.com/kubernetes/dashboard/src/app/backend/resource/common" "github.com/kubernetes/dashboard/src/app/backend/resource/dataselect" "github.com/kubernetes/dashboard/src/app/backend/resource/event" - v1 "k8s.io/api/core/v1" ) // getRestartCount return the restart count of given pod (total number of its containers restarts). @@ -32,18 +35,98 @@ func getRestartCount(pod v1.Pod) int32 { return restartCount } -// getPodStatus returns a PodStatus object containing a summary of the pod's status. -func getPodStatus(pod v1.Pod, warnings []common.Event) PodStatus { - var states []v1.ContainerState - for _, containerStatus := range pod.Status.ContainerStatuses { - states = append(states, containerStatus.State) +// getPodStatus returns status string calculated based on the same logic as kubectl +// Base code: https://github.com/kubernetes/kubernetes/blob/master/pkg/printers/internalversion/printers.go#L734 +func getPodStatus(pod v1.Pod) string { + restarts := 0 + readyContainers := 0 + + reason := string(pod.Status.Phase) + if pod.Status.Reason != "" { + reason = pod.Status.Reason } - return PodStatus{ - Status: string(getPodStatusPhase(pod, warnings)), - PodPhase: pod.Status.Phase, - ContainerStates: states, + initializing := false + for i := range pod.Status.InitContainerStatuses { + container := pod.Status.InitContainerStatuses[i] + restarts += int(container.RestartCount) + switch { + case container.State.Terminated != nil && container.State.Terminated.ExitCode == 0: + continue + case container.State.Terminated != nil: + // initialization is failed + if len(container.State.Terminated.Reason) == 0 { + if container.State.Terminated.Signal != 0 { + reason = fmt.Sprintf("Init: Signal %d", container.State.Terminated.Signal) + } else { + reason = fmt.Sprintf("Init: ExitCode %d", container.State.Terminated.ExitCode) + } + } else { + reason = "Init:" + container.State.Terminated.Reason + } + initializing = true + case container.State.Waiting != nil && len(container.State.Waiting.Reason) > 0 && container.State.Waiting.Reason != "PodInitializing": + reason = fmt.Sprintf("Init: %s", container.State.Waiting.Reason) + initializing = true + default: + reason = fmt.Sprintf("Init: %d/%d", i, len(pod.Spec.InitContainers)) + initializing = true + } + break + } + if !initializing { + restarts = 0 + hasRunning := false + for i := len(pod.Status.ContainerStatuses) - 1; i >= 0; i-- { + container := pod.Status.ContainerStatuses[i] + + restarts += int(container.RestartCount) + if container.State.Waiting != nil && container.State.Waiting.Reason != "" { + reason = container.State.Waiting.Reason + } else if container.State.Terminated != nil && container.State.Terminated.Reason != "" { + reason = container.State.Terminated.Reason + } else if container.State.Terminated != nil && container.State.Terminated.Reason == "" { + if container.State.Terminated.Signal != 0 { + reason = fmt.Sprintf("Signal: %d", container.State.Terminated.Signal) + } else { + reason = fmt.Sprintf("ExitCode: %d", container.State.Terminated.ExitCode) + } + } else if container.Ready && container.State.Running != nil { + hasRunning = true + readyContainers++ + } + } + + // change pod status back to "Running" if there is at least one container still reporting as "Running" status + if reason == "Completed" && hasRunning { + if hasPodReadyCondition(pod.Status.Conditions) { + reason = string(v1.PodRunning) + } else { + reason = "NotReady" + } + } + } + + if pod.DeletionTimestamp != nil && pod.Status.Reason == "NodeLost" { + reason = string(v1.PodUnknown) + } else if pod.DeletionTimestamp != nil { + reason = "Terminating" + } + + if len(reason) == 0 { + reason = string(v1.PodUnknown) + } + + return reason +} + +func hasPodReadyCondition(conditions []v1.PodCondition) bool { + for _, condition := range conditions { + if condition.Type == v1.PodReady && condition.Status == v1.ConditionTrue { + return true + } } + return false } // getPodStatusPhase returns one of four pod status phases (Pending, Running, Succeeded, Failed, Unknown, Terminating) diff --git a/src/app/backend/resource/pod/common_test.go b/src/app/backend/resource/pod/common_test.go index c5af25c0c8b943613aec38516c8ddfe0f0b004a6..6f5f8c6798181b53f7a0c586efe468596855cc80 100644 --- a/src/app/backend/resource/pod/common_test.go +++ b/src/app/backend/resource/pod/common_test.go @@ -40,10 +40,7 @@ func TestToPodPodStatusFailed(t *testing.T) { expected := Pod{ TypeMeta: api.TypeMeta{Kind: api.ResourceKindPod}, - PodStatus: PodStatus{ - Status: string(v1.PodFailed), - PodPhase: v1.PodFailed, - }, + Status: string(v1.PodFailed), Warnings: []common.Event{}, } @@ -70,10 +67,7 @@ func TestToPodPodStatusSucceeded(t *testing.T) { expected := Pod{ TypeMeta: api.TypeMeta{Kind: api.ResourceKindPod}, - PodStatus: PodStatus{ - Status: string(v1.PodSucceeded), - PodPhase: v1.PodSucceeded, - }, + Status: string(v1.PodSucceeded), Warnings: []common.Event{}, } @@ -104,10 +98,7 @@ func TestToPodPodStatusRunning(t *testing.T) { expected := Pod{ TypeMeta: api.TypeMeta{Kind: api.ResourceKindPod}, - PodStatus: PodStatus{ - Status: string(v1.PodRunning), - PodPhase: v1.PodRunning, - }, + Status: string(v1.PodRunning), Warnings: []common.Event{}, } @@ -134,10 +125,7 @@ func TestToPodPodStatusPending(t *testing.T) { expected := Pod{ TypeMeta: api.TypeMeta{Kind: api.ResourceKindPod}, - PodStatus: PodStatus{ - Status: string(v1.PodPending), - PodPhase: v1.PodPending, - }, + Status: string(v1.PodPending), Warnings: []common.Event{}, } @@ -157,14 +145,14 @@ func TestToPodContainerStates(t *testing.T) { { State: v1.ContainerState{ Terminated: &v1.ContainerStateTerminated{ - Reason: "Terminated Test Reason", + Reason: "Terminated", }, }, }, { State: v1.ContainerState{ Waiting: &v1.ContainerStateWaiting{ - Reason: "Waiting Test Reason", + Reason: "Waiting", }, }, }, @@ -174,22 +162,7 @@ func TestToPodContainerStates(t *testing.T) { expected := Pod{ TypeMeta: api.TypeMeta{Kind: api.ResourceKindPod}, - PodStatus: PodStatus{ - PodPhase: v1.PodRunning, - Status: string(v1.PodPending), - ContainerStates: []v1.ContainerState{ - { - Terminated: &v1.ContainerStateTerminated{ - Reason: "Terminated Test Reason", - }, - }, - { - Waiting: &v1.ContainerStateWaiting{ - Reason: "Waiting Test Reason", - }, - }, - }, - }, + Status: "Terminated", Warnings: []common.Event{}, } @@ -211,9 +184,7 @@ func TestToPod(t *testing.T) { pod: &v1.Pod{}, metrics: &MetricsByPod{}, expected: Pod{ TypeMeta: api.TypeMeta{Kind: api.ResourceKindPod}, - PodStatus: PodStatus{ - Status: string(v1.PodPending), - }, + Status: string(v1.PodUnknown), Warnings: []common.Event{}, }, }, { @@ -228,9 +199,7 @@ func TestToPod(t *testing.T) { Name: "test-pod", Namespace: "test-namespace", }, - PodStatus: PodStatus{ - Status: string(v1.PodPending), - }, + Status: string(v1.PodUnknown), Warnings: []common.Event{}, }, }, diff --git a/src/app/backend/resource/pod/detail.go b/src/app/backend/resource/pod/detail.go index ffc33ac67a8f51e2df79ac5be24bdc1dd04c9b54..374c1b7ab214e56c27451a866a4c46c6a42ee7da 100644 --- a/src/app/backend/resource/pod/detail.go +++ b/src/app/backend/resource/pod/detail.go @@ -24,6 +24,12 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" + v1 "k8s.io/api/core/v1" + res "k8s.io/apimachinery/pkg/api/resource" + metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/kubernetes" + "github.com/kubernetes/dashboard/src/app/backend/api" errorHandler "github.com/kubernetes/dashboard/src/app/backend/errors" metricapi "github.com/kubernetes/dashboard/src/app/backend/integration/metric/api" @@ -31,18 +37,13 @@ import ( "github.com/kubernetes/dashboard/src/app/backend/resource/controller" "github.com/kubernetes/dashboard/src/app/backend/resource/dataselect" "github.com/kubernetes/dashboard/src/app/backend/resource/persistentvolumeclaim" - v1 "k8s.io/api/core/v1" - res "k8s.io/apimachinery/pkg/api/resource" - metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/client-go/kubernetes" ) // PodDetail is a presentation layer view of Kubernetes Pod resource. type PodDetail struct { ObjectMeta api.ObjectMeta `json:"objectMeta"` TypeMeta api.TypeMeta `json:"typeMeta"` - PodPhase v1.PodPhase `json:"podPhase"` + PodPhase string `json:"podPhase"` PodIP string `json:"podIP"` NodeName string `json:"nodeName"` RestartCount int32 `json:"restartCount"` @@ -213,7 +214,7 @@ func toPodDetail(pod *v1.Pod, metrics []metricapi.Metric, configMaps *v1.ConfigM return PodDetail{ ObjectMeta: api.NewObjectMeta(pod.ObjectMeta), TypeMeta: api.NewTypeMeta(api.ResourceKindPod), - PodPhase: pod.Status.Phase, + PodPhase: getPodStatus(*pod), PodIP: pod.Status.PodIP, RestartCount: getRestartCount(*pod), QOSClass: string(pod.Status.QOSClass), diff --git a/src/app/backend/resource/pod/detail_test.go b/src/app/backend/resource/pod/detail_test.go index 3c4b1ecd1ea3da265db85962dd8f9322a686cc7e..5408b8ec75f3f0798049edec65df37da17769678 100644 --- a/src/app/backend/resource/pod/detail_test.go +++ b/src/app/backend/resource/pod/detail_test.go @@ -43,6 +43,7 @@ func TestGetPodDetail(t *testing.T) { }}}}, expected: &PodDetail{ TypeMeta: api.TypeMeta{Kind: api.ResourceKindPod}, + PodPhase: string(v1.PodUnknown), ObjectMeta: api.ObjectMeta{ Name: "test-pod", Namespace: "test-namespace", diff --git a/src/app/backend/resource/pod/list.go b/src/app/backend/resource/pod/list.go index 63c7911e0087df512866aadf4fecbae96cd09fc6..782981d6d9c3aa788013dc2e5d7358e75f9a2aed 100644 --- a/src/app/backend/resource/pod/list.go +++ b/src/app/backend/resource/pod/list.go @@ -55,10 +55,10 @@ type Pod struct { ObjectMeta api.ObjectMeta `json:"objectMeta"` TypeMeta api.TypeMeta `json:"typeMeta"` - // More info on pod status - PodStatus PodStatus `json:"podStatus"` + // Status determined based on the same logic as kubectl. + Status string `json:"status"` - // Count of containers restarts. + // RestartCount of containers restarts. RestartCount int32 `json:"restartCount"` // Pod metrics. @@ -67,7 +67,7 @@ type Pod struct { // Pod warning events Warnings []common.Event `json:"warnings"` - // Name of the Node this Pod runs on. + // NodeName of the Node this Pod runs on. NodeName string `json:"nodeName"` } @@ -154,7 +154,7 @@ func toPod(pod *v1.Pod, metrics *MetricsByPod, warnings []common.Event) Pod { ObjectMeta: api.NewObjectMeta(pod.ObjectMeta), TypeMeta: api.NewTypeMeta(api.ResourceKindPod), Warnings: warnings, - PodStatus: getPodStatus(*pod, warnings), + Status: getPodStatus(*pod), RestartCount: getRestartCount(*pod), NodeName: pod.Spec.NodeName, } diff --git a/src/app/backend/resource/pod/list_test.go b/src/app/backend/resource/pod/list_test.go index 35439fa97902e737ad889859737bcbbcb04f8dbf..f66e8dc557d5b4091a182c0eaa14f3de16468bca 100644 --- a/src/app/backend/resource/pod/list_test.go +++ b/src/app/backend/resource/pod/list_test.go @@ -102,9 +102,9 @@ func TestGetPodListFromChannels(t *testing.T) { Labels: map[string]string{"key": "value"}, CreationTimestamp: metav1.Unix(111, 222), }, - TypeMeta: api.TypeMeta{Kind: api.ResourceKindPod}, - PodStatus: pod.PodStatus{Status: string(v1.PodPending)}, - Warnings: []common.Event{}, + TypeMeta: api.TypeMeta{Kind: api.ResourceKindPod}, + Status: string(v1.PodUnknown), + Warnings: []common.Event{}, }}, Errors: []error{}, }, diff --git a/src/app/backend/resource/service/pods_test.go b/src/app/backend/resource/service/pods_test.go index 9146f395e070aefd93db4048a68d97155b872144..096093e5a6f64b92797fc57d9cfd0b39e9fe3198 100644 --- a/src/app/backend/resource/service/pods_test.go +++ b/src/app/backend/resource/service/pods_test.go @@ -57,9 +57,9 @@ func TestGetServicePods(t *testing.T) { Name: "pod-1", UID: "test-uid", Namespace: "ns-1"}, - TypeMeta: api.TypeMeta{Kind: api.ResourceKindPod}, - PodStatus: pod.PodStatus{Status: string(v1.PodPending)}, - Warnings: []common.Event{}, + TypeMeta: api.TypeMeta{Kind: api.ResourceKindPod}, + Status: string(v1.PodUnknown), + Warnings: []common.Event{}, }, }, Errors: []error{}, diff --git a/src/app/frontend/_theming.scss b/src/app/frontend/_theming.scss index 2713d7a810553827ee3f8aa3baab6d7be7c1114a..ed6d864ab08fe62ff11eab9b5fa4c682eb157f9d 100644 --- a/src/app/frontend/_theming.scss +++ b/src/app/frontend/_theming.scss @@ -108,6 +108,10 @@ background-color: lighten(map-get($colors, indicator-error), 33%); } + .kd-help { + color: $muted; + } + .kd-muted { color: $muted; } diff --git a/src/app/frontend/common/components/namespace/component.ts b/src/app/frontend/common/components/namespace/component.ts index 8ab07b5f0bee93646fa2ba515e56e32c815d3dd6..b0c17b1cde813e0f380f05ffa55303a34fbb5533 100644 --- a/src/app/frontend/common/components/namespace/component.ts +++ b/src/app/frontend/common/components/namespace/component.ts @@ -150,9 +150,9 @@ export class NamespaceSelectorComponent implements OnInit, OnDestroy { private loadNamespaces_(): void { this.namespaceUpdate_ - .pipe(takeUntil(this.unsubscribe_)) .pipe(startWith({})) .pipe(switchMap(() => this.namespace_.get(this.endpoint_.list()))) + .pipe(takeUntil(this.unsubscribe_)) .subscribe( namespaceList => { this.namespaces = namespaceList.namespaces.map(n => n.objectMeta.name); diff --git a/src/app/frontend/common/components/resourcelist/pod/component.ts b/src/app/frontend/common/components/resourcelist/pod/component.ts index dbad0b837487a39cf6145b2aa2d6b100ac72f2b5..3da5932f2f5eabbf74ad868103bb53267f7862b9 100644 --- a/src/app/frontend/common/components/resourcelist/pod/component.ts +++ b/src/app/frontend/common/components/resourcelist/pod/component.ts @@ -16,7 +16,6 @@ import {HttpParams} from '@angular/common/http'; import {ChangeDetectionStrategy, ChangeDetectorRef, Component, Input} from '@angular/core'; import {Event, Metric, Pod, PodList} from '@api/backendapi'; import {Observable} from 'rxjs'; - import {ResourceListWithStatuses} from '../../../resources/list'; import {NotificationsService} from '../../../services/global/notifications'; import {EndpointManager, Resource} from '../../../services/resource/endpoint'; @@ -24,6 +23,19 @@ import {NamespacedResourceService} from '../../../services/resource/resource'; import {MenuComponent} from '../../list/column/menu/component'; import {ListGroupIdentifier, ListIdentifier} from '../groupids'; +enum Status { + Pending = 'Pending', + ContainerCreating = 'ContainerCreating', + Running = 'Running', + Succeeded = 'Succeeded', + Completed = 'Completed', + Failed = 'Failed', + Unknown = 'Unknown', + NotReady = 'NotReady', + Terminating = 'Terminating', + Error = 'Error', +} + @Component({ selector: 'kd-pod-list', templateUrl: './template.html', @@ -65,23 +77,21 @@ export class PodListComponent extends ResourceListWithStatuses { } isInErrorState(resource: Pod): boolean { - return resource.podStatus.status === 'Failed'; + return ( + [Status.Failed, Status.Error].some(s => resource.status === s) || + (resource.warnings.length > 0 && + ![Status.Pending, Status.NotReady, Status.Terminating, Status.Unknown, Status.ContainerCreating].some( + s => resource.status === s + )) + ); } isInPendingState(resource: Pod): boolean { - return resource.podStatus.status === 'Pending'; + return [Status.Pending, Status.ContainerCreating].some(s => resource.status === s); } isInSuccessState(resource: Pod): boolean { - return resource.podStatus.status === 'Succeeded' || resource.podStatus.status === 'Running'; - } - - protected getDisplayColumns(): string[] { - return ['statusicon', 'name', 'labels', 'node', 'status', 'restarts', 'cpu', 'mem', 'created']; - } - - private shouldShowNamespaceColumn_(): boolean { - return this.namespaceService_.areMultipleNamespacesSelected(); + return [Status.Succeeded, Status.Running, Status.Completed].some(s => resource.status === s); } hasErrors(pod: Pod): boolean { @@ -93,43 +103,14 @@ export class PodListComponent extends ResourceListWithStatuses { } getDisplayStatus(pod: Pod): string { - // See kubectl printers.go for logic in kubectl: - // https://github.com/kubernetes/kubernetes/blob/39857f486511bd8db81868185674e8b674b1aeb9/pkg/printers/internalversion/printers.go - let msgState = 'running'; - let reason = undefined; - - // Init container statuses are currently not taken into account. - // However, init containers with errors will still show as failed because of warnings. - if (pod.podStatus.containerStates) { - // Container states array may be null when no containers have started yet. - for (let i = pod.podStatus.containerStates.length - 1; i >= 0; i--) { - const state = pod.podStatus.containerStates[i]; - if (state.waiting) { - msgState = 'waiting'; - reason = state.waiting.reason; - } - if (state.terminated) { - msgState = 'terminated'; - reason = state.terminated.reason; - if (!reason) { - if (state.terminated.signal) { - reason = `Signal:${state.terminated.signal}`; - } else { - reason = `ExitCode:${state.terminated.exitCode}`; - } - } - } - } - } - - if (msgState === 'waiting') { - return `Waiting: ${reason}`; - } - - if (msgState === 'terminated') { - return `Terminated: ${reason}`; - } - - return pod.podStatus.podPhase; + return pod.status; + } + + protected getDisplayColumns(): string[] { + return ['statusicon', 'name', 'labels', 'node', 'status', 'restarts', 'cpu', 'mem', 'created']; + } + + private shouldShowNamespaceColumn_(): boolean { + return this.namespaceService_.areMultipleNamespacesSelected(); } } diff --git a/src/app/frontend/common/resources/list.ts b/src/app/frontend/common/resources/list.ts index 91e80ddce44e6fa239758d51fef1126d83f2ae44..618dbe23c65b9dad8a87cdf28c96ecb3a590d197 100644 --- a/src/app/frontend/common/resources/list.ts +++ b/src/app/frontend/common/resources/list.ts @@ -366,7 +366,7 @@ export abstract class ResourceListWithStatuses