• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

kdash-rs / kdash / 27765565575

18 Jun 2026 02:11PM UTC coverage: 76.366% (+0.04%) from 76.331%
27765565575

push

github

web-flow
Merge pull request #533 from kdash-rs/fix/kubectl-context-532

fix: pass selected context to kubectl shell commands

109 of 150 new or added lines in 6 files covered. (72.67%)

1 existing line in 1 file now uncovered.

14017 of 18355 relevant lines covered (76.37%)

116.66 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

67.8
/src/cmd/shell.rs
1
use std::process::{Command, ExitStatus, Stdio};
2

3
use anyhow::anyhow;
4

5
use super::{is_valid_kubectl_arg, push_context_arg};
6

7
const SHELL_CANDIDATES: [&str; 2] = ["/bin/bash", "/bin/sh"];
8

9
#[derive(Clone, Debug, Eq, PartialEq)]
10
pub struct ShellExecTarget {
11
  pub namespace: String,
12
  pub pod: String,
13
  pub container: String,
14
  /// In-app selected context, or `None` to use the kubeconfig default (#532).
15
  pub context: Option<String>,
16
}
17

18
#[derive(Clone, Debug, Eq, PartialEq)]
19
pub struct ShellExecCommand {
20
  pub program: String,
21
  pub args: Vec<String>,
22
  pub shell: String,
23
}
24

25
#[derive(Clone, Debug, Eq, PartialEq)]
26
pub enum ShellProbeResult {
27
  Supported,
28
  Unsupported,
29
  Failed(String),
30
}
31

32
#[derive(Clone, Debug, Eq, PartialEq)]
33
pub enum ShellExecPrepareError {
34
  InvalidNamespace,
35
  InvalidPod,
36
  InvalidContainer,
37
  InvalidContext,
38
  UnsupportedShell,
39
  ProbeFailed(String),
40
}
41

42
#[derive(Clone, Debug, Eq, PartialEq)]
43
pub enum ShellExecRunError {
44
  Spawn(String),
45
  Wait(String),
46
  Exit(String),
47
}
48

49
impl std::fmt::Display for ShellExecPrepareError {
50
  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
×
51
    match self {
×
52
      Self::InvalidNamespace => write!(f, "Invalid namespace for shell exec"),
×
53
      Self::InvalidPod => write!(f, "Invalid pod name for shell exec"),
×
54
      Self::InvalidContainer => write!(f, "Invalid container name for shell exec"),
×
NEW
55
      Self::InvalidContext => write!(f, "Invalid context for shell exec"),
×
56
      Self::UnsupportedShell => write!(f, "Unable to find a supported shell in the container"),
×
57
      Self::ProbeFailed(message) => write!(f, "Unable to probe container shell support: {message}"),
×
58
    }
59
  }
×
60
}
61

62
impl std::fmt::Display for ShellExecRunError {
63
  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
×
64
    match self {
×
65
      Self::Spawn(message) => write!(f, "Unable to start kubectl exec: {message}"),
×
66
      Self::Wait(message) => write!(f, "Unable to wait for kubectl exec: {message}"),
×
67
      Self::Exit(message) => write!(f, "kubectl exec exited unsuccessfully: {message}"),
×
68
    }
69
  }
×
70
}
71

72
pub fn prepare_shell_exec(
×
73
  target: &ShellExecTarget,
×
74
) -> Result<ShellExecCommand, ShellExecPrepareError> {
×
75
  prepare_shell_exec_with_probe(target, probe_shell_support)
×
76
}
×
77

78
pub fn prepare_shell_exec_with_probe<F>(
9✔
79
  target: &ShellExecTarget,
9✔
80
  mut probe: F,
9✔
81
) -> Result<ShellExecCommand, ShellExecPrepareError>
9✔
82
where
9✔
83
  F: FnMut(&ShellExecTarget, &str) -> ShellProbeResult,
