Skip to content

Instantly share code, notes, and snippets.

@sunjon
Last active March 2, 2019 17:01
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save sunjon/a3ac0c1513f3569a15e40f48718fbfbc to your computer and use it in GitHub Desktop.
Save sunjon/a3ac0c1513f3569a15e40f48718fbfbc to your computer and use it in GitHub Desktop.
Chef Recipe include/dependency checker
#!/usr/bin/env bash
# Senghan Bright
# Delta Projects
REPO_ROOT=$HOME/repos/chef-repo
COOKBOOK_DIRECTORIES=(cookbooks community_cookbooks forked_cookbooks)
COLOR_COOKBOOK=(131 165 151)
COLOR_DELIMITER=(215 153 32)
COLOR_DUPE_PARENTHESES=(250 73 51)
COLOR_DUPE_TEXT=(124 111 100)
INDENT_TAB_SIZE=4
check_recipe_includes() {
local _namespace="$1"
local _indent_level="$2"
# has the recipe already been checked?
# TODO: some debug output to indicate that we had a duplicate include
[[ ${INCLUDED_RECIPE_MAP[$_namespace]} -eq 1 ]] && return 1
local _cookbook_name _recipe_name _cookbook_path _recipe_path
_cookbook_name=$(cookbook_name_from_namespace "$_namespace")
_recipe_name=$(recipe_name_from_namespace "$_namespace")
_cookbook_path=$(get_cookbook_path "$_cookbook_name")
_recipe_path="${_cookbook_path}/recipes/${_recipe_name}.rb"
# process recipe if it's a valid file
if [[ -f $_recipe_path ]]; then
local _included_recipes
_included_recipes=($(awk '$1 ~ /^include_recipe/ {print $2}' "$_recipe_path"))
# TODO: we don't really need an associative array after all..
INCLUDED_RECIPE_MAP[$_namespace]=1
# return if we have no includes
[[ ${#_included_recipes[@]} -eq 0 ]] && return
# print branches for the included recipes that were found
local _index=1
local _output
for _recipe in "${_included_recipes[@]}"; do
_recipe=$(format_namespace "$_recipe")
_output=$_recipe
[[ _index -eq ${#_included_recipes[@]} ]] &&
_last_branch=1 ||
_last_branch=0
[[ $COLOR_MODE -eq 1 ]] && _output=$(color_namespace "$_output")
_output=$(create_indented_branch "$_output" "$_indent_level" $_last_branch)
BUFFERED_OUTPUT+=("$_output")
connect_branch_to_tree
# check this included recipe branch for further includes
check_recipe_includes "$_recipe" $((_indent_level + 1))
((_index++))
done
fi
}
check_cookbook() {
local _cookbook_name=$1
local _indent_level=$2
local _cookbook_dependencies _cookbook_path _output _last_branch
# return if the cookbook had already been processed
[[ ${DEPENDENCY_MAP[$_cookbook_name]} -eq 1 ]] && return 1
DEPENDENCY_MAP[$_cookbook_name]=1
_cookbook_path=$(get_cookbook_path "$_cookbook_name")
_cookbook_dependencies=($(get_dependencies_from_metadata "$_cookbook_path"))
local _index=1
local _dependency_name
for _dependency in "${_cookbook_dependencies[@]}"; do
# color duplicates
if [[ ${DEPENDENCY_MAP[$_dependency]} -eq 1 ]] && [[ $COLOR_MODE -eq 1 ]]; then
_dependency_name=$(color_string "$_dependency" COLOR_DUPE_TEXT)
_output=$(color_string '‹' COLOR_DUPE_PARENTHESES)
else
_dependency_name=$_dependency
_output=''
fi
[[ $COLOR_MODE -eq 1 ]] &&
_output="${_output}$(color_string "$_dependency_name" COLOR_COOKBOOK)" ||
_output="${_output}${_dependency_name}"
if [[ ${DEPENDENCY_MAP[$_dependency]} -eq 1 ]] && [[ $COLOR_MODE -eq 1 ]]; then
_output=${_output}$(color_string "›" COLOR_DUPE_PARENTHESES)
fi
[[ _index -eq ${#_cookbook_dependencies[@]} ]] &&
_last_branch=1 ||
_last_branch=0
_output=$(create_indented_branch "$_output" "$_indent_level" $_last_branch)
BUFFERED_OUTPUT+=("$_output")
connect_branch_to_tree
# check this cookbook dependency branch for further dependencies.
check_cookbook "$_dependency" $((_indent_level + 1))
((_index++))
done
}
get_dependencies_from_metadata() {
local _cookbook_path=$1
local _result=()
local _metafile="${_cookbook_path}/metadata.rb"
if [[ -f $_metafile ]]; then
_result=($(awk '$1 ~ /^depends/ {print $2}' "$_metafile"))
i=0
for _cookbook_name in "${_result[@]}"; do
_result[i]=$(cleanup_string "$_cookbook_name")
((i++))
done
echo "${_result[*]}"
fi
}
get_cookbook_path() {
local _cookbook_name=$1
for _dir in "${COOKBOOK_DIRECTORIES[@]}"; do
local _result
_result=$(find "$REPO_ROOT"/"$_dir" -maxdepth 1 -type d -name "$_cookbook_name")
if [[ -n "$_result" ]]; then
echo "$_result"
return 0
fi
done
return 1
}
# ensure the recipe is in the `cookbook::recipe` format
format_namespace() {
local _namespace
_namespace=$(cleanup_string "$1")
local _self_reference='#{cookbook_name}'
# check if 'cookbook::recipe' contains Chefs' `cookbook_name` reference
if [[ $_namespace =~ $_self_reference ]]; then
# remove the self_reference placeholder
_namespace=${_namespace#$_self_reference}
# ..and replace it with the current cookbook name
_namespace="${_cookbook_name}${_namespace}"
fi
# if recipe name is not specified, use default.
[[ $_namespace != *\:\:* ]] &&
echo "${_namespace}::default" ||
echo "$_namespace"
}
cleanup_string() {
local _str=$1
# remove single/double quotes and trailing commas
_str="${_str//\"/}"
_str="${_str//\'/}"
_str="${_str//,/}"
# echo return_value
echo "$_str"
}
cookbook_name_from_namespace() {
local _namespace=$1
echo "${_namespace%::*}" # remove suffix starting with "::"
}
recipe_name_from_namespace() {
local _namespace=$1
echo "${_namespace#*::}" # remove prefix ending in "::"
}
create_indented_branch() {
local _string="$1"
local _indent_level="$2"
local _last_branch="$3" # bool
local _branch_symbol _result
[[ $_last_branch -eq 0 ]] && _branch_symbol='├' || _branch_symbol='└'
# each indent level is 4 spaces
local _num_spaces=$((_indent_level * INDENT_TAB_SIZE))
local _line_length=$((INDENT_TAB_SIZE - 1))
# print the branch start at the current indent level
_result=$(printf "%${_num_spaces}s%s" "$_branch_symbol")
_result=$(printf "%s%${_line_length}s %s\n" "$_result" '──' "$_string")
echo "$_result"
}
connect_branch_to_tree() {
local _line _check_char
local _branch_symbol='│'
local _branch_start_column
_branch_start_column=$(( ( (_indent_level - 1) * INDENT_TAB_SIZE) + 1))
# connect the last branch in $BUFFERED_OUTPUT to the main tree
# check each character in the vertical column above the start of the branch
# if it is empty, replace it with a vertical branch line
for ((index = ${#BUFFERED_OUTPUT[@]} - 2; index >= 0; index--)); do
_line="${BUFFERED_OUTPUT[index]}"
_check_char=${_line:_branch_start_column:1}
if [[ $_check_char == ' ' ]]; then
# replace the character
BUFFERED_OUTPUT[index]="${_line:0:_branch_start_column}$_branch_symbol${_line:(($_branch_start_column + 1))}"
else
# stop after reaching another part of the tree
break
fi
done
}
color_string() {
local _string=$1
local -n _color=$2 # note: rgb array is passed as reference
printf "\x1b[38;2;%d;%d;%dm%s\x1b[0m\n" "${_color[0]}" "${_color[1]}" "${_color[2]}" "$_string"
}
color_namespace() {
local _namespace=$1
local _cookbook_name _recipe_name _delimiter
_cookbook_name=$(cookbook_name_from_namespace "$_namespace")
_recipe_name=$(recipe_name_from_namespace "$_namespace")
_cookbook_name=$(color_string "$_cookbook_name" COLOR_COOKBOOK)
_delimiter=$(color_string "::" COLOR_DELIMITER)
printf "%s%s%s" "${_cookbook_name}" "${_delimiter}" "${_recipe_name}"
}
# MAIN
PARAMS=""
COLOR_MODE=1
# parse the command line arguments
while (("$#")); do
case "$1" in
--nocolor)
COLOR_MODE=0
shift
;;
-* | --*=) # unsupported flags
echo "Error: Unsupported flag $1" >&2
exit 1
;;
*) # preserve positional arguments
PARAMS="$PARAMS $1"
shift
;;
esac
done
# set the arguments array to the list of positional arguments we saved
eval set -- "$PARAMS"
for recipe in "$@"; do
BUFFERED_OUTPUT=()
unset DEPENDENCY_MAP
typeset -A DEPENDENCY_MAP # an array of '['cookbook::name'][something]'
indent_level=1
namespace=$(format_namespace "$recipe")
# print the cookbook/recipe root
output=$namespace
[[ $COLOR_MODE -eq 1 ]] && output=$(color_namespace "$output")
printf "•%s\n" "$output"
# if a recipe was specified, print any `include_recipe`s
unset INCLUDED_RECIPE_MAP
typeset -A INCLUDED_RECIPE_MAP # an array of '['cookbook::name'][something]'
# enter the recursive cookbook 'include_recipe' search
BUFFERED_OUTPUT+=(" ┬")
check_recipe_includes "$namespace" $indent_level
# enter the recursive cookbook `depends` search
BUFFERED_OUTPUT+=(" ┬")
check_cookbook "$(cookbook_name_from_namespace $namespace)" $indent_level
# print the output
for line in "${BUFFERED_OUTPUT[@]}"; do
echo "$line"
done
done
exit 0
# TODO: can't resolve stuff like this:
# w(nfs::_common nfs::_idmap).each do |component|
# include_recipe component
# end
@sunjon
Copy link
Author

sunjon commented Mar 2, 2019

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment