├── .markdownlint.yaml ├── Cargo.toml ├── .yamllint.yaml ├── .github ├── dependabot.yaml └── workflows │ ├── test.yaml │ ├── lint.yaml │ └── mirror.yaml ├── Cargo.lock ├── .rustfmt.toml ├── .gitignore ├── LICENSE ├── CHANGELOG.md ├── README.md └── src └── main.rs /.markdownlint.yaml: -------------------------------------------------------------------------------- 1 | line-length: false 2 | ol-prefix: 3 | style: one 4 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "kubekeeper" 3 | version = "2.5.0" 4 | edition = "2021" 5 | -------------------------------------------------------------------------------- /.yamllint.yaml: -------------------------------------------------------------------------------- 1 | extends: default 2 | rules: 3 | document-start: disable 4 | indentation: 5 | indent-sequences: false 6 | line-length: disable 7 | -------------------------------------------------------------------------------- /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | updates: 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | interval: "daily" 8 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "kubekeeper" 7 | version = "2.5.0" 8 | -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | # See configuration in https://rust-lang.github.io/rustfmt 2 | 3 | # format_macro_bodies = true 4 | # format_macro_matchers = true 5 | max_width = 110 6 | use_small_heuristics = "Max" 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | debug/ 4 | target/ 5 | 6 | # These are backup files generated by rustfmt 7 | **/*.rs.bk 8 | 9 | # MSVC Windows builds of rustc generate these, which store debugging information 10 | *.pdb 11 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: # yamllint disable-line rule:truthy 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | 9 | jobs: 10 | cargo-test: 11 | name: cargo-test 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | - uses: actions-rs/cargo@v1 16 | with: 17 | command: test 18 | -------------------------------------------------------------------------------- /.github/workflows/lint.yaml: -------------------------------------------------------------------------------- 1 | name: lint 2 | 3 | on: # yamllint disable-line rule:truthy 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | 9 | jobs: 10 | clippy-lint: 11 | name: clippy-lint 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | - uses: actions-rs/clippy-check@v1 16 | with: 17 | name: clippy-summary 18 | token: ${{ secrets.GITHUB_TOKEN }} 19 | markdown-lint: 20 | name: markdown-lint 21 | runs-on: ubuntu-latest 22 | steps: 23 | - uses: actions/checkout@v3 24 | - uses: DavidAnson/markdownlint-cli2-action@v10 25 | with: 26 | globs: "**/*.md" 27 | yaml-lint: 28 | name: yaml-lint 29 | runs-on: ubuntu-latest 30 | steps: 31 | - uses: actions/checkout@v3 32 | - uses: ibiqlik/action-yamllint@v3 33 | -------------------------------------------------------------------------------- /.github/workflows/mirror.yaml: -------------------------------------------------------------------------------- 1 | name: mirror 2 | 3 | on: # yamllint disable-line rule:truthy 4 | - create 5 | - delete 6 | - push 7 | - workflow_dispatch 8 | 9 | jobs: 10 | gitlab: 11 | name: gitlab 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | with: 16 | fetch-depth: 0 17 | - name: Push to gitlab 18 | env: 19 | GITLAB_DEPLOY_KEY: ${{ secrets.GITLAB_DEPLOY_KEY }} 20 | GITLAB_PUBLIC_KEY: gitlab.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAfuCHKVTjquxvt6CM6tdG4SLp1Btn/nOeHHE5UOzRdf 21 | GITLAB_REPOSITORY: git@gitlab.com:${{ github.repository }}.git 22 | run: | 23 | eval `ssh-agent` 24 | ssh-add - <<< $GITLAB_DEPLOY_KEY 25 | install -D <(echo $GITLAB_PUBLIC_KEY) ~/.ssh/known_hosts 26 | git push --force --prune $GITLAB_REPOSITORY +refs/remotes/origin/*:refs/heads/* +refs/tags/*:refs/tags/* 27 | kill $SSH_AGENT_PID 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Ayaz BADOURALY 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Kubekeeper Changelog 2 | 3 | ## Unreleased 4 | 5 | - Mirror repository to gitlab 6 | - Bump markdownlint-cli2 action from 8 to 10 () 7 | 8 | ## v2.5.0 9 | 10 | - Apply clippy lint suggestions on macro rules 11 | - Bump markdownlint-cli2 action from 7 to 8 () 12 | - Update magic to detect native kubectl commands () 13 | - Fix link in changelog 14 | 15 | ## v2.4.0 16 | 17 | - Use official markdownlint github action 18 | - Use variable debug format in debug logs 19 | - Update command include and exclude lists 20 | - Print included contexts in red 21 | 22 | ## v2.3.0 23 | 24 | - Rewrite table-driven tests 25 | - Display namespace in validation prompt 26 | - Add new check context empty test 27 | - Add rustfmt config file 28 | - Add plain text reason when identifying actions 29 | - Print kubekeeper output to stderr 30 | - Update doc comments 31 | - Add debug mode 32 | 33 | ## v2.2.0 34 | 35 | - Add clippy check 36 | - Simplify save context return value 37 | - Highlight current context in bold yellow 38 | - Rename clippy job 39 | - Apply clippy lint suggestions automagically 40 | - Fix remaining clippy warnings 41 | - Add changelog 42 | - Add support for patterns in context include and exclude lists 43 | - Run cargo test in github actions 44 | - Update context include and exclude lists 45 | - Update demo screenshot with new validation prompt 46 | 47 | ## v2.1.0 48 | 49 | - Add doc link to cobra command to request completion 50 | - Return non-zero exit code when validation failed 51 | - Prefix current context to native kubectl commands only 52 | - Validate context without having to press enter 53 | - Fix double newline on user input 54 | 55 | ## v2.0.2 56 | 57 | - Remove debug logs 58 | - Include commands edit and label 59 | - Fix cobra dynamic completion 60 | 61 | ## v2.0.1 62 | 63 | - Add demo screenshot 64 | - Add lint workflow 65 | - Add dependabot config file 66 | - Bump actions/checkout from 2 to 3 () 67 | - Add new commands without validation 68 | 69 | ## v2.0.0 70 | 71 | - Rewrite kubekeeper in rust 72 | - Add cargo lock 73 | - Add new installation and configuration steps 74 | - Bump license year 75 | - Only append context if it is not already set 76 | 77 | ## v1.3.0 78 | 79 | - Use logging instead of print 80 | - Lint python files 81 | - Add debug logs 82 | - Exclude cobra completion hidden function 83 | 84 | ## v1.2.0 85 | 86 | - Exclude help commands 87 | - Prevent run with `--cluster` 88 | 89 | ## v1.1.0 90 | 91 | - Increase fzf height 92 | - Add default config 93 | - Add uninstall command to bootstrap 94 | - Include commands apply and scale 95 | - Add doc for autocompletion 96 | - Fix curl-sh install 97 | - Add missing ro commands to the exclude list 98 | - Fix context save heuristic 99 | - Use kube dir for default config dir 100 | - Troubleshoot kubectl-fzf completion 101 | 102 | ## v1.0.0 103 | 104 | - Commit for a dream 105 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kubekeeper 2 | 3 | > Stop! Who would cross the Bridge of Death must answer me these questions three, ere the other side he see. 4 | > 5 | >> [The Bridgekeeper, _Monty Python and the Holy Grail_](https://www.youtube.com/watch?v=pWS8Mg-JWSg) 6 | 7 | `kubekeeper` asks for context confirmation before running yolo `kubectl` commands on prod clusters. Just like `sudo`, 8 | confirmation is required only if the command is run for the first time in a while. 9 | 10 | ![kubekeeper screenshot](https://user-images.githubusercontent.com/19719047/167436060-6def5cf8-5233-464f-9330-bb17271b3b67.png) 11 | 12 | ## Installation 13 | 14 | ### Homebrew 15 | 16 | ```bash 17 | brew install badouralix/tap/kubekeeper 18 | ``` 19 | 20 | ### Do it yourself 21 | 22 | Please install the following tools before using `kubekeeper`: 23 | 24 | - [kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl/) 25 | - [rust and cargo](https://doc.rust-lang.org/cargo/getting-started/installation.html) 26 | 27 | `kubekeeper` comes with a default configuration that fits all normal usages of `kubectl`. It can be installed by just 28 | creating a `kubekeeper` file into your path: 29 | 30 | ```shell 31 | # Download the latest version of kubekeeper 32 | git clone https://github.com/badouralix/kubekeeper.git 33 | 34 | # Build kubekeeper 35 | cd kubekeeper/ 36 | cargo build --release --bin kubekeeper 37 | 38 | # Install kubekeeper 39 | sudo install target/release/kubekeeper /usr/local/bin/kubekeeper 40 | ``` 41 | 42 | ### Aliases and autocompletion 43 | 44 | You may add the following lines to your `.zshrc` to use `kubekeeper` with all your existing aliases: 45 | 46 | ```shell 47 | # kubekeeper completion must be added after kubectl completion 48 | 49 | alias kubectl=kubekeeper 50 | compdef kubekeeper=kubectl 51 | ``` 52 | 53 | Autocompletion is also available in other shells with: 54 | 55 | ```shell 56 | # kubekeeper completion must be added after kubectl completion 57 | 58 | complete -o default -F __start_kubectl kubekeeper 59 | ``` 60 | 61 | ## Usage 62 | 63 | `kubekeeper` is an invisible wrapper on top of `kubectl`, and must therefore be used just like the latter. 64 | 65 | ## Configuration 66 | 67 | `kubekeeper` itself can be configured with the following environment variables: 68 | 69 | | Environment Variable | Description | Default | 70 | | :-------------------------: | :----------------------------------------------------------------------- | :--------------: | 71 | | `KUBEKEEPER_CHECK_INTERVAL` | Number of seconds without execution before asking for confirmation again | 900 | 72 | | `KUBEKEEPER_DEBUG` | If set, runs in debug mode | _unset_ | 73 | | `KUBEKEEPER_PIDFILE` | Pidfile name | `kubekeeper.pid` | 74 | 75 | Note that the pidfile is always located in the user temp folder (usually `/tmp` on Linux, and 76 | `/var/folders/xx/xxx..xxx/T` on macOS). 77 | 78 | ## Troubleshooting 79 | 80 | ### Completion with [`kubectl-fzf`](https://github.com/bonnefoa/kubectl-fzf) shows files and dirs 81 | 82 | `kubekeeper` integrates well with [`kubectl-fzf`](https://github.com/bonnefoa/kubectl-fzf). Make sure to define 83 | completion in the right order: 84 | 85 | ```shell 86 | source <(kubectl completion $SHELL) 87 | source $GOPATH/src/github.com/bonnefoa/kubectl-fzf/kubectl_fzf.sh 88 | complete -o default -F __start_kubectl kubekeeper 89 | ``` 90 | 91 | ## License 92 | 93 | Unless expressly stated otherwise, all contents licensed under the [MIT License](LICENSE). 94 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::env; 3 | use std::fs; 4 | use std::io::Write; 5 | use std::os::unix::process::CommandExt; 6 | use std::process::Command; 7 | use std::time::SystemTime; 8 | 9 | /// Prints to the standard error with a newline, if the debug environment variable is set. 10 | macro_rules! edebugln { 11 | ($($arg:tt)*) => ( 12 | if env::var("KUBEKEEPER_DEBUG").is_ok() { 13 | eprintln!($($arg)*) 14 | } 15 | ) 16 | } 17 | 18 | /// Returns true iff command uses a subcommand in allowlist. 19 | fn check_command(command: &str, allowlist: Vec<&str>) -> bool { 20 | for allowlist_command_prefix in allowlist { 21 | if command.starts_with(allowlist_command_prefix) { 22 | return true; 23 | } 24 | } 25 | 26 | false 27 | } 28 | 29 | /// Returns true iff context matches at least one context pattern in allowlist. 30 | fn check_context(context: &str, allowlist: Vec<&str>) -> bool { 31 | for allowlist_context_pattern in allowlist { 32 | if check_context_against_pattern(context, allowlist_context_pattern) { 33 | return true; 34 | } 35 | } 36 | 37 | false 38 | } 39 | 40 | /// Returns true iff context matches pattern. 41 | /// 42 | /// A context contains regular chars. A pattern contains zero, one or many 43 | /// wildcards, and regular chars. Wildcards can match zero, one or many regular 44 | /// chars. For instance `kube-production-1` matches `*prod*`. 45 | /// 46 | /// This algorithm is a tweaked version of the regex backtracking. In the worst 47 | /// case scenario, it runs in `O(context.len() * pattern.len())`. It is inspired 48 | /// by https://research.swtch.com/glob. 49 | fn check_context_against_pattern(context: &str, pattern: &str) -> bool { 50 | // Store the context/pattern index of the current iteration 51 | let mut current_c_idx = 0; 52 | let mut current_p_idx = 0; 53 | // Store the context/pattern index to jump to when backtracking 54 | let mut backtrack_c_idx = 0; 55 | let mut backtrack_p_idx = 0; 56 | 57 | while current_c_idx < context.len() && backtrack_p_idx <= pattern.len() { 58 | if current_p_idx < pattern.len() { 59 | match pattern.as_bytes()[current_p_idx] { 60 | b'*' => { 61 | backtrack_c_idx = current_c_idx + 1; 62 | backtrack_p_idx = current_p_idx + 1; 63 | 64 | current_p_idx += 1; 65 | 66 | continue; 67 | } 68 | _ => { 69 | if context.as_bytes()[current_c_idx] == pattern.as_bytes()[current_p_idx] { 70 | current_c_idx += 1; 71 | current_p_idx += 1; 72 | 73 | continue; 74 | } 75 | } 76 | } 77 | } 78 | 79 | // At this point, either the end of pattern was reached, or context does not match pattern 80 | // If a wildcard was encountered previously, then try to backtrack to the previous known wildcard 81 | if backtrack_p_idx != 0 { 82 | current_c_idx = backtrack_c_idx; 83 | current_p_idx = backtrack_p_idx; 84 | 85 | backtrack_c_idx += 1; 86 | 87 | continue; 88 | } 89 | 90 | return false; 91 | } 92 | 93 | // If context is shorter than pattern, we still want to return a match when pattern contains trailing wildcards 94 | if current_p_idx < pattern.len() { 95 | return pattern.as_bytes()[current_p_idx..].iter().all(|&char| char == b'*'); 96 | } 97 | 98 | // No need to check the indices against the lengths since they are only incremented by one per iteration 99 | true 100 | } 101 | 102 | /// Returns true iff context has already been validated earlier. 103 | fn check_last_validation(context: &str) -> bool { 104 | let check_interval: u64 = 105 | env::var("KUBEKEEPER_CHECK_INTERVAL").unwrap_or_else(|_| "900".to_string()).parse().unwrap_or(900); 106 | let pidfile = 107 | env::temp_dir().join(env::var("KUBEKEEPER_PIDFILE").unwrap_or_else(|_| "kubekeeper.pid".to_string())); 108 | 109 | let mut outdated = false; 110 | 111 | match fs::metadata(pidfile.clone()) { 112 | Ok(metadata) => { 113 | if SystemTime::now() 114 | .duration_since(metadata.modified().unwrap_or(SystemTime::UNIX_EPOCH)) 115 | .unwrap() 116 | .as_secs() 117 | > check_interval 118 | { 119 | outdated = true; 120 | } 121 | } 122 | // We are conservative here, we assume we did not validate the context recently 123 | Err(_) => outdated = true, 124 | } 125 | 126 | // Use unwrap_or_default to ask for validation if reading from pidfile failed 127 | if fs::read_to_string(pidfile).unwrap_or_default() != context { 128 | outdated = true; 129 | } 130 | 131 | !outdated 132 | } 133 | 134 | /// Returns default include and exclude config. 135 | #[allow(clippy::type_complexity)] 136 | fn get_config() -> (HashMap<&'static str, Vec<&'static str>>, HashMap<&'static str, Vec<&'static str>>) { 137 | // These contexts and/or commands may _never_ require validation 138 | let mut exclude = HashMap::new(); 139 | exclude.insert("context", vec!["kind-*", "minikube"]); 140 | exclude.insert( 141 | "command", 142 | vec![ 143 | "api-resources", 144 | "api-versions", 145 | "cluster-info", 146 | "completion", 147 | "config current-context", 148 | "config get-clusters", 149 | "config get-contexts", 150 | "config view", 151 | "describe", 152 | "diff", 153 | "explain", 154 | "get", 155 | "help", 156 | "logs", 157 | "options", 158 | "top", 159 | "version", 160 | "wait", 161 | ], 162 | ); 163 | 164 | // These contexts and/or commands may _always_ require validation 165 | let mut include = HashMap::new(); 166 | include.insert("context", vec!["*fed*", "*prod*"]); 167 | include 168 | .insert("command", vec!["annotate", "apply", "delete", "edit", "label", "patch", "replace", "scale"]); 169 | 170 | (include, exclude) 171 | } 172 | 173 | /// Identifies which actions must be taken: validation? record? amendment? 174 | /// Returns one boolean per question, the color of the context (red for included 175 | /// contexts, yellow otherwise) and a human-readable reason for these choices. 176 | fn identify_actions( 177 | context: &str, 178 | command: &str, 179 | include: HashMap<&str, Vec<&str>>, 180 | exclude: HashMap<&str, Vec<&str>>, 181 | ) -> (bool, bool, bool, &'static str, &'static str) { 182 | // If command is empty, skip all actions 183 | if command.is_empty() { 184 | return (false, false, false, "", "command is empty"); 185 | } 186 | 187 | // If command is cobra dynamic completion, skip all actions 188 | // See https://github.com/spf13/cobra/blob/b9460cc/completions.go#L12-L19 189 | if command.starts_with("__complete") { 190 | return (false, false, false, "", "command is cobra dynamic completion"); 191 | } 192 | 193 | // If the context is set as an argument, skip all actions 194 | for arg in env::args() { 195 | if arg.starts_with("--context") { 196 | return (false, false, false, "", "context option is already provided"); 197 | } 198 | } 199 | 200 | // Here we try to figure out if the command is a native kubectl command or a plugin 201 | // The --context option can only be prefixed to native commands 202 | // See https://github.com/kubernetes/kubernetes/pull/92343 203 | let amendment = if command.starts_with('-') { 204 | // If a global option is already provided before the command, then we assume it is a native command 205 | // It would be better to iterate over all args and exclude options with their value if any 206 | // But it would require to be able to handle cases where the option and the value are separated by spaces 207 | true 208 | } else { 209 | String::from_utf8( 210 | Command::new("kubectl") 211 | .args(["__complete", ""]) 212 | .output() 213 | .expect("failed to execute process") 214 | .stdout, 215 | ) 216 | .unwrap() 217 | .lines() 218 | // Filter out plugins based on description from https://github.com/kubernetes/kubernetes/pull/105867 219 | .filter(|line| !line.ends_with("is a plugin installed by the user")) 220 | // Extract native kubectl commands without description 221 | // It contains an extra ":4", but that merely affects the heuristic 222 | .map(|line| line.split('\t').next().unwrap().to_owned()) 223 | // Match first kubekeeper argument against native kubectl commands 224 | .any(|native_kubectl_command| env::args().nth(1).unwrap() == native_kubectl_command) 225 | }; 226 | 227 | if check_context(context, include["context"].clone()) { 228 | if check_command(command, exclude["command"].clone()) { 229 | return (false, false, amendment, "", "context is included and command is excluded"); 230 | } else { 231 | return (true, true, amendment, "\x1b[1;91m", "context is included and command is not excluded"); 232 | } 233 | } 234 | 235 | if check_command(command, include["command"].clone()) { 236 | if check_context(context, exclude["context"].clone()) { 237 | return (false, false, amendment, "", "command is included and context is excluded"); 238 | } else { 239 | return (true, true, amendment, "\x1b[1;93m", "command is included and context is not excluded"); 240 | } 241 | } 242 | 243 | if check_context(context, exclude["context"].clone()) { 244 | return (false, false, amendment, "", "context is excluded and command is not included"); 245 | } 246 | 247 | if check_command(command, exclude["command"].clone()) { 248 | return (false, false, amendment, "", "command is excluded and context is not included"); 249 | } 250 | 251 | if check_last_validation(context) { 252 | return (false, true, amendment, "", "context has already been validated earlier"); 253 | } 254 | 255 | (true, true, amendment, "\x1b[1;93m", "fallback to default behavior") 256 | } 257 | 258 | fn save_context(context: &str) -> std::io::Result<()> { 259 | let pidfile = 260 | env::temp_dir().join(env::var("KUBEKEEPER_PIDFILE").unwrap_or_else(|_| "kubekeeper.pid".to_string())); 261 | 262 | fs::write(pidfile, context) 263 | } 264 | 265 | /// Instead of forking to kubectx, explicitly ask if the current context is correct. 266 | #[allow(clippy::print_literal)] 267 | fn validate_context(context: &str, namespace: &str, color: &str) -> std::io::Result { 268 | eprint!("Really run command in {}{context}:{namespace}{}? ", color, "\x1b[0m"); 269 | eprint!("Press \"y\" to continue. Anything else will exit. "); 270 | std::io::stdout().flush()?; 271 | 272 | if let Ok(status) = Command::new("sh") 273 | .arg("-c") 274 | .arg("read -n1 && ([[ $REPLY != '' ]] && echo 1>&2) && [[ $REPLY == 'y' ]]") 275 | .status() 276 | { 277 | return Ok(status.success()); 278 | } 279 | 280 | // If executing a child process fails for some reasons, fallback to reading stdin 281 | let mut buffer = String::new(); 282 | std::io::stdin().read_line(&mut buffer)?; 283 | buffer = buffer.trim().to_string(); 284 | Ok(buffer == "y") 285 | } 286 | 287 | fn main() { 288 | // Parse configuration 289 | let (include, exclude) = get_config(); 290 | 291 | // Fetch current context and namespace 292 | let context = String::from_utf8( 293 | Command::new("kubectl") 294 | .arg("config") 295 | .arg("current-context") 296 | .output() 297 | .expect("failed to execute process") 298 | .stdout, 299 | ) 300 | .unwrap() 301 | .trim() 302 | .to_owned(); 303 | let namespace = { 304 | let namespace = String::from_utf8( 305 | Command::new("kubectl") 306 | .arg("config") 307 | .arg("view") 308 | .arg("--minify") 309 | .arg("--output=jsonpath={..namespace}") 310 | .output() 311 | .expect("failed to execute process") 312 | .stdout, 313 | ) 314 | .unwrap(); 315 | if namespace.is_empty() { 316 | "default".to_string() 317 | } else { 318 | namespace 319 | } 320 | }; 321 | edebugln!("Found context={context:?} namespace={namespace:?}"); 322 | 323 | // Rebuild command 324 | let command = env::args().skip(1).collect::>().join(" "); 325 | edebugln!("Received command={command:?}"); 326 | 327 | // Figure out what to do 328 | let (validation, record, amendment, color, reason) = 329 | identify_actions(&context, &command, include, exclude); 330 | edebugln!( 331 | "Decided validation={validation:?} record={record:?} amendment={amendment:?} color={color:?} reason={reason:?}" 332 | ); 333 | 334 | // Set new context if needed 335 | if validation { 336 | match validate_context(&context, &namespace, color) { 337 | Ok(true) => {} 338 | _ => { 339 | eprintln!("Failed to validate context. Abort."); 340 | std::process::exit(1); 341 | } 342 | } 343 | } 344 | 345 | // Save new context to prevent revalidation 346 | if record { 347 | // Use unwrap_or_default to do nothing if writing to pidfile failed 348 | save_context(&context).unwrap_or_default(); 349 | } 350 | 351 | // Run kubectl with context 352 | if amendment { 353 | Command::new("kubectl") 354 | // Context may be empty and will result in running command in the current context 355 | .arg(format!("--context={context}")) 356 | .args(env::args().skip(1)) 357 | .exec(); 358 | } else { 359 | Command::new("kubectl").args(env::args().skip(1)).exec(); 360 | } 361 | } 362 | 363 | #[cfg(test)] 364 | mod tests { 365 | use super::*; 366 | 367 | #[test] 368 | fn test_check_context_against_pattern() { 369 | let scenarios = [ 370 | // No wildcard 371 | ("kube-production-1", "kube-production-1", true), // Exact match 372 | ("kube-production-1", "kube-production-2", false), // Non-matching suffix 373 | ("kube-production-1", "kube-staging-1", false), // Non-matching infix 374 | ("kube-production-1", "mesos-production-1", false), // Non-matching prefix 375 | ("kube-production-1", "kube-production-123", false), // Missing suffix 376 | ("kube-production-1", "kube-prod", false), // Extra suffix 377 | ("kube-production-1", "extra-kube-production-1", false), // Missing prefix 378 | ("kube-production-1", "production-1", false), // Extra prefix 379 | ("kube-production-1", "", false), // Non-matching empty 380 | ("", "", true), // Matching empty 381 | // Single wildcard 382 | ("kube-production-1", "*", true), // Global wildcard 383 | ("kube-production-1", "*-production-1", true), // Prefix wildcard 384 | ("kube-production-1", "kube-*-1", true), // Infix wildcard 385 | ("kube-production-1", "kube-prod*", true), // Suffix wildcard 386 | ("kube-production-1", "*kube-production-1", true), // Extra prefix wildcard 387 | ("kube-production-1", "kube-product*ion-1", true), // Extra infix wildcard 388 | ("kube-production-1", "kube-production-1*", true), // Extra suffix wildcard 389 | ("kube-production-1", "*-staging-1", false), // Non-matching suffix 390 | ("kube-production-1", "kube-*-2", false), // Non-matching infix 391 | ("kube-production-1", "kube-staging*", false), // Non-matching prefix 392 | ("", "*", true), // Empty 393 | // Multiple wildcards 394 | ("kube-production-1", "kube-prod*-*", true), // Any wildcards 395 | ("kube-production-1", "*prod*", true), // Contains string 396 | ("kube-production-1", "**prod**", true), // Contains string with repeated wildcards 397 | ("kube-production-1", "***", true), // Repeated wildcards smaller than length 398 | ("kube-production-1", "*****************", true), // Repeated wildcards equals to length 399 | ("kube-production-1", "********************", true), // Repeated wildcards longer than length 400 | ("kube-production-1", "*staging*", false), // Contains non-matching string 401 | ("", "*prod*", false), // Non-matching empty 402 | ("", "***", true), // Matching empty 403 | ]; 404 | 405 | for (context, pattern, expected) in scenarios { 406 | assert_eq!( 407 | check_context_against_pattern(context, pattern), 408 | expected, 409 | "context={context} pattern={pattern}", 410 | ); 411 | } 412 | } 413 | } 414 | --------------------------------------------------------------------------------