From 2fd883d2913b510a70f7876358b17f23f58ca779 Mon Sep 17 00:00:00 2001 From: Robert Kaussow Date: Fri, 27 Sep 2024 20:40:51 +0200 Subject: [PATCH] initial commit --- .gitignore | 11 ++++ .markdownlint.yml | 7 ++ .prettierignore | 1 + .woodpecker/docs.yaml | 47 +++++++++++++ .woodpecker/lint.yaml | 30 +++++++++ .woodpecker/notify.yml | 26 ++++++++ .woodpecker/test.yaml | 24 +++++++ .yamllint | 20 ++++++ LICENSE | 21 ++++++ README.md | 1 + defaults/main.yml | 25 +++++++ meta/main.yml | 24 +++++++ molecule/default/converge.yml | 22 +++++++ molecule/default/molecule.yml | 17 +++++ molecule/default/prepare.yml | 11 ++++ molecule/default/tests/test_default.py | 13 ++++ pyproject.toml | 17 +++++ requirements.yml | 4 ++ tasks/main.yml | 91 ++++++++++++++++++++++++++ templates/account.json.j2 | 12 ++++ templates/cron_lego_renew.sh.j2 | 15 +++++ vars/main.yml | 6 ++ 22 files changed, 445 insertions(+) create mode 100644 .gitignore create mode 100644 .markdownlint.yml create mode 100644 .prettierignore create mode 100644 .woodpecker/docs.yaml create mode 100644 .woodpecker/lint.yaml create mode 100644 .woodpecker/notify.yml create mode 100644 .woodpecker/test.yaml create mode 100644 .yamllint create mode 100644 LICENSE create mode 100644 README.md create mode 100644 defaults/main.yml create mode 100644 meta/main.yml create mode 100644 molecule/default/converge.yml create mode 100644 molecule/default/molecule.yml create mode 100644 molecule/default/prepare.yml create mode 100644 molecule/default/tests/test_default.py create mode 100644 pyproject.toml create mode 100644 requirements.yml create mode 100644 tasks/main.yml create mode 100644 templates/account.json.j2 create mode 100644 templates/cron_lego_renew.sh.j2 create mode 100644 vars/main.yml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d97b7cd --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +# ---> Ansible +*.retry +plugins +library + +# ---> Python +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + diff --git a/.markdownlint.yml b/.markdownlint.yml new file mode 100644 index 0000000..da116c7 --- /dev/null +++ b/.markdownlint.yml @@ -0,0 +1,7 @@ +--- +default: True +MD013: False +MD041: False +MD024: False +MD004: + style: dash diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..6b1d0bf --- /dev/null +++ b/.prettierignore @@ -0,0 +1 @@ +LICENSE diff --git a/.woodpecker/docs.yaml b/.woodpecker/docs.yaml new file mode 100644 index 0000000..857444b --- /dev/null +++ b/.woodpecker/docs.yaml @@ -0,0 +1,47 @@ +--- +when: + - event: [pull_request] + - event: [push, manual] + branch: + - ${CI_REPO_DEFAULT_BRANCH} + +steps: + - name: generate + image: quay.io/thegeeklab/ansible-doctor + environment: + ANSIBLE_DOCTOR_EXCLUDE_FILES: "['molecule/']" + ANSIBLE_DOCTOR_RENDERER__FORCE_OVERWRITE: "true" + ANSIBLE_DOCTOR_LOGGING__LEVEL: info + ANSIBLE_DOCTOR_ROLE__NAME: ${CI_REPO_NAME} + ANSIBLE_DOCTOR_TEMPLATE__NAME: readme + + - name: format + image: quay.io/thegeeklab/alpine-tools + commands: + - prettier -w README.md + + - name: diff + image: quay.io/thegeeklab/alpine-tools + commands: + - git diff --color=always README.md + + - name: publish + image: quay.io/thegeeklab/wp-git-action + settings: + action: + - commit + - push + author_email: ci-bot@rknet.org + author_name: ci-bot + branch: main + message: "[skip ci] automated docs update" + netrc_machine: gitea.rknet.org + netrc_password: + from_secret: gitea_token + when: + - event: [push, manual] + branch: + - ${CI_REPO_DEFAULT_BRANCH} + +depends_on: + - test diff --git a/.woodpecker/lint.yaml b/.woodpecker/lint.yaml new file mode 100644 index 0000000..c48a8e4 --- /dev/null +++ b/.woodpecker/lint.yaml @@ -0,0 +1,30 @@ +--- +when: + - event: [pull_request, tag] + - event: [push, manual] + branch: + - ${CI_REPO_DEFAULT_BRANCH} + +steps: + - name: ansible-lint + image: quay.io/thegeeklab/ansible-dev-tools:1 + commands: + - ansible-lint + environment: + FORCE_COLOR: "1" + + - name: python-format + image: docker.io/python:3.12 + commands: + - pip install -qq ruff + - ruff format --check --diff . + environment: + PY_COLORS: "1" + + - name: python-lint + image: docker.io/python:3.12 + commands: + - pip install -qq ruff + - ruff check . + environment: + PY_COLORS: "1" diff --git a/.woodpecker/notify.yml b/.woodpecker/notify.yml new file mode 100644 index 0000000..45bc21e --- /dev/null +++ b/.woodpecker/notify.yml @@ -0,0 +1,26 @@ +--- +when: + - event: [tag] + - event: [push, manual] + branch: + - ${CI_REPO_DEFAULT_BRANCH} + +runs_on: [success, failure] + +steps: + - name: matrix + image: quay.io/thegeeklab/wp-matrix + settings: + homeserver: + from_secret: matrix_homeserver + room_id: + from_secret: matrix_room_id + user_id: + from_secret: matrix_user_id + access_token: + from_secret: matrix_access_token + when: + - status: [failure] + +depends_on: + - docs diff --git a/.woodpecker/test.yaml b/.woodpecker/test.yaml new file mode 100644 index 0000000..661dc8b --- /dev/null +++ b/.woodpecker/test.yaml @@ -0,0 +1,24 @@ +--- +when: + - event: [pull_request, tag] + - event: [push, manual] + branch: + - ${CI_REPO_DEFAULT_BRANCH} + +variables: + - &molecule_base + image: quay.io/thegeeklab/ansible-dev-tools:1 + group: molecule + environment: + PY_COLORS: "1" + HCLOUD_TOKEN: + from_secret: molecule_hcloud_token + +steps: + - name: molecule-default + <<: *molecule_base + commands: + - molecule test -s default + +depends_on: + - lint diff --git a/.yamllint b/.yamllint new file mode 100644 index 0000000..df1d39e --- /dev/null +++ b/.yamllint @@ -0,0 +1,20 @@ +--- +extends: default + +rules: + truthy: + allowed-values: ["True", "False"] + comments: + min-spaces-from-content: 1 + comments-indentation: False + line-length: disable + braces: + min-spaces-inside: 0 + max-spaces-inside: 1 + brackets: + min-spaces-inside: 0 + max-spaces-inside: 0 + indentation: enable + octal-values: + forbid-implicit-octal: True + forbid-explicit-octal: True diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..3812eb4 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Robert Kaussow + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice (including the next +paragraph) shall be included in all copies or substantial portions of the +Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS +OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF +OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..052d3b4 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# xoxys.lego diff --git a/defaults/main.yml b/defaults/main.yml new file mode 100644 index 0000000..701a8dd --- /dev/null +++ b/defaults/main.yml @@ -0,0 +1,25 @@ +--- +lego_version: 4.18.0 + +lego_server: https://acme-v02.api.letsencrypt.org/directory + +lego_cloudflare_email: "" +lego_cloudflare_api_key: "" + +# @var lego_accounts:example: > +# lego_accounts: +# - account_email: user@example.com +# account_number: "862bf8e9-b02a-43f1-9c05-ea073e0e1c7c" +# account_key: "94ecba99-bfbd-4c5a-9fd4-790f1c061a4c" +# @end +lego_accounts: [] + +# @var lego_certificates:example: +# lego_certificates: +# - account_email: user@example.com +# domains: +# - example.com +# - www.example.com +# skip_create: False +# @end +lego_certificates: [] diff --git a/meta/main.yml b/meta/main.yml new file mode 100644 index 0000000..84de547 --- /dev/null +++ b/meta/main.yml @@ -0,0 +1,24 @@ +--- +galaxy_info: + # @meta author:value: [Robert Kaussow](https://gitea.rknet.org/xoxys) + author: Robert Kaussow + namespace: xoxys + role_name: lego + # @meta description: > + # [![Build Status](https://ci.rknet.org/api/badges/ansible/xoxys.lego/status.svg)](https://ci.rknet.org/repos/ansible/xoxys.lego) + # [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg?label=license)](https://gitea.rknet.org/ansible/xoxys.lego/src/branch/main/LICENSE) + # + # Deploy Lego ACME client to manage Lets Encrypt certificates. + # @end + description: Deploy Lego ACME client to manage Lets Encrypt certificates + license: MIT + min_ansible_version: "2.10" + platforms: + - name: EL + versions: + - "9" + galaxy_tags: + - ca + - lego + - corporate +dependencies: [] diff --git a/molecule/default/converge.yml b/molecule/default/converge.yml new file mode 100644 index 0000000..44f1c13 --- /dev/null +++ b/molecule/default/converge.yml @@ -0,0 +1,22 @@ +--- +- name: Converge + hosts: all + vars: + lego_server: https://acme-staging-v02.api.letsencrypt.org/directory + lego_accounts: + - account_email: user@example.com + account_number: "862bf8e9-b02a-43f1-9c05-ea073e0e1c7c" + account_key: "94ecba99-bfbd-4c5a-9fd4-790f1c061a4c" + lego_certificates: + - account_email: user@example.com + domains: + - example.com + - www.example.com + skip_create: True + pre_tasks: + - name: Install requirements + ansible.builtin.package: + name: tar + state: present + roles: + - role: xoxys.lego diff --git a/molecule/default/molecule.yml b/molecule/default/molecule.yml new file mode 100644 index 0000000..2bb4b3c --- /dev/null +++ b/molecule/default/molecule.yml @@ -0,0 +1,17 @@ +--- +driver: + name: molecule_hetznercloud +dependency: + name: galaxy + options: + role-file: requirements.yml + requirements-file: requirements.yml +platforms: + - name: "rocky9-lego" + server_type: "cx22" + image: "rocky-9" +provisioner: + name: ansible + log: False +verifier: + name: testinfra diff --git a/molecule/default/prepare.yml b/molecule/default/prepare.yml new file mode 100644 index 0000000..0df1d77 --- /dev/null +++ b/molecule/default/prepare.yml @@ -0,0 +1,11 @@ +--- +- name: Prepare + hosts: all + gather_facts: False + tasks: + - name: Bootstrap Python for Ansible + ansible.builtin.raw: | + command -v python3 python || + ((test -e /usr/bin/apt && (apt -y update && apt install -y python-minimal)) || + echo "Warning: Python not boostrapped due to unknown platform.") + changed_when: False diff --git a/molecule/default/tests/test_default.py b/molecule/default/tests/test_default.py new file mode 100644 index 0000000..2d7d931 --- /dev/null +++ b/molecule/default/tests/test_default.py @@ -0,0 +1,13 @@ +import os + +import testinfra.utils.ansible_runner + +testinfra_hosts = testinfra.utils.ansible_runner.AnsibleRunner( + os.environ["MOLECULE_INVENTORY_FILE"] +).get_hosts("all") + + +def test_lego_bin(host): + cmd = host.run("lego --version") + + assert cmd.succeeded diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..7193140 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,17 @@ +[tool.ruff] +exclude = [".git", "__pycache__"] + +line-length = 99 +indent-width = 4 + +[tool.ruff.lint] +ignore = ["W191", "E111", "E114", "E117", "S101", "S105"] +select = ["F", "E", "I", "W", "S"] + +[tool.ruff.format] +quote-style = "double" +indent-style = "space" +line-ending = "lf" + +[tool.pytest.ini_options] +filterwarnings = ["ignore::FutureWarning", "ignore::DeprecationWarning"] diff --git a/requirements.yml b/requirements.yml new file mode 100644 index 0000000..5b676bf --- /dev/null +++ b/requirements.yml @@ -0,0 +1,4 @@ +--- +collections: [] + +roles: [] diff --git a/tasks/main.yml b/tasks/main.yml new file mode 100644 index 0000000..f1737f6 --- /dev/null +++ b/tasks/main.yml @@ -0,0 +1,91 @@ +--- +- name: Install lego + ansible.legacy.unarchive: + src: https://github.com/go-acme/lego/releases/download/v{{ lego_version }}/lego_v{{ lego_version }}_linux_amd64.tar.gz + dest: "{{ __lego_bin_dir }}" + remote_src: True + extra_opts: + - "{{ __lego_bin_name }}" + mode: "0750" + +- name: Create lego base dir + ansible.builtin.file: + path: "{{ __lego_base_dir }}/bin" + state: directory + owner: root + group: root + mode: "0750" + recurse: True + +- name: Create LetsEncrypt certificates directory + ansible.builtin.file: + path: "{{ __lego_base_dir }}/.lego/certificates" + state: directory + owner: root + group: root + mode: "0700" + recurse: True + +- name: Create LetsEncrypt account directory + ansible.builtin.file: + path: "{{ __lego_base_dir }}/.lego/accounts/acme-v02.api.letsencrypt.org/{{ item.account_email }}/keys" + state: directory + owner: root + group: root + mode: "0700" + recurse: True + loop: "{{ lego_accounts }}" + loop_control: + label: "{{ item.account_email }}" + +- name: Deploy account json + ansible.builtin.template: + dest: "{{ __lego_base_dir }}/.lego/accounts/acme-v02.api.letsencrypt.org/{{ item.account_email | mandatory }}/account.json" + group: root + owner: root + mode: "0600" + src: account.json.j2 + loop: "{{ lego_accounts }}" + loop_control: + label: "{{ item.account_email }}" + +- name: Deploy account key + ansible.builtin.copy: + content: "{{ item.account_key }}" + dest: "{{ __lego_base_dir }}/.lego/accounts/acme-v02.api.letsencrypt.org/{{ item.account_email | mandatory }}/keys/{{ item.account_email }}.key" + owner: root + group: root + mode: "0600" + diff: False + loop: "{{ lego_accounts }}" + loop_control: + label: "{{ item.account_email }}" + +- name: Obtain certificates for domains + ansible.builtin.command: '{{ __lego_bin_file }} --email="{{ item.account_email }}" --domains {{ " --domains ".join(item.domains) }} --dns="cloudflare" run' + args: + creates: "{{ __lego_base_dir }}/.lego/certificates/{{ item.domains[0] }}.crt" + environment: + LEGO_SERVER: "{{ lego_server }}" + LEGO_PATH: "{{ __lego_base_dir }}/.lego" + CLOUDFLARE_EMAIL: "{{ lego_cloudflare_email }}" + CLOUDFLARE_API_KEY: "{{ lego_cloudflare_api_key }}" + when: not item.skip_create | bool + loop: "{{ lego_certificates }}" + loop_control: + label: "{{ item.account_email }}" + +- name: Add cron scipt to renew certificates + ansible.builtin.template: + dest: "{{ __lego_base_dir }}/bin/cron_lego_renew.sh" + mode: "0755" + src: cron_lego_renew.sh.j2 + +- name: Add cron job to renew certificates + ansible.builtin.cron: + name: "lego-renew" + cron_file: "lego-renew" + job: "{{ __lego_base_dir }}/bin/cron_lego_renew.sh >> {{ __lego_base_dir }}/cron_lego_renew.log 2>&1" + hour: 2 + minute: 5 + user: root diff --git a/templates/account.json.j2 b/templates/account.json.j2 new file mode 100644 index 0000000..faa4e14 --- /dev/null +++ b/templates/account.json.j2 @@ -0,0 +1,12 @@ +{ + "email": "{{ item.account_email }}", + "registration": { + "body": { + "status": "valid", + "contact": [ + "mailto:{{ item.account_email }}" + ] + }, + "uri": "https://acme-v02.api.letsencrypt.org/acme/acct/{{ item.account_number }}" + } +} diff --git a/templates/cron_lego_renew.sh.j2 b/templates/cron_lego_renew.sh.j2 new file mode 100644 index 0000000..5596feb --- /dev/null +++ b/templates/cron_lego_renew.sh.j2 @@ -0,0 +1,15 @@ +#!/bin/env bash +# run this script daily to renew any letsencrypt certs that need renewing +# renew cert if it expires within 30 days + +export LEGO_SERVER="{{ lego_server }}" +export LEGO_PATH="{{ __lego_base_dir }}/.lego" + +export CLOUDFLARE_EMAIL="{{ lego_cloudflare_email }}" +export CLOUDFLARE_API_KEY="{{ lego_cloudflare_api_key }}" + +{% for cert in lego_certificates %} +echo "$(date) checking for cert update for {{ ', '.join(cert.domains) }}." +{{ __lego_bin_file }} --email="{{ cert.account_email }}" --domains {{ ' --domains '.join(cert.domains) }} --dns="cloudflare" renew --days 30 + +{% endfor %} diff --git a/vars/main.yml b/vars/main.yml new file mode 100644 index 0000000..67d40e6 --- /dev/null +++ b/vars/main.yml @@ -0,0 +1,6 @@ +--- +__lego_base_dir: /etc/lego + +__lego_bin_dir: /usr/bin +__lego_bin_name: lego +__lego_bin_file: "{{ __lego_bin_dir }}/{{ __lego_bin_name }}"