diff --git a/src/ContainerRenameModal.jsx b/src/ContainerRenameModal.jsx
index 79445e88f..e586b6743 100644
--- a/src/ContainerRenameModal.jsx
+++ b/src/ContainerRenameModal.jsx
@@ -7,6 +7,7 @@ import {
import cockpit from 'cockpit';
import * as client from './client.js';
+import * as utils from './util.js';
import { ErrorNotification } from './Notification.jsx';
import { useDialogs } from "dialogs.jsx";
@@ -24,7 +25,7 @@ const ContainerRenameModal = ({ container, version, updateContainerAfterEvent })
setName(value);
if (value === "") {
setNameError(_("Container name is required."));
- } else if (/^[a-zA-Z0-9][a-zA-Z0-9_\\.-]*$/.test(value)) {
+ } else if (utils.is_valid_container_name(value)) {
setNameError(null);
} else {
setNameError(_("Invalid characters. Name can only contain letters, numbers, and certain punctuation (_ . -)."));
diff --git a/src/Containers.jsx b/src/Containers.jsx
index 441589221..37b71a640 100644
--- a/src/Containers.jsx
+++ b/src/Containers.jsx
@@ -37,6 +37,7 @@ import './Containers.scss';
import '@patternfly/patternfly/utilities/Accessibility/accessibility.css';
import { ImageRunModal } from './ImageRunModal.jsx';
import { PodActions } from './PodActions.jsx';
+import { PodCreateModal } from './PodCreateModal.jsx';
const _ = cockpit.gettext;
@@ -668,6 +669,16 @@ class Containers extends React.Component {
onAddNotification={this.props.onAddNotification} />);
};
+ const createPod = () => {
+ Dialogs.show();
+ };
+
const filterRunning =
@@ -681,6 +692,13 @@ class Containers extends React.Component {
+
+
+
}
+ />
+ } className={"dynamic-form-group " + formclass}>
+ {
+ dialogValues.list.length
+ ? <>
+ {dialogValues.list.map((item, idx) => {
+ return React.cloneElement(this.props.itemcomponent, {
+ idx: idx, item: item, id: id + "-" + idx,
+ key: idx,
+ onChange: this.onItemChange, removeitem: this.removeItem, additem: this.addItem, options: this.props.options,
+ itemCount: Object.keys(dialogValues.list).length,
+ });
+ })
+ }
+ {helperText &&
+
+ {helperText}
+
+ }
+ >
+ :
+
+ {emptyStateString}
+
+
+ }
+
+ );
+ }
+}
+DynamicListForm.propTypes = {
+ emptyStateString: PropTypes.string.isRequired,
+ onChange: PropTypes.func.isRequired,
+ id: PropTypes.string.isRequired,
+ itemcomponent: PropTypes.object.isRequired,
+ formclass: PropTypes.string,
+ options: PropTypes.object,
+};
diff --git a/src/DynamicListForm.scss b/src/DynamicListForm.scss
new file mode 100644
index 000000000..70effa6cf
--- /dev/null
+++ b/src/DynamicListForm.scss
@@ -0,0 +1,39 @@
+@import "global-variables";
+
+.dynamic-form-group {
+ .pf-c-empty-state {
+ padding: 0;
+ }
+
+ .pf-c-form__label {
+ // Don't allow labels to wrap
+ white-space: nowrap;
+ }
+
+ .remove-button-group {
+ // Move 'Remove' button the the end of the row
+ grid-column: -1;
+ // Move 'Remove' button to the bottom of the line so as to align with the other form fields
+ display: flex;
+ align-items: flex-end;
+ }
+
+ // Set check to the same height as input widgets and vertically align
+ .pf-c-form__group-control > .pf-c-check {
+ // Set height to the same as inputs
+ // Font height is font size * line height (1rem * 1.5)
+ // Widgets have 5px padding, 1px border (top & bottom): (5 + 1) * 2 = 12
+ // This all equals to 36px
+ height: calc(var(--pf-global--FontSize--md) * var(--pf-global--LineHeight--md) + 12px);
+ align-content: center;
+ }
+
+ // We use FormFieldGroup PF component for the nested look and for ability to add buttons to the header
+ // However we want to save space and not add indent to the left so we need to override it
+ .pf-c-form__field-group-body {
+ // Stretch content fully
+ --pf-c-form__field-group-body--GridColumn: 1 / -1;
+ // Reduce padding at the top
+ --pf-c-form__field-group-body--PaddingTop: var(--pf-global--spacer--xs);
+ }
+}
diff --git a/src/ImageRunModal.jsx b/src/ImageRunModal.jsx
index a739b655e..a5fbdc4f6 100644
--- a/src/ImageRunModal.jsx
+++ b/src/ImageRunModal.jsx
@@ -1,14 +1,12 @@
import React from 'react';
-import PropTypes from 'prop-types';
import {
Button, Checkbox,
- EmptyState, EmptyStateBody,
- Form, FormGroup, FormFieldGroup, FormFieldGroupHeader,
+ Form, FormGroup,
FormSelect, FormSelectOption,
Grid, GridItem,
- HelperText, HelperTextItem,
- InputGroup, Radio, InputGroupText, InputGroupTextVariant,
- Modal, NumberInput, Select, SelectVariant,
+ Modal, Radio, Select, SelectVariant,
+ NumberInput, InputGroupTextVariant,
+ InputGroup, InputGroupText,
SelectOption, SelectGroup,
TextInput, Tabs, Tab, TabTitleText,
ToggleGroup, ToggleGroupItem,
@@ -19,13 +17,15 @@ import { MinusIcon, OutlinedQuestionCircleIcon } from '@patternfly/react-icons';
import * as dockerNames from 'docker-names';
import { ErrorNotification } from './Notification.jsx';
-import { FileAutoComplete } from 'cockpit-components-file-autocomplete.jsx';
import * as utils from './util.js';
import * as client from './client.js';
import rest from './rest.js';
import cockpit from 'cockpit';
import { onDownloadContainer, onDownloadContainerFinished } from './Containers.jsx';
import { DialogsContext } from "dialogs.jsx";
+import { PublishPort } from './PublishPort.jsx';
+import { DynamicListForm } from './DynamicListForm.jsx';
+import { Volume } from './Volume.jsx';
import { debounce } from 'throttle-debounce';
@@ -50,79 +50,6 @@ const units = {
},
};
-const PublishPort = ({ id, item, onChange, idx, removeitem, itemCount }) =>
- (
-
-
-
-
- }>
- onChange(idx, 'IP', value)} />
-
-
-
-
- }>
- onChange(idx, 'hostPort', value)} />
-
-
- onChange(idx, 'containerPort', value)} />
-
-
- onChange(idx, 'protocol', value)}>
-
-
-
-
-
- }
- onClick={() => removeitem(idx)} />
-
-
- );
-
const handleEnvValue = (key, value, idx, onChange, additem, itemCount) => {
// Allow the input of KEY=VALUE separated value pairs for bulk import
if (value.includes('=')) {
@@ -171,128 +98,6 @@ const EnvVar = ({ id, item, onChange, idx, removeitem, additem, itemCount }) =>
);
-const Volume = ({ id, item, onChange, idx, removeitem, additem, options, itemCount }) =>
- (
-
-
- onChange(idx, 'hostPath', value) } />
-
-
- onChange(idx, 'containerPath', value)} />
-
-
- onChange(idx, 'mode', value ? "rw" : "ro")} />
-
- { options && options.selinuxAvailable &&
-
- onChange(idx, 'selinux', value)}>
-
-
-
-
- }
-
- }
- onClick={() => removeitem(idx)} />
-
-
- );
-
-class DynamicListForm extends React.Component {
- constructor(props) {
- super(props);
- this.state = {
- list: [],
- };
- this.keyCounter = 0;
- this.removeItem = this.removeItem.bind(this);
- this.addItem = this.addItem.bind(this);
- this.onItemChange = this.onItemChange.bind(this);
- }
-
- removeItem(idx, field, value) {
- this.setState(state => {
- const items = state.list.concat();
- items.splice(idx, 1);
- return { list: items };
- }, () => this.props.onChange(this.state.list.concat()));
- }
-
- addItem() {
- this.setState(state => {
- return { list: [...state.list, Object.assign({ key: this.keyCounter++ }, this.props.default)] };
- }, () => this.props.onChange(this.state.list.concat()));
- }
-
- onItemChange(idx, field, value) {
- this.setState(state => {
- const items = state.list.concat();
- items[idx][field] = value || null;
- return { list: items };
- }, () => this.props.onChange(this.state.list.concat()));
- }
-
- render () {
- const { id, label, actionLabel, formclass, emptyStateString, helperText } = this.props;
- const dialogValues = this.state;
- return (
- {actionLabel}}
- />
- } className={"dynamic-form-group " + formclass}>
- {
- dialogValues.list.length
- ? <>
- {dialogValues.list.map((item, idx) => {
- return React.cloneElement(this.props.itemcomponent, {
- idx: idx, item: item, id: id + "-" + idx,
- key: idx,
- onChange: this.onItemChange, removeitem: this.removeItem, additem: this.addItem, options: this.props.options,
- itemCount: Object.keys(dialogValues.list).length,
- });
- })
- }
- {helperText &&
-
- {helperText}
-
- }
- >
- :
-
- {emptyStateString}
-
-
- }
-
- );
- }
-}
-DynamicListForm.propTypes = {
- emptyStateString: PropTypes.string.isRequired,
- onChange: PropTypes.func.isRequired,
- id: PropTypes.string.isRequired,
- itemcomponent: PropTypes.object.isRequired,
- formclass: PropTypes.string,
- options: PropTypes.object,
-};
-
export class ImageRunModal extends React.Component {
static contextType = DialogsContext;
diff --git a/src/ImageRunModal.scss b/src/ImageRunModal.scss
index 12598963a..ccb093780 100644
--- a/src/ImageRunModal.scss
+++ b/src/ImageRunModal.scss
@@ -1,34 +1,5 @@
@import "global-variables";
-.dynamic-form-group {
- .pf-c-empty-state {
- padding: 0;
- }
-
- .pf-c-form__label {
- // Don't allow labels to wrap
- white-space: nowrap;
- }
-
- .remove-button-group {
- // Move 'Remove' button the the end of the row
- grid-column: -1;
- // Move 'Remove' button to the bottom of the line so as to align with the other form fields
- display: flex;
- align-items: flex-end;
- }
-
- // Set check to the same height as input widgets and vertically align
- .pf-c-form__group-control > .pf-c-check {
- // Set height to the same as inputs
- // Font height is font size * line height (1rem * 1.5)
- // Widgets have 5px padding, 1px border (top & bottom): (5 + 1) * 2 = 12
- // This all equals to 36px
- height: calc(var(--pf-global--FontSize--md) * var(--pf-global--LineHeight--md) + 12px);
- align-content: center;
- }
-}
-
// Ensure the width fits within the screen boundaries (with padding on the sides)
.pf-c-select__menu {
// 3xl is the left+right padding for an iPhone SE;
@@ -65,15 +36,6 @@
}
}
-// We use FormFieldGroup PF component for the nested look and for ability to add buttons to the header
-// However we want to save space and not add indent to the left so we need to override it
-.dynamic-form-group .pf-c-form__field-group-body {
- // Stretch content fully
- --pf-c-form__field-group-body--GridColumn: 1 / -1;
- // Reduce padding at the top
- --pf-c-form__field-group-body--PaddingTop: var(--pf-global--spacer--xs);
-}
-
// HACK: A local copy of pf-m-horizontal (as ct-m-horizontal),
// but applied at the FormGroup level instead of Form
@media (min-width: $pf-global--breakpoint--md) {
diff --git a/src/PodCreateModal.jsx b/src/PodCreateModal.jsx
new file mode 100644
index 000000000..68fc58f5f
--- /dev/null
+++ b/src/PodCreateModal.jsx
@@ -0,0 +1,163 @@
+import React, { useState } from 'react';
+import {
+ Button,
+ Form, FormGroup, Modal, Radio,
+ TextInput,
+} from '@patternfly/react-core';
+import * as dockerNames from 'docker-names';
+
+import { ErrorNotification } from './Notification.jsx';
+import { PublishPort } from './PublishPort.jsx';
+import { DynamicListForm } from './DynamicListForm.jsx';
+import { Volume } from './Volume.jsx';
+import * as client from './client.js';
+import * as utils from './util.js';
+import cockpit from 'cockpit';
+import { useDialogs } from "dialogs.jsx";
+
+const _ = cockpit.gettext;
+
+const systemOwner = "system";
+
+export const PodCreateModal = ({ user, selinuxAvailable, systemServiceAvailable, userServiceAvailable, version }) => {
+ const [podName, setPodName] = useState(dockerNames.getRandomName());
+ const [nameError, setNameError] = useState(null);
+ const [publish, setPublish] = useState([]);
+ const [volumes, setVolumes] = useState([]);
+ const [owner, setOwner] = useState(systemServiceAvailable ? systemOwner : user);
+ const [dialogError, setDialogError] = useState(null);
+ const [dialogErrorDetail, setDialogErrorDetail] = useState(null);
+ const Dialogs = useDialogs();
+
+ const getCreateConfig = () => {
+ const createConfig = {};
+
+ if (podName)
+ createConfig.name = podName;
+
+ if (publish.length > 0)
+ createConfig.portmappings = publish
+ .filter(port => port.containerPort)
+ .map(port => {
+ const pm = { container_port: parseInt(port.containerPort), protocol: port.protocol };
+ if (port.hostPort !== null)
+ pm.host_port = parseInt(port.hostPort);
+ if (port.IP !== null)
+ pm.host_ip = port.IP;
+ return pm;
+ });
+
+ if (volumes.length > 0) {
+ createConfig.mounts = volumes
+ .filter(volume => volume.hostPath && volume.containerPath)
+ .map(volume => {
+ const record = { source: volume.hostPath, destination: volume.containerPath, type: "bind" };
+ record.options = [];
+ if (volume.mode)
+ record.options.push(volume.mode);
+ if (volume.selinux)
+ record.options.push(volume.selinux);
+ return record;
+ });
+ }
+
+ return createConfig;
+ };
+
+ const createPod = (isSystem, createConfig) => {
+ client.createPod(isSystem, createConfig)
+ .then(() => Dialogs.close())
+ .catch(ex => {
+ setDialogError(_("Pod failed to be created"));
+ setDialogErrorDetail(cockpit.format("$0: $1", ex.reason, ex.message));
+ });
+ };
+
+ const onCreateClicked = () => {
+ const createConfig = getCreateConfig();
+ createPod(owner === systemOwner, createConfig);
+ };
+
+ const onValueChanged = (key, value) => {
+ if (key === "podName") {
+ setPodName(value);
+ }
+ if (utils.is_valid_container_name(value)) {
+ setNameError(null);
+ } else {
+ setNameError(_("Invalid characters. Name can only contain letters, numbers, and certain punctuation (_ . -)."));
+ }
+ };
+
+ const defaultBody = (
+
+ );
+
+ return (
+
+ {dialogError && }
+
+
+ >}
+ >
+ {defaultBody}
+
+ );
+};
diff --git a/src/PublishPort.jsx b/src/PublishPort.jsx
new file mode 100644
index 000000000..173c93fcb
--- /dev/null
+++ b/src/PublishPort.jsx
@@ -0,0 +1,86 @@
+import React from 'react';
+import {
+ Button,
+ FormGroup,
+ FormSelect, FormSelectOption,
+ Grid,
+ TextInput,
+ Popover,
+} from '@patternfly/react-core';
+import { MinusIcon, OutlinedQuestionCircleIcon } from '@patternfly/react-icons';
+import cockpit from 'cockpit';
+
+const _ = cockpit.gettext;
+
+export const PublishPort = ({ id, item, onChange, idx, removeitem, itemCount }) =>
+ (
+
+
+
+
+ }>
+ onChange(idx, 'IP', value)} />
+
+
+
+
+ }>
+ onChange(idx, 'hostPort', value)} />
+
+
+ onChange(idx, 'containerPort', value)} />
+
+
+ onChange(idx, 'protocol', value)}>
+
+
+
+
+
+ }
+ onClick={() => removeitem(idx)} />
+
+
+ );
diff --git a/src/Volume.jsx b/src/Volume.jsx
new file mode 100644
index 000000000..438a4f93f
--- /dev/null
+++ b/src/Volume.jsx
@@ -0,0 +1,54 @@
+import React from 'react';
+import {
+ Button, Checkbox,
+ FormGroup,
+ FormSelect, FormSelectOption,
+ Grid,
+ TextInput,
+} from '@patternfly/react-core';
+import { MinusIcon } from '@patternfly/react-icons';
+import { FileAutoComplete } from 'cockpit-components-file-autocomplete.jsx';
+import cockpit from 'cockpit';
+
+const _ = cockpit.gettext;
+
+export const Volume = ({ id, item, onChange, idx, removeitem, additem, options, itemCount }) =>
+ (
+
+
+ onChange(idx, 'hostPath', value) } />
+
+
+ onChange(idx, 'containerPath', value)} />
+
+
+ onChange(idx, 'mode', value ? "rw" : "ro")} />
+
+ { options && options.selinuxAvailable &&
+
+ onChange(idx, 'selinux', value)}>
+
+
+
+
+ }
+
+ }
+ onClick={() => removeitem(idx)} />
+
+
+ );
diff --git a/src/client.js b/src/client.js
index 81c027ad2..6a43beaaa 100644
--- a/src/client.js
+++ b/src/client.js
@@ -101,6 +101,8 @@ export const postPod = (system, action, id, args) => podmanCall("libpod/pods/" +
export const delPod = (system, id, force) => podmanCall("libpod/pods/" + id, "DELETE", { force }, system);
+export const createPod = (system, config) => podmanCall("libpod/pods/create", "POST", {}, system, JSON.stringify(config));
+
export function execContainer(system, id) {
const args = {
AttachStderr: true,
diff --git a/src/util.js b/src/util.js
index fd736591f..22b4f8bc4 100644
--- a/src/util.js
+++ b/src/util.js
@@ -140,3 +140,7 @@ export function unquote_cmdline(text) {
export function image_name(image) {
return image.RepoTags ? image.RepoTags[0] : ":";
}
+
+export function is_valid_container_name(name) {
+ return /^[a-zA-Z0-9][a-zA-Z0-9_\\.-]*$/.test(name);
+}
diff --git a/test/check-application b/test/check-application
index 8d21cbc0d..a1ddccdda 100755
--- a/test/check-application
+++ b/test/check-application
@@ -4,6 +4,7 @@
# "class Browser" and "class MachineCase" for the available API.
import os
+import json
import sys
# import Cockpit's machinery for test VMs and its browser test API
@@ -26,6 +27,7 @@ NOT_RUNNING = ["Exited", "Stopped"]
# Ubuntu 2204 lacks user systemd units https://github.com/containers/podman/commit/9312d458b4254b48e331d1ae40cb2f6d0fec9bd0
DISTROS_WITHOUT_PODMAN_USER_RESTART = ['ubuntu-stable', 'ubuntu-2204']
+DISTROS_WITHOUT_PODMAN_POD_VOLUMES = ['ubuntu-stable', 'ubuntu-2204', 'debian-testing', 'fedora-35']
def showImages(browser):
@@ -178,10 +180,10 @@ class TestApplication(testlib.MachineCase):
else:
self.browser.wait_not_present("#table-" + podName)
- def waitPodContainer(self, podName, containerList):
+ def waitPodContainer(self, podName, containerList, system=True):
if len(containerList):
for container in containerList:
- self.waitContainer(container["id"], True, name=container["name"], image=container["image"],
+ self.waitContainer(container["id"], system, name=container["name"], image=container["image"],
cmd=container["command"], state=container["state"], pod=podName)
else:
if self.browser.val("#containers-containers-filter") == "all":
@@ -1459,6 +1461,9 @@ class TestApplication(testlib.MachineCase):
# Just drop user images so we can use simpler selectors
if auth:
self.execute(False, "podman rmi alpine busybox registry:2")
+ _, _, version = self.execute(False, "podman -v").split(' ')
+ if int(version[0]) < 4:
+ self.execute(False, "podman rmi pause:3.5")
self.login(auth)
@@ -2274,6 +2279,101 @@ class TestApplication(testlib.MachineCase):
for i in range(31):
self.execute(True, f"podman rm -f container{i}")
+ def testCreatePodSystem(self):
+ self._createPod(True)
+
+ # HACK: give podman time to kill the pod containers, can be removed if we
+ # have podman 4.0 everywhere and can use podman pod rm -t0
+ @testlib.timeout(300)
+ def testCreatePodUser(self):
+ self._createPod(False)
+
+ def _createPod(self, auth):
+ b = self.browser
+ m = self.machine
+ pod_name = "testpod1"
+
+ self.login(auth)
+
+ b.click("#containers-containers-create-pod-btn")
+ b.set_input_text("#create-pod-dialog-name", pod_name)
+
+ if auth:
+ print('owner')
+ else:
+ b.wait_not_present("#create-pod-dialog-owner-system")
+
+ # Ports
+ b.click('.publish-port-form .btn-add')
+ b.set_input_text('#create-pod-dialog-publish-0-host-port', '6000')
+ b.set_input_text('#create-pod-dialog-publish-0-container-port', '5000')
+ b.click('.publish-port-form .btn-add')
+ b.set_input_text('#create-pod-dialog-publish-1-ip-address', '127.0.0.1')
+ b.set_input_text('#create-pod-dialog-publish-1-host-port', '6001')
+ b.set_input_text('#create-pod-dialog-publish-1-container-port', '5001')
+ b.set_val('#create-pod-dialog-publish-1-protocol', "udp")
+ b.click('.publish-port-form .btn-add')
+ b.set_input_text('#create-pod-dialog-publish-2-ip-address', '127.0.0.2')
+ b.set_input_text('#create-pod-dialog-publish-2-container-port', '9001')
+
+ # Volumes
+ if self.machine.image not in DISTROS_WITHOUT_PODMAN_POD_VOLUMES:
+ b.click('.volume-form .btn-add')
+ rodir, rwdir = m.execute("mktemp; mktemp").split('\n')[:2]
+ m.execute(f"chown admin:admin {rodir}")
+ m.execute(f"chown admin:admin {rwdir}")
+
+ if self.has_selinux:
+ b.set_val('#create-pod-dialog-volume-0-selinux', "z")
+ else:
+ b.wait_not_present('#create-pod-dialog-volume-0-selinux')
+
+ b.set_file_autocomplete_val("#create-pod-dialog-volume-0 .pf-c-select", rodir)
+ b.set_input_text('#create-pod-dialog-volume-0-container-path', '/tmp/ro')
+ b.click('.volume-form .btn-add')
+
+ b.set_file_autocomplete_val("#create-pod-dialog-volume-1 .pf-c-select", rwdir)
+ b.set_input_text('#create-pod-dialog-volume-1-container-path', '/tmp/rw')
+
+ b.click("#create-pod-create-btn")
+ b.set_val("#containers-containers-filter", "all")
+ self.waitPodContainer(pod_name, [])
+
+ container_name = 'test-pod-1-system' if auth else 'test-pod-1'
+ containerId = self.execute(auth, f"podman run -d --pod {pod_name} --name {container_name} alpine sleep 500").strip()
+ self.waitPodContainer(pod_name, [{"name": container_name, "image": "alpine", "command": "sleep 500", "state": "Running", "id": containerId}], auth)
+
+ self.toggleExpandedContainer(container_name)
+ b.click(".pf-m-expanded button:contains('Integration')")
+ if self.machine.image not in DISTROS_WITHOUT_PODMAN_POD_VOLUMES:
+ b.wait_in_text('#containers-containers tr:contains("alpine") dt:contains("Volumes") + dd', f"{rodir} \u2194 /tmp/ro")
+ b.wait_in_text('#containers-containers tr:contains("alpine") dt:contains("Volumes") + dd', f"{rwdir} \u2194 /tmp/rw")
+
+ b.wait_in_text('#containers-containers tr:contains("alpine") dt:contains("Ports") + dd', '0.0.0.0:6000 \u2192 5000/tcp')
+ b.wait_in_text('#containers-containers tr:contains("alpine") dt:contains("Ports") + dd', '127.0.0.1:6001 \u2192 5001/udp')
+ b.wait_in_text('#containers-containers tr:contains("alpine") dt:contains("Ports") + dd', ' \u2192 9001/tcp')
+
+ # Validate ports via inspect as we do not show them in the UI yet
+ pod_info = json.loads(self.execute(auth, f"podman pod inspect {pod_name}").strip())
+ port_bindings = pod_info['InfraConfig']['PortBindings']
+ self.assertEqual(port_bindings['5000/tcp'], [{'HostIp': '', 'HostPort': '6000'}])
+ self.assertEqual(port_bindings['5001/udp'], [{'HostIp': '127.0.0.1', 'HostPort': '6001'}])
+ # Host port is randomized as not provided
+ self.assertTrue(port_bindings['9001/tcp'])
+ # Force stop, so tearDown does not hang
+ self.execute(auth, f"podman pod stop -t0 {pod_name} || true")
+
+ # Create pod as admin
+ if auth:
+ pod_name = 'testpod2'
+ b.click("#containers-containers-create-pod-btn")
+ b.set_input_text("#create-pod-dialog-name", pod_name)
+ b.click("#create-pod-dialog-owner-user")
+ b.click("#create-pod-create-btn")
+
+ b.set_val("#containers-containers-filter", "all")
+ self.waitPodContainer(pod_name, [])
+
if __name__ == '__main__':
testlib.test_main()
diff --git a/test/vm.install b/test/vm.install
index 2223b67c6..64aad60e8 100755
--- a/test/vm.install
+++ b/test/vm.install
@@ -1,4 +1,4 @@
-#!/bin/sh
+#!/bin/bash
# image-customize script to prepare a bots VM for cockpit-podman testing
set -eu
@@ -35,12 +35,21 @@ if [ "$ID" = "fedora" ]; then
fi
fi
+images=(quay.io/libpod/busybox quay.io/libpod/alpine quay.io/cockpit/registry:2)
+podman_version=$(podman -v | awk '{ print substr($3, 1, 1) }')
+echo $podman_version
+
+# Since 4.0 podman now ships the pause image
+if [ "${podman_version}" -lt 4 ]; then
+ images+=("k8s.gcr.io/pause:3.5")
+fi
+
# copy images for user podman tests; podman insists on user session
loginctl enable-linger $(id -u admin)
-for img in quay.io/libpod/busybox quay.io/libpod/alpine quay.io/cockpit/registry:2; do
- podman save $img | sudo -i -u admin podman load
+for img in "${images[@]}"; do
+ podman save $img | sudo -i -u admin podman load
done
-loginctl disable-linger $(id -u admin)
+ loginctl disable-linger $(id -u admin)
# 15minutes after boot tmp files are removed and podman stores some tmp lock files
systemctl disable --now systemd-tmpfiles-clean.timer