LCOV - code coverage report
Current view: top level - src - app.jsx Coverage Total Hit
Test: cockpit-podman Lines: 88.4 % 636 562
Test Date: 2025-05-21 17:35:03

            Line data    Source code
       1              : /*
       2              :  * This file is part of Cockpit.
       3              :  *
       4              :  * Copyright (C) 2017 Red Hat, Inc.
       5              :  *
       6              :  * Cockpit is free software; you can redistribute it and/or modify it
       7              :  * under the terms of the GNU Lesser General Public License as published by
       8              :  * the Free Software Foundation; either version 2.1 of the License, or
       9              :  * (at your option) any later version.
      10              :  *
      11              :  * Cockpit is distributed in the hope that it will be useful, but
      12              :  * WITHOUT ANY WARRANTY; without even the implied warranty of
      13              :  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
      14              :  * Lesser General Public License for more details.
      15              :  *
      16              :  * You should have received a copy of the GNU Lesser General Public License
      17              :  * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
      18              :  */
      19              : 
      20           43 : import React from 'react';
      21              : 
      22              : import { Alert, AlertActionCloseButton, AlertGroup } from "@patternfly/react-core/dist/esm/components/Alert";
      23              : import { Button } from "@patternfly/react-core/dist/esm/components/Button";
      24              : import { EmptyState, EmptyStateFooter, EmptyStateActions, EmptyStateVariant } from "@patternfly/react-core/dist/esm/components/EmptyState";
      25              : import { Page, PageSection, } from "@patternfly/react-core/dist/esm/components/Page";
      26              : import { Stack } from "@patternfly/react-core/dist/esm/layouts/Stack";
      27              : import { ExclamationCircleIcon } from '@patternfly/react-icons';
      28              : import { WithDialogs } from "dialogs.jsx";
      29              : 
      30              : import cockpit from 'cockpit';
      31              : import { superuser } from "superuser";
      32              : 
      33              : import ContainerHeader from './ContainerHeader.jsx';
      34              : import Containers from './Containers.jsx';
      35              : import Images from './Images.jsx';
      36              : import * as client from './client.js';
      37              : import rest from './rest.js';
      38              : import { makeKey, WithPodmanInfo, debug } from './util.js';
      39              : 
      40           43 : const _ = cockpit.gettext;
      41              : 
      42              : // sort order of "users" state for dialogs: system, session user, then other users by ascending name
      43           27 : function compareUser(a, b) {
      44           27 :     if (a.uid === 0)
      45           27 :         return -1;
      46           27 :     if (b.uid === 0)
      47           27 :         return 1;
      48            1 :     if (a.uid === null)
      49            1 :         return -1;
      50            1 :     if (b.uid === null)
      51            1 :         return 1;
      52            1 :     return a.name.localeCompare(b.name);
      53           27 : }
      54              : 
      55           43 : class Application extends React.Component {
      56           43 :     constructor(props) {
      57           43 :         super(props);
      58           43 :         this.state = {
      59              :             // currently connected services per user: { con, uid, name, imagesLoaded, containersLoaded, podsLoaded }
      60              :             // start with dummy state to wait for initialization
      61           43 :             users: [{ con: null, uid: 0, name: _("system") }, { con: null, uid: null, name: _("user") }],
      62           43 :             images: null,
      63           43 :             containers: null,
      64           43 :             containersFilter: "all",
      65           43 :             containersStats: {},
      66           43 :             textFilter: "",
      67           43 :             ownerFilter: "all",
      68           43 :             dropDownValue: 'Everything',
      69           43 :             notifications: [],
      70           43 :             version: '1.3.0',
      71           43 :             selinuxAvailable: false,
      72           43 :             podmanRestartAvailable: false,
      73           43 :             userPodmanRestartAvailable: false,
      74           43 :             userLingeringEnabled: null,
      75           43 :             location: {},
      76           43 :         };
      77           43 :         this.onAddNotification = this.onAddNotification.bind(this);
      78           43 :         this.onDismissNotification = this.onDismissNotification.bind(this);
      79           43 :         this.onFilterChanged = this.onFilterChanged.bind(this);
      80           43 :         this.onOwnerChanged = this.onOwnerChanged.bind(this);
      81           43 :         this.onContainerFilterChanged = this.onContainerFilterChanged.bind(this);
      82           43 :         this.updateContainer = this.updateContainer.bind(this);
      83           43 :         this.goToServicePage = this.goToServicePage.bind(this);
      84           43 :         this.onNavigate = this.onNavigate.bind(this);
      85              : 
      86           43 :         this.pendingUpdateContainer = {}; // key (uid-id) → promise
      87           43 :     }
      88              : 
      89            1 :     onAddNotification(notification) {
      90            1 :         notification.index = this.state.notifications.length;
      91              : 
      92            1 :         this.setState(prevState => ({
      93            1 :             notifications: [
      94            1 :                 ...prevState.notifications,
      95            1 :                 notification
      96            1 :             ]
      97            1 :         }));
      98            1 :     }
      99              : 
     100            0 :     onDismissNotification(notificationIndex) {
     101            0 :         const notificationsArray = this.state.notifications.concat();
     102            0 :         const index = notificationsArray.findIndex(current => current.index == notificationIndex);
     103              : 
     104            0 :         if (index !== -1) {
     105            0 :             notificationsArray.splice(index, 1);
     106            0 :             this.setState({ notifications: notificationsArray });
     107            0 :         }
     108            0 :     }
     109              : 
     110           18 :     updateUrl(options) {
     111           18 :         cockpit.location.go([], options);
     112           18 :     }
     113              : 
     114            3 :     onFilterChanged(value) {
     115            3 :         this.setState({
     116            3 :             textFilter: value
     117            3 :         });
     118              : 
     119            3 :         const options = { ...this.state.location };
     120            3 :         if (value === "")
     121            3 :             delete options.name;
     122              :         else
     123            3 :             options.name = value;
     124            3 :         this.updateUrl(options);
     125            3 :     }
     126              : 
     127            1 :     onOwnerChanged(value) {
     128            1 :         this.setState({
     129            1 :             ownerFilter: value
     130            1 :         });
     131              : 
     132            1 :         const options = { ...this.state.location };
     133            1 :         if (value == "all")
     134            0 :             delete options.owner;
     135              :         else
     136            1 :             options.owner = value.toString();
     137            1 :         this.updateUrl(options);
     138            1 :     }
     139              : 
     140           17 :     onContainerFilterChanged(value) {
     141           17 :         this.setState({
     142           17 :             containersFilter: value
     143           17 :         });
     144              : 
     145           17 :         const options = { ...this.state.location };
     146           17 :         if (value == "running")
     147            4 :             delete options.container;
     148              :         else
     149           17 :             options.container = value;
     150           17 :         this.updateUrl(options);
     151           17 :     }
     152              : 
     153           40 :     updateState(state, key, newValue) {
     154           40 :         this.setState(prevState => {
     155           40 :             return {
     156           40 :                 [state]: { ...prevState[state], [key]: newValue }
     157           40 :             };
     158           40 :         });
     159           40 :     }
     160              : 
     161           43 :     updateContainerStats(con) {
     162           43 :         client.streamContainerStats(con, reply => {
     163           43 :             if (reply.Error != null) // executed when container stop
     164            0 :                 console.warn("Failed to update container stats:", JSON.stringify(reply.message));
     165           43 :             else {
     166           24 :                 reply.Stats.forEach(stat => this.updateState("containersStats", makeKey(con.uid, stat.ContainerID), stat));
     167           43 :             }
     168           43 :         }).catch(ex => {
     169            0 :             if (ex.cause == "no support for CGroups V1 in rootless environments" || ex.cause == "Container stats resource only available for cgroup v2") {
     170            0 :                 console.log("This OS does not support CgroupsV2. Some information may be missing.");
     171            0 :             } else
     172            0 :                 console.warn("Failed to update container stats:", JSON.stringify(ex.message));
     173            0 :         });
     174           43 :     }
     175              : 
     176           43 :     initContainers(con) {
     177           43 :         return client.getContainers(con)
     178           43 :                 .then(containerList => Promise.all(
     179           13 :                     containerList.map(container => client.inspectContainer(con, container.Id))
     180           43 :                 ))
     181           43 :                 .then(containerDetails => {
     182           43 :                     this.setState(prevState => {
     183              :                         // keep/copy the containers of other users
     184           43 :                         const copyContainers = {};
     185            3 :                         Object.entries(prevState.containers || {}).forEach(([id, container]) => {
     186            3 :                             if (container.uid !== con.uid)
     187            3 :                                 copyContainers[id] = container;
     188            3 :                         });
     189           13 :                         for (const detail of containerDetails) {
     190           13 :                             detail.uid = con.uid;
     191           13 :                             detail.key = makeKey(con.uid, detail.Id);
     192           13 :                             copyContainers[detail.key] = detail;
     193           13 :                         }
     194              : 
     195           27 :                         const users = prevState.users.map(u => u.uid === con.uid ? { ...u, containersLoaded: true } : u);
     196           43 :                         return { containers: copyContainers, users };
     197           43 :                     });
     198           43 :                     this.updateContainerStats(con);
     199           43 :                 })
     200            0 :                 .catch(e => console.warn("initContainers uid", con.uid, "getContainers failed:", e.toString()));
     201           43 :     }
     202              : 
     203           43 :     updateImages(con) {
     204           43 :         client.getImages(con)
     205           43 :                 .then(reply => {
     206           43 :                     this.setState(prevState => {
     207              :                         // Copy only images that could not be deleted with this event
     208              :                         // So when event from one uid comes, only copy the other images
     209           43 :                         const copyImages = {};
     210           38 :                         Object.entries(prevState.images || {}).forEach(([Id, image]) => {
     211           38 :                             if (image.uid !== con.uid)
     212           26 :                                 copyImages[Id] = image;
     213           38 :                         });
     214           43 :                         Object.entries(reply).forEach(([Id, image]) => {
     215           43 :                             image.uid = con.uid;
     216           43 :                             image.key = makeKey(con.uid, Id);
     217           43 :                             copyImages[image.key] = image;
     218           43 :                         });
     219              : 
     220           27 :                         const users = prevState.users.map(u => u.uid === con.uid ? { ...u, imagesLoaded: true } : u);
     221           43 :                         return { images: copyImages, users };
     222           43 :                     });
     223           43 :                 })
     224            1 :                 .catch(ex => {
     225            1 :                     console.warn("Failed to do updateImages for uid", con.uid, ":", JSON.stringify(ex));
     226            1 :                 });
     227           43 :     }
     228              : 
     229           43 :     updatePods(con) {
     230           43 :         return client.getPods(con)
     231           43 :                 .then(reply => {
     232           43 :                     this.setState(prevState => {
     233              :                         // Copy only pods that could not be deleted with this event
     234              :                         // So when event from one uid comes, only copy the other pods
     235           43 :                         const copyPods = {};
     236            2 :                         Object.entries(prevState.pods || {}).forEach(([id, pod]) => {
     237            2 :                             if (pod.uid !== con.uid)
     238            1 :                                 copyPods[id] = pod;
     239            2 :                         });
     240            0 :                         for (const pod of reply || []) {
     241            5 :                             pod.uid = con.uid;
     242            5 :                             pod.key = makeKey(con.uid, pod.Id);
     243            5 :                             copyPods[pod.key] = pod;
     244            5 :                         }
     245              : 
     246           27 :                         const users = prevState.users.map(u => u.uid === con.uid ? { ...u, podsLoaded: true } : u);
     247           43 :                         return { pods: copyPods, users };
     248           43 :                     });
     249           43 :                 })
     250            0 :                 .catch(ex => {
     251            0 :                     console.warn("Failed to do updatePods for uid", con.uid, ":", JSON.stringify(ex));
     252            0 :                 });
     253           43 :     }
     254              : 
     255           37 :     updateContainer(con, id, event) {
     256              :         /* when firing off multiple calls in parallel, podman can return them in a random order.
     257              :          * This messes up the state. So we need to serialize them for a particular container. */
     258           37 :         const key = makeKey(con.uid, id);
     259           37 :         const wait = this.pendingUpdateContainer[key] ?? Promise.resolve();
     260              : 
     261           37 :         const new_wait = wait.then(() => client.inspectContainer(con, id))
     262           37 :                 .then(details => {
     263           37 :                     details.uid = con.uid;
     264           37 :                     details.key = key;
     265              :                     // HACK: during restart State never changes from "running"
     266              :                     //       override it to reconnect console after restart
     267           37 :                     if (event?.Action === "restart")
     268            0 :                         details.State.Status = "restarting";
     269           37 :                     this.updateState("containers", key, details);
     270           37 :                 })
     271            6 :                 .catch(e => console.warn("updateContainer uid", con.uid, "inspectContainer failed:", e.toString()));
     272           37 :         this.pendingUpdateContainer[key] = new_wait;
     273           37 :         new_wait.finally(() => { delete this.pendingUpdateContainer[key] });
     274              : 
     275           37 :         return new_wait;
     276           37 :     }
     277              : 
     278            4 :     updateImage(con, id) {
     279            4 :         client.getImages(con, id)
     280            4 :                 .then(reply => {
     281            4 :                     const image = reply[id];
     282            4 :                     image.uid = con.uid;
     283            4 :                     image.key = makeKey(con.uid, id);
     284            4 :                     this.updateState("images", image.key, image);
     285            4 :                 })
     286            2 :                 .catch(ex => {
     287            2 :                     console.warn("Failed to do updateImage for uid", con.uid, ":", JSON.stringify(ex));
     288            2 :                 });
     289            4 :     }
     290              : 
     291            8 :     updatePod(con, id) {
     292            8 :         return client.getPods(con, id)
     293            8 :                 .then(reply => {
     294            8 :                     if (reply && reply.length > 0) {
     295            8 :                         const pod = reply[0];
     296              : 
     297            8 :                         pod.uid = con.uid;
     298            8 :                         pod.key = makeKey(con.uid, id);
     299            8 :                         this.updateState("pods", pod.key, pod);
     300            8 :                     }
     301            8 :                 })
     302            0 :                 .catch(ex => {
     303            0 :                     console.warn("Failed to do updatePod for uid", con.uid, ":", JSON.stringify(ex));
     304            0 :                 });
     305            8 :     }
     306              : 
     307              :     // see https://docs.podman.io/en/latest/markdown/podman-events.1.html
     308              : 
     309           31 :     handleImageEvent(event, con) {
     310           31 :         switch (event.Action) {
     311            2 :         case 'push':
     312            2 :         case 'save':
     313            4 :         case 'tag':
     314            4 :             this.updateImage(con, event.Actor.ID);
     315            4 :             break;
     316           28 :         case 'pull': // Pull event has not event.id
     317           31 :         case 'untag':
     318           31 :         case 'remove':
     319           31 :         case 'prune':
     320           31 :         case 'build':
     321           31 :             this.updateImages(con);
     322           31 :             break;
     323            0 :         default:
     324            0 :             console.warn('Unhandled event type ', event.Type, event.Action);
     325           31 :         }
     326           31 :     }
     327              : 
     328           37 :     handleContainerEvent(event, con) {
     329           37 :         const id = event.Actor.ID;
     330              : 
     331           37 :         switch (event.Action) {
     332              :         /* The following events do not need to trigger any state updates */
     333            2 :         case 'attach':
     334            9 :         case 'exec':
     335            9 :         case 'export':
     336            9 :         case 'import':
     337           32 :         case 'init':
     338           32 :         case 'kill':
     339           32 :         case 'mount':
     340           32 :         case 'prune':
     341           32 :         case 'restart':
     342           32 :         case 'sync':
     343           32 :         case 'unmount':
     344           32 :         case 'wait':
     345           32 :             break;
     346              :         /* The following events need only to update the Container list
     347              :          * We do get the container affected in the event object but for
     348              :          * now we 'll do a batch update
     349              :          */
     350           32 :         case 'start':
     351              :             // HACK: We don't get 'started' event for pods got started by the first container which was added to them
     352              :             // https://github.com/containers/podman/issues/7213
     353           32 :             (event.Actor.Attributes.podId
     354            8 :                 ? this.updatePod(con, event.Actor.Attributes.podId)
     355           28 :                 : this.updatePods(con)
     356           32 :             ).then(() => this.updateContainer(con, id, event));
     357           32 :             break;
     358            1 :         case 'checkpoint':
     359           16 :         case 'cleanup':
     360           35 :         case 'create':
     361           35 :         case 'died':
     362           35 :         case 'exec_died': // HACK: pick up health check runs with older podman versions, see https://github.com/containers/podman/issues/19237
     363           35 :         case 'health_status':
     364           37 :         case 'pause':
     365           37 :         case 'restore':
     366           37 :         case 'stop':
     367           37 :         case 'unpause':
     368           37 :         case 'rename': // rename event is available starting podman v4.1; until then the container does not get refreshed after renaming
     369           37 :             this.updateContainer(con, id, event);
     370           37 :             break;
     371              : 
     372            8 :         case 'remove':
     373            8 :             this.setState(prevState => {
     374            8 :                 const containers = { ...prevState.containers };
     375            8 :                 delete containers[makeKey(con.uid, id)];
     376            8 :                 let pods;
     377              : 
     378            1 :                 if (event.Actor.Attributes.podId) {
     379            1 :                     const podKey = makeKey(con.uid, event.Actor.Attributes.podId);
     380            1 :                     const newPod = { ...prevState.pods[podKey] };
     381            1 :                     newPod.Containers = newPod.Containers.filter(container => container.Id !== id);
     382            1 :                     pods = { ...prevState.pods, [podKey]: newPod };
     383            0 :                 } else {
     384              :                     // HACK: with podman < 4.3.0 we don't get a pod event when a container in a pod is removed
     385              :                     // https://github.com/containers/podman/issues/15408
     386            7 :                     pods = prevState.pods;
     387            7 :                     this.updatePods(con);
     388            7 :                 }
     389              : 
     390            8 :                 return { containers, pods };
     391            8 :             });
     392            8 :             break;
     393              : 
     394              :         // only needs to update the Image list, this ought to be an image event
     395            2 :         case 'commit':
     396            2 :             this.updateImages(con);
     397            2 :             break;
     398            0 :         default:
     399            0 :             console.warn('Unhandled event type ', event.Type, event.Action);
     400           37 :         }
     401           37 :     }
     402              : 
     403            8 :     handlePodEvent(event, con) {
     404            8 :         switch (event.Action) {
     405            8 :         case 'create':
     406            8 :         case 'kill':
     407            8 :         case 'pause':
     408            8 :         case 'start':
     409            8 :         case 'stop':
     410            8 :         case 'unpause':
     411            8 :             this.updatePod(con, event.Actor.ID);
     412            8 :             break;
     413            1 :         case 'remove':
     414            1 :             this.setState(prevState => {
     415            1 :                 const pods = { ...prevState.pods };
     416            1 :                 delete pods[makeKey(con.uid, event.Actor.ID)];
     417            1 :                 return { pods };
     418            1 :             });
     419            1 :             break;
     420            0 :         default:
     421            0 :             console.warn('Unhandled event type ', event.Type, event.Action);
     422            8 :         }
     423            8 :     }
     424              : 
     425           40 :     handleEvent(event, con) {
     426           40 :         switch (event.Type) {
     427           37 :         case 'container':
     428           37 :             this.handleContainerEvent(event, con);
     429           37 :             break;
     430           31 :         case 'image':
     431           31 :             this.handleImageEvent(event, con);
     432           31 :             break;
     433            8 :         case 'pod':
     434            8 :             this.handlePodEvent(event, con);
     435            8 :             break;
     436            2 :         default:
     437            2 :             console.warn('Unhandled event type ', event.Type);
     438           40 :         }
     439           40 :     }
     440              : 
     441            2 :     cleanupAfterService(con) {
     442            2 :         debug("cleanupAfterService", con.uid, "current owner filter:", this.state.ownerFilter);
     443            2 :         ["images", "containers", "pods"].forEach(t => {
     444            2 :             if (this.state[t])
     445            2 :                 this.setState(prevState => {
     446            2 :                     const copy = {};
     447            0 :                     Object.entries(prevState[t] || {}).forEach(([id, v]) => {
     448            2 :                         if (v.uid !== con.uid)
     449            2 :                             copy[id] = v;
     450            2 :                     });
     451            2 :                     return { [t]: copy };
     452            2 :                 });
     453            2 :         });
     454              : 
     455              :         // keep dummy (null) connections from other users, only remove valid ones
     456            2 :         this.setState(prevState => ({ users: prevState.users.filter(u => u.con === null || u.uid !== con.uid) }));
     457              : 
     458              :         // reset owner filter if the current filter is the closed connection
     459            2 :         if (con.uid == this.state.ownerFilter)
     460            0 :             this.onOwnerChanged("all");
     461            2 :     }
     462              : 
     463           43 :     async init(uid, username) {
     464           43 :         debug("init uid", uid, "name", username);
     465           43 :         const system = uid === 0;
     466           41 :         const is_other_user = (uid !== 0 && uid !== null);
     467              : 
     468           43 :         let con = null;
     469              : 
     470           43 :         try {
     471           43 :             const start_args = [
     472            1 :                 ...(is_other_user ? ["runuser", "-u", username, "--"] : []),
     473           43 :                 "systemctl",
     474           41 :                 ...(system ? [] : ["--user"]),
     475           43 :                 "start", "podman.socket"
     476           43 :             ];
     477            1 :             const environ = is_other_user ? ["XDG_RUNTIME_DIR=/run/user/" + uid] : [];
     478           41 :             await cockpit.spawn(start_args, { superuser: uid === null ? null : "require", err: "message", environ });
     479           43 :             con = rest.connect(uid);
     480           43 :             const reply = await client.getInfo(con);
     481           43 :             this.setState(prevState => {
     482           43 :                 const users = prevState.users.filter(u => u.uid !== uid);
     483           43 :                 users.push({ con, uid, name: username, containersLoaded: false, podsLoaded: false, imagesLoaded: false });
     484              :                 // keep a nice sort order for dialogs
     485           43 :                 users.sort(compareUser);
     486           43 :                 debug("init uid", uid, "username", username, "new users:", users);
     487           43 :                 return {
     488           43 :                     users,
     489           43 :                     version: reply.version.Version,
     490           43 :                     registries: reply.registries,
     491           43 :                     cgroupVersion: reply.host.cgroupVersion,
     492           43 :                 };
     493           43 :             });
     494           15 :         } catch (err) {
     495           15 :             if (!system || err.problem != 'access-denied')
     496            0 :                 console.warn("init uid", uid, "getInfo failed:", err.toString());
     497              : 
     498           15 :             this.setState(prevState => ({ users: prevState.users.filter(u => u.uid !== uid) }));
     499           15 :             return;
     500           15 :         }
     501              : 
     502           43 :         this.updateImages(con);
     503           43 :         this.initContainers(con);
     504           43 :         this.updatePods(con);
     505              : 
     506           40 :         client.streamEvents(con, message => this.handleEvent(message, con))
     507            0 :                 .catch(e => console.error("uid", uid, "streamEvents failed:", e.toString()))
     508            2 :                 .finally(() => {
     509            2 :                     console.log("uid", uid, "podman service closed");
     510            2 :                     this.cleanupAfterService(con);
     511            2 :                 });
     512           43 :     }
     513              : 
     514           43 :     componentDidMount() {
     515           43 :         superuser.addEventListener("changed", () => this.init(0, _("system")));
     516              : 
     517           43 :         cockpit.user().then(user => {
     518              :             // there is no "user service" for root, ignore that
     519            2 :             if (user.id === 0) {
     520              :                 // clear the dummy init user, otherwise UI waits forever for initialization
     521            2 :                 this.setState(prevState => ({ users: prevState.users.filter(u => u.uid !== null) }));
     522            2 :                 return;
     523            2 :             }
     524              : 
     525           41 :             cockpit.script("echo $XDG_RUNTIME_DIR")
     526           41 :                     .then(xrd => {
     527           41 :                         sessionStorage.setItem('XDG_RUNTIME_DIR', xrd.trim());
     528            0 :                         this.init(null, user.name || _("User"));
     529           41 :                         this.checkUserRestartService();
     530           41 :                     })
     531            0 :                     .catch(e => console.log("Could not read $XDG_RUNTIME_DIR:", e.message));
     532              : 
     533              :             // HACK: https://github.com/systemd/systemd/issues/22244#issuecomment-1210357701
     534           41 :             cockpit.file(`/var/lib/systemd/linger/${user.name}`).watch((content, tag) => {
     535           40 :                 if (content == null && tag === '-') {
     536           40 :                     this.setState({ userLingeringEnabled: false });
     537            0 :                 } else {
     538            1 :                     this.setState({ userLingeringEnabled: true });
     539            1 :                 }
     540           41 :             });
     541              : 
     542              :             // detect which other users have containers running
     543           41 :             cockpit.spawn([
     544           41 :                 'find', '/sys/fs/cgroup',
     545              :                 // RHEL 8 version still calls it "podman-*.scope", newer ones "libpod*"
     546           41 :                 '(', '-name', 'libpod.*scope', '-o', '-name', 'podman-*.scope',
     547           41 :                 '-o', '-name', 'libpod-payload*', ')',
     548           41 :                 '-exec', 'stat', '--format=%u %U', '{}', ';'],
     549              :                           // this find command doesn't need root, but user switching does;
     550              :                           // hence skip the whole detection for unpriv sessions
     551           41 :                           { superuser: "require", error: "message" })
     552           27 :                     .then(output => {
     553           27 :                         const other_users = [];
     554           27 :                         const trimmed = output.trim();
     555           27 :                         if (!trimmed)
     556           27 :                             return;
     557              : 
     558           27 :                         trimmed.split('\n').forEach(line => {
     559           27 :                             const [uid_str, username] = line.split(' ');
     560           27 :                             const uid = parseInt(uid_str);
     561            0 :                             if (isNaN(uid)) {
     562            0 :                                 console.error(`User container detection: invalid uid '${uid_str}' in output '${output}'`); // not-covered: Should Not Happen™
     563            0 :                                 return; // not-covered: dito
     564            0 :                             }
     565              :                             // ignore standard users
     566           27 :                             if (uid === 0 || uid === user.id)
     567           27 :                                 return;
     568            1 :                             if (!other_users.find(u => u.uid === uid))
     569            1 :                                 other_users.push({ uid, name: username, con: null });
     570           27 :                         });
     571           27 :                         debug("other users who have containers running:", JSON.stringify(other_users));
     572           27 :                         this.setState(prevState => ({ users: prevState.users.concat(other_users) }));
     573           27 :                     })
     574           14 :                     .catch(ex => {
     575           14 :                         if (ex.problem == 'access-denied')
     576            0 :                             debug("unprivileged session, skipping detection of other users");
     577              :                         else
     578            0 :                             console.warn("failed to detect other users:", ex);
     579           14 :                     });
     580           43 :         });
     581              : 
     582           43 :         cockpit.spawn("selinuxenabled", { error: "ignore" })
     583           43 :                 .then(() => this.setState({ selinuxAvailable: true }))
     584            0 :                 .catch(() => this.setState({ selinuxAvailable: false }));
     585              : 
     586           43 :         cockpit.spawn(["systemctl", "show", "--value", "-p", "LoadState", "podman-restart"], { environ: ["LC_ALL=C"], error: "ignore" })
     587           43 :                 .then(out => this.setState({ podmanRestartAvailable: out.trim() === "loaded" }));
     588              : 
     589           43 :         cockpit.addEventListener("locationchanged", this.onNavigate);
     590           43 :         this.onNavigate();
     591           43 :     }
     592              : 
     593            0 :     componentWillUnmount() {
     594            0 :         cockpit.removeEventListener("locationchanged", this.onNavigate);
     595            0 :     }
     596              : 
     597           43 :     onNavigate() {
     598              :         // HACK: Use usePageLocation when this is rewritten into a functional component
     599           43 :         const { options, path } = cockpit.location;
     600           43 :         this.setState({ location: options }, () => {
     601              :             // only use the root path
     602           43 :             if (path.length === 0) {
     603            3 :                 if (options.name) {
     604            3 :                     this.onFilterChanged(options.name);
     605            3 :                 }
     606           17 :                 if (options.container) {
     607           17 :                     this.onContainerFilterChanged(options.container);
     608           17 :                 }
     609           43 :                 if (["all", undefined].includes(options.owner)) {
     610              :                     // disconnect all non-standard users
     611           43 :                     this.setState(prevState => ({
     612           43 :                         users: prevState.users.map(u => {
     613            0 :                             if (u.uid !== 0 && u.uid !== null && u.con) {
     614            0 :                                 debug("onNavigate All: closing unused connection to", u.name);
     615            0 :                                 u.con.close();
     616            0 :                                 return { uid: u.uid, name: u.name, con: null };
     617            0 :                             } else
     618           43 :                                 return u;
     619           43 :                         }),
     620           43 :                         ownerFilter: "all",
     621           43 :                     }));
     622            1 :                 } else {
     623            0 :                     const uid = options.owner === "user" ? null : parseInt(options.owner);
     624            1 :                     const user = this.state.users.find(u => u.uid === uid);
     625            1 :                     if (user) {
     626              :                         // disconnect other non-standard users, to avoid piling up connections
     627            1 :                         this.setState(prevState => ({
     628            1 :                             users: prevState.users.map(u => {
     629            0 :                                 if (u.uid !== uid && u.uid !== 0 && u.uid !== null && u.con) {
     630            0 :                                     debug("onNavigate", user.name, ": closing unused connection to", u.name);
     631            0 :                                     u.con.close();
     632            0 :                                     return { uid: u.uid, name: u.name, con: null };
     633            0 :                                 } else
     634            1 :                                     return u;
     635            1 :                             }),
     636            0 :                             ownerFilter: uid === null ? "user" : uid,
     637            1 :                         }), () => {
     638            1 :                             if (user.con === null) {
     639            1 :                                 debug("onNavigate", user.name, ": initializing connection");
     640            1 :                                 this.init(user.uid, user.name);
     641            0 :                             } else {
     642            0 :                                 debug("onNavigate", user.name, ": connection already initialized");
     643            0 :                             }
     644            1 :                         });
     645            0 :                     } else {
     646            0 :                         console.warn("Unknown user", options.owner, "in URL, ignoring");
     647            0 :                         debug("known users:", JSON.stringify(this.state.users.map(u => [u.name, u.uid])));
     648              :                         // reset URL to current value
     649            0 :                         this.updateUrl({ ...this.state.location, owner: this.state.ownerFilter });
     650            0 :                     }
     651            1 :                 }
     652           43 :             }
     653           43 :         });
     654           43 :     }
     655              : 
     656           41 :     async checkUserRestartService() {
     657           41 :         const out = await cockpit.spawn(
     658           41 :             ["systemctl", "--user", "show", "--value", "-p", "LoadState", "podman-restart"],
     659           41 :             { environ: ["LC_ALL=C"], error: "ignore" });
     660           41 :         this.setState({ userPodmanRestartAvailable: out.trim() === "loaded" });
     661           41 :     }
     662              : 
     663            0 :     goToServicePage(e) {
     664            0 :         if (!e || e.button !== 0)
     665            0 :             return;
     666            0 :         cockpit.jump("/system/services#/podman.socket");
     667            0 :     }
     668              : 
     669           43 :     render() {
     670              :         // show troubleshoot if no users are available, i.e. all user's podman services failed
     671            1 :         if (this.state.users.length === 0) {
     672            1 :             return (
     673            1 :                 <Page className="no-masthead-sidebar">
     674            1 :                     <PageSection hasBodyWrapper={false}>
     675            1 :                         <EmptyState headingLevel="h2" icon={ExclamationCircleIcon} titleText={_("Podman service failed")} variant={EmptyStateVariant.full}>
     676            1 :                             <EmptyStateFooter>
     677            1 :                                 <EmptyStateActions>
     678            1 :                                     <Button variant="primary" onClick={this.goToServicePage}>
     679            1 :                                         {_("Troubleshoot")}
     680            1 :                                     </Button>
     681            1 :                                 </EmptyStateActions>
     682            1 :                             </EmptyStateFooter>
     683            1 :                         </EmptyState>
     684            1 :                     </PageSection>
     685            1 :                 </Page>
     686              :             );
     687            1 :         }
     688              : 
     689           32 :         if (this.state.users.find(u => u.con === null && (u.uid === 0 || u.uid === null))) // not initialized yet
     690           43 :             return null;
     691              : 
     692           43 :         let imageContainerList = {};
     693           43 :         if (this.state.containers !== null) {
     694           41 :             Object.keys(this.state.containers).forEach(c => {
     695           41 :                 const container = this.state.containers[c];
     696           41 :                 const imageKey = makeKey(container.uid, container.Image);
     697           41 :                 if (!imageContainerList[imageKey])
     698           41 :                     imageContainerList[imageKey] = [];
     699           41 :                 imageContainerList[imageKey].push({
     700           41 :                     container,
     701           41 :                     stats: this.state.containersStats[makeKey(container.uid, container.Id)],
     702           41 :                 });
     703           41 :             });
     704           43 :         } else
     705           38 :             imageContainerList = null;
     706              : 
     707           43 :         const loadingImages = this.state.users.find(u => u.con && !u.imagesLoaded);
     708           43 :         const loadingContainers = this.state.users.find(u => u.con && !u.containersLoaded);
     709           43 :         const loadingPods = this.state.users.find(u => u.con && !u.podsLoaded);
     710              : 
     711           43 :         const imageList = (
     712           43 :             <Images
     713           43 :                 key="imageList"
     714           43 :                 images={loadingImages ? null : this.state.images}
     715           43 :                 imageContainerList={imageContainerList}
     716           43 :                 onAddNotification={this.onAddNotification}
     717           43 :                 textFilter={this.state.textFilter}
     718           43 :                 ownerFilter={this.state.ownerFilter}
     719            2 :                 showAll={ () => this.setState({ containersFilter: "all" }) }
     720           43 :                 users={this.state.users}
     721           43 :             />
     722              :         );
     723           43 :         const containerList = (
     724           43 :             <Containers
     725           43 :                 key="containerList"
     726           43 :                 version={this.state.version}
     727           43 :                 images={loadingImages ? null : this.state.images}
     728           43 :                 containers={loadingContainers ? null : this.state.containers}
     729           43 :                 pods={loadingPods ? null : this.state.pods}
     730           43 :                 containersStats={this.state.containersStats}
     731           43 :                 filter={this.state.containersFilter}
     732           43 :                 handleFilterChange={this.onContainerFilterChanged}
     733           43 :                 textFilter={this.state.textFilter}
     734           43 :                 ownerFilter={this.state.ownerFilter}
     735           43 :                 users={this.state.users}
     736           43 :                 onAddNotification={this.onAddNotification}
     737           43 :                 cgroupVersion={this.state.cgroupVersion}
     738           43 :                 updateContainer={this.updateContainer}
     739           43 :             />
     740              :         );
     741              : 
     742           43 :         const notificationList = (
     743           43 :             <AlertGroup isToast>
     744            1 :                 {this.state.notifications.map((notification, index) => {
     745            1 :                     return (
     746            1 :                         <Alert key={index} title={notification.error} variant={notification.type}
     747            1 :                                isLiveRegion
     748            0 :                                actionClose={<AlertActionCloseButton onClose={() => this.onDismissNotification(notification.index)} />}>
     749            1 :                             {notification.errorDetail}
     750            1 :                         </Alert>
     751              :                     );
     752            1 :                 })}
     753           43 :             </AlertGroup>
     754              :         );
     755              : 
     756           43 :         const contextInfo = {
     757           43 :             cgroupVersion: this.state.cgroupVersion,
     758           43 :             registries: this.state.registries,
     759           43 :             selinuxAvailable: this.state.selinuxAvailable,
     760           43 :             podmanRestartAvailable: this.state.podmanRestartAvailable,
     761           43 :             userPodmanRestartAvailable: this.state.userPodmanRestartAvailable,
     762           43 :             userLingeringEnabled: this.state.userLingeringEnabled,
     763           43 :             version: this.state.version,
     764           43 :         };
     765              : 
     766           43 :         return (
     767           43 :             <WithPodmanInfo value={contextInfo}>
     768           43 :                 <WithDialogs>
     769           43 :                     <Page id="overview" key="overview" className="no-masthead-sidebar">
     770           43 :                         {notificationList}
     771           43 :                         <PageSection hasBodyWrapper={false} className="content-filter"
     772              :                         >
     773           43 :                             <ContainerHeader
     774           43 :                               handleFilterChanged={this.onFilterChanged}
     775           43 :                               handleOwnerChanged={this.onOwnerChanged}
     776           43 :                               ownerFilter={this.state.ownerFilter}
     777           43 :                               textFilter={this.state.textFilter}
     778           43 :                               users={this.state.users}
     779           43 :                             />
     780           43 :                         </PageSection>
     781           43 :                         <PageSection hasBodyWrapper={false} className='ct-pagesection-mobile'>
     782           43 :                             <Stack hasGutter>
     783           43 :                                 {imageList}
     784           43 :                                 {containerList}
     785           43 :                             </Stack>
     786           43 :                         </PageSection>
     787           43 :                     </Page>
     788           43 :                 </WithDialogs>
     789           43 :             </WithPodmanInfo>
     790              :         );
     791           43 :     }
     792           43 : }
     793              : 
     794           43 : export default Application;
        

Generated by: LCOV version 2.0-1