From 94f15cc402deb4cd05ef99e54c4c04504a22df3e Mon Sep 17 00:00:00 2001 From: transcaffeine Date: Tue, 4 Feb 2025 17:27:12 +0100 Subject: [PATCH 1/4] feat(jenkins): add role to deploy jenkins --- README.md | 2 + playbooks/jenkins.yml | 6 +++ roles/jenkins/README.md | 3 ++ roles/jenkins/defaults/main/container.yml | 59 +++++++++++++++++++++++ roles/jenkins/defaults/main/main.yml | 17 +++++++ roles/jenkins/handlers/default.yml | 13 +++++ roles/jenkins/tasks/check.yml | 21 ++++++++ roles/jenkins/tasks/deploy-docker.yml | 40 +++++++++++++++ roles/jenkins/tasks/main.yml | 26 ++++++++++ roles/jenkins/templates/docker-passwd.j2 | 19 ++++++++ roles/jenkins/vars/main.yml | 8 +++ 11 files changed, 214 insertions(+) create mode 100644 playbooks/jenkins.yml create mode 100644 roles/jenkins/README.md create mode 100644 roles/jenkins/defaults/main/container.yml create mode 100644 roles/jenkins/defaults/main/main.yml create mode 100644 roles/jenkins/handlers/default.yml create mode 100644 roles/jenkins/tasks/check.yml create mode 100644 roles/jenkins/tasks/deploy-docker.yml create mode 100644 roles/jenkins/tasks/main.yml create mode 100644 roles/jenkins/templates/docker-passwd.j2 create mode 100644 roles/jenkins/vars/main.yml diff --git a/README.md b/README.md index ae92de0..3d15e17 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,8 @@ proxies and artifact registries. ## Roles +- [`jenkins`](roles/jenkins/README.md): Deploy [jenkins](https://jenkins.io), the self-proclaimed 'leading open source automation server'. + ## License [CNPLv7+](LICENSE.md): Cooperative Nonviolent Public License diff --git a/playbooks/jenkins.yml b/playbooks/jenkins.yml new file mode 100644 index 0000000..75cffe0 --- /dev/null +++ b/playbooks/jenkins.yml @@ -0,0 +1,6 @@ +--- +- name: Install and configure jenkins + hosts: "{{ jenkins_hosts | default('jenkins', true) }}" + become: "{{ jenkins_become | default(true, true) }}" + roles: + - role: finallycoffee.cicd.jenkins diff --git a/roles/jenkins/README.md b/roles/jenkins/README.md new file mode 100644 index 0000000..de149c7 --- /dev/null +++ b/roles/jenkins/README.md @@ -0,0 +1,3 @@ +# `finallycoffee.cicd.jenkins` ansible role + +Deploy and configure [Jenkins](https://jenkins.io) using ansible. diff --git a/roles/jenkins/defaults/main/container.yml b/roles/jenkins/defaults/main/container.yml new file mode 100644 index 0000000..ca72b26 --- /dev/null +++ b/roles/jenkins/defaults/main/container.yml @@ -0,0 +1,59 @@ +--- +jenkins_container_name: "jenkins" +jenkins_container_image: >-2 + {{ + [ + jenkins_container_image_repository, + jenkins_container_image_tag + | default( + jenkins_version + ( + ((jenkins_container_image_flavour is string) + and (jenkins_container_image_flavour | length > 0)) + | ternary( + '-' + jenkins_container_image_flavour | default('', true), + '' + ) + ), + true + ) + ] | join(':') + }} +jenkins_container_image_registry: docker.io +jenkins_container_image_namespace: jenkins +jenkins_container_image_name: jenkins +jenkins_container_image_repository: >-2 + {{ + [ + jenkins_container_image_registry | default([], true), + jenkins_container_image_namespace | default([], true), + jenkins_container_image_name + ] | flatten | join('/') + }} +jenkins_container_image_source: "pull" +jenkins_container_image_force_source: >-2 + {{ jenkins_container_image_tag | default(true, true) }} +jenkins_container_image_tag: ~ +jenkins_container_image_flavour: "jdk17" + +jenkins_container_env: ~ +jenkins_container_user: >-2 + {{ jenkins_user_uid }}:{{ jenkins_user_gid }} +jenkins_container_ports: ~ +jenkins_container_state: >-2 + {{ (jenkins_state == 'present') | ternary('started', 'absent') }} +jenkins_container_labels: + version: "{{ jenkins_container_image_tag | default(jenkins_version, true) }}" +jenkins_container_networks: ~ +jenkins_container_etc_hosts: ~ +jenkins_container_base_volumes: + - "{{ jenkins_home_path }}:{{ jenkins_container_home_path }}:rw" + - "{{ jenkins_etc_passwd_shim_path }}:/etc/passwd:ro" +jenkins_container_volumes: ~ +jenkins_container_all_volumes: >-2 + {{ jenkins_container_base_volumes | default([], true) + + jenkins_container_volumes | default([], true) }} +jenkins_container_restart_policy: "on-failure" + +# Determined by upstream image +jenkins_container_home_path: "/var/jenkins_home" +jenkins_container_tcp_listen_port: "8080" diff --git a/roles/jenkins/defaults/main/main.yml b/roles/jenkins/defaults/main/main.yml new file mode 100644 index 0000000..7399fb1 --- /dev/null +++ b/roles/jenkins/defaults/main/main.yml @@ -0,0 +1,17 @@ +--- +jenkins_user: "jenkins" +jenkins_user_is_system: true +jenkins_user_create_home: false +jenkins_versions: + lts: "2.479.3" + weekly: "2.496" +jenkins_version_channel: "lts" +jenkins_version: "{{ jenkins_versions[jenkins_version_channel] }}" + +jenkins_state: present +jenkins_deployment_method: docker + +jenkins_home_path: "/var/lib/jenkins" +jenkins_etc_passwd_shim_path: "/etc/jenkins/docker-passwd" +jenkins_user_uid: "{{ jenkins_user_info.uid }}" +jenkins_user_gid: "{{ jenkins_user_info.group }}" diff --git a/roles/jenkins/handlers/default.yml b/roles/jenkins/handlers/default.yml new file mode 100644 index 0000000..79aa3b2 --- /dev/null +++ b/roles/jenkins/handlers/default.yml @@ -0,0 +1,13 @@ +--- +- name: Restart jenkins container '{{ jenkins_container_name }}' + community.docker.docker_container: + name: "{{ jenkins_container_name }}" + state: "started" + restart: true + comparisons: + '*': "ignore" + when: + - jenkins_deployment_method == 'docker' + - jenkins_container_state == 'started' + listen: jenkins_restart + ignore_errors: "{{ ansible_check_mode }}" diff --git a/roles/jenkins/tasks/check.yml b/roles/jenkins/tasks/check.yml new file mode 100644 index 0000000..f48eea0 --- /dev/null +++ b/roles/jenkins/tasks/check.yml @@ -0,0 +1,21 @@ +--- +- name: Ensure 'jenkins_state' is valid + ansible.builtin.fail: + msg: >-2 + Unsupported jenkins_state '{{ jenkins_state }}'. + Supported values are: {{ jenkins_states | join(', ') }} + when: jenkins_state not in jenkins_states + +- name: Ensure 'jenkins_deployment_method' is valid + ansible.builtin.fail: + msg: >-2 + Unsupported jenkins_deployment_method '{{ jenkins_deployment_method }}'. + Supported values are: {{ jenkins_deployment_methods | join(', ') }} + when: jenkins_deployment_method not in jenkins_deployment_methods + +- name: Ensure 'jenkins_version_channel' is valid + ansible.builtin.fail: + msg: >-2 + Unsupported jenkins_version_channel '{{ jenkins_version_channel }}'. + Supported values are: {{ jenkins_version_channels | join(', ') }} + when: jenkins_version_channel not in jenkins_version_channels diff --git a/roles/jenkins/tasks/deploy-docker.yml b/roles/jenkins/tasks/deploy-docker.yml new file mode 100644 index 0000000..d608fb1 --- /dev/null +++ b/roles/jenkins/tasks/deploy-docker.yml @@ -0,0 +1,40 @@ +--- +- name: Ensure jenkins container image '{{ jenkins_container_image }}' is {{ jenkins_state }} + community.docker.docker_image: + name: "{{ jenkins_container_image }}" + state: "{{ jenkins_state }}" + source: "{{ jenkins_container_image_source }}" + force_source: "{{ jenkins_container_image_force_source }}" + +- name: Ensure jenkins configuration folder is created + ansible.builtin.file: + path: "{{ jenkins_etc_passwd_shim_path | dirname }}" + state: directory + mode: "0755" + owner: "root" + group: "root" + when: jenkins_state == 'present' + +- name: Ensure jenkins fake '/etc/passwd' is created + ansible.builtin.template: + src: "docker-passwd.j2" + dest: "{{ jenkins_etc_passwd_shim_path }}" + mode: "0644" + owner: "root" + group: "root" + when: jenkins_state == 'present' + notify: + - jenkins_restart + +- name: Ensure jenkins container '{{ jenkins_container_name }}' is {{ jenkins_container_state }} + community.docker.docker_container: + name: "{{ jenkins_container_name }}" + image: "{{ jenkins_container_image }}" + env: "{{ jenkins_container_env | default(omit, true) }}" + user: "{{ jenkins_container_user | default(omit, true) }}" + ports: "{{ jenkins_container_ports | default(omit, true) }}" + labels: "{{ jenkins_container_labels | default(omit, true) }}" + networks: "{{ jenkins_container_networks | default(omit, true) }}" + volumes: "{{ jenkins_container_all_volumes }}" + restart_policy: "{{ jenkins_container_restart_policy }}" + state: "{{ jenkins_container_state }}" diff --git a/roles/jenkins/tasks/main.yml b/roles/jenkins/tasks/main.yml new file mode 100644 index 0000000..bfe8680 --- /dev/null +++ b/roles/jenkins/tasks/main.yml @@ -0,0 +1,26 @@ +--- +- name: Ensure role arguments are valid + ansible.builtin.include_tasks: + file: "check.yml" + +- name: Ensure jenkins user '{{ jenkins_user }}' is {{ jenkins_state }} + ansible.builtin.user: + name: "{{ jenkins_user }}" + state: "{{ jenkins_state }}" + system: "{{ jenkins_user_is_system }}" + create_home: "{{ jenkins_user_create_home }}" + register: jenkins_user_info + +- name: Ensure jenkins home '{{ jenkins_home_path }}' is {{ jenkins_state }} + ansible.builtin.file: + path: "{{ jenkins_home_path }}" + state: "{{ (jenkins_state == 'present') | ternary('directory', 'absent') }}" + mode: "{{ jenkins_home_path_mode | default('0750', true) }}" + owner: "{{ jenkins_user_uid | default(jenkins_user, true) }}" + group: "{{ jenkins_user_gid | default(jenkins_user, true) }}" + notify: + - jenkins_restart + +- name: Ensure jenkins is deployed using {{ jenkins_deployment_method }} + ansible.builtin.include_tasks: + file: "deploy-{{ jenkins_deployment_method }}.yml" diff --git a/roles/jenkins/templates/docker-passwd.j2 b/roles/jenkins/templates/docker-passwd.j2 new file mode 100644 index 0000000..f293507 --- /dev/null +++ b/roles/jenkins/templates/docker-passwd.j2 @@ -0,0 +1,19 @@ +root:x:0:0:root:/root:/bin/bash +daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin +bin:x:2:2:bin:/bin:/usr/sbin/nologin +sys:x:3:3:sys:/dev:/usr/sbin/nologin +sync:x:4:65534:sync:/bin:/bin/sync +games:x:5:60:games:/usr/games:/usr/sbin/nologin +man:x:6:12:man:/var/cache/man:/usr/sbin/nologin +lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin +mail:x:8:8:mail:/var/mail:/usr/sbin/nologin +news:x:9:9:news:/var/spool/news:/usr/sbin/nologin +uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin +proxy:x:13:13:proxy:/bin:/usr/sbin/nologin +www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin +backup:x:34:34:backup:/var/backups:/usr/sbin/nologin +list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin +irc:x:39:39:ircd:/run/ircd:/usr/sbin/nologin +_apt:x:42:65534::/nonexistent:/usr/sbin/nologin +nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin +jenkins:x:{{ jenkins_user_uid }}:{{ jenkins_user_gid }}::/var/jenkins_home:/bin/bash diff --git a/roles/jenkins/vars/main.yml b/roles/jenkins/vars/main.yml new file mode 100644 index 0000000..6cfe507 --- /dev/null +++ b/roles/jenkins/vars/main.yml @@ -0,0 +1,8 @@ +--- +jenkins_states: + - absent + - present +jenkins_deployment_methods: + - docker +jenkins_version_channels: >-2 + {{ (jenkins_versions.keys()) | list }} -- 2.47.2 From 98b0567c514614807a92b92431e965fe1ff75092 Mon Sep 17 00:00:00 2001 From: transcaffeine Date: Wed, 5 Feb 2025 13:48:09 +0100 Subject: [PATCH 2/4] feat(jenkins_inbound_agent): add ansible role for deployment with docker --- README.md | 1 + roles/jenkins_inbound_agent/README.md | 11 +++ .../defaults/main/container.yml | 79 +++++++++++++++++++ .../defaults/main/main.yml | 18 +++++ roles/jenkins_inbound_agent/handlers/main.yml | 13 +++ roles/jenkins_inbound_agent/tasks/check.yml | 38 +++++++++ .../tasks/deploy-docker.yml | 58 ++++++++++++++ roles/jenkins_inbound_agent/tasks/main.yml | 16 ++++ .../templates/docker-passwd.j2 | 18 +++++ roles/jenkins_inbound_agent/vars/main.yml | 13 +++ 10 files changed, 265 insertions(+) create mode 100644 roles/jenkins_inbound_agent/README.md create mode 100644 roles/jenkins_inbound_agent/defaults/main/container.yml create mode 100644 roles/jenkins_inbound_agent/defaults/main/main.yml create mode 100644 roles/jenkins_inbound_agent/handlers/main.yml create mode 100644 roles/jenkins_inbound_agent/tasks/check.yml create mode 100644 roles/jenkins_inbound_agent/tasks/deploy-docker.yml create mode 100644 roles/jenkins_inbound_agent/tasks/main.yml create mode 100644 roles/jenkins_inbound_agent/templates/docker-passwd.j2 create mode 100644 roles/jenkins_inbound_agent/vars/main.yml diff --git a/README.md b/README.md index 3d15e17..65f37bd 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ proxies and artifact registries. ## Roles - [`jenkins`](roles/jenkins/README.md): Deploy [jenkins](https://jenkins.io), the self-proclaimed 'leading open source automation server'. +- [`jenkins_inbound_agent`](roles/jenkins_inbound_agent/README.md): Deploy Jenkins 'inbound agent', formerly known as 'JNLP agent'. ## License diff --git a/roles/jenkins_inbound_agent/README.md b/roles/jenkins_inbound_agent/README.md new file mode 100644 index 0000000..23fb292 --- /dev/null +++ b/roles/jenkins_inbound_agent/README.md @@ -0,0 +1,11 @@ +# `finallycoffee.cicd.jenkins_inbound_agent` ansible role + +## Overview + +Deploy Jenkins inbound agents using docker. + +## Required configuration + +- `jenkins_agent_server_url` - URL of the jenkins control node, including protocol and port, i.e.: http://jenkins-server:port +- `jenkins_agent_secret` - Secret for this jenkins agent generated by the jenkins control node +- `jenkins_agent_name` - Name of this (inbound) agent, must match agent name in jenkins control node diff --git a/roles/jenkins_inbound_agent/defaults/main/container.yml b/roles/jenkins_inbound_agent/defaults/main/container.yml new file mode 100644 index 0000000..5b37501 --- /dev/null +++ b/roles/jenkins_inbound_agent/defaults/main/container.yml @@ -0,0 +1,79 @@ +--- +jenkins_agent_container_name: "jenkins-inbound-agent-{{ jenkins_agent_name }}" +jenkins_agent_container_image: >-2 + {{ + [ + jenkins_agent_container_image_repository, + jenkins_agent_container_image_tag + | default( + jenkins_agent_version + jenkins_agent_container_suffix, + true + ) + ] | join(':') + }} +jenkins_agent_container_image_registry: docker.io +jenkins_agent_container_image_namespace: jenkins +jenkins_agent_container_image_name: inbound-agent +jenkins_agent_container_image_repository: >-2 + {{ + [ + jenkins_agent_container_image_registry | default([], true), + jenkins_agent_container_image_namespace | default([], true), + jenkins_agent_container_image_name + ] | flatten | join('/') + }} +jenkins_agent_container_image_source: "pull" +jenkins_agent_container_image_force_source: >-2 + {{ jenkins_agent_container_image_tag | default(true, true) }} +jenkins_agent_container_image_tag: ~ +jenkins_agent_container_image_jdk_version: "jdk17" +jenkins_agent_container_image_distribution: "alpine" + +jenkins_agent_container_suffix: >-2 + {{ + ( + ((jenkins_agent_container_image_distribution is string) + and (jenkins_agent_container_image_distribution | length > 0)) + | ternary( + '-' + jenkins_agent_container_image_distribution | default('', true), + '' + ) + ) + + + ( + ((jenkins_agent_container_image_jdk_version is string) + and (jenkins_agent_container_image_jdk_version | length > 0)) + | ternary( + '-' + jenkins_agent_container_image_jdk_version | default('', true), + '' + ) + ) + }} + +jenkins_agent_container_env: ~ +jenkins_agent_container_base_env: + JENKINS_URL: "{{ jenkins_agent_server_url | ansible.builtin.mandatory }}" + JENKINS_AGENT_NAME: "{{ jenkins_agent_name | ansible.builtin.mandatory }}" + JENKINS_AGENT_WORKDIR: "{{ jenkins_agent_work_dir | default('/home/jenkins/agent') }}" + JENKINS_WEB_SOCKET: "true" + JENKINS_SECRET: "@{{ jenkins_agent_secret_file }}" +jenkins_agent_container_all_env: >-2 + {{ jenkins_agent_container_base_env + | combine(jenkins_agent_container_env | default({}, true)) }} +jenkins_agent_container_user: >-2 + {{ jenkins_agent_user_uid }}:{{ jenkins_agent_user_gid }} +jenkins_agent_container_ports: ~ +jenkins_agent_container_state: >-2 + {{ (jenkins_agent_state == 'present') | ternary('started', 'absent') }} +jenkins_agent_container_labels: + version: "{{ jenkins_agent_container_image_tag | default(jenkins_agent_version, true) }}" +jenkins_agent_container_networks: ~ +jenkins_agent_container_etc_hosts: ~ +jenkins_agent_container_base_volumes: + - "{{ jenkins_agent_passwd_shim_file }}:/etc/passwd:ro" + - "{{ jenkins_agent_secret_file }}:{{ jenkins_agent_secret_file }}:ro" +jenkins_agent_container_volumes: ~ +jenkins_agent_container_all_volumes: >-2 + {{ jenkins_agent_container_base_volumes | default([], true) + + jenkins_agent_container_volumes | default([], true) }} +jenkins_agent_container_restart_policy: "on-failure" diff --git a/roles/jenkins_inbound_agent/defaults/main/main.yml b/roles/jenkins_inbound_agent/defaults/main/main.yml new file mode 100644 index 0000000..bf00e0f --- /dev/null +++ b/roles/jenkins_inbound_agent/defaults/main/main.yml @@ -0,0 +1,18 @@ +--- +jenkins_agent_user: "jenkins-agent" +jenkins_agent_user_create_home: false +jenkins_agent_user_is_system: false +jenkins_agent_user_uid: "{{ jenkins_agent_user_info.uid }}" +jenkins_agent_user_gid: "{{ jenkins_agent_user_info.group }}" + +jenkins_agent_version: "3283.v92c105e0f819-8" + +jenkins_agent_state: "present" +jenkins_agent_deployment_method: "docker" + +jenkins_agent_name: ~ +jenkins_agent_secret: ~ +jenkins_agent_server_url: ~ + +jenkins_agent_secret_file: "/etc/jenkins/agent/{{ jenkins_agent_name }}.secret" +jenkins_agent_passwd_shim_file: "/etc/jenkins/agent/{{ jenkins_agent_name }}-passwd" diff --git a/roles/jenkins_inbound_agent/handlers/main.yml b/roles/jenkins_inbound_agent/handlers/main.yml new file mode 100644 index 0000000..b721cd3 --- /dev/null +++ b/roles/jenkins_inbound_agent/handlers/main.yml @@ -0,0 +1,13 @@ +--- +- name: Restart jenkins agent container '{{ jenkins_agent_container_name }}' + community.docker.docker_container: + name: "{{ jenkins_agent_container_name }}" + state: "started" + restart: true + comparisons: + '*': "ignore" + listen: jenkins_agent_restart + when: + - jenkins_deployment_method == 'docker' + - jenkins_agent_container_state == 'started' + ignore_errors: "{{ ansible_check_mode }}" diff --git a/roles/jenkins_inbound_agent/tasks/check.yml b/roles/jenkins_inbound_agent/tasks/check.yml new file mode 100644 index 0000000..fe852fb --- /dev/null +++ b/roles/jenkins_inbound_agent/tasks/check.yml @@ -0,0 +1,38 @@ +--- +- name: Ensure 'jenkins_agent_state' is valid + ansible.builtin.fail: + msg: >-2 + Unsupported jenkins_agent_state '{{ jenkins_agent_state }}'. + Supported values are: {{ jenkins_agent_states | join(', ') }} + when: jenkins_agent_state not in jenkins_agent_states + +- name: Ensure 'jenkins_agent_deployment_method' is valid + ansible.builtin.fail: + msg: >-2 + Unsupported jenkins_agent_deployment_method '{{ jenkins_agent_deployment_method }}'. + Supported values are: {{ jenkins_agent_deployment_methods | join(', ') }} + when: jenkins_agent_deployment_method not in jenkins_agent_deployment_methods + +- name: Ensure Jenkins agent JDK version is valid if specified + ansible.builtin.fail: + msg: >-2 + Unsupported jenkins_agent_container_image_jdk_version + '{{ jenkins_agent_container_image_jdk_version }}' specified! + Supported JDK versions are: + {{ jenkins_agent_container_image_jdk_versions | join(', ') }} + when: + - jenkins_agent_container_image_jdk_version is string + - jenkins_agent_container_image_jdk_version | length > 0 + - jenkins_agent_container_image_jdk_version not in jenkins_agent_container_image_jdk_versions + +- name: Ensure Jenkins agent distribution is valid if specified + ansible.builtin.fail: + msg: >-2 + Unsupported jenkins_agent_container_image_distribution + '{{ jenkins_agent_container_image_distribution }}' specified! + Supported JDK versions are: + {{ jenkins_agent_container_image_distibrutions | join(', ') }} + when: + - jenkins_agent_container_image_distribution is string + - jenkins_agent_container_image_distribution | length > 0 + - jenkins_agent_container_image_distribution not in jenkins_agent_container_image_distributions diff --git a/roles/jenkins_inbound_agent/tasks/deploy-docker.yml b/roles/jenkins_inbound_agent/tasks/deploy-docker.yml new file mode 100644 index 0000000..be2a6f2 --- /dev/null +++ b/roles/jenkins_inbound_agent/tasks/deploy-docker.yml @@ -0,0 +1,58 @@ +--- +- name: Ensure container image '{{ jenkins_agent_container_image }}' is {{ jenkins_agent_state }} + community.docker.docker_image: + name: "{{ jenkins_agent_container_image }}" + state: "{{ jenkins_agent_state }}" + source: "{{ jenkins_agent_container_image_source }}" + force_source: "{{ jenkins_agent_container_image_force_source }}" + +- name: Ensure jenkins configuration directory exists + ansible.builtin.file: + path: "{{ jenkins_agent_secret_file | dirname }}" + state: directory + mode: "0755" + recurse: true + when: jenkins_agent_state == 'present' + +- name: Ensure jenkins agent secret is persisted + ansible.builtin.copy: + dest: "{{ jenkins_agent_secret_file }}" + content: "{{ jenkins_agent_secret }}" + mode: "0400" + owner: "{{ jenkins_agent_user_uid | default(jenkins_agent_user) }}" + group: "{{ jenkins_agent_user_gid | default(jenkins_agent_user) }}" + when: jenkins_agent_state == 'present' + notify: + - jenkins_agent_restart + +- name: Ensure jenkins agent fake '/etc/passwd' is templated + ansible.builtin.template: + src: "docker-passwd.j2" + dest: "{{ jenkins_agent_passwd_shim_file }}" + mode: "0644" + owner: "root" + group: "root" + +- name: Ensure jenkins configuration is removed + ansible.builtin.file: + path: "{{ jenkins_agent_secret_file | dirname }}" + state: absent + recurse: true + when: jenkins_agent_state == 'absent' + +- name: Ensure jenkins-agent container '{{ jenkins_agent_container_name }}' is {{ jenkins_agent_container_state }} + community.docker.docker_container: + name: "{{ jenkins_agent_container_name }}" + image: "{{ jenkins_agent_container_image }}" + env: "{{ jenkins_agent_container_all_env | default(omit, true) }}" + init: "{{ jenkins_agent_container_init | default(true, true) }}" + user: "{{ jenkins_agent_container_user | default(omit, true) }}" + ports: "{{ jenkins_agent_container_ports | default(omit, true) }}" + labels: "{{ jenkins_agent_container_labels | default(omit, true) }}" + volumes: "{{ jenkins_agent_container_all_volumes }}" + networks: "{{ jenkins_agent_container_networks | default(omit, true) }}" + etc_hosts: "{{ jenkins_agent_container_etc_hosts | default(omit, true) }}" + restart_policy: "{{ jenkins_agent_container_restart_policy }}" + state: "{{ jenkins_agent_container_state }}" + comparisons: + "env": "strict" diff --git a/roles/jenkins_inbound_agent/tasks/main.yml b/roles/jenkins_inbound_agent/tasks/main.yml new file mode 100644 index 0000000..4450d7d --- /dev/null +++ b/roles/jenkins_inbound_agent/tasks/main.yml @@ -0,0 +1,16 @@ +--- +- name: Ensure required variables are populated (correctly) + ansible.builtin.include_tasks: + file: "check.yml" + +- name: Ensure jenkins-agent user '{{ jenkins_agent_user }}' is {{ jenkins_agent_state }} + ansible.builtin.user: + name: "{{ jenkins_agent_user }}" + state: "{{ jenkins_agent_state }}" + system: "{{ jenkins_agent_user_is_system }}" + create_home: "{{ jenkins_agent_user_create_home }}" + register: jenkins_agent_user_info + +- name: Ensure jenkins-agent '{{ jenkins_agent_name }}' is deployed using {{ jenkins_agent_deployment_method }} + ansible.builtin.include_tasks: + file: "deploy-{{ jenkins_agent_deployment_method }}.yml" diff --git a/roles/jenkins_inbound_agent/templates/docker-passwd.j2 b/roles/jenkins_inbound_agent/templates/docker-passwd.j2 new file mode 100644 index 0000000..7b45fea --- /dev/null +++ b/roles/jenkins_inbound_agent/templates/docker-passwd.j2 @@ -0,0 +1,18 @@ +root:x:0:0:root:/root:/bin/sh +bin:x:1:1:bin:/bin:/sbin/nologin +daemon:x:2:2:daemon:/sbin:/sbin/nologin +lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin +sync:x:5:0:sync:/sbin:/bin/sync +shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown +halt:x:7:0:halt:/sbin:/sbin/halt +mail:x:8:12:mail:/var/mail:/sbin/nologin +news:x:9:13:news:/usr/lib/news:/sbin/nologin +uucp:x:10:14:uucp:/var/spool/uucppublic:/sbin/nologin +cron:x:16:16:cron:/var/spool/cron:/sbin/nologin +ftp:x:21:21::/var/lib/ftp:/sbin/nologin +sshd:x:22:22:sshd:/dev/null:/sbin/nologin +games:x:35:35:games:/usr/games:/sbin/nologin +ntp:x:123:123:NTP:/var/empty:/sbin/nologin +guest:x:405:100:guest:/dev/null:/sbin/nologin +nobody:x:65534:65534:nobody:/:/sbin/nologin +jenkins:x:1000:1000:Linux User,,,:/home/jenkins:/bin/sh diff --git a/roles/jenkins_inbound_agent/vars/main.yml b/roles/jenkins_inbound_agent/vars/main.yml new file mode 100644 index 0000000..d2d6a9d --- /dev/null +++ b/roles/jenkins_inbound_agent/vars/main.yml @@ -0,0 +1,13 @@ +--- +jenkins_agent_states: + - "present" + - "absent" +jenkins_agent_deployment_methods: + - "docker" +jenkins_agent_container_image_jdk_versions: + - "jdk17" + - "jdk21" +jenkins_agent_container_image_distributions: + - "alpine" + - "alpine3.21" + - "rhel-ubi9" -- 2.47.2 From 9f7c63ec3facd07a21c2938dee8c15d979c9912b Mon Sep 17 00:00:00 2001 From: transcaffeine Date: Sat, 8 Feb 2025 15:40:05 +0100 Subject: [PATCH 3/4] feat(jenkins_node_info): add info module --- plugins/module_utils/Jenkins.py | 46 +++++++++++ plugins/modules/jenkins_node.py | 0 plugins/modules/jenkins_node_info.py | 112 +++++++++++++++++++++++++++ requirements-dev.txt | 1 + requirements.txt | 91 ++++++++++++++++++++++ 5 files changed, 250 insertions(+) create mode 100644 plugins/module_utils/Jenkins.py create mode 100644 plugins/modules/jenkins_node.py create mode 100644 plugins/modules/jenkins_node_info.py create mode 100644 requirements-dev.txt create mode 100644 requirements.txt diff --git a/plugins/module_utils/Jenkins.py b/plugins/module_utils/Jenkins.py new file mode 100644 index 0000000..9f4defd --- /dev/null +++ b/plugins/module_utils/Jenkins.py @@ -0,0 +1,46 @@ +import xml.etree.ElementTree as ET +from typing import Optional +from dataclasses import dataclass + +import requests +from pathspec import PathSpec + + +class Jenkins: + + @dataclass + class NodeInfo: + server_url: str + name: str + secret: Optional[str] + work_dir: PathSpec + internal_dir: PathSpec + server_url: str + + def __init__(self, server_url, username, api_token): + self.server_url = server_url + self.username = username + self.api_token = api_token + + def _log_in(self, username: str, password: str) -> (str, str): + response = requests.get(f"{self.server_url}/crumbIssuer/api/json") + response.raise_for_status() + payload = response.json() + return payload["crumbRequestField"], payload["crumb"] + + def get_node_jnlp(self, node_name) -> str: + response = requests.get( + f"{self.server_url}/manage/computer/{node_name}/slave-agent.jnlp", + auth=(self.username, self.api_token), + ) + response.raise_for_status() + return response.text + + def get_node_info(self, node_name: str) -> NodeInfo: + jnlp_info_raw = self.get_node_jnlp(node_name) + tree = ET.ElementTree(ET.fromstring(jnlp_info_raw)) + arguments = tree.findall("./application-desc/") + (node_secret, node_name, _, work_dir, _, internal_dir, _, url) = [ + arg.text for arg in arguments[:8] + ] + return Jenkins.NodeInfo(url, node_name, node_secret, work_dir, internal_dir) diff --git a/plugins/modules/jenkins_node.py b/plugins/modules/jenkins_node.py new file mode 100644 index 0000000..e69de29 diff --git a/plugins/modules/jenkins_node_info.py b/plugins/modules/jenkins_node_info.py new file mode 100644 index 0000000..19c1825 --- /dev/null +++ b/plugins/modules/jenkins_node_info.py @@ -0,0 +1,112 @@ +# pylint: disable=E0401 + +from __future__ import absolute_import, annotations, division, print_function + +__metaclass__ = type # pylint: disable=C0103 + +from typing import TYPE_CHECKING + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.finallycoffee.cicd.plugins.module_utils.Jenkins import Jenkins + +if TYPE_CHECKING: + from typing import Optional, Dict, Any + +DOCUMENTATION = r""" +--- +module: jenkins_node + +short_description: Retrieve Jenkins node information +# If this is part of a collection, you need to use semantic versioning, +# i.e. the version is of the form "2.5.0" and not "2.4". +version_added: "0.0.1" + +description: This is my longer description explaining my test module. + +options: + name: + description: The name of the jenkins node. + required: true + type: str + aliases: + - agent + server: + description: URL of the jenkins instance + required: true + type: str + aliases: + - server_url + username: + description: Username to use for authentication to jenkins + required: true + type: str + aliases: + - user + api_token: + description: Jenkins API token for the user + required: true + type: str +author: + - transcaffeine (@transcaffeine) +""" + +EXAMPLES = r""" +# Pass in a message +- name: Retrieve information about the jenkins node named 'my_jenkins_node_name' + finallycoffee.cicd.jenkins_node_info: + name: my_jenkins_node_name + server: https://jenkins.example.org + username: admin + api_token: yoursecretapitokenhere +""" + +RETURN = r""" +# These are examples of possible return values, and in general should use other names for return values. +name: + description: The name of the jenkins node + type: str + returned: always + sample: 'jenkins-agent-jdk21-alpine' +secret: + description: The secret of the agent + type: str + returned: always + sample: 'secretverylongstringwith64chars' +work_dir: + description: The local working directory of the jenkins agent + type: str + returned: always +""" + + +def run_module(): + module_args = dict( + name=dict(type="str", required=True, aliases=["node", "node_name"]), + server=dict(type="str", required=True, aliases=["server_url", "url"]), + username=dict(type="str", required=True, aliases=["user"]), + api_token=dict(type="str", required=True, aliases=["password", "pass"]), + ) + result = dict(changed=False) + module = AnsibleModule(argument_spec=module_args, supports_check_mode=True) + + jenkins = Jenkins( + module.params["server"], module.params["username"], module.params["api_token"] + ) + node = jenkins.get_node_info(module.params["name"]) + + result["name"] = node.name + result["secret"] = node.secret + result["work_dir"] = node.work_dir + + # in the event of a successful module execution, you will want to + # simple AnsibleModule.exit_json(), passing the key/value results + module.exit_json(**result) + + +def main(): + run_module() + + +if __name__ == "__main__": + main() diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..5726625 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1 @@ +ansible-dev-tools==25.* \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..413df83 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,91 @@ +alabaster==1.0.0 +annotated-types==0.7.0 +ansible==11.2.0 +ansible-core==2.18.2 +argcomplete==3.4.0 +attrs==23.2.1.dev0 +autocommand==2.2.2 +Babel==2.15.0 +Brlapi==0.8.6 +btrfsutil==6.12 +build==1.2.2 +canonicaljson==2.0.0 +certifi==2025.1.31 +cffi==1.17.1 +charset-normalizer==3.4.1 +coverage==7.6.10 +cryptography==44.0.0 +dbus-python==1.3.2 +diceware==1.0.1 +distro==1.9.0 +distro-info==1.10 +dnspython==2.7.0 +docutils==0.21.2 +fastjsonschema==2.21.1 +filelock==3.17.0 +idna==3.10 +imagesize==1.4.1 +inflect==7.5.0 +iniconfig==2.0.0 +installer==0.7.0 +jaraco.collections==5.1.0 +jaraco.context==6.0.1 +jaraco.functools==4.1.0 +jaraco.text==4.0.0 +Jinja2==3.1.5 +lensfun==0.3.4 +libfdt==1.7.2 +libvirt-python==11.0.0 +louis==3.32.0 +lxml==5.3.0 +MarkupSafe==2.1.5 +more-itertools==10.5.0 +netsnmp-python==1.0a1 +nftables==0.1 +ordered-set==4.1.0 +packaging==24.2 +platformdirs==4.3.6 +pluggy==1.5.0 +ply==3.11 +pycairo==1.27.0 +pycparser==2.22 +pydantic==2.10.6 +pydantic_core==2.27.2 +Pygments==2.19.1 +PyGObject==3.50.0 +PyNaCl==1.5.0 +pyproject_hooks==1.2.0 +pytest==8.3.4 +pytest-cov==6.0.0 +pytz==2024.2 +PyYAML==6.0.2 +requests==2.32.3 +resolvelib==1.0.1 +setuptools==75.2.0 +signedjson==1.1.4 +six==1.17.0 +snowballstemmer==2.2.0 +Sphinx==8.1.3 +sphinx_rtd_theme==2.0.0 +sphinxcontrib-applehelp==2.0.0 +sphinxcontrib-devhelp==2.0.0 +sphinxcontrib-htmlhelp==2.1.0 +sphinxcontrib-jquery==4.1 +sphinxcontrib-jsmath==1.0.1 +sphinxcontrib-qthelp==2.0.0 +sphinxcontrib-serializinghtml==2.0.0 +TBB==0.2 +tomli==2.0.1 +tomlkit==0.13.2 +trove-classifiers==2025.1.15.22 +typeguard==4.4.1 +typing_extensions==4.12.2 +ufw==0.36.2 +unpaddedbase64==2.1.0 +urllib3==2.3.0 +validate-pyproject==0.23.post1.dev0+gf45606b.d20250111 +wheel==0.45.0 +wxPython==4.2.2 +xmltodict==0.14.2 +yq==3.4.3 +yt-dlp==2025.1.26 -- 2.47.2 From dad480d4b35feee99412f1c2c63802b97a13ee36 Mon Sep 17 00:00:00 2001 From: transcaffeine Date: Sun, 9 Feb 2025 18:26:49 +0100 Subject: [PATCH 4/4] chore(jenkins_node_info): move documentation in own file --- plugins/__init__.py | 0 plugins/module_utils/__init__.py | 0 plugins/module_utils/docs/__init__.py | 0 .../module_utils/docs/jenkins_node_info.py | 65 +++++++++++++++++ plugins/modules/__init__.py | 0 plugins/modules/jenkins_node_info.py | 71 ++----------------- 6 files changed, 70 insertions(+), 66 deletions(-) create mode 100644 plugins/__init__.py create mode 100644 plugins/module_utils/__init__.py create mode 100644 plugins/module_utils/docs/__init__.py create mode 100644 plugins/module_utils/docs/jenkins_node_info.py create mode 100644 plugins/modules/__init__.py diff --git a/plugins/__init__.py b/plugins/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/plugins/module_utils/__init__.py b/plugins/module_utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/plugins/module_utils/docs/__init__.py b/plugins/module_utils/docs/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/plugins/module_utils/docs/jenkins_node_info.py b/plugins/module_utils/docs/jenkins_node_info.py new file mode 100644 index 0000000..d68dcfc --- /dev/null +++ b/plugins/module_utils/docs/jenkins_node_info.py @@ -0,0 +1,65 @@ +DOCUMENTATION = r""" +--- +module: jenkins_node + +short_description: Retrieve Jenkins node information +# If this is part of a collection, you need to use semantic versioning, +# i.e. the version is of the form "2.5.0" and not "2.4". +version_added: "0.0.1" + +description: This is my longer description explaining my test module. + +options: + name: + description: The name of the jenkins node. + required: true + type: str + aliases: + - agent + server: + description: URL of the jenkins instance + required: true + type: str + aliases: + - server_url + username: + description: Username to use for authentication to jenkins + required: true + type: str + aliases: + - user + api_token: + description: Jenkins API token for the user + required: true + type: str +author: + - transcaffeine (@transcaffeine) +""" + +EXAMPLES = r""" +# Pass in a message +- name: Retrieve information about the jenkins node named 'my_jenkins_node_name' + finallycoffee.cicd.jenkins_node_info: + name: my_jenkins_node_name + server: https://jenkins.example.org + username: admin + api_token: yoursecretapitokenhere +""" + +RETURN = r""" +# These are examples of possible return values, and in general should use other names for return values. +name: + description: The name of the jenkins node + type: str + returned: always + sample: 'jenkins-agent-jdk21-alpine' +secret: + description: The secret of the agent + type: str + returned: always + sample: 'secretverylongstringwith64chars' +work_dir: + description: The local working directory of the jenkins agent + type: str + returned: always +""" diff --git a/plugins/modules/__init__.py b/plugins/modules/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/plugins/modules/jenkins_node_info.py b/plugins/modules/jenkins_node_info.py index 19c1825..7676f25 100644 --- a/plugins/modules/jenkins_node_info.py +++ b/plugins/modules/jenkins_node_info.py @@ -9,76 +9,15 @@ from typing import TYPE_CHECKING from ansible.module_utils.basic import AnsibleModule from ansible_collections.finallycoffee.cicd.plugins.module_utils.Jenkins import Jenkins +from ansible_collections.finallycoffee.cicd.plugins.module_utils.docs.jenkins_node_info import ( + DOCUMENTATION, + EXAMPLES, + RETURN, +) if TYPE_CHECKING: from typing import Optional, Dict, Any -DOCUMENTATION = r""" ---- -module: jenkins_node - -short_description: Retrieve Jenkins node information -# If this is part of a collection, you need to use semantic versioning, -# i.e. the version is of the form "2.5.0" and not "2.4". -version_added: "0.0.1" - -description: This is my longer description explaining my test module. - -options: - name: - description: The name of the jenkins node. - required: true - type: str - aliases: - - agent - server: - description: URL of the jenkins instance - required: true - type: str - aliases: - - server_url - username: - description: Username to use for authentication to jenkins - required: true - type: str - aliases: - - user - api_token: - description: Jenkins API token for the user - required: true - type: str -author: - - transcaffeine (@transcaffeine) -""" - -EXAMPLES = r""" -# Pass in a message -- name: Retrieve information about the jenkins node named 'my_jenkins_node_name' - finallycoffee.cicd.jenkins_node_info: - name: my_jenkins_node_name - server: https://jenkins.example.org - username: admin - api_token: yoursecretapitokenhere -""" - -RETURN = r""" -# These are examples of possible return values, and in general should use other names for return values. -name: - description: The name of the jenkins node - type: str - returned: always - sample: 'jenkins-agent-jdk21-alpine' -secret: - description: The secret of the agent - type: str - returned: always - sample: 'secretverylongstringwith64chars' -work_dir: - description: The local working directory of the jenkins agent - type: str - returned: always -""" - def run_module(): module_args = dict( -- 2.47.2