Skip to content

Commit

Permalink
Automatically start podman.socket
Browse files Browse the repository at this point in the history
Always start the system and user `podman.socket` unit on initialization.
There really is no reason to explicitly ask the user about it -- we can
treat it as "extended cockpit" to just access podman.

There also isn't a reason to enable the socket unit -- starting it
on demand is fine from cockpit-podman's perspective.

Now the user will only see the empty state if the service fails, which
should be very rare. So turn the Troubleshoot button into a primary one.

We also don't need the "System/User podman is also available" alerts any
more -- gaining root privileges auto-starts the system service.
  • Loading branch information
martinpitt committed Jan 27, 2025
1 parent 6f62541 commit d7de01a
Show file tree
Hide file tree
Showing 3 changed files with 23 additions and 222 deletions.
111 changes: 13 additions & 98 deletions src/app.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,8 @@

import React from 'react';

import { Alert, AlertActionCloseButton, AlertActionLink, AlertGroup } from "@patternfly/react-core/dist/esm/components/Alert";
import { Alert, AlertActionCloseButton, AlertGroup } from "@patternfly/react-core/dist/esm/components/Alert";
import { Button } from "@patternfly/react-core/dist/esm/components/Button";
import { Checkbox } from "@patternfly/react-core/dist/esm/components/Checkbox";
import { EmptyState, EmptyStateHeader, EmptyStateFooter, EmptyStateIcon, EmptyStateActions, EmptyStateVariant } from "@patternfly/react-core/dist/esm/components/EmptyState";
import { Page, PageSection, PageSectionVariants } from "@patternfly/react-core/dist/esm/components/Page";
import { Stack } from "@patternfly/react-core/dist/esm/layouts/Stack";
Expand Down Expand Up @@ -61,14 +60,12 @@ class Application extends React.Component {
ownerFilter: "all",
dropDownValue: 'Everything',
notifications: [],
showStartService: true,
version: '1.3.0',
selinuxAvailable: false,
podmanRestartAvailable: false,
userPodmanRestartAvailable: false,
currentUser: _("User"),
userLingeringEnabled: null,
privileged: false,
location: {},
};
this.onAddNotification = this.onAddNotification.bind(this);
Expand All @@ -77,9 +74,7 @@ class Application extends React.Component {
this.onOwnerChanged = this.onOwnerChanged.bind(this);
this.onContainerFilterChanged = this.onContainerFilterChanged.bind(this);
this.updateContainer = this.updateContainer.bind(this);
this.startService = this.startService.bind(this);
this.goToServicePage = this.goToServicePage.bind(this);
this.checkUserService = this.checkUserService.bind(this);
this.onNavigate = this.onNavigate.bind(this);

this.pendingUpdateContainer = {}; // id+system → promise
Expand Down Expand Up @@ -458,6 +453,8 @@ class Application extends React.Component {

async init(system) {
try {
await cockpit.spawn(["systemctl", ...(system ? [] : ["--user"]), "start", "podman.socket"],
{ superuser: system ? "require" : null, err: "message" });
const reply = await client.getInfo(system);
this.setState({
[system ? "systemServiceAvailable" : "userServiceAvailable"]: true,
Expand Down Expand Up @@ -502,14 +499,15 @@ class Application extends React.Component {
}

componentDidMount() {
this.init(true);
superuser.addEventListener("changed", () => this.init(true));

cockpit.script("[ `id -u` -eq 0 ] || echo $XDG_RUNTIME_DIR")
.then(xrd => {
const isRoot = !xrd || xrd.split("/").pop() == "root";
if (!isRoot) {
sessionStorage.setItem('XDG_RUNTIME_DIR', xrd.trim());
this.init(false);
this.checkUserService();
this.checkUserRestartService();
} else {
this.setState({
userImagesLoaded: true,
Expand All @@ -527,9 +525,6 @@ class Application extends React.Component {
cockpit.spawn(["systemctl", "show", "--value", "-p", "LoadState", "podman-restart"], { environ: ["LC_ALL=C"], error: "ignore" })
.then(out => this.setState({ podmanRestartAvailable: out.trim() === "loaded" }));

superuser.addEventListener("changed", () => this.setState({ privileged: !!superuser.allowed }));
this.setState({ privileged: superuser.allowed });

cockpit.user().then(user => {
this.setState({ currentUser: user.name || _("User") });
// HACK: https://github.com/systemd/systemd/issues/22244#issuecomment-1210357701
Expand Down Expand Up @@ -570,59 +565,11 @@ class Application extends React.Component {
});
}

checkUserService() {
const argv = ["systemctl", "--user", "is-enabled", "podman.socket"];

cockpit.spawn(["systemctl", "--user", "show", "--value", "-p", "LoadState", "podman-restart"], { environ: ["LC_ALL=C"], error: "ignore" })
.then(out => this.setState({ userPodmanRestartAvailable: out.trim() === "loaded" }));

cockpit.spawn(argv, { environ: ["LC_ALL=C"], err: "out" })
.then(() => this.setState({ userServiceExists: true }))
.catch((_, response) => {
if (response.trim() !== "disabled")
this.setState({ userServiceExists: false });
else
this.setState({ userServiceExists: true });
});
}

startService(e) {
if (!e || e.button !== 0)
return;

let argv;
if (this.state.enableService)
argv = ["systemctl", "enable", "--now", "podman.socket"];
else
argv = ["systemctl", "start", "podman.socket"];

cockpit.spawn(argv, { superuser: "require", err: "message" })
.then(() => this.init(true))
.catch(err => {
this.setState({
systemServiceAvailable: false,
systemContainersLoaded: true,
systemImagesLoaded: true
});
console.warn("Failed to start system podman.socket:", JSON.stringify(err));
});

if (this.state.enableService)
argv = ["systemctl", "--user", "enable", "--now", "podman.socket"];
else
argv = ["systemctl", "--user", "start", "podman.socket"];

cockpit.spawn(argv, { err: "message" })
.then(() => this.init(false))
.catch(err => {
this.setState({
userServiceAvailable: false,
userContainersLoaded: true,
userPodsLoaded: true,
userImagesLoaded: true
});
console.warn("Failed to start user podman.socket:", JSON.stringify(err));
});
async checkUserRestartService() {
const out = await cockpit.spawn(
["systemctl", "--user", "show", "--value", "-p", "LoadState", "podman-restart"],
{ environ: ["LC_ALL=C"], error: "ignore" });
this.setState({ userPodmanRestartAvailable: out.trim() === "loaded" });
}

goToServicePage(e) {
Expand All @@ -640,22 +587,13 @@ class Application extends React.Component {
<Page>
<PageSection variant={PageSectionVariants.light}>
<EmptyState variant={EmptyStateVariant.full}>
<EmptyStateHeader titleText={_("Podman service is not active")} icon={<EmptyStateIcon icon={ExclamationCircleIcon} />} headingLevel="h2" />
<EmptyStateHeader titleText={_("Podman service failed")} icon={<EmptyStateIcon icon={ExclamationCircleIcon} />} headingLevel="h2" />
<EmptyStateFooter>
<Checkbox isChecked={this.state.enableService}
id="enable"
label={_("Automatically start podman on boot")}
onChange={ (_event, checked) => this.setState({ enableService: checked }) } />
<Button onClick={this.startService}>
{_("Start podman")}
</Button>
{ cockpit.manifests.system &&
<EmptyStateActions>
<Button variant="link" onClick={this.goToServicePage}>
<Button variant="primary" onClick={this.goToServicePage}>
{_("Troubleshoot")}
</Button>
</EmptyStateActions>
}
</EmptyStateFooter>
</EmptyState>
</PageSection>
Expand Down Expand Up @@ -683,28 +621,6 @@ class Application extends React.Component {
} else
imageContainerList = null;

let startService = "";
const action = (
<>
<AlertActionLink variant='secondary' onClick={this.startService}>{_("Start")}</AlertActionLink>
<AlertActionCloseButton onClose={() => this.setState({ showStartService: false })} />
</>
);
if (!this.state.systemServiceAvailable && this.state.privileged) {
startService = (
<Alert
title={_("System Podman service is also available")}
actionClose={action} />
);
}
if (!this.state.userServiceAvailable && this.state.userServiceExists) {
startService = (
<Alert
title={_("User Podman service is also available")}
actionClose={action} />
);
}

const imageList = (
<Images
key="imageList"
Expand Down Expand Up @@ -782,7 +698,6 @@ class Application extends React.Component {
</PageSection>
<PageSection className='ct-pagesection-mobile'>
<Stack hasGutter>
{ this.state.showStartService ? startService : null }
{imageList}
{containerList}
</Stack>
Expand Down
2 changes: 1 addition & 1 deletion test/browser/browser.sh
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ podman rmi $CONTAINER
# image setup, shared with upstream tests
sh -x test/vm.install

systemctl enable --now cockpit.socket podman.socket
systemctl enable --now cockpit.socket

# HACK: https://issues.redhat.com/browse/RHEL-49567
if rpm -q selinux-policy | grep -q el10; then
Expand Down
132 changes: 9 additions & 123 deletions test/check-application
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ class TestApplication(testlib.MachineCase):
super().setUp()
m = self.machine
m.execute("""
systemctl stop podman.service; systemctl --now enable podman.socket
systemctl stop podman.service
# Ensure podman is really stopped, otherwise it keeps the containers/ directory busy
pkill -e -9 podman || true
while pgrep podman; do sleep 0.1; done
Expand All @@ -99,7 +99,7 @@ class TestApplication(testlib.MachineCase):
podman stop --time 0 --all
podman pod stop --time 0 --all
systemctl reset-failed podman.service podman.socket
systemctl reset-failed podman.service podman.socket || true
podman system reset --force
pkill -e -9 podman || true
while pgrep podman; do sleep 0.1; done
Expand Down Expand Up @@ -129,7 +129,6 @@ class TestApplication(testlib.MachineCase):
self.admin_s.execute("""
systemctl --user stop podman.service
for img in $(ls /var/lib/test-images/*.tar | grep -v cockpitws); do podman load < "$img"; done
systemctl --now --user enable podman.socket
""")
self.addCleanup(self.admin_s.execute, """
systemctl --user stop podman.service podman.socket
Expand All @@ -145,9 +144,6 @@ class TestApplication(testlib.MachineCase):
self.addCleanup(self.admin_s.execute, "podman rm --force --time 0 --all")
self.addCleanup(self.admin_s.execute, "podman pod rm --force --time 0 --all")

# But disable it globally so that "systemctl --user disable" does what we expect
m.execute("systemctl --global disable podman.socket")

self.allow_journal_messages("/run.*/podman/podman: couldn't connect.*")
self.allow_journal_messages(".*/run.*/podman/podman.*Connection reset by peer")

Expand Down Expand Up @@ -486,11 +482,6 @@ WantedBy=multi-user.target default.target
# Gain privileges
b.become_superuser(passwordless=self.machine.image == "rhel4edge")

# We are notified that we can also start the system one
b.wait_in_text("#overview div.pf-v5-c-alert .pf-v5-c-alert__title", "System Podman service is also available")
b.click("#overview div.pf-v5-c-alert .pf-v5-c-alert__action > button:contains(Start)")
b.wait_not_present("#overview div.pf-v5-c-alert .pf-v5-c-alert__title")

checkImage(b, IMG_REGISTRY, "system")
checkImage(b, IMG_REGISTRY, "admin")
b.wait_visible("#containers-containers .pod-name:contains('pod_user')")
Expand Down Expand Up @@ -1641,128 +1632,23 @@ WantedBy=multi-user.target default.target
b.click('.pf-v5-c-modal-box button:contains(Restore)')
b.wait(lambda: self.getContainerAttr("swamped-crate", "State") in 'Running')

def testNotRunning(self):
def testFailingPodmanService(self):
b = self.browser

def disable_system():
self.execute(True, "systemctl disable --now podman.socket; systemctl stop podman.service")

def enable_system():
self.execute(True, "systemctl enable --now podman.socket")

def enable_user():
self.execute(False, "systemctl --user enable --now podman.socket")

def disable_user():
self.execute(False, "systemctl --user disable --now podman.socket")

def is_active_system(string):
b.wait(lambda: self.execute(True, "systemctl is-active podman.socket || true").strip() == string)

def is_enabled_system(string):
b.wait(lambda: self.execute(True, "systemctl is-enabled podman.socket || true").strip() == string)

def is_active_user(string):
b.wait(lambda: self.execute(False, "systemctl --user is-active podman.socket || true").strip() == string)

def is_enabled_user(string):
b.wait(lambda: self.execute(False, "systemctl --user is-enabled podman.socket || true").strip() == string)

disable_system()
disable_user()
self.execute(True, "systemctl mask podman.service")
self.addCleanup(self.execute, True, "systemctl unmask podman.service")
self.execute(False, "systemctl --user mask podman.service")
self.addCleanup(self.execute, False, "systemctl --user unmask podman.service")
self.login_and_go("/podman")

# Troubleshoot action
b.click("#app .pf-v5-c-empty-state button.pf-m-link")
b.wait_text("#app .pf-v5-c-empty-state__title", "Podman service failed")
b.click("#app .pf-v5-c-empty-state button")
b.enter_page("/system/services")
# services page is too slow
with b.wait_timeout(60):
b.wait_in_text("#service-details", "podman.socket")

# Start action, with enabling (by default)
b.go("/podman")
b.enter_page("/podman")
b.click("#app .pf-v5-c-empty-state button.pf-m-primary")

b.wait_visible("#containers-containers")
b.wait_not_present("#overview div.pf-v5-c-alert.pf-m-info")

is_active_system("active")
is_active_user("active")
is_enabled_system("enabled")
is_enabled_user("enabled")

# Start action, without enabling
disable_system()
disable_user()
b.click("#app .pf-v5-c-empty-state input[type=checkbox]")
b.assert_pixels("#app .pf-v5-c-empty-state", "podman-service-disabled", skip_layouts=["medium", "mobile"])
b.click("#app .pf-v5-c-empty-state button.pf-m-primary")

b.wait_visible("#containers-containers")
is_enabled_system("disabled")
is_enabled_user("disabled")
is_active_system("active")
is_active_user("active")

b.logout()
disable_system()
# HACK: Due to https://github.com/containers/podman/issues/7180, avoid
# user podman.service to time out; make sure to start it afresh
disable_user()
enable_user()
self.login_and_go("/podman")
b.wait_in_text("#overview div.pf-v5-c-alert .pf-v5-c-alert__title", "System Podman service is also available")
b.click("#overview div.pf-v5-c-alert .pf-v5-c-alert__action > button:contains(Start)")
b.wait_not_present("#overview div.pf-v5-c-alert")
is_active_system("active")
is_active_user("active")
is_enabled_user("enabled")
is_enabled_system("enabled")

b.logout()
disable_user()
enable_system()
self.login_and_go("/podman")
b.wait_in_text("#overview div.pf-v5-c-alert .pf-v5-c-alert__title", "User Podman service is also available")
b.click("#overview div.pf-v5-c-alert .pf-v5-c-alert__action > button:contains(Start)")
b.wait_not_present("#overview div.pf-v5-c-alert")
is_active_system("active")
is_active_user("active")
is_enabled_user("enabled")
is_enabled_system("enabled")

b.logout()
disable_user()
disable_system()
self.login_and_go("/podman", superuser=False)
b.click("#app .pf-v5-c-empty-state button.pf-m-primary")
b.wait_visible("#containers-containers")
b.wait_not_present("#overview div.pf-v5-c-alert")

is_active_system("inactive")
is_active_user("active")
is_enabled_user("enabled")
is_enabled_system("disabled")
b.logout()

# no Troubleshoot action without cockpit-system package
disable_system()
disable_user()
self.restore_dir("/usr/share/cockpit/systemd")
self.machine.execute("rm /usr/share/cockpit/systemd/manifest.json")
self.login_and_go("/podman")
b.wait_visible("#app .pf-v5-c-empty-state button.pf-m-primary")
self.assertFalse(b.is_present("#app .pf-v5-c-empty-state button.pf-m-link"))
# starting still works
b.click("#app .pf-v5-c-empty-state button.pf-m-primary")
b.wait_visible("#containers-containers")

self.allow_restart_journal_messages()
self.allow_journal_messages(".*podman/podman.sock/.*: couldn't connect:.*")
self.allow_journal_messages(".*podman/podman.sock: .*Connection.*Error.*")
self.allow_journal_messages(".*podman/podman.sock/.*/events.*: received truncated HTTP response.*")

def testCreateContainerSystem(self):
self._testCreateContainer(True)

Expand Down

0 comments on commit d7de01a

Please sign in to comment.