9✔
84
{
85
  validate_target(target)?;
9✔
86

87
  for shell in SHELL_CANDIDATES {
7✔
88
    match probe(target, shell) {
7✔
89
      ShellProbeResult::Supported => return Ok(build_shell_exec_command(target, shell)),
3✔
90
      ShellProbeResult::Unsupported => continue,
3✔
91
      ShellProbeResult::Failed(message) => return Err(ShellExecPrepareError::ProbeFailed(message)),
1✔
92
    }
93
  }
94

95
  Err(ShellExecPrepareError::UnsupportedShell)
1✔
96
}
9✔
97

98
pub fn run_shell_exec(command: &ShellExecCommand) -> Result<(), ShellExecRunError> {
×
99
  let mut child = Command::new(&command.program);
×
100
  child
×
101
    .args(&command.args)
×
102
    .stdin(Stdio::inherit())
×
103
    .stdout(Stdio::inherit())
×
104
    .stderr(Stdio::inherit());
×
105

106
  let mut child = child
×
107
    .spawn()
×
108
    .map_err(|error| ShellExecRunError::Spawn(error.to_string()))?;
×
109
  let status = child
×
110
    .wait()
×
111
    .map_err(|error| ShellExecRunError::Wait(error.to_string()))?;
×
112

113
  if status.success() {
×
114
    Ok(())
×
115
  } else {
116
    Err(ShellExecRunError::Exit(format_exit_status(status)))
×
117
  }
118
}
×
119

120
fn validate_target(target: &ShellExecTarget) -> Result<(), ShellExecPrepareError> {
9✔
121
  validate_component(&target.namespace, ShellExecPrepareError::InvalidNamespace)?;
9✔
122
  validate_component(&target.pod, ShellExecPrepareError::InvalidPod)?;
8✔
123
  validate_component(&target.container, ShellExecPrepareError::InvalidContainer)?;
7✔
124
  if let Some(context) = target.context.as_deref() {
6✔
125
    if !is_valid_kubectl_arg(context) {
2✔
126
      return Err(ShellExecPrepareError::InvalidContext);
1✔
127
    }
1✔
128
  }
4✔
129
  Ok(())
5✔
130
}
9✔
131

132
fn validate_component(
24✔
133
  value: &str,
24✔
134
  error: ShellExecPrepareError,
24✔
135
) -> Result<(), ShellExecPrepareError> {
24✔
136
  if value.trim().is_empty() || !is_valid_kubectl_arg(value) {
24✔
137
    Err(error)
3✔
138
  } else {
139
    Ok(())
21✔
140
  }
141
}
24✔
142

143
fn build_shell_exec_command(target: &ShellExecTarget, shell: &str) -> ShellExecCommand {
3✔
144
  let mut args = vec!["exec".into()];
3✔
145
  push_context_arg(&mut args, target.context.as_deref());
3✔
146
  args.extend([
3✔
147
    "-it".into(),
3✔
148
    "-n".into(),
3✔
149
    target.namespace.clone(),
3✔
150
    target.pod.clone(),
3✔
151
    "-c".into(),
3✔
152
    target.container.clone(),
3✔
153
    "--".into(),
3✔
154
    shell.into(),
3✔
155
  ]);
3✔
156
  ShellExecCommand {
3✔
157
    program: "kubectl".into(),
3✔
158
    args,
3✔
159
    shell: shell.into(),
3✔
160
  }
3✔
161
}
3✔
162

