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

kdash-rs / kdash / 24143194687

08 Apr 2026 03:16PM UTC coverage: 65.554% (+0.5%) from 65.099%
24143194687

push

github

deepu105
polish

8505 of 12974 relevant lines covered (65.55%)

154.46 hits per line

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

29.35
/src/cmd/mod.rs
1
pub mod shell;
2

3
use std::sync::Arc;
4

5
use anyhow::anyhow;
6
use duct::cmd;
7
use log::{error, info};
8
use regex::Regex;
9
use serde_json::Value as JValue;
10
use tokio::sync::Mutex;
11

12
use crate::app::{self, models::ScrollableTxt, App, Cli};
13

14
#[derive(Clone, Debug, Eq, PartialEq)]
15
pub enum IoCmdEvent {
16
  GetCliInfo,
17
  GetDescribe {
18
    kind: String,
19
    value: String,
20
    ns: Option<String>,
21
  },
22
}
23

24
#[derive(Clone)]
25
pub struct CmdRunner<'a> {
26
  pub app: &'a Arc<Mutex<App>>,
27
}
28

29
static NOT_FOUND: &str = "Not found";
30

31
pub(crate) fn is_valid_kubectl_arg(s: &str) -> bool {
34✔
32
  !s.contains('\n')
34✔
33
    && !s.contains('\r')
32✔
34
    && !s.contains('\0')
31✔
35
    && !s.contains(';')
30✔
36
    && !s.contains('|')
28✔
37
    && !s.contains('&')
27✔
38
    && !s.contains('`')
26✔
39
    && !s.contains('$')
25✔
40
}
34✔
41

42
impl<'a> CmdRunner<'a> {
43
  pub fn new(app: &'a Arc<Mutex<App>>) -> Self {
×
44
    CmdRunner { app }
×
45
  }
×
46

47
  pub async fn handle_cmd_event(&mut self, io_event: IoCmdEvent) {
×
48
    match io_event {
×
49
      IoCmdEvent::GetCliInfo => {
50
        self.get_cli_info().await;
×
51
      }
52
      IoCmdEvent::GetDescribe { kind, value, ns } => {
×
53
        self.get_describe(kind, value, ns).await;
×
54
      }
55
    };
56

57
    let mut app = self.app.lock().await;
×
58
    app.loading_complete();
×
59
  }
×
60

61
  async fn handle_error(&self, e: anyhow::Error) {
×
62
    error!("{:?}", e);
×
63
    let mut app = self.app.lock().await;
×
64
    app.handle_error(e);
×
65
  }
×
66

67
  async fn get_cli_info(&self) {
×
68
    let clis = tokio::task::spawn_blocking(|| {
×
69
      let mut clis: Vec<Cli> = vec![];
×
70

71
      let (version_c, version_s) = match cmd!("kubectl", "version", "-o", "json")
×
72
        .stderr_null()
×
73
        .read()
×
74
      {
75
        Ok(out) => {
×
76
          info!("kubectl version: {}", out);
×
77
          let v: serde_json::Result<JValue> = serde_json::from_str(&out);
×
78
          match v {
×
79
            Ok(val) => (
×
80
              Some(
×
81
                val["clientVersion"]["gitVersion"]
×
82
                  .to_string()
×
83
                  .replace('"', ""),
×
84
              ),
×
85
              Some(
×
86
                val["serverVersion"]["gitVersion"]
×
87
                  .to_string()
×
88
                  .replace('"', ""),
×
89
              ),
×
90
            ),
×
91
            _ => (None, None),
×
92
          }
93
        }
94
        _ => (None, None),
×
95
      };
96

97
      clis.push(build_cli("kubectl client", version_c));
×
98
      clis.push(build_cli("kubectl server", version_s));
×
99

100
      let version = cmd!("docker", "version", "--format", "'{{.Client.Version}}'")
×
101
        .stderr_null()
×
102
        .read()
×
103
        .map_or(None, |out| {
×
104
          if out.is_empty() {
×
105
            None
×
106
          } else {
107
            Some(format!("v{}", out.replace('\'', "")))
×
108
          }
109
        });
×
110

111
      clis.push(build_cli("docker", version));
×
112

113
      let version = cmd!("docker-compose", "version", "--short")
×
114
        .stderr_null()
×
115
        .read()
×
116
        .map_or(None, |out| {
×
117
          if out.is_empty() {
×
118
            cmd!("docker", "compose", "version", "--short")
×
119
              .stderr_null()
×
120
              .read()
×
121
              .map_or(None, |out| {
×
122
                if out.is_empty() {
×
123
                  None
×
124
                } else {
125
                  Some(format!("v{}", out.replace('\'', "")))
×
126
                }
127
              })
×
128
          } else {
129
            Some(format!("v{}", out.replace('\'', "")))
×
130
          }
131
        });
×
132

133
      clis.push(build_cli("docker-compose", version));
×
134

135
      let version = get_info_by_regex("kind", &["version"], r"(v[0-9.]+)");
×
136

137
      clis.push(build_cli("kind", version));
×
138

139
      let version = get_info_by_regex("helm", &["version", "-c"], r"(v[0-9.]+)");
×
140

141
      clis.push(build_cli("helm", version));
×
142

143
      let version = get_info_by_regex("istioctl", &["version"], r"([0-9.]+)");
×
144

145
      clis.push(build_cli("istioctl", version.map(|v| format!("v{}", v))));
×
146

147
      clis
×
148
    })
×
149
    .await
×
150
    .unwrap_or_default();
×
151

152
    let mut app = self.app.lock().await;
×
153
    app.data.clis = clis;
×
154
  }
×
155

156
  // TODO temp solution, should build this from API response
157
  async fn get_describe(&self, kind: String, value: String, ns: Option<String>) {
×
158
    if !is_valid_kubectl_arg(&kind) || !is_valid_kubectl_arg(&value) {
×
159
      self
×
160
        .handle_error(anyhow!("Invalid characters in resource kind or name"))
×
161
        .await;
×
162
      return;
×
163
    }
×
164
    if let Some(ref ns) = ns {
×
165
      if !is_valid_kubectl_arg(ns) {
×
166
        self
×
167
          .handle_error(anyhow!("Invalid characters in namespace"))
×
168
          .await;
×
169
        return;
×
170
      }
×
171
    }
×
172

173
    let kind_clone = kind.clone();
×
174
    let result = tokio::task::spawn_blocking(move || {
×
175
      let mut args = vec!["describe", kind.as_str(), value.as_str()];
×
176

177
      if let Some(ns) = ns.as_ref() {
×
178
        args.push("-n");
×
179
        args.push(ns.as_str());
×
180
      }
×
181

182
      duct::cmd("kubectl", &args).stderr_null().read()
×
183
    })
×
184
    .await;
×
185

186
    match result {
×
187
      Ok(Ok(out)) => {
×
188
        let mut app = self.app.lock().await;
×
189
        app.data.describe_out = ScrollableTxt::with_string(out);
×
190
      }
191
      Ok(Err(e)) => {
×
192
        self
×
193
          .handle_error(anyhow!(
×
194
            "Error running {} describe. Make sure you have kubectl installed: {:?}",
×
195
            kind_clone,
×
196
            e
×
197
          ))
×
198
          .await
×
199
      }
200
      Err(e) => {
×
201
        self
×
202
          .handle_error(anyhow!("Describe task panicked: {:?}", e))
×
203
          .await
×
204
      }
205
    }
206
  }
×
207
}
208

