Skip to content

Commit

Permalink
Batch mode and fix variable casts
Browse files Browse the repository at this point in the history
  • Loading branch information
bviktor committed Nov 21, 2023
1 parent d3b986a commit 10aacd4
Show file tree
Hide file tree
Showing 10 changed files with 86 additions and 30 deletions.
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ This role obtains HTTPS certificates using the ACME protocol from Let's Encrypt,

| Name | Required | Example | Description |
|---|---|---|---|
| `domain` | true | `foobar.com` | Domain to obtain certificates for. |
| `provider` | true | `cf` | DNS provider to use. See [How to use DNS API](https://github.com/acmesh-official/acme.sh/wiki/dnsapi) for details. E.g. if the command is `--dns dns_cf`, then this argument should be `cf`. |
| `credential` | true | See in Examples | Dictionary holding all your `export ...` variables, as explained on the above link. |
| `acme_batch` | no | `<list>` | Supply the below parameters as a list, see examples. |
| `domain` | yes | `foobar.com` | Domain to obtain certificates for. |
| `provider` | yes | `cf` | DNS provider to use. See [How to use DNS API](https://github.com/acmesh-official/acme.sh/wiki/dnsapi) for details. E.g. if the command is `--dns dns_cf`, then this argument should be `cf`. |
| `credential` | yes | See in Examples | Dictionary holding all your `export ...` variables, as explained on the above link. |
| `wildcard` | no | `true` | If `true`, obtains not only the base certificate, but the wildcard certificate too, via SAN. E.g. if the domain is `foobar.com`, the certificate will be valid for `*.foobar.com` as well. Defaults to `false`. |
| `cronjob` | no | `true`| If `true`, deploy cronjob to automatically renew the certificate every month. Defaults to `false`. |
| `staging` | no | `true` | If `true`, uses staging servers instead of production. Use for testing. Defaults to `false`. |
Expand Down
8 changes: 4 additions & 4 deletions tasks/acme.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,22 +37,22 @@

- name: Obtain initial cert
command:
cmd: "{{ acme_install_dir }}/acme.sh {% if eff_staging %}--staging{% else %}--server letsencrypt{% endif %} --ecc --issue --dns dns_{{ provider }} --dnssleep {{ eff_sleep }} --force --domain {{ domain }}{% if eff_wildcard %} --domain *.{{ domain }}{% endif %}"
cmd: "{{ acme_install_dir }}/acme.sh {% if eff_staging | bool %}--staging{% else %}--server letsencrypt{% endif %} --ecc --issue --dns dns_{{ provider }} --dnssleep {{ eff_sleep | int }} --force --domain {{ domain }}{% if eff_wildcard | bool %} --domain *.{{ domain }}{% endif %}"
environment: "{{ credential }}"
changed_when: false
when: not current_file.stat.exists or (eff_wildcard and current_wildcard.rc != 0)
when: not current_file.stat.exists or (eff_wildcard | bool and current_wildcard.rc != 0)

- name: Renew existing cert
command:
cmd: "{{ acme_install_dir }}/acme.sh {% if eff_staging %}--staging{% else %}--server letsencrypt{% endif %} --ecc --renew --dns dns_{{ provider }} --dnssleep {{ eff_sleep }} --force --domain {{ domain }}{% if eff_wildcard %} --domain *.{{ domain }}{% endif %}"
cmd: "{{ acme_install_dir }}/acme.sh {% if eff_staging | bool %}--staging{% else %}--server letsencrypt{% endif %} --ecc --renew --dns dns_{{ provider }} --dnssleep {{ eff_sleep | int }} --force --domain {{ domain }}{% if eff_wildcard | bool %} --domain *.{{ domain }}{% endif %}"
environment: "{{ credential }}"
changed_when: false
when: current_file.stat.exists and current_expiry.rc != 0

# Unlike --issue, --renew updates the files in the target automatically, but it won't hurt to copy twice
- name: Deploy cert
command:
cmd: "{{ acme_install_dir }}/acme.sh --install-cert --ecc --domain {{ domain }}{% if eff_wildcard %} --domain *.{{ domain }}{% endif %} --cert-file {{ deploy_cert_file }} --key-file {{ deploy_key_file }} --ca-file {{ deploy_ca_file }} --fullchain-file {{ deploy_fullchain_file }}"
cmd: "{{ acme_install_dir }}/acme.sh --install-cert --ecc --domain {{ domain }}{% if eff_wildcard | bool %} --domain *.{{ domain }}{% endif %} --cert-file {{ deploy_cert_file }} --key-file {{ deploy_key_file }} --ca-file {{ deploy_ca_file }} --fullchain-file {{ deploy_fullchain_file }}"
changed_when: false

- include_role:
Expand Down
30 changes: 17 additions & 13 deletions tasks/main.yml
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
---
- include_tasks: deps.yml
when: installed_acme_deps is not defined

- include_tasks: acme.yml

- include_role:
name: noobient.setfcontext
- include_tasks: main2.yml
vars:
path: "{{ acme_deploy_dir }}"
type: 'cert_t'
pattern: "{{ acme_deploy_dir }}(/.*)?"
when: ansible_os_family == 'RedHat'
domain: "{{ item.domain }}"
provider: "{{ item.provider }}"
credential: "{{ item.credential }}"
wildcard: "{{ item.wildcard | default('') }}"
cronjob: "{{ item.cronjob | default('') }}"
staging: "{{ item.staging | default('') }}"
sleep: "{{ item.sleep | default('') }}"
min_days: "{{ item.min_days | default('') }}"
reload_cmd: "{{ item.reload_cmd | default('') }}"
loop: "{{ acme_batch }}"
loop_control:
# make sure we don't leak credentials to stdout lol
label: "{{ item.domain }}"
when: (acme_batch is defined) and (acme_batch.__class__.__name__ == 'list')

- include_tasks: cron.yml
when: eff_cronjob
- include_tasks: main2.yml
when: (acme_batch is not defined) or (acme_batch.__class__.__name__ != 'list')
16 changes: 16 additions & 0 deletions tasks/main2.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
- include_tasks: deps.yml
when: installed_acme_deps is not defined

- include_tasks: acme.yml

- include_role:
name: noobient.setfcontext
vars:
path: "{{ acme_deploy_dir }}"
type: 'cert_t'
pattern: "{{ acme_deploy_dir }}(/.*)?"
when: ansible_os_family == 'RedHat'

- include_tasks: cron.yml
when: eff_cronjob | bool
6 changes: 3 additions & 3 deletions templates/acme.j2
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Minutes are pseudo-random, to ensure idempotency, but still don't flood ACME with all certs at once.
{{ domain.split('.')[0] | length % 6 }}{{ domain | length % 10 }} 07 1 * * root {{ acme_install_dir }}/acme.sh {% if eff_staging %}--staging{% else %}--server letsencrypt{% endif %} --ecc --renew --dns dns_{{ provider }} --dnssleep {{ eff_sleep }} --force --domain {{ domain }}{% if eff_wildcard %} --domain *.{{ domain }}{% endif %} >> /var/log/letsencrypt 2>&1
{{ domain.split('.')[0] | length % 6 }}{{ domain | length % 10 }} 07 1 * * root {{ acme_install_dir }}/acme.sh {% if eff_staging | bool %}--staging{% else %}--server letsencrypt{% endif %} --ecc --renew --dns dns_{{ provider }} --dnssleep {{ eff_sleep | int }} --force --domain {{ domain }}{% if eff_wildcard | bool %} --domain *.{{ domain }}{% endif %} >> /var/log/letsencrypt 2>&1

{% if reload_cmd is defined and reload_cmd | length %}
{{ domain.split('.')[0] | length % 6 }}{{ domain | length % 10 }} 08 1 * * root {{ reload_cmd }}
{% if eff_reload_cmd | length %}
{{ domain.split('.')[0] | length % 6 }}{{ domain | length % 10 }} 08 1 * * root {{ eff_reload_cmd }}
{% endif %}
8 changes: 8 additions & 0 deletions tests/batch.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
# Idempotency and batch
- include_role:
name: "{{ playbook_dir.split('/')[:-1] | last }}"
vars:
acme_batch:
- { domain: "{{ noobient_acme_test_domain_2 }}", provider: 'cf', credential: { CF_Token: "{{ lookup('ansible.builtin.env', 'CF_Token') }}", CF_Account_ID: "{{ lookup('ansible.builtin.env', 'CF_Account_ID') }}", CF_Zone_ID: "{{ lookup('ansible.builtin.env', 'CF_Zone_ID') }}" }, cronjob: 'true', staging: 1 }
- { domain: "{{ noobient_acme_test_domain }}", provider: 'cf', credential: { CF_Token: "{{ lookup('ansible.builtin.env', 'CF_Token') }}", CF_Account_ID: "{{ lookup('ansible.builtin.env', 'CF_Account_ID') }}", CF_Zone_ID: "{{ lookup('ansible.builtin.env', 'CF_Zone_ID') }}" }, wildcard: yes, staging: 'true' }
22 changes: 22 additions & 0 deletions tests/batch_return.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
# set_fact appears to be async, so acme_new may be undefined without individual calls
- set_fact:
acme_new: "{ '{{ item.domain }}.cert_file': '{{ item.domain }}.cer' }"

# the closes thing to add()
- set_fact:
acme: "{{ acme | combine( acme_new ) }}"

# TODO still won't work. The combine part adds the previous stuff to the new keys.
- set_fact:
acme_new: "{ '{{ item.domain }}.key_file': '{{ item.domain }}.key' }"

# the closes thing to add()
- set_fact:
acme: "{{ acme | combine( acme_new ) }}"

# the closes thing to unset_fact
- set_fact:
acme_new:

- include_tasks: print.yml
3 changes: 3 additions & 0 deletions tests/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,6 @@

- include_tasks: idempotency2.yml
- include_tasks: print.yml

- include_tasks: batch.yml
- include_tasks: print.yml
12 changes: 6 additions & 6 deletions vars/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ deploy_key_file: "{{ acme_domain_dir }}/{{ domain }}.key"
deploy_ca_file: "{{ acme_domain_dir }}/ca.cer"
deploy_fullchain_file: "{{ acme_domain_dir }}/fullchain.cer"

eff_staging: "{{ staging | default(false) | bool }}"
eff_wildcard: "{{ wildcard | default(false) | bool }}"
eff_cronjob: "{{ cronjob | default(false) | bool }}"

eff_wildcard: "{% if wildcard is defined and wildcard | string | length %}{{ wildcard }}{% else %}false{% endif %}"
eff_cronjob: "{% if cronjob is defined and cronjob | string | length %}{{ cronjob }}{% else %}false{% endif %}"
eff_staging: "{% if staging is defined and staging | string | length %}{{ staging }}{% else %}false{% endif %}"
# TODO this will always have to be casted in math expressions as well, because Ansible braindamage
# https://stackoverflow.com/a/69109779/1708230
eff_sleep: "{{ sleep | default(20) | int }}"
eff_min_days: "{{ (min_days | default(60)) | int }}"
eff_sleep: "{% if sleep is defined and sleep | string | length %}{{ sleep }}{% else %}20{% endif %}"
eff_min_days: "{% if min_days is defined and min_days | string | length %}{{ min_days }}{% else %}60{% endif %}"
eff_reload_cmd: "{% if reload_cmd is defined and reload_cmd | string | length %}{{ reload_cmd }}{% endif %}"
4 changes: 3 additions & 1 deletion vars/test.yml
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
---
noobient_acme_test_domain: "{{ ansible_distribution | lower }}-{{ ansible_distribution_major_version }}-{{ ansible_date_time.hour }}{{ ansible_date_time.minute }}{{ ansible_date_time.second }}.vault-tec.info"
noobient_acme_test_prefix: "{{ ansible_distribution | lower }}-{{ ansible_distribution_major_version }}-{{ ansible_date_time.hour }}{{ ansible_date_time.minute }}{{ ansible_date_time.second }}"
noobient_acme_test_domain: "{{ noobient_acme_test_prefix }}a.vault-tec.info"
noobient_acme_test_domain_2: "{{ noobient_acme_test_prefix }}b.vault-tec.info"

0 comments on commit 10aacd4

Please sign in to comment.