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(", ")}
/>
-
-
+
+