diff --git a/api/pod-handlers.go b/api/pod-handlers.go index 6e1c4c94bbd..1626fa99a65 100644 --- a/api/pod-handlers.go +++ b/api/pod-handlers.go @@ -219,6 +219,10 @@ func getDescribePodResponse(session *models.Principal, params operator_api.Descr if err != nil { return nil, ErrorWithContext(ctx, err) } + return getDescribePod(pod) +} + +func getDescribePod(pod *corev1.Pod) (*models.DescribePodWrapper, *models.Error) { retval := &models.DescribePodWrapper{ Name: pod.Name, Namespace: pod.Namespace, @@ -247,6 +251,8 @@ func getDescribePodResponse(session *models.Principal, params operator_api.Descr retval.Annotations = annotationArray if pod.DeletionTimestamp != nil { retval.DeletionTimestamp = translateTimestampSince(*pod.DeletionTimestamp) + } + if pod.DeletionGracePeriodSeconds != nil { retval.DeletionGracePeriodSeconds = *pod.DeletionGracePeriodSeconds } retval.Phase = string(pod.Status.Phase) @@ -263,34 +269,37 @@ func getDescribePodResponse(session *models.Principal, params operator_api.Descr } for i := range pod.Spec.Containers { + container := pod.Spec.Containers[i] retval.Containers[i] = &models.Container{ - Name: pod.Spec.Containers[i].Name, - Image: pod.Spec.Containers[i].Image, - Ports: describeContainerPorts(pod.Spec.Containers[i].Ports), - HostPorts: describeContainerHostPorts(pod.Spec.Containers[i].Ports), - Args: pod.Spec.Containers[i].Args, + Name: container.Name, + Image: container.Image, + Ports: describeContainerPorts(container.Ports), + HostPorts: describeContainerHostPorts(container.Ports), + Args: container.Args, } - if slices.Contains(statusKeys, pod.Spec.Containers[i].Name) { - retval.Containers[i].ContainerID = statusMap[pod.Spec.Containers[i].Name].ContainerID - retval.Containers[i].ImageID = statusMap[pod.Spec.Containers[i].Name].ImageID - retval.Containers[i].Ready = statusMap[pod.Spec.Containers[i].Name].Ready - retval.Containers[i].RestartCount = int64(statusMap[pod.Spec.Containers[i].Name].RestartCount) - retval.Containers[i].State, retval.Containers[i].LastState = describeStatus(statusMap[pod.Spec.Containers[i].Name]) + if slices.Contains(statusKeys, container.Name) { + containerStatus := statusMap[container.Name] + retval.Containers[i].ContainerID = containerStatus.ContainerID + retval.Containers[i].ImageID = containerStatus.ImageID + retval.Containers[i].Ready = containerStatus.Ready + retval.Containers[i].RestartCount = int64(containerStatus.RestartCount) + retval.Containers[i].State = describeContainerState(containerStatus.State) + retval.Containers[i].LastState = describeContainerState(containerStatus.LastTerminationState) } - retval.Containers[i].EnvironmentVariables = make([]*models.EnvironmentVariable, len(pod.Spec.Containers[i].Env)) - for j := range pod.Spec.Containers[i].Env { + retval.Containers[i].EnvironmentVariables = make([]*models.EnvironmentVariable, len(container.Env)) + for j := range container.Env { retval.Containers[i].EnvironmentVariables[j] = &models.EnvironmentVariable{ - Key: pod.Spec.Containers[i].Env[j].Name, - Value: pod.Spec.Containers[i].Env[j].Value, + Key: container.Env[j].Name, + Value: container.Env[j].Value, } } - retval.Containers[i].Mounts = make([]*models.Mount, len(pod.Spec.Containers[i].VolumeMounts)) - for j := range pod.Spec.Containers[i].VolumeMounts { + retval.Containers[i].Mounts = make([]*models.Mount, len(container.VolumeMounts)) + for j := range container.VolumeMounts { retval.Containers[i].Mounts[j] = &models.Mount{ - Name: pod.Spec.Containers[i].VolumeMounts[j].Name, - MountPath: pod.Spec.Containers[i].VolumeMounts[j].MountPath, - SubPath: pod.Spec.Containers[i].VolumeMounts[j].SubPath, - ReadOnly: pod.Spec.Containers[i].VolumeMounts[j].ReadOnly, + Name: container.VolumeMounts[j].Name, + MountPath: container.VolumeMounts[j].MountPath, + SubPath: container.VolumeMounts[j].SubPath, + ReadOnly: container.VolumeMounts[j].ReadOnly, } } } @@ -332,7 +341,11 @@ func getDescribePodResponse(session *models.Principal, params operator_api.Descr } } if pod.Spec.Volumes[i].Projected.Sources[j].ServiceAccountToken != nil { - retval.Volumes[i].Projected.Sources[j].ServiceAccountToken = &models.ServiceAccountToken{ExpirationSeconds: *pod.Spec.Volumes[i].Projected.Sources[j].ServiceAccountToken.ExpirationSeconds} + if pod.Spec.Volumes[i].Projected.Sources[j].ServiceAccountToken.ExpirationSeconds != nil { + retval.Volumes[i].Projected.Sources[j].ServiceAccountToken = &models.ServiceAccountToken{ + ExpirationSeconds: *pod.Spec.Volumes[i].Projected.Sources[j].ServiceAccountToken.ExpirationSeconds, + } + } } } } @@ -358,46 +371,28 @@ func getDescribePodResponse(session *models.Principal, params operator_api.Descr return retval, nil } -func describeStatus(status corev1.ContainerStatus) (*models.State, *models.State) { +func describeContainerState(status corev1.ContainerState) *models.State { retval := &models.State{} - last := &models.State{} - state := status.State - lastState := status.LastTerminationState switch { - case state.Running != nil: + case status.Running != nil: retval.State = "Running" - retval.Started = state.Running.StartedAt.Time.Format(time.RFC1123Z) - case state.Waiting != nil: + retval.Started = status.Running.StartedAt.Time.Format(time.RFC1123Z) + case status.Waiting != nil: retval.State = "Waiting" - retval.Reason = state.Waiting.Reason - case state.Terminated != nil: + retval.Reason = status.Waiting.Reason + retval.Message = status.Waiting.Message + case status.Terminated != nil: retval.State = "Terminated" - retval.Message = state.Terminated.Message - retval.ExitCode = int64(state.Terminated.ExitCode) - retval.Signal = int64(state.Terminated.Signal) - retval.Started = state.Terminated.StartedAt.Time.Format(time.RFC1123Z) - retval.Finished = state.Terminated.FinishedAt.Time.Format(time.RFC1123Z) - switch { - case lastState.Running != nil: - last.State = "Running" - last.Started = lastState.Running.StartedAt.Time.Format(time.RFC1123Z) - case lastState.Waiting != nil: - last.State = "Waiting" - last.Reason = lastState.Waiting.Reason - case lastState.Terminated != nil: - last.State = "Terminated" - last.Message = lastState.Terminated.Message - last.ExitCode = int64(lastState.Terminated.ExitCode) - last.Signal = int64(lastState.Terminated.Signal) - last.Started = lastState.Terminated.StartedAt.Time.Format(time.RFC1123Z) - last.Finished = lastState.Terminated.FinishedAt.Time.Format(time.RFC1123Z) - default: - last.State = "Waiting" - } + retval.Message = status.Terminated.Message + retval.Reason = status.Terminated.Reason + retval.ExitCode = int64(status.Terminated.ExitCode) + retval.Signal = int64(status.Terminated.Signal) + retval.Started = status.Terminated.StartedAt.Time.Format(time.RFC1123Z) + retval.Finished = status.Terminated.FinishedAt.Time.Format(time.RFC1123Z) default: retval.State = "Waiting" } - return retval, last + return retval } func describeContainerPorts(cPorts []corev1.ContainerPort) []string { @@ -416,6 +411,7 @@ func describeContainerHostPorts(cPorts []corev1.ContainerPort) []string { return ports } +// getPodQOS gets Pod's Quality of Service Class func getPodQOS(pod *corev1.Pod) corev1.PodQOSClass { requests := corev1.ResourceList{} limits := corev1.ResourceList{} diff --git a/api/pod-handlers_test.go b/api/pod-handlers_test.go index efdc0ce6531..d4551d71f6f 100644 --- a/api/pod-handlers_test.go +++ b/api/pod-handlers_test.go @@ -20,11 +20,14 @@ import ( "net/http" "time" + "github.com/go-openapi/swag" + "github.com/minio/operator/api/operations" "github.com/minio/operator/api/operations/operator_api" "github.com/minio/operator/models" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/duration" ) func (suite *TenantTestSuite) TestDeletePodHandlerWithoutError() { @@ -121,3 +124,431 @@ func (suite *TenantTestSuite) initDescribePodRequest() (params operator_api.Desc params.PodName = "mock-pod" return params, api } + +func (suite *TenantTestSuite) TestGetDescribePodBuildsResponseFromPodInfo() { + mockTime := time.Date(2023, 4, 25, 14, 30, 45, 100, time.UTC) + mockContainerOne := corev1.Container{ + Name: "c1", + Image: "c1-image", + Ports: []corev1.ContainerPort{ + { + Name: "c1-port-1", + HostPort: int32(9000), + ContainerPort: int32(8080), + Protocol: corev1.ProtocolTCP, + }, + { + Name: "c1-port-2", + HostPort: int32(3000), + ContainerPort: int32(7070), + Protocol: corev1.ProtocolUDP, + }, + }, + Args: []string{"c1-arg1", "c1-arg2"}, + Env: []corev1.EnvVar{ + { + Name: "c1-env-var1", + Value: "c1-env-value1", + }, + { + Name: "c1-env-var2", + Value: "c1-env-value2", + }, + }, + VolumeMounts: []corev1.VolumeMount{ + { + Name: "c1-mount1", + MountPath: "c1-mount1-path", + ReadOnly: true, + SubPath: "c1-mount1-subpath", + }, + { + Name: "c1-mount2", + MountPath: "c1-mount2-path", + ReadOnly: true, + SubPath: "c1-mount2-subpath", + }, + }, + } + mockContainerTwo := corev1.Container{ + Name: "c2", + Image: "c2-image", + Ports: []corev1.ContainerPort{ + { + Name: "c2-port-1", + HostPort: int32(9000), + ContainerPort: int32(8080), + Protocol: corev1.ProtocolTCP, + }, + { + Name: "c2-port-2", + HostPort: int32(3000), + ContainerPort: int32(7070), + Protocol: corev1.ProtocolUDP, + }, + }, + Args: []string{"c2-arg1", "c2-arg2"}, + Env: []corev1.EnvVar{ + { + Name: "c2-env-var1", + Value: "c2-env-value1", + }, + { + Name: "c2-env-var2", + Value: "c2-env-value2", + }, + }, + VolumeMounts: []corev1.VolumeMount{ + { + Name: "c2-mount1", + MountPath: "c2-mount1-path", + ReadOnly: true, + SubPath: "c2-mount1-subpath", + }, + { + Name: "c2-mount2", + MountPath: "c2-mount2-path", + ReadOnly: true, + SubPath: "c2-mount2-subpath", + }, + }, + } + mockPodInfo := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "mock-pod", + Namespace: "mock-namespace", + Labels: map[string]string{"Key1": "Val1", "Key2": "Val2"}, + Annotations: map[string]string{"Annotation1": "Annotation1Val1", "Annotation2": "Annotation1Val2"}, + DeletionTimestamp: &metav1.Time{Time: mockTime}, + DeletionGracePeriodSeconds: swag.Int64(60), + OwnerReferences: []metav1.OwnerReference{ + { + APIVersion: "v1", + Kind: "ReferenceKind", + Name: "ReferenceName", + Controller: swag.Bool(true), + }, + }, + }, + Spec: corev1.PodSpec{ + PriorityClassName: "mock-priority-class", + NodeName: "mock-node", + Priority: swag.Int32(10), + Containers: []corev1.Container{mockContainerOne, mockContainerTwo}, + Volumes: []corev1.Volume{ + { + Name: "v1", + VolumeSource: corev1.VolumeSource{ + PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ + ClaimName: "v1-pvc", + ReadOnly: true, + }, + }, + }, + { + Name: "v1", + VolumeSource: corev1.VolumeSource{ + Projected: &corev1.ProjectedVolumeSource{ + Sources: []corev1.VolumeProjection{ + { + Secret: &corev1.SecretProjection{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "v1-vs-secret1", + }, + }, + DownwardAPI: &corev1.DownwardAPIProjection{ + Items: []corev1.DownwardAPIVolumeFile{}, + }, + ConfigMap: &corev1.ConfigMapProjection{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "v1-vs-configmap1", + }, + }, + ServiceAccountToken: &corev1.ServiceAccountTokenProjection{ + ExpirationSeconds: swag.Int64(1000), + }, + }, + }, + DefaultMode: swag.Int32(511), + }, + }, + }, + }, + NodeSelector: map[string]string{ + "p1-ns-key1": "p1-ns-val1", + "p1-ns-key2": "p1-ns-val2", + }, + Tolerations: []corev1.Toleration{ + { + Key: "p1-t1-key", + Operator: corev1.TolerationOpExists, + Value: "p1-t1-val", + Effect: corev1.TaintEffectNoSchedule, + TolerationSeconds: swag.Int64(60), + }, + { + Key: "p1-t2-key", + Operator: corev1.TolerationOpEqual, + Value: "p1-t2-val", + Effect: corev1.TaintEffectPreferNoSchedule, + TolerationSeconds: swag.Int64(70), + }, + }, + }, + Status: corev1.PodStatus{ + StartTime: &metav1.Time{Time: mockTime}, + ContainerStatuses: []corev1.ContainerStatus{ + { + Name: "c1", + ContainerID: "c1-id", + ImageID: "c1-image-id", + Ready: true, + RestartCount: int32(3), + State: corev1.ContainerState{ + Running: &corev1.ContainerStateRunning{ + StartedAt: metav1.Time{Time: mockTime}, + }, + }, + LastTerminationState: corev1.ContainerState{ + Waiting: &corev1.ContainerStateWaiting{ + Reason: "c1-some-reason", + Message: "c1-some-message", + }, + }, + }, + { + Name: "c2", + ContainerID: "c2-id", + ImageID: "c2-image-id", + Ready: true, + RestartCount: int32(3), + State: corev1.ContainerState{ + Running: &corev1.ContainerStateRunning{ + StartedAt: metav1.Time{Time: mockTime}, + }, + }, + LastTerminationState: corev1.ContainerState{ + Terminated: &corev1.ContainerStateTerminated{ + Reason: "c2-some-reason", + Message: "c2-some-message", + ExitCode: int32(4), + Signal: int32(1), + StartedAt: metav1.Time{Time: mockTime}, + FinishedAt: metav1.Time{Time: mockTime}, + ContainerID: "c2-id", + }, + }, + }, + }, + Phase: corev1.PodPhase("phase"), + Reason: "StatusReason", + Message: "StatusMessage", + PodIP: "192.1.2.3", + Conditions: []corev1.PodCondition{ + { + Type: corev1.ContainersReady, + Status: corev1.ConditionTrue, + }, + { + Type: corev1.PodInitialized, + Status: corev1.ConditionFalse, + }, + }, + }, + } + + res, err := getDescribePod(mockPodInfo) + + suite.assert.NotNil(res) + suite.assert.Nil(err) + suite.assert.Equal(mockPodInfo.Name, res.Name) + suite.assert.Equal(mockPodInfo.Namespace, res.Namespace) + suite.assert.Equal(mockPodInfo.Spec.PriorityClassName, res.PriorityClassName) + suite.assert.Equal(mockPodInfo.Spec.NodeName, res.NodeName) + suite.assert.Equal(int64(10), res.Priority) + suite.assert.Equal(mockPodInfo.Status.StartTime.Time.String(), res.StartTime) + suite.assert.Contains(res.Labels, &models.Label{Key: "Key1", Value: "Val1"}) + suite.assert.Contains(res.Labels, &models.Label{Key: "Key2", Value: "Val2"}) + suite.assert.Contains(res.Annotations, &models.Annotation{Key: "Annotation1", Value: "Annotation1Val1"}) + suite.assert.Contains(res.Annotations, &models.Annotation{Key: "Annotation2", Value: "Annotation1Val2"}) + suite.assert.Equal(duration.HumanDuration(time.Since(mockTime)), res.DeletionTimestamp) + suite.assert.Equal(int64(60), res.DeletionGracePeriodSeconds) + suite.assert.Equal("phase", res.Phase) + suite.assert.Equal("StatusReason", res.Reason) + suite.assert.Equal("StatusMessage", res.Message) + suite.assert.Equal("192.1.2.3", res.PodIP) + suite.assert.Equal("&OwnerReference{Kind:ReferenceKind,Name:ReferenceName,UID:,APIVersion:v1,Controller:*true,BlockOwnerDeletion:nil,}", res.ControllerRef) + suite.assert.Equal([]*models.Container{ + { + Name: "c1", + Image: "c1-image", + Ports: []string{"8080/TCP", "7070/UDP"}, + HostPorts: []string{"9000/TCP", "3000/UDP"}, + Args: []string{"c1-arg1", "c1-arg2"}, + ContainerID: "c1-id", + ImageID: "c1-image-id", + Ready: true, + RestartCount: int64(3), + State: &models.State{ + ExitCode: int64(0), + Finished: "", + Message: "", + Reason: "", + Signal: int64(0), + Started: "Tue, 25 Apr 2023 14:30:45 +0000", + State: "Running", + }, + LastState: &models.State{ + ExitCode: int64(0), + Finished: "", + Message: "c1-some-message", + Reason: "c1-some-reason", + Signal: int64(0), + Started: "", + State: "Waiting", + }, + EnvironmentVariables: []*models.EnvironmentVariable{ + { + Key: "c1-env-var1", + Value: "c1-env-value1", + }, + { + Key: "c1-env-var2", + Value: "c1-env-value2", + }, + }, + Mounts: []*models.Mount{ + { + Name: "c1-mount1", + MountPath: "c1-mount1-path", + ReadOnly: true, + SubPath: "c1-mount1-subpath", + }, + { + Name: "c1-mount2", + MountPath: "c1-mount2-path", + ReadOnly: true, + SubPath: "c1-mount2-subpath", + }, + }, + }, + { + Name: "c2", + Image: "c2-image", + Ports: []string{"8080/TCP", "7070/UDP"}, + HostPorts: []string{"9000/TCP", "3000/UDP"}, + Args: []string{"c2-arg1", "c2-arg2"}, + ContainerID: "c2-id", + ImageID: "c2-image-id", + Ready: true, + RestartCount: int64(3), + State: &models.State{ + ExitCode: int64(0), + Finished: "", + Message: "", + Reason: "", + Signal: int64(0), + Started: "Tue, 25 Apr 2023 14:30:45 +0000", + State: "Running", + }, + LastState: &models.State{ + ExitCode: int64(4), + Finished: "Tue, 25 Apr 2023 14:30:45 +0000", + Message: "c2-some-message", + Reason: "c2-some-reason", + Signal: int64(1), + Started: "Tue, 25 Apr 2023 14:30:45 +0000", + State: "Terminated", + }, + EnvironmentVariables: []*models.EnvironmentVariable{ + { + Key: "c2-env-var1", + Value: "c2-env-value1", + }, + { + Key: "c2-env-var2", + Value: "c2-env-value2", + }, + }, + Mounts: []*models.Mount{ + { + Name: "c2-mount1", + MountPath: "c2-mount1-path", + ReadOnly: true, + SubPath: "c2-mount1-subpath", + }, + { + Name: "c2-mount2", + MountPath: "c2-mount2-path", + ReadOnly: true, + SubPath: "c2-mount2-subpath", + }, + }, + }, + }, res.Containers) + suite.assert.Equal([]*models.Condition{ + { + Type: "ContainersReady", + Status: "True", + }, + { + Type: "Initialized", + Status: "False", + }, + }, res.Conditions) + suite.assert.Equal([]*models.Volume{ + { + Name: "v1", + Pvc: &models.Pvc{ + ClaimName: "v1-pvc", + ReadOnly: true, + }, + }, + { + Name: "v1", + Projected: &models.ProjectedVolume{ + Sources: []*models.ProjectedVolumeSource{ + { + Secret: &models.Secret{ + Name: "v1-vs-secret1", + Optional: false, + }, + DownwardAPI: true, + ConfigMap: &models.ConfigMap{ + Name: "v1-vs-configmap1", + Optional: false, + }, + ServiceAccountToken: &models.ServiceAccountToken{ + ExpirationSeconds: int64(1000), + }, + }, + }, + }, + }, + }, res.Volumes) + suite.assert.Equal("BestEffort", res.QosClass) + suite.assert.Contains(res.NodeSelector, &models.NodeSelector{ + Key: "p1-ns-key1", + Value: "p1-ns-val1", + }) + suite.assert.Contains(res.NodeSelector, &models.NodeSelector{ + Key: "p1-ns-key2", + Value: "p1-ns-val2", + }) + suite.assert.Equal([]*models.Toleration{ + { + Key: "p1-t1-key", + Operator: "Exists", + Value: "p1-t1-val", + Effect: "NoSchedule", + TolerationSeconds: int64(60), + }, + { + Key: "p1-t2-key", + Operator: "Equal", + Value: "p1-t2-val", + Effect: "PreferNoSchedule", + TolerationSeconds: int64(70), + }, + }, res.Tolerations) +} diff --git a/web-app/src/screens/Console/Tenants/TenantDetails/pods/PodDescribe.tsx b/web-app/src/screens/Console/Tenants/TenantDetails/pods/PodDescribe.tsx index 5184a4a9067..9dd6395220f 100644 --- a/web-app/src/screens/Console/Tenants/TenantDetails/pods/PodDescribe.tsx +++ b/web-app/src/screens/Console/Tenants/TenantDetails/pods/PodDescribe.tsx @@ -346,8 +346,11 @@ const PodDescribeContainers = ({ containers }: IPodDescribeContainersProps) => { label={"Arguments"} value={container.args.join(", ")} /> - - + +