163
fn probe_shell_support(target: &ShellExecTarget, shell: &str) -> ShellProbeResult {
×
NEW
164
  let mut args = vec!["exec".to_string()];
×
NEW
165
  push_context_arg(&mut args, target.context.as_deref());
×
NEW
166
  args.extend([
×
NEW
167
    "-n".into(),
×
NEW
168
    target.namespace.clone(),
×
NEW
169
    target.pod.clone(),
×
NEW
170
    "-c".into(),
×
NEW
171
    target.container.clone(),
×
NEW
172
    "--".into(),
×
NEW
173
    shell.into(),
×
NEW
174
    "-c".into(),
×
NEW
175
    "exit".into(),
×
NEW
176
  ]);
×
177
  let output = Command::new("kubectl")
×
NEW
178
    .args(&args)
×
179
    .stdin(Stdio::null())
×
180
    .stdout(Stdio::null())
×
181
    .output();
×
182

183
  match output {
×
184
    Ok(output) if output.status.success() => ShellProbeResult::Supported,
×
185
    Ok(output) => {
×
186
      let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
×
187
      if is_missing_shell_error(&stderr, shell) {
×
188
        ShellProbeResult::Unsupported
×
189
      } else {
190
        ShellProbeResult::Failed(format_probe_error(output.status, &stderr))
×
191
      }
192
    }
193
    Err(error) => ShellProbeResult::Failed(anyhow!(error).to_string()),
×
194
  }
195
}
×
196

197
fn is_missing_shell_error(stderr: &str, shell: &str) -> bool {
2✔
198
  let stderr = stderr.to_ascii_lowercase();
2✔
199
  let shell = shell.to_ascii_lowercase();
2✔
200

201
  stderr.contains(&shell)
2✔
202
    && (stderr.contains("not found")
1✔
203
      || stderr.contains("no such file")
1✔
204
      || stderr.contains("executable file"))
×
205
}
2✔
206

207
fn format_probe_error(status: ExitStatus, stderr: &str) -> String {
×
208
  if stderr.is_empty() {
×
209
    format!("kubectl exec probe exited with status {status}")
×
210
  } else {
211
    format!("kubectl exec probe exited with status {status}: {stderr}")
×
212
  }
213
}
×
214

215
fn format_exit_status(status: ExitStatus) -> String {
×
216
  status
×
217
    .code()
×
218
    .map_or_else(|| status.to_string(), |code| code.to_string())
×
219
}
×
220