209
// utils
210

211
fn build_cli(name: &str, version: Option<String>) -> app::Cli {
×
212
  app::Cli {
213
    name: name.to_owned(),
×
214
    status: version.is_some(),
×
215
    version: version.unwrap_or_else(|| NOT_FOUND.into()),
×
216
  }
217
}
×
218

219
/// execute a command and get info from it using regex
220
fn get_info_by_regex(command: &str, args: &[&str], regex: &str) -> Option<String> {
2✔
221
  match cmd(command, args).stderr_null().read() {
2✔
222
    Ok(out) => match Regex::new(regex) {
2✔
223
      Ok(re) => match re.captures(out.as_str()) {
2✔
224
        Some(cap) => cap.get(1).map(|text| text.as_str().into()),
2✔
225
        _ => None,
×
226
      },
227
      _ => None,
×
228
    },
229
    _ => None,
×
230
  }
231
}
2✔
232

233
#[cfg(test)]
234
mod tests {
235
  #[test]
236
  fn test_get_info_by_regex() {
1✔
237
    use super::get_info_by_regex;
238

239
    assert_eq!(
1✔
240
      get_info_by_regex(
1✔
241
        "echo",
1✔
242
        &["Client: &version.Version{SemVer:\"v2.17.0\", GitCommit:\"a690bad98af45b015bd3da1a41f6218b1a451dbe\", GitTreeState:\"clean\"} \n Error: could not find tiller"],
1✔
243
        r"(v[0-9.]+)"
1✔
244
      ),
245
      Some("v2.17.0".into())
1✔
246
    );
247
    assert_eq!(
1✔
248
      get_info_by_regex(
1✔
249
        "echo",
1✔
250
        &["no running Istio pods in \"istio-system\"\n1.8.2"],
1✔
251
        r"([0-9.]+)"
1✔
252
      ),
253
      Some("1.8.2".into())
1✔
254
    );
255
  }
1✔
256

257
  #[test]
258
  fn test_is_valid_arg_accepts_normal_input() {
1✔
259
    // Normal k8s resource names should pass
260
    assert!(super::is_valid_kubectl_arg("pod"));
1✔
261
    assert!(super::is_valid_kubectl_arg("my-deployment"));
1✔
262
    assert!(super::is_valid_kubectl_arg("my_namespace"));
1✔
263
    assert!(super::is_valid_kubectl_arg("kube-system"));
1✔
264
    assert!(super::is_valid_kubectl_arg(
1✔
265
      "nginx-ingress-controller-abc123"
1✔
266
    ));
267
    assert!(super::is_valid_kubectl_arg("default"));
1✔
268
    assert!(super::is_valid_kubectl_arg("my.resource.name"));
1✔
269
  }
1✔
270

271
  #[test]
272
  fn test_is_valid_arg_rejects_injection_attempts() {
1✔
273
    // Shell injection attempts should be rejected
274
    assert!(!super::is_valid_kubectl_arg("pod; rm -rf /"));
1✔
275
    assert!(!super::is_valid_kubectl_arg("pod | cat /etc/passwd"));
1✔
276
    assert!(!super::is_valid_kubectl_arg("pod & malicious-cmd"));
1✔
277
    assert!(!super::is_valid_kubectl_arg("pod `whoami`"));
1✔
278
    assert!(!super::is_valid_kubectl_arg("pod\nmalicious"));
1✔
279
    assert!(!super::is_valid_kubectl_arg("pod\rmalicious"));
1✔
280
    assert!(!super::is_valid_kubectl_arg("pod\0malicious"));
1✔
281
    assert!(!super::is_valid_kubectl_arg("$HOME"));
1✔
282
    assert!(!super::is_valid_kubectl_arg("$(whoami)"));
1✔
283
  }
1✔
284

285
  #[test]
286
  fn test_is_valid_arg_empty_string() {
1✔
287
    // Empty string should be considered valid (kubectl will error separately)
288
    assert!(super::is_valid_kubectl_arg(""));
1✔
289
  }
1✔
290
}
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