diff --git a/common/common.go b/common/common.go index d2e47aa5b1607..5523b30e8193d 100644 --- a/common/common.go +++ b/common/common.go @@ -93,6 +93,12 @@ const ( PluginConfigFileName = "plugin.yaml" ) +// consts for podrequests metrics in cache/info +const ( + PodRequestsCPU = "Requests (CPU)" + PodRequestsMEM = "Requests (MEM)" +) + // Argo CD application related constants const ( diff --git a/controller/cache/info.go b/controller/cache/info.go index 7260487af859f..9dc713a05278b 100644 --- a/controller/cache/info.go +++ b/controller/cache/info.go @@ -446,6 +446,8 @@ func populatePodInfo(un *unstructured.Unstructured, res *ResourceInfo) { } req, _ := resourcehelper.PodRequestsAndLimits(&pod) + CPUReq, MemoryReq := req[v1.ResourceCPU], req[v1.ResourceMemory] + res.PodInfo = &PodInfo{NodeName: pod.Spec.NodeName, ResourceRequests: req, Phase: pod.Status.Phase} res.Info = append(res.Info, v1alpha1.InfoItem{Name: "Node", Value: pod.Spec.NodeName}) @@ -454,6 +456,13 @@ func populatePodInfo(un *unstructured.Unstructured, res *ResourceInfo) { res.Info = append(res.Info, v1alpha1.InfoItem{Name: "Restart Count", Value: fmt.Sprintf("%d", restarts)}) } + // requests will be released for terminated pods either with success or failed state termination. + + if reason != "Completed" && reason != "Error" { + res.Info = append(res.Info, v1alpha1.InfoItem{Name: common.PodRequestsCPU, Value: fmt.Sprintf("%d", CPUReq.MilliValue())}) + res.Info = append(res.Info, v1alpha1.InfoItem{Name: common.PodRequestsMEM, Value: fmt.Sprintf("%d", MemoryReq.MilliValue())}) + } + var urls []string if res.NetworkingInfo != nil { urls = res.NetworkingInfo.ExternalURLs diff --git a/controller/cache/info_test.go b/controller/cache/info_test.go index db58d209f19b8..fa27d0bda46ba 100644 --- a/controller/cache/info_test.go +++ b/controller/cache/info_test.go @@ -15,6 +15,7 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "sigs.k8s.io/yaml" + "github.com/argoproj/argo-cd/v2/common" "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1" "github.com/argoproj/argo-cd/v2/util/argo/normalizers" ) @@ -306,6 +307,8 @@ func TestGetPodInfo(t *testing.T) { assert.Equal(t, []v1alpha1.InfoItem{ {Name: "Node", Value: "minikube"}, {Name: "Containers", Value: "0/1"}, + {Name: common.PodRequestsCPU, Value: "0"}, // strings imported from common + {Name: common.PodRequestsMEM, Value: "134217728000"}, }, info.Info) assert.Equal(t, []string{"bar"}, info.Images) assert.Equal(t, &PodInfo{ @@ -367,9 +370,81 @@ func TestGetPodInfo(t *testing.T) { {Name: "Status Reason", Value: "Running"}, {Name: "Node", Value: "minikube"}, {Name: "Containers", Value: "1/1"}, + {Name: common.PodRequestsCPU, Value: "0"}, + {Name: common.PodRequestsMEM, Value: "0"}, }, info.Info) }) + t.Run("TestGetPodWithInitialContainerInfoWithResources", func(t *testing.T) { + pod := strToUnstructured(` + apiVersion: "v1" + kind: "Pod" + metadata: + labels: + app: "app-with-initial-container" + name: "app-with-initial-container-5f46976fdb-vd6rv" + namespace: "default" + ownerReferences: + - apiVersion: "apps/v1" + kind: "ReplicaSet" + name: "app-with-initial-container-5f46976fdb" + spec: + containers: + - image: "alpine:latest" + imagePullPolicy: "Always" + name: "app-with-initial-container" + resources: + requests: + cpu: "100m" + memory: "128Mi" + limits: + cpu: "500m" + memory: "512Mi" + initContainers: + - image: "alpine:latest" + imagePullPolicy: "Always" + name: "app-with-initial-container-logshipper" + resources: + requests: + cpu: "50m" + memory: "64Mi" + limits: + cpu: "250m" + memory: "256Mi" + nodeName: "minikube" + status: + containerStatuses: + - image: "alpine:latest" + name: "app-with-initial-container" + ready: true + restartCount: 0 + started: true + state: + running: + startedAt: "2024-10-08T08:44:25Z" + initContainerStatuses: + - image: "alpine:latest" + name: "app-with-initial-container-logshipper" + ready: true + restartCount: 0 + started: false + state: + terminated: + exitCode: 0 + reason: "Completed" + phase: "Running" + `) + + info := &ResourceInfo{} + populateNodeInfo(pod, info, []string{}) + assert.Equal(t, []v1alpha1.InfoItem{ + {Name: "Status Reason", Value: "Running"}, + {Name: "Node", Value: "minikube"}, + {Name: "Containers", Value: "1/1"}, + {Name: common.PodRequestsCPU, Value: "100"}, + {Name: common.PodRequestsMEM, Value: "134217728000"}, + }, info.Info) + }) t.Run("TestGetPodInfoWithSidecar", func(t *testing.T) { t.Parallel() @@ -424,6 +499,8 @@ func TestGetPodInfo(t *testing.T) { {Name: "Status Reason", Value: "Running"}, {Name: "Node", Value: "minikube"}, {Name: "Containers", Value: "2/2"}, + {Name: common.PodRequestsCPU, Value: "0"}, + {Name: common.PodRequestsMEM, Value: "0"}, }, info.Info) }) @@ -482,6 +559,8 @@ func TestGetPodInfo(t *testing.T) { {Name: "Status Reason", Value: "Init:0/1"}, {Name: "Node", Value: "minikube"}, {Name: "Containers", Value: "0/1"}, + {Name: common.PodRequestsCPU, Value: "0"}, + {Name: common.PodRequestsMEM, Value: "0"}, }, info.Info) }) @@ -539,6 +618,8 @@ func TestGetPodInfo(t *testing.T) { {Name: "Node", Value: "minikube"}, {Name: "Containers", Value: "0/3"}, {Name: "Restart Count", Value: "3"}, + {Name: common.PodRequestsCPU, Value: "0"}, + {Name: common.PodRequestsMEM, Value: "0"}, }, info.Info) }) @@ -596,6 +677,8 @@ func TestGetPodInfo(t *testing.T) { {Name: "Node", Value: "minikube"}, {Name: "Containers", Value: "0/3"}, {Name: "Restart Count", Value: "3"}, + {Name: common.PodRequestsCPU, Value: "0"}, + {Name: common.PodRequestsMEM, Value: "0"}, }, info.Info) }) @@ -656,6 +739,8 @@ func TestGetPodInfo(t *testing.T) { {Name: "Node", Value: "minikube"}, {Name: "Containers", Value: "1/3"}, {Name: "Restart Count", Value: "7"}, + {Name: common.PodRequestsCPU, Value: "0"}, + {Name: common.PodRequestsMEM, Value: "0"}, }, info.Info) }) @@ -698,6 +783,8 @@ func TestGetPodInfo(t *testing.T) { {Name: "Node", Value: "minikube"}, {Name: "Containers", Value: "0/1"}, {Name: "Restart Count", Value: "3"}, + {Name: common.PodRequestsCPU, Value: "0"}, + {Name: common.PodRequestsMEM, Value: "0"}, }, info.Info) }) @@ -733,6 +820,45 @@ func TestGetPodInfo(t *testing.T) { }, info.Info) }) + // Test pod condition succeed which had some allocated resources + t.Run("TestPodConditionSucceededWithResources", func(t *testing.T) { + t.Parallel() + + pod := strToUnstructured(` + apiVersion: v1 + kind: Pod + metadata: + name: test8 + spec: + nodeName: minikube + containers: + - name: container + resources: + requests: + cpu: "50m" + memory: "64Mi" + limits: + cpu: "250m" + memory: "256Mi" + status: + phase: Succeeded + containerStatuses: + - ready: false + restartCount: 0 + state: + terminated: + reason: Completed + exitCode: 0 +`) + info := &ResourceInfo{} + populateNodeInfo(pod, info, []string{}) + assert.Equal(t, []v1alpha1.InfoItem{ + {Name: "Status Reason", Value: "Completed"}, + {Name: "Node", Value: "minikube"}, + {Name: "Containers", Value: "0/1"}, + }, info.Info) + }) + // Test pod condition failed t.Run("TestPodConditionFailed", func(t *testing.T) { t.Parallel() @@ -765,6 +891,46 @@ func TestGetPodInfo(t *testing.T) { }, info.Info) }) + // Test pod condition failed with allocated resources + + t.Run("TestPodConditionFailedWithResources", func(t *testing.T) { + t.Parallel() + + pod := strToUnstructured(` + apiVersion: v1 + kind: Pod + metadata: + name: test9 + spec: + nodeName: minikube + containers: + - name: container + resources: + requests: + cpu: "50m" + memory: "64Mi" + limits: + cpu: "250m" + memory: "256Mi" + status: + phase: Failed + containerStatuses: + - ready: false + restartCount: 0 + state: + terminated: + reason: Error + exitCode: 1 +`) + info := &ResourceInfo{} + populateNodeInfo(pod, info, []string{}) + assert.Equal(t, []v1alpha1.InfoItem{ + {Name: "Status Reason", Value: "Error"}, + {Name: "Node", Value: "minikube"}, + {Name: "Containers", Value: "0/1"}, + }, info.Info) + }) + // Test pod condition succeed with deletion t.Run("TestPodConditionSucceededWithDeletion", func(t *testing.T) { t.Parallel() @@ -826,6 +992,8 @@ func TestGetPodInfo(t *testing.T) { {Name: "Status Reason", Value: "Terminating"}, {Name: "Node", Value: "minikube"}, {Name: "Containers", Value: "0/1"}, + {Name: common.PodRequestsCPU, Value: "0"}, + {Name: common.PodRequestsMEM, Value: "0"}, }, info.Info) }) @@ -852,6 +1020,8 @@ func TestGetPodInfo(t *testing.T) { {Name: "Status Reason", Value: "Terminating"}, {Name: "Node", Value: "minikube"}, {Name: "Containers", Value: "0/1"}, + {Name: common.PodRequestsCPU, Value: "0"}, + {Name: common.PodRequestsMEM, Value: "0"}, }, info.Info) }) @@ -882,6 +1052,8 @@ func TestGetPodInfo(t *testing.T) { {Name: "Status Reason", Value: "SchedulingGated"}, {Name: "Node", Value: "minikube"}, {Name: "Containers", Value: "0/2"}, + {Name: common.PodRequestsCPU, Value: "0"}, + {Name: common.PodRequestsMEM, Value: "0"}, }, info.Info) }) } diff --git a/ui/src/app/applications/components/application-pod-view/pod-tooltip.tsx b/ui/src/app/applications/components/application-pod-view/pod-tooltip.tsx index 51303a1334b56..f11f77d10a2e7 100644 --- a/ui/src/app/applications/components/application-pod-view/pod-tooltip.tsx +++ b/ui/src/app/applications/components/application-pod-view/pod-tooltip.tsx @@ -1,7 +1,8 @@ import * as React from 'react'; import Moment from 'react-moment'; import {Pod} from '../../../shared/models'; -import {isYoungerThanXMinutes} from '../utils'; +import {isYoungerThanXMinutes, podRequests} from '../utils'; +import {formatSize} from './pod-view'; export const PodTooltip = (props: {pod: Pod}) => { const pod = props.pod; @@ -16,15 +17,22 @@ export const PodTooltip = (props: {pod: Pod}) => {
{pod.health}
{(pod.info || []) - .filter(i => i.name !== 'Node') - .map(i => ( -
-
- {i.name}: + .filter(i => { + //filter out 0 values for CPU and mem on pod info + return i.name !== 'Node' && !((i.name === podRequests.CPU || i.name === podRequests.MEMORY) && parseInt(i.value, 10) === 0); + }) + .map(i => { + //formatted the values for cpu and mem + const formattedValue = formatPodMetric(i.name, i.value); + return ( +
+
+ {i.name}: +
+
{formattedValue}
-
{i.value}
-
- ))} + ); + })} {pod.createdAt && (
@@ -49,3 +57,17 @@ export const PodTooltip = (props: {pod: Pod}) => {
); }; + +function formatPodMetric(name: string, value: string) { + const numericValue = parseInt(value, 10); + + switch (name) { + case podRequests.CPU: + return `${numericValue}m`; + case podRequests.MEMORY: { + return formatSize(numericValue / 1000); // divide by 1000 to convert "milli bytes" to bytes + } + default: + return value; + } +} diff --git a/ui/src/app/applications/components/application-pod-view/pod-view.scss b/ui/src/app/applications/components/application-pod-view/pod-view.scss index 372bf1b0fa6ca..af479bb94b4bf 100644 --- a/ui/src/app/applications/components/application-pod-view/pod-view.scss +++ b/ui/src/app/applications/components/application-pod-view/pod-view.scss @@ -59,7 +59,7 @@ $pod-age-icon-clr: #ffce25; } } &__info { - margin-top: 1em; + margin-top: 3em; display: flex; justify-content: end; div { @@ -69,6 +69,22 @@ $pod-age-icon-clr: #ffce25; margin-right: 5px; padding: 3px 5px; } + #cost { + background-color: $argo-color-gray-7; + color: whitesmoke; + padding: 5px 8px; + font-weight: bold; + margin-right: auto; + transition: background-color 0.3s ease, box-shadow 0.3s ease, transform 0.2s ease; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + border: 1px solid lighten($argo-color-gray-7, 10%); + + + &:hover { + background-color: darken($argo-color-gray-7, 10%); + box-shadow: 0 6px 8px rgba(0, 0, 0, 0.2); + } + } } &__info--large { margin: 1em 0; diff --git a/ui/src/app/applications/components/application-pod-view/pod-view.tsx b/ui/src/app/applications/components/application-pod-view/pod-view.tsx index caba162b82eba..7c0ea520c5348 100644 --- a/ui/src/app/applications/components/application-pod-view/pod-view.tsx +++ b/ui/src/app/applications/components/application-pod-view/pod-view.tsx @@ -11,7 +11,7 @@ import {PodViewPreferences, services, ViewPreferences} from '../../../shared/ser import {ResourceTreeNode} from '../application-resource-tree/application-resource-tree'; import {ResourceIcon} from '../resource-icon'; import {ResourceLabel} from '../resource-label'; -import {ComparisonStatusIcon, isYoungerThanXMinutes, HealthStatusIcon, nodeKey, PodHealthIcon} from '../utils'; +import {ComparisonStatusIcon, isYoungerThanXMinutes, HealthStatusIcon, nodeKey, PodHealthIcon, podRequests} from '../utils'; import './pod-view.scss'; import {PodTooltip} from './pod-tooltip'; @@ -90,13 +90,13 @@ export class PodView extends React.Component { if (group.type === 'node' && group.name === 'Unschedulable' && podPrefs.hideUnschedulable) { return ; } + const GroupPodResources = calculatePodGrouprResquests(group.pods); return (
-
this.props.onItemClick(group.fullName)} - style={group.kind === 'node' ? {} : {cursor: 'pointer'}}> -
+
+
this.props.onItemClick(group.fullName)}>

@@ -138,6 +138,28 @@ export class PodView extends React.Component {
) : (
+ { + +
Requests:
+
+
+ CPU: +
+
{GroupPodResources.cpuRequest}
+
+
+
+ Memory: +
+
{GroupPodResources.memoryRequests}
+
+ + }> +
Requests
+
+ } {group.createdAt ? (
@@ -369,7 +391,7 @@ const labelForSortMode = { }; const sizes = ['Bytes', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi', 'Yi']; -function formatSize(bytes: number) { +export function formatSize(bytes: number) { if (!bytes) { return '0 Bytes'; } @@ -388,6 +410,31 @@ function formatMetric(name: ResourceName, val: number) { return (val || '0') + 'm'; } +function calculatePodGrouprResquests(pods: Pod[]) { + const resources = { + cpu: 0, + memory: 0 + }; + + pods.forEach(pod => { + pod.info?.forEach(info => { + const numericValue = parseInt(info.value, 10); + + if (info.name === podRequests.CPU) { + resources.cpu += numericValue; + } + if (info.name === podRequests.MEMORY) { + resources.memory += numericValue; + } + }); + }); + + return { + cpuRequest: `${resources.cpu}m`, + memoryRequests: `${formatSize(resources.memory / 1000)}` // divide by 1000 to convert "milli bytes" to bytes + }; +} + function renderStats(info: HostResourceInfo) { const neighborsHeight = 100 * (info.requestedByNeighbors / info.capacity); const appHeight = 100 * (info.requestedByApp / info.capacity); diff --git a/ui/src/app/applications/components/utils.test.tsx b/ui/src/app/applications/components/utils.test.tsx index 2f42d34b0b42d..0ebe983b91bce 100644 --- a/ui/src/app/applications/components/utils.test.tsx +++ b/ui/src/app/applications/components/utils.test.tsx @@ -323,6 +323,72 @@ describe('getPodStateReason', () => { expect(reason).toBe('Running'); }); + it('TestGetPodWithInitialContainerInfoWithResources', () => { + const podYaml = ` + apiVersion: "v1" + kind: "Pod" + metadata: + labels: + app: "app-with-initial-container" + name: "app-with-initial-container-5f46976fdb-vd6rv" + namespace: "default" + ownerReferences: + - apiVersion: "apps/v1" + kind: "ReplicaSet" + name: "app-with-initial-container-5f46976fdb" + spec: + containers: + - image: "alpine:latest" + imagePullPolicy: "Always" + name: "app-with-initial-container" + resources: + requests: + cpu: "100m" + memory: "128Mi" + limits: + cpu: "500m" + memory: "512Mi" + initContainers: + - image: "alpine:latest" + imagePullPolicy: "Always" + name: "app-with-initial-container-logshipper" + resources: + requests: + cpu: "50m" + memory: "64Mi" + limits: + cpu: "250m" + memory: "256Mi" + nodeName: "minikube" + status: + containerStatuses: + - image: "alpine:latest" + name: "app-with-initial-container" + ready: true + restartCount: 0 + started: true + state: + running: + startedAt: "2024-10-08T08:44:25Z" + initContainerStatuses: + - image: "alpine:latest" + name: "app-with-initial-container-logshipper" + ready: true + restartCount: 0 + started: false + state: + terminated: + exitCode: 0 + reason: "Completed" + phase: "Running" +`; + + const pod = jsYaml.load(podYaml); + + const {reason} = getPodStateReason(pod as State); + expect(reason).toBe('Running'); + }); + it('TestGetPodInfoWithSidecar', () => { const podYaml = ` apiVersion: v1 @@ -631,6 +697,40 @@ status: expect(reason).toBe('Completed'); }); + it('TestPodConditionSucceededWithResources', () => { + const podYaml = ` + apiVersion: v1 + kind: Pod + metadata: + name: test8 + spec: + nodeName: minikube + containers: + - name: container + resources: + requests: + cpu: "50m" + memory: "64Mi" + limits: + cpu: "250m" + memory: "256Mi" + status: + phase: Succeeded + containerStatuses: + - ready: false + restartCount: 0 + state: + terminated: + reason: Completed + exitCode: 0 +`; + const pod = jsYaml.load(podYaml); + + const {reason} = getPodStateReason(pod as State); + + expect(reason).toBe('Completed'); + }); + it('TestPodConditionFailed', () => { const podYaml = ` apiVersion: v1 @@ -658,6 +758,40 @@ status: expect(reason).toBe('Error'); }); + it('TestPodConditionFailedWithResources', () => { + const podYaml = ` +apiVersion: v1 +kind: Pod +metadata: + name: test9 +spec: + nodeName: minikube + containers: + - name: container + resources: + requests: + cpu: "50m" + memory: "64Mi" + limits: + cpu: "250m" + memory: "256Mi" +status: + phase: Failed + containerStatuses: + - ready: false + restartCount: 0 + state: + terminated: + reason: Error + exitCode: 1 +`; + const pod = jsYaml.load(podYaml); + + const {reason} = getPodStateReason(pod as State); + + expect(reason).toBe('Error'); + }); + it('TestPodConditionSucceededWithDeletion', () => { const podYaml = ` apiVersion: v1 diff --git a/ui/src/app/applications/components/utils.tsx b/ui/src/app/applications/components/utils.tsx index 54ca382cdcbd4..7a0de70b40408 100644 --- a/ui/src/app/applications/components/utils.tsx +++ b/ui/src/app/applications/components/utils.tsx @@ -1471,3 +1471,9 @@ export const userMsgsList: {[key: string]: string} = { groupNodes: `Since the number of pods has surpassed the threshold pod count of 15, you will now be switched to the group node view. If you prefer the tree view, you can simply click on the Group Nodes toolbar button to deselect the current view.` }; + +// constant for podrequests +export const podRequests = { + CPU: 'Requests (CPU)', + MEMORY: 'Requests (MEM)' +} as const;