mirror of
https://github.com/alexbelgium/hassio-addons.git
synced 2026-06-08 08:35:57 +02:00
Improved code
This commit is contained in:
@@ -1,208 +1,184 @@
|
|||||||
#!/usr/bin/with-contenv bashio
|
#!/usr/bin/with-contenv bashio
|
||||||
# shellcheck shell=bash
|
# shellcheck shell=bash
|
||||||
# shellcheck disable=SC2155,SC1087,SC2163,SC2116,SC2086
|
# shellcheck disable=SC2155,SC1087,SC2163,SC2116,SC2086
|
||||||
set -e
|
# -----------------------------------------------------------------------------
|
||||||
|
# Robust environment‑variable loader for Home‑Assistant add‑ons
|
||||||
|
# • Parses YAML with embedded Python, no external yq dependency
|
||||||
|
# • Supports !secret look‑ups
|
||||||
|
# • Correctly escapes values containing quotes, $, \\ , newlines …
|
||||||
|
# • Exports to: shell, env.py, .env, /etc/environment, s6, service scripts
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
##################
|
##################
|
||||||
# INITIALIZATION #
|
# INITIALIZATION #
|
||||||
##################
|
##################
|
||||||
|
|
||||||
# Disable if config not present
|
# Run outside HA? then do nothing
|
||||||
if [ ! -d /config ] || ! bashio::supervisor.ping 2>/dev/null; then
|
if [[ ! -d /config ]] || ! bashio::supervisor.ping &>/dev/null; then
|
||||||
echo "..."
|
echo "..."
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Define slug
|
slug="${HOSTNAME/-/_}" ; slug="${slug#*_}"
|
||||||
slug="${HOSTNAME/-/_}"
|
|
||||||
slug="${slug#*_}"
|
|
||||||
|
|
||||||
# Check type of config folder
|
# -----------------------------------------------------------------------------
|
||||||
if [ ! -f /config/configuration.yaml ] && [ ! -f /config/configuration.json ]; then
|
# Resolve CONFIGSOURCE #
|
||||||
# New config location
|
# -----------------------------------------------------------------------------
|
||||||
CONFIGLOCATION="/config"
|
if [[ ! -f /config/configuration.yaml && ! -f /config/configuration.json ]]; then
|
||||||
|
CONFIGLOCATION="/config" # New architecture
|
||||||
CONFIGFILEBROWSER="/addon_configs/${HOSTNAME/-/_}/config.yaml"
|
CONFIGFILEBROWSER="/addon_configs/${HOSTNAME/-/_}/config.yaml"
|
||||||
else
|
else
|
||||||
# Legacy config location
|
CONFIGLOCATION="/config/addons_config/${slug}" # Legacy architecture
|
||||||
CONFIGLOCATION="/config/addons_config/${slug}"
|
CONFIGFILEBROWSER="/homeassistant/addons_config/${slug}/config.yaml"
|
||||||
CONFIGFILEBROWSER="/homeassistant/addons_config/$slug/config.yaml"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Default location
|
mkdir -p "$CONFIGLOCATION"
|
||||||
mkdir -p "$CONFIGLOCATION" || true
|
CONFIGSOURCE="$CONFIGLOCATION/config.yaml"
|
||||||
CONFIGSOURCE="$CONFIGLOCATION"/config.yaml
|
|
||||||
|
|
||||||
# Is there a custom path
|
|
||||||
if bashio::config.has_value 'CONFIG_LOCATION'; then
|
if bashio::config.has_value 'CONFIG_LOCATION'; then
|
||||||
|
CONFIGSOURCE="$(bashio::config "CONFIG_LOCATION")"
|
||||||
CONFIGSOURCE=$(bashio::config "CONFIG_LOCATION")
|
[[ "$CONFIGSOURCE" == *.* ]] && CONFIGSOURCE="$(dirname "$CONFIGSOURCE")"
|
||||||
if [[ "$CONFIGSOURCE" == *.* ]]; then
|
[[ "$CONFIGSOURCE" != *.yaml ]] && CONFIGSOURCE="${CONFIGSOURCE%/}/config.yaml"
|
||||||
CONFIGSOURCE=$(dirname "$CONFIGSOURCE")
|
case "$CONFIGSOURCE" in
|
||||||
fi
|
/share/*|/config/*|/data/*) :;;
|
||||||
# If does not end by config.yaml, remove trailing slash and add config.yaml
|
*) bashio::log.red "CONFIG_LOCATION must be in /share, /config or /data – defaulting." && CONFIGSOURCE="$CONFIGLOCATION/config.yaml";;
|
||||||
if [[ "$CONFIGSOURCE" != *".yaml" ]]; then
|
esac
|
||||||
CONFIGSOURCE="${CONFIGSOURCE%/}"/config.yaml
|
|
||||||
fi
|
|
||||||
# Check if config is located in an acceptable location
|
|
||||||
LOCATIONOK=""
|
|
||||||
for location in "/share" "/config" "/data"; do
|
|
||||||
if [[ "$CONFIGSOURCE" == "$location"* ]]; then
|
|
||||||
LOCATIONOK=true
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
if [ -z "$LOCATIONOK" ]; then
|
|
||||||
bashio::log.red "Watch-out : your CONFIG_LOCATION values can only be set in /share, /config or /data (internal to addon). It will be reset to the default location : $CONFIGLOCATION/config.yaml"
|
|
||||||
CONFIGSOURCE="$CONFIGLOCATION"/config.yaml
|
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Migrate if needed
|
if [[ "$CONFIGLOCATION" == "/config" && -f "/homeassistant/addons_config/${slug}/config.yaml" && ! -L "/homeassistant/addons_config/${slug}" ]]; then
|
||||||
if [[ "$CONFIGLOCATION" == "/config" ]]; then
|
echo "Migrating config.yaml to $CONFIGLOCATION"
|
||||||
# Migrate file
|
mv "/homeassistant/addons_config/${slug}/config.yaml" "$CONFIGSOURCE"
|
||||||
if [ -f "/homeassistant/addons_config/${slug}/config.yaml" ] && [ ! -L "/homeassistant/addons_config/${slug}" ]; then
|
|
||||||
echo "Migrating config.yaml to new config location"
|
|
||||||
mv /homeassistant/addons_config/"${slug}"/config.yaml /config/config.yaml
|
|
||||||
fi
|
|
||||||
# Migrate option
|
|
||||||
if [[ "$(bashio::config "CONFIG_LOCATION")" == "/config/addons_config"* ]] && [ -f /config/config.yaml ]; then
|
|
||||||
bashio::addon.option "CONFIG_LOCATION" "/config/config.yaml"
|
|
||||||
CONFIGSOURCE="/config/config.yaml"
|
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ "$CONFIGSOURCE" != *".yaml" ]]; then
|
chmod -R 755 "$(dirname "$CONFIGSOURCE")"
|
||||||
bashio::log.error "Something is going wrong in the config location, quitting"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Permissions
|
|
||||||
if [[ "$CONFIGSOURCE" == *".yaml" ]]; then
|
|
||||||
echo "Setting permissions for the config.yaml directory"
|
|
||||||
mkdir -p "$(dirname "${CONFIGSOURCE}")"
|
|
||||||
chmod -R 755 "$(dirname "${CONFIGSOURCE}")" 2>/dev/null
|
|
||||||
fi
|
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# LOAD CONFIG.YAML #
|
# CONFIG TEMPLATE #
|
||||||
####################
|
####################
|
||||||
|
|
||||||
echo ""
|
if [[ ! -f "$CONFIGSOURCE" ]]; then
|
||||||
bashio::log.green "Load environment variables from $CONFIGSOURCE if existing"
|
echo "... no config file, creating one from template."
|
||||||
if [[ "$CONFIGSOURCE" == "/config"* ]]; then
|
mkdir -p "$(dirname "$CONFIGSOURCE")"
|
||||||
bashio::log.green "If accessing the file with filebrowser it should be mapped to $CONFIGFILEBROWSER"
|
if [[ -f /templates/config.yaml ]]; then
|
||||||
else
|
cp /templates/config.yaml "$CONFIGSOURCE"
|
||||||
bashio::log.green "If accessing the file with filebrowser it should be mapped to $CONFIGSOURCE"
|
|
||||||
fi
|
|
||||||
bashio::log.green "---------------------------------------------------------"
|
|
||||||
bashio::log.green "Wiki here on how to use : github.com/alexbelgium/hassio-addons/wiki/Add‐ons-feature-:-add-env-variables"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Check if config file is there, or create one from template
|
|
||||||
if [ ! -f "$CONFIGSOURCE" ]; then
|
|
||||||
echo "... no config file, creating one from template. Please customize the file in $CONFIGSOURCE before restarting."
|
|
||||||
# Create folder
|
|
||||||
mkdir -p "$(dirname "${CONFIGSOURCE}")"
|
|
||||||
# Placing template in config
|
|
||||||
if [ -f /templates/config.yaml ]; then
|
|
||||||
# Use available template
|
|
||||||
cp /templates/config.yaml "$(dirname "${CONFIGSOURCE}")"
|
|
||||||
else
|
else
|
||||||
# Download template
|
curl -fsSL "https://raw.githubusercontent.com/alexbelgium/hassio-addons/master/.templates/config.template" -o "$CONFIGSOURCE"
|
||||||
TEMPLATESOURCE="https://raw.githubusercontent.com/alexbelgium/hassio-addons/master/.templates/config.template"
|
|
||||||
curl -f -L -s -S "$TEMPLATESOURCE" --output "$CONFIGSOURCE"
|
|
||||||
fi
|
fi
|
||||||
|
bashio::log.green "Edit $CONFIGSOURCE then restart the add‑on."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Check if there are lines to read
|
if ! grep -qE '^[[:space:]]*[A-Za-z0-9_]+:' "$CONFIGSOURCE"; then
|
||||||
cp "$CONFIGSOURCE" /tempenv
|
|
||||||
sed -i '/^#/d' /tempenv
|
|
||||||
sed -i '/^[[:space:]]*$/d' /tempenv
|
|
||||||
sed -i '/^$/d' /tempenv
|
|
||||||
# Exit if empty
|
|
||||||
if [ ! -s /tempenv ]; then
|
|
||||||
bashio::log.green "... no env variables found, exiting"
|
bashio::log.green "... no env variables found, exiting"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
rm /tempenv
|
|
||||||
|
|
||||||
# Check if yaml is valid
|
############################################
|
||||||
EXIT_CODE=0
|
# HELPER: read_yaml() (Python) #
|
||||||
yamllint -d relaxed "$CONFIGSOURCE" &>ERROR || EXIT_CODE=$?
|
############################################
|
||||||
if [ "$EXIT_CODE" != 0 ]; then
|
# Prints flattened KEY=value lines for scalar leaves.
|
||||||
cat ERROR
|
read_yaml() {
|
||||||
bashio::log.yellow "... config file has an invalid yaml format. Please check the file in $CONFIGSOURCE. Errors list above."
|
python3 - "$1" <<'PY'
|
||||||
fi
|
import sys, yaml, json, pathlib
|
||||||
|
from collections.abc import Mapping, Sequence
|
||||||
|
|
||||||
# Export all yaml entries as env variables
|
def walk(node, prefix=""):
|
||||||
# Helper function
|
if isinstance(node, Mapping):
|
||||||
function parse_yaml {
|
for k, v in node.items():
|
||||||
local prefix=$2 || local prefix=""
|
yield from walk(v, f"{prefix}{k}_")
|
||||||
local s='[[:space:]]*' w='[a-zA-Z0-9_]*' fs=$(echo @ | tr @ '\034')
|
elif isinstance(node, Sequence) and not isinstance(node, (str, bytes)):
|
||||||
sed -ne "s|^\($s\):|\1|" \
|
for i, v in enumerate(node):
|
||||||
-e "s| #.*$||g" \
|
yield from walk(v, f"{prefix}{i}_")
|
||||||
-e "s|#.*$||g" \
|
else:
|
||||||
-e "s|^\($s\)\($w\)$s:$s[\"']\(.*\)[\"']$s\$|\1$fs\2$fs\3|p" \
|
yield prefix[:-1], node
|
||||||
-e "s|^\($s\)\($w\)$s:$s\(.*\)$s\$|\1$fs\2$fs\3|p" $1 |
|
|
||||||
awk -F$fs '{
|
fname = sys.argv[1]
|
||||||
indent = length($1)/2;
|
with open(fname, 'r') as f:
|
||||||
vname[indent] = $2;
|
data = yaml.safe_load(f) or {}
|
||||||
for (i in vname) {if (i > indent) {delete vname[i]}}
|
for k, v in walk(data):
|
||||||
if (length($3) > 0) {
|
if isinstance(v, (str, int, float, bool)):
|
||||||
vn=""; for (i=0; i<indent; i++) {vn=(vn)(vname[i])("_")}
|
print(f"{k}={v}")
|
||||||
printf("%s%s%s=\"%s\"\n", "'$prefix'",vn, $2, $3);
|
PY
|
||||||
}
|
|
||||||
}'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Get list of parameters in a file
|
############################################
|
||||||
parse_yaml "$CONFIGSOURCE" "" >/tmpfile
|
# HELPER: shell_escape() #
|
||||||
# Escape dollars
|
############################################
|
||||||
sed -i 's|$.|\$|g' /tmpfile
|
# Uses printf %q – POSIX‑sh safe quoting.
|
||||||
|
shell_escape() {
|
||||||
|
printf '%q' "$1"
|
||||||
|
}
|
||||||
|
|
||||||
# Look where secrets.yaml is located
|
############################################
|
||||||
|
# Locate secrets.yaml #
|
||||||
|
############################################
|
||||||
SECRETSFILE="/config/secrets.yaml"
|
SECRETSFILE="/config/secrets.yaml"
|
||||||
if [ -f "$SECRETSFILE" ]; then SECRETSFILE="/homeassistant/secrets.yaml"; fi
|
[[ -f "$SECRETSFILE" ]] || SECRETSFILE="/homeassistant/secrets.yaml"
|
||||||
|
|
||||||
while IFS= read -r line; do
|
get_secret() {
|
||||||
# Clean output
|
python3 - "$SECRETSFILE" "$1" <<'PY'
|
||||||
line="${line//[\"\']/}"
|
import sys, yaml, pathlib
|
||||||
# Check if secret
|
sec, key = sys.argv[1:3]
|
||||||
if [[ "${line}" == *'!secret '* ]]; then
|
try:
|
||||||
echo "secret detected"
|
with open(sec) as f:
|
||||||
secret=${line#*secret }
|
data = yaml.safe_load(f) or {}
|
||||||
# Check if single match
|
print(data.get(key, ""))
|
||||||
secretnum=$(sed -n "/$secret:/=" "$SECRETSFILE")
|
except FileNotFoundError:
|
||||||
[[ $(echo $secretnum) == *' '* ]] && bashio::exit.nok "There are multiple matches for your password name. Please check your secrets.yaml file"
|
pass
|
||||||
# Get text
|
PY
|
||||||
secret=$(sed -n "/$secret:/p" "$SECRETSFILE")
|
}
|
||||||
secret=${secret#*: }
|
|
||||||
line="${line%%=*}='$secret'"
|
############################################
|
||||||
|
# MAIN LOOP #
|
||||||
|
############################################
|
||||||
|
while IFS= read -r PAIR; do
|
||||||
|
KEY="${PAIR%%=*}" ; VALUE="${PAIR#*=}"
|
||||||
|
|
||||||
|
# !secret support
|
||||||
|
if [[ "$VALUE" =~ ^!secret[[:space:]]+(.+) ]]; then
|
||||||
|
NAME="${BASH_REMATCH[1]}"
|
||||||
|
VALUE="$(get_secret "$NAME")"
|
||||||
|
[[ -z "$VALUE" ]] && bashio::exit.nok "Secret '$NAME' not found in $SECRETSFILE"
|
||||||
fi
|
fi
|
||||||
# Data validation
|
|
||||||
if [[ "$line" =~ ^.+[=].+$ ]]; then
|
SAFE_VALUE=$(shell_escape "$VALUE")
|
||||||
# extract keys and values
|
|
||||||
KEYS="${line%%=*}"
|
# 1) Export to current shell
|
||||||
VALUE="${line#*=}"
|
export "$KEY=$VALUE"
|
||||||
line="${KEYS}='${VALUE}'"
|
|
||||||
export "$line"
|
# 2) env.py (idempotent)
|
||||||
# export to python
|
python3 - "$KEY" "$VALUE" <<'PY'
|
||||||
if command -v "python3" &>/dev/null; then
|
import json, os, pathlib, sys
|
||||||
[ ! -f /env.py ] && echo "import os" >/env.py
|
k, v = sys.argv[1:3]
|
||||||
echo "os.environ['${KEYS}'] = '${VALUE//[\"\']/}'" >>/env.py
|
p = pathlib.Path('/env.py')
|
||||||
python3 /env.py
|
if not p.exists():
|
||||||
|
p.write_text('import os\n')
|
||||||
|
with p.open('a') as f:
|
||||||
|
f.write(f"os.environ[{json.dumps(k)}] = {json.dumps(v)}\n")
|
||||||
|
os.environ[k] = v
|
||||||
|
PY
|
||||||
|
|
||||||
|
# 3) .env & /etc/environment (double‑quoted, internal " escaped)
|
||||||
|
env_val="${VALUE//\"/\"}"
|
||||||
|
printf '%s="%s"\n' "$KEY" "$env_val" >> /.env
|
||||||
|
printf '%s="%s"\n' "$KEY" "$env_val" >> /etc/environment
|
||||||
|
|
||||||
|
# 4) s6 container_environment (raw value)
|
||||||
|
if [[ -d /var/run/s6/container_environment ]]; then
|
||||||
|
printf '%s' "$VALUE" > "/var/run/s6/container_environment/$KEY"
|
||||||
fi
|
fi
|
||||||
# set .env
|
|
||||||
if [ -f /.env ]; then echo "$line" >>/.env; fi
|
# 5) Prepend export to service scripts
|
||||||
mkdir -p /etc
|
for script in /etc/services.d/*/*run* /etc/cont-init.d/*run*; do
|
||||||
echo "$line" >>/etc/environment
|
[[ -f $script ]] || continue
|
||||||
# Export to scripts
|
grep -q "^export $KEY=" "$script" || sed -i "1i export $KEY=$SAFE_VALUE" "$script"
|
||||||
if cat /etc/services.d/*/*run* &>/dev/null; then sed -i "1a export $line" /etc/services.d/*/*run* 2>/dev/null; fi
|
done
|
||||||
if cat /etc/cont-init.d/*run* &>/dev/null; then sed -i "1a export $line" /etc/cont-init.d/*run* 2>/dev/null; fi
|
|
||||||
# For s6
|
# 6) Persist for interactive shells
|
||||||
if [ -d /var/run/s6/container_environment ]; then printf "%s" "${VALUE}" >/var/run/s6/container_environment/"${KEYS}"; fi
|
grep -q "^export $KEY=" ~/.bashrc || echo "export $KEY=$SAFE_VALUE" >> ~/.bashrc
|
||||||
echo "export $line" >>~/.bashrc
|
|
||||||
# Show in log
|
# 7) Log (truncate long values)
|
||||||
if ! bashio::config.false "verbose"; then bashio::log.blue "$line"; fi
|
bashio::log.blue "$KEY='${VALUE:0:60}'${VALUE:60:+…}"
|
||||||
else
|
done < <(read_yaml "$CONFIGSOURCE")
|
||||||
bashio::log.red "$line does not follow the correct structure. Please check your yaml file."
|
|
||||||
fi
|
bashio::log.green "Environment variables successfully loaded."
|
||||||
done <"/tmpfile"
|
|
||||||
|
|||||||
Reference in New Issue
Block a user