Last active
April 11, 2025 11:48
-
-
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)
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
#!/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