Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
45867ece38 | ||
|
|
33ed86adf1 | ||
|
|
f86f84ff93 | ||
|
|
b429644d9e | ||
|
|
9fbdce2972 | ||
|
|
1b3ee2c3fd | ||
|
|
1f448f4457 | ||
|
|
7fa8b580d2 | ||
|
|
bf69c110a7 | ||
|
|
a582e8be13 | ||
|
|
e38051a92f | ||
|
|
8162d337b5 | ||
|
|
bac453c435 | ||
|
|
a2010cd914 |
11
.github/workflows/ci.yml
vendored
11
.github/workflows/ci.yml
vendored
@@ -1,12 +1,11 @@
|
|||||||
name: CI
|
name: ci
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
|
||||||
pull_request:
|
pull_request:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
tests:
|
||||||
uses: ./.github/workflows/reusable-test.yml
|
uses: ./.github/workflows/reusable-test.yml
|
||||||
with:
|
|
||||||
python-version: "3.12"
|
|
||||||
matomo-token-description: "ci-token"
|
|
||||||
|
|||||||
74
.github/workflows/publish-image.yml
vendored
Normal file
74
.github/workflows/publish-image.yml
vendored
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
name: publish-image
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- "*"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
tests:
|
||||||
|
uses: ./.github/workflows/reusable-test.yml
|
||||||
|
|
||||||
|
build-and-push:
|
||||||
|
needs: tests
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Extract tag
|
||||||
|
id: meta
|
||||||
|
run: |
|
||||||
|
echo "tag=${GITHUB_REF_NAME}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Check semver tag
|
||||||
|
id: semver
|
||||||
|
run: |
|
||||||
|
if [[ "${GITHUB_REF_NAME}" =~ ^v?[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||||
|
echo "is_semver=true" >> "$GITHUB_OUTPUT"
|
||||||
|
else
|
||||||
|
echo "is_semver=false" >> "$GITHUB_OUTPUT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Log in to GHCR
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Build and push (tag)
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ./Dockerfile
|
||||||
|
push: true
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
tags: |
|
||||||
|
ghcr.io/${{ github.repository }}:${{ steps.meta.outputs.tag }}
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
|
|
||||||
|
- name: Build and push (latest)
|
||||||
|
if: steps.semver.outputs.is_semver == 'true'
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ./Dockerfile
|
||||||
|
push: true
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
tags: |
|
||||||
|
ghcr.io/${{ github.repository }}:latest
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
40
.github/workflows/reusable-test.yml
vendored
40
.github/workflows/reusable-test.yml
vendored
@@ -1,4 +1,4 @@
|
|||||||
name: Reusable Test (ruff + e2e)
|
name: reusable-test
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_call:
|
workflow_call:
|
||||||
@@ -29,10 +29,32 @@ on:
|
|||||||
default: "ci-token"
|
default: "ci-token"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
lint:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 10
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: ${{ inputs.python-version }}
|
||||||
|
cache: "pip"
|
||||||
|
|
||||||
|
- name: Install lint deps
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
pip install ruff
|
||||||
|
|
||||||
|
- name: Ruff
|
||||||
|
run: |
|
||||||
|
ruff check .
|
||||||
|
ruff format --check .
|
||||||
|
|
||||||
|
e2e:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 30
|
timeout-minutes: 30
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
@@ -48,22 +70,16 @@ jobs:
|
|||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install -y curl
|
sudo apt-get install -y curl
|
||||||
|
|
||||||
- name: Install Python deps
|
- name: Install Python deps (editable + e2e)
|
||||||
run: |
|
run: |
|
||||||
python -m pip install --upgrade pip
|
python -m pip install --upgrade pip
|
||||||
pip install -e ".[e2e]"
|
pip install -e ".[e2e]"
|
||||||
pip install ruff
|
|
||||||
|
|
||||||
- name: Ruff
|
|
||||||
run: |
|
|
||||||
ruff check .
|
|
||||||
ruff format --check .
|
|
||||||
|
|
||||||
- name: Install Playwright Chromium
|
- name: Install Playwright Chromium
|
||||||
run: |
|
run: |
|
||||||
python -m playwright install --with-deps chromium
|
python -m playwright install --with-deps --force chromium
|
||||||
|
|
||||||
- name: E2E (docker compose + installer + tests)
|
- name: Run E2E (docker compose)
|
||||||
env:
|
env:
|
||||||
MATOMO_URL: ${{ inputs.matomo-url }}
|
MATOMO_URL: ${{ inputs.matomo-url }}
|
||||||
MATOMO_ADMIN_USER: ${{ inputs.matomo-admin-user }}
|
MATOMO_ADMIN_USER: ${{ inputs.matomo-admin-user }}
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
|
## [1.1.0] - 2025-12-23
|
||||||
|
|
||||||
|
* Implemented bootstrap docker image to auto install matomo in docker compose
|
||||||
|
|
||||||
|
|
||||||
## [1.0.1] - 2025-12-23
|
## [1.0.1] - 2025-12-23
|
||||||
|
|
||||||
* * Support for running `matomo-bootstrap` **fully via Nix** in a clean, containerized environment.
|
* * Support for running `matomo-bootstrap` **fully via Nix** in a clean, containerized environment.
|
||||||
|
|||||||
21
Dockerfile
Normal file
21
Dockerfile
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# Playwright Python image with Chromium + all required OS dependencies
|
||||||
|
# Version should roughly match your playwright requirement
|
||||||
|
FROM mcr.microsoft.com/playwright/python:v1.46.0-jammy
|
||||||
|
|
||||||
|
# Keep stdout clean (token-only), logs go to stderr
|
||||||
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install matomo-bootstrap
|
||||||
|
# Option A: from PyPI (recommended once published)
|
||||||
|
# RUN pip install --no-cache-dir matomo-bootstrap==1.0.1
|
||||||
|
|
||||||
|
# Option B: build from source (current repo)
|
||||||
|
COPY pyproject.toml README.md LICENSE /app/
|
||||||
|
COPY constraints.txt /app/
|
||||||
|
COPY src /app/src
|
||||||
|
RUN pip install --no-cache-dir -c /app/constraints.txt .
|
||||||
|
|
||||||
|
# Default entrypoint: environment-driven bootstrap
|
||||||
|
ENTRYPOINT ["matomo-bootstrap"]
|
||||||
131
Makefile
131
Makefile
@@ -1,4 +1,8 @@
|
|||||||
PYTHON ?= python3
|
PYTHON ?= python3
|
||||||
|
|
||||||
|
# ----------------------------
|
||||||
|
# E2E (existing)
|
||||||
|
# ----------------------------
|
||||||
COMPOSE_FILE := tests/e2e/docker-compose.yml
|
COMPOSE_FILE := tests/e2e/docker-compose.yml
|
||||||
COMPOSE := docker compose -f $(COMPOSE_FILE)
|
COMPOSE := docker compose -f $(COMPOSE_FILE)
|
||||||
|
|
||||||
@@ -13,7 +17,25 @@ MATOMO_ADMIN_PASSWORD ?= AdminSecret123!
|
|||||||
MATOMO_ADMIN_EMAIL ?= administrator@example.org
|
MATOMO_ADMIN_EMAIL ?= administrator@example.org
|
||||||
MATOMO_TOKEN_DESCRIPTION ?= e2e-make-token
|
MATOMO_TOKEN_DESCRIPTION ?= e2e-make-token
|
||||||
|
|
||||||
.PHONY: help venv deps-e2e playwright-install e2e-up e2e-install e2e-test e2e-down e2e logs clean
|
# ----------------------------
|
||||||
|
# Container image (production-like)
|
||||||
|
# ----------------------------
|
||||||
|
IMAGE_NAME ?= ghcr.io/kevinveenbirkenbach/matomo-bootstrap
|
||||||
|
IMAGE_VERSION ?= 1.0.1
|
||||||
|
|
||||||
|
# Optional .env file for container runs
|
||||||
|
ENV_FILE ?= .env
|
||||||
|
|
||||||
|
# ----------------------------
|
||||||
|
# docker-compose stack (Matomo + MariaDB + Bootstrap)
|
||||||
|
# ----------------------------
|
||||||
|
COMPOSE_STACK_FILE ?= docker-compose.yml
|
||||||
|
COMPOSE_STACK := docker compose -f $(COMPOSE_STACK_FILE)
|
||||||
|
|
||||||
|
.PHONY: help \
|
||||||
|
venv deps-e2e playwright-install e2e-up e2e-install e2e-test e2e-down e2e logs clean \
|
||||||
|
image-build image-run image-shell image-push image-clean \
|
||||||
|
stack-up stack-down stack-logs stack-ps stack-bootstrap stack-rebootstrap stack-clean stack-reset
|
||||||
|
|
||||||
help:
|
help:
|
||||||
@echo "Targets:"
|
@echo "Targets:"
|
||||||
@@ -25,11 +47,33 @@ help:
|
|||||||
@echo " e2e-test Run E2E tests (unittest)"
|
@echo " e2e-test Run E2E tests (unittest)"
|
||||||
@echo " e2e-down Stop and remove E2E containers"
|
@echo " e2e-down Stop and remove E2E containers"
|
||||||
@echo " e2e Full cycle: up → install → test → down"
|
@echo " e2e Full cycle: up → install → test → down"
|
||||||
@echo " logs Show Matomo logs"
|
@echo " logs Show Matomo logs (E2E compose)"
|
||||||
@echo " clean Stop containers + remove venv"
|
@echo " clean Stop E2E containers + remove venv"
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo "Variables (override like: make e2e MATOMO_URL=http://127.0.0.1:8081):"
|
@echo "Container image targets:"
|
||||||
@echo " MATOMO_URL, MATOMO_ADMIN_USER, MATOMO_ADMIN_PASSWORD, MATOMO_ADMIN_EMAIL, MATOMO_TOKEN_DESCRIPTION"
|
@echo " image-build Build matomo-bootstrap container image"
|
||||||
|
@echo " image-run Run container bootstrap using $(ENV_FILE) (token-only stdout)"
|
||||||
|
@echo " image-shell Start interactive shell in container"
|
||||||
|
@echo " image-push Push image tags ($(IMAGE_VERSION) + latest)"
|
||||||
|
@echo " image-clean Remove local image tags"
|
||||||
|
@echo ""
|
||||||
|
@echo "docker-compose stack targets (docker-compose.yml):"
|
||||||
|
@echo " stack-up Start MariaDB + Matomo (no bootstrap)"
|
||||||
|
@echo " stack-bootstrap Run one-shot bootstrap (prints token to stdout)"
|
||||||
|
@echo " stack-reset Full reset: down -v → build → up → bootstrap"
|
||||||
|
@echo " stack-down Stop stack"
|
||||||
|
@echo " stack-clean Stop stack and REMOVE volumes (DANGER)"
|
||||||
|
@echo " stack-logs Follow Matomo logs (stack)"
|
||||||
|
@echo " stack-ps Show stack status"
|
||||||
|
@echo ""
|
||||||
|
@echo "Variables:"
|
||||||
|
@echo " E2E: MATOMO_URL, MATOMO_ADMIN_USER, MATOMO_ADMIN_PASSWORD, MATOMO_ADMIN_EMAIL, MATOMO_TOKEN_DESCRIPTION"
|
||||||
|
@echo " IMG: IMAGE_NAME, IMAGE_VERSION, ENV_FILE"
|
||||||
|
@echo " STK: COMPOSE_STACK_FILE"
|
||||||
|
|
||||||
|
# ----------------------------
|
||||||
|
# E2E targets
|
||||||
|
# ----------------------------
|
||||||
|
|
||||||
venv:
|
venv:
|
||||||
@test -x "$(VENV_PY)" || ($(PYTHON) -m venv $(VENV_DIR))
|
@test -x "$(VENV_PY)" || ($(PYTHON) -m venv $(VENV_DIR))
|
||||||
@@ -72,6 +116,11 @@ e2e-test: deps-e2e
|
|||||||
e2e-down:
|
e2e-down:
|
||||||
$(COMPOSE) down -v
|
$(COMPOSE) down -v
|
||||||
|
|
||||||
|
e2e-nix:
|
||||||
|
docker compose -f tests/e2e/docker-compose.yml up -d
|
||||||
|
python3 -m unittest -v tests/e2e/test_bootstrap_nix.py
|
||||||
|
docker compose -f tests/e2e/docker-compose.yml down -v
|
||||||
|
|
||||||
e2e: e2e-up e2e-install e2e-test e2e-down
|
e2e: e2e-up e2e-install e2e-test e2e-down
|
||||||
|
|
||||||
logs:
|
logs:
|
||||||
@@ -79,3 +128,75 @@ logs:
|
|||||||
|
|
||||||
clean: e2e-down
|
clean: e2e-down
|
||||||
rm -rf $(VENV_DIR)
|
rm -rf $(VENV_DIR)
|
||||||
|
|
||||||
|
# ----------------------------
|
||||||
|
# Container image workflow
|
||||||
|
# ----------------------------
|
||||||
|
|
||||||
|
image-build:
|
||||||
|
docker build -t $(IMAGE_NAME):$(IMAGE_VERSION) -t $(IMAGE_NAME):latest .
|
||||||
|
|
||||||
|
image-run:
|
||||||
|
@test -f "$(ENV_FILE)" || (echo "Missing $(ENV_FILE). Create it from env.sample."; exit 1)
|
||||||
|
docker run --rm \
|
||||||
|
--env-file "$(ENV_FILE)" \
|
||||||
|
--network host \
|
||||||
|
$(IMAGE_NAME):$(IMAGE_VERSION)
|
||||||
|
|
||||||
|
image-shell:
|
||||||
|
@test -f "$(ENV_FILE)" || (echo "Missing $(ENV_FILE). Create it from env.sample."; exit 1)
|
||||||
|
docker run --rm -it \
|
||||||
|
--env-file "$(ENV_FILE)" \
|
||||||
|
--network host \
|
||||||
|
--entrypoint /bin/bash \
|
||||||
|
$(IMAGE_NAME):$(IMAGE_VERSION)
|
||||||
|
|
||||||
|
image-push:
|
||||||
|
docker push $(IMAGE_NAME):$(IMAGE_VERSION)
|
||||||
|
docker push $(IMAGE_NAME):latest
|
||||||
|
|
||||||
|
image-clean:
|
||||||
|
docker rmi $(IMAGE_NAME):$(IMAGE_VERSION) $(IMAGE_NAME):latest || true
|
||||||
|
|
||||||
|
# ----------------------------
|
||||||
|
# docker-compose stack workflow
|
||||||
|
# ----------------------------
|
||||||
|
|
||||||
|
## Start MariaDB + Matomo (without bootstrap)
|
||||||
|
stack-up:
|
||||||
|
$(COMPOSE_STACK) up -d db matomo
|
||||||
|
@echo "Matomo is starting on http://127.0.0.1:8080"
|
||||||
|
|
||||||
|
## Run one-shot bootstrap (prints token to stdout)
|
||||||
|
stack-bootstrap:
|
||||||
|
$(COMPOSE_STACK) run --rm bootstrap
|
||||||
|
|
||||||
|
## Re-run bootstrap (forces a fresh one-shot run)
|
||||||
|
stack-rebootstrap:
|
||||||
|
$(COMPOSE_STACK) rm -f bootstrap || true
|
||||||
|
$(COMPOSE_STACK) run --rm bootstrap
|
||||||
|
|
||||||
|
## Follow Matomo logs (stack)
|
||||||
|
stack-logs:
|
||||||
|
$(COMPOSE_STACK) logs -f matomo
|
||||||
|
|
||||||
|
## Show running services (stack)
|
||||||
|
stack-ps:
|
||||||
|
$(COMPOSE_STACK) ps
|
||||||
|
|
||||||
|
## Stop stack
|
||||||
|
stack-down:
|
||||||
|
$(COMPOSE_STACK) down
|
||||||
|
|
||||||
|
## Stop stack and REMOVE volumes (DANGER)
|
||||||
|
stack-clean:
|
||||||
|
$(COMPOSE_STACK) down -v
|
||||||
|
|
||||||
|
## Full reset: down -v → rebuild bootstrap → up → bootstrap
|
||||||
|
stack-reset:
|
||||||
|
$(COMPOSE_STACK) down -v
|
||||||
|
$(COMPOSE_STACK) build --no-cache bootstrap
|
||||||
|
$(COMPOSE_STACK) up -d db matomo
|
||||||
|
@echo "Waiting for Matomo to become reachable..."
|
||||||
|
@sleep 10
|
||||||
|
$(COMPOSE_STACK) run --rm bootstrap
|
||||||
|
|||||||
276
README.md
276
README.md
@@ -1,79 +1,68 @@
|
|||||||
# matomo-bootstrap
|
# matomo-bootstrap
|
||||||
[](https://github.com/sponsors/kevinveenbirkenbach) [](https://www.patreon.com/c/kevinveenbirkenbach) [](https://buymeacoffee.com/kevinveenbirkenbach) [](https://s.veen.world/paypaldonate)
|
|
||||||
|
|
||||||
|
|
||||||
Headless bootstrap tooling for **Matomo**
|
|
||||||
Automates **installation** (via recorded Playwright flow) and **API token provisioning** for fresh Matomo instances.
|
|
||||||
|
|
||||||
This tool is designed for **CI, containers, and reproducible environments**, where no interactive browser access is available.
|
|
||||||
|
|
||||||
|
Headless bootstrap tooling for **Matomo**. Automates **first-time installation** and **API token provisioning** for fresh Matomo instances.
|
||||||
---
|
---
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
* 🚀 **Fully headless Matomo installation**
|
- 🚀 **Fully headless Matomo installation**
|
||||||
|
- Drives the official Matomo web installer using **Playwright**
|
||||||
* Drives the official Matomo web installer using **Playwright**
|
- Automatically skips the installer if Matomo is already installed
|
||||||
* Automatically skips installation if Matomo is already installed
|
- 🔐 **API token provisioning**
|
||||||
* 🔐 **API token provisioning**
|
- Creates an **app-specific token** via an authenticated Matomo session
|
||||||
|
- Compatible with **Matomo 5.3.x** Docker images
|
||||||
* Creates an *app-specific token* via authenticated Matomo session
|
- 🧪 **E2E-tested**
|
||||||
* Compatible with Matomo 5.3.x Docker images
|
- Docker-based end-to-end tests included
|
||||||
* 🧪 **E2E-tested**
|
- ❄️ **First-class Nix support**
|
||||||
|
- Flake-based packaging and pinned `flake.lock`
|
||||||
* Docker-based end-to-end tests included
|
- Uses `nixpkgs` browsers via `playwright-driver` (no Playwright downloads)
|
||||||
* ❄️ **First-class Nix support**
|
- 🧼 **Token-only stdout contract**
|
||||||
|
- **stdout contains only the token** (safe for scripting)
|
||||||
* Flake-based packaging
|
- Logs go to **stderr**
|
||||||
* Reproducible CLI and dev environments
|
|
||||||
* 🐍 **Standard Python CLI**
|
|
||||||
|
|
||||||
* Installable via `pip`
|
|
||||||
* Clean stdout (token only), logs on stderr
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
* A running Matomo instance (e.g. Docker)
|
- A running Matomo instance (e.g. via Docker)
|
||||||
* For fresh installs:
|
- For fresh installs:
|
||||||
|
- Chromium (provided by Playwright or by the Playwright base container image)
|
||||||
* Chromium (managed automatically by Playwright)
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
### Using **Nix** (recommended)
|
### Nix (recommended)
|
||||||
|
|
||||||
If you use **Nix** with flakes:
|
Run directly from the repository:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
nix run github:kevinveenbirkenbach/matomo-bootstrap
|
nix run github:kevinveenbirkenbach/matomo-bootstrap
|
||||||
```
|
```
|
||||||
|
|
||||||
Install Playwright’s Chromium browser (one-time):
|
In Nix mode, browsers are provided via `nixpkgs` (`playwright-driver`) and Playwright downloads are disabled.
|
||||||
|
|
||||||
```bash
|
|
||||||
nix run github:kevinveenbirkenbach/matomo-bootstrap#matomo-bootstrap-playwright-install
|
|
||||||
```
|
|
||||||
|
|
||||||
This installs Chromium into the user cache used by Playwright.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Using **Python / pip**
|
### Python / pip
|
||||||
|
|
||||||
Requires **Python ≥ 3.10**
|
Requires **Python ≥ 3.10**:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pip install matomo-bootstrap
|
pip install matomo-bootstrap
|
||||||
|
python -m playwright install chromium
|
||||||
```
|
```
|
||||||
|
|
||||||
Install Chromium for Playwright:
|
---
|
||||||
|
|
||||||
|
### Docker image (GHCR)
|
||||||
|
|
||||||
|
Pull the prebuilt image:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python -m playwright install chromium
|
docker pull ghcr.io/kevinveenbirkenbach/matomo-bootstrap:stable
|
||||||
|
# or:
|
||||||
|
docker pull ghcr.io/kevinveenbirkenbach/matomo-bootstrap:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -86,11 +75,12 @@ python -m playwright install chromium
|
|||||||
matomo-bootstrap \
|
matomo-bootstrap \
|
||||||
--base-url http://127.0.0.1:8080 \
|
--base-url http://127.0.0.1:8080 \
|
||||||
--admin-user administrator \
|
--admin-user administrator \
|
||||||
--admin-password AdminSecret123! \
|
--admin-password 'AdminSecret123!' \
|
||||||
--admin-email administrator@example.org
|
--admin-email administrator@example.org \
|
||||||
|
--token-description my-ci-token
|
||||||
```
|
```
|
||||||
|
|
||||||
On success, the command prints **only the API token** to stdout:
|
On success, the command prints **only the token** to stdout:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
6c7a8c2b0e9e4a3c8e1d0c4e8a6b9f21
|
6c7a8c2b0e9e4a3c8e1d0c4e8a6b9f21
|
||||||
@@ -98,14 +88,14 @@ On success, the command prints **only the API token** to stdout:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Environment Variables
|
### Environment variables
|
||||||
|
|
||||||
All options can be provided via environment variables:
|
All options can be provided via environment variables:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
export MATOMO_URL=http://127.0.0.1:8080
|
export MATOMO_URL=http://127.0.0.1:8080
|
||||||
export MATOMO_ADMIN_USER=administrator
|
export MATOMO_ADMIN_USER=administrator
|
||||||
export MATOMO_ADMIN_PASSWORD=AdminSecret123!
|
export MATOMO_ADMIN_PASSWORD='AdminSecret123!'
|
||||||
export MATOMO_ADMIN_EMAIL=administrator@example.org
|
export MATOMO_ADMIN_EMAIL=administrator@example.org
|
||||||
export MATOMO_TOKEN_DESCRIPTION=my-ci-token
|
export MATOMO_TOKEN_DESCRIPTION=my-ci-token
|
||||||
|
|
||||||
@@ -114,9 +104,9 @@ matomo-bootstrap
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Debug Mode
|
### Debug mode
|
||||||
|
|
||||||
Enable verbose logs (stderr only):
|
Enable verbose logs (**stderr only**):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
matomo-bootstrap --debug
|
matomo-bootstrap --debug
|
||||||
@@ -124,27 +114,172 @@ matomo-bootstrap --debug
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## How It Works
|
## Docker Compose integration (one-shot bootstrap)
|
||||||
|
|
||||||
1. **Reachability check**
|
### Why “one-shot”?
|
||||||
|
|
||||||
* Waits until Matomo responds over HTTP (any status)
|
The bootstrap container is meant to:
|
||||||
2. **Installation (if needed)**
|
|
||||||
|
|
||||||
* Uses a recorded Playwright flow to complete the Matomo web installer
|
1. Run once,
|
||||||
3. **Authentication**
|
2. Print the token to stdout,
|
||||||
|
3. Exit with code `0`.
|
||||||
|
|
||||||
* Logs in using the `Login.logme` controller
|
You should **not** start it automatically on every `docker compose up`.
|
||||||
4. **Token creation**
|
Instead, start Matomo normally, then run the bootstrap via `docker compose run`.
|
||||||
|
|
||||||
* Calls `UsersManager.createAppSpecificTokenAuth`
|
The cleanest Compose pattern is to put `bootstrap` behind a **profile**.
|
||||||
5. **Output**
|
|
||||||
|
|
||||||
* Prints the token to stdout (safe for scripting)
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## End-to-End Tests
|
### Example `docker-compose.yml` (recommended: `profiles`)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
db:
|
||||||
|
image: mariadb:11
|
||||||
|
container_name: matomo-db
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
MARIADB_DATABASE: matomo
|
||||||
|
MARIADB_USER: matomo
|
||||||
|
MARIADB_PASSWORD: matomo_pw
|
||||||
|
MARIADB_ROOT_PASSWORD: root_pw
|
||||||
|
volumes:
|
||||||
|
- mariadb_data:/var/lib/mysql
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "mariadb-admin ping -uroot -proot_pw --silent"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 60
|
||||||
|
|
||||||
|
matomo:
|
||||||
|
image: matomo:5.3.2
|
||||||
|
container_name: matomo
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
ports:
|
||||||
|
- "${MATOMO_PORT:-8080}:80"
|
||||||
|
environment:
|
||||||
|
MATOMO_DATABASE_HOST: db
|
||||||
|
MATOMO_DATABASE_ADAPTER: mysql
|
||||||
|
MATOMO_DATABASE_USERNAME: matomo
|
||||||
|
MATOMO_DATABASE_PASSWORD: matomo_pw
|
||||||
|
MATOMO_DATABASE_DBNAME: matomo
|
||||||
|
volumes:
|
||||||
|
- matomo_data:/var/www/html
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "wget -qO- http://127.0.0.1/ >/dev/null || exit 1"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 60
|
||||||
|
|
||||||
|
bootstrap:
|
||||||
|
# This prevents automatic startup during a normal `docker compose up`
|
||||||
|
profiles: ["bootstrap"]
|
||||||
|
|
||||||
|
# Option A: use the published image (recommended)
|
||||||
|
image: ghcr.io/kevinveenbirkenbach/matomo-bootstrap:1.0.1
|
||||||
|
|
||||||
|
# Option B: build locally from the repository checkout
|
||||||
|
# build:
|
||||||
|
# context: .
|
||||||
|
# dockerfile: Dockerfile
|
||||||
|
# image: matomo-bootstrap:local
|
||||||
|
|
||||||
|
container_name: matomo-bootstrap
|
||||||
|
depends_on:
|
||||||
|
matomo:
|
||||||
|
condition: service_started
|
||||||
|
environment:
|
||||||
|
# inside the compose network, Matomo is reachable via the service name
|
||||||
|
MATOMO_URL: "http://matomo"
|
||||||
|
|
||||||
|
MATOMO_ADMIN_USER: "administrator"
|
||||||
|
MATOMO_ADMIN_PASSWORD: "AdminSecret123!"
|
||||||
|
MATOMO_ADMIN_EMAIL: "administrator@example.org"
|
||||||
|
MATOMO_TOKEN_DESCRIPTION: "docker-compose-bootstrap"
|
||||||
|
|
||||||
|
# Values used by the recorded installer flow
|
||||||
|
MATOMO_SITE_NAME: "Matomo (docker-compose)"
|
||||||
|
MATOMO_SITE_URL: "http://127.0.0.1:${MATOMO_PORT:-8080}"
|
||||||
|
MATOMO_TIMEZONE: "Germany - Berlin"
|
||||||
|
|
||||||
|
# Optional stability knobs
|
||||||
|
MATOMO_TIMEOUT: "30"
|
||||||
|
MATOMO_PLAYWRIGHT_HEADLESS: "1"
|
||||||
|
MATOMO_PLAYWRIGHT_NAV_TIMEOUT_MS: "60000"
|
||||||
|
MATOMO_PLAYWRIGHT_SLOWMO_MS: "0"
|
||||||
|
|
||||||
|
restart: "no"
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
mariadb_data:
|
||||||
|
matomo_data:
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Commands
|
||||||
|
|
||||||
|
Start DB + Matomo **without** bootstrap:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose up -d db matomo
|
||||||
|
```
|
||||||
|
|
||||||
|
Run bootstrap once (prints token to stdout):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose --profile bootstrap run --rm bootstrap
|
||||||
|
```
|
||||||
|
|
||||||
|
Re-run bootstrap (creates a new token by default):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose --profile bootstrap run --rm bootstrap
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Idempotency / avoiding new tokens on every run
|
||||||
|
|
||||||
|
By default, `UsersManager.createAppSpecificTokenAuth` creates a new token each time.
|
||||||
|
|
||||||
|
If you want strictly idempotent runs in automation, you can provide an existing token
|
||||||
|
and make the bootstrap return it instead of creating a new one:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export MATOMO_BOOTSTRAP_TOKEN_AUTH="0123456789abcdef..."
|
||||||
|
matomo-bootstrap
|
||||||
|
```
|
||||||
|
|
||||||
|
> This is useful for CI re-runs or configuration management tools.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## How it works
|
||||||
|
|
||||||
|
1. **Reachability check**
|
||||||
|
|
||||||
|
* waits until Matomo responds via HTTP (any status is considered “reachable”)
|
||||||
|
2. **Installation (if needed)**
|
||||||
|
|
||||||
|
* uses a recorded Playwright flow to complete the Matomo web installer
|
||||||
|
3. **Authentication**
|
||||||
|
|
||||||
|
* logs in using Matomo’s `Login.logme` controller (cookie session)
|
||||||
|
4. **Token creation**
|
||||||
|
|
||||||
|
* calls `UsersManager.createAppSpecificTokenAuth`
|
||||||
|
5. **Output**
|
||||||
|
|
||||||
|
* prints the token to stdout (token-only contract)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## End-to-end tests
|
||||||
|
|
||||||
Run the full E2E cycle locally:
|
Run the full E2E cycle locally:
|
||||||
|
|
||||||
@@ -157,27 +292,18 @@ This will:
|
|||||||
1. Start Matomo + MariaDB via Docker
|
1. Start Matomo + MariaDB via Docker
|
||||||
2. Install Matomo headlessly
|
2. Install Matomo headlessly
|
||||||
3. Create an API token
|
3. Create an API token
|
||||||
4. Validate the token via Matomo API
|
4. Validate the token via the Matomo API
|
||||||
5. Tear everything down again
|
5. Tear everything down again
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Project Status
|
|
||||||
|
|
||||||
* ✔ Stable for CI / automation
|
|
||||||
* ✔ Tested against Matomo 5.3.x Docker images
|
|
||||||
* ⚠ Installer flow is UI-recorded (robust, but may need updates for future Matomo UI changes)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Author
|
## Author
|
||||||
|
|
||||||
**Kevin Veen-Birkenbach**
|
**Kevin Veen-Birkenbach**
|
||||||
🌐 [https://www.veen.world/](https://www.veen.world/)
|
[https://www.veen.world/](https://www.veen.world/)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
MIT License
|
MIT — see [LICENSE](LICENSE)
|
||||||
See [LICENSE](LICENSE)
|
|
||||||
|
|||||||
1
constraints.txt
Normal file
1
constraints.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
playwright==1.46.0
|
||||||
74
docker-compose.yml
Normal file
74
docker-compose.yml
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
services:
|
||||||
|
db:
|
||||||
|
image: mariadb:11
|
||||||
|
container_name: matomo-db
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
MARIADB_DATABASE: matomo
|
||||||
|
MARIADB_USER: matomo
|
||||||
|
MARIADB_PASSWORD: matomo_pw
|
||||||
|
MARIADB_ROOT_PASSWORD: root_pw
|
||||||
|
volumes:
|
||||||
|
- mariadb_data:/var/lib/mysql
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "mariadb-admin ping -uroot -proot_pw --silent"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 60
|
||||||
|
|
||||||
|
matomo:
|
||||||
|
image: matomo:5.3.2
|
||||||
|
container_name: matomo
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
ports:
|
||||||
|
- "${MATOMO_PORT:-8080}:80"
|
||||||
|
environment:
|
||||||
|
MATOMO_DATABASE_HOST: db
|
||||||
|
MATOMO_DATABASE_ADAPTER: mysql
|
||||||
|
MATOMO_DATABASE_USERNAME: matomo
|
||||||
|
MATOMO_DATABASE_PASSWORD: matomo_pw
|
||||||
|
MATOMO_DATABASE_DBNAME: matomo
|
||||||
|
volumes:
|
||||||
|
- matomo_data:/var/www/html
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "wget -qO- http://127.0.0.1/ >/dev/null || exit 1"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 60
|
||||||
|
|
||||||
|
bootstrap:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
image: matomo-bootstrap:local
|
||||||
|
container_name: matomo-bootstrap
|
||||||
|
depends_on:
|
||||||
|
matomo:
|
||||||
|
condition: service_started
|
||||||
|
environment:
|
||||||
|
MATOMO_URL: "http://matomo"
|
||||||
|
MATOMO_ADMIN_USER: "administrator"
|
||||||
|
MATOMO_ADMIN_PASSWORD: "AdminSecret123!"
|
||||||
|
MATOMO_ADMIN_EMAIL: "administrator@example.org"
|
||||||
|
MATOMO_TOKEN_DESCRIPTION: "docker-compose-bootstrap"
|
||||||
|
|
||||||
|
# Installer flow values
|
||||||
|
MATOMO_SITE_NAME: "Matomo (docker-compose)"
|
||||||
|
MATOMO_SITE_URL: "http://127.0.0.1:${MATOMO_PORT:-8080}"
|
||||||
|
MATOMO_TIMEZONE: "Germany - Berlin"
|
||||||
|
|
||||||
|
# Optional stability knobs
|
||||||
|
MATOMO_TIMEOUT: "30"
|
||||||
|
MATOMO_PLAYWRIGHT_HEADLESS: "1"
|
||||||
|
MATOMO_PLAYWRIGHT_NAV_TIMEOUT_MS: "60000"
|
||||||
|
MATOMO_PLAYWRIGHT_SLOWMO_MS: "0"
|
||||||
|
# bootstrap is a one-shot command that prints the token and exits
|
||||||
|
# if you want to re-run, do: docker compose run --rm bootstrap
|
||||||
|
restart: "no"
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
mariadb_data:
|
||||||
|
matomo_data:
|
||||||
29
env.sample
Normal file
29
env.sample
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
# --- REQUIRED ---
|
||||||
|
MATOMO_URL=http://127.0.0.1:8080
|
||||||
|
MATOMO_ADMIN_USER=administrator
|
||||||
|
MATOMO_ADMIN_PASSWORD=AdminSecret123!
|
||||||
|
MATOMO_ADMIN_EMAIL=administrator@example.org
|
||||||
|
|
||||||
|
# --- OPTIONAL ---
|
||||||
|
# Description for the app-specific token
|
||||||
|
MATOMO_TOKEN_DESCRIPTION=ansible-bootstrap
|
||||||
|
|
||||||
|
# Timeout (seconds)
|
||||||
|
MATOMO_TIMEOUT=30
|
||||||
|
|
||||||
|
# Debug logs to stderr (stdout stays token-only)
|
||||||
|
# MATOMO_DEBUG=1
|
||||||
|
|
||||||
|
# If set, bootstrap will NOT create a new token
|
||||||
|
# but return this one instead (idempotent runs)
|
||||||
|
# MATOMO_BOOTSTRAP_TOKEN_AUTH=0123456789abcdef...
|
||||||
|
|
||||||
|
# Values used by the recorded installer flow
|
||||||
|
MATOMO_SITE_NAME=Matomo
|
||||||
|
MATOMO_SITE_URL=http://127.0.0.1:8080
|
||||||
|
MATOMO_TIMEZONE=Germany - Berlin
|
||||||
|
|
||||||
|
# Playwright knobs (usually not needed)
|
||||||
|
# MATOMO_PLAYWRIGHT_HEADLESS=1
|
||||||
|
# MATOMO_PLAYWRIGHT_NAV_TIMEOUT_MS=60000
|
||||||
|
# MATOMO_PLAYWRIGHT_SLOWMO_MS=0
|
||||||
42
flake.nix
42
flake.nix
@@ -15,17 +15,25 @@
|
|||||||
let
|
let
|
||||||
pkgs = import nixpkgs { inherit system; };
|
pkgs = import nixpkgs { inherit system; };
|
||||||
python = pkgs.python312;
|
python = pkgs.python312;
|
||||||
|
playwrightDriver = pkgs.playwright-driver;
|
||||||
in
|
in
|
||||||
rec {
|
rec {
|
||||||
matomo-bootstrap = python.pkgs.buildPythonApplication {
|
matomo-bootstrap = python.pkgs.buildPythonApplication {
|
||||||
pname = "matomo-bootstrap";
|
pname = "matomo-bootstrap";
|
||||||
version = "1.0.1"; # keep in sync with pyproject.toml
|
version = "1.1.0"; # keep in sync with pyproject.toml
|
||||||
pyproject = true;
|
pyproject = true;
|
||||||
src = self;
|
src = self;
|
||||||
|
|
||||||
nativeBuildInputs = with python.pkgs; [
|
# disable import-check phase (prevents Playwright/installer side effects)
|
||||||
|
pythonImportsCheck = [ ];
|
||||||
|
|
||||||
|
nativeBuildInputs =
|
||||||
|
(with python.pkgs; [
|
||||||
setuptools
|
setuptools
|
||||||
wheel
|
wheel
|
||||||
|
])
|
||||||
|
++ [
|
||||||
|
pkgs.makeWrapper
|
||||||
];
|
];
|
||||||
|
|
||||||
propagatedBuildInputs = with python.pkgs; [
|
propagatedBuildInputs = with python.pkgs; [
|
||||||
@@ -34,6 +42,20 @@
|
|||||||
|
|
||||||
doCheck = false;
|
doCheck = false;
|
||||||
|
|
||||||
|
# IMPORTANT (Nix):
|
||||||
|
# Do NOT let Playwright download ubuntu/fhs browser binaries into ~/.cache/ms-playwright.
|
||||||
|
# Instead, point Playwright to nixpkgs-provided browsers (playwright-driver).
|
||||||
|
#
|
||||||
|
# This fixes errors like:
|
||||||
|
# BrowserType.launch ... headless_shell ENOENT
|
||||||
|
#
|
||||||
|
# ...which happens when Playwright downloads a fallback ubuntu build that cannot run on NixOS.
|
||||||
|
postFixup = ''
|
||||||
|
wrapProgram "$out/bin/matomo-bootstrap" \
|
||||||
|
--set PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD 1 \
|
||||||
|
--set PLAYWRIGHT_BROWSERS_PATH "${playwrightDriver.browsers}"
|
||||||
|
'';
|
||||||
|
|
||||||
meta = with pkgs.lib; {
|
meta = with pkgs.lib; {
|
||||||
description = "Headless bootstrap tooling for Matomo (installation + API token provisioning)";
|
description = "Headless bootstrap tooling for Matomo (installation + API token provisioning)";
|
||||||
homepage = "https://github.com/kevinveenbirkenbach/matomo-bootstrap";
|
homepage = "https://github.com/kevinveenbirkenbach/matomo-bootstrap";
|
||||||
@@ -50,6 +72,7 @@
|
|||||||
let
|
let
|
||||||
pkgs = import nixpkgs { inherit system; };
|
pkgs = import nixpkgs { inherit system; };
|
||||||
python = pkgs.python312;
|
python = pkgs.python312;
|
||||||
|
playwrightDriver = pkgs.playwright-driver;
|
||||||
|
|
||||||
pythonPlaywright = python.withPackages (ps: [
|
pythonPlaywright = python.withPackages (ps: [
|
||||||
ps.playwright
|
ps.playwright
|
||||||
@@ -62,9 +85,20 @@
|
|||||||
runtimeInputs = [ pythonPlaywright ];
|
runtimeInputs = [ pythonPlaywright ];
|
||||||
|
|
||||||
text = ''
|
text = ''
|
||||||
# Install Playwright browsers.
|
# Nix mode: NO browser downloads.
|
||||||
|
#
|
||||||
|
# Playwright upstream "install" downloads ubuntu/fhs browser binaries into ~/.cache/ms-playwright.
|
||||||
|
# Those binaries often don't run on NixOS, producing ENOENT on launch (missing loader/libs).
|
||||||
|
#
|
||||||
|
# We keep this app for backwards-compat (tests/docs call it), but it is intentionally a NO-OP.
|
||||||
|
#
|
||||||
# IMPORTANT: Do not print anything to stdout (tests expect token-only stdout).
|
# IMPORTANT: Do not print anything to stdout (tests expect token-only stdout).
|
||||||
exec ${pythonPlaywright}/bin/python -m playwright install chromium 1>&2
|
{
|
||||||
|
echo "Playwright browsers are provided by nixpkgs (playwright-driver)."
|
||||||
|
echo "Using PLAYWRIGHT_BROWSERS_PATH=${playwrightDriver.browsers}"
|
||||||
|
echo "Set PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 to prevent downloads."
|
||||||
|
} 1>&2
|
||||||
|
exit 0
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
in
|
in
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "matomo-bootstrap"
|
name = "matomo-bootstrap"
|
||||||
version = "1.0.1"
|
version = "1.1.0"
|
||||||
description = "Headless bootstrap tooling for Matomo (installation + API token provisioning)"
|
description = "Headless bootstrap tooling for Matomo (installation + API token provisioning)"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.10"
|
requires-python = ">=3.10"
|
||||||
@@ -12,9 +12,7 @@ authors = [{ name = "Kevin Veen-Birkenbach", email = "kevin@veen.world" }]
|
|||||||
license = { text = "MIT" }
|
license = { text = "MIT" }
|
||||||
urls = { Homepage = "https://github.com/kevinveenbirkenbach/matomo-bootstrap" }
|
urls = { Homepage = "https://github.com/kevinveenbirkenbach/matomo-bootstrap" }
|
||||||
|
|
||||||
dependencies = [
|
dependencies = ["playwright>=1.46.0,<2"]
|
||||||
"playwright>=1.40.0",
|
|
||||||
]
|
|
||||||
|
|
||||||
# Provides a stable CLI name for Nix + pip installs:
|
# Provides a stable CLI name for Nix + pip installs:
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
|
|||||||
135
tests/e2e/test_docker_compose_stack.py
Normal file
135
tests/e2e/test_docker_compose_stack.py
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
import json
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
import unittest
|
||||||
|
import urllib.request
|
||||||
|
|
||||||
|
|
||||||
|
COMPOSE_FILE = os.environ.get("MATOMO_STACK_COMPOSE_FILE", "docker-compose.yml")
|
||||||
|
|
||||||
|
# Pick a non-default port to avoid collisions with other CI stacks that use 8080
|
||||||
|
MATOMO_PORT = os.environ.get("MATOMO_PORT", "18080")
|
||||||
|
MATOMO_HOST_URL = os.environ.get("MATOMO_STACK_URL", f"http://127.0.0.1:{MATOMO_PORT}")
|
||||||
|
|
||||||
|
# How long we wait for Matomo HTTP to respond at all (seconds)
|
||||||
|
WAIT_TIMEOUT_SECONDS = int(os.environ.get("MATOMO_STACK_WAIT_TIMEOUT", "180"))
|
||||||
|
|
||||||
|
|
||||||
|
def _run(
|
||||||
|
cmd: list[str],
|
||||||
|
*,
|
||||||
|
check: bool = True,
|
||||||
|
extra_env: dict[str, str] | None = None,
|
||||||
|
) -> subprocess.CompletedProcess:
|
||||||
|
return subprocess.run(
|
||||||
|
cmd,
|
||||||
|
check=check,
|
||||||
|
env={**os.environ, **(extra_env or {})},
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _compose_cmd(*args: str) -> list[str]:
|
||||||
|
return ["docker", "compose", "-f", COMPOSE_FILE, *args]
|
||||||
|
|
||||||
|
|
||||||
|
def _wait_for_http_any_status(url: str, timeout_s: int) -> None:
|
||||||
|
"""
|
||||||
|
Consider the service "up" once the HTTP server answers anything.
|
||||||
|
urllib raises HTTPError on 4xx/5xx, but that's still "reachable".
|
||||||
|
"""
|
||||||
|
deadline = time.time() + timeout_s
|
||||||
|
last_exc: Exception | None = None
|
||||||
|
|
||||||
|
while time.time() < deadline:
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(url, timeout=2) as resp:
|
||||||
|
_ = resp.read(64)
|
||||||
|
return
|
||||||
|
except Exception as exc: # includes HTTPError
|
||||||
|
last_exc = exc
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
raise RuntimeError(f"Matomo did not become reachable at {url} ({last_exc})")
|
||||||
|
|
||||||
|
|
||||||
|
class TestRootDockerComposeStack(unittest.TestCase):
|
||||||
|
"""
|
||||||
|
E2E test for repository root docker-compose.yml:
|
||||||
|
|
||||||
|
1) docker compose down -v
|
||||||
|
2) docker compose build bootstrap
|
||||||
|
3) docker compose up -d db matomo
|
||||||
|
4) wait for Matomo HTTP on host port (default 8080, overridden here)
|
||||||
|
5) docker compose run --rm bootstrap -> token on stdout
|
||||||
|
6) validate token via Matomo API call
|
||||||
|
7) docker compose down -v (cleanup)
|
||||||
|
"""
|
||||||
|
|
||||||
|
def setUp(self) -> None:
|
||||||
|
# Always start from a clean slate (also clears volumes)
|
||||||
|
_run(
|
||||||
|
_compose_cmd("down", "-v"),
|
||||||
|
check=False,
|
||||||
|
extra_env={"MATOMO_PORT": MATOMO_PORT},
|
||||||
|
)
|
||||||
|
|
||||||
|
def tearDown(self) -> None:
|
||||||
|
# Cleanup even if assertions fail
|
||||||
|
_run(
|
||||||
|
_compose_cmd("down", "-v"),
|
||||||
|
check=False,
|
||||||
|
extra_env={"MATOMO_PORT": MATOMO_PORT},
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_root_docker_compose_yml_stack_bootstraps_and_token_works(self) -> None:
|
||||||
|
# Build bootstrap image from Dockerfile (as defined in docker-compose.yml)
|
||||||
|
build = _run(
|
||||||
|
_compose_cmd("build", "bootstrap"),
|
||||||
|
check=True,
|
||||||
|
extra_env={"MATOMO_PORT": MATOMO_PORT},
|
||||||
|
)
|
||||||
|
self.assertEqual(build.returncode, 0, build.stderr)
|
||||||
|
|
||||||
|
# Start db + matomo (bootstrap is one-shot and started via "run")
|
||||||
|
up = _run(
|
||||||
|
_compose_cmd("up", "-d", "db", "matomo"),
|
||||||
|
check=True,
|
||||||
|
extra_env={"MATOMO_PORT": MATOMO_PORT},
|
||||||
|
)
|
||||||
|
self.assertEqual(up.returncode, 0, up.stderr)
|
||||||
|
|
||||||
|
# Wait until Matomo answers on the published port
|
||||||
|
_wait_for_http_any_status(MATOMO_HOST_URL + "/", WAIT_TIMEOUT_SECONDS)
|
||||||
|
|
||||||
|
# Run bootstrap: it should print ONLY the token to stdout
|
||||||
|
boot = _run(
|
||||||
|
_compose_cmd("run", "--rm", "bootstrap"),
|
||||||
|
check=True,
|
||||||
|
extra_env={"MATOMO_PORT": MATOMO_PORT},
|
||||||
|
)
|
||||||
|
|
||||||
|
token = (boot.stdout or "").strip()
|
||||||
|
self.assertRegex(
|
||||||
|
token,
|
||||||
|
r"^[a-f0-9]{32,64}$",
|
||||||
|
f"Expected token_auth on stdout, got stdout={boot.stdout!r} stderr={boot.stderr!r}",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verify token works against Matomo API
|
||||||
|
api_url = (
|
||||||
|
f"{MATOMO_HOST_URL}/index.php"
|
||||||
|
f"?module=API&method=SitesManager.getSitesWithAtLeastViewAccess"
|
||||||
|
f"&format=json&token_auth={token}"
|
||||||
|
)
|
||||||
|
with urllib.request.urlopen(api_url, timeout=10) as resp:
|
||||||
|
data = json.loads(resp.read().decode("utf-8", errors="replace"))
|
||||||
|
|
||||||
|
self.assertIsInstance(data, list)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user