mirror of
https://github.com/alexbelgium/hassio-addons.git
synced 2026-01-09 17:31:03 +01:00
Refactor global variable script for better handling
Refactor global variable script to improve readability and maintainability. Added support for handling secrets and environment variables more effectively.
This commit is contained in:
@@ -1,163 +1,290 @@
|
||||
#!/usr/bin/with-contenv bashio
|
||||
# shellcheck shell=bash
|
||||
|
||||
set -e
|
||||
|
||||
if ! bashio::supervisor.ping 2> /dev/null; then
|
||||
if ! bashio::supervisor.ping 2>/dev/null; then
|
||||
echo "..."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
###################################
|
||||
# Export all addon options as env #
|
||||
###################################
|
||||
|
||||
echo ""
|
||||
bashio::log.notice "This script converts all addon options to environment variables. Custom variables can be set using env_vars."
|
||||
bashio::log.notice "Additional informations : https://github.com/alexbelgium/hassio-addons/wiki/Add-Environment-variables-to-your-Addon-2"
|
||||
echo ""
|
||||
|
||||
# For all keys in options.json
|
||||
JSONSOURCE="/data/options.json"
|
||||
|
||||
# Define secrets location
|
||||
if [ -f /homeassistant/secrets.yaml ]; then
|
||||
# Define secrets location (optional)
|
||||
SECRETSOURCE=""
|
||||
if [[ -f /homeassistant/secrets.yaml ]]; then
|
||||
SECRETSOURCE="/homeassistant/secrets.yaml"
|
||||
elif [ -f /config/secrets.yaml ]; then
|
||||
elif [[ -f /config/secrets.yaml ]]; then
|
||||
SECRETSOURCE="/config/secrets.yaml"
|
||||
else
|
||||
SECRETSOURCE="false"
|
||||
fi
|
||||
|
||||
# Export keys as env variables
|
||||
# echo "All addon options were exported as variables"
|
||||
mapfile -t arr < <(jq -r 'keys[]' "${JSONSOURCE}")
|
||||
# Injection block markers (single block, idempotent)
|
||||
BLOCK_BEGIN="# --- BEGIN ADDON ENV (generated) ---"
|
||||
BLOCK_END="# --- END ADDON ENV (generated) ---"
|
||||
|
||||
# Escape special characters using printf and enclose in double quotes
|
||||
sanitize_variable() {
|
||||
local raw="$1"
|
||||
local escaped
|
||||
if [[ "$raw" == \[* ]]; then
|
||||
echo "One of your options is an array, skipping"
|
||||
return
|
||||
EXPORT_BLOCK_FILE="$(mktemp)"
|
||||
trap 'rm -f "$EXPORT_BLOCK_FILE"' EXIT
|
||||
|
||||
{
|
||||
echo "${BLOCK_BEGIN}"
|
||||
echo "# Do not edit: generated from ${JSONSOURCE}"
|
||||
echo "${BLOCK_END}"
|
||||
} > "${EXPORT_BLOCK_FILE}"
|
||||
|
||||
# Protected variables that should not be overwritten
|
||||
declare -A PROTECTED_VARS=(
|
||||
["PATH"]=1
|
||||
["HOME"]=1
|
||||
["PWD"]=1
|
||||
["SHLVL"]=1
|
||||
["_"]=1
|
||||
["S6_BEHAVIOR_IF_STAGE2_FAILS"]=1
|
||||
)
|
||||
|
||||
is_valid_env_name() {
|
||||
[[ "$1" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]]
|
||||
}
|
||||
|
||||
# Quote for shell *code* (for injection into scripts). Keep punctuation intact.
|
||||
# - single-line => single quotes
|
||||
# - multi-line => $'...\n...' (one physical line; safe for injection)
|
||||
shell_quote_for_code() {
|
||||
local s="$1"
|
||||
|
||||
if [[ "$s" == *$'\n'* || "$s" == *$'\r'* || "$s" == *$'\t'* ]]; then
|
||||
s="${s//\\/\\\\}"
|
||||
s="${s//\'/\\\'}"
|
||||
s="${s//$'\n'/\\n}"
|
||||
s="${s//$'\r'/\\r}"
|
||||
s="${s//$'\t'/\\t}"
|
||||
printf "\$'%s'" "$s"
|
||||
return 0
|
||||
fi
|
||||
printf -v escaped '%q' "$raw"
|
||||
# Do not espace spaces
|
||||
escaped="${escaped//\\ / }"
|
||||
if [[ "$raw" == "$escaped" ]]; then
|
||||
printf '%s' "$raw"
|
||||
else
|
||||
printf '%s' "$escaped"
|
||||
|
||||
# single-quote with embedded '"'"' for literal '
|
||||
s="${s//\'/\'\"\'\"\' }"
|
||||
s="${s% }"
|
||||
printf "'%s'" "$s"
|
||||
}
|
||||
|
||||
dotenv_quote() {
|
||||
# For /.env and /etc/environment: double quotes + minimal escaping
|
||||
local v="$1"
|
||||
v="${v//\\/\\\\}"
|
||||
v="${v//\"/\\\"}"
|
||||
v="${v//$'\n'/\\n}"
|
||||
v="${v//$'\r'/\\r}"
|
||||
printf '"%s"' "$v"
|
||||
}
|
||||
|
||||
resolve_secret_if_needed() {
|
||||
local v="$1"
|
||||
local name line
|
||||
|
||||
if [[ "$v" =~ ^[[:space:]]*\!secret[[:space:]]+(.+)[[:space:]]*$ ]]; then
|
||||
name="${BASH_REMATCH[1]}"
|
||||
name="${name#\"}"; name="${name%\"}"
|
||||
name="${name#\'}"; name="${name%\'}"
|
||||
|
||||
if [[ -z "${SECRETSOURCE}" ]]; then
|
||||
bashio::log.warning "Homeassistant config not mounted, secrets are not supported"
|
||||
printf '%s' "$v"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Exact key match at start of line; ignore comments
|
||||
line="$(
|
||||
awk -v k="$name" '
|
||||
/^[[:space:]]*#/ {next}
|
||||
$0 ~ "^[[:space:]]*" k ":[[:space:]]*" {
|
||||
sub("^[[:space:]]*" k ":[[:space:]]*", "", $0)
|
||||
print
|
||||
exit
|
||||
}
|
||||
' "$SECRETSOURCE"
|
||||
)"
|
||||
|
||||
[[ -z "$line" ]] && bashio::exit.nok "Secret '${name}' not found in ${SECRETSOURCE}"
|
||||
printf '%s' "$line"
|
||||
return 0
|
||||
fi
|
||||
|
||||
printf '%s' "$v"
|
||||
}
|
||||
|
||||
append_export_line_for_injection() {
|
||||
local key="$1"
|
||||
local value="$2"
|
||||
local quoted
|
||||
quoted="$(shell_quote_for_code "$value")"
|
||||
|
||||
awk -v k="$key" -v q="$quoted" -v end="$BLOCK_END" '
|
||||
$0 == end { print "export " k "=" q }
|
||||
{ print }
|
||||
' "${EXPORT_BLOCK_FILE}" > "${EXPORT_BLOCK_FILE}.tmp"
|
||||
mv -f "${EXPORT_BLOCK_FILE}.tmp" "${EXPORT_BLOCK_FILE}"
|
||||
}
|
||||
|
||||
is_shell_run_script() {
|
||||
local f="$1"
|
||||
local h
|
||||
h="$(head -n 1 "$f" 2>/dev/null || true)"
|
||||
|
||||
[[ "$h" =~ ^#! ]] || return 1
|
||||
[[ "$h" =~ (sh|bash|with-contenv) ]] && return 0
|
||||
return 1
|
||||
}
|
||||
|
||||
inject_block_into_file() {
|
||||
local file="$1"
|
||||
local tmp
|
||||
tmp="$(mktemp)"
|
||||
|
||||
awk -v bfile="${EXPORT_BLOCK_FILE}" -v begin="${BLOCK_BEGIN}" -v end="${BLOCK_END}" '
|
||||
function print_block() {
|
||||
while ((getline l < bfile) > 0) print l
|
||||
close(bfile)
|
||||
}
|
||||
BEGIN { inblock=0; printed=0 }
|
||||
{
|
||||
if ($0 == begin) {
|
||||
inblock=1
|
||||
if (!printed) { print_block(); printed=1 }
|
||||
next
|
||||
}
|
||||
if ($0 == end) { inblock=0; next }
|
||||
if (inblock) next
|
||||
|
||||
if (NR == 1) {
|
||||
if ($0 ~ /^#!/) {
|
||||
print $0
|
||||
if (!printed) { print_block(); printed=1 }
|
||||
next
|
||||
} else {
|
||||
if (!printed) { print_block(); printed=1 }
|
||||
print $0
|
||||
next
|
||||
}
|
||||
}
|
||||
print $0
|
||||
}
|
||||
END {
|
||||
if (!printed) print_block()
|
||||
}
|
||||
' "$file" > "$tmp"
|
||||
|
||||
cat "$tmp" > "$file"
|
||||
rm -f "$tmp"
|
||||
}
|
||||
|
||||
update_scripts_with_block() {
|
||||
local f
|
||||
local -A seen=()
|
||||
|
||||
shopt -s nullglob
|
||||
|
||||
# Added /etc/s6-overlay/s6-rc.d/*/run for newer S6 implementation (optional)
|
||||
for f in /etc/services.d/*/run /etc/services.d/*/*run* /etc/cont-init.d/*.sh /etc/s6-overlay/s6-rc.d/*/run; do
|
||||
[[ -f "$f" ]] || continue
|
||||
[[ -n "${seen[$f]:-}" ]] && continue
|
||||
seen["$f"]=1
|
||||
|
||||
if ! is_shell_run_script "$f"; then
|
||||
bashio::log.debug "Skipping non-shell script: $f"
|
||||
continue
|
||||
fi
|
||||
|
||||
inject_block_into_file "$f"
|
||||
done
|
||||
|
||||
shopt -u nullglob
|
||||
}
|
||||
|
||||
export_option() {
|
||||
local key="$1"
|
||||
local value="$2"
|
||||
local line secret secretnum valuepy
|
||||
|
||||
value=$(sanitize_variable "$value")
|
||||
|
||||
if [[ -z "$value" ]]; then
|
||||
line="${key}=''"
|
||||
else
|
||||
line="${key}='${value//\'/\'\\\'\'}'"
|
||||
if [[ -n "${PROTECTED_VARS[$key]:-}" ]]; then
|
||||
bashio::log.warning "Skipping protected environment variable: ${key}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ "${line}" == *"!secret "* ]]; then
|
||||
echo "secret detected"
|
||||
secret=${line#*secret }
|
||||
secret="${secret%[\"\']}"
|
||||
if [[ "$SECRETSOURCE" == "false" ]]; then
|
||||
bashio::log.warning "Homeassistant config not mounted, secrets are not supported"
|
||||
return
|
||||
fi
|
||||
secretnum=$(sed -n "/$secret:/=" "$SECRETSOURCE")
|
||||
[[ "$secretnum" == *' '* ]] && bashio::exit.nok "There are multiple matches for your password name. Please check your secrets.yaml file"
|
||||
secret=$(sed -n "/$secret:/p" "$SECRETSOURCE")
|
||||
secret=${secret#*: }
|
||||
line="${line%%=*}='$secret'"
|
||||
value="$secret"
|
||||
if ! is_valid_env_name "$key"; then
|
||||
bashio::log.warning "Skipping invalid env var name: ${key}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if bashio::config.false "verbose" || [[ "${key,,}" == *"pass"* ]]; then
|
||||
value="$(resolve_secret_if_needed "$value")"
|
||||
|
||||
if bashio::config.false "verbose" || [[ "${key,,}" =~ (pass|secret|token|apikey|api_key|private|pwd) ]]; then
|
||||
bashio::log.blue "${key}=******"
|
||||
else
|
||||
bashio::log.blue "$line"
|
||||
bashio::log.blue "${key}=${value}"
|
||||
fi
|
||||
|
||||
export "$line"
|
||||
export "${key}=${value}"
|
||||
|
||||
if command -v "python3" &> /dev/null; then
|
||||
[ ! -f /env.py ] && echo "import os" > /env.py
|
||||
valuepy="${value//\\/\\\\}"
|
||||
valuepy="${valuepy//[\"\']/}"
|
||||
echo "os.environ['${key}'] = '$valuepy'" >> /env.py
|
||||
python3 /env.py
|
||||
if [[ -d /var/run/s6/container_environment ]]; then
|
||||
printf '%s' "${value}" > "/var/run/s6/container_environment/${key}"
|
||||
fi
|
||||
|
||||
echo "$line" >> /.env || true
|
||||
echo "${key}=$(dotenv_quote "$value")" >> "/.env" 2>/dev/null || true
|
||||
mkdir -p /etc
|
||||
echo "$line" >> /etc/environment
|
||||
if cat /etc/services.d/*/*run* &> /dev/null; then sed -i "1a export $line" /etc/services.d/*/*run* 2> /dev/null; fi
|
||||
if cat /etc/cont-init.d/*.sh &> /dev/null; then sed -i "1a export $line" /etc/cont-init.d/*.sh 2> /dev/null; fi
|
||||
if [ -d /var/run/s6/container_environment ]; then printf "%s" "${value}" > /var/run/s6/container_environment/"${key}"; fi
|
||||
echo "export ${key}='${value}'" >> ~/.bashrc
|
||||
echo "${key}=$(dotenv_quote "$value")" >> /etc/environment 2>/dev/null || true
|
||||
|
||||
append_export_line_for_injection "$key" "$value"
|
||||
}
|
||||
|
||||
mapfile -t arr < <(jq -r 'keys[]' "${JSONSOURCE}")
|
||||
|
||||
for KEYS in "${arr[@]}"; do
|
||||
# export key
|
||||
VALUE=$(jq -r --raw-output ".\"$KEYS\"" "$JSONSOURCE")
|
||||
# Check if the value is an array
|
||||
if [[ "$VALUE" == \[* ]]; then
|
||||
jtype="$(jq -r --arg k "$KEYS" '.[$k] | type' "$JSONSOURCE")"
|
||||
|
||||
if [[ "$jtype" == "array" ]]; then
|
||||
if [[ "$KEYS" == "env_vars" ]]; then
|
||||
mapfile -t env_entries < <(jq -c ".\"$KEYS\"[]" "$JSONSOURCE")
|
||||
if [[ "${#env_entries[@]}" -eq 0 ]]; then
|
||||
continue
|
||||
fi
|
||||
env_processed=false
|
||||
mapfile -t env_entries < <(jq -c '.env_vars[]?' "$JSONSOURCE")
|
||||
for entry in "${env_entries[@]}"; do
|
||||
if [[ "$entry" == \{* ]]; then
|
||||
env_name=$(jq -r 'if has("name") and has("value") then .name else empty end' <<< "$entry")
|
||||
env_name="$(jq -r 'if has("name") and has("value") then .name else empty end' <<<"$entry")"
|
||||
if [[ -n "$env_name" ]]; then
|
||||
env_value=$(jq -r '.value // empty' <<< "$entry")
|
||||
env_value="$(jq -r '.value // "" | tostring' <<<"$entry")"
|
||||
export_option "$env_name" "$env_value"
|
||||
env_processed=true
|
||||
continue
|
||||
else
|
||||
mapfile -t env_keys < <(jq -r 'keys[]' <<<"$entry")
|
||||
for env_key in "${env_keys[@]}"; do
|
||||
env_value="$(jq -r --arg k "$env_key" '.[$k] // "" | tostring' <<<"$entry")"
|
||||
export_option "$env_key" "$env_value"
|
||||
done
|
||||
fi
|
||||
|
||||
# Preserve multiline values: iterate keys and extract raw values without @tsv
|
||||
mapfile -t env_keys < <(jq -r 'keys[]' <<< "$entry")
|
||||
for env_key in "${env_keys[@]}"; do
|
||||
# Use --arg to select the key; // empty to avoid "null"
|
||||
env_value=$(jq -r --arg k "$env_key" '.[$k] // empty' <<< "$entry")
|
||||
export_option "$env_key" "$env_value"
|
||||
env_processed=true
|
||||
done
|
||||
elif [[ "${entry:0:1}" == '"' ]]; then
|
||||
env_pair=$(jq -r '.' <<< "$entry")
|
||||
else
|
||||
env_pair="$(jq -r '.' <<<"$entry")"
|
||||
if [[ "$env_pair" == *=* ]]; then
|
||||
env_key=${env_pair%%=*}
|
||||
env_value=${env_pair#*=}
|
||||
export_option "$env_key" "$env_value"
|
||||
env_processed=true
|
||||
export_option "${env_pair%%=*}" "${env_pair#*=}"
|
||||
else
|
||||
bashio::log.warning "env_vars entry '$env_pair' is not in KEY=VALUE format, skipping"
|
||||
fi
|
||||
else
|
||||
bashio::log.warning "env_vars entry format not supported, skipping"
|
||||
fi
|
||||
done
|
||||
if [[ "$env_processed" == false ]]; then
|
||||
bashio::log.warning "env_vars option format not supported, skipping"
|
||||
fi
|
||||
else
|
||||
bashio::log.warning "One of your option is an array, skipping"
|
||||
bashio::log.warning "Option '${KEYS}' is an array, skipping"
|
||||
fi
|
||||
elif [[ "$jtype" == "object" ]]; then
|
||||
bashio::log.warning "Option '${KEYS}' is an object, skipping"
|
||||
elif [[ "$jtype" == "null" ]]; then
|
||||
continue
|
||||
else
|
||||
VALUE="$(jq -r --arg k "$KEYS" '.[$k] // "" | tostring' "$JSONSOURCE")"
|
||||
export_option "$KEYS" "$VALUE"
|
||||
fi
|
||||
done
|
||||
|
||||
update_scripts_with_block
|
||||
|
||||
################
|
||||
# Set timezone #
|
||||
################
|
||||
|
||||
Reference in New Issue
Block a user