click 0.6.0

A command-line REPL for Kubernetes that integrates into existing cli workflows
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
// Copyright 2021 Databricks, Inc.

// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at

// http://www.apache.org/licenses/LICENSE-2.0

// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use chrono::offset::Utc;
use chrono::{DateTime, Duration};
use clap::ArgMatches;
use humantime::parse_duration;
use k8s_openapi::{
    apimachinery::pkg::apis::meta::v1::ObjectMeta,
    http::{self, Request},
    List, ListOptional, ListResponse, ListableResource, Metadata, RequestError, ResponseBody,
};
use regex::Regex;
use serde::Deserialize;

use crate::env::Env;
use crate::error::ClickError;
use crate::kobj::KObj;
use crate::output::ClickWriter;
use crate::table::CellSpec;

use std::borrow::Cow;
use std::collections::{HashMap, HashSet};
use std::fmt::Debug;
use std::io::{stderr, Write};

#[macro_use]
pub mod command_def;

pub mod alias; // commands for alias/unalias
pub mod click; // commands internal to click (setting config values, etc)
pub mod configmaps; // commands relating to configmaps
pub mod copy; // command to copy files to/from pods
pub mod crds; // commands to query crd created objects
pub mod daemonsets; // commands for daemonsets
pub mod delete; // command to delete objects
pub mod deployments; // command to list deployments
pub mod describe; // the describe command
pub mod events; // commands to print events
pub mod exec; // command to exec into pods
pub mod jobs; // commands relating to jobs
pub mod logs; // command to get pod logs
pub mod namespaces; // commands relating to namespaces
pub mod nodes; // commands relating to nodes
pub mod pods; //commands relating to pods
pub mod portforwards; // commands for forwarding ports
pub mod replicasets; // commands relating to relicasets
pub mod secrets; // commands for secrets
pub mod services; // commands for services
pub mod statefulsets; // commands for statefulsets
pub mod storage; // commands relating to storage objects (like storageclass)
pub mod volumes; // commands relating to volumes

#[cfg(feature = "argorollouts")]
pub mod rollouts;

// utility types
type RowSpec<'a> = Vec<CellSpec<'a>>;
type Extractor<T> = fn(&T) -> Option<CellSpec<'_>>;

fn mapped_val(key: &str, map: &[(&'static str, &'static str)]) -> Option<&'static str> {
    for (map_key, val) in map.iter() {
        if &key == map_key {
            return Some(val);
        }
    }
    None
}

#[allow(clippy::too_many_arguments)] // factoring this out into structs just makes it worse
pub fn run_list_command<T, F>(
    matches: ArgMatches,
    env: &mut Env,
    writer: &mut ClickWriter,
    mut cols: Vec<&str>,
    request: Request<Vec<u8>>,
    col_map: &[(&'static str, &'static str)],
    extra_col_map: Option<&[(&'static str, &'static str)]>,
    extractors: Option<&HashMap<String, Extractor<T>>>,
    get_kobj: F,
) -> Result<(), ClickError>
where
    T: ListableResource + Metadata<Ty = ObjectMeta> + for<'de> Deserialize<'de> + Debug,
    F: Fn(&T) -> KObj,
{
    let regex = match crate::table::get_regex(&matches) {
        Ok(r) => r,
        Err(s) => {
            writeln!(stderr(), "{}", s).unwrap_or(());
            return Ok(()); // TODO: Return the error when that does something
        }
    };

    let list_res = env.run_on_context::<_, List<T>>(|c| c.execute_list(request));
    if list_res.is_err() {
        env.clear_last_objs();
    }
    let list = list_res?;

    let mut flags: Vec<&str> = if matches.is_valid_arg("show") {
        match matches.values_of("show") {
            Some(v) => v.collect(),
            None => vec![],
        }
    } else {
        vec![]
    };

    let sort = matches.value_of("sort").map(|s| {
        let colname = s.to_lowercase();
        if let Some(col) = mapped_val(&colname, col_map) {
            command_def::SortCol(col)
        } else if let Some(ecm) = extra_col_map {
            let mut func = None;
            for (flag, col) in ecm.iter() {
                if flag.eq(&colname) {
                    flags.push(flag);
                    func = Some(command_def::SortCol(col));
                }
            }
            match func {
                Some(f) => f,
                None => panic!("Shouldn't be allowed to ask to sort by: {}", colname),
            }
        } else {
            panic!("Shouldn't be allowed to ask to sort by: {}", colname);
        }
    });

    if let Some(ecm) = extra_col_map {
        // if we're not in a namespace, we want to add a namespace col if it's in extra_col_map
        if env.namespace.is_none() && mapped_val("namespace", ecm).is_some() {
            flags.push("namespace");
        }

        let labels_present = if matches.is_valid_arg("labels") {
            matches.is_present("labels")
        } else {
            false
        };
        command_def::add_extra_cols(&mut cols, labels_present, flags, ecm);
    }

    handle_list_result(
        env,
        writer,
        cols,
        list,
        extractors,
        regex,
        sort,
        matches.is_present("reverse"),
        get_kobj,
    )
}

