Skip to main content

qcode/engine/
known.rs

1//! Engine refusals QCode can put into a plain sentence.
2//!
3//! An engine that refuses says why in its own words, and those words are written for someone who
4//! already knows containers: "short-name did not resolve to an alias", "permission denied while
5//! trying to connect to the docker API". A handful of these refusals come up again and again, and
6//! each of them has one thing the person can do about it. Those are recognised here, so a screen
7//! can say that thing first and keep the engine's words underneath as the detail.
8//!
9//! The matching is loose on purpose, the way [`detect`](super::detect) reads a failed `info`: a
10//! release that rewords its sentence costs the person the plain sentence, never a wrong one,
11//! because anything not recognised is left as the engine's own words.
12
13/// A refusal QCode recognises, each asking one thing of the person.
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
15pub enum Known {
16    /// The image a container was to be made from is not in this engine. Mostly an image built
17    /// with the other engine, or one removed by hand.
18    ImageMissing,
19    /// The engine's service is not running: Docker's daemon, or the socket or machine podman is
20    /// pointed at.
21    NotRunning,
22    /// The engine runs and this account may not use it: Docker's socket belongs to the `docker`
23    /// group and the person is not in it yet.
24    NoPermission,
25    /// Podman runs without root and this account has no ranges of user and group ids of its own
26    /// in `/etc/subuid` and `/etc/subgid`, which every image with more than one user needs.
27    NoIdRanges,
28}
29
30/// What Docker says when its daemon is not answering, in the releases QCode has met: 29 and
31/// earlier on Linux, and Docker Desktop on Windows.
32pub(super) const DOCKER_DOWN: &[&str] = &[
33    "cannot connect to the docker daemon",
34    "failed to connect to the docker api",
35    "is the docker daemon running",
36    "error during connect",
37    "docker_engine",
38    "dockerdesktoplinuxengine",
39];
40
41/// What podman says when the service or the virtual machine it is pointed at is not there.
42pub(super) const PODMAN_DOWN: &[&str] =
43    &["cannot connect to podman", "unable to connect to podman socket", "podman machine start", "podman machine init"];
44
45/// Docker's words for a socket this account may not open, measured against docker 29 with a
46/// socket nobody may open: "permission denied while trying to connect to the docker API at
47/// unix:///…". Earlier releases said "Got permission denied while trying to connect to the
48/// Docker daemon socket".
49const NO_PERMISSION: &[&str] = &[
50    "permission denied while trying to connect to the docker api",
51    "permission denied while trying to connect to the docker daemon",
52];
53
54/// Podman's words when an account has no id ranges: containers/storage's "no subuid ranges found
55/// for user", podman's "cannot find UID/GID for user … check rootless mode", and the warning an
56/// image with files of several owners ends in, which names the two files itself.
57const NO_ID_RANGES: &[&str] = &[
58    "no subuid ranges found",
59    "no subgid ranges found",
60    "cannot find uid/gid for user",
61    "insufficient uids or gids available in user namespace",
62    "check /etc/subuid and /etc/subgid",
63];
64
65/// The engines' words for an image they do not have, measured on podman 6.1 and docker 29 for a
66/// name nobody built: podman's "image not known" (with `--pull=never`) and its short-name
67/// sentence (without, and with no registries configured); docker's "No such image" (with
68/// `--pull=never`) and, without, its answer from Docker Hub: "pull access denied … repository
69/// does not exist".
70const IMAGE_MISSING: &[&str] = &[
71    "image not known",
72    "did not resolve to an alias",
73    "no such image",
74    "pull access denied",
75    "repository does not exist",
76];
77
78/// Which known refusal `output` is, if it is one.
79///
80/// The engine's own trouble is asked about before the image: an engine that cannot be reached
81/// cannot have been asked whether it has an image either, so its words are about itself.
82#[must_use]
83pub fn recognise(output: &str) -> Option<Known> {
84    let said = output.to_lowercase();
85    let says = |marks: &[&str]| marks.iter().any(|mark| said.contains(mark));
86    if says(NO_PERMISSION) {
87        Some(Known::NoPermission)
88    } else if says(DOCKER_DOWN) || says(PODMAN_DOWN) {
89        Some(Known::NotRunning)
90    } else if says(NO_ID_RANGES) {
91        Some(Known::NoIdRanges)
92    } else if says(IMAGE_MISSING) {
93        Some(Known::ImageMissing)
94    } else {
95        None
96    }
97}
98
99#[cfg(test)]
100mod tests {
101    use super::{Known, recognise};
102
103    #[test]
104    fn a_missing_image_is_recognised_in_either_engines_words() {
105        for said in [
106            // What the owner met: the image built with docker, the engine switched to podman.
107            "Error: short-name \"qcode/profile/claude-code\" did not resolve to an alias and no \
108             containers-registries.conf(5) was found",
109            "Error: qcode/profile/motortest-absent: image not known",
110            "Error response from daemon: No such image: qcode/profile/motortest-absent:latest",
111            "Unable to find image 'qcode/profile/x:latest' locally\nError response from daemon: pull access \
112             denied for qcode/profile/x, repository does not exist or may require 'docker login'",
113        ] {
114            assert_eq!(recognise(said), Some(Known::ImageMissing), "{said}");
115        }
116    }
117
118    #[test]
119    fn an_engine_that_is_not_running_is_recognised_before_anything_it_could_not_find() {
120        for said in [
121            "failed to connect to the docker API at unix:///run/docker.sock; check if the path is correct and if \
122             the daemon is running: dial unix /run/docker.sock: connect: no such file or directory",
123            "Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?",
124            "Error: unable to connect to Podman socket: Get \"http://d/v6.1.1/libpod/_ping\": dial unix \
125             /run/user/1000/podman/podman.sock: connect: no such file or directory",
126        ] {
127            assert_eq!(recognise(said), Some(Known::NotRunning), "{said}");
128        }
129    }
130
131    #[test]
132    fn a_socket_this_account_may_not_open_is_a_matter_of_the_docker_group() {
133        let said = "permission denied while trying to connect to the docker API at unix:///var/run/docker.sock";
134        assert_eq!(recognise(said), Some(Known::NoPermission));
135        let older = "Got permission denied while trying to connect to the Docker daemon socket at \
136                     unix:///var/run/docker.sock: Post \"http://%2Fvar%2Frun%2Fdocker.sock/v1.24/containers/create\"";
137        assert_eq!(recognise(older), Some(Known::NoPermission));
138    }
139
140    #[test]
141    fn an_account_without_id_ranges_is_recognised() {
142        for said in [
143            "Error: cannot find UID/GID for user hakan: no subuid ranges found for user \"hakan\" in /etc/subuid - \
144             check rootless mode in man pages.",
145            "Error: writing blob: adding layer with blob \"sha256:…\": processing tar file(potentially insufficient \
146             UIDs or GIDs available in user namespace (requested 0:42 for /etc/shadow): Check /etc/subuid and \
147             /etc/subgid if configured locally and run \"podman system migrate\": lchown /etc/shadow: invalid \
148             argument): exit status 1",
149        ] {
150            assert_eq!(recognise(said), Some(Known::NoIdRanges), "{said}");
151        }
152    }
153
154    #[test]
155    fn anything_else_keeps_the_engines_own_words() {
156        for said in ["Error: no space left on device", "exit status 1", ""] {
157            assert_eq!(recognise(said), None, "{said}");
158        }
159    }
160}