diff --git a/poc/README.md b/poc/README.md index 39b477e..7224654 100644 --- a/poc/README.md +++ b/poc/README.md @@ -5,12 +5,13 @@ testing of the NetBird GitOps reconciler. ## Stack overview -| Component | Purpose | -| ---------- | ------------------------------------------- | -| Caddy | TLS termination, reverse proxy | -| NetBird | Management, Signal, Relay, Dashboard, TURN | -| Reconciler | Declarative config → NetBird API reconciler | -| Gitea | Local Git server (optional, off by default) | +| Component | Purpose | +| --------------- | ------------------------------------------- | +| Caddy | TLS termination, reverse proxy | +| NetBird | Management, Signal, Relay, Dashboard, TURN | +| Reconciler | Declarative config → NetBird API reconciler | +| Gitea | Git server for GitOps source-of-truth | +| Gitea Runner | Executes CI workflows (Actions) | All services run as Docker containers on a single VPS, connected via a `netbird` Docker bridge network. Caddy handles ACME certificates automatically. @@ -18,7 +19,9 @@ Docker bridge network. Caddy handles ACME certificates automatically. ## Prerequisites - SSH access to `46.225.220.61` (root, key-based) -- DNS A record: `vps-a.networkmonitor.cc` → `46.225.220.61` +- DNS A records: + - `vps-a.networkmonitor.cc` → `46.225.220.61` + - `gitea.vps-a.networkmonitor.cc` → `46.225.220.61` - `rsync` installed locally (used to sync reconciler source) - Ansible 2.15+ with `community.general` and `ansible.posix` collections @@ -28,22 +31,16 @@ Install collections if needed: ansible-galaxy collection install community.general ansible.posix ``` -## Setup +## Deployment (multi-phase) -### 1. Create vault file +The deployment is intentionally multi-phase because some tokens can only be +obtained after services are running. + +### Phase 1: Initial deploy ```bash cd poc/ansible cp group_vars/all/vault.yml.example group_vars/all/vault.yml -``` - -For the first deploy, leave all values as empty strings — the playbook -auto-generates NetBird secrets and a reconciler token. - -### 2. Deploy - -```bash -cd poc/ansible ansible-playbook -i inventory.yml playbook.yml ``` @@ -51,48 +48,61 @@ The playbook will: 1. Generate secrets (encryption key, TURN password, relay secret, reconciler token) -2. Install Docker if not present -3. Configure UFW firewall -4. Rsync the reconciler source code to VPS-A -5. Template all config files -6. Build the reconciler Docker image on VPS-A -7. Pull NetBird/Gitea images and start all services -8. Run health checks and print a summary with generated secrets +2. Install Docker, configure UFW +3. Rsync the reconciler source code and build the Docker image +4. Template configs and start all services +5. Skip the Gitea Actions runner (no token yet) +6. Print a summary with generated secrets -**Save the generated secrets** printed at the end into `vault.yml` so subsequent -runs are idempotent. +**Save the generated secrets** into `vault.yml` so subsequent runs are +idempotent. -### 3. Create NetBird admin + API token +### Phase 2: Create NetBird admin + API token 1. Open `https://vps-a.networkmonitor.cc` in a browser 2. Create the first admin account (embedded IdP — no external OAuth) 3. Go to **Settings → Personal Access Tokens → Generate** 4. Copy the token into `vault.yml` as `vault_netbird_api_token` -5. Re-run the playbook: + +### Phase 3: Set up Gitea + +1. Open `https://gitea.vps-a.networkmonitor.cc` and complete the install wizard +2. Create an admin account (user: `blastpilot`) +3. Create org `BlastPilot` and repo `netbird-gitops` +4. Generate a Gitea API token (**Settings → Applications**) → `vault_gitea_token` +5. Go to **Site Administration → Actions → Runners** → copy runner registration + token → `vault_gitea_admin_password` and `vault_gitea_runner_token` + +### Phase 4: Re-deploy with all tokens ```bash ansible-playbook -i inventory.yml playbook.yml ``` -The reconciler will now start successfully with a valid API token. +This run will: +- Start the reconciler with a valid NetBird API token +- Register and start the Gitea Actions runner +- Wire the reconciler to poll Gitea for `netbird.json` changes -### 4. (Optional) Enable Gitea +### Phase 5: Push code and test CI -To enable Gitea-backed GitOps polling: +```bash +cd /path/to/netbird-gitops +git remote add poc git@gitea.vps-a.networkmonitor.cc:BlastPilot/netbird-gitops.git +git push poc main +``` -1. Open `http://vps-a.networkmonitor.cc:3000` and complete the install wizard -2. Create an admin account (user: `blastpilot`) -3. Create org `BlastPilot` and repo `netbird-gitops` -4. Push `netbird.json` to the repo -5. Generate a Gitea API token (Settings → Applications) -6. In `vars.yml`, set `gitea_enabled: "true"` -7. In `vault.yml`, fill in `vault_gitea_token` and `vault_gitea_admin_password` -8. Re-run the playbook +Then configure Gitea repo secrets (Settings → Actions → Secrets): +- `RECONCILER_TOKEN` — the reconciler bearer token +- `RECONCILER_URL` — `https://vps-a.networkmonitor.cc/reconciler` +- `GITEA_TOKEN` — same Gitea API token + +Create a branch, modify `netbird.json`, open a PR — the dry-run workflow should +post a plan as a PR comment. ## Testing -All commands below assume you have the reconciler token. Replace `` with -the value of `vault_reconciler_token`. +Replace `` with `vault_reconciler_token`. ### Health check @@ -127,14 +137,6 @@ curl -H "Authorization: Bearer " \ 'https://vps-a.networkmonitor.cc/reconciler/export' ``` -### Enroll a peer - -Use a setup key from the reconcile response (`created_keys` field): - -```bash -sudo netbird up --management-url https://vps-a.networkmonitor.cc --setup-key -``` - ## Teardown Remove all containers and volumes: @@ -143,6 +145,12 @@ Remove all containers and volumes: ssh root@46.225.220.61 "cd /opt/netbird-poc && docker compose down -v" ``` +Stop the runner: + +```bash +ssh root@46.225.220.61 "systemctl stop gitea-runner && systemctl disable gitea-runner" +``` + ## File structure ``` @@ -158,7 +166,7 @@ poc/ templates/ docker-compose.yml.j2 # All services (NetBird + Gitea + Reconciler) management.json.j2 # NetBird management config - Caddyfile.j2 # Caddy reverse proxy with reconciler route + Caddyfile.j2 # Caddy reverse proxy (NetBird + Gitea) dashboard.env.j2 # NetBird dashboard env relay.env.j2 # NetBird relay env turnserver.conf.j2 # TURN server config diff --git a/poc/ansible/group_vars/all/vars.yml b/poc/ansible/group_vars/all/vars.yml index 8837685..9ee0d33 100644 --- a/poc/ansible/group_vars/all/vars.yml +++ b/poc/ansible/group_vars/all/vars.yml @@ -12,15 +12,21 @@ coturn_version: "4.8.0-r0" # --- Reconciler --- reconciler_port: 8080 -# --- Gitea (standalone mode by default) --- -# String "false" because it goes into an env var verbatim. -gitea_enabled: "false" +# --- Gitea --- +gitea_enabled: "true" gitea_version: "1.23" +gitea_domain: "gitea.vps-a.networkmonitor.cc" gitea_http_port: 3000 gitea_ssh_port: 2222 gitea_admin_user: "blastpilot" gitea_org_name: "BlastPilot" gitea_repo_name: "netbird-gitops" +# --- Gitea Actions Runner --- +gitea_runner_version: "0.2.11" +gitea_runner_dir: "/opt/gitea-runner" +gitea_runner_name: "poc-runner" +gitea_runner_labels: "ubuntu-latest:docker://node:20-bookworm,ubuntu-22.04:docker://ubuntu:22.04" + # --- Paths --- base_dir: /opt/netbird-poc diff --git a/poc/ansible/group_vars/all/vault.yml.example b/poc/ansible/group_vars/all/vault.yml.example index e18f32a..a58a24e 100644 --- a/poc/ansible/group_vars/all/vault.yml.example +++ b/poc/ansible/group_vars/all/vault.yml.example @@ -18,3 +18,7 @@ vault_netbird_api_token: "" # Gitea API token (created via Gitea UI after first deploy): vault_gitea_token: "" + +# Gitea Actions runner registration token +# (get from Gitea: Site Administration → Actions → Runners): +vault_gitea_runner_token: "" diff --git a/poc/ansible/playbook.yml b/poc/ansible/playbook.yml index 74ecf56..e153d39 100644 --- a/poc/ansible/playbook.yml +++ b/poc/ansible/playbook.yml @@ -166,10 +166,10 @@ port: "3478" proto: tcp - - name: Allow Gitea HTTP + - name: Allow Gitea SSH community.general.ufw: rule: allow - port: "{{ gitea_http_port | string }}" + port: "{{ gitea_ssh_port | string }}" proto: tcp - name: Enable UFW (default deny incoming) @@ -327,7 +327,88 @@ changed_when: false # ========================================================================= - # 9. Summary + # 9. Gitea Actions Runner + # ========================================================================= + # The runner needs Gitea to be up and a registration token. + # On first deploy, skip this (vault_gitea_runner_token is empty). + # After Gitea is running, get the token from Site Administration → + # Actions → Runners, add it to vault.yml, and re-run. + + - name: Create runner directory + ansible.builtin.file: + path: "{{ gitea_runner_dir }}" + state: directory + mode: "0755" + when: vault_gitea_runner_token | default('') | length > 0 + + - name: Download act_runner binary + ansible.builtin.get_url: + url: "https://gitea.com/gitea/act_runner/releases/download/v{{ gitea_runner_version }}/act_runner-{{ gitea_runner_version }}-linux-amd64" + dest: "{{ gitea_runner_dir }}/act_runner" + mode: "0755" + when: vault_gitea_runner_token | default('') | length > 0 + + - name: Check if runner is already registered + ansible.builtin.stat: + path: "{{ gitea_runner_dir }}/.runner" + register: _runner_config + when: vault_gitea_runner_token | default('') | length > 0 + + - name: Register runner with Gitea + ansible.builtin.command: + cmd: >- + {{ gitea_runner_dir }}/act_runner register + --instance https://{{ gitea_domain }} + --token {{ vault_gitea_runner_token }} + --name {{ gitea_runner_name }} + --labels {{ gitea_runner_labels }} + --no-interactive + chdir: "{{ gitea_runner_dir }}" + when: + - vault_gitea_runner_token | default('') | length > 0 + - not (_runner_config.stat.exists | default(false)) + + - name: Create systemd service for runner + ansible.builtin.copy: + dest: /etc/systemd/system/gitea-runner.service + mode: "0644" + content: | + [Unit] + Description=Gitea Actions Runner + After=network.target docker.service + Requires=docker.service + + [Service] + Type=simple + User=root + WorkingDirectory={{ gitea_runner_dir }} + ExecStart={{ gitea_runner_dir }}/act_runner daemon + Restart=always + RestartSec=10 + + [Install] + WantedBy=multi-user.target + when: vault_gitea_runner_token | default('') | length > 0 + + - name: Start and enable runner service + ansible.builtin.systemd: + name: gitea-runner + daemon_reload: true + state: started + enabled: true + when: vault_gitea_runner_token | default('') | length > 0 + + - name: Skip runner (no token provided) + ansible.builtin.debug: + msg: >- + Skipping Gitea Actions runner — vault_gitea_runner_token is empty. + After Gitea is running, get the token from + https://{{ gitea_domain }}/-/admin/actions/runners + and add it to vault.yml. + when: vault_gitea_runner_token | default('') | length == 0 + + # ========================================================================= + # 10. Summary # ========================================================================= - name: Note about NetBird API token @@ -343,11 +424,12 @@ ansible.builtin.debug: msg: | ============================================================ - NetBird + Reconciler PoC deployed on VPS-A + NetBird + Reconciler + Gitea PoC deployed on VPS-A ============================================================ Dashboard: https://{{ netbird_domain }} - Gitea: http://{{ netbird_domain }}:{{ gitea_http_port }} + Gitea: https://{{ gitea_domain }} + Gitea SSH: ssh://git@{{ gitea_domain }}:{{ gitea_ssh_port }} Reconciler: https://{{ netbird_domain }}/reconciler/health Reconciler status: {{ 'healthy' if (_reconciler_check.status | default(0)) == 200 else 'NOT YET READY (see note above)' }} @@ -362,5 +444,8 @@ 1. Open the dashboard and create an admin account 2. Go to Settings > API > generate a Personal Access Token 3. Put the token in vault.yml as vault_netbird_api_token - 4. Re-run: ansible-playbook -i inventory.yml playbook.yml + 4. Open Gitea, complete install wizard, create org + repo + 5. Go to Site Administration > Actions > Runners, copy token + 6. Put the token in vault.yml as vault_gitea_runner_token + 7. Re-run: ansible-playbook -i inventory.yml playbook.yml ============================================================ diff --git a/poc/ansible/templates/Caddyfile.j2 b/poc/ansible/templates/Caddyfile.j2 index 598443b..f07a403 100644 --- a/poc/ansible/templates/Caddyfile.j2 +++ b/poc/ansible/templates/Caddyfile.j2 @@ -44,3 +44,11 @@ # NetBird Dashboard (catch-all — must be last) reverse_proxy /* dashboard:80 } + +# ============================================================================= +# Gitea +# ============================================================================= +{{ gitea_domain }} { + import security_headers + reverse_proxy gitea:{{ gitea_http_port }} +} diff --git a/poc/ansible/templates/docker-compose.yml.j2 b/poc/ansible/templates/docker-compose.yml.j2 index 16eef0b..7e217c9 100644 --- a/poc/ansible/templates/docker-compose.yml.j2 +++ b/poc/ansible/templates/docker-compose.yml.j2 @@ -96,13 +96,14 @@ services: networks: - netbird environment: - - GITEA__server__DOMAIN={{ netbird_domain }} - - GITEA__server__ROOT_URL=http://{{ netbird_domain }}:{{ gitea_http_port }} + - GITEA__server__DOMAIN={{ gitea_domain }} + - GITEA__server__ROOT_URL=https://{{ gitea_domain }} + - GITEA__server__SSH_DOMAIN={{ gitea_domain }} - GITEA__database__DB_TYPE=sqlite3 + - GITEA__actions__ENABLED=true volumes: - gitea_data:/data ports: - - "{{ gitea_http_port }}:3000" - "{{ gitea_ssh_port }}:22" logging: driver: json-file diff --git a/poc/ansible/templates/reconciler.env.j2 b/poc/ansible/templates/reconciler.env.j2 index 561d494..6ee3848 100644 --- a/poc/ansible/templates/reconciler.env.j2 +++ b/poc/ansible/templates/reconciler.env.j2 @@ -2,11 +2,9 @@ NETBIRD_API_URL=http://management:80/api NETBIRD_API_TOKEN={{ vault_netbird_api_token }} RECONCILER_TOKEN={{ vault_reconciler_token }} GITEA_ENABLED={{ gitea_enabled }} -{% if gitea_enabled == "true" %} GITEA_URL=http://gitea:{{ gitea_http_port }} GITEA_TOKEN={{ vault_gitea_token }} GITEA_REPO={{ gitea_org_name }}/{{ gitea_repo_name }} -{% endif %} POLL_INTERVAL_SECONDS=30 PORT={{ reconciler_port }} DATA_DIR=/data diff --git a/state/test.json b/state/test.json new file mode 100644 index 0000000..f8b2737 --- /dev/null +++ b/state/test.json @@ -0,0 +1,60 @@ +{ + "groups": { + "ground-stations": { + "peers": [] + }, + "pilots": { + "peers": [] + } + }, + "setup_keys": { + "GS-TestHawk-1": { + "type": "one-off", + "expires_in": 604800, + "usage_limit": 1, + "auto_groups": [ + "ground-stations" + ], + "enrolled": false + }, + "Pilot-TestHawk-1": { + "type": "one-off", + "expires_in": 604800, + "usage_limit": 1, + "auto_groups": [ + "pilots" + ], + "enrolled": false + } + }, + "policies": { + "pilots-to-gs": { + "description": "", + "enabled": true, + "sources": [ + "pilots" + ], + "destinations": [ + "ground-stations" + ], + "bidirectional": true, + "protocol": "all", + "action": "accept", + "source_posture_checks": [] + } + }, + "posture_checks": {}, + "networks": {}, + "peers": {}, + "users": { + "admin@example.com": { + "name": "admin", + "role": "owner", + "auto_groups": [] + } + }, + "routes": {}, + "dns": { + "nameserver_groups": {} + } +}