diff --git a/.gitignore b/.gitignore index 15d6286..d4e22f7 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,5 @@ build/ *.iml .DS_Store jenkins_home/ +compose/.env +lib/ diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 74fcdfd..0000000 --- a/Dockerfile +++ /dev/null @@ -1,21 +0,0 @@ -FROM jenkins/jenkins:lts-jdk21 - -USER root -RUN apt-get update \ - && apt-get install -y --no-install-recommends \ - ca-certificates \ - curl \ - git \ - maven \ - docker.io \ - docker-compose \ - chromium \ - firefox-esr \ - && rm -rf /var/lib/apt/lists/* -COPY plugins.txt /usr/share/jenkins/ref/plugins.txt -RUN jenkins-plugin-cli --plugin-file /usr/share/jenkins/ref/plugins.txt - -COPY init.groovy.d/ /usr/share/jenkins/ref/init.groovy.d/ -COPY jobs/ /usr/share/jenkins/ref/job-xml/ - -USER jenkins diff --git a/README.md b/README.md index 6bea969..3752f55 100644 --- a/README.md +++ b/README.md @@ -1,59 +1,140 @@ -# OTUS Homework 8: Jenkins Jobs +# OTUS HW8: Jenkins + Ansible + JJB + Docker Slaves -Проект поднимает Jenkins и отдельный Docker daemon (`docker:dind`) в Docker и автоматически создает 2 job: -- `selenium-tests` для Selenium/Selenide тестов по Otus с выбором браузера; -- `mobile-appium-tests` для Appium тестов мобильного приложения с Allure-отчетом. +Полностью автоматизированный проект для ДЗ8: +- Jenkins в Docker; +- reverse proxy (Nginx); +- локальный Docker Registry; +- docker-slaves (агенты Jenkins); +- Jenkins Job Builder (JJB) для автоматического создания/обновления job; +- Ansible playbook для раскатки всего с нуля; +- 6 готовых job, включая runner с параллельным запуском тестов; +- Allure-отчеты для Selenium и Appium job. -## Что входит в проект -- `Dockerfile`, `docker-compose.yml` — Jenkins с предустановленными инструментами и отдельным Docker daemon для job. -- `init.groovy.d/` — автосоздание пользователя `admin/admin`, job и Allure CLI. -- `jobs/selenium-tests.xml` — job для `https://git.kovbasa.ru/otus-autotests/homework_4.git`. -- `jobs/mobile-appium-tests.xml` — job для `https://git.kovbasa.ru/otus-autotests/homework_7.git`. +Проект использует локальные домашки из корня: +- `C:/Users/spawn/IdeaProjects/otus-autotests/homework_4` (Selenium + Citrus); +- `C:/Users/spawn/IdeaProjects/otus-autotests/hw7` (Appium). -## Требования -- Docker и Docker Compose. -- Доступ Jenkins-контейнера в интернет для клонирования репозиториев и загрузки Maven dependencies. -- Для Appium job: - - доступ к БД `jdbc:postgresql://sql.otus.kartushin.su:5432/wishlist`; - - хост, на котором Docker может поднять `budtmo/docker-android`. +## Что развертывается -## Запуск -1. Поднять Jenkins: +### Сервисы (Docker Compose) +- `registry` (`localhost:5005`) — хранение образов тестов и слейвов. +- `jenkins` (`localhost:8081`) — Jenkins controller (JCasC). +- `nginx` (`localhost:8088`) — внешний вход в Jenkins. +- `agent-maven` — swarm-agent с label `maven docker`. +- `agent-jjb` — swarm-agent с label `jjb docker`. +- `jobs_uploader` — one-shot контейнер, который накатывает job через `jenkins-jobs update`. + +### Автоматизация (Ansible) +Playbook: +1. Поднимает `registry + jenkins + nginx`. +2. Ждет готовности Jenkins. +3. Собирает и пушит в registry все нужные образы: + - `localhost:5005/otus/slave-maven:1.0.0` + - `localhost:5005/otus/slave-jjb:1.0.0` + - `localhost:5005/otus/test-selenium:1.0.0` + - `localhost:5005/otus/test-api:1.0.0` + - `localhost:5005/otus/test-mobile:1.0.0` +4. Поднимает слейвы. +5. Накатывает все job через JJB. + +По умолчанию UI через nginx опубликован на `8088`, чтобы не конфликтовать с Selenoid UI из `homework_4` (часто занимает `8080`). + +## 6 Jenkins job + +1. `jobs-uploader` — обновление всех job из `hw8/config/jobs`. +2. `infra-health-check` — проверка инфраструктуры и образов. +3. `qa-selenium-tests` — Selenium/Selenide (browser parameter + Allure). +4. `qa-mobile-appium-tests` — Appium (APP_URL для автоскачивания APK + Allure). +5. `qa-api-citrus-tests` — API (citrus-tests). +6. `qa-runner` — запускает `qa-selenium-tests`, `qa-mobile-appium-tests`, `qa-api-citrus-tests` параллельно. + +Есть вьюхи: +- `DevOps`; +- `QA`. + +## Важные требования окружения +- Docker + Docker Compose V2. +- Ansible. +- Linux/WSL2 host для mobile-части (желательно с `/dev/kvm` для эмуляторов `budtmo/docker-android`). +- Доступ к БД для `hw7` (`DB_URL/DB_USER/DB_PASSWORD`). + +## Быстрый старт с нуля + +### 1. Перейти в `hw8` ```bash -docker compose up -d --build +cd C:/Users/spawn/IdeaProjects/otus-autotests/hw8 ``` -2. Открыть `http://localhost:8081`. -3. Войти под `admin` / `admin`. -4. Убедиться, что автоматически созданы job: - - `selenium-tests` - - `mobile-appium-tests` -Jenkins job запускают Docker не через сокет хоста, а через отдельный сервис `docker`. Это нужно, чтобы mobile job корректно работала с файлами из Jenkins workspace. +### 2. Подготовить `.env` для compose +```bash +cp compose/.env.example compose/.env +``` +Обязательно заполните в `compose/.env`: +- `JENKINS_ADMIN_ID`, `JENKINS_ADMIN_PASSWORD`; +- `MOBILE_DB_PASSWORD` (если не хотите передавать `DB_PASSWORD` руками в параметрах job); +- при необходимости `NGINX_PORT` и `JENKINS_URL_PUBLIC`. -## Запуск job +### 3. Запустить полный деплой playbook +```bash +ansible-playbook -i ansible/inventory/hosts.ini ansible/playbooks/site.yml +``` -### selenium-tests -Запускать через **Build with Parameters**. Рабочие значения по умолчанию: -- `REPO_URL=https://git.kovbasa.ru/otus-autotests/homework_4.git` -- `BRANCH=master` -- `BROWSER=chrome` -- `EXECUTION_MODE=local` -- `HEADLESS=true` +Если запускаете из Windows PowerShell без установленного Ansible: +```powershell +wsl bash -lc "cd /mnt/c/Users/spawn/IdeaProjects/otus-autotests/hw8 && ansible-playbook -i ansible/inventory/hosts.ini ansible/playbooks/site.yml" +``` -### mobile-appium-tests -Запускать через **Build with Parameters**. Рабочие значения по умолчанию: -- `REPO_URL=https://git.kovbasa.ru/otus-autotests/homework_7.git` -- `BRANCH=master` -- `DB_URL=jdbc:postgresql://sql.otus.kartushin.su:5432/wishlist` -- `DB_USER=student` -- `APPIUM_URL=http://docker:4723` +### 4. Открыть Jenkins +- через nginx: `http://localhost:8088` +- напрямую: `http://localhost:8081` -Обязательно заполнить: -- `DB_PASSWORD` +Логин/пароль: из `compose/.env` (`JENKINS_ADMIN_ID`, `JENKINS_ADMIN_PASSWORD`). -Опционально: -- `APP_URL` — если задан, APK будет скачан автоматически; если пусто, используется APK из репозитория `homework_7`. +## Как запускать тесты -## Результат -- Allure-отчет публикуется в каждой job после завершения билда. -- XML job лежат в каталоге `jobs/` и могут быть выданы как результат домашнего задания. +### Рекомендуемый запуск +Запустить `qa-runner` через **Build with Parameters**. + +### Selenium отдельно +Запустить `qa-selenium-tests`: +- `BROWSER`: `chrome|firefox`; +- `HEADLESS`: `true|false`. + +### Appium отдельно +Запустить `qa-mobile-appium-tests`: +- `DB_PASSWORD` (по умолчанию `student`, если пусто — берется из env `MOBILE_DB_PASSWORD` Jenkins controller); +- `APP_URL` по умолчанию: `http://wiremock:8080/wishlist.apk`. +- дефолтные пароли тест-аккаунтов из `hw7`: `user1us/user2us/user3us` (соответствуют логинам). + +`APP_URL` передается в capability `app`, поэтому APK скачивается автоматически Appium-сервером эмулятора. + +## Отчеты +- `qa-selenium-tests` публикует Allure + JUnit. +- `qa-mobile-appium-tests` публикует Allure + JUnit. +- артефакты `target/**` архивируются в Jenkins. + +## Структура проекта + +- `ansible/` — playbook раскатки/удаления. +- `compose/` — инфраструктура Jenkins (compose, Dockerfile, JCasC, nginx, jobs_uploader). +- `compose/images/` — Dockerfile для слейвов и test-runner образов. +- `config/jobs/` — JJB YAML + Groovy pipeline scripts + views. + +## Повторное обновление job + +1. Через Jenkins job `jobs-uploader`, либо +2. Ручным one-shot: +```bash +docker compose -f compose/docker-compose.yml --env-file compose/.env run --rm jobs_uploader +``` + +## Полное удаление стенда +```bash +ansible-playbook -i ansible/inventory/hosts.ini ansible/playbooks/down.yml +``` + +## Чистый повторный запуск (reset + deploy) +```bash +ansible-playbook -i ansible/inventory/hosts.ini ansible/playbooks/down.yml +ansible-playbook -i ansible/inventory/hosts.ini ansible/playbooks/site.yml +``` diff --git a/ansible/ansible.cfg b/ansible/ansible.cfg new file mode 100644 index 0000000..594be38 --- /dev/null +++ b/ansible/ansible.cfg @@ -0,0 +1,5 @@ +[defaults] +inventory = ./inventory/hosts.ini +host_key_checking = False +stdout_callback = yaml +timeout = 30 diff --git a/ansible/inventory/hosts.ini b/ansible/inventory/hosts.ini new file mode 100644 index 0000000..13cfabe --- /dev/null +++ b/ansible/inventory/hosts.ini @@ -0,0 +1,2 @@ +[local] +localhost ansible_connection=local diff --git a/ansible/playbooks/down.yml b/ansible/playbooks/down.yml new file mode 100644 index 0000000..e302db2 --- /dev/null +++ b/ansible/playbooks/down.yml @@ -0,0 +1,16 @@ +--- +- name: Destroy OTUS HW8 Jenkins infrastructure + hosts: local + gather_facts: false + vars: + hw8_root: "{{ playbook_dir }}/../.." + compose_dir: "{{ hw8_root }}/compose" + compose_file: "{{ compose_dir }}/docker-compose.yml" + compose_env_file: "{{ compose_dir }}/.env" + + tasks: + - name: Stop and remove compose stack + ansible.builtin.command: + cmd: docker compose -f {{ compose_file }} --env-file {{ compose_env_file }} down -v --remove-orphans + args: + chdir: "{{ compose_dir }}" diff --git a/ansible/playbooks/site.yml b/ansible/playbooks/site.yml new file mode 100644 index 0000000..1ef12da --- /dev/null +++ b/ansible/playbooks/site.yml @@ -0,0 +1,100 @@ +--- +- name: Deploy OTUS HW8 Jenkins infrastructure + hosts: local + gather_facts: true + vars: + hw8_root: "{{ playbook_dir }}/../.." + compose_dir: "{{ hw8_root }}/compose" + compose_file: "{{ compose_dir }}/docker-compose.yml" + compose_env_file: "{{ compose_dir }}/.env" + + tasks: + - name: Check Docker CLI availability + ansible.builtin.command: docker --version + changed_when: false + + - name: Check Docker Compose availability + ansible.builtin.command: docker compose version + changed_when: false + + - name: Create compose env file from template if missing + ansible.builtin.copy: + src: "{{ compose_dir }}/.env.example" + dest: "{{ compose_env_file }}" + force: true + mode: "0644" + + - name: Ensure image build script is executable + ansible.builtin.file: + path: "{{ compose_dir }}/scripts/build_and_push_images.sh" + mode: "0755" + + - name: Pre-pull Jenkins base image (retry on Docker Hub timeouts) + ansible.builtin.command: + cmd: docker pull jenkins/jenkins:2.541.3-lts-jdk21 + register: pull_jenkins_base + retries: 6 + delay: 20 + until: pull_jenkins_base.rc == 0 + + - name: Build and start registry, jenkins and nginx + ansible.builtin.command: + cmd: docker compose -f {{ compose_file }} --env-file {{ compose_env_file }} up -d --build registry jenkins nginx + args: + chdir: "{{ compose_dir }}" + register: compose_bootstrap + retries: 4 + delay: 20 + until: compose_bootstrap.rc == 0 + + - name: Wait for Jenkins to become available + ansible.builtin.uri: + url: "http://127.0.0.1:8081/login" + status_code: 200 + register: jenkins_ready + retries: 60 + delay: 5 + until: jenkins_ready.status == 200 + + - name: Build and push slave/test images to local registry + ansible.builtin.command: + cmd: ./scripts/build_and_push_images.sh localhost:5005 1.0.0 + args: + chdir: "{{ compose_dir }}" + register: build_and_push_result + retries: 3 + delay: 20 + until: build_and_push_result.rc == 0 + + - name: Start swarm agents + ansible.builtin.command: + cmd: docker compose -f {{ compose_file }} --env-file {{ compose_env_file }} up -d agent-maven agent-jjb + args: + chdir: "{{ compose_dir }}" + + - name: Build jobs_uploader image with latest changes + ansible.builtin.command: + cmd: docker compose -f {{ compose_file }} --env-file {{ compose_env_file }} build jobs_uploader + args: + chdir: "{{ compose_dir }}" + + - name: Remove stale jobs_uploader run containers + ansible.builtin.shell: | + ids="$(docker ps -aq --filter "name=jobs_uploader-run" || true)" + if [ -n "${ids}" ]; then + docker rm -f ${ids} + fi + args: + executable: /bin/bash + + - name: Upload Jenkins jobs via JJB container + ansible.builtin.command: + cmd: timeout 900 docker compose -f {{ compose_file }} --env-file {{ compose_env_file }} run --rm jobs_uploader + args: + chdir: "{{ compose_dir }}" + + - name: Show endpoint details + ansible.builtin.debug: + msg: + - "Jenkins UI: http://localhost:8088 (via nginx) or http://localhost:8081" + - "Registry: http://localhost:5005/v2/" diff --git a/compose/.env.example b/compose/.env.example new file mode 100644 index 0000000..e390bf7 --- /dev/null +++ b/compose/.env.example @@ -0,0 +1,5 @@ +JENKINS_ADMIN_ID=admin +JENKINS_ADMIN_PASSWORD=admin +JENKINS_URL_PUBLIC=http://localhost:8088/ +NGINX_PORT=8088 +MOBILE_DB_PASSWORD=student diff --git a/compose/docker-compose.yml b/compose/docker-compose.yml new file mode 100644 index 0000000..773cbec --- /dev/null +++ b/compose/docker-compose.yml @@ -0,0 +1,122 @@ +services: + registry: + image: registry:2.8.3 + restart: unless-stopped + environment: + REGISTRY_HTTP_ADDR: 0.0.0.0:5000 + ports: + - "5005:5000" + volumes: + - registry_data:/var/lib/registry + networks: + - jenkins_net + + jenkins: + build: + context: ./jenkins + image: local-jenkins:latest + restart: unless-stopped + user: root + environment: + CASC_JENKINS_CONFIG: /var/jenkins_home/casc_configs/jenkins.yaml + JAVA_OPTS: >- + -Djenkins.install.runSetupWizard=false + -Dhudson.model.DownloadService.noSignatureCheck=true + JENKINS_ADMIN_ID: ${JENKINS_ADMIN_ID} + JENKINS_ADMIN_PASSWORD: ${JENKINS_ADMIN_PASSWORD} + JENKINS_URL_PUBLIC: ${JENKINS_URL_PUBLIC} + MOBILE_DB_PASSWORD: ${MOBILE_DB_PASSWORD:-} + ports: + - "8081:8080" + - "50000:50000" + volumes: + - jenkins_home:/var/jenkins_home + - ./jenkins/casc:/var/jenkins_home/casc_configs:ro + - /var/run/docker.sock:/var/run/docker.sock + - ../..:/workspace/otus-autotests:ro + healthcheck: + test: ["CMD-SHELL", "curl -fsS http://127.0.0.1:8080/login >/dev/null"] + interval: 10s + timeout: 5s + retries: 30 + networks: + - jenkins_net + + nginx: + image: nginx:1.28.0 + restart: unless-stopped + depends_on: + jenkins: + condition: service_healthy + ports: + - "${NGINX_PORT:-8088}:80" + volumes: + - ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro + networks: + - jenkins_net + + jobs_uploader: + build: + context: ./jobs_uploader + image: local-jobs-uploader:latest + restart: "no" + depends_on: + jenkins: + condition: service_healthy + environment: + JENKINS_HOSTNAME: http://jenkins:8080 + JENKINS_USERNAME: ${JENKINS_ADMIN_ID} + JENKINS_PASSWORD: ${JENKINS_ADMIN_PASSWORD} + JJB_PATH: /workspace/otus-autotests/hw8/config/jobs + volumes: + - ../..:/workspace/otus-autotests:ro + networks: + - jenkins_net + + agent-maven: + image: localhost:5005/otus/slave-maven:1.0.0 + restart: unless-stopped + depends_on: + jenkins: + condition: service_healthy + environment: + JENKINS_URL: http://jenkins:8080 + JENKINS_USER: ${JENKINS_ADMIN_ID} + JENKINS_PASSWORD: ${JENKINS_ADMIN_PASSWORD} + JENKINS_AGENT_NAME: maven-agent + JENKINS_AGENT_WORKDIR: /home/jenkins/agent + JENKINS_WEB_SOCKET: "true" + JENKINS_LABELS: maven docker + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - ../..:/workspace/otus-autotests:ro + networks: + - jenkins_net + + agent-jjb: + image: localhost:5005/otus/slave-jjb:1.0.0 + restart: unless-stopped + depends_on: + jenkins: + condition: service_healthy + environment: + JENKINS_URL: http://jenkins:8080 + JENKINS_USER: ${JENKINS_ADMIN_ID} + JENKINS_PASSWORD: ${JENKINS_ADMIN_PASSWORD} + JENKINS_AGENT_NAME: jjb-agent + JENKINS_AGENT_WORKDIR: /home/jenkins/agent + JENKINS_WEB_SOCKET: "true" + JENKINS_LABELS: jjb docker + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - ../..:/workspace/otus-autotests:ro + networks: + - jenkins_net + +networks: + jenkins_net: + driver: bridge + +volumes: + jenkins_home: + registry_data: diff --git a/compose/images/slave-jjb/Dockerfile b/compose/images/slave-jjb/Dockerfile new file mode 100644 index 0000000..4df9542 --- /dev/null +++ b/compose/images/slave-jjb/Dockerfile @@ -0,0 +1,19 @@ +FROM eclipse-temurin:21-jre + +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + ca-certificates \ + curl \ + docker.io \ + python3 \ + python3-pip \ + && pip3 install --break-system-packages --no-cache-dir "setuptools<81" "jenkins-job-builder>=6.4.3" \ + && rm -rf /var/lib/apt/lists/* + +RUN curl -fsSL -o /usr/local/bin/swarm-client.jar \ + https://repo.jenkins-ci.org/releases/org/jenkins-ci/plugins/swarm-client/3.51/swarm-client-3.51.jar + +COPY entrypoint.sh /usr/local/bin/entrypoint.sh +RUN chmod +x /usr/local/bin/entrypoint.sh + +ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] diff --git a/compose/images/slave-jjb/entrypoint.sh b/compose/images/slave-jjb/entrypoint.sh new file mode 100644 index 0000000..153bcff --- /dev/null +++ b/compose/images/slave-jjb/entrypoint.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +set -euo pipefail + +: "${JENKINS_URL:?JENKINS_URL is required}" +: "${JENKINS_USER:?JENKINS_USER is required}" +: "${JENKINS_PASSWORD:?JENKINS_PASSWORD is required}" +: "${JENKINS_AGENT_NAME:?JENKINS_AGENT_NAME is required}" + +exec java -jar /usr/local/bin/swarm-client.jar \ + -master "${JENKINS_URL}" \ + -username "${JENKINS_USER}" \ + -password "${JENKINS_PASSWORD}" \ + -name "${JENKINS_AGENT_NAME}" \ + -labels "${JENKINS_LABELS:-jjb docker}" \ + -executors 1 \ + -mode exclusive \ + -disableSslVerification diff --git a/compose/images/slave-maven/Dockerfile b/compose/images/slave-maven/Dockerfile new file mode 100644 index 0000000..518b99e --- /dev/null +++ b/compose/images/slave-maven/Dockerfile @@ -0,0 +1,32 @@ +FROM eclipse-temurin:21-jre + +ARG DOCKER_COMPOSE_VERSION=2.36.2 +ARG ALLURE_VERSION=2.29.0 + +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + ca-certificates \ + curl \ + docker.io \ + unzip \ + && rm -rf /var/lib/apt/lists/* + +RUN curl -fsSL -o /usr/local/bin/swarm-client.jar \ + https://repo.jenkins-ci.org/releases/org/jenkins-ci/plugins/swarm-client/3.51/swarm-client-3.51.jar + +RUN mkdir -p /usr/local/lib/docker/cli-plugins \ + && curl -fsSL -o /usr/local/lib/docker/cli-plugins/docker-compose \ + "https://github.com/docker/compose/releases/download/v${DOCKER_COMPOSE_VERSION}/docker-compose-linux-x86_64" \ + && chmod +x /usr/local/lib/docker/cli-plugins/docker-compose + +RUN curl -fsSL -o /tmp/allure.zip \ + "https://github.com/allure-framework/allure2/releases/download/${ALLURE_VERSION}/allure-${ALLURE_VERSION}.zip" \ + && unzip -q /tmp/allure.zip -d /opt \ + && ln -s "/opt/allure-${ALLURE_VERSION}" /opt/allure \ + && ln -s /opt/allure/bin/allure /usr/local/bin/allure \ + && rm -f /tmp/allure.zip + +COPY entrypoint.sh /usr/local/bin/entrypoint.sh +RUN chmod +x /usr/local/bin/entrypoint.sh + +ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] diff --git a/compose/images/slave-maven/entrypoint.sh b/compose/images/slave-maven/entrypoint.sh new file mode 100644 index 0000000..2477e32 --- /dev/null +++ b/compose/images/slave-maven/entrypoint.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +set -euo pipefail + +: "${JENKINS_URL:?JENKINS_URL is required}" +: "${JENKINS_USER:?JENKINS_USER is required}" +: "${JENKINS_PASSWORD:?JENKINS_PASSWORD is required}" +: "${JENKINS_AGENT_NAME:?JENKINS_AGENT_NAME is required}" + +exec java -jar /usr/local/bin/swarm-client.jar \ + -master "${JENKINS_URL}" \ + -username "${JENKINS_USER}" \ + -password "${JENKINS_PASSWORD}" \ + -name "${JENKINS_AGENT_NAME}" \ + -labels "${JENKINS_LABELS:-maven docker}" \ + -executors 1 \ + -mode exclusive \ + -disableSslVerification diff --git a/compose/images/test-api/Dockerfile b/compose/images/test-api/Dockerfile new file mode 100644 index 0000000..df48a41 --- /dev/null +++ b/compose/images/test-api/Dockerfile @@ -0,0 +1,3 @@ +FROM maven:3.9.11-eclipse-temurin-21 + +WORKDIR /workspace diff --git a/compose/images/test-mobile/Dockerfile b/compose/images/test-mobile/Dockerfile new file mode 100644 index 0000000..df48a41 --- /dev/null +++ b/compose/images/test-mobile/Dockerfile @@ -0,0 +1,3 @@ +FROM maven:3.9.11-eclipse-temurin-21 + +WORKDIR /workspace diff --git a/compose/images/test-selenium/Dockerfile b/compose/images/test-selenium/Dockerfile new file mode 100644 index 0000000..98b0240 --- /dev/null +++ b/compose/images/test-selenium/Dockerfile @@ -0,0 +1,9 @@ +FROM maven:3.9.11-eclipse-temurin-21 + +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + chromium \ + chromium-driver \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /workspace diff --git a/compose/jenkins/Dockerfile b/compose/jenkins/Dockerfile new file mode 100644 index 0000000..6081630 --- /dev/null +++ b/compose/jenkins/Dockerfile @@ -0,0 +1,26 @@ +FROM jenkins/jenkins:2.541.3-lts-jdk21 + +USER root +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + ca-certificates \ + curl \ + git \ + docker.io \ + python3 \ + python3-pip \ + unzip \ + && rm -rf /var/lib/apt/lists/* + +ARG ALLURE_VERSION=2.29.0 +RUN curl -fsSL -o /tmp/allure.zip \ + "https://github.com/allure-framework/allure2/releases/download/${ALLURE_VERSION}/allure-${ALLURE_VERSION}.zip" \ + && unzip -q /tmp/allure.zip -d /opt \ + && ln -s "/opt/allure-${ALLURE_VERSION}" /opt/allure \ + && ln -s /opt/allure/bin/allure /usr/local/bin/allure \ + && rm -f /tmp/allure.zip + +COPY plugins.txt /usr/share/jenkins/ref/plugins.txt +RUN jenkins-plugin-cli --plugin-file /usr/share/jenkins/ref/plugins.txt + +USER jenkins diff --git a/compose/jenkins/casc/jenkins.yaml b/compose/jenkins/casc/jenkins.yaml new file mode 100644 index 0000000..9acd3f2 --- /dev/null +++ b/compose/jenkins/casc/jenkins.yaml @@ -0,0 +1,45 @@ +credentials: + system: + domainCredentials: + - credentials: + - usernamePassword: + id: "jenkins-admin-userpass" + scope: GLOBAL + username: "${JENKINS_ADMIN_ID}" + password: "${JENKINS_ADMIN_PASSWORD}" + +jenkins: + systemMessage: "Jenkins is configured by code (Docker + JCasC + JJB + Ansible)." + numExecutors: 0 + mode: EXCLUSIVE + securityRealm: + local: + allowsSignup: false + users: + - id: "${JENKINS_ADMIN_ID}" + password: "${JENKINS_ADMIN_PASSWORD}" + authorizationStrategy: + loggedInUsersCanDoAnything: + allowAnonymousRead: false + crumbIssuer: + standard: + excludeClientIPFromCrumb: false + globalNodeProperties: + - envVars: + env: + - key: JENKINS_URL_INTERNAL + value: "http://jenkins:8080" + - key: OTUS_WORKSPACE_ROOT + value: "/workspace/otus-autotests" + - key: MOBILE_DB_PASSWORD + value: "${MOBILE_DB_PASSWORD}" + +unclassified: + location: + url: "${JENKINS_URL_PUBLIC}" + +tool: + allure: + installations: + - name: "allure" + home: "/opt/allure" diff --git a/compose/jenkins/plugins.txt b/compose/jenkins/plugins.txt new file mode 100644 index 0000000..ab41be7 --- /dev/null +++ b/compose/jenkins/plugins.txt @@ -0,0 +1,13 @@ +workflow-aggregator +git +credentials +credentials-binding +matrix-project +docker-workflow +allure-jenkins-plugin +configuration-as-code +swarm +ansicolor +timestamper +build-user-vars-plugin +pipeline-utility-steps diff --git a/compose/jobs_uploader/Dockerfile b/compose/jobs_uploader/Dockerfile new file mode 100644 index 0000000..428ec72 --- /dev/null +++ b/compose/jobs_uploader/Dockerfile @@ -0,0 +1,14 @@ +FROM python:3.12-slim + +WORKDIR /opt/jobs_uploader + +RUN apt-get update \ + && apt-get install -y --no-install-recommends curl \ + && rm -rf /var/lib/apt/lists/* \ + && pip install --no-cache-dir jinja2 "setuptools<81" "jenkins-job-builder>=6.4.3" + +COPY entrypoint.sh /opt/jobs_uploader/entrypoint.sh + +RUN chmod +x /opt/jobs_uploader/entrypoint.sh + +ENTRYPOINT ["/opt/jobs_uploader/entrypoint.sh"] diff --git a/compose/jobs_uploader/entrypoint.sh b/compose/jobs_uploader/entrypoint.sh new file mode 100644 index 0000000..2d87b40 --- /dev/null +++ b/compose/jobs_uploader/entrypoint.sh @@ -0,0 +1,29 @@ +#!/bin/sh +set -eu + +export JENKINS_HOSTNAME="${JENKINS_HOSTNAME:?JENKINS_HOSTNAME is required}" +export JENKINS_USERNAME="${JENKINS_USERNAME:?JENKINS_USERNAME is required}" +export JENKINS_PASSWORD="${JENKINS_PASSWORD:?JENKINS_PASSWORD is required}" +export JJB_PATH="${JJB_PATH:?JJB_PATH is required}" + +cat > /tmp/jenkins-job-builder.ini </dev/null; do + sleep 5 +done + +echo "Uploading jobs from ${JJB_PATH}..." +jenkins-jobs --conf /tmp/jenkins-job-builder.ini --flush-cache update "${JJB_PATH}" + +echo "Jobs upload completed." diff --git a/compose/nginx/default.conf b/compose/nginx/default.conf new file mode 100644 index 0000000..c2de70d --- /dev/null +++ b/compose/nginx/default.conf @@ -0,0 +1,16 @@ +server { + listen 80 default_server; + server_name _; + + location / { + proxy_pass http://jenkins:8080; + proxy_set_header Host $http_host; + proxy_set_header X-Forwarded-Host $http_host; + proxy_set_header X-Forwarded-Port $server_port; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_http_version 1.1; + proxy_request_buffering off; + } +} diff --git a/compose/scripts/build_and_push_images.sh b/compose/scripts/build_and_push_images.sh new file mode 100644 index 0000000..3847c22 --- /dev/null +++ b/compose/scripts/build_and_push_images.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash +set -euo pipefail + +REGISTRY="${1:-localhost:5005}" +TAG="${2:-1.0.0}" +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +RETRIES="${RETRIES:-6}" +RETRY_DELAY_SEC="${RETRY_DELAY_SEC:-15}" + +retry_cmd() { + local attempt=1 + local max_attempts="$1" + shift + + until "$@"; do + if [ "${attempt}" -ge "${max_attempts}" ]; then + echo "Command failed after ${attempt} attempts: $*" + return 1 + fi + echo "Attempt ${attempt}/${max_attempts} failed: $*" + attempt=$((attempt + 1)) + sleep "${RETRY_DELAY_SEC}" + done +} + +prepull_base_images() { + local images=( + "eclipse-temurin:21-jre" + "maven:3.9.11-eclipse-temurin-21" + "python:3.12-slim" + ) + for image in "${images[@]}"; do + echo "Pulling base image ${image}" + retry_cmd "${RETRIES}" docker pull "${image}" + done +} + +build_and_push() { + local image_name="$1" + local context_dir="$2" + local full_image="${REGISTRY}/${image_name}:${TAG}" + echo "Building ${full_image}" + retry_cmd "${RETRIES}" docker build -t "${full_image}" "${context_dir}" + echo "Pushing ${full_image}" + retry_cmd "${RETRIES}" docker push "${full_image}" +} + +prepull_base_images +build_and_push "otus/slave-maven" "${ROOT_DIR}/images/slave-maven" +build_and_push "otus/slave-jjb" "${ROOT_DIR}/images/slave-jjb" +build_and_push "otus/test-selenium" "${ROOT_DIR}/images/test-selenium" +build_and_push "otus/test-api" "${ROOT_DIR}/images/test-api" +build_and_push "otus/test-mobile" "${ROOT_DIR}/images/test-mobile" diff --git a/config/compose/mobile-ci.compose.yml b/config/compose/mobile-ci.compose.yml new file mode 100644 index 0000000..32884a8 --- /dev/null +++ b/config/compose/mobile-ci.compose.yml @@ -0,0 +1,52 @@ +services: + wiremock: + image: wiremock/wiremock:3.9.1 + volumes: + - ${PROJECT_DIR}/wiremock:/home/wiremock + healthcheck: + test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:8080/__admin/health | grep -q 'healthy'"] + interval: 10s + timeout: 5s + retries: 12 + + android-emulator-1: + image: budtmo/docker-android:emulator_13.0 + depends_on: + - wiremock + devices: + - /dev/kvm:/dev/kvm + environment: + - DEVICE=Pixel_5 + - APPIUM=true + - WEB_VNC=true + - ENABLE_VNC=true + - AUTO_GRANT_PERMISSIONS=true + - EMULATOR_PARAMS=-no-window -no-audio -gpu swiftshader_indirect -no-snapshot -no-boot-anim + shm_size: 2gb + healthcheck: + test: ["CMD-SHELL", "[ \"$(cat /home/androidusr/device_status 2>/dev/null)\" = \"READY\" ]"] + interval: 15s + timeout: 5s + retries: 40 + start_period: 30s + + android-emulator-2: + image: budtmo/docker-android:emulator_12.0 + depends_on: + - wiremock + devices: + - /dev/kvm:/dev/kvm + environment: + - DEVICE=Pixel_4 + - APPIUM=true + - WEB_VNC=true + - ENABLE_VNC=true + - AUTO_GRANT_PERMISSIONS=true + - EMULATOR_PARAMS=-no-window -no-audio -gpu swiftshader_indirect -no-snapshot -no-boot-anim + shm_size: 2gb + healthcheck: + test: ["CMD-SHELL", "[ \"$(cat /home/androidusr/device_status 2>/dev/null)\" = \"READY\" ]"] + interval: 15s + timeout: 5s + retries: 40 + start_period: 30s diff --git a/config/jobs/global.yaml b/config/jobs/global.yaml new file mode 100644 index 0000000..8eb5f9e --- /dev/null +++ b/config/jobs/global.yaml @@ -0,0 +1,6 @@ +--- +- defaults: + name: global + project_folder: /workspace/otus-autotests + test_image_tag: "1.0.0" + build_keep: 40 diff --git a/config/jobs/macros/common.yaml b/config/jobs/macros/common.yaml new file mode 100644 index 0000000..6ae2084 --- /dev/null +++ b/config/jobs/macros/common.yaml @@ -0,0 +1,7 @@ +--- +- property: + name: hw8-build-policy + properties: + - build-discarder: + num-to-keep: 40 + - disable-concurrent-builds diff --git a/config/jobs/scripts/infra-health-check.groovy b/config/jobs/scripts/infra-health-check.groovy new file mode 100644 index 0000000..3f642e5 --- /dev/null +++ b/config/jobs/scripts/infra-health-check.groovy @@ -0,0 +1,31 @@ +pipeline { + agent { label 'jjb' } + options { + timestamps() + ansiColor('xterm') + } + stages { + stage('Check Docker & Registry') { + steps { + sh ''' + set -eux + docker version + curl -fsS http://registry:5000/v2/_catalog + docker pull localhost:5005/otus/test-selenium:1.0.0 + docker pull localhost:5005/otus/test-api:1.0.0 + docker pull localhost:5005/otus/test-mobile:1.0.0 + ''' + } + } + stage('Check Sources') { + steps { + sh ''' + set -eux + test -f /workspace/otus-autotests/homework_4/pom.xml + test -f /workspace/otus-autotests/hw7/pom.xml + test -f /workspace/otus-autotests/hw8/config/jobs/global.yaml + ''' + } + } + } +} diff --git a/config/jobs/scripts/jobs-uploader.groovy b/config/jobs/scripts/jobs-uploader.groovy new file mode 100644 index 0000000..7db1cc2 --- /dev/null +++ b/config/jobs/scripts/jobs-uploader.groovy @@ -0,0 +1,44 @@ +pipeline { + agent { label 'jjb' } + options { + timestamps() + ansiColor('xterm') + } + stages { + stage('Validate Input') { + steps { + sh ''' + set -eux + test -d "${JJB_PATH}" + test -f "${JJB_PATH}/global.yaml" + ''' + } + } + stage('Deploy Jobs') { + steps { + withCredentials([usernamePassword( + credentialsId: 'jenkins-admin-userpass', + usernameVariable: 'J_USER', + passwordVariable: 'J_PASS' + )]) { + sh ''' + set -eux + cat > /tmp/jenkins-job-builder.ini </dev/null 2>&1 || true + } + trap cleanup_container EXIT INT TERM + tar -C /workspace/otus-autotests/homework_4 -cf - . | docker cp - "${CID}:/workspace" + set +e + docker start -a "${CID}" + TEST_RC=$? + docker cp "${CID}:/workspace/citrus-tests/target" "./artifacts/citrus-target" || true + trap - EXIT INT TERM + docker rm -f "${CID}" || true + exit "${TEST_RC}" + ''' + } + } + } + post { + always { + junit allowEmptyResults: true, testResults: 'artifacts/citrus-target/surefire-reports/*.xml' + archiveArtifacts allowEmptyArchive: true, artifacts: 'artifacts/citrus-target/**' + } + } +} diff --git a/config/jobs/scripts/qa-mobile-appium-tests.groovy b/config/jobs/scripts/qa-mobile-appium-tests.groovy new file mode 100644 index 0000000..efeb28d --- /dev/null +++ b/config/jobs/scripts/qa-mobile-appium-tests.groovy @@ -0,0 +1,148 @@ +pipeline { + agent { label 'maven' } + options { + timestamps() + ansiColor('xterm') + } + stages { + stage('Prepare Workspace') { + steps { + sh ''' + set -eux + rm -rf ./project + mkdir -p ./project + cp -a /workspace/otus-autotests/hw7/. ./project/ + ''' + } + } + stage('Start Mobile Environment') { + steps { + dir('project') { + sh ''' + set -eux + COMPOSE_FILE="/workspace/otus-autotests/hw8/config/compose/mobile-ci.compose.yml" + if docker compose version >/dev/null 2>&1; then + compose_cmd() { PROJECT_DIR="$PWD" docker compose -f "${COMPOSE_FILE}" "$@"; } + elif docker-compose version >/dev/null 2>&1; then + compose_cmd() { PROJECT_DIR="$PWD" docker-compose -f "${COMPOSE_FILE}" "$@"; } + else + echo "Neither docker compose plugin nor docker-compose binary is available" + exit 1 + fi + export COMPOSE_PROJECT_NAME=mobileci + compose_cmd down -v --remove-orphans || true + compose_cmd up -d wiremock android-emulator-1 android-emulator-2 + + EMULATORS="" + for service in android-emulator-1 android-emulator-2; do + cid="$(compose_cmd ps -q "$service")" + if [ -z "${cid}" ]; then + echo "Container for ${service} not found, skipping" + continue + fi + healthy="false" + for i in $(seq 1 80); do + status="$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}starting{{end}}' "${cid}" || true)" + if [ "${status}" = "healthy" ]; then + healthy="true" + break + fi + sleep 10 + done + + if [ "${healthy}" = "true" ]; then + if [ -n "${EMULATORS}" ]; then + EMULATORS="${EMULATORS}," + fi + EMULATORS="${EMULATORS}${service}|http://${service}:4723|Android Emulator|${APP_URL}" + else + echo "Service ${service} is not healthy in time, excluding from test run" + docker logs "${cid}" || true + fi + done + + if [ -z "${EMULATORS}" ]; then + echo "No healthy emulators available" + exit 1 + fi + + printf '%s' "${EMULATORS}" > .mobile_emulators.txt + ''' + } + } + } + stage('Run Appium Tests In Docker') { + steps { + dir('project') { + sh ''' + set -eux + rm -rf ./target + MOBILE_EMULATORS_VALUE="$(cat ./.mobile_emulators.txt)" + DB_PASSWORD_EFFECTIVE="${DB_PASSWORD:-${MOBILE_DB_PASSWORD:-}}" + if [ -z "${DB_PASSWORD_EFFECTIVE}" ]; then + echo "DB password is not set. Provide DB_PASSWORD job parameter or MOBILE_DB_PASSWORD env variable." + exit 1 + fi + CID="$(docker create \ + --network mobileci_default \ + -e DB_URL="${DB_URL:-}" \ + -e DB_USER="${DB_USER:-}" \ + -e DB_PASSWORD="${DB_PASSWORD_EFFECTIVE}" \ + -e APP_URL="${APP_URL:-}" \ + -e MOBILE_EMULATORS="${MOBILE_EMULATORS_VALUE}" \ + -e WISHLISTS_USERNAME="${WISHLISTS_USERNAME:-}" \ + -e WISHLISTS_PASSWORD="${WISHLISTS_PASSWORD:-}" \ + -e GIFTS_USERNAME="${GIFTS_USERNAME:-}" \ + -e GIFTS_PASSWORD="${GIFTS_PASSWORD:-}" \ + -e RESERVATION_USERNAME="${RESERVATION_USERNAME:-}" \ + -e RESERVATION_PASSWORD="${RESERVATION_PASSWORD:-}" \ + -e RESERVATION_OWNER="${RESERVATION_OWNER:-}" \ + localhost:5005/otus/test-mobile:1.0.0 \ + bash -lc "set -e; cd /workspace; timeout 1800s mvn -Dallure.results.directory=target/allure-results test")" + cleanup_container() { + docker rm -f "${CID}" >/dev/null 2>&1 || true + } + trap cleanup_container EXIT INT TERM + tar -C "$PWD" -cf - . | docker cp - "${CID}:/workspace" + set +e + docker start -a "${CID}" + TEST_RC=$? + docker cp "${CID}:/workspace/target" "./target" || true + trap - EXIT INT TERM + docker rm -f "${CID}" || true + exit "${TEST_RC}" + ''' + } + } + } + } + post { + always { + dir('project') { + sh ''' + set +e + COMPOSE_FILE="/workspace/otus-autotests/hw8/config/compose/mobile-ci.compose.yml" + if docker compose version >/dev/null 2>&1; then + compose_cmd() { PROJECT_DIR="$PWD" docker compose -f "${COMPOSE_FILE}" "$@"; } + elif docker-compose version >/dev/null 2>&1; then + compose_cmd() { PROJECT_DIR="$PWD" docker-compose -f "${COMPOSE_FILE}" "$@"; } + else + echo "compose command is not available for cleanup" + exit 0 + fi + export COMPOSE_PROJECT_NAME=mobileci + compose_cmd down -v --remove-orphans || true + ''' + } + script { + try { + allure commandline: 'allure', includeProperties: false, jdk: '', results: [[path: 'project/target/allure-results']] + } catch (Exception ex) { + echo "Allure publisher unavailable: ${ex.message}" + } + } + junit allowEmptyResults: true, testResults: 'project/target/surefire-reports/*.xml' + archiveArtifacts allowEmptyArchive: true, artifacts: 'project/target/**' + } + } +} diff --git a/config/jobs/scripts/qa-runner.groovy b/config/jobs/scripts/qa-runner.groovy new file mode 100644 index 0000000..2558f17 --- /dev/null +++ b/config/jobs/scripts/qa-runner.groovy @@ -0,0 +1,54 @@ +pipeline { + agent { label 'jjb' } + options { + timestamps() + ansiColor('xterm') + } + stages { + stage('Run Jobs In Parallel') { + steps { + script { + def fanout = [:] + + fanout['selenium'] = { + build job: 'qa-selenium-tests', + wait: true, + propagate: true, + parameters: [ + string(name: 'BROWSER', value: params.BROWSER), + string(name: 'BASE_URL', value: params.BASE_URL), + string(name: 'EXECUTION_MODE', value: params.EXECUTION_MODE), + string(name: 'SELENOID_URL', value: params.SELENOID_URL), + string(name: 'HEADLESS', value: params.HEADLESS) + ] + } + + fanout['mobile'] = { + build job: 'qa-mobile-appium-tests', + wait: true, + propagate: true, + parameters: [ + string(name: 'APP_URL', value: params.APP_URL), + string(name: 'DB_URL', value: params.DB_URL), + string(name: 'DB_USER', value: params.DB_USER), + string(name: 'DB_PASSWORD', value: params.DB_PASSWORD), + string(name: 'WISHLISTS_USERNAME', value: params.WISHLISTS_USERNAME), + string(name: 'WISHLISTS_PASSWORD', value: params.WISHLISTS_PASSWORD), + string(name: 'GIFTS_USERNAME', value: params.GIFTS_USERNAME), + string(name: 'GIFTS_PASSWORD', value: params.GIFTS_PASSWORD), + string(name: 'RESERVATION_USERNAME', value: params.RESERVATION_USERNAME), + string(name: 'RESERVATION_PASSWORD', value: params.RESERVATION_PASSWORD), + string(name: 'RESERVATION_OWNER', value: params.RESERVATION_OWNER) + ] + } + + fanout['api'] = { + build job: 'qa-api-citrus-tests', wait: true, propagate: true + } + + parallel fanout + } + } + } + } +} diff --git a/config/jobs/scripts/qa-selenium-tests.groovy b/config/jobs/scripts/qa-selenium-tests.groovy new file mode 100644 index 0000000..c859b71 --- /dev/null +++ b/config/jobs/scripts/qa-selenium-tests.groovy @@ -0,0 +1,53 @@ +pipeline { + agent { label 'maven' } + options { + timestamps() + ansiColor('xterm') + } + stages { + stage('Run Selenium Tests In Docker') { + steps { + sh ''' + set -eux + rm -rf ./artifacts + mkdir -p ./artifacts + + EXTRA_ARGS="" + if [ "${BROWSER}" = "chrome" ]; then + EXTRA_ARGS="-Dchrome.binary=/usr/bin/chromium" + fi + + CID="$(docker create \ + --add-host=host.docker.internal:host-gateway \ + localhost:5005/otus/test-selenium:1.0.0 \ + bash -lc "set -e; cd /workspace; mvn -Dexecution.mode=${EXECUTION_MODE} -Dbrowser=${BROWSER} -Dbrowser.version= -Dselenoid.url=${SELENOID_URL} -Dbase.url=${BASE_URL} -Dselenide.headless=${HEADLESS} -Dallure.results.directory=target/allure-results ${EXTRA_ARGS} test")" + cleanup_container() { + docker rm -f "${CID}" >/dev/null 2>&1 || true + } + trap cleanup_container EXIT INT TERM + tar -C /workspace/otus-autotests/homework_4 -cf - . | docker cp - "${CID}:/workspace" + set +e + docker start -a "${CID}" + TEST_RC=$? + docker cp "${CID}:/workspace/target" "./artifacts/target" || true + trap - EXIT INT TERM + docker rm -f "${CID}" || true + exit "${TEST_RC}" + ''' + } + } + } + post { + always { + script { + try { + allure commandline: 'allure', includeProperties: false, jdk: '', results: [[path: 'artifacts/target/allure-results']] + } catch (Exception ex) { + echo "Allure publisher unavailable: ${ex.message}" + } + } + junit allowEmptyResults: true, testResults: 'artifacts/target/surefire-reports/*.xml' + archiveArtifacts allowEmptyArchive: true, artifacts: 'artifacts/target/**' + } + } +} diff --git a/config/jobs/templates/infra-health-check.yaml b/config/jobs/templates/infra-health-check.yaml new file mode 100644 index 0000000..dd5c34d --- /dev/null +++ b/config/jobs/templates/infra-health-check.yaml @@ -0,0 +1,11 @@ +--- +- job: + name: infra-health-check + description: "Проверка инфраструктуры Jenkins/Registry/Agent образов." + project-type: pipeline + concurrent: false + sandbox: true + properties: + - build-discarder: + num-to-keep: 30 + dsl: !include-raw-verbatim: ../scripts/infra-health-check.groovy diff --git a/config/jobs/templates/jobs-uploader.yaml b/config/jobs/templates/jobs-uploader.yaml new file mode 100644 index 0000000..197e680 --- /dev/null +++ b/config/jobs/templates/jobs-uploader.yaml @@ -0,0 +1,16 @@ +--- +- job: + name: jobs-uploader + description: "Обновляет Jenkins jobs из JJB YAML (config/jobs)." + project-type: pipeline + concurrent: false + sandbox: true + properties: + - build-discarder: + num-to-keep: 30 + parameters: + - string: + name: JJB_PATH + default: /workspace/otus-autotests/hw8/config/jobs + description: "Путь до JJB-конфигов" + dsl: !include-raw-verbatim: ../scripts/jobs-uploader.groovy diff --git a/config/jobs/templates/qa-api-citrus-tests.yaml b/config/jobs/templates/qa-api-citrus-tests.yaml new file mode 100644 index 0000000..0e04bf4 --- /dev/null +++ b/config/jobs/templates/qa-api-citrus-tests.yaml @@ -0,0 +1,11 @@ +--- +- job: + name: qa-api-citrus-tests + description: "API тесты модуля citrus-tests (homework_4)." + project-type: pipeline + concurrent: true + sandbox: true + properties: + - build-discarder: + num-to-keep: 50 + dsl: !include-raw-verbatim: ../scripts/qa-api-citrus-tests.groovy diff --git a/config/jobs/templates/qa-mobile-appium-tests.yaml b/config/jobs/templates/qa-mobile-appium-tests.yaml new file mode 100644 index 0000000..c2eacb4 --- /dev/null +++ b/config/jobs/templates/qa-mobile-appium-tests.yaml @@ -0,0 +1,49 @@ +--- +- job: + name: qa-mobile-appium-tests + description: "Appium mobile тесты (hw7) с автоскачиванием APK через APP_URL." + project-type: pipeline + concurrent: true + sandbox: true + properties: + - build-discarder: + num-to-keep: 30 + parameters: + - string: + name: APP_URL + default: http://wiremock:8080/wishlist.apk + description: "URL APK для автоскачивания (capability app)" + - string: + name: DB_URL + default: jdbc:postgresql://sql.otus.kartushin.su:5432/wishlist + description: "JDBC URL" + - string: + name: DB_USER + default: student + description: "DB user" + - string: + name: DB_PASSWORD + default: "student" + description: "DB password" + - string: + name: WISHLISTS_USERNAME + default: user1us + - string: + name: WISHLISTS_PASSWORD + default: user1us + - string: + name: GIFTS_USERNAME + default: user2us + - string: + name: GIFTS_PASSWORD + default: user2us + - string: + name: RESERVATION_USERNAME + default: user3us + - string: + name: RESERVATION_PASSWORD + default: user3us + - string: + name: RESERVATION_OWNER + default: user4us + dsl: !include-raw-verbatim: ../scripts/qa-mobile-appium-tests.groovy diff --git a/config/jobs/templates/qa-runner.yaml b/config/jobs/templates/qa-runner.yaml new file mode 100644 index 0000000..1ae725d --- /dev/null +++ b/config/jobs/templates/qa-runner.yaml @@ -0,0 +1,64 @@ +--- +- job: + name: qa-runner + description: "Оркестратор: запускает тестовые job параллельно." + project-type: pipeline + concurrent: true + sandbox: true + properties: + - build-discarder: + num-to-keep: 50 + parameters: + - choice: + name: BROWSER + choices: + - chrome + - firefox + - string: + name: BASE_URL + default: https://otus.ru + - string: + name: EXECUTION_MODE + default: selenoid + - string: + name: SELENOID_URL + default: http://host.docker.internal:4444/wd/hub + - choice: + name: HEADLESS + choices: + - "true" + - "false" + - string: + name: APP_URL + default: http://wiremock:8080/wishlist.apk + - string: + name: DB_URL + default: jdbc:postgresql://sql.otus.kartushin.su:5432/wishlist + - string: + name: DB_USER + default: student + - string: + name: DB_PASSWORD + default: "student" + - string: + name: WISHLISTS_USERNAME + default: user1us + - string: + name: WISHLISTS_PASSWORD + default: user1us + - string: + name: GIFTS_USERNAME + default: user2us + - string: + name: GIFTS_PASSWORD + default: user2us + - string: + name: RESERVATION_USERNAME + default: user3us + - string: + name: RESERVATION_PASSWORD + default: user3us + - string: + name: RESERVATION_OWNER + default: user4us + dsl: !include-raw-verbatim: ../scripts/qa-runner.groovy diff --git a/config/jobs/templates/qa-selenium-tests.yaml b/config/jobs/templates/qa-selenium-tests.yaml new file mode 100644 index 0000000..336064f --- /dev/null +++ b/config/jobs/templates/qa-selenium-tests.yaml @@ -0,0 +1,36 @@ +--- +- job: + name: qa-selenium-tests + description: "Selenium/Selenide тесты (homework_4) с выбором браузера." + project-type: pipeline + concurrent: true + sandbox: true + properties: + - build-discarder: + num-to-keep: 50 + parameters: + - choice: + name: BROWSER + choices: + - chrome + - firefox + description: "Браузер для запуска UI-тестов" + - string: + name: BASE_URL + default: https://otus.ru + description: "Базовый URL тестируемого сайта" + - string: + name: EXECUTION_MODE + default: selenoid + description: "Режим запуска (local|selenoid)" + - string: + name: SELENOID_URL + default: http://host.docker.internal:4444/wd/hub + description: "URL Selenoid (используется при EXECUTION_MODE=selenoid)" + - choice: + name: HEADLESS + choices: + - "true" + - "false" + description: "Headless режим" + dsl: !include-raw-verbatim: ../scripts/qa-selenium-tests.groovy diff --git a/config/jobs/view.yaml b/config/jobs/view.yaml new file mode 100644 index 0000000..fe4d6ab --- /dev/null +++ b/config/jobs/view.yaml @@ -0,0 +1,38 @@ +--- +- view: + name: DevOps + view-type: list + description: "Инфраструктурные job" + filter-executors: true + filter-queue: true + job-name: + - jobs-uploader + - infra-health-check + columns: + - status + - weather + - job + - last-success + - last-failure + - last-duration + - build-button + +- view: + name: QA + view-type: list + description: "Тестовые job и раннер" + filter-executors: true + filter-queue: true + job-name: + - qa-runner + - qa-selenium-tests + - qa-api-citrus-tests + - qa-mobile-appium-tests + columns: + - status + - weather + - job + - last-success + - last-failure + - last-duration + - build-button diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index 437bc97..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,25 +0,0 @@ -services: - docker: - image: docker:27-dind - privileged: true - environment: - - DOCKER_TLS_CERTDIR= - command: - - --host=tcp://0.0.0.0:2375 - - --tls=false - volumes: - - ./jenkins_home:/var/jenkins_home - - jenkins: - build: . - depends_on: - - docker - ports: - - "8081:8080" - - "50000:50000" - environment: - - JAVA_OPTS=-Djenkins.install.runSetupWizard=false - - DOCKER_HOST=tcp://docker:2375 - user: root - volumes: - - ./jenkins_home:/var/jenkins_home diff --git a/init.groovy.d/00-security.groovy b/init.groovy.d/00-security.groovy deleted file mode 100644 index dd338af..0000000 --- a/init.groovy.d/00-security.groovy +++ /dev/null @@ -1,16 +0,0 @@ -import jenkins.model.Jenkins -import hudson.security.HudsonPrivateSecurityRealm -import hudson.security.FullControlOnceLoggedInAuthorizationStrategy - -def jenkins = Jenkins.instance -def realm = new HudsonPrivateSecurityRealm(false) -if (realm.getAllUsers().isEmpty()) { - realm.createAccount("admin", "admin") -} -jenkins.setSecurityRealm(realm) - -def strategy = new FullControlOnceLoggedInAuthorizationStrategy() -strategy.setAllowAnonymousRead(true) -jenkins.setAuthorizationStrategy(strategy) - -jenkins.save() diff --git a/init.groovy.d/10-create-jobs.groovy b/init.groovy.d/10-create-jobs.groovy deleted file mode 100644 index 528d697..0000000 --- a/init.groovy.d/10-create-jobs.groovy +++ /dev/null @@ -1,24 +0,0 @@ -import jenkins.model.Jenkins - -def jenkins = Jenkins.instance -def xmlDir = new File(jenkins.root, "job-xml") -if (!xmlDir.exists()) { - return -} - -xmlDir.eachFileMatch(~/.*\.xml/) { file -> - def jobName = file.name.replaceFirst(/\.xml$/, "") - def existing = jenkins.getItem(jobName) - if (existing != null) { - return - } - def fis = new FileInputStream(file) - try { - jenkins.createProjectFromXML(jobName, fis) - println "Created job: ${jobName}" - } finally { - fis.close() - } -} - -jenkins.save() diff --git a/init.groovy.d/20-allure.groovy b/init.groovy.d/20-allure.groovy deleted file mode 100644 index dab7302..0000000 --- a/init.groovy.d/20-allure.groovy +++ /dev/null @@ -1,18 +0,0 @@ -import jenkins.model.Jenkins -import ru.yandex.qatools.allure.jenkins.tools.AllureCommandlineInstallation -import ru.yandex.qatools.allure.jenkins.tools.AllureCommandlineInstaller -import hudson.tools.InstallSourceProperty - -def jenkins = Jenkins.instance -def desc = jenkins.getDescriptorByType(AllureCommandlineInstallation.DescriptorImpl) -def existing = desc.getInstallations() - -def alreadyConfigured = existing.any { it.name == "allure" } -if (!alreadyConfigured) { - def installer = new AllureCommandlineInstaller("2.29.0") - def prop = new InstallSourceProperty([installer]) - def installation = new AllureCommandlineInstallation("allure", "", [prop]) - desc.setInstallations(installation) - desc.save() -} - diff --git a/jobs/mobile-appium-tests.xml b/jobs/mobile-appium-tests.xml deleted file mode 100644 index 99e34e1..0000000 --- a/jobs/mobile-appium-tests.xml +++ /dev/null @@ -1,140 +0,0 @@ - - - Appium mobile tests with APK download and Allure report. - false - - - - - REPO_URL - Git repo with Appium tests (hw7) - https://git.kovbasa.ru/otus-autotests/homework_7.git - true - - - BRANCH - Git branch - master - true - - - APP_URL - APK URL (optional). If empty, repo APK will be used. - - true - - - DB_URL - JDBC url - jdbc:postgresql://sql.otus.kartushin.su:5432/wishlist - true - - - DB_USER - DB user - student - true - - - APPIUM_URL - Appium server URL - http://docker:4723 - true - - - DB_PASSWORD - DB password - - - - - - - true - - - false - diff --git a/jobs/selenium-tests.xml b/jobs/selenium-tests.xml deleted file mode 100644 index 74e10b3..0000000 --- a/jobs/selenium-tests.xml +++ /dev/null @@ -1,117 +0,0 @@ - - - Selenium/Selenide tests with browser parameter and Allure report. - false - - - - - REPO_URL - Git repo with Selenium/Selenide tests - https://git.kovbasa.ru/otus-autotests/homework_4.git - true - - - BRANCH - Git branch - master - true - - - BROWSER - Browser to run tests - - - chrome - firefox - - - - - BROWSER_VERSION - Browser version (optional) - - true - - - EXECUTION_MODE - local or selenoid - local - true - - - SELENOID_URL - Selenoid URL (used when execution.mode=selenoid) - http://localhost/wd/hub - true - - - BASE_URL - Base URL for tests - https://otus.ru - true - - - HEADLESS - Run browsers in headless mode - - - true - false - - - - - - - - - true - - - false - diff --git a/plugins.txt b/plugins.txt deleted file mode 100644 index 42a2a73..0000000 --- a/plugins.txt +++ /dev/null @@ -1,5 +0,0 @@ -workflow-aggregator -git -allure-jenkins-plugin -docker-workflow -matrix-project