From 54287ad9a0cb63475f7ebe5bcf8ba6e358997093 Mon Sep 17 00:00:00 2001 From: Fabien ZARIFIAN Date: Tue, 14 Apr 2026 00:17:42 +0200 Subject: [PATCH] feat(ci): add multi-platform CI testing via Docker Add CI infrastructure that tests the Ansible role on every distro declared in meta/main.yml (EL 8/9, Debian bullseye/bookworm, Ubuntu focal/jammy/noble) using Docker containers. - ci/build_matrix.py: parse meta/main.yml platforms into JSON matrix - ci/test_playbook.yml: test playbook (state=present + validate) - ci/Dockerfile.el, ci/Dockerfile.debian: per-family Docker images - Makefile: orchestrator (make test, make test-, make lint) - .gitea/workflows/ci.yml: Gitea Actions with dynamic matrix - .gitlab-ci.yml: GitLab CI pipeline - Jenkinsfile: Jenkins pipeline with parallel stages - .yamllint.yml: linter configuration - .dockerignore: exclude .git and CI configs from Docker context --- .dockerignore | 5 ++++ .gitea/workflows/ci.yml | 43 +++++++++++++++++++++++++++++++ .gitlab-ci.yml | 24 +++++++++++++++++ .yamllint.yml | 10 ++++++++ Jenkinsfile | 35 +++++++++++++++++++++++++ Makefile | 57 +++++++++++++++++++++++++++++++++++++++++ ci/Dockerfile.debian | 14 ++++++++++ ci/Dockerfile.el | 11 ++++++++ ci/build_matrix.py | 56 ++++++++++++++++++++++++++++++++++++++++ ci/test_playbook.yml | 17 ++++++++++++ 10 files changed, 272 insertions(+) create mode 100644 .dockerignore create mode 100644 .gitea/workflows/ci.yml create mode 100644 .gitlab-ci.yml create mode 100644 .yamllint.yml create mode 100644 Jenkinsfile create mode 100644 Makefile create mode 100644 ci/Dockerfile.debian create mode 100644 ci/Dockerfile.el create mode 100644 ci/build_matrix.py create mode 100644 ci/test_playbook.yml diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..49e7b0d --- /dev/null +++ b/.dockerignore @@ -0,0 +1,5 @@ +.git +.gitea +.gitlab-ci.yml +Jenkinsfile +docs/ diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 0000000..952fac2 --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -0,0 +1,43 @@ +--- +name: CI + +on: + push: + branches: ["*"] + pull_request: + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Install linters + run: pip install ansible-lint yamllint + - name: Lint + run: make lint + + matrix: + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.build.outputs.matrix }} + steps: + - uses: actions/checkout@v4 + - name: Install PyYAML + run: pip install pyyaml + - name: Build matrix + id: build + run: echo "matrix=$(python3 ci/build_matrix.py)" >> "$GITHUB_OUTPUT" + + test: + needs: [lint, matrix] + runs-on: ubuntu-latest + strategy: + matrix: + include: ${{ fromJson(needs.matrix.outputs.matrix) }} + fail-fast: false + steps: + - uses: actions/checkout@v4 + - name: Install PyYAML + run: pip install pyyaml + - name: Test ${{ matrix.slug }} + run: make test-${{ matrix.slug }} diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..2edeee6 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,24 @@ +--- +stages: + - lint + - test + +lint: + stage: lint + image: python:3.11-slim + before_script: + - pip install ansible-lint yamllint pyyaml + script: + - make lint + +test: + stage: test + image: docker:latest + services: + - docker:dind + variables: + DOCKER_TLS_CERTDIR: "" + before_script: + - apk add --no-cache python3 py3-pip py3-yaml make bash + script: + - make test diff --git a/.yamllint.yml b/.yamllint.yml new file mode 100644 index 0000000..471437d --- /dev/null +++ b/.yamllint.yml @@ -0,0 +1,10 @@ +--- +extends: default + +rules: + line-length: + max: 120 + truthy: + check-keys: false + comments: + min-spaces-from-content: 1 diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 0000000..17319de --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,35 @@ +pipeline { + agent any + + stages { + stage('Lint') { + steps { + sh 'pip install ansible-lint yamllint pyyaml' + sh 'make lint' + } + } + + stage('Test Matrix') { + steps { + script { + def matrixJson = sh(script: 'python3 ci/build_matrix.py', returnStdout: true).trim() + def matrix = readJSON(text: matrixJson) + def parallelStages = [:] + matrix.each { entry -> + def slug = entry.slug + parallelStages[slug] = { + sh "make test-${slug}" + } + } + parallel parallelStages + } + } + } + } + + post { + always { + sh 'make clean || true' + } + } +} diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..e27a9f8 --- /dev/null +++ b/Makefile @@ -0,0 +1,57 @@ +.PHONY: help matrix lint test clean + +SHELL := /bin/bash +IMAGE_PREFIX := remote-users-fact-test + +help: ## Show this help + @grep -E '^[a-zA-Z_-]+:.*##' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*##"}; {printf " \033[36m%-20s\033[0m %s\n", $$1, $$2}' + +matrix: ## Print the test matrix as JSON + @python3 ci/build_matrix.py + +lint: ## Run yamllint and ansible-lint + yamllint roles/ + ansible-lint + +test: ## Run tests on all distros from the matrix + @python3 ci/build_matrix.py | python3 -c "\ + import sys, json; \ + matrix = json.load(sys.stdin); \ + [print(e['slug'] + ' ' + e['image'] + ' ' + e['platform']) for e in matrix]" | \ + while read slug image platform; do \ + $(MAKE) _test-one SLUG=$$slug IMAGE=$$image PLATFORM=$$platform; \ + done + +test-%: ## Run test on a single distro (e.g. make test-el9) + @python3 ci/build_matrix.py | python3 -c "\ + import sys, json; \ + slug = '$(*)';\ + matrix = json.load(sys.stdin); \ + matches = [e for e in matrix if e['slug'] == slug]; \ + assert matches, f'Unknown slug: {slug}. Valid: {[e[\"slug\"] for e in matrix]}'; \ + e = matches[0]; print(e['slug'] + ' ' + e['image'] + ' ' + e['platform'])" | \ + while read slug image platform; do \ + $(MAKE) _test-one SLUG=$$slug IMAGE=$$image PLATFORM=$$platform; \ + done + +_test-one: + @echo "=== Testing $(SLUG) ($(IMAGE)) ===" + @dockerfile=ci/Dockerfile.el; \ + if [ "$(PLATFORM)" = "Debian" ] || [ "$(PLATFORM)" = "Ubuntu" ]; then \ + dockerfile=ci/Dockerfile.debian; \ + fi; \ + docker build \ + --build-arg "IMAGE=$(IMAGE)" \ + -t "$(IMAGE_PREFIX)-$(SLUG)" \ + -f "$$dockerfile" . \ + && docker run --rm "$(IMAGE_PREFIX)-$(SLUG)" + @echo "=== $(SLUG) OK ===" + +clean: ## Remove test Docker images + @python3 ci/build_matrix.py | python3 -c "\ + import sys, json; \ + [print('$(IMAGE_PREFIX)-' + e['slug']) for e in json.load(sys.stdin)]" | \ + while read img; do \ + docker rmi $$img 2>/dev/null || true; \ + done + @echo "Clean done." diff --git a/ci/Dockerfile.debian b/ci/Dockerfile.debian new file mode 100644 index 0000000..e1d6144 --- /dev/null +++ b/ci/Dockerfile.debian @@ -0,0 +1,14 @@ +ARG IMAGE +FROM ${IMAGE} + +ENV DEBIAN_FRONTEND=noninteractive + +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + ansible iproute2 procps bash sudo \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /workspace +COPY . /workspace + +CMD ["ansible-playbook", "-i", "localhost,", "ci/test_playbook.yml"] diff --git a/ci/Dockerfile.el b/ci/Dockerfile.el new file mode 100644 index 0000000..6961051 --- /dev/null +++ b/ci/Dockerfile.el @@ -0,0 +1,11 @@ +ARG IMAGE +FROM ${IMAGE} + +RUN dnf install -y epel-release \ + && dnf install -y ansible-core iproute procps-ng bash sudo \ + && dnf clean all + +WORKDIR /workspace +COPY . /workspace + +CMD ["ansible-playbook", "-i", "localhost,", "ci/test_playbook.yml"] diff --git a/ci/build_matrix.py b/ci/build_matrix.py new file mode 100644 index 0000000..021b21b --- /dev/null +++ b/ci/build_matrix.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python3 +"""Parse meta/main.yml platforms and output a Docker test matrix as JSON.""" + +import json +import sys +from pathlib import Path + +try: + import yaml +except ImportError: + print("ERROR: PyYAML is required. Install with: pip install pyyaml", file=sys.stderr) + sys.exit(1) + +# Mapping Galaxy platform name -> Docker image prefix +PLATFORM_IMAGE_MAP = { + "EL": "rockylinux", + "Debian": "debian", + "Ubuntu": "ubuntu", +} + +def build_matrix(meta_path: str = "roles/remote_users_fact/meta/main.yml") -> list[dict]: + meta_file = Path(meta_path) + if not meta_file.exists(): + print(f"ERROR: {meta_file} not found", file=sys.stderr) + sys.exit(1) + + with open(meta_file) as f: + meta = yaml.safe_load(f) + + platforms = meta.get("galaxy_info", {}).get("platforms", []) + if not platforms: + print("ERROR: No platforms found in meta/main.yml", file=sys.stderr) + sys.exit(1) + + matrix = [] + for platform in platforms: + name = platform["name"] + if name not in PLATFORM_IMAGE_MAP: + print(f"ERROR: Unknown platform '{name}'. Known: {list(PLATFORM_IMAGE_MAP.keys())}", file=sys.stderr) + sys.exit(1) + + image_prefix = PLATFORM_IMAGE_MAP[name] + for version in platform.get("versions", []): + version_str = str(version) + slug = f"{name.lower()}{version_str}" if name == "EL" else f"{name.lower()}-{version_str}" + matrix.append({ + "slug": slug, + "image": f"{image_prefix}:{version_str}", + "platform": name, + "version": version_str, + }) + + return matrix + +if __name__ == "__main__": + print(json.dumps(build_matrix(), indent=2)) diff --git a/ci/test_playbook.yml b/ci/test_playbook.yml new file mode 100644 index 0000000..b151170 --- /dev/null +++ b/ci/test_playbook.yml @@ -0,0 +1,17 @@ +--- +# ci/test_playbook.yml — Playbook de test CI +# Execute le role avec state=present et validation complete + +- name: Test remote_users_fact + hosts: localhost + connection: local + become: true + gather_facts: true + + vars: + remote_users_fact_state: present + remote_users_fact_validate: true + remote_users_fact_display_summary: true + + roles: + - role: remote_users_fact