Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Show pod details #1037

Merged
merged 3 commits into from
Sep 26, 2022
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/ContainerIntegration.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { EmptyStatePanel } from "cockpit-components-empty-state.jsx";

const _ = cockpit.gettext;

const renderContainerPublishedPorts = (ports) => {
export const renderContainerPublishedPorts = (ports) => {
if (!ports)
return null;

Expand All @@ -28,7 +28,7 @@ const renderContainerPublishedPorts = (ports) => {
return <List isPlain>{result}</List>;
};

const renderContainerVolumes = (volumes) => {
export const renderContainerVolumes = (volumes) => {
if (!volumes.length)
return null;

Expand Down
119 changes: 111 additions & 8 deletions src/Containers.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,19 @@ import {
Dropdown, DropdownItem, DropdownSeparator,
Flex,
KebabToggle,
Popover,
LabelGroup,
Text, TextVariants, FormSelect, FormSelectOption,
Toolbar, ToolbarContent, ToolbarItem,
Tooltip, Toolbar, ToolbarContent, ToolbarItem,
} from '@patternfly/react-core';
import { cellWidth } from '@patternfly/react-table';
import { MicrochipIcon, MemoryIcon, PortIcon, VolumeIcon, } from '@patternfly/react-icons';

import cockpit from 'cockpit';
import { ListingTable } from "cockpit-components-table.jsx";
import { ListingPanel } from 'cockpit-components-listing-panel.jsx';
import ContainerDetails from './ContainerDetails.jsx';
import ContainerIntegration from './ContainerIntegration.jsx';
import ContainerIntegration, { renderContainerPublishedPorts, renderContainerVolumes } from './ContainerIntegration.jsx';
import ContainerTerminal from './ContainerTerminal.jsx';
import ContainerLogs from './ContainerLogs.jsx';
import ContainerHealthLogs from './ContainerHealthLogs.jsx';
Expand All @@ -32,6 +34,7 @@ import ContainerRenameModal from './ContainerRenameModal.jsx';
import { useDialogs, DialogsContext } from "dialogs.jsx";

import './Containers.scss';
import '@patternfly/patternfly/utilities/Accessibility/accessibility.css';
import { ImageRunModal } from './ImageRunModal.jsx';
import { PodActions } from './PodActions.jsx';

Expand Down Expand Up @@ -326,6 +329,7 @@ class Containers extends React.Component {
};
this.renderRow = this.renderRow.bind(this);
this.onWindowResize = this.onWindowResize.bind(this);
this.podStats = this.podStats.bind(this);

this.cardRef = React.createRef();

Expand Down Expand Up @@ -442,6 +446,101 @@ class Containers extends React.Component {
this.setState({ width: this.cardRef.current.clientWidth });
}

podStats(pod) {
const { containersStats } = this.props;
// when no containers exists pod.Containers is null
if (!containersStats || !pod.Containers) {
return null;
}

// As podman does not provide per pod memory/cpu statistics we do the following:
// - don't add up CPU usage, instead display the highest found CPU usage of the containers in a pod
// - add up memory usage so it displays the total memory of the pod.
let cpu = 0;
let mem = 0;
for (const container of pod.Containers) {
const containerStats = containersStats[container.Id + pod.isSystem.toString()];
if (!containerStats)
continue;

if (containerStats.CPU != undefined) {
const val = containerStats.CPU === 0 ? containerStats.CPU : containerStats.CPU.toFixed(2);
if (val > cpu)
cpu = val;
}
if (containerStats.MemUsage != undefined)
mem += containerStats.MemUsage;
}

return {
cpu,
mem,
};
}

renderPodDetails(pod, podStatus) {
const podStats = this.podStats(pod);
const infraContainer = this.props.containers[pod.InfraId + pod.isSystem.toString()];
const infraContainerDetails = this.props.containersDetails[pod.InfraId + pod.isSystem.toString()];

return (
<>
{podStats && podStatus === "Running" &&
<>
<Flex className='pod-stat' spaceItems={{ default: 'spaceItemsSm' }}>
<Tooltip content={_("CPU")}>
<MicrochipIcon />
</Tooltip>
<Text component={TextVariants.p} className="pf-u-hidden-on-sm">{_("CPU")}</Text>
<Text component={TextVariants.p} className="pod-cpu">{podStats.cpu}%</Text>
</Flex>
<Flex className='pod-stat' spaceItems={{ default: 'spaceItemsSm' }}>
<Tooltip content={_("Memory")}>
<MemoryIcon />
</Tooltip>
<Text component={TextVariants.p} className="pf-u-hidden-on-sm">{_("Memory")}</Text>
<Text component={TextVariants.p} className="pod-memory">{utils.format_memory_and_limit(podStats.mem) || "0 KB"}</Text>
</Flex>
</>
}
{infraContainer && infraContainerDetails &&
<>
{infraContainer.Ports && infraContainer.Ports.length !== 0 &&
<Tooltip content={_("Click to see published ports")}>
<Popover
enableFlip
bodyContent={renderContainerPublishedPorts(infraContainer.Ports)}
>
<Button isSmall variant="link" className="pod-details-button pod-details-ports-btn"
icon={<PortIcon className="pod-details-button-color" />}
>
{infraContainer.Ports.length}
<Text component={TextVariants.p} className="pf-u-hidden-on-sm">{_("ports")}</Text>
</Button>
</Popover>
</Tooltip>
}
{infraContainerDetails.Mounts && infraContainerDetails.Mounts.length !== 0 &&
<Tooltip content={_("Click to see volumes")}>
<Popover
enableFlip
bodyContent={renderContainerVolumes(infraContainerDetails.Mounts)}
>
<Button isSmall variant="link" className="pod-details-button pod-details-volumes-btn"
icon={<VolumeIcon className="pod-details-button-color" />}
>
{infraContainerDetails.Mounts.length}
<Text component={TextVariants.p} className="pf-u-hidden-on-sm">{_("volumes")}</Text>
</Button>
</Popover>
</Tooltip>
}
</>
}
</>
);
}

render() {
const Dialogs = this.context;
const columnTitles = [
Expand Down Expand Up @@ -627,9 +726,10 @@ class Containers extends React.Component {
let caption;
let podStatus;
if (section !== 'no-pod') {
tableProps['aria-label'] = cockpit.format("Containers of Pod $0", this.props.pods[section].Name);
podStatus = this.props.pods[section].Status;
caption = this.props.pods[section].Name;
const pod = this.props.pods[section];
tableProps['aria-label'] = cockpit.format("Containers of pod $0", pod.Name);
podStatus = pod.Status;
caption = pod.Name;
} else {
tableProps['aria-label'] = _("Containers");
}
Expand All @@ -638,11 +738,14 @@ class Containers extends React.Component {
id={'table-' + (section == "no-pod" ? section : this.props.pods[section].Name)}
isPlain={section == "no-pod"}
isFlat={section != "no-pod"}
className="container-section">
className="container-pod">
{caption && <CardHeader>
<CardTitle>
<span className='pod-name'>{caption}</span>
<span>{_("pod group")}</span>
<Flex justifyContent={{ default: 'justifyContentFlexStart' }}>
<h3 className='pod-name'>{caption}</h3>
<span>{_("pod group")}</span>
{this.renderPodDetails(this.props.pods[section], podStatus)}
</Flex>
</CardTitle>
<CardActions className='panel-actions'>
<Badge isRead className={"ct-badge-pod-" + podStatus.toLowerCase()}>{_(podStatus)}</Badge>
Expand Down
52 changes: 49 additions & 3 deletions src/Containers.scss
Original file line number Diff line number Diff line change
@@ -1,9 +1,28 @@
.container-section {
@import "global-variables";

.container-pod {
.pf-c-card__header {
border-color: #ddd;
padding-top: var(--pf-global--spacer--md);
}

.pod-header-details {
border-color: #ddd;
margin-top: var(--pf-global--spacer--md);
margin-left: var(--pf-global--spacer--md);
margin-right: var(--pf-global--spacer--md);
}
jelly marked this conversation as resolved.
Show resolved Hide resolved

.pod-details-button {
padding-left: 0;
padding-right: 0;
margin-right: var(--pf-global--spacer--md);
}

.pod-details-button-color {
color: var(--pf-c-button--m-secondary--Color);
}

.pf-c-card__title {
padding: 0;
font-weight: var(--pf-global--FontWeight--normal);
Expand All @@ -16,8 +35,35 @@
}
}

.pf-c-card__header:not(:last-child) {
padding-bottom: var(--pf-global--spacer-sm);
> .pf-c-card__header {
&:not(:last-child) {
padding-bottom: var(--pf-global--spacer-sm);
}

// Reduce vertical padding of pod header items
> .pf-c-card__title > .pf-l-flex {
row-gap: var(--pf-global--spacer--sm);
}
}
}

.pod-stat {
@media (max-width: $pf-global--breakpoint--sm - 1) {
// Place each pod stat on its own row
flex-basis: 100%;
display: grid;
// Give labels to the same space
grid-template-columns: minmax(auto, 4rem) 1fr;

> svg {
// Hide icons in mobile to be consistent with container lists
display: none;
}
}

// Center the icons for proper vertical alignment
> svg {
align-self: center;
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/podman.scss
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@
}

// Add borders to no pod containers list and images list
.container-section.pf-m-plain tbody,
.container-pod.pf-m-plain tbody,
.containers-images tbody {
border: var(--pf-c-card--m-flat--BorderWidth) solid var(--pf-c-card--m-flat--BorderColor);
}
Expand Down
34 changes: 33 additions & 1 deletion test/check-application
Original file line number Diff line number Diff line change
Expand Up @@ -214,9 +214,27 @@ class TestApplication(testlib.MachineCase):
self.filter_containers("all")
self.waitPodContainer("pod-1", [])

def get_pod_cpu_usage(pod_name):
cpu = self.browser.text(f"#table-{pod_name}-title .pod-cpu")
self.assertIn('%', cpu)
return float(cpu[:-1])

def get_pod_memory(pod_name):
memory = self.browser.text(f"#table-{pod_name}-title .pod-memory")
memory, unit = memory.split(' ')
self.assertIn(unit, ["GB", "MB", "KB"])
return float(memory)

containerId = self.machine.execute("podman run -d --pod pod-1 --name test-pod-1-system alpine sleep 100").strip()
self.waitPodContainer("pod-1", [{"name": "test-pod-1-system", "image": "alpine", "command": "sleep 100", "state": "Running", "id": containerId}])
self.machine.execute("podman pod stop pod-1")
cpu = get_pod_cpu_usage("pod-1")
b.wait(lambda: get_pod_memory("pod-1") > 0)

# Test that cpu usage increases
self.machine.execute("podman exec -i test-pod-1-system sh -c 'dd bs=1024 count=1000000 < /dev/urandom > /dev/null'")
b.wait(lambda: get_pod_cpu_usage("pod-1") > cpu)

self.machine.execute("podman pod stop -t0 pod-1") # disable timeout, so test doesn't wait endlessly
self.waitPodContainer("pod-1", [{"name": "test-pod-1-system", "image": "alpine", "command": "sleep 100", "state": NOT_RUNNING, "id": containerId}])
self.filter_containers("running")
self.waitPodRow("pod-1", False)
Expand Down Expand Up @@ -267,6 +285,20 @@ class TestApplication(testlib.MachineCase):
b.click(".pf-c-modal-box button:contains('Delete')")
self.waitPodRow("pod-2", False)

# Volumes / mounts
self.machine.execute("podman pod create -p 9999:9999 -v /tmp:/app --name pod-3")
self.machine.execute("podman pod start pod-3")

self.waitPodContainer("pod-3", [])
# Verify 1 port mapping
b.wait_in_text("#table-pod-3-title .pod-details-ports-btn", "1")
b.click("#table-pod-3-title .pod-details-ports-btn")
b.wait_in_text(".pf-c-popover__content", "0.0.0.0:9999 → 9999/tcp")
# Verify 1 mount
b.wait_in_text("#table-pod-3-title .pod-details-volumes-btn", "1")
b.click("#table-pod-3-title .pod-details-volumes-btn")
b.wait_in_text(".pf-c-popover__content", "/tmp ↔ /app")

@testlib.nondestructive
def testBasicSystem(self):
self._testBasic(True)
Expand Down