Skip to content

Commit

Permalink
Feature: search in secret values
Browse files Browse the repository at this point in the history
  • Loading branch information
lebe-dev committed May 28, 2024
1 parent 3c3e7fe commit 5cda527
Show file tree
Hide file tree
Showing 7 changed files with 137 additions and 46 deletions.
13 changes: 8 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,19 @@ alias ks=kubectl-search
# Find all configmaps values which contains search mask

# ks values <namespace> <search-mask>
$ 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
Expand All @@ -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
34 changes: 33 additions & 1 deletion src/cli.rs
Original file line number Diff line number Diff line change
@@ -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 = ".";
Expand All @@ -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")
Expand All @@ -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()
Expand All @@ -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);
Expand Down
25 changes: 0 additions & 25 deletions src/k8s/mod.rs

This file was deleted.

22 changes: 18 additions & 4 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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)
}
Expand Down
12 changes: 11 additions & 1 deletion src/output.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use kubectl_wrapper_rs::KubernetesResourceType;

use crate::usecase::SearchResult;

pub fn print_search_results(search_results: &Vec<SearchResult>, search_mask: &str) {
Expand All @@ -11,7 +13,15 @@ pub fn print_search_results(search_results: &Vec<SearchResult>, 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}'")
Expand Down
3 changes: 3 additions & 0 deletions src/usecase/mod.rs
Original file line number Diff line number Diff line change
@@ -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<String, String>
}
74 changes: 64 additions & 10 deletions src/usecase/values.rs
Original file line number Diff line number Diff line change
@@ -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<Vec<SearchResult>> {
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<Vec<SearchResult>> {
info!("search values with mask '{mask}', namespace '{namespace}'..");

let mut results: Vec<SearchResult> = vec![];

match kubectl_tool.get_resource_names(namespace, KubernetesResourceType::ConfigMap) {
Ok(names) => {
debug!("configmaps received: {:?}", names);

let mut results: Vec<SearchResult> = vec![];

let names: Vec<String> = names.into_iter().filter(|n| {
let name = n.trim();
!name.is_empty()
Expand All @@ -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(),
};

Expand All @@ -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<String> = names.into_iter().filter(|n| {
let name = n.trim();
!name.is_empty()
}).collect::<Vec<String>>();

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<String,String> = 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)
}

0 comments on commit 5cda527

Please sign in to comment.