Last active
March 2, 2019 17:01
-
-
Save sunjon/a3ac0c1513f3569a15e40f48718fbfbc to your computer and use it in GitHub Desktop.
Chef Recipe include/dependency checker
This file contains 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 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 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Screenshot