/// Uppercase the first letter of the given str
pub fn uppercase_first(s: &str) -> String {
    let mut cs = s.chars();
    match cs.next() {
        None => String::new(),
        Some(f) => f.to_uppercase().collect::<String>() + cs.as_str(),
    }
}

/// a clap validator for duration
fn valid_duration(s: &str) -> Result<(), String> {
    parse_duration(s).map(|_| ()).map_err(|e| e.to_string())
}

/// a clap validator for rfc3339 dates
fn valid_date(s: &str) -> Result<(), String> {
    DateTime::parse_from_rfc3339(s)
        .map(|_| ())
        .map_err(|e| e.to_string())
}

/// a clap validator for u32
pub fn valid_u32(s: &str) -> Result<(), String> {
    s.parse::<u32>().map(|_| ()).map_err(|e| e.to_string())
}

// table printing / building
/* this function abstracts the standard handling code for when a k8s call returns a list of objects.
 * it does the following thins:
 * - builds the row specs based on the passed extractors/regex
 * - gets the kobks from each listable object
 * -- sets the env to have the built list as its current list
 * -- clears the env list if the built list was empty
 *
 * NB: This function assumes you want the printed list to be numbered. It further assumes the cols
 * will NOT include a colume named ####, and inserts it for you at the start.
 */
#[allow(clippy::too_many_arguments)]
pub fn handle_list_result<'a, T, F>(
    env: &mut Env,
    writer: &mut ClickWriter,
    cols: Vec<&str>,
    list: List<T>,
    extractors: Option<&HashMap<String, Extractor<T>>>,
    regex: Option<Regex>,
    sort: Option<command_def::SortCol>,
    reverse: bool,
    get_kobj: F,
) -> Result<(), ClickError>
where
    T: 'a + ListableResource + Metadata<Ty = ObjectMeta>,
    F: Fn(&T) -> KObj,
{
    let mut specs = build_specs(&cols, &list, extractors, true, regex, get_kobj);

    let mut titles: Vec<&str> = vec!["####"];
    titles.reserve(cols.len());
    for col in cols.iter() {
        titles.push(col);
    }

    if let Some(command_def::SortCol(colname)) = sort {
        let index = cols.iter().position(|&c| c == colname);
        match index {
            Some(index) => {
                let idx = index + 1; // +1 for #### col
                specs.sort_by(|a, b| a.1.get(idx).unwrap().cmp(b.1.get(idx).unwrap()));
            }
            None => clickwriteln!(
                writer,
                "Asked to sort by {}, but it's not a column in the output",
                colname
            ),
        }
    }

    let (kobjs, rows): (Vec<KObj>, Vec<RowSpec>) = if reverse {
        specs.into_iter().rev().unzip()
    } else {
        specs.into_iter().unzip()
    };

    crate::table::print_table(titles, rows, writer);
    env.set_last_objs(kobjs);
    Ok(())
}

// row building

/* Build row specs and a kobj vec from data returned from k8s.
 *
 * cols is a list of names of columns to build. "Name" * and "Age" are handled, other names need to
 * be in 'extractors', and the extractor for the specified name will be used.
 *
 * include_index = true will put an index (numbered) column as the first item in the row
 *
 * regex: if this is Some(regex) then only rows that have some cell that matches the regex will be
 * included in the output
 *
 * get_kobj: this needs to be a function that maps the list items to crate::kobj::KObjs
 *
 * This returns the vector of built kobjs that can be then passed to the env to set the last list of
 * things returned, and the row specs that can be used to print out that list.
 */
pub fn build_specs<'a, T, F>(
    cols: &[&str],
    list: &'a List<T>,
    extractors: Option<&HashMap<String, Extractor<T>>>,
    include_index: bool,
    regex: Option<Regex>,
    get_kobj: F,
) -> Vec<(KObj, RowSpec<'a>)>
where
    T: 'a + ListableResource + Metadata<Ty = ObjectMeta>,
    F: Fn(&T) -> KObj,
{
    let mut ret = vec![];
    for item in list.items.iter() {
        let mut row: Vec<CellSpec> = if include_index {
            vec![CellSpec::new_index()]
        } else {
            vec![]
        };
        for col in cols.iter() {
            match *col {
                "Age" => row.push(extract_age(item).into()),
                "Labels" => row.push(extract_labels(item).into()),
                "Name" => row.push(extract_name(item).into()),
                "Namespace" => row.push(extract_namespace(item).into()),
                _ => match extractors {
                    Some(extractors) => match extractors.get(*col) {
                        Some(extractor) => row.push(extractor(item).into()),
                        None => panic!("Can't extract"),
                    },
                    None => panic!("Can't extract"),
                },
            }
        }
        match regex {
            Some(ref regex) => {
                if row_matches(&row, regex) {
                    ret.push((get_kobj(item), row));
                }
            }
            None => {
                ret.push((get_kobj(item), row));
            }
        }
    }
    ret
}

// common extractors

/// An extractor for the Name field. Extracts the name out of the object metadata
pub fn extract_name<T: Metadata<Ty = ObjectMeta>>(obj: &T) -> Option<Cow<'_, str>> {
    let meta = obj.metadata();
    meta.name.as_ref().map(|n| n.into())
}

