Skip to content

Instantly share code, notes, and snippets.

@aslafy-z
Last active April 11, 2025 11:48
Show Gist options
  • Save aslafy-z/cd3bd9f9181e09f91a55f3cfa4431257 to your computer and use it in GitHub Desktop.
Save aslafy-z/cd3bd9f9181e09f91a55f3cfa4431257 to your computer and use it in GitHub Desktop.
Script that compares two Helm charts from various sources (local path, local tarball, remote tarball, Git repository, Helm repository, OCI registry)
#!/bin/sh
set -e
# Function to display usage information
usage() {
echo "Helm Chart Comparison Tool"
echo
echo "This script compares two Helm charts from various sources (local, tarball, Git, OCI)."
echo
echo "Usage:"
echo " $0 SOURCE1 SOURCE2 [OPTIONS]"
echo
echo "Sources can be:"
echo " - Local path: /path/to/chart"
echo " - Local tarball: /path/to/chart.tgz"
echo " - Remote tarball: https://example.com/path/to/chart.tgz"
echo " - Git repository: git+https://github.com/org/repo@path/to/chart/{name}-{version}.tgz[?ref=v{version}]"
echo " - Helm repository: repo/chart:{version}"
echo " - OCI registry: oci://registry.example.com/chart:{version}"
echo
echo "Options:"
echo " -n, --namespace NAMESPACE Set namespace for rendering (default: default)"
echo " -r, --release-name NAME Set release name for rendering (default: release)"
echo " -v, --values FILE Values file for rendering templates"
echo " --values1 FILE Values file for first chart only"
echo " --values2 FILE Values file for second chart only"
echo " -o, --output-dir DIR Directory to save comparison results"
echo " -e, --expanded Show expanded diff (show unchanged parts)"
echo " --debug Enable debug logging"
echo " -h, --help Show this help message"
exit 1
}
# Function to log messages
log() {
level=$1
message=$2
if [ "$level" = "DEBUG" ] && [ "$DEBUG" = "false" ]; then
return
fi
echo "[$level] $message" >&2
}
# Function to check dependencies
check_dependencies() {
missing_deps=0
for cmd in helm jq yq diff mktemp; do
if ! command -v $cmd > /dev/null 2>&1; then
log "ERROR" "$cmd is not installed. Please install it before running this script."
missing_deps=1
fi
done
if [ $missing_deps -eq 1 ]; then
exit 1
fi
}
# Function to detect chart source type
detect_source_type() {
source=$1
case $source in
oci://*)
log "DEBUG" "Detected source type: oci"
echo "oci"
;;
git+*)
log "DEBUG" "Detected source type: git"
echo "git"
;;
*.tgz)
log "DEBUG" "Detected source type: tarball"
echo "tarball"
;;
*)
if [ -d "$source" ]; then
log "DEBUG" "Detected source type: local"
echo "local"
else
log "DEBUG" "Detected source type: repo"
echo "repo"
fi
;;
esac
}
# Function to pull a chart
pull_chart() {
source=$1
output_dir=$2
source_type=$(detect_source_type "$source")
log "INFO" "Pulling chart from $source (type: $source_type)"
case $source_type in
"oci")
# Handle oci://registry.example.com/chart:tag format
chart_name=$(echo "$source" | cut -d':' -f1-2)
version=$(echo "$source" | cut -d':' -f3)
version_flag=""
if [ "$chart_name" != "$version" ]; then
version_flag="--version $version"
fi
error_file=$(mktemp)
# Attempt to pull the chart and capture any errors
if ! helm pull "$chart_name" $version_flag --destination "$output_dir" 2> "$error_file"; then
error_msg=$(cat "$error_file")
log "ERROR" "Failed to pull chart from $source"
log "ERROR" "Helm error: $error_msg"
rm "$error_file"
exit 1
fi
rm "$error_file"
# Extract the pulled tarball
tarballs=$(find "$output_dir" -name "*.tgz")
if [ -z "$tarballs" ]; then
log "ERROR" "No tarball found after pulling $source"
exit 1
fi
for tarball in $tarballs; do
extract_dir="${tarball%.tgz}"
mkdir -p "$extract_dir"
tar -xzf "$tarball" -C "$extract_dir" --strip-components=1
rm "$tarball"
echo "$extract_dir"
return
done
;;
"git")
# For git+https://github.com/org/repo.git@branch:path/to/chart
# First, add the helm-git plugin if not already installed
if ! helm plugin list | grep -q "helm-git"; then
log "INFO" "Installing helm-git plugin"
helm plugin install https://github.com/aslafy-z/helm-git
fi
# Process the git URL
chart_path=$(echo "$source" | sed 's/.*://')
git_url=$(echo "$source" | sed 's/:.*//')
error_file=$(mktemp)
# Attempt to pull the chart and capture any errors
if ! helm pull "$git_url:$chart_path" --destination "$output_dir" 2> "$error_file"; then
error_msg=$(cat "$error_file")
log "ERROR" "Failed to pull chart from Git repository $source"
log "ERROR" "Helm error: $error_msg"
rm "$error_file"
exit 1
fi
rm "$error_file"
# Extract the pulled tarball
tarballs=$(find "$output_dir" -name "*.tgz")
if [ -z "$tarballs" ]; then
log "ERROR" "No tarball found after pulling from Git repository $source"
exit 1
fi
for tarball in $tarballs; do
extract_dir="${tarball%.tgz}"
mkdir -p "$extract_dir"
tar -xzf "$tarball" -C "$extract_dir" --strip-components=1
rm "$tarball"
echo "$extract_dir"
return
done
;;
"tarball")
# Download the file if an URL is given
if echo "$source" | grep -q "^https\?://"; then
log "DEBUG" "Remote tarball detected: $source"
# Create temporary file for download
temp_tarball=$(mktemp -u --suffix=.tgz)
log "DEBUG" "Downloading to temporary file: $temp_tarball"
if ! wget -q "$source" -O "$temp_tarball"; then
log "ERROR" "Failed to download tarball from $source"
exit 1
fi
source="$temp_tarball"
log "DEBUG" "Using downloaded tarball: $source"
fi
if [ ! -f "$source" ]; then
log "ERROR" "Tarball not found: $source"
exit 1
fi
basename=$(basename "$source")
chart_name="${basename%.tgz}"
extract_dir="$output_dir/$chart_name"
mkdir -p "$extract_dir"
if ! tar -xzf "$source" -C "$extract_dir" --strip-components=1; then
log "ERROR" "Failed to extract tarball: $source"
exit 1
fi
echo "$extract_dir"
;;
"local")
if [ ! -d "$source" ]; then
log "ERROR" "Local chart directory not found: $source"
exit 1
fi
if [ ! -f "$source/Chart.yaml" ]; then
log "ERROR" "Not a valid Helm chart directory (missing Chart.yaml): $source"
exit 1
fi
basename=$(basename "$source")
target_dir="$output_dir/$basename"
mkdir -p "$target_dir"
cp -r "$source"/* "$target_dir"/
echo "$target_dir"
;;
"repo")
# Handle repo/chart:version format
chart_name=$(echo "$source" | cut -d':' -f1)
version=$(echo "$source" | cut -d':' -f2-)
version_flag=""
if [ "$chart_name" != "$version" ]; then
version_flag="--version $version"
fi
error_file=$(mktemp)
# Attempt to pull the chart and capture any errors
if ! helm pull "$chart_name" $version_flag --destination "$output_dir" 2> "$error_file"; then
error_msg=$(cat "$error_file")
log "ERROR" "Failed to pull chart $chart_name with version $version"
log "ERROR" "Helm error: $error_msg"
rm "$error_file"
exit 1
fi
rm "$error_file"
# Extract the pulled tarball
tarballs=$(find "$output_dir" -name "*.tgz")
if [ -z "$tarballs" ]; then
log "ERROR" "No tarball found after pulling $source"
exit 1
fi
for tarball in $tarballs; do
extract_dir="${tarball%.tgz}"
mkdir -p "$extract_dir"
tar -xzf "$tarball" -C "$extract_dir" --strip-components=1
rm "$tarball"
echo "$extract_dir"
return
done
;;
*)
log "ERROR" "Unknown source type for $source"
exit 1
;;
esac
}
# Function to render helm templates
render_templates() {
chart_dir=$1
output_dir=$2
release_name=$3
namespace=$4
values_file=$5
values_arg=""
if [ -n "$values_file" ] && [ -f "$values_file" ]; then
values_arg="--values $values_file"
fi
log "INFO" "Rendering templates for $(basename "$chart_dir")"
error_file=$(mktemp)
if ! helm template "$release_name" "$chart_dir" \
--namespace "$namespace" \
$values_arg \
--output-dir "$output_dir" 2> "$error_file"; then
error_msg=$(cat "$error_file")
log "ERROR" "Failed to render templates for $(basename "$chart_dir")"
log "ERROR" "Helm error: $error_msg"
rm "$error_file"
exit 1
fi
rm "$error_file"
}
# Function to compare two files or directories
compare_items() {
item1=$1
item2=$2
output_file=$3
title=$4
expanded=$5
diff_args=""
if [ "$expanded" = "true" ]; then
diff_args="--unified=999999"
else
diff_args="--unified=3"
fi
echo "=== Comparing $title ===" >> "$output_file"
if [ -f "$item1" ] && [ -f "$item2" ]; then
# For YAML files, normalize them first
case "$item1" in
*.yaml|*.yml)
temp_dir=$(mktemp -d)
log "DEBUG" "Created temporary directory for YAML normalization: $temp_dir"
yq eval -P '.' "$item1" > "$temp_dir/file1.yaml"
yq eval -P '.' "$item2" > "$temp_dir/file2.yaml"
diff $diff_args "$temp_dir/file1.yaml" "$temp_dir/file2.yaml" >> "$output_file" || true
rm -rf "$temp_dir"
;;
*)
diff $diff_args "$item1" "$item2" >> "$output_file" || true
;;
esac
elif [ -d "$item1" ] && [ -d "$item2" ]; then
diff $diff_args -r "$item1" "$item2" >> "$output_file" || true
else
echo "Items are of different types and cannot be compared directly." >> "$output_file"
fi
}
# Main script starts here
check_dependencies
# Initialize variables
CHART1=""
CHART2=""
NAMESPACE="default"
RELEASE_NAME="release"
VALUES_FILE=""
VALUES1_FILE=""
VALUES2_FILE=""
OUTPUT_DIR=""
EXPANDED="false"
DEBUG="${DEBUG:-false}"
# Parse command line arguments
if [ $# -lt 2 ]; then
usage
fi
CHART1=$1
CHART2=$2
shift 2
while [ $# -gt 0 ]; do
case $1 in
-n|--namespace)
NAMESPACE="$2"
shift 2
;;
-r|--release-name)
RELEASE_NAME="$2"
shift 2
;;
-v|--values)
VALUES_FILE="$2"
shift 2
;;
--values1)
VALUES1_FILE="$2"
shift 2
;;
--values2)
VALUES2_FILE="$2"
shift 2
;;
-o|--output-dir)
OUTPUT_DIR="$2"
shift 2
;;
-e|--expanded)
EXPANDED="true"
shift 1
;;
--debug)
DEBUG="true"
shift 1
;;
-h|--help)
usage
;;
*)
log "ERROR" "Unknown option: $1"
usage
;;
esac
done
# Create output directory if specified
if [ -z "$OUTPUT_DIR" ]; then
OUTPUT_DIR=$(mktemp -d)
log "INFO" "Using temporary directory: $OUTPUT_DIR"
else
mkdir -p "$OUTPUT_DIR"
fi
# Create temporary directories for processing
TEMP_DIR=$(mktemp -d)
CHART1_DIR="$TEMP_DIR/chart1"
CHART2_DIR="$TEMP_DIR/chart2"
RENDERED1_DIR="$TEMP_DIR/rendered1"
RENDERED2_DIR="$TEMP_DIR/rendered2"
mkdir -p "$CHART1_DIR" "$CHART2_DIR" "$RENDERED1_DIR" "$RENDERED2_DIR"
# Create comparison output file
COMPARISON_FILE="$OUTPUT_DIR/comparison.patch"
echo "Helm Chart Comparison Results" > "$COMPARISON_FILE"
echo "Chart 1: $CHART1" >> "$COMPARISON_FILE"
echo "Chart 2: $CHART2" >> "$COMPARISON_FILE"
echo "Generated on: $(date)" >> "$COMPARISON_FILE"
echo "------------------------------------------------" >> "$COMPARISON_FILE"
# Pull and extract charts
log "INFO" "Processing first chart: $CHART1"
EXTRACTED_CHART1=$(pull_chart "$CHART1" "$CHART1_DIR")
log "INFO" "Successfully extracted first chart to: $EXTRACTED_CHART1"
log "INFO" "Processing second chart: $CHART2"
EXTRACTED_CHART2=$(pull_chart "$CHART2" "$CHART2_DIR")
log "INFO" "Successfully extracted second chart to: $EXTRACTED_CHART2"
# Render templates
if [ -n "$VALUES1_FILE" ]; then
render_templates "$EXTRACTED_CHART1" "$RENDERED1_DIR" "$RELEASE_NAME" "$NAMESPACE" "$VALUES1_FILE"
elif [ -n "$VALUES_FILE" ]; then
render_templates "$EXTRACTED_CHART1" "$RENDERED1_DIR" "$RELEASE_NAME" "$NAMESPACE" "$VALUES_FILE"
else
render_templates "$EXTRACTED_CHART1" "$RENDERED1_DIR" "$RELEASE_NAME" "$NAMESPACE"
fi
if [ -n "$VALUES2_FILE" ]; then
render_templates "$EXTRACTED_CHART2" "$RENDERED2_DIR" "$RELEASE_NAME" "$NAMESPACE" "$VALUES2_FILE"
elif [ -n "$VALUES_FILE" ]; then
render_templates "$EXTRACTED_CHART2" "$RENDERED2_DIR" "$RELEASE_NAME" "$NAMESPACE" "$VALUES_FILE"
else
render_templates "$EXTRACTED_CHART2" "$RENDERED2_DIR" "$RELEASE_NAME" "$NAMESPACE"
fi
# Compare entire chart directories
compare_items "$EXTRACTED_CHART1" "$EXTRACTED_CHART2" "$COMPARISON_FILE" "complete chart directories" "$EXPANDED"
# Compare rendered templates
compare_items "$RENDERED1_DIR" "$RENDERED2_DIR" "$COMPARISON_FILE" "rendered templates" "$EXPANDED"
# Clean up
log "INFO" "Cleaning up temporary files"
rm -rf "$TEMP_DIR"
log "SUCCESS" "Comparison completed! Results saved to $COMPARISON_FILE"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment