Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8729cb5062 | ||
|
|
f1e4a1088d | ||
|
|
b357ef95d8 | ||
|
|
67422663b7 | ||
|
|
88705fec88 |
No files matched your search
@@ -3,7 +3,7 @@ name: ci
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- "**"
|
- main
|
||||||
pull_request:
|
pull_request:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
@@ -142,6 +142,7 @@ jobs:
|
|||||||
export PATH="$tools_dir:$PATH"
|
export PATH="$tools_dir:$PATH"
|
||||||
ruff check .
|
ruff check .
|
||||||
ruff format --check .
|
ruff format --check .
|
||||||
|
python3 -m unittest discover -s tests -v
|
||||||
|
|
||||||
lint-yaml:
|
lint-yaml:
|
||||||
runs-on: [self-hosted, linux, arch, homelab]
|
runs-on: [self-hosted, linux, arch, homelab]
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ on:
|
|||||||
# workflow_dispatch so a red lint/validate run can never reach the cluster.
|
# workflow_dispatch so a red lint/validate run can never reach the cluster.
|
||||||
workflow_run:
|
workflow_run:
|
||||||
workflows: [ci]
|
workflows: [ci]
|
||||||
|
branches: [main]
|
||||||
types: [completed]
|
types: [completed]
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,23 @@ name: renovate-ci
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- "renovate/**"
|
||||||
|
- ".gitea/workflows/renovate-ci.yaml"
|
||||||
|
- ".gitea/workflows/sync-renovate-configmap.sh"
|
||||||
|
- ".gitea/workflows/compose-lint.sh"
|
||||||
|
- ".gitea/workflows/install-ci-tools.sh"
|
||||||
|
- ".gitea/workflows/tool-versions.env"
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
|
paths:
|
||||||
|
- "renovate/**"
|
||||||
|
- ".gitea/workflows/renovate-ci.yaml"
|
||||||
|
- ".gitea/workflows/sync-renovate-configmap.sh"
|
||||||
|
- ".gitea/workflows/compose-lint.sh"
|
||||||
|
- ".gitea/workflows/install-ci-tools.sh"
|
||||||
|
- ".gitea/workflows/tool-versions.env"
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
|
|||||||
Executable
+109
@@ -0,0 +1,109 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
umask 077
|
||||||
|
|
||||||
|
TEMPLATE_PATH=/opt/netbird/config.template.yaml
|
||||||
|
RENDERED_PATH=/run/netbird/config.yaml
|
||||||
|
RELAY_SECRET_PATH=/run/secrets/relay_auth_secret
|
||||||
|
ENCRYPTION_KEY_PATH=/run/secrets/datastore_encryption_key
|
||||||
|
|
||||||
|
is_valid_proxy_subnet() {
|
||||||
|
candidate="$1"
|
||||||
|
case "$candidate" in
|
||||||
|
0.0.0.0/0)
|
||||||
|
return 1
|
||||||
|
;;
|
||||||
|
*/*)
|
||||||
|
address="${candidate%%/*}"
|
||||||
|
prefix="${candidate#*/}"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
return 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
case "$prefix" in
|
||||||
|
0|[1-9]|[1-2][0-9]|3[0-2]) ;;
|
||||||
|
*)
|
||||||
|
return 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
old_ifs="$IFS"
|
||||||
|
IFS=.
|
||||||
|
# shellcheck disable=SC2086
|
||||||
|
set -- $address
|
||||||
|
IFS="$old_ifs"
|
||||||
|
[ "$#" -eq 4 ] || return 1
|
||||||
|
|
||||||
|
for octet do
|
||||||
|
case "$octet" in
|
||||||
|
0|[1-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5]) ;;
|
||||||
|
*)
|
||||||
|
return 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
read_secret() {
|
||||||
|
secret_path="$1"
|
||||||
|
|
||||||
|
if [ ! -r "$secret_path" ]; then
|
||||||
|
echo "Required secret is not readable: $secret_path" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
secret_value="$(cat "$secret_path")"
|
||||||
|
if [ -z "$secret_value" ]; then
|
||||||
|
echo "Required secret is empty: $secret_path" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '%s' "$secret_value"
|
||||||
|
}
|
||||||
|
|
||||||
|
if [ -z "${NETBIRD_DOMAIN:-}" ]; then
|
||||||
|
echo "NETBIRD_DOMAIN must be set" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "$NETBIRD_DOMAIN" in
|
||||||
|
*[!A-Za-z0-9.-]*)
|
||||||
|
echo "NETBIRD_DOMAIN contains unsupported characters" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ -z "${NETBIRD_PROXY_SUBNET:-}" ] || [ "$NETBIRD_PROXY_SUBNET" = "auto" ]; then
|
||||||
|
echo "NETBIRD_PROXY_SUBNET must be an explicit IPv4 CIDR; run netbird/setup.sh first" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if ! is_valid_proxy_subnet "$NETBIRD_PROXY_SUBNET"; then
|
||||||
|
echo "NETBIRD_PROXY_SUBNET must be a non-default IPv4 CIDR, for example 172.20.0.0/16" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$#" -ne 2 ] || [ "$1" != "--config" ] || [ "$2" != "$RENDERED_PATH" ]; then
|
||||||
|
echo "Expected: --config $RENDERED_PATH" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
relay_secret="$(read_secret "$RELAY_SECRET_PATH")"
|
||||||
|
encryption_key="$(read_secret "$ENCRYPTION_KEY_PATH")"
|
||||||
|
|
||||||
|
mkdir -p "$(dirname "$RENDERED_PATH")"
|
||||||
|
sed \
|
||||||
|
-e "s|__NETBIRD_DOMAIN__|${NETBIRD_DOMAIN}|g" \
|
||||||
|
-e "s|__NETBIRD_AUTH_SECRET__|${relay_secret}|g" \
|
||||||
|
-e "s|__NETBIRD_ENCRYPTION_KEY__|${encryption_key}|g" \
|
||||||
|
-e "s|__NETBIRD_PROXY_SUBNET__|${NETBIRD_PROXY_SUBNET}|g" \
|
||||||
|
"$TEMPLATE_PATH" >"$RENDERED_PATH"
|
||||||
|
|
||||||
|
if grep -q '__NETBIRD_' "$RENDERED_PATH"; then
|
||||||
|
echo "Rendered NetBird configuration still contains unresolved placeholders" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec /go/bin/netbird-server "$@"
|
||||||
Executable
+38
@@ -0,0 +1,38 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Prepare local Compose configuration without replacing existing credentials.
|
||||||
|
set -euo pipefail
|
||||||
|
cd "$(dirname "${BASH_SOURCE[0]}")"
|
||||||
|
umask 077
|
||||||
|
if [ ! -f .env ]; then
|
||||||
|
cp .env.example .env
|
||||||
|
fi
|
||||||
|
|
||||||
|
if grep -q '^NETBIRD_PROXY_SUBNET=auto$' .env; then
|
||||||
|
subnet="$(docker network inspect proxy --format '{{range .IPAM.Config}}{{println .Subnet}}{{end}}' | awk '/^[0-9]+\./ { print; exit }')"
|
||||||
|
if [ -z "$subnet" ]; then
|
||||||
|
echo "No IPv4 subnet found on the Docker proxy network. Set NETBIRD_PROXY_SUBNET in .env." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
# The detected value must be safe to substitute into the env file.
|
||||||
|
if [[ ! "$subnet" =~ ^[0-9.]+/[0-9]+$ ]]; then
|
||||||
|
echo "Unexpected Docker network subnet: $subnet" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
sed -i "s|^NETBIRD_PROXY_SUBNET=auto$|NETBIRD_PROXY_SUBNET=$subnet|" .env
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p secrets
|
||||||
|
chmod 700 secrets
|
||||||
|
for name in relay-auth-secret datastore-encryption-key; do
|
||||||
|
path="secrets/$name"
|
||||||
|
if [ -e "$path" ]; then
|
||||||
|
if [ ! -s "$path" ]; then
|
||||||
|
echo "Existing secret is empty: $path. Restore it before continuing." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
openssl rand -base64 32 >"$path"
|
||||||
|
fi
|
||||||
|
chmod 600 "$path"
|
||||||
|
done
|
||||||
|
printf '%s\n' 'Local files are ready. Review .env, then run docker compose config --quiet.'
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
"""Exercise local setup and config rendering without a Docker daemon."""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
|
||||||
|
|
||||||
|
class NetbirdRuntimeTests(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.temp = tempfile.TemporaryDirectory()
|
||||||
|
self.addCleanup(self.temp.cleanup)
|
||||||
|
self.root = Path(self.temp.name)
|
||||||
|
self.stack = self.root / 'netbird'
|
||||||
|
self.stack.mkdir()
|
||||||
|
for name in ('setup.sh', '.env.example', 'config.template.yaml'):
|
||||||
|
shutil.copy(ROOT / 'netbird' / name, self.stack / name)
|
||||||
|
binary = self.root / 'bin'
|
||||||
|
binary.mkdir()
|
||||||
|
docker = binary / 'docker'
|
||||||
|
docker.write_text('#!/bin/sh\nprintf "%s\\n" 172.20.0.0/16\n')
|
||||||
|
docker.chmod(0o755)
|
||||||
|
self.env = dict(os.environ, PATH=f'{binary}:{os.environ["PATH"]}')
|
||||||
|
|
||||||
|
def setup(self):
|
||||||
|
return subprocess.run( # noqa: S603 - executes the repository script copied into this test's temp dir
|
||||||
|
['/bin/bash', str(self.stack / 'setup.sh')], env=self.env, capture_output=True, check=False
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_setup_preserves_existing_secrets_and_env(self):
|
||||||
|
self.assertEqual(self.setup().returncode, 0)
|
||||||
|
paths = [self.stack / '.env', *sorted((self.stack / 'secrets').iterdir())]
|
||||||
|
before = [p.read_bytes() for p in paths]
|
||||||
|
self.assertIn(b'NETBIRD_PROXY_SUBNET=172.20.0.0/16', before[0])
|
||||||
|
self.assertEqual(self.setup().returncode, 0)
|
||||||
|
self.assertEqual(before, [p.read_bytes() for p in paths])
|
||||||
|
for p in paths[1:]:
|
||||||
|
self.assertEqual(p.stat().st_mode & 0o777, 0o600)
|
||||||
|
|
||||||
|
def test_setup_rejects_empty_existing_secret(self):
|
||||||
|
(self.stack / 'secrets').mkdir()
|
||||||
|
secret = self.stack / 'secrets/datastore-encryption-key'
|
||||||
|
secret.touch()
|
||||||
|
self.assertNotEqual(self.setup().returncode, 0)
|
||||||
|
self.assertEqual(secret.read_bytes(), b'')
|
||||||
|
|
||||||
|
def render(self, subnet):
|
||||||
|
self.assertEqual(self.setup().returncode, 0)
|
||||||
|
rendered = self.root / 'run/config.yaml'
|
||||||
|
script = (ROOT / 'netbird/entrypoint.sh').read_text()
|
||||||
|
replacements = {
|
||||||
|
'/opt/netbird/config.template.yaml': str(self.stack / 'config.template.yaml'),
|
||||||
|
'/run/netbird/config.yaml': str(rendered),
|
||||||
|
'/run/secrets/relay_auth_secret': str(self.stack / 'secrets/relay-auth-secret'),
|
||||||
|
'/run/secrets/datastore_encryption_key': str(self.stack / 'secrets/datastore-encryption-key'),
|
||||||
|
'/go/bin/netbird-server': '/bin/true',
|
||||||
|
}
|
||||||
|
for original, local in replacements.items():
|
||||||
|
script = script.replace(original, local)
|
||||||
|
result = subprocess.run( # noqa: S603 - repository renderer, with test-local paths
|
||||||
|
['/bin/sh', '-c', script, 'entrypoint', '--config', str(rendered)],
|
||||||
|
env=dict(self.env, NETBIRD_DOMAIN='nb.example.com', NETBIRD_PROXY_SUBNET=subnet),
|
||||||
|
capture_output=True,
|
||||||
|
check=False,
|
||||||
|
)
|
||||||
|
return result, rendered
|
||||||
|
|
||||||
|
def test_renderer_replaces_placeholders_and_restricts_file_permissions(self):
|
||||||
|
result, rendered = self.render('172.20.0.0/16')
|
||||||
|
self.assertEqual(result.returncode, 0, result.stderr)
|
||||||
|
self.assertNotIn('__NETBIRD_', rendered.read_text())
|
||||||
|
self.assertIn('nb.example.com', rendered.read_text())
|
||||||
|
self.assertEqual(rendered.stat().st_mode & 0o777, 0o600)
|
||||||
|
|
||||||
|
def test_renderer_rejects_auto_and_default_route(self):
|
||||||
|
for subnet in ('auto', '0.0.0.0/0', '999.1.1.1/24'):
|
||||||
|
with self.subTest(subnet=subnet):
|
||||||
|
result, _ = self.render(subnet)
|
||||||
|
self.assertNotEqual(result.returncode, 0)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
Reference in new issue
Block a user