/// An extractor for the Age field. Extracts the age out of the object metadata
pub fn extract_age<T: Metadata<Ty = ObjectMeta>>(obj: &T) -> Option<CellSpec<'_>> {
    let meta = obj.metadata();
    meta.creation_timestamp.as_ref().map(|ts| ts.0.into())
}

/// An extractor for the Namespace field. Extracts the namespace out of the object metadata
pub fn extract_namespace<T: Metadata<Ty = ObjectMeta>>(obj: &T) -> Option<Cow<'_, str>> {
    let meta = obj.metadata();
    meta.namespace.as_ref().map(|ns| ns.as_str().into())
}

/// An extractor for the Labels field. Extracts the labels out of the object metadata
pub fn extract_labels<T: Metadata<Ty = ObjectMeta>>(obj: &T) -> Option<Cow<'_, str>> {
    let meta = obj.metadata();
    meta.labels
        .as_ref()
        .map(|labels| keyval_string(labels.iter(), None).into())
}

// utility functions
fn row_matches<'a>(row: &[CellSpec<'a>], regex: &Regex) -> bool {
    let mut has_match = false;
    for cell_spec in row.iter() {
        if !has_match {
            has_match = cell_spec.matches(regex);
        }
    }
    has_match
}

pub fn format_duration(duration: Duration) -> String {
    if duration.num_days() > 365 {
        // TODO: maybe be more smart about printing years, or at least have an option
        let days = duration.num_days();
        let yrs = days / 365;
        format!("{}y {}d", yrs, (duration.num_days() - (yrs * 365)))
    } else if duration.num_days() > 0 {
        format!(
            "{}d {}h",
            duration.num_days(),
            (duration.num_hours() - (24 * duration.num_days()))
        )
    } else if duration.num_hours() > 0 {
        format!(
            "{}h {}m",
            duration.num_hours(),
            (duration.num_minutes() - (60 * duration.num_hours()))
        )
    } else if duration.num_minutes() > 0 {
        format!(
            "{}m {}s",
            duration.num_minutes(),
            (duration.num_seconds() - (60 * duration.num_minutes()))
        )
    } else {
        format!("{}s", duration.num_seconds())
    }
}

pub fn time_since(date: DateTime<Utc>) -> Duration {
    let now = Utc::now();
    now.signed_duration_since(date)
}

/// Build a multi-line string of the specified keyvals.
/// keys in skip will be skipped
pub fn keyval_string<I, K, V>(keyvals: I, skip: Option<&HashSet<String>>) -> String
where
    I: Iterator<Item = (K, V)>,
    K: AsRef<str>,
    V: AsRef<str>,
{
    let mut buf = String::new();
    let mut first = true;
    for (key, val) in keyvals {
        if let Some(skip_set) = skip {
            if skip_set.contains(key.as_ref()) {
                continue;
            }
        }
        if first {
            first = false;
        } else {
            buf.push('\n');
        }
        buf.push_str(key.as_ref());
        buf.push('=');
        buf.push_str(val.as_ref());
    }
    if first {
        // if first is still true, didn't have anything in the iter
        buf.push_str("<none>");
    }
    buf
}

// utils for getting custom requests

/// Get a read request for a custom url
// used by features, so without them isn't used
// type is from k8s_openapi, so we can't change it
#[allow(dead_code, clippy::type_complexity)]
pub fn get_read_request_for_url<T: k8s_openapi::Response>(
    url: String,
) -> Result<(Request<Vec<u8>>, fn(_: http::StatusCode) -> ResponseBody<T>), RequestError> {
    let request = http::Request::get(url);
    let body = vec![];
    match request.body(body) {
        Ok(request) => Ok((request, ResponseBody::new)),
        Err(err) => Err(RequestError::Http(err)),
    }
}

/// Get a request for a custom url. The item must be listable.
// used by features, so without them isn't used
// type is from k8s_openapi, so we can't change it
#[allow(dead_code, clippy::type_complexity)]
pub fn get_list_request_for_url<T: ListableResource + for<'de> serde::Deserialize<'de>>(
    url: String,
    optional: ListOptional<'_>,
) -> Result<
    (
        Request<Vec<u8>>,
        fn(_: http::StatusCode) -> ResponseBody<ListResponse<T>>,
    ),
    RequestError,
> {
    let mut query_pairs = url::form_urlencoded::Serializer::new(url);
    optional.__serialize(&mut query_pairs);
    let __url = query_pairs.finish();
    let __request = Request::get(__url);
    let __body = vec![];
    match __request.body(__body) {
        Ok(request) => Ok((request, ResponseBody::new)),
        Err(err) => Err(RequestError::Http(err)),
    }
}