diff --git a/README.md b/README.md index 199919e..3b5e2e3 100644 --- a/README.md +++ b/README.md @@ -12,16 +12,19 @@ alias ks=kubectl-search # Find all configmaps values which contains search mask # ks values -$ ks values apps "backup" +$ ks values --secrets apps "backup" -- ConfigMap: app-cm +- config-map: app-cm Keys: - 'BACKUP_SRV_HOST': 'app-backup-svc' -- ConfigMap: another-app-cm +- config-map: another-app-cm Keys: - 'BACKUP_USER': 'app-backup-svc' - + +- secret: db-secret + Keys: + - 'BACKUP_USERNAME': 'backupper' ``` ## How it works @@ -34,6 +37,6 @@ Tool doesn't use any write or delete commands inside cluster. ## Roadmap -1. Search values in Secrets +1. Mask secret values by mask: PASSWORD, TOKEN 2. Search values in Vault secret values 3. Support cache \ No newline at end of file diff --git a/src/cli.rs b/src/cli.rs index 967e82c..7d3a113 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,7 +1,7 @@ use std::env; use std::path::Path; -use clap::{Arg, ArgMatches, Command}; +use clap::{Arg, ArgAction, ArgMatches, Command}; use log::debug; pub const WORKDIR: &str = "."; @@ -16,6 +16,11 @@ pub const VALUES_COMMAND: &str = "values"; pub const NAMESPACE_ARG: &str = "namespace"; pub const SEARCH_MASK_ARG: &str = "mask"; +pub const SECRETS_FLAG: &str = "secrets"; + +pub const IGNORE_BASE64_ERRORS_FLAG: &str = "ignore-base64-errors"; +pub const IGNORE_UTF8_ERRORS_FLAG: &str = "ignore-utf8-errors"; + pub fn init_cli_app() -> ArgMatches { Command::new("kubectl-search") .version("0.2.0") @@ -40,8 +45,11 @@ pub fn init_cli_app() -> ArgMatches { .subcommand( Command::new(VALUES_COMMAND) .about("search values by mask") + .arg(get_ignore_base64_errors_flag()) + .arg(get_ignore_utf8_errors_flag()) .arg(get_k8s_namespace_arg()) .arg(get_search_mask_arg()) + .arg(get_secrets_flag()) ) .get_matches() @@ -59,6 +67,30 @@ fn get_search_mask_arg() -> Arg { .required(true) } +fn get_secrets_flag() -> Arg { + Arg::new(SECRETS_FLAG) + .long(SECRETS_FLAG) + .help("search in secret values") + .action(ArgAction::SetTrue) + .required(false) +} + +fn get_ignore_base64_errors_flag() -> Arg { + Arg::new(IGNORE_BASE64_ERRORS_FLAG) + .long(IGNORE_BASE64_ERRORS_FLAG) + .help("ignore base64 decoding errors. Use secret value AS IS if base64 related error occurs") + .action(ArgAction::SetTrue) + .required(false) +} + +fn get_ignore_utf8_errors_flag() -> Arg { + Arg::new(IGNORE_UTF8_ERRORS_FLAG) + .long(IGNORE_UTF8_ERRORS_FLAG) + .help("ignore utf-8 related errors. Use secret value AS IS if utf-8 related error occurs") + .action(ArgAction::SetTrue) + .required(false) +} + pub fn init_working_dir(matches: &ArgMatches) { let working_directory: &Path = get_argument_path_value( &matches, WORK_DIR_ARGUMENT, WORKDIR); diff --git a/src/k8s/mod.rs b/src/k8s/mod.rs deleted file mode 100644 index 7527491..0000000 --- a/src/k8s/mod.rs +++ /dev/null @@ -1,25 +0,0 @@ -use std::collections::HashMap; - -use serde::Deserialize; - -pub const KUBECTL_EXEC_PATH: &str = "kubectl"; - -pub trait KubectlTool { -} - -pub struct KubectlToolImpl; - -impl KubectlToolImpl { - pub fn new() -> Self { - Self - } -} - -impl KubectlTool for KubectlToolImpl { - -} - -#[derive(Deserialize)] -pub struct KubernetesResource { - pub data: HashMap -} diff --git a/src/main.rs b/src/main.rs index a9c742c..ddee98d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,14 +5,13 @@ use clap::ArgMatches; use kubectl_wrapper_rs::executor::DefaultKubectlExecutor; use kubectl_wrapper_rs::KubectlWrapperImpl; -use crate::cli::{init_cli_app, init_working_dir, LOG_LEVEL_ARGUMENT, LOG_LEVEL_DEFAULT_VALUE, NAMESPACE_ARG, SEARCH_MASK_ARG}; +use crate::cli::{IGNORE_BASE64_ERRORS_FLAG, IGNORE_UTF8_ERRORS_FLAG, init_cli_app, init_working_dir, LOG_LEVEL_ARGUMENT, LOG_LEVEL_DEFAULT_VALUE, NAMESPACE_ARG, SEARCH_MASK_ARG, SECRETS_FLAG}; use crate::logging::get_logging_config; use crate::output::print_search_results; -use crate::usecase::values::search_values_in_configmaps; +use crate::usecase::values::{search_values, ValuesSearchOptions}; mod cli; mod logging; -mod k8s; mod usecase; mod output; @@ -30,12 +29,27 @@ fn main() { println!("find configmap values in '{namespace}' namespace with mask '{search_mask}'.."); + let search_in_secrets = matches.get_flag(SECRETS_FLAG); + let ignore_base64_errors = matches.get_flag(IGNORE_BASE64_ERRORS_FLAG); + let ignore_utf8_errors = matches.get_flag(IGNORE_UTF8_ERRORS_FLAG); + + println!("- search in secret values: {search_in_secrets}"); + println!("- ignore base64 errors: {ignore_base64_errors}"); + println!("- ignore utf8 errors: {ignore_utf8_errors}"); + check_required_env_vars(&vec!["KUBECONFIG"]); let executor = DefaultKubectlExecutor::new(); let kubectl_tool = KubectlWrapperImpl::new(&executor); - match search_values_in_configmaps(&kubectl_tool, &kubectl_tool, &namespace, &search_mask) { + let search_options = ValuesSearchOptions { + search_in_secrets, + ignore_base64_errors, + ignore_utf8_errors, + }; + + match search_values(&kubectl_tool, &kubectl_tool, &kubectl_tool, + &namespace, &search_mask, &search_options) { Ok(search_results) => print_search_results(&search_results, &search_mask), Err(e) => eprintln!("error: {}", e) } diff --git a/src/output.rs b/src/output.rs index c26a2be..85c75f5 100644 --- a/src/output.rs +++ b/src/output.rs @@ -1,3 +1,5 @@ +use kubectl_wrapper_rs::KubernetesResourceType; + use crate::usecase::SearchResult; pub fn print_search_results(search_results: &Vec, search_mask: &str) { @@ -11,7 +13,15 @@ pub fn print_search_results(search_results: &Vec, search_mask: &st } else { for search_result in search_results { if !search_result.values.is_empty() { - println!("- config-map: '{}'", search_result.resource_name); + match search_result.resource_type { + KubernetesResourceType::ConfigMap => { + println!("- config-map: '{}'", search_result.resource_name) + } + KubernetesResourceType::Secret => { + println!("- secret: '{}'", search_result.resource_name) + } + _ => {} + } for (k, v) in &search_result.values { println!(" - '{k}': '{v}'") diff --git a/src/usecase/mod.rs b/src/usecase/mod.rs index 5a176bf..0458352 100644 --- a/src/usecase/mod.rs +++ b/src/usecase/mod.rs @@ -1,8 +1,11 @@ use std::collections::HashMap; +use kubectl_wrapper_rs::KubernetesResourceType; + pub mod values; pub struct SearchResult { pub resource_name: String, + pub resource_type: KubernetesResourceType, pub values: HashMap } \ No newline at end of file diff --git a/src/usecase/values.rs b/src/usecase/values.rs index f85e328..6f17c98 100644 --- a/src/usecase/values.rs +++ b/src/usecase/values.rs @@ -1,27 +1,34 @@ use std::collections::HashMap; -use std::error::Error; use anyhow::anyhow; use kubectl_wrapper_rs::{KubectlWrapper, KubernetesResourceType}; use kubectl_wrapper_rs::configmap::KubectlConfigMapWrapper; use kubectl_wrapper_rs::error::KubectlWrapperError; +use kubectl_wrapper_rs::secret::KubectlSecretWrapper; use log::{debug, info}; -use crate::k8s::KubectlTool; use crate::usecase::SearchResult; -pub fn search_values_in_configmaps( +pub struct ValuesSearchOptions { + pub search_in_secrets: bool, + pub ignore_base64_errors: bool, + pub ignore_utf8_errors: bool, +} + +pub fn search_values( kubectl_tool: &dyn KubectlWrapper, kubectl_configmap_tool: &dyn KubectlConfigMapWrapper, - namespace: &str, mask: &str) -> anyhow::Result> { - info!("search values in config-maps with mask '{mask}', namespace '{namespace}'.."); + kubectl_secret_tool: &dyn KubectlSecretWrapper, + namespace: &str, mask: &str, + search_options: &ValuesSearchOptions) -> anyhow::Result> { + info!("search values with mask '{mask}', namespace '{namespace}'.."); + + let mut results: Vec = vec![]; match kubectl_tool.get_resource_names(namespace, KubernetesResourceType::ConfigMap) { Ok(names) => { debug!("configmaps received: {:?}", names); - let mut results: Vec = vec![]; - let names: Vec = names.into_iter().filter(|n| { let name = n.trim(); !name.is_empty() @@ -43,6 +50,7 @@ pub fn search_values_in_configmaps( let result = SearchResult { resource_name: configmap_name.to_string(), + resource_type: KubernetesResourceType::ConfigMap, values: filtered_map.clone(), }; @@ -53,11 +61,57 @@ pub fn search_values_in_configmaps( } } } - - Ok(results) } Err(_) => { - Err(anyhow!(format!("unable to get configmap names in namespace '{namespace}'"))) + return Err(anyhow!(format!("unable to get configmap names in namespace '{namespace}'"))) } } + + if search_options.search_in_secrets { + match kubectl_tool.get_resource_names(namespace, KubernetesResourceType::Secret) { + Ok(names) => { + debug!("secrets received: {:?}", names); + + let names: Vec = names.into_iter().filter(|n| { + let name = n.trim(); + !name.is_empty() + }).collect::>(); + + for configmap_name in names { + match kubectl_secret_tool.get_secret_key_values(&namespace, &configmap_name, + search_options.ignore_base64_errors, + search_options.ignore_utf8_errors) { + Ok(config_map_values) => { + let mut filtered_map: HashMap = HashMap::new(); + + for (k, v) in config_map_values { + let lowercased_values = v.to_lowercase(); + + if lowercased_values.contains(&mask) { + info!("- match '{k}': '{v}'"); + let _ = &filtered_map.insert(k, v); + } + } + + let result = SearchResult { + resource_name: configmap_name.to_string(), + resource_type: KubernetesResourceType::Secret, + values: filtered_map.clone(), + }; + + results.push(result); + } + Err(_) => { + return Err(anyhow!(format!("unable to get key-values from secret '{configmap_name}'"))) + } + } + } + } + Err(_) => { + return Err(anyhow!(format!("unable to get secrets in namespace '{namespace}'"))) + } + } + } + + Ok(results) } \ No newline at end of file