diff --git a/configure b/configure index 69f6cec2..b57e1823 100755 --- a/configure +++ b/configure @@ -22,12 +22,10 @@ EOF main() { local verify= - parse_command_line "$@" - verify_binaries - if [[ "${verify}" == 1 ]]; then + verify_master_count verify_ansible_hosts verify_metallb verify_kubevip @@ -40,39 +38,30 @@ main() { # generate sops configuration file envsubst < "${PROJECT_DIR}/tmpl/.sops.yaml" \ > "${PROJECT_DIR}/.sops.yaml" - # generate cluster settings envsubst < "${PROJECT_DIR}/tmpl/kubernetes/flux/cluster-settings.yaml" \ > "${PROJECT_DIR}/kubernetes/flux/vars/cluster-settings.yaml" - envsubst < "${PROJECT_DIR}/tmpl/kubernetes/flux/cluster.yaml" \ > "${PROJECT_DIR}/kubernetes/flux/config/cluster.yaml" - # generate secrets envsubst < "${PROJECT_DIR}/tmpl/kubernetes/cluster-secrets.sops.yaml" \ > "${PROJECT_DIR}/kubernetes/flux/vars/cluster-secrets.sops.yaml" sops --encrypt --in-place "${PROJECT_DIR}/kubernetes/flux/vars/cluster-secrets.sops.yaml" - envsubst < "${PROJECT_DIR}/tmpl/kubernetes/cert-manager-secret.sops.yaml" \ > "${PROJECT_DIR}/kubernetes/apps/cert-manager/cert-manager/issuers/secret.sops.yaml" sops --encrypt --in-place "${PROJECT_DIR}/kubernetes/apps/cert-manager/cert-manager/issuers/secret.sops.yaml" - envsubst < "${PROJECT_DIR}/tmpl/kubernetes/cloudflare-ddns-secret.sops.yaml" \ > "${PROJECT_DIR}/kubernetes/apps/networking/cloudflare-ddns/app/secret.sops.yaml" sops --encrypt --in-place "${PROJECT_DIR}/kubernetes/apps/networking/cloudflare-ddns/app/secret.sops.yaml" - envsubst < "${PROJECT_DIR}/tmpl/kubernetes/external-dns-secret.sops.yaml" \ > "${PROJECT_DIR}/kubernetes/apps/networking/external-dns/app/secret.sops.yaml" sops --encrypt --in-place "${PROJECT_DIR}/kubernetes/apps/networking/external-dns/app/secret.sops.yaml" - envsubst < "${PROJECT_DIR}/tmpl/terraform/secret.sops.yaml" \ > "${PROJECT_DIR}/terraform/cloudflare/secret.sops.yaml" sops --encrypt --in-place "${PROJECT_DIR}/terraform/cloudflare/secret.sops.yaml" - # generate ansible settings envsubst < "${PROJECT_DIR}/tmpl/ansible/kube-vip.yml" \ > "${PROJECT_DIR}/ansible/inventory/group_vars/kubernetes/kube-vip.yml" - # generate ansible hosts file and secrets generate_ansible_hosts generate_ansible_host_secrets @@ -99,7 +88,6 @@ parse_command_line() { shift done - if [[ -z "$verify" ]]; then verify=0 fi @@ -107,7 +95,7 @@ parse_command_line() { _has_binary() { command -v "${1}" >/dev/null 2>&1 || { - _log "ERROR" "${1} is not installed or not found in \$PATH" + _log "ERROR(${FUNCNAME[0]})" "${1} is not installed or not found in \$PATH" exit 1 } } @@ -118,7 +106,7 @@ _has_optional_envar() { [[ "${!option}" == "" ]] && { _log "WARN" "Unset optional variable ${option}" } || { - _log "INFO" "Found variable '${option}' with value '${!option}'" + _log "INFO(${FUNCNAME[0]})" "Found variable '${option}' with value '${!option}'" } } @@ -126,22 +114,21 @@ _has_envar() { local option="${1}" # shellcheck disable=SC2015 [[ "${!option}" == "" ]] && { - _log "ERROR" "Unset variable ${option}" + _log "ERROR(${FUNCNAME[0]})" "Unset variable ${option}" exit 1 } || { - _log "INFO" "Found variable '${option}' with value '${!option}'" + _log "INFO(${FUNCNAME[0]})" "Found variable '${option}' with value '${!option}'" } } _has_valid_ip() { local ip="${1}" local variable_name="${2}" - if ! ipcalc "${ip}" | awk 'BEGIN{FS=":"; is_invalid=0} /^INVALID/ {is_invalid=1; print $1} END{exit is_invalid}' >/dev/null 2>&1; then - _log "INFO" "Variable '${variable_name}' has an invalid IP address '${ip}'" + _log "INFO(${FUNCNAME[0]})" "Variable '${variable_name}' has an invalid IP address '${ip}'" exit 1 else - _log "INFO" "Variable '${variable_name}' has a valid IP address '${ip}'" + _log "INFO(${FUNCNAME[0]})" "Variable '${variable_name}' has a valid IP address '${ip}'" fi } @@ -149,31 +136,27 @@ verify_addressing() { local found_kube_vip="false" local found_k8s_gateway="false" local found_ingress="false" - # Verify the metallb min and metallb ceiling are in the same network metallb_subnet_min=$(echo "${BOOTSTRAP_METALLB_LB_RANGE}" | cut -d- -f1 | cut -d. -f1,2,3) metallb_subnet_ceil=$(echo "${BOOTSTRAP_METALLB_LB_RANGE}" | cut -d- -f2 | cut -d. -f1,2,3) if [[ "${metallb_subnet_min}" != "${metallb_subnet_ceil}" ]]; then - _log "ERROR" "The provided MetalLB IP range '${BOOTSTRAP_METALLB_LB_RANGE}' is not in the same subnet" + _log "ERROR(${FUNCNAME[0]})" "The provided MetalLB IP range '${BOOTSTRAP_METALLB_LB_RANGE}' is not in the same subnet" exit 1 fi - # Verify the node IP addresses are on the same network as the metallb range for var in "${!BOOTSTRAP_ANSIBLE_HOST_ADDR_@}"; do node_subnet=$(echo "${!var}" | cut -d. -f1,2,3) if [[ "${node_subnet}" != "${metallb_subnet_min}" ]]; then - _log "ERROR" "The subnet for node '${!var}' is not in the same subnet as the provided metallb range '${BOOTSTRAP_METALLB_LB_RANGE}'" + _log "ERROR(${FUNCNAME[0]})" "The subnet for node '${!var}' is not in the same subnet as the provided metallb range '${BOOTSTRAP_METALLB_LB_RANGE}'" exit 1 fi done - # Verify the kube-vip IP is in the same network as the metallb range kubevip_subnet=$(echo "${BOOTSTRAP_KUBE_VIP_ADDR}" | cut -d. -f1,2,3) if [[ "${kubevip_subnet}" != "${metallb_subnet_min}" ]]; then - _log "ERROR" "The subnet for kupe-vip '${BOOTSTRAP_KUBE_VIP_ADDR}' is not the same subnet as the provided metallb range '${BOOTSTRAP_METALLB_LB_RANGE}'" + _log "ERROR(${FUNCNAME[0]})" "The subnet for kupe-vip '${BOOTSTRAP_KUBE_VIP_ADDR}' is not the same subnet as the provided metallb range '${BOOTSTRAP_METALLB_LB_RANGE}'" exit 1 fi - # Depending on the IP address, verify if it should be in the metallb range or not metallb_octet_min=$(echo "${BOOTSTRAP_METALLB_LB_RANGE}" | cut -d- -f1 | cut -d. -f4) metallb_octet_ceil=$(echo "${BOOTSTRAP_METALLB_LB_RANGE}" | cut -d- -f2 | cut -d. -f4) @@ -188,27 +171,23 @@ verify_addressing() { if [[ "${addr}" == "${BOOTSTRAP_METALLB_INGRESS_ADDR}" ]]; then found_ingress="true" fi - for var in "${!BOOTSTRAP_ANSIBLE_HOST_ADDR_@}"; do if [[ "${!var}" == "${addr}" ]]; then - _log "ERROR" "The IP for node '${!var}' should NOT be in the provided metallb range '${BOOTSTRAP_METALLB_LB_RANGE}'" + _log "ERROR(${FUNCNAME[0]})" "The IP for node '${!var}' should NOT be in the provided metallb range '${BOOTSTRAP_METALLB_LB_RANGE}'" exit 1 fi done done - if [[ "${found_kube_vip}" == "true" ]]; then - _log "ERROR" "The IP for kube-vip '${BOOTSTRAP_KUBE_VIP_ADDR}' should NOT be in the provided metallb range '${BOOTSTRAP_METALLB_LB_RANGE}'" + _log "ERROR(${FUNCNAME[0]})" "The IP for kube-vip '${BOOTSTRAP_KUBE_VIP_ADDR}' should NOT be in the provided metallb range '${BOOTSTRAP_METALLB_LB_RANGE}'" exit 1 fi - if [[ "${found_k8s_gateway}" == "false" ]]; then - _log "ERROR" "The IP for k8s_gateway '${BOOTSTRAP_METALLB_K8S_GATEWAY_ADDR}' should be in the provided metallb range '${BOOTSTRAP_METALLB_LB_RANGE}'" + _log "ERROR(${FUNCNAME[0]})" "The IP for k8s_gateway '${BOOTSTRAP_METALLB_K8S_GATEWAY_ADDR}' should be in the provided metallb range '${BOOTSTRAP_METALLB_LB_RANGE}'" exit 1 fi - if [[ "${found_ingress}" == "false" ]]; then - _log "ERROR" "The IP for ingress '${BOOTSTRAP_METALLB_INGRESS_ADDR}' should be in the provided metallb range '${BOOTSTRAP_METALLB_LB_RANGE}'" + _log "ERROR(${FUNCNAME[0]})" "The IP for ingress '${BOOTSTRAP_METALLB_INGRESS_ADDR}' should be in the provided metallb range '${BOOTSTRAP_METALLB_LB_RANGE}'" exit 1 fi } @@ -216,19 +195,17 @@ verify_addressing() { verify_age() { _has_envar "BOOTSTRAP_AGE_PUBLIC_KEY" _has_envar "SOPS_AGE_KEY_FILE" - if [[ ! "$BOOTSTRAP_AGE_PUBLIC_KEY" =~ ^age.* ]]; then - _log "ERROR" "BOOTSTRAP_AGE_PUBLIC_KEY does not start with age" + _log "ERROR(${FUNCNAME[0]})" "BOOTSTRAP_AGE_PUBLIC_KEY does not start with age" exit 1 else - _log "INFO" "Age public key is in the correct format" + _log "INFO(${FUNCNAME[0]})" "Age public key is in the correct format" fi - if [[ ! -f ~/.config/sops/age/keys.txt ]]; then - _log "ERROR" "Unable to find Age file keys.txt in ~/.config/sops/age" + _log "ERROR(${FUNCNAME[0]})" "Unable to find Age file keys.txt in ~/.config/sops/age" exit 1 else - _log "INFO" "Found Age public key '${BOOTSTRAP_AGE_PUBLIC_KEY}'" + _log "INFO(${FUNCNAME[0]})" "Found Age public key '${BOOTSTRAP_AGE_PUBLIC_KEY}'" fi } @@ -248,14 +225,12 @@ verify_binaries() { _has_binary "task" _has_binary "terraform" _has_binary "tflint" - if ! [[ "$(sops --version)" =~ 3\.[0-9]+\.[0-9]+ ]]; then - _log "ERROR" "Incompatible sops version, make sure you are using the latest release of github.com/mozilla/sops" + _log "ERROR(${FUNCNAME[0]})" "Incompatible sops version, make sure you are using the latest release of github.com/mozilla/sops" exit 1 fi - if ! [[ "$(yq --version)" =~ 4\.[0-9]+\.[0-9]+ ]]; then - _log "ERROR" "Incompatible yq version, make sure you are using the latest release of github.com/mikefarah/yq" + _log "ERROR(${FUNCNAME[0]})" "Incompatible yq version, make sure you are using the latest release of github.com/mikefarah/yq" exit 1 fi } @@ -266,91 +241,62 @@ verify_kubevip() { } verify_metallb() { - local ip_floor= - local ip_ceil= _has_envar "BOOTSTRAP_METALLB_LB_RANGE" _has_envar "BOOTSTRAP_METALLB_K8S_GATEWAY_ADDR" _has_envar "BOOTSTRAP_METALLB_INGRESS_ADDR" - - ip_floor=$(echo "${BOOTSTRAP_METALLB_LB_RANGE}" | cut -d- -f1) - ip_ceil=$(echo "${BOOTSTRAP_METALLB_LB_RANGE}" | cut -d- -f2) - - _has_valid_ip "${ip_floor}" "BOOTSTRAP_METALLB_LB_RANGE" - _has_valid_ip "${ip_ceil}" "BOOTSTRAP_METALLB_LB_RANGE" + _has_valid_ip "$(echo "${BOOTSTRAP_METALLB_LB_RANGE}" | cut -d- -f1)" "BOOTSTRAP_METALLB_LB_RANGE" + _has_valid_ip "$(echo "${BOOTSTRAP_METALLB_LB_RANGE}" | cut -d- -f2)" "BOOTSTRAP_METALLB_LB_RANGE" _has_valid_ip "${BOOTSTRAP_METALLB_K8S_GATEWAY_ADDR}" "BOOTSTRAP_METALLB_K8S_GATEWAY_ADDR" _has_valid_ip "${BOOTSTRAP_METALLB_INGRESS_ADDR}" "BOOTSTRAP_METALLB_INGRESS_ADDR" } verify_git_repository() { _has_envar "BOOTSTRAP_GIT_REPOSITORY" - export GIT_TERMINAL_PROMPT=0 pushd "$(mktemp -d)" >/dev/null 2>&1 [ "$(git ls-remote "${BOOTSTRAP_GIT_REPOSITORY}" 2> /dev/null)" ] || { - _log "ERROR" "Unable to find the remote Git repository '${BOOTSTRAP_GIT_REPOSITORY}'" + _log "ERROR(${FUNCNAME[0]})" "Unable to find the remote Git repository '${BOOTSTRAP_GIT_REPOSITORY}'" exit 1 } popd >/dev/null 2>&1 export GIT_TERMINAL_PROMPT=1 } -setup_github_webhook() { - _has_envar "BOOTSTRAP_FLUX_GITHUB_WEBHOOK_SECRET" - - WEBHOOK_SECRET="${BOOTSTRAP_FLUX_GITHUB_WEBHOOK_SECRET}" - - if [[ "${WEBHOOK_SECRET}" == "generated" ]]; then - WEBHOOK_SECRET="$(openssl rand -base64 30)" +verify_master_count() { + local master_node_count= + master_node_count=0 + for var in "${!BOOTSTRAP_ANSIBLE_HOST_ADDR_@}"; do + node_id=$(echo "${var}" | awk -F"_" '{print $5}') + node_control="BOOTSTRAP_ANSIBLE_CONTROL_NODE_${node_id}" + if [[ "${!node_control}" == "true" ]]; then + master_node_count=$((master_node_count+1)) + fi + done + # Check for existance of master nodes and that their count is not an even number + if [[ ${master_node_count} -eq 0 || $((master_node_count%2)) -eq 0 ]]; then + _log "ERROR(${FUNCNAME[0]})" "You must have 1, 3 or an odd number of master nodes >=3 inorder for etcd to have quorum" + exit 1 fi - - export BOOTSTRAP_FLUX_GITHUB_WEBHOOK_SECRET="${WEBHOOK_SECRET}" - _log "INFO" "Using GitHub Token '${WEBHOOK_SECRET}' for Flux" - - envsubst < "${PROJECT_DIR}/tmpl/kubernetes/github-webhook-token-secret.sops.yaml" \ - > "${PROJECT_DIR}/kubernetes/apps/flux-system/addons/webhooks/github/secret.sops.yaml" - sops --encrypt --in-place "${PROJECT_DIR}/kubernetes/apps/flux-system/addons/webhooks/github/secret.sops.yaml" -} - -setup_weave_gitops() { - _has_envar "BOOTSTRAP_WEAVE_GITOPS_ADMIN_PASSWORD" - - WEAVE_GITOPS_ADMIN_PASSWORD="${BOOTSTRAP_WEAVE_GITOPS_ADMIN_PASSWORD}" - - if [[ "${WEAVE_GITOPS_ADMIN_PASSWORD}" == "generated" ]]; then - WEAVE_GITOPS_ADMIN_PASSWORD="$(openssl rand -base64 30)" - fi - - export BOOTSTRAP_WEAVE_GITOPS_ADMIN_PASSWORD="${WEAVE_GITOPS_ADMIN_PASSWORD}" - _log "INFO" "Using admin password '${WEAVE_GITOPS_ADMIN_PASSWORD}' for Weave Gitops" - - # Convert password to bcrypt hash - export BOOTSTRAP_WEAVE_GITOPS_ADMIN_PASSWORD="$(echo -n "${BOOTSTRAP_WEAVE_GITOPS_ADMIN_PASSWORD}" | gitops get bcrypt-hash)" - - envsubst < "${PROJECT_DIR}/tmpl/kubernetes/weave-gitops-secret.sops.yaml" \ - > "${PROJECT_DIR}/kubernetes/apps/flux-system/weave-gitops/app/secret.sops.yaml" - sops --encrypt --in-place "${PROJECT_DIR}/kubernetes/apps/flux-system/weave-gitops/app/secret.sops.yaml" + _log "INFO(${FUNCNAME[0]})" "Verified control plane node count" } verify_cloudflare() { local account_zone= local errors= - _has_envar "BOOTSTRAP_CLOUDFLARE_APIKEY" _has_envar "BOOTSTRAP_CLOUDFLARE_DOMAIN" _has_envar "BOOTSTRAP_CLOUDFLARE_EMAIL" - # Try to retrieve zone information from Cloudflare's API account_zone=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones?name=${BOOTSTRAP_CLOUDFLARE_DOMAIN}&status=active" \ -H "X-Auth-Email: ${BOOTSTRAP_CLOUDFLARE_EMAIL}" \ -H "X-Auth-Key: ${BOOTSTRAP_CLOUDFLARE_APIKEY}" \ -H "Content-Type: application/json" ) - if [[ "$(echo "${account_zone}" | jq ".success")" == "true" ]]; then - _log "INFO" "Verified Cloudflare Account and Zone information" + _log "INFO(${FUNCNAME[0]})" "Verified Cloudflare Account and Zone information" else errors=$(echo "${account_zone}" | jq -c ".errors") - _log "ERROR" "Unable to get Cloudflare Account and Zone information ${errors}" + _log "ERROR(${FUNCNAME[0]})" "Unable to get Cloudflare Account and Zone information ${errors}" exit 1 fi } @@ -364,12 +310,10 @@ verify_ansible_hosts() { local node_hostname= local default_control_node_prefix= local default_worker_node_prefix= - default_control_node_prefix="BOOTSTRAP_ANSIBLE_DEFAULT_CONTROL_NODE_HOSTNAME_PREFIX" default_worker_node_prefix="BOOTSTRAP_ANSIBLE_DEFAULT_NODE_HOSTNAME_PREFIX" _has_optional_envar "${default_control_node_prefix}" _has_optional_envar "${default_worker_node_prefix}" - for var in "${!BOOTSTRAP_ANSIBLE_HOST_ADDR_@}"; do node_id=$(echo "${var}" | awk -F"_" '{print $5}') node_addr="BOOTSTRAP_ANSIBLE_HOST_ADDR_${node_id}" @@ -382,34 +326,30 @@ verify_ansible_hosts() { _has_envar "${node_password}" _has_envar "${node_control}" _has_optional_envar "${node_hostname}" - if [[ "${!node_addr}" == "${BOOTSTRAP_KUBE_VIP_ADDR}" && "${BOOTSTRAP_KUBE_VIP_ENABLED}" == "true" ]]; then - _log "ERROR" "The kube-vip IP '${BOOTSTRAP_KUBE_VIP_ADDR}' should not be the same as the IP for node '${!node_addr}'" + _log "ERROR(${FUNCNAME[0]})" "The kube-vip IP '${BOOTSTRAP_KUBE_VIP_ADDR}' should not be the same as the IP for node '${!node_addr}'" exit 1 fi - if [[ "${!node_addr}" == "${BOOTSTRAP_METALLB_K8S_GATEWAY_ADDR}" ]]; then - _log "ERROR" "The k8s-gateway load balancer IP '${BOOTSTRAP_METALLB_K8S_GATEWAY_ADDR}' should not be the same as the IP for node '${!node_addr}'" + _log "ERROR(${FUNCNAME[0]})" "The k8s-gateway load balancer IP '${BOOTSTRAP_METALLB_K8S_GATEWAY_ADDR}' should not be the same as the IP for node '${!node_addr}'" exit 1 fi - if [[ "${!node_addr}" == "${BOOTSTRAP_METALLB_INGRESS_ADDR}" ]]; then - _log "ERROR" "The ingress load balancer IP '${BOOTSTRAP_METALLB_INGRESS_ADDR}' should not be the same as the IP for node '${!node_addr}'" + _log "ERROR(${FUNCNAME[0]})" "The ingress load balancer IP '${BOOTSTRAP_METALLB_INGRESS_ADDR}' should not be the same as the IP for node '${!node_addr}'" exit 1 fi - if ssh -q -o BatchMode=yes -o ConnectTimeout=5 "${!node_username}"@"${!var}" "true"; then - _log "INFO" "SSH into host '${!var}' with username '${!node_username}' was successfull" + _log "INFO(${FUNCNAME[0]})" "SSH into host '${!var}' with username '${!node_username}' was successfull" else - _log "ERROR" "SSH into host '${!var}' with username '${!node_username}'was NOT successful, did you copy over your SSH key?" + _log "ERROR(${FUNCNAME[0]})" "SSH into host '${!var}' with username '${!node_username}'was NOT successful, did you copy over your SSH key?" exit 1 fi done } verify_success() { - _log "INFO" "All checks passed!" - _log "INFO" "Run the script without --verify to template all the files out" + _log "INFO(${FUNCNAME[0]})" "All checks passed!" + _log "INFO(${FUNCNAME[0]})" "Run the script without --verify to template all the files out" exit 0 } @@ -504,8 +444,37 @@ generate_ansible_hosts() { } > "${PROJECT_DIR}/ansible/inventory/hosts.yml" } +setup_github_webhook() { + _has_envar "BOOTSTRAP_FLUX_GITHUB_WEBHOOK_SECRET" + WEBHOOK_SECRET="${BOOTSTRAP_FLUX_GITHUB_WEBHOOK_SECRET}" + if [[ "${WEBHOOK_SECRET}" == "generated" ]]; then + WEBHOOK_SECRET="$(openssl rand -base64 30)" + fi + export BOOTSTRAP_FLUX_GITHUB_WEBHOOK_SECRET="${WEBHOOK_SECRET}" + _log "INFO(${FUNCNAME[0]})" "Using GitHub Token '${WEBHOOK_SECRET}' for Flux" + envsubst < "${PROJECT_DIR}/tmpl/kubernetes/github-webhook-token-secret.sops.yaml" \ + > "${PROJECT_DIR}/kubernetes/apps/flux-system/addons/webhooks/github/secret.sops.yaml" + sops --encrypt --in-place "${PROJECT_DIR}/kubernetes/apps/flux-system/addons/webhooks/github/secret.sops.yaml" +} + +setup_weave_gitops() { + _has_envar "BOOTSTRAP_WEAVE_GITOPS_ADMIN_PASSWORD" + WEAVE_GITOPS_ADMIN_PASSWORD="${BOOTSTRAP_WEAVE_GITOPS_ADMIN_PASSWORD}" + if [[ "${WEAVE_GITOPS_ADMIN_PASSWORD}" == "generated" ]]; then + WEAVE_GITOPS_ADMIN_PASSWORD="$(openssl rand -base64 30)" + fi + export BOOTSTRAP_WEAVE_GITOPS_ADMIN_PASSWORD="${WEAVE_GITOPS_ADMIN_PASSWORD}" + _log "INFO(${FUNCNAME[0]})" "Using admin password '${WEAVE_GITOPS_ADMIN_PASSWORD}' for Weave Gitops" + # Convert password to bcrypt hash + # shellcheck disable=SC2155 + export BOOTSTRAP_WEAVE_GITOPS_ADMIN_PASSWORD="$(echo -n "${BOOTSTRAP_WEAVE_GITOPS_ADMIN_PASSWORD}" | gitops get bcrypt-hash)" + envsubst < "${PROJECT_DIR}/tmpl/kubernetes/weave-gitops-secret.sops.yaml" \ + > "${PROJECT_DIR}/kubernetes/apps/flux-system/weave-gitops/app/secret.sops.yaml" + sops --encrypt --in-place "${PROJECT_DIR}/kubernetes/apps/flux-system/weave-gitops/app/secret.sops.yaml" +} + success() { - _log "INFO" "All files have been templated, proceed to the next steps outlined in the README" + _log "INFO(${FUNCNAME[0]})" "All files have been templated, proceed to the next steps outlined in the README" exit 0 }