diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..6313b56 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto eol=lf diff --git a/.github/workflows/hadolint.yml b/.github/workflows/hadolint.yml index 23402cf..de00b60 100644 --- a/.github/workflows/hadolint.yml +++ b/.github/workflows/hadolint.yml @@ -32,13 +32,9 @@ jobs: - name: Checkout code uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - - name: Run hadolint - uses: hadolint/hadolint-action@2332a7b74a6de0dda2e2221d575162eba76ba5e5 # v3.3.0 - with: - dockerfile: ./Dockerfile - format: sarif - output-file: hadolint-results.sarif - no-fail: true + - name: Run hadolint via Makefile + continue-on-error: true + run: make lint-docker HADOLINT_FORMAT=sarif HADOLINT_OUTPUT_FILE=hadolint-results.sarif - name: Upload analysis results to GitHub uses: github/codeql-action/upload-sarif@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 diff --git a/.github/workflows/headscale-config-checker.yml b/.github/workflows/headscale-config-checker.yml index afc68aa..1ed5b56 100644 --- a/.github/workflows/headscale-config-checker.yml +++ b/.github/workflows/headscale-config-checker.yml @@ -17,100 +17,33 @@ jobs: steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - - name: Get version and download upstream config - run: | - VERSION=$(grep -oP 'HEADSCALE_VERSION="\K[^"]*' Dockerfile) - echo "Found version: $VERSION" - - curl -f -o upstream-config.yaml \ - "https://raw.githubusercontent.com/juanfont/headscale/refs/tags/v${VERSION}/config-example.yaml" - - echo "Successfully downloaded upstream config" - - - name: Generate config from template with defaults - run: | - # Source the defaults - source scripts/defaults.sh - source scripts/ci-defaults.sh - - # Generate config - envsubst < templates/headscale.template.yaml > generated-config.yaml - - - name: Check for new options + - name: Check for new options via Makefile id: check - run: | - # Simple approach: extract keys and apply ignores - extract_keys() { - grep -E "^[[:space:]]*[^#\-].*:" "$1" | \ - sed 's/:[[:space:]]*.*$//' | \ - sed 's/^[[:space:]]*//' | \ - sort -u - } - - # Get list of keys to ignore from DIFF_IGNORE comments (including commented lines) - get_ignored_keys() { - grep "# DIFF_IGNORE" "templates/headscale.template.yaml" | \ - sed -E 's/^[[:space:]]*#?[[:space:]]*//' | \ - sed -E 's/:.*# DIFF_IGNORE.*$//' | \ - sort -u - } - - echo "=== Getting ignored keys ===" - get_ignored_keys > ignored_keys.txt - echo "Keys to ignore:" - cat ignored_keys.txt - echo "=== End ignored keys ===" - - # Extract all keys - extract_keys "generated-config.yaml" > local_all_keys.txt - extract_keys "upstream-config.yaml" > upstream_all_keys.txt - - # Normalize keys (strip optional leading '#' and surrounding spaces) and sort-unique - sed -E 's/^[[:space:]]*#?[[:space:]]*//' upstream_all_keys.txt | sed 's/[[:space:]]*$//' | sort -u > upstream_all_keys_norm.txt - sed -E 's/^[[:space:]]*#?[[:space:]]*//' local_all_keys.txt | sed 's/[[:space:]]*$//' | sort -u > local_all_keys_norm.txt - - # Remove ignored keys from upstream (operate on normalized list) - cp upstream_all_keys_norm.txt upstream_filtered_keys.txt - while IFS= read -r ignore_key; do - if [ -n "$ignore_key" ]; then - grep -v "^${ignore_key}$" upstream_filtered_keys.txt > temp_filtered.txt - mv temp_filtered.txt upstream_filtered_keys.txt - fi - done < ignored_keys.txt - - # Ensure upstream filtered file is sorted/unique for comm - sort -u upstream_filtered_keys.txt -o upstream_filtered_keys.txt - - # Find missing keys using normalized local list - comm -23 upstream_filtered_keys.txt local_all_keys_norm.txt > new-options.txt - - echo "Final comparison:" - echo "Local keys: $(wc -l < local_all_keys_norm.txt)" - echo "Upstream filtered keys: $(wc -l < upstream_filtered_keys.txt)" - - if [ -s new-options.txt ]; then - echo "has_missing=true" >> $GITHUB_OUTPUT - echo "🆕 New configuration keys found:" - cat new-options.txt - else - echo "has_missing=false" >> $GITHUB_OUTPUT - echo "✅ No new configuration keys found" - fi - - # Cleanup - rm -f ignored_keys.txt local_all_keys.txt upstream_all_keys.txt upstream_filtered_keys.txt temp_filtered.txt upstream_all_keys_norm.txt local_all_keys_norm.txt + continue-on-error: true + run: make check-config-drift - name: Comment on PR - if: github.event_name == 'pull_request' && steps.check.outputs.has_missing == 'true' + if: github.event_name == 'pull_request' && steps.check.outcome == 'failure' && hashFiles('new-options.txt') != '' uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | const fs = require('fs'); + if (!fs.existsSync('new-options.txt')) { + console.log('Skipping PR comment because new-options.txt was not created.'); + return; + } + const newOptions = fs.readFileSync('new-options.txt', 'utf8'); - - github.rest.issues.createComment({ + + await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, body: `## 🆕 New Headscale Config Options\n\n\`\`\`diff\n${newOptions.split('\n').map(line => line.trim() ? `+ ${line}` : '').filter(line => line).join('\n')}\`\`\`` }); + + - name: Fail when drift check fails + if: steps.check.outcome == 'failure' + run: | + echo 'Headscale config drift check failed (see logs above).' + exit 1 diff --git a/Dockerfile b/Dockerfile index fae056e..8071904 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,157 +1,155 @@ -################### -# BUILD PREP -################### -# Tool version arguments -# Bump these every time there is a new release. -# We're pulling these from github source, don't forget to bump the checksum! -ARG HEADSCALE_VERSION="0.29.2" -ARG HEADSCALE_SHA256="858ef94bca9ecdfc742c24119c831e437e1b75691fdd5041be4059db1a38aac0" - -ARG LITESTREAM_VERSION="0.5.13" -ARG LITESTREAM_SHA256="fc3420fea7d2f92d4d604aceeb0d7c63dc2c91f6ee5c1547cc05e25629e70f9f" - -# We're building these from source, so we need to specify the versions here rather than hash -ARG HEADSCALE_ADMIN_ENDPOINT="/admin" -ARG HEADSCALE_ADMIN_REPO="https://github.com/privacyint/headscale-admin" -ARG HEADSCALE_ADMIN_VERSION="v0.28.0" -ARG HEADSCALE_ADMIN_NODE_VERSION="25" - -# No checksum needed for these tools, we pull from official images -ARG CADDY_VERSION="2.11.4" -ARG MAIN_IMAGE_ALPINE_VERSION="3.24.1" - -# github download links -# These should never need adjusting unless the URIs change -ARG HEADSCALE_DOWNLOAD_URL="https://github.com/juanfont/headscale/releases/download/v${HEADSCALE_VERSION}/headscale_${HEADSCALE_VERSION}_linux_amd64" -ARG LITESTREAM_DOWNLOAD_URL="https://github.com/benbjohnson/litestream/releases/download/v${LITESTREAM_VERSION}/litestream-${LITESTREAM_VERSION}-linux-x86_64.tar.gz" - -################### -# BUILD PROCESS -################### - -# Build caddy with Cloudflare DNS support -FROM caddy:${CADDY_VERSION}-builder AS caddy-builder - # Set SHELL flags for RUN commands to allow -e and pipefail - # Rationale: https://github.com/hadolint/hadolint/wiki/DL4006 - SHELL ["/bin/ash", "-eo", "pipefail", "-c"] - - RUN xcaddy build \ - --with github.com/caddy-dns/cloudflare - -# Build the admin GUI from source -FROM node:${HEADSCALE_ADMIN_NODE_VERSION}-alpine AS admin-gui - # Set SHELL flags for RUN commands to allow -e and pipefail - # Rationale: https://github.com/hadolint/hadolint/wiki/DL4006 - SHELL ["/bin/ash", "-eo", "pipefail", "-c"] - - ARG HEADSCALE_ADMIN_REPO - ARG HEADSCALE_ADMIN_VERSION - ARG HEADSCALE_ADMIN_ENDPOINT - - RUN apk --no-cache upgrade; \ - apk add --no-cache --virtual BuildTimeDeps git; - - WORKDIR /app - RUN git clone ${HEADSCALE_ADMIN_REPO} . && \ - git checkout ${HEADSCALE_ADMIN_VERSION} - - ENV ENDPOINT="${HEADSCALE_ADMIN_ENDPOINT}" - RUN npm install && npm run build; - - RUN mv /app/build /app${HEADSCALE_ADMIN_ENDPOINT} - - RUN apk del BuildTimeDeps - -# Build our main image -FROM alpine:${MAIN_IMAGE_ALPINE_VERSION} - # Set SHELL flags for RUN commands to allow -e and pipefail - # Rationale: https://github.com/hadolint/hadolint/wiki/DL4006 - SHELL ["/bin/ash", "-eo", "pipefail", "-c"] - - # Import our "global" `ARG` values into this stage - ARG HEADSCALE_DOWNLOAD_URL - ARG HEADSCALE_SHA256 - ARG LITESTREAM_DOWNLOAD_URL - ARG LITESTREAM_SHA256 - - # Upgrade system and install various dependencies - # - BusyBox's wget isn't reliable enough - # - I'm gonna need a better shell - # - gettext provides `envsubst` for templating - # hadolint ignore=DL3018,SC2086 - RUN BUILD_DEPS="wget"; \ - RUNTIME_DEPS="bash gettext"; \ - apk --no-cache upgrade; \ - apk add --no-cache --virtual BuildTimeDeps ${BUILD_DEPS}; \ - apk add --no-cache ${RUNTIME_DEPS} - - # Copy caddy from the first stage - COPY --from=caddy-builder /usr/bin/caddy /usr/local/bin/caddy - # Caddy smoke test - RUN [ "$(command -v caddy)" = '/usr/local/bin/caddy' ]; \ - caddy version - - # Headscale - RUN set -ex; { \ - wget --retry-connrefused \ - --waitretry=1 \ - --read-timeout=20 \ - --timeout=15 \ - -t 0 \ - -q \ - -O headscale \ - ${HEADSCALE_DOWNLOAD_URL} || { \ - echo "Failed to download Headscale from ${HEADSCALE_DOWNLOAD_URL}"; \ - exit 1; \ - }; \ - echo "${HEADSCALE_SHA256} *headscale" | sha256sum -c - >/dev/null 2>&1; \ - chmod +x headscale; \ - mv headscale /usr/local/bin/; \ - }; \ - # Headscale smoke test - [ "$(command -v headscale)" = '/usr/local/bin/headscale' ]; \ - headscale version; - - # Litestream - RUN set -ex; { \ - wget --retry-connrefused \ - --waitretry=1 \ - --read-timeout=20 \ - --timeout=15 \ - -t 0 \ - -q \ - -O litestream.tar.gz \ - ${LITESTREAM_DOWNLOAD_URL} \ - ; \ - echo "${LITESTREAM_SHA256} *litestream.tar.gz" | sha256sum -c - >/dev/null 2>&1; \ - tar -xf litestream.tar.gz; \ - mv litestream /usr/local/bin/; \ - rm -f litestream.tar.gz; \ - }; \ - # Litestream smoke test - [ "$(command -v litestream)" = '/usr/local/bin/litestream' ]; \ - litestream version; - - # Headscale web GUI - COPY --from=admin-gui /app/admin/ /admin-gui/admin/ - - # Remove build-time dependencies - RUN apk del BuildTimeDeps - - # Copy configuration templates - COPY ./templates/headscale.template.yaml /etc/headscale/config.yaml - COPY ./templates/litestream.template.yml /etc/litestream.yml - COPY ./templates/Caddyfile-http.template /etc/caddy/Caddyfile-http - COPY ./templates/Caddyfile-https.template /etc/caddy/Caddyfile-https - - # Copy and setup scripts into a safe bin directory - COPY --chmod=755 ./scripts/ /usr/local/bin/ - - # Default HTTPS port - override with $PUBLIC_LISTEN_PORT environment variable - EXPOSE 443 - - # Health check to ensure services are running - HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \ - CMD headscale version && caddy version || exit 1 - - ENTRYPOINT ["/usr/local/bin/container-entrypoint.sh"] +################### +# BUILD PREP +################### +# Tool version arguments +# Bump these every time there is a new release. +# We're pulling these from github source, don't forget to bump the checksum! +ARG HEADSCALE_VERSION="0.29.2" +ARG HEADSCALE_SHA256="858ef94bca9ecdfc742c24119c831e437e1b75691fdd5041be4059db1a38aac0" + +ARG LITESTREAM_VERSION="0.5.13" +ARG LITESTREAM_SHA256="fc3420fea7d2f92d4d604aceeb0d7c63dc2c91f6ee5c1547cc05e25629e70f9f" + +# We're building these from source, so we need to specify the versions here rather than hash +ARG HEADSCALE_ADMIN_ENDPOINT="/admin" +ARG HEADSCALE_ADMIN_REPO="https://github.com/privacyint/headscale-admin" +ARG HEADSCALE_ADMIN_VERSION="v0.28.0" +ARG HEADSCALE_ADMIN_NODE_VERSION="25" + +# No checksum needed for these tools, we pull from official images +ARG CADDY_VERSION="2.11.4" +ARG MAIN_IMAGE_ALPINE_VERSION="3.24.1" + +# github download links +# These should never need adjusting unless the URIs change +ARG HEADSCALE_DOWNLOAD_URL="https://github.com/juanfont/headscale/releases/download/v${HEADSCALE_VERSION}/headscale_${HEADSCALE_VERSION}_linux_amd64" +ARG LITESTREAM_DOWNLOAD_URL="https://github.com/benbjohnson/litestream/releases/download/v${LITESTREAM_VERSION}/litestream-${LITESTREAM_VERSION}-linux-x86_64.tar.gz" + +################### +# BUILD PROCESS +################### + +# Build caddy with Cloudflare DNS support +FROM caddy:${CADDY_VERSION}-builder AS caddy-builder + # Set SHELL flags for RUN commands to allow -e and pipefail + # Rationale: https://github.com/hadolint/hadolint/wiki/DL4006 + SHELL ["/bin/ash", "-eo", "pipefail", "-c"] + + RUN xcaddy build \ + --with github.com/caddy-dns/cloudflare + +# Build the admin GUI from source +FROM node:${HEADSCALE_ADMIN_NODE_VERSION}-alpine AS admin-gui + # Set SHELL flags for RUN commands to allow -e and pipefail + # Rationale: https://github.com/hadolint/hadolint/wiki/DL4006 + SHELL ["/bin/ash", "-eo", "pipefail", "-c"] + + ARG HEADSCALE_ADMIN_REPO + ARG HEADSCALE_ADMIN_VERSION + ARG HEADSCALE_ADMIN_ENDPOINT + + # hadolint ignore=DL3018 + RUN apk --no-cache upgrade; \ + apk add --no-cache --virtual BuildTimeDeps git; + + WORKDIR /app + ENV ENDPOINT="${HEADSCALE_ADMIN_ENDPOINT}" + RUN git clone --depth 1 --branch "${HEADSCALE_ADMIN_VERSION}" "${HEADSCALE_ADMIN_REPO}" . && \ + npm install && \ + npm run build && \ + mv /app/build "/app${HEADSCALE_ADMIN_ENDPOINT}" && \ + apk del BuildTimeDeps + +# Build our main image +FROM alpine:${MAIN_IMAGE_ALPINE_VERSION} + # Set SHELL flags for RUN commands to allow -e and pipefail + # Rationale: https://github.com/hadolint/hadolint/wiki/DL4006 + SHELL ["/bin/ash", "-eo", "pipefail", "-c"] + + # Import our "global" `ARG` values into this stage + ARG HEADSCALE_DOWNLOAD_URL + ARG HEADSCALE_SHA256 + ARG LITESTREAM_DOWNLOAD_URL + ARG LITESTREAM_SHA256 + + # Upgrade system and install various dependencies + # - BusyBox's wget isn't reliable enough + # - I'm gonna need a better shell + # - gettext provides `envsubst` for templating + # hadolint ignore=DL3018,SC2086 + RUN BUILD_DEPS="wget"; \ + RUNTIME_DEPS="bash gettext"; \ + apk --no-cache upgrade; \ + apk add --no-cache --virtual BuildTimeDeps ${BUILD_DEPS}; \ + apk add --no-cache ${RUNTIME_DEPS} + + # Copy caddy from the first stage + COPY --from=caddy-builder /usr/bin/caddy /usr/local/bin/caddy + # Caddy smoke test + RUN [ "$(command -v caddy)" = '/usr/local/bin/caddy' ]; \ + caddy version + + # Headscale + RUN set -ex; { \ + wget --retry-connrefused \ + --waitretry=1 \ + --read-timeout=20 \ + --timeout=15 \ + -t 0 \ + -q \ + -O headscale \ + ${HEADSCALE_DOWNLOAD_URL} || { \ + echo "Failed to download Headscale from ${HEADSCALE_DOWNLOAD_URL}"; \ + exit 1; \ + }; \ + echo "${HEADSCALE_SHA256} *headscale" | sha256sum -c - >/dev/null 2>&1; \ + chmod +x headscale; \ + mv headscale /usr/local/bin/; \ + }; \ + # Headscale smoke test + [ "$(command -v headscale)" = '/usr/local/bin/headscale' ]; \ + headscale version; + + # Litestream + RUN set -ex; { \ + wget --retry-connrefused \ + --waitretry=1 \ + --read-timeout=20 \ + --timeout=15 \ + -t 0 \ + -q \ + -O litestream.tar.gz \ + ${LITESTREAM_DOWNLOAD_URL} \ + ; \ + echo "${LITESTREAM_SHA256} *litestream.tar.gz" | sha256sum -c - >/dev/null 2>&1; \ + tar -xf litestream.tar.gz; \ + mv litestream /usr/local/bin/; \ + rm -f litestream.tar.gz; \ + }; \ + # Litestream smoke test + [ "$(command -v litestream)" = '/usr/local/bin/litestream' ]; \ + litestream version; + + # Headscale web GUI + COPY --from=admin-gui /app/admin/ /admin-gui/admin/ + + # Remove build-time dependencies + RUN apk del BuildTimeDeps + + # Copy configuration templates + COPY ./templates/headscale.template.yaml /etc/headscale/config.yaml + COPY ./templates/litestream.template.yml /etc/litestream.yml + COPY ./templates/Caddyfile-http.template /etc/caddy/Caddyfile-http + COPY ./templates/Caddyfile-https.template /etc/caddy/Caddyfile-https + + # Copy and setup scripts into a safe bin directory + COPY --chmod=755 ./scripts/ /usr/local/bin/ + + # Default HTTPS port - override with $PUBLIC_LISTEN_PORT environment variable + EXPOSE 443 + + # Health check to ensure services are running + HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \ + CMD headscale version && caddy version || exit 1 + + ENTRYPOINT ["/usr/local/bin/container-entrypoint.sh"] diff --git a/Makefile b/Makefile index 4a3a0b7..66b141a 100644 --- a/Makefile +++ b/Makefile @@ -4,24 +4,79 @@ DEFAULT_SMOKE_TEST_IMAGE := headscale:latest DEFAULT_SMOKE_TEST_CONTAINER := headscale-container DEFAULT_SMOKE_TEST_HOST := 127.0.0.1 DEFAULT_SMOKE_TEST_PORT := 8008 +DEFAULT_HADOLINT_IMAGE := hadolint/hadolint:v2.12.0-alpine +DEFAULT_SHELLCHECK_IMAGE := koalaman/shellcheck:v0.10.0 +DEFAULT_YQ_IMAGE := mikefarah/yq:4.44.3 .DEFAULT_GOAL := help -.PHONY: help check-envsubst render-fly-config render-azure-container-apps smoke-test +.PHONY: help check-curl check-docker check-envsubst build lint-docker lint-shell render-fly-config render-azure-container-apps render-headscale-config check-config-drift check clean smoke-test help: @printf '%s\n' \ 'Available targets:' \ + ' make build Build the Docker image locally' \ + ' make lint-docker Lint Dockerfile with hadolint' \ + ' make lint-shell Lint shell scripts with shellcheck' \ ' make render-fly-config Render fly.toml from templates/fly.template.toml' \ ' make render-azure-container-apps Render azure-container-apps.yaml from templates/azure-container-apps.template.yaml' \ + ' make render-headscale-config Render generated-config.yaml from templates/headscale.template.yaml' \ + ' make check-config-drift Compare rendered config against upstream Headscale config' \ + ' make check Run the local validation suite' \ + ' make clean Remove generated local artefacts' \ ' make smoke-test Build the image and run local smoke tests' +check-curl: + @command -v curl >/dev/null || { \ + echo 'curl is required for upstream config checks.'; \ + exit 1; \ + } + +check-docker: + @command -v docker >/dev/null || { \ + echo 'docker is required for image builds and containerised linters.'; \ + exit 1; \ + } + check-envsubst: @command -v envsubst >/dev/null || { \ echo 'envsubst is required to render deployment templates. Install gettext first.'; \ exit 1; \ } +build: check-docker + @set -euo pipefail; \ + image="$${SMOKE_TEST_IMAGE:-$(DEFAULT_SMOKE_TEST_IMAGE)}"; \ + echo "Building Docker image: $${image}"; \ + docker build -t "$${image}" . + +lint-docker: check-docker + @set -euo pipefail; \ + image="$${HADOLINT_IMAGE:-$(DEFAULT_HADOLINT_IMAGE)}"; \ + format="$${HADOLINT_FORMAT:-tty}"; \ + output_file="$${HADOLINT_OUTPUT_FILE:-}"; \ + echo "Running hadolint using $${image}"; \ + if [[ -n "$${output_file}" ]]; then \ + docker run --rm -i -v "$${PWD}:/workdir" -w /workdir --entrypoint hadolint "$${image}" \ + --format "$${format}" Dockerfile > "$${output_file}"; \ + printf '%s\n' "Wrote $${output_file}"; \ + else \ + docker run --rm -i -v "$${PWD}:/workdir" -w /workdir --entrypoint hadolint "$${image}" \ + --format "$${format}" Dockerfile; \ + fi + +lint-shell: check-docker + @set -euo pipefail; \ + image="$${SHELLCHECK_IMAGE:-$(DEFAULT_SHELLCHECK_IMAGE)}"; \ + set -- scripts/*.sh; \ + if [[ "$${1}" == 'scripts/*.sh' ]]; then \ + echo 'No shell scripts found under scripts/'; \ + exit 0; \ + fi; \ + echo "Running shellcheck using $${image}"; \ + docker run --rm -i -v "$${PWD}:/workdir" -w /workdir --entrypoint shellcheck "$${image}" \ + --shell=bash "$${@}" + render-fly-config: check-envsubst @: $${FLY_APP:?Set FLY_APP} @: $${PUBLIC_SERVER_URL:?Set PUBLIC_SERVER_URL} @@ -40,7 +95,115 @@ render-azure-container-apps: check-envsubst @envsubst < templates/azure-container-apps.template.yaml > azure-container-apps.yaml @printf '%s\n' 'Wrote azure-container-apps.yaml' -smoke-test: +render-headscale-config: check-envsubst + @set -euo pipefail; \ + source scripts/defaults.sh; \ + source scripts/ci-defaults.sh; \ + envsubst < templates/headscale.template.yaml > generated-config.yaml; \ + printf '%s\n' 'Wrote generated-config.yaml' + +check-config-drift: check-curl check-docker render-headscale-config + @set -euo pipefail; \ + version="$$(awk -F'"' '/^ARG HEADSCALE_VERSION=/{ print $$2; exit }' Dockerfile)"; \ + yq_image="$${YQ_IMAGE:-$(DEFAULT_YQ_IMAGE)}"; \ + cleanup() { \ + rm -f ignored_paths.txt local_all_paths.txt upstream_all_paths.txt upstream_filtered_paths.txt temp_filtered.txt; \ + }; \ + trap cleanup EXIT; \ + echo "Downloading upstream Headscale config for v$${version}"; \ + curl --fail --silent --show-error -o upstream-config.yaml \ + "https://raw.githubusercontent.com/juanfont/headscale/refs/tags/v$${version}/config-example.yaml"; \ + extract_paths() { \ + docker run --rm -i -v "$${PWD}:/workdir" -w /workdir --entrypoint yq "$${yq_image}" \ + e '.. | path | select(length > 0) | map(select(tag == "!!str")) | select(length > 0) | join(".")' "$$1" | \ + sort -u; \ + }; \ + get_ignored_paths() { \ + awk 'BEGIN { depth = 0; } \ + function get_indent(text) { \ + indent = match(text, /[^ ]/) - 1; \ + return indent < 0 ? 0 : indent; \ + } \ + function get_key(text) { \ + key = text; \ + sub(/^[[:space:]]*#[[:space:]]?/, "", key); \ + sub(/^[[:space:]]*/, "", key); \ + sub(/:.*/, "", key); \ + return key; \ + } \ + function print_path(indent, key, current_depth, current, path_index) { \ + current_depth = depth; \ + while (current_depth > 0 && indent <= indents[current_depth]) { \ + current_depth--; \ + } \ + current = key; \ + if (current_depth > 0) { \ + current = path[1]; \ + for (path_index = 2; path_index <= current_depth; path_index++) { \ + current = current "." path[path_index]; \ + } \ + current = current "." key; \ + } \ + print current; \ + } \ + { \ + line = $$0; \ + if (line ~ /# DIFF_IGNORE/ && line ~ /^[[:space:]]*#/) { \ + candidate = line; \ + sub(/[[:space:]]+# DIFF_IGNORE[[:space:]]*$$/, "", candidate); \ + sub(/#/, "", candidate); \ + if (candidate !~ /^[[:space:]]*[[:alnum:]_.-]+[[:space:]]*:[[:space:]]*([^#].*)?$$/) { \ + next; \ + } \ + print_path(get_indent(candidate), get_key(candidate)); \ + next; \ + } \ + if (line ~ /^[[:space:]]*[[:alnum:]_.-]+[[:space:]]*:[[:space:]]*([^#].*)?$$/) { \ + indent = get_indent(line); \ + key = get_key(line); \ + while (depth > 0 && indent <= indents[depth]) { \ + delete path[depth]; \ + delete indents[depth]; \ + depth--; \ + } \ + depth++; \ + path[depth] = key; \ + indents[depth] = indent; \ + if (line ~ /# DIFF_IGNORE/) { \ + print_path(indent, key); \ + } \ + } \ + }' templates/headscale.template.yaml | \ + sort -u; \ + }; \ + get_ignored_paths > ignored_paths.txt; \ + extract_paths generated-config.yaml > local_all_paths.txt; \ + extract_paths upstream-config.yaml > upstream_all_paths.txt; \ + cp upstream_all_paths.txt upstream_filtered_paths.txt; \ + while IFS= read -r ignore_path; do \ + if [[ -n "$${ignore_path}" ]]; then \ + grep -F -x -v "$${ignore_path}" upstream_filtered_paths.txt > temp_filtered.txt || true; \ + mv temp_filtered.txt upstream_filtered_paths.txt; \ + fi; \ + done < ignored_paths.txt; \ + sort -u upstream_filtered_paths.txt -o upstream_filtered_paths.txt; \ + comm -23 upstream_filtered_paths.txt local_all_paths.txt > new-options.txt; \ + echo "Local paths: $$(wc -l < local_all_paths.txt)"; \ + echo "Upstream filtered paths: $$(wc -l < upstream_filtered_paths.txt)"; \ + if [[ -s new-options.txt ]]; then \ + echo 'New configuration keys found:'; \ + cat new-options.txt; \ + exit 1; \ + fi; \ + rm -f new-options.txt; \ + echo 'No new configuration keys found' + +check: lint-docker lint-shell check-config-drift + +clean: + @rm -f generated-config.yaml upstream-config.yaml new-options.txt azure-container-apps.yaml fly.toml hadolint-results.sarif + +smoke-test: build @set -euo pipefail; \ image="$${SMOKE_TEST_IMAGE:-$(DEFAULT_SMOKE_TEST_IMAGE)}"; \ container="$${SMOKE_TEST_CONTAINER:-$(DEFAULT_SMOKE_TEST_CONTAINER)}"; \ @@ -55,8 +218,6 @@ smoke-test: }; \ trap cleanup EXIT; \ docker rm -f "$${container}" >/dev/null 2>&1 || true; \ - echo "Building Docker image: $${image}"; \ - docker build -t "$${image}" .; \ echo "Starting container: $${container}"; \ docker run -d --name "$${container}" \ -p "$${host}:$${port}:8008" \