diff --git a/.gitea/workflows/ansible-runner.yaml b/.gitea/workflows/ansible-runner.yaml index e5b35b1..bc4ed3c 100644 --- a/.gitea/workflows/ansible-runner.yaml +++ b/.gitea/workflows/ansible-runner.yaml @@ -1,102 +1,102 @@ -name: ๐ŸƒRun Ansible +name: ๐Ÿƒ Run Ansible on: workflow_call: inputs: playbook_path: + description: 'Path to playbook relative to the role repo (e.g. playbooks/deploy.yml)' required: true type: string role_repo: + description: 'Gitea repository of the role (e.g. ansible/role-samba)' required: true type: string + inventory: + description: 'Inventory file relative to ansible-runner repo' + required: false + type: string + default: 'inventory/raspberries.yaml' + ansible_extra_args: + description: 'Additional Ansible arguments (e.g. --tags install)' + required: false + type: string + default: '' secrets: TOKEN: required: true jobs: - install_ansible: - runs-on: ubuntu-latest + run_ansible: + # Uses the custom ansible-act-runner image with Node, Python and Ansible pre-installed + runs-on: ansible container: - image: cattheinvoker/ubuntu-act-baked:22.04 + image: gitea.mod.home/${{ gitea.repository_owner }}/ansible-act-runner:latest + steps: - - name: ๐Ÿ”Setup SSH for submodules + - name: ๐Ÿ”‘ Setup SSH run: | - echo "Key length: ${#SSH_PRIVATE_KEY}" mkdir -p $HOME/.ssh echo "$SSH_PRIVATE_KEY" | base64 -d > $HOME/.ssh/id_ed25519 chmod 600 $HOME/.ssh/id_ed25519 - ls -laR - # ssh-keyscan -t rsa -p 2222 gitlab.mod.home > $HOME/.ssh/known_hosts - echo "Host *" >> ~/.ssh/config - echo " StrictHostKeyChecking no" >> ~/.ssh/config - echo " UserKnownHostsFile /dev/null" >> ~/.ssh/config - echo "Host gitea.mod.home" >> ~/.ssh/config - echo " port 2222" >> ~/.ssh/config + cat > ~/.ssh/config << 'SSHEOF' + Host * + StrictHostKeyChecking no + UserKnownHostsFile /dev/null + IdentityFile ~/.ssh/id_ed25519 + Host gitea.mod.home + Port 2222 + SSHEOF + chmod 600 ~/.ssh/config env: SSH_PRIVATE_KEY: ${{ secrets.SSHKEY_B64 }} - - name: ๐Ÿ› ๏ธ Install Node.js fallback - run: | - if ! command -v node &> /dev/null; then - echo "Node nicht gefunden. Installiere..." - sudo apt-get update && sudo apt-get install -y nodejs - fi - - - name: ๐Ÿ”ŽCheckout Repository + - name: ๐Ÿ”Ž Checkout ansible-runner (Inventory & Vault) uses: actions/checkout@v4 with: - submodules: recursive + repository: ${{ gitea.repository_owner }}/ansible-runner + token: ${{ secrets.TOKEN }} fetch-depth: 0 - # Python 3 installieren, was fรผr pip und Ansible notwendig ist - - name: โš™๏ธSetup Python - uses: actions/setup-python@v5 - with: - python-version: "3.x" # Wรคhlt die neueste Python 3 Version - - # Abhรคngigkeiten aktualisieren und Ansible รผber pip installieren - - name: โš™๏ธInstall Ansible via pip - run: | - python -m pip install --upgrade pip - pip install ansible - - # vault file anlegen - - name: ๐Ÿ”‘create vault file - run: echo "${{ secrets.ANSIBLE_VAULT_KEY }}" > .vault_pass.txt - - - name: ๐Ÿ“‹Manuelles Submodule Update - run: | - git submodule init - git submodule update --recursive --init --force - - - name: โš™๏ธInstall Ansible roles - run: | - ansible-galaxy role install -r requirements.yml --roles-path ./roles - - - name: ๐Ÿ”ŽCheck Ansible roles exists - run: | - ansible-galaxy list - - - name: ๐Ÿ”Ž Checkout Triggering Role Repo + - name: ๐Ÿ”Ž Checkout Role Repo uses: actions/checkout@v4 with: token: ${{ secrets.TOKEN }} repository: ${{ inputs.role_repo }} path: active_role + fetch-depth: 0 - - name: ๐Ÿ”ŽCheck Ansible Playbook Syntax + - name: ๐Ÿ”‘ Setup Vault Key run: | - ansible-playbook --syntax-check active_role/${{ inputs.playbook_path }} + echo "${{ secrets.ANSIBLE_VAULT_KEY }}" > .vault_pass.txt + chmod 600 .vault_pass.txt - - name: ๐ŸƒRun Ansible deploy_valkey.yml.ansible + - name: ๐Ÿ”Ž Syntax Check run: | - ansible-playbook -i inventory/raspberries.yaml active_role/${{ inputs.playbook_path }} --vault-password-file .vault_pass.txt -v - # Beispiel fรผr den Benachrichtigungsschritt - - name: ๐Ÿ“จTelegram Benachrichtigung senden + ansible-playbook \ + --syntax-check \ + -i ${{ inputs.inventory }} \ + active_role/${{ inputs.playbook_path }} + + - name: ๐Ÿƒ Run Playbook + run: | + ansible-playbook \ + -i ${{ inputs.inventory }} \ + active_role/${{ inputs.playbook_path }} \ + --vault-password-file .vault_pass.txt \ + ${{ inputs.ansible_extra_args }} \ + -v + + - name: ๐Ÿงน Cleanup Secrets + if: always() + run: | + rm -f .vault_pass.txt + rm -f $HOME/.ssh/id_ed25519 + + - name: ๐Ÿ“จ Telegram Notification uses: chapvic/telegram-notify@master - if: always() # Stellt sicher, dass die Benachrichtigung immer gesendet wird + if: always() with: - token: ${{ secrets.TELEGRAM_BOT_TOKEN }} # Ihr Bot-Token Secret - chat: ${{ secrets.TELEGRAM_CHAT_ID }} # Ihre Chat-ID Secret - status: ${{ job.status }} # Sendet den Job-Status (success/failure/cancelled) - title: "Deploy: ${{ inputs.role_repo }}" + token: ${{ secrets.TELEGRAM_BOT_TOKEN }} + chat: ${{ secrets.TELEGRAM_CHAT_ID }} + status: ${{ job.status }} + title: "Deploy: ${{ inputs.role_repo }} โ†’ ${{ inputs.playbook_path }}" diff --git a/.gitea/workflows/build-image.yaml b/.gitea/workflows/build-image.yaml new file mode 100644 index 0000000..35b8e05 --- /dev/null +++ b/.gitea/workflows/build-image.yaml @@ -0,0 +1,73 @@ +name: ๐Ÿณ Build Ansible Act Runner Image + +on: + push: + branches: + - main + paths: + - 'docker/Dockerfile' + workflow_dispatch: + inputs: + force_rebuild: + description: 'Force rebuild without cache' + required: false + default: 'false' + type: boolean + +jobs: + build: + # Runs directly on the runner host to access the DinD sidecar + # DOCKER_HOST=tcp://localhost:2376 is already set via runner configmap + runs-on: docker + + steps: + - name: ๐Ÿ”Ž Checkout + uses: actions/checkout@v4 + + - name: ๐Ÿท๏ธ Set Image Tags + id: tags + run: | + REGISTRY="gitea.mod.home" + ORG="${{ gitea.repository_owner }}" + IMAGE="ansible-act-runner" + SHORT_SHA="${{ gitea.sha }}" + SHORT_SHA="${SHORT_SHA:0:8}" + + echo "image=${REGISTRY}/${ORG}/${IMAGE}" >> $GITHUB_OUTPUT + echo "tag_latest=${REGISTRY}/${ORG}/${IMAGE}:latest" >> $GITHUB_OUTPUT + echo "tag_sha=${REGISTRY}/${ORG}/${IMAGE}:${SHORT_SHA}" >> $GITHUB_OUTPUT + echo "short_sha=${SHORT_SHA}" >> $GITHUB_OUTPUT + + - name: ๐Ÿณ Docker Login โ†’ Gitea Registry + run: | + echo "${{ secrets.REGISTRY_PASSWORD }}" | \ + docker login gitea.mod.home \ + --username "${{ secrets.REGISTRY_USER }}" \ + --password-stdin + + - name: ๐Ÿณ Build Image + run: | + BUILD_ARGS="" + if [ "${{ inputs.force_rebuild }}" = "true" ]; then + BUILD_ARGS="--no-cache" + fi + + docker build ${BUILD_ARGS} \ + -t ${{ steps.tags.outputs.tag_latest }} \ + -t ${{ steps.tags.outputs.tag_sha }} \ + -f docker/Dockerfile \ + docker/ + + - name: ๐Ÿณ Push Image + run: | + docker push ${{ steps.tags.outputs.tag_latest }} + docker push ${{ steps.tags.outputs.tag_sha }} + + - name: ๐Ÿ“จ Telegram Notification + uses: chapvic/telegram-notify@master + if: always() + with: + token: ${{ secrets.TELEGRAM_BOT_TOKEN }} + chat: ${{ secrets.TELEGRAM_CHAT_ID }} + status: ${{ job.status }} + title: "๐Ÿณ Build: ansible-act-runner:${{ steps.tags.outputs.short_sha }}" diff --git a/README.md b/README.md new file mode 100644 index 0000000..d5d0c3d --- /dev/null +++ b/README.md @@ -0,0 +1,173 @@ +# ansible-runner + +Centralized Ansible runner repository for the homelab. Contains the inventory, +vault configuration, SSH credentials, and two reusable Gitea Actions workflows: +one that builds the custom runner image, and one that executes Ansible playbooks +from any role repository. + +--- + +## Repository Structure + +``` +ansible-runner/ +โ”œโ”€โ”€ docker/ +โ”‚ โ””โ”€โ”€ Dockerfile # Custom ansible-act-runner image +โ”œโ”€โ”€ inventory/ +โ”‚ โ””โ”€โ”€ raspberries.yaml # Ansible inventory +โ”œโ”€โ”€ .gitea/ +โ”‚ โ””โ”€โ”€ workflows/ +โ”‚ โ”œโ”€โ”€ build-image.yaml # Builds and pushes the runner image +โ”‚ โ””โ”€โ”€ ansible-runner.yaml # Reusable workflow for all role repos +โ””โ”€โ”€ example-caller-workflow.yaml # Example: how to call from another repo +``` + +--- + +## Workflows + +### `build-image.yaml` โ€” Build the Ansible Act Runner Image + +Triggers automatically when `docker/Dockerfile` changes on `main`, or manually +via `workflow_dispatch` with an optional force-rebuild flag. + +Runs on the `docker` label (directly on the runner host) to access the DinD +sidecar that is configured in the OKD runner pod. Builds and pushes two tags: + +- `gitea.mod.home/ansible/ansible-act-runner:latest` +- `gitea.mod.home/ansible/ansible-act-runner:` + +### `ansible-runner.yaml` โ€” Reusable Ansible Playbook Runner + +A `workflow_call` workflow that can be called from any role repository. +Runs on the `ansible` label using the custom image, which has Node.js, Python, +Ansible, and all required collections pre-installed. + +**Inputs:** + +| Input | Required | Default | Description | +|-------|----------|---------|-------------| +| `role_repo` | โœ… | โ€” | Gitea repo of the role, e.g. `ansible/role-samba` | +| `playbook_path` | โœ… | โ€” | Path to playbook inside the role repo | +| `inventory` | โŒ | `inventory/raspberries.yaml` | Inventory file relative to this repo | +| `ansible_extra_args` | โŒ | `''` | Additional Ansible CLI arguments | + +**Secrets passed through:** + +| Secret | Description | +|--------|-------------| +| `TOKEN` | Gitea access token for checking out private repos | + +--- + +## Calling from a Role Repository + +Place a workflow file in `.gitea/workflows/` of your role repository: + +```yaml +name: ๐Ÿš€ Deploy + +on: + push: + branches: + - main + workflow_dispatch: + +jobs: + deploy: + uses: ansible/ansible-runner/.gitea/workflows/ansible-runner.yaml@main + with: + role_repo: ansible/role-samba + playbook_path: playbooks/deploy.yml + ansible_extra_args: '--tags install' # optional + secrets: + TOKEN: ${{ secrets.TOKEN }} +``` + +--- + +## Required Secrets + +All secrets are configured at the **Organization level** in Gitea +(`ansible` org โ†’ Settings โ†’ Secrets) so they are available to all role +repositories without duplication. + +| Secret | Used in | Description | +|--------|---------|-------------| +| `SSHKEY_B64` | `ansible-runner.yaml` | Base64-encoded ED25519 private key for SSH access to managed hosts | +| `ANSIBLE_VAULT_KEY` | `ansible-runner.yaml` | Ansible Vault password | +| `TOKEN` | `ansible-runner.yaml` | Gitea access token (`repo` scope) for checking out role repos | +| `REGISTRY_USER` | `build-image.yaml` | Gitea username for container registry login | +| `REGISTRY_PASSWORD` | `build-image.yaml` | Gitea access token with `package:write` scope | +| `TELEGRAM_BOT_TOKEN` | both | Telegram bot token for notifications | +| `TELEGRAM_CHAT_ID` | both | Telegram chat ID for notifications | + +### Creating the Gitea Access Token + +In Gitea โ†’ User Settings โ†’ Applications โ†’ Generate Token: +- For `TOKEN`: scopes `repo` (read/write) +- For `GITEA_REGISTRY_PASSWORD`: scope `package` (read/write) + +--- + +## OKD Runner Configuration + +The act runner pod in the OKD cluster (`gitea-act-runner` namespace) runs with +a DinD sidecar. The `build-image.yaml` workflow uses `runs-on: docker` to +execute directly on the runner host where `DOCKER_HOST=tcp://localhost:2376` +is available via the sidecar. + +The `ansible-runner.yaml` workflow uses `runs-on: ansible` and spawns a +container from the custom image. Node.js, Python, Ansible, and all collections +are pre-installed โ€” no runtime installation required. + +### Runner Labels (configured in OKD ConfigMap) + +The runner ConfigMap (`gitea-act-runner-config`) must have the following labels +registered. Without the `docker` label, `build-image.yaml` will not be picked +up by the runner. + +```yaml +# configmap.yaml โ€” labels section +runner: + labels: + - "docker:host" # required for build-image.yaml (runs-on: docker) + - "ansible:host" # required for ansible-runner.yaml (runs-on: ansible) + - "ubuntu-latest:docker://..." +``` + +| Label | `runs-on` value | Purpose | +|-------|----------------|---------| +| `docker:host` | `docker` | Direct host execution with DinD sidecar โ€” used for Docker builds | +| `ansible:host` | `ansible` | Direct host execution โ€” Ansible jobs via container image | +| `ubuntu-latest` | `ubuntu-latest` | Container execution via DinD | + +> **Note:** After changing the ConfigMap labels, the runner pod must re-register. +> Delete the pod to force a restart: `kubectl delete pod -n gitea-act-runner -l app=gitea-act-runner` + +--- + +## Bootstrap: First Image Build + +No manual build from a laptop is required. The existing `gitea/act_runner:latest` +pod already has a DinD sidecar and the `docker:host` label registered, so it can +build and push the custom image itself. + +**Steps:** + +1. Create the repository in Gitea and push all files +2. Set the required secrets in the `ansible` org (see above) +3. Trigger the build manually via `workflow_dispatch` in Gitea Actions UI + +The runner will build the image and push it to `gitea.mod.home/ansible/ansible-act-runner:latest`. +All subsequent builds are triggered automatically when `docker/Dockerfile` changes on `main`. + +--- + +## Roadmap + +- [ ] TLS for Gitea registry via cert-manager (remove insecure flag) +- [ ] Samba AD DC deployment playbook +- [ ] Bind9 DNS backend playbook +- [ ] Windows domain join playbook +- [ ] Fluentbit โ†’ VictoriaLogs for Samba log shipping diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000..afa278c --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,78 @@ +FROM ubuntu:24.04 + +LABEL maintainer="homelab" +LABEL description="Ansible Act Runner - Custom Image" + +ENV DEBIAN_FRONTEND=noninteractive +ENV NODE_VERSION=20 +ENV ANSIBLE_FORCE_COLOR=1 +ENV PIP_NO_CACHE_DIR=1 + +ARG ACT_RUNNER_VERSION=0.2.11 + +RUN apt-get update && apt-get install -y --no-install-recommends \ + curl \ + wget \ + git \ + ca-certificates \ + gnupg \ + unzip \ + jq \ + rsync \ + openssh-client \ + sshpass \ + python3 \ + python3-pip \ + python3-venv \ + python3-dev \ + smbclient \ + krb5-user \ + libkrb5-dev \ + python3-kerberos \ + dnsutils \ + build-essential \ + libssl-dev \ + libffi-dev \ + && rm -rf /var/lib/apt/lists/* + +# Node.js 20 LTS +RUN curl -fsSL https://deb.nodesource.com/setup_${NODE_VERSION}.x | bash - \ + && apt-get install -y --no-install-recommends nodejs \ + && rm -rf /var/lib/apt/lists/* + +# act_runner binary +RUN curl -fsSL \ + https://gitea.com/gitea/act_runner/releases/download/v${ACT_RUNNER_VERSION}/act_runner-${ACT_RUNNER_VERSION}-linux-amd64 \ + -o /usr/local/bin/act_runner \ + && chmod +x /usr/local/bin/act_runner + +# Ansible + pip packages +RUN python3 -m pip install --break-system-packages \ + ansible-core \ + ansible \ + jmespath \ + netaddr \ + passlib \ + cryptography \ + pywinrm \ + requests \ + boto3 + +# Ansible Collections +RUN ansible-galaxy collection install \ + community.general \ + community.crypto \ + ansible.posix \ + kubernetes.core \ + community.windows \ + microsoft.ad + +RUN useradd -m -s /bin/bash runner + +WORKDIR /data + +# Smoke tests +RUN node --version \ + && python3 --version \ + && ansible --version \ + && act_runner --version diff --git a/example-caller-workflow.yaml b/example-caller-workflow.yaml new file mode 100644 index 0000000..54bf5c6 --- /dev/null +++ b/example-caller-workflow.yaml @@ -0,0 +1,18 @@ +name: ๐Ÿš€ Deploy + +on: + push: + branches: + - main + workflow_dispatch: + +jobs: + deploy: + uses: ansible/ansible-runner/.gitea/workflows/ansible-runner.yaml@main + with: + role_repo: ansible/role-samba # role repository to check out + playbook_path: playbooks/deploy.yml # path inside the role repo + inventory: inventory/raspberries.yaml # optional โ€” this is the default + ansible_extra_args: '--tags install' # optional + secrets: + TOKEN: ${{ secrets.TOKEN }}