1#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
15pub enum Known {
16 ImageMissing,
19 NotRunning,
22 NoPermission,
25 NoIdRanges,
28}
29
30pub(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
41pub(super) const PODMAN_DOWN: &[&str] =
43 &["cannot connect to podman", "unable to connect to podman socket", "podman machine start", "podman machine init"];
44
45const 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
54const 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
65const 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#[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 "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}