Created
August 13, 2025 22:11
-
-
Save orangewolf/a9fa5040cc2722071172651c55b91c33 to your computer and use it in GitHub Desktop.
Script to find all bitnami images in the current k8s cluster
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env ruby | |
# frozen_string_literal: true | |
require 'json' | |
require 'open3' | |
require 'optparse' | |
# Ruby script to find Bitnami-based container images deployed in Kubernetes cluster | |
# Shows namespace, pod/deployment info, and associated Helm chart when available | |
class BitnamiFinder | |
# ANSI color codes | |
COLORS = { | |
red: "\033[0;31m", | |
green: "\033[0;32m", | |
blue: "\033[0;34m", | |
yellow: "\033[1;33m", | |
reset: "\033[0m" | |
}.freeze | |
def initialize(options = {}) | |
@options = options | |
@verbose = options[:verbose] || false | |
@summary_only = options[:summary_only] || false | |
@namespace_filter = options[:namespace] | |
end | |
def run | |
print_header | |
check_prerequisites | |
show_cluster_info | |
unless @summary_only | |
find_bitnami_pods | |
find_bitnami_workloads | |
puts | |
end | |
print_summary | |
end | |
private | |
def print_header | |
puts colorize("==========================================", :blue) | |
puts colorize(" Bitnami Images in Kubernetes Cluster ", :blue) | |
puts colorize("==========================================", :blue) | |
puts | |
end | |
def colorize(text, color) | |
return text unless $stdout.tty? | |
"#{COLORS[color]}#{text}#{COLORS[:reset]}" | |
end | |
def check_prerequisites | |
unless command_exists?('kubectl') | |
puts colorize("Error: kubectl is not installed or not in PATH", :red) | |
exit 1 | |
end | |
stdout, stderr, status = Open3.capture3('kubectl cluster-info') | |
unless status.success? | |
puts colorize("Error: Cannot connect to Kubernetes cluster", :red) | |
puts stderr if @verbose | |
exit 1 | |
end | |
end | |
def command_exists?(command) | |
system("which #{command} > /dev/null 2>&1") | |
end | |
def show_cluster_info | |
current_context, = Open3.capture3('kubectl config current-context') | |
puts "#{colorize('Current cluster context:', :green)} #{current_context.strip}" | |
puts | |
end | |
def execute_kubectl(command) | |
stdout, stderr, status = Open3.capture3("kubectl #{command}") | |
unless status.success? | |
puts colorize("Error executing: kubectl #{command}", :red) if @verbose | |
puts stderr if @verbose | |
return nil | |
end | |
stdout | |
end | |
def get_helm_info(namespace, resource_type, resource_name) | |
# Try to get Helm labels | |
labels_cmd = "get #{resource_type} #{resource_name} -n #{namespace} -o jsonpath='{.metadata.labels}'" | |
labels_output = execute_kubectl(labels_cmd) | |
return "No Helm info" unless labels_output | |
# Try different Helm label formats | |
helm_chart = get_label_value(namespace, resource_type, resource_name, 'helm\.sh/chart') || | |
get_label_value(namespace, resource_type, resource_name, 'chart') | |
helm_release = get_label_value(namespace, resource_type, resource_name, 'app\.kubernetes\.io/instance') || | |
get_label_value(namespace, resource_type, resource_name, 'release') | |
if helm_release && helm_chart | |
"Helm: #{helm_release} (#{helm_chart})" | |
elsif helm_release | |
"Helm: #{helm_release}" | |
elsif helm_chart | |
"Chart: #{helm_chart}" | |
else | |
"No Helm info" | |
end | |
rescue | |
"No Helm info" | |
end | |
def get_label_value(namespace, resource_type, resource_name, label_key) | |
cmd = "get #{resource_type} #{resource_name} -n #{namespace} -o jsonpath='{.metadata.labels.#{label_key}}'" | |
output = execute_kubectl(cmd) | |
output&.strip&.empty? ? nil : output&.strip | |
end | |
def find_bitnami_pods | |
puts colorize("Searching for Bitnami images across all namespaces...", :green) | |
puts | |
namespace_selector = @namespace_filter ? "-n #{@namespace_filter}" : "--all-namespaces" | |
pods_json = execute_kubectl("get pods #{namespace_selector} -o json") | |
return unless pods_json | |
pods_data = JSON.parse(pods_json) | |
bitnami_pods = [] | |
pods_data['items'].each do |pod| | |
bitnami_images = pod.dig('spec', 'containers')&.map { |c| c['image'] }&.select { |img| img.include?('bitnami') } | |
next if bitnami_images.nil? || bitnami_images.empty? | |
namespace = pod.dig('metadata', 'namespace') | |
name = pod.dig('metadata', 'name') | |
owner_refs = pod.dig('metadata', 'ownerReferences') || [] | |
bitnami_pods << { | |
namespace: namespace, | |
name: name, | |
images: bitnami_images, | |
owner_refs: owner_refs | |
} | |
end | |
display_bitnami_resources(bitnami_pods, 'pod') | |
end | |
def find_bitnami_workloads | |
puts colorize("Checking Deployments, StatefulSets, and DaemonSets for Bitnami images...", :green) | |
puts | |
%w[deployment statefulset daemonset].each do |resource_type| | |
find_bitnami_in_resource_type(resource_type) | |
end | |
end | |
def find_bitnami_in_resource_type(resource_type) | |
namespace_selector = @namespace_filter ? "-n #{@namespace_filter}" : "--all-namespaces" | |
resources_json = execute_kubectl("get #{resource_type} #{namespace_selector} -o json") | |
return unless resources_json | |
begin | |
resources_data = JSON.parse(resources_json) | |
bitnami_resources = [] | |
resources_data['items'].each do |resource| | |
containers = resource.dig('spec', 'template', 'spec', 'containers') || [] | |
bitnami_images = containers.map { |c| c['image'] }.select { |img| img.include?('bitnami') } | |
next if bitnami_images.empty? | |
namespace = resource.dig('metadata', 'namespace') | |
name = resource.dig('metadata', 'name') | |
bitnami_resources << { | |
namespace: namespace, | |
name: name, | |
images: bitnami_images, | |
resource_type: resource_type | |
} | |
end | |
display_workload_resources(bitnami_resources) | |
rescue JSON::ParserError => e | |
puts colorize("Warning: Could not parse #{resource_type} JSON: #{e.message}", :yellow) if @verbose | |
end | |
end | |
def display_bitnami_resources(resources, type) | |
resources.each do |resource| | |
puts "#{colorize('Namespace:', :yellow)} #{resource[:namespace]}" | |
puts "#{colorize('Pod:', :yellow)} #{resource[:name]}" | |
# Handle owner references | |
if resource[:owner_refs].any? | |
owner = resource[:owner_refs].first | |
puts "#{colorize('Owner:', :yellow)} #{owner['kind']}/#{owner['name']}" | |
helm_info = get_helm_info(resource[:namespace], owner['kind'].downcase, owner['name']) | |
else | |
helm_info = get_helm_info(resource[:namespace], 'pod', resource[:name]) | |
end | |
puts "#{colorize('Helm Info:', :yellow)} #{helm_info}" | |
puts colorize('Bitnami Images:', :yellow) | |
resource[:images].each do |image| | |
puts " - #{image}" | |
end | |
puts "---" | |
end | |
end | |
def display_workload_resources(resources) | |
resources.each do |resource| | |
puts "#{colorize('Namespace:', :yellow)} #{resource[:namespace]}" | |
puts "#{colorize('Resource:', :yellow)} #{resource[:resource_type]}/#{resource[:name]}" | |
helm_info = get_helm_info(resource[:namespace], resource[:resource_type], resource[:name]) | |
puts "#{colorize('Helm Info:', :yellow)} #{helm_info}" | |
puts colorize('Bitnami Images:', :yellow) | |
resource[:images].each do |image| | |
puts " - #{image}" | |
end | |
puts "---" | |
end | |
end | |
def print_summary | |
puts colorize("========== SUMMARY ==========", :blue) | |
# Get all images from all pods | |
namespace_selector = @namespace_filter ? "-n #{@namespace_filter}" : "--all-namespaces" | |
all_images_output = execute_kubectl("get pods #{namespace_selector} -o jsonpath='{range .items[*]}{.spec.containers[*].image}{\" \"}{end}'") | |
return unless all_images_output | |
all_images = all_images_output.split(' ').select { |img| img.include?('bitnami') } | |
unique_images = all_images.uniq.sort | |
# Get namespace and pod counts | |
pods_json = execute_kubectl("get pods #{namespace_selector} -o json") | |
return unless pods_json | |
pods_data = JSON.parse(pods_json) | |
namespaces_with_bitnami = Set.new | |
pods_with_bitnami = 0 | |
pods_data['items'].each do |pod| | |
bitnami_containers = pod.dig('spec', 'containers')&.select { |c| c['image'].include?('bitnami') } | |
if bitnami_containers && !bitnami_containers.empty? | |
namespaces_with_bitnami << pod.dig('metadata', 'namespace') | |
pods_with_bitnami += 1 | |
end | |
end | |
puts "#{colorize('Unique Bitnami images:', :green)} #{unique_images.count}" | |
puts "#{colorize('Namespaces with Bitnami:', :green)} #{namespaces_with_bitnami.count}" | |
puts "#{colorize('Pods using Bitnami images:', :green)} #{pods_with_bitnami}" | |
puts | |
puts colorize('Most common Bitnami images:', :yellow) | |
image_counts = all_images.each_with_object(Hash.new(0)) { |img, hash| hash[img] += 1 } | |
image_counts.sort_by { |_, count| -count }.first(10).each do |image, count| | |
puts " #{count} x #{image}" | |
end | |
# Show namespace breakdown if not filtering by namespace | |
unless @namespace_filter | |
puts | |
puts colorize('Namespaces with Bitnami images:', :yellow) | |
namespaces_with_bitnami.sort.each do |ns| | |
puts " - #{ns}" | |
end | |
end | |
end | |
end | |
# Command line option parsing | |
options = {} | |
OptionParser.new do |opts| | |
opts.banner = "Usage: #{$0} [options]" | |
opts.on("-v", "--verbose", "Run verbosely") do |v| | |
options[:verbose] = v | |
end | |
opts.on("-s", "--summary", "Show summary only") do |s| | |
options[:summary_only] = s | |
end | |
opts.on("-n", "--namespace NAMESPACE", "Filter by namespace") do |n| | |
options[:namespace] = n | |
end | |
opts.on("-h", "--help", "Prints this help") do | |
puts opts | |
exit | |
end | |
end.parse! | |
# Run the finder | |
finder = BitnamiFinder.new(options) | |
finder.run |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment