diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 2cddf91..b0f462c 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -16,7 +16,7 @@ jobs: docker: if: | github.event_name == 'workflow_dispatch' || - github.repository == 'rightup/pyMC_Repeater' || + github.repository == 'pyMC-dev/pyMC_Repeater' || github.repository == 'yellowcooln/pyMC_Repeater' runs-on: ubuntu-latest permissions: @@ -77,7 +77,7 @@ jobs: cache-to: type=gha,mode=max - name: Notify Home Assistant add-on repository - if: github.repository == 'rightup/pyMC_Repeater' + if: github.repository == 'pyMC-dev/pyMC_Repeater' env: DISPATCH_TOKEN: ${{ secrets.HA_ADDON_REPO_DISPATCH_TOKEN }} CHANNEL: ${{ github.ref_name }} diff --git a/docker-compose.yml b/docker-compose.yml index 1048993..3465abc 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,10 @@ services: pymc-repeater: - build: . + build: + context: . + args: + PUID: ${PUID:-1000} + PGID: ${PGID:-1000} container_name: pymc-repeater restart: unless-stopped ports: diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh new file mode 100644 index 0000000..16788ac --- /dev/null +++ b/docker-entrypoint.sh @@ -0,0 +1,87 @@ +#!/bin/sh +set -eu + +INSTALL_DIR="${INSTALL_DIR:-/opt/pymc_repeater}" +CONFIG_DIR="${CONFIG_DIR:-/etc/pymc_repeater}" +CONFIG_PATH="${PYMC_REPEATER_CONFIG:-${CONFIG_DIR}/config.yaml}" +EXAMPLE_PATH="${CONFIG_DIR}/config.yaml.example" +BUNDLED_EXAMPLE_PATH="${INSTALL_DIR}/config.yaml.example" +RUNTIME_USER="${USER:-repeater}" +RUNTIME_UID="${PUID:-unknown}" +RUNTIME_GID="${PGID:-unknown}" +YQ_CMD="${YQ_CMD:-/usr/local/bin/yq}" + +mkdir -p "${CONFIG_DIR}" + +copy_or_die() { + src="$1" + dest="$2" + if ! cp "${src}" "${dest}"; then + echo "Failed to initialize ${dest} from ${src}." >&2 + echo "If you are bind-mounting ./config.yaml, ensure the host path is writable by ${RUNTIME_USER} (${RUNTIME_UID}:${RUNTIME_GID})." >&2 + exit 1 + fi +} + +merge_config_from_example() { + config_path="$1" + + if [ ! -f "${config_path}" ] || [ ! -f "${EXAMPLE_PATH}" ]; then + return 0 + fi + + if [ ! -x "${YQ_CMD}" ] || ! "${YQ_CMD}" --version 2>&1 | grep -q "mikefarah/yq"; then + echo "Skipping config merge: mikefarah yq is not available at ${YQ_CMD}." >&2 + return 0 + fi + + tmpdir="$(mktemp -d)" + stripped_user="${tmpdir}/config.stripped.yaml" + merged_config="${tmpdir}/config.merged.yaml" + + cleanup_merge() { + rm -rf "${tmpdir}" + } + trap cleanup_merge EXIT HUP INT TERM + + # Keep only the example's comments to avoid comment duplication across upgrades. + "${YQ_CMD}" eval '... comments=""' "${config_path}" > "${stripped_user}" 2>/dev/null || cp "${config_path}" "${stripped_user}" + + if ! "${YQ_CMD}" eval-all '. as $item ireduce ({}; . * $item)' "${EXAMPLE_PATH}" "${stripped_user}" > "${merged_config}" 2>/dev/null; then + echo "Failed to merge ${config_path} with ${EXAMPLE_PATH}; keeping the existing config." >&2 + cleanup_merge + trap - EXIT HUP INT TERM + return 0 + fi + + if ! "${YQ_CMD}" eval '.' "${merged_config}" >/dev/null 2>&1; then + echo "Merged config for ${config_path} is invalid; keeping the existing config." >&2 + cleanup_merge + trap - EXIT HUP INT TERM + return 0 + fi + + if ! cmp -s "${config_path}" "${merged_config}"; then + copy_or_die "${merged_config}" "${config_path}" + fi + + cleanup_merge + trap - EXIT HUP INT TERM +} + +if [ ! -f "${EXAMPLE_PATH}" ] && [ -f "${BUNDLED_EXAMPLE_PATH}" ]; then + copy_or_die "${BUNDLED_EXAMPLE_PATH}" "${EXAMPLE_PATH}" +fi + +if [ -d "${CONFIG_PATH}" ]; then + if [ ! -s "${CONFIG_PATH}/config.yaml" ] && [ -f "${EXAMPLE_PATH}" ]; then + copy_or_die "${EXAMPLE_PATH}" "${CONFIG_PATH}/config.yaml" + fi + CONFIG_PATH="${CONFIG_PATH}/config.yaml" +elif [ ! -s "${CONFIG_PATH}" ] && [ -f "${EXAMPLE_PATH}" ]; then + copy_or_die "${EXAMPLE_PATH}" "${CONFIG_PATH}" +fi + +merge_config_from_example "${CONFIG_PATH}" + +exec python3 -m repeater.main --config "${CONFIG_PATH}" diff --git a/dockerfile b/dockerfile index c32b466..f0b56d6 100644 --- a/dockerfile +++ b/dockerfile @@ -1,15 +1,25 @@ FROM python:3.12-slim-bookworm ARG PACKAGE_VERSION=1.0.5 +ARG USER=repeater +ARG GROUP=repeater +ARG PUID=15888 +ARG PGID=15888 +ARG TARGETARCH +ARG YQ_VERSION=v4.40.5 ENV INSTALL_DIR=/opt/pymc_repeater \ CONFIG_DIR=/etc/pymc_repeater \ DATA_DIR=/var/lib/pymc_repeater \ + HOME_DIR=/home/${USER} \ + PATH=/home/${USER}/.local/bin:${PATH} \ PYTHONUNBUFFERED=1 \ - SETUPTOOLS_SCM_PRETEND_VERSION_FOR_PYMC_REPEATER=${PACKAGE_VERSION} + SETUPTOOLS_SCM_PRETEND_VERSION_FOR_PYMC_REPEATER=${PACKAGE_VERSION} \ + PUID=${PUID} \ + PGID=${PGID} # Install runtime dependencies only -RUN apt-get update && apt-get install -y \ +RUN DEBIAN_FRONTEND=noninteractive apt-get update && apt-get install -y \ libffi-dev \ python3-rrdtool \ jq \ @@ -21,20 +31,47 @@ RUN apt-get update && apt-get install -y \ python3-dev \ && rm -rf /var/lib/apt/lists/* +RUN arch="${TARGETARCH:-}" \ + && if [ -z "${arch}" ]; then arch="$(uname -m)"; fi \ + && case "${arch}" in \ + amd64|x86_64) YQ_BINARY="yq_linux_amd64" ;; \ + arm64|aarch64) YQ_BINARY="yq_linux_arm64" ;; \ + arm|armv7|armv7l) YQ_BINARY="yq_linux_arm" ;; \ + *) echo "Unsupported architecture for yq: ${arch}" >&2; exit 1 ;; \ + esac \ + && wget -qO /usr/local/bin/yq "https://github.com/mikefarah/yq/releases/download/${YQ_VERSION}/${YQ_BINARY}" \ + && chmod +x /usr/local/bin/yq + +# Create the group and user in order to run without root privileges +RUN groupadd --gid "$PGID" "$GROUP" \ + && useradd --uid "$PUID" --gid "$PGID" --home-dir "$HOME_DIR" --create-home --shell /usr/bin/bash "$USER" + # Create runtime directories -RUN mkdir -p ${INSTALL_DIR} ${CONFIG_DIR} ${DATA_DIR} +RUN mkdir -p ${INSTALL_DIR} ${CONFIG_DIR} ${DATA_DIR} \ + && chown -R "$USER":"$GROUP" ${INSTALL_DIR} ${CONFIG_DIR} ${DATA_DIR} ${HOME_DIR} WORKDIR ${INSTALL_DIR} # Copy source COPY repeater ./repeater COPY pyproject.toml . +COPY config.yaml.example . COPY radio-presets.json . COPY radio-settings.json . +COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh + +# Switch to the unprivileged runtime user +USER ${USER} # Install package RUN pip install --no-cache-dir . +USER root + +RUN chmod +x /usr/local/bin/docker-entrypoint.sh + +USER ${USER} + EXPOSE 8000 -ENTRYPOINT ["python3", "-m", "repeater.main", "--config", "/etc/pymc_repeater/config.yaml"] +ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]