221
#[cfg(test)]
222
mod tests {
223
  use super::*;
224

225
  fn target() -> ShellExecTarget {
9✔
226
    ShellExecTarget {
9✔
227
      namespace: "default".into(),
9✔
228
      pod: "api-123".into(),
9✔
229
      container: "web".into(),
9✔
230
      context: None,
9✔
231
    }
9✔
232
  }
9✔
233

234
  #[test]
235
  fn test_prepare_shell_exec_builds_interactive_kubectl_command() {
1✔
236
    let command = prepare_shell_exec_with_probe(&target(), |_, shell| {
1✔
237
      assert_eq!(shell, "/bin/bash");
1✔
238
      ShellProbeResult::Supported
1✔
239
    })
1✔
240
    .expect("shell command should prepare");
1✔
241

242
    assert_eq!(command.program, "kubectl");
1✔
243
    assert_eq!(
1✔
244
      command.args,
245
      vec![
1✔
246
        "exec",
247
        "-it",
1✔
248
        "-n",
1✔
249
        "default",
1✔
250
        "api-123",
1✔
251
        "-c",
1✔
252
        "web",
1✔
253
        "--",
1✔
254
        "/bin/bash",
1✔
255
      ]
256
    );
257
    assert_eq!(command.shell, "/bin/bash");
1✔
258
  }
1✔
259

260
  #[test]
261
  fn test_prepare_shell_exec_includes_context_before_separator() {
1✔
262
    let mut with_context = target();
1✔
263
    with_context.context = Some("prod".into());
1✔
264
    let command = prepare_shell_exec_with_probe(&with_context, |_, _| ShellProbeResult::Supported)
1✔
265
      .expect("shell command should prepare");
1✔
266

267
    assert_eq!(
1✔
268
      command.args,
269
      vec![
1✔
270
        "exec",
271
        "--context",
1✔
272
        "prod",
1✔
273
        "-it",
1✔
274
        "-n",
1✔
275
        "default",
1✔
276
        "api-123",
1✔
277
        "-c",
1✔
278
        "web",
1✔
279
        "--",
1✔
280
        "/bin/bash",
1✔
281
      ]
282
    );
283
  }
1✔
284

285
  #[test]
286
  fn test_prepare_shell_exec_rejects_invalid_context() {
1✔
287
    let mut invalid = target();
1✔
288
    invalid.context = Some("prod; rm -rf /".into());
1✔
289
    assert_eq!(
1✔
290
      prepare_shell_exec_with_probe(&invalid, |_, _| ShellProbeResult::Supported),
1✔
291
      Err(ShellExecPrepareError::InvalidContext)
292
    );
293
  }
1✔
294

295
  #[test]
296
  fn test_prepare_shell_exec_rejects_invalid_target_values() {
1✔
297
    let mut invalid = target();
1✔
298
    invalid.namespace = "default; rm -rf /".into();
1✔
299

300
    assert_eq!(
1✔
301
      prepare_shell_exec_with_probe(&invalid, |_, _| ShellProbeResult::Supported),
1✔
302
      Err(ShellExecPrepareError::InvalidNamespace)
303
    );
304

305
    let mut invalid = target();
1✔
306
    invalid.pod = String::new();
1✔
307
    assert_eq!(
1✔
308
      prepare_shell_exec_with_probe(&invalid, |_, _| ShellProbeResult::Supported),
1✔
309
      Err(ShellExecPrepareError::InvalidPod)
310
    );
311

312
    let mut invalid = target();
1✔
313
    invalid.container = "web\nmalicious".into();
1✔
314
    assert_eq!(
1✔
315
      prepare_shell_exec_with_probe(&invalid, |_, _| ShellProbeResult::Supported),
1✔
316
      Err(ShellExecPrepareError::InvalidContainer)
317
    );
318
  }
1✔
319

320
  #[test]
321
  fn test_prepare_shell_exec_falls_back_to_sh_when_bash_missing() {
1✔
322
    let mut probed = vec![];
1✔
323
    let command = prepare_shell_exec_with_probe(&target(), |_, shell| {
2✔
324
      probed.push(shell.to_string());
2✔
325
      if shell == "/bin/bash" {
2✔
326
        ShellProbeResult::Unsupported
1✔
327
      } else {
328
        ShellProbeResult::Supported
1✔
329
      }
330
    })
2✔
331
    .expect("shell command should fall back");
1✔
332

333
    assert_eq!(probed, vec!["/bin/bash", "/bin/sh"]);
1✔
334
    assert_eq!(command.shell, "/bin/sh");
1✔
335
    assert_eq!(
1✔
336
      command.args,
337
      vec!["exec", "-it", "-n", "default", "api-123", "-c", "web", "--", "/bin/sh",]
1✔
338
    );
339
  }
1✔
340

341
  #[test]
342
  fn test_prepare_shell_exec_returns_unsupported_when_no_shell_exists() {
1✔
343
    assert_eq!(
1✔
344
      prepare_shell_exec_with_probe(&target(), |_, _| ShellProbeResult::Unsupported),
1✔
345
      Err(ShellExecPrepareError::UnsupportedShell)
346
    );
347
  }
1✔
348

349
  #[test]
350
  fn test_prepare_shell_exec_returns_probe_failure() {
1✔
351
    assert_eq!(
1✔
352
      prepare_shell_exec_with_probe(&target(), |_, _| {
1✔
353
        ShellProbeResult::Failed("kubectl unavailable".into())
1✔
354
      }),
1✔
355
      Err(ShellExecPrepareError::ProbeFailed(
1✔
356
        "kubectl unavailable".into()
1✔
357
      ))
1✔
358
    );
359
  }
1✔
360

361
  #[test]
362
  fn test_is_missing_shell_error_matches_shell_specific_missing_binary_message() {
1✔
363
    assert!(is_missing_shell_error(
1✔
364
      "exec: \"/bin/bash\": stat /bin/bash: no such file or directory",
1✔
365
      "/bin/bash"
1✔
366
    ));
367
    assert!(!is_missing_shell_error(
1✔
368
      "Error from server (NotFound): pods \"api-123\" not found",
1✔
369
      "/bin/bash"
1✔
370
    ));
1✔
371
  }
1✔
372
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc