├── README.md ├── exceptions.txt ├── get-orphaned-configmaps.py └── requirements.txt /README.md: -------------------------------------------------------------------------------- 1 | # Orphaned ConfigMaps 2 | 3 | This repository contains a script to identify orphaned ConfigMaps in a Kubernetes namespace. Orphaned ConfigMaps are those that are not referenced by any active Pods or containers within the namespace. 4 | 5 | ## Deprecation notice 6 | 7 | This repo is deprecated for https://github.com/yonahd/kor 8 | 9 | Kor covers finding unused configmaps and many other resource. It also contains many other features and enhancements. 10 | 11 | ## Prerequisites 12 | 13 | Before running the script, make sure you have the following prerequisites installed: 14 | 15 | - Python 3.x 16 | - `pip` package installer 17 | 18 | ## Installation 19 | 20 | 1. Clone this repository to your local machine: 21 | 22 | ```shell 23 | git clone https://github.com/yonahd/orphaned-configmaps.git 24 | ``` 25 | 2. Change into the repository directory: 26 | ```shell 27 | cd orphaned-configmaps 28 | ``` 29 | 3. Install the required Python dependencies: 30 | ```shell 31 | pip install -r requirements.txt 32 | ``` 33 | 34 | ### Usage Examples 35 | 36 | - To scan all namespaces for orphaned ConfigMaps: 37 | ``` 38 | python get-orphaned-configmaps.py 39 | ``` 40 | 41 | - To specify one or more namespaces to scan for orphaned ConfigMaps: 42 | ``` 43 | python get-orphaned-configmaps.py -n namespace1 namespace2 44 | ``` 45 | 46 | - To exclude specific namespaces from the scan: 47 | ``` 48 | python get-orphaned-configmaps.py --exclude exclude_ns1 exclude_ns2 49 | ``` 50 | 51 | Note: The `--exclude` flag cannot be used together with the `-n/--namespace` flag. 52 | 53 | 54 | The script will display a table of orphaned ConfigMaps, if any are found. 55 | 56 | ## Example output 57 | ```shell 58 | 59 | Unused Config Maps in Namespace: my-namespace 60 | ┏━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ 61 | ┃ # ┃ Config Map Name ┃ 62 | ┡━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ 63 | │ 1 │ unused-configmap-1 │ 64 | │ 2 │ super-important-configmap │ 65 | └────┴─────────────────────────────────────────────────────────────────┘ 66 | ``` 67 | 68 | ## Exceptions File 69 | The exception file (exceptions.txt) is a file that allows you to define exceptions to the orphaned ConfigMap detection. Sometimes, not every ConfigMap exists for a direct pod reference. 70 | 71 | For example, a ConfigMap like aws-auth in the kube-system namespace is necessary for configuring AWS authentication and is actively monitored by a Kubernetes-native application at runtime. Similarly, ConfigMaps can be used to pass data between different parts of your infrastructure. 72 | 73 | The exception file follows the format: 74 | 75 | ``` 76 | config_map_name, namespace, explanation 77 | ``` 78 | - config_map_name: The name of the ConfigMap that is an exception. 79 | - namespace: The namespace where the exception applies. 80 | - explanation: An explanation or description of why this ConfigMap is considered an exception. 81 | 82 | You can add exceptions to the exceptions.txt file to prevent the script from flagging those ConfigMaps as orphaned. 83 | 84 | 85 | ## Caveat 86 | The script looks at pods and containers, so if you are consuming it in a deployment that is scaled to 0 it will not be detected. 87 | -------------------------------------------------------------------------------- /exceptions.txt: -------------------------------------------------------------------------------- 1 | my-configmap,my-namespace,Exception: Custom exception explanation 2 | aws-auth,kube-system,Exception: Contains critical cluster configuration 3 | 4 | -------------------------------------------------------------------------------- /get-orphaned-configmaps.py: -------------------------------------------------------------------------------- 1 | from kubernetes import client, config 2 | import argparse 3 | from rich.table import Table 4 | from rich.console import Console 5 | import os 6 | 7 | EXCEPTIONS_FILE = "exceptions.txt" 8 | 9 | 10 | def load_exceptions(): 11 | exceptions = [] 12 | file_path = os.path.join(os.path.dirname(__file__), EXCEPTIONS_FILE) 13 | with open(file_path, "r") as file: 14 | for line in file: 15 | line = line.strip() 16 | if line and not line.startswith("#"): 17 | parts = line.split(",") 18 | if len(parts) >= 3: 19 | config_map_name = parts[0].strip() 20 | namespace = parts[1].strip() 21 | explanation = ",".join(parts[2:]).strip() 22 | exceptions.append((config_map_name, namespace, explanation)) 23 | return exceptions 24 | 25 | 26 | def get_namespace_exceptions(namespace, exceptions): 27 | namespace_exceptions = [] 28 | for exception in exceptions: 29 | config_map_name, exception_namespace, explanation = exception 30 | if exception_namespace == namespace: 31 | namespace_exceptions.append(config_map_name) 32 | return namespace_exceptions 33 | 34 | 35 | def retrieve_volumes_and_env(api_instance, namespace): 36 | volumesCM = [] 37 | volumesProjectedCM = [] 38 | envCM = [] 39 | envFromCM = [] 40 | envFromContainerCM = [] 41 | 42 | # Retrieve pods in the specified namespace 43 | pods = api_instance.list_namespaced_pod(namespace) 44 | 45 | # Extract volume and environment information from pods 46 | for pod in pods.items: 47 | for volume in pod.spec.volumes: 48 | if volume.config_map: 49 | volumesCM.append(volume.config_map.name) 50 | if volume.projected: 51 | for source in volume.projected.sources: 52 | if source.config_map: 53 | volumesProjectedCM.append(source.config_map.name) 54 | for container in pod.spec.containers: 55 | if container.env: 56 | for env in container.env: 57 | if env.value_from and env.value_from.config_map_key_ref: 58 | envCM.append(env.value_from.config_map_key_ref.name) 59 | 60 | if container.env_from: 61 | for env_from in container.env_from: 62 | if env_from.config_map_ref: 63 | envFromCM.append(env_from.config_map_ref.name) 64 | if container.env_from: 65 | for env_from in container.env_from: 66 | if env_from.config_map_ref: 67 | envFromContainerCM.append(env_from.config_map_ref.name) 68 | 69 | return volumesCM, volumesProjectedCM, envCM, envFromCM, envFromContainerCM 70 | 71 | 72 | def retrieve_configmap_names(api_instance, namespace): 73 | configmaps = api_instance.list_namespaced_config_map(namespace) 74 | return [configmap.metadata.name for configmap in configmaps.items] 75 | 76 | 77 | def calculate_difference(used_configmaps, configmap_names): 78 | return sorted(set(configmap_names) - set(used_configmaps)) 79 | 80 | 81 | def format_output(namespace, configmap_names): 82 | if not configmap_names: 83 | return f"No unused config maps found in the namespace: {namespace}" 84 | 85 | table = Table(show_header=True, header_style="bold", title=f"Unused Config Maps in Namespace: {namespace}") 86 | table.add_column("#", justify="right") 87 | table.add_column("Config Map Name") 88 | 89 | for i, name in enumerate(configmap_names, start=1): 90 | table.add_row(str(i), name) 91 | 92 | return table 93 | 94 | 95 | def process_namespace(api_instance, namespace, exceptions): 96 | # Retrieve volumes and environment information for the namespace 97 | volumesCM, volumesProjectedCM, envCM, envFromCM, envFromContainerCM = retrieve_volumes_and_env(api_instance, namespace) 98 | 99 | # Remove duplicates and sort the lists 100 | volumesCM = sorted(set(volumesCM)) 101 | volumesProjectedCM = sorted(set(volumesProjectedCM)) 102 | envCM = sorted(set(envCM)) 103 | envFromCM = sorted(set(envFromCM)) 104 | envFromContainerCM = sorted(set(envFromContainerCM)) 105 | 106 | # Retrieve config map names for the namespace 107 | configmap_names = retrieve_configmap_names(api_instance, namespace) 108 | 109 | # Calculate the difference between the two sets of names 110 | used_configmaps = volumesCM + volumesProjectedCM + envCM + envFromCM + envFromContainerCM + exceptions 111 | diff = calculate_difference(used_configmaps, configmap_names) 112 | 113 | # Format and return the output for the namespace 114 | return format_output(namespace, diff) 115 | 116 | 117 | def main(namespaces=None, exclude_list=None): 118 | # Load the Kubernetes configuration 119 | config.load_kube_config() 120 | 121 | # Create Kubernetes API client 122 | api_instance = client.CoreV1Api() 123 | 124 | exceptions = load_exceptions() 125 | 126 | if namespaces: 127 | if exclude_list: 128 | print("Error: --exclude flag cannot be used together with -n/--namespace flag.") 129 | return 130 | 131 | if namespaces is None: 132 | namespaces = [] 133 | namespace_list = api_instance.list_namespace() 134 | for ns in namespace_list.items: 135 | if exclude_list and ns.metadata.name in exclude_list: 136 | continue 137 | namespaces.append(ns.metadata.name) 138 | 139 | for namespace in namespaces: 140 | # Process a specific namespace 141 | namespaced_exceptions = get_namespace_exceptions(namespace, exceptions) 142 | output = process_namespace(api_instance, namespace, namespaced_exceptions) 143 | console = Console() 144 | console.print(output) 145 | console.print("\n") 146 | 147 | 148 | if __name__ == "__main__": 149 | parser = argparse.ArgumentParser(description="Identify orphaned ConfigMaps in a Kubernetes namespace.") 150 | parser.add_argument("-n", "--namespace", nargs="+", help="Specify one or more namespaces to scan for orphaned" 151 | " ConfigMaps.") 152 | parser.add_argument("--exclude", nargs="+", help="Specify namespaces to exclude from scanning.") 153 | args = parser.parse_args() 154 | 155 | main(args.namespace, args.exclude) 156 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | kubernetes 2 | rich 3 | tabulate 4 | --------------------------------------------------------------------------------