diff --git a/data/RedHat-family.yaml b/data/RedHat-family.yaml index ab6b13a..404b411 100644 --- a/data/RedHat-family.yaml +++ b/data/RedHat-family.yaml @@ -1,3 +1,4 @@ --- letsencrypt::configure_epel: true letsencrypt::plugin::dns_rfc2136::package_name: 'python2-certbot-dns-rfc2136' +letsencrypt::plugin::dns_route53::package_name: 'python2-certbot-dns-route53' diff --git a/data/os/Debian/10.yaml b/data/os/Debian/10.yaml index cd92a13..d0641b2 100644 --- a/data/os/Debian/10.yaml +++ b/data/os/Debian/10.yaml @@ -1,2 +1,3 @@ --- letsencrypt::plugin::dns_rfc2136::package_name: 'python3-certbot-dns-rfc2136' +letsencrypt::plugin::dns_route53::package_name: 'python3-certbot-dns-route53' diff --git a/data/os/Fedora.yaml b/data/os/Fedora.yaml index b2bc628..56c3cd5 100644 --- a/data/os/Fedora.yaml +++ b/data/os/Fedora.yaml @@ -1,3 +1,4 @@ --- letsencrypt::configure_epel: false letsencrypt::plugin::dns_rfc2136::package_name: 'python3-certbot-dns-rfc2136' +letsencrypt::plugin::dns_route53::package_name: 'python3-certbot-dns-route53' diff --git a/data/os/Ubuntu/18.04.yaml b/data/os/Ubuntu/18.04.yaml index cd92a13..d0641b2 100644 --- a/data/os/Ubuntu/18.04.yaml +++ b/data/os/Ubuntu/18.04.yaml @@ -1,2 +1,3 @@ --- letsencrypt::plugin::dns_rfc2136::package_name: 'python3-certbot-dns-rfc2136' +letsencrypt::plugin::dns_route53::package_name: 'python3-certbot-dns-route53' diff --git a/manifests/certonly.pp b/manifests/certonly.pp index 9277bd9..068ff56 100644 --- a/manifests/certonly.pp +++ b/manifests/certonly.pp @@ -1,212 +1,221 @@ # @summary Request a certificate using the `certonly` installer # # This type can be used to request a certificate using the `certonly` installer. # # @param ensure # Intended state of the resource # Will remove certificates for specified domains if set to 'absent'. Will # also remove cronjobs and renewal scripts if `manage_cron` is set to 'true'. # @param domains # An array of domains to include in the CSR. # @param custom_plugin Whether to use a custom plugin in additional_args and disable -a flag. # @param plugin The authenticator plugin to use when requesting the certificate. # @param webroot_paths # An array of webroot paths for the domains in `domains`. # Required if using `plugin => 'webroot'`. If `domains` and # `webroot_paths` are not the same length, the last `webroot_paths` # element will be used for all subsequent domains. # @param letsencrypt_command Command to run letsencrypt # @param additional_args An array of additional command line arguments to pass to the `letsencrypt-auto` command. # @param environment An optional array of environment variables (in addition to VENV_PATH). # @param key_size Size for the RSA public key # @param manage_cron # Indicating whether or not to schedule cron job for renewal. # Runs daily but only renews if near expiration, e.g. within 10 days. # @param suppress_cron_output Redirect cron output to devnull # @param cron_before_command Representation of a command that should be run before renewal command # @param cron_success_command Representation of a command that should be run if the renewal command succeeds. # @param cron_hour # Optional hour(s) that the renewal command should execute. # e.g. '[0,12]' execute at midnight and midday. Default - seeded random hour. # @param cron_minute # Optional minute(s) that the renewal command should execute. # e.g. 0 or '00' or [0,30]. Default - seeded random minute. # @param cron_monthday # Optional string, integer or array of monthday(s) the renewal command should # run. E.g. '2-30/2' to run on even days. Default: Every day. # @param config_dir The path to the configuration directory. # @param pre_hook_commands Array of commands to run in a shell before attempting to obtain/renew the certificate. # @param post_hook_commands Array of command(s) to run in a shell after attempting to obtain/renew the certificate. # @param deploy_hook_commands # Array of command(s) to run in a shell once if the certificate is successfully issued. # Two environmental variables are supplied by certbot: # - $RENEWED_LINEAGE: Points to the live directory with the cert files and key. # Example: /etc/letsencrypt/live/example.com # - $RENEWED_DOMAINS: A space-delimited list of renewed certificate domains. # Example: "example.com www.example.com" # define letsencrypt::certonly ( Enum['present','absent'] $ensure = 'present', Array[String[1]] $domains = [$title], String[1] $cert_name = $title, Boolean $custom_plugin = false, Letsencrypt::Plugin $plugin = 'standalone', Array[Stdlib::Unixpath] $webroot_paths = [], String[1] $letsencrypt_command = $letsencrypt::command, Integer[2048] $key_size = $letsencrypt::key_size, Array[String[1]] $additional_args = [], Array[String[1]] $environment = [], Boolean $manage_cron = false, Boolean $suppress_cron_output = false, Optional[String[1]] $cron_before_command = undef, Optional[String[1]] $cron_success_command = undef, Array[Variant[Integer[0, 59], String[1]]] $cron_monthday = ['*'], Variant[Integer[0,23], String, Array] $cron_hour = fqdn_rand(24, $title), Variant[Integer[0,59], String, Array] $cron_minute = fqdn_rand(60, fqdn_rand_string(10, $title)), Stdlib::Unixpath $config_dir = $letsencrypt::config_dir, Variant[String[1], Array[String[1]]] $pre_hook_commands = [], Variant[String[1], Array[String[1]]] $post_hook_commands = [], Variant[String[1], Array[String[1]]] $deploy_hook_commands = [], ) { if $plugin == 'webroot' and empty($webroot_paths) { fail("The 'webroot_paths' parameter must be specified when using the 'webroot' plugin") } # Wildcard-less title for use in file paths $title_nowc = regsubst($title, '^\*\.', '') if $ensure == 'present' { if ($custom_plugin) { $default_args = "--text --agree-tos --non-interactive certonly --rsa-key-size ${key_size}" } else { $default_args = "--text --agree-tos --non-interactive certonly --rsa-key-size ${key_size} -a ${plugin}" } } else { $default_args = '--text --agree-tos --non-interactive delete' } case $plugin { 'webroot': { $_plugin_args = zip($domains, $webroot_paths).map |$domain| { if $domain[1] { "--webroot-path ${domain[1]} -d '${domain[0]}'" } else { "-d '${domain[0]}'" } } $plugin_args = ["--cert-name '${cert_name}'"] + $_plugin_args } 'dns-rfc2136': { require letsencrypt::plugin::dns_rfc2136 $_domains = join($domains, '\' -d \'') $plugin_args = [ "--cert-name '${cert_name}' -d", "'${_domains}'", "--dns-rfc2136-credentials ${letsencrypt::plugin::dns_rfc2136::config_dir}/dns-rfc2136.ini", "--dns-rfc2136-propagation-seconds ${letsencrypt::plugin::dns_rfc2136::propagation_seconds}", ] } + 'dns-route53': { + require letsencrypt::plugin::dns_route53 + $_domains = join($domains, '\' -d \'') + $plugin_args = [ + "--cert-name '${cert_name}' -d '${_domains}'", + "--dns-route53-propagation-seconds ${letsencrypt::plugin::dns_route53::propagation_seconds}", + ] + } + default: { if $ensure == 'present' { $_domains = join($domains, '\' -d \'') $plugin_args = "--cert-name '${cert_name}' -d '${_domains}'" } else { $plugin_args = "--cert-name '${cert_name}'" } } } $hook_args = ['pre', 'post', 'deploy'].map | String $type | { $commands = getvar("${type}_hook_commands") if (!empty($commands)) { $hook_file = "${config_dir}/renewal-hooks-puppet/${title_nowc}-${type}.sh" letsencrypt::hook { "${title}-${type}": type => $type, hook_file => $hook_file, commands => $commands, before => Exec["letsencrypt certonly ${title}"], } "--${type}-hook \"${hook_file}\"" } else { undef } } # certbot uses --cert-name to generate the file path $live_path_certname = regsubst($cert_name, '^\*\.', '') $live_path = "${config_dir}/live/${live_path_certname}/cert.pem" $_command = flatten([ $letsencrypt_command, $default_args, $plugin_args, $hook_args, $additional_args, ]).filter | $arg | { $arg =~ NotUndef and $arg != [] } $command = join($_command, ' ') $execution_environment = [ "VENV_PATH=${letsencrypt::venv_path}", ] + $environment $verify_domains = join(unique($domains), '\' \'') if $ensure == 'present' { $exec_ensure = { 'unless' => "/usr/local/sbin/letsencrypt-domain-validation ${live_path} '${verify_domains}'" } } else { $exec_ensure = { 'onlyif' => "/usr/local/sbin/letsencrypt-domain-validation ${live_path} '${verify_domains}'" } } exec { "letsencrypt certonly ${title}": command => $command, * => $exec_ensure, path => $facts['path'], environment => $execution_environment, provider => 'shell', require => [ Class['letsencrypt'], File['/usr/local/sbin/letsencrypt-domain-validation'], ], } if $manage_cron { $maincommand = join(["${letsencrypt_command} --keep-until-expiring"] + $_command[1,-1], ' ') $cron_script_ensure = $ensure ? { 'present' => 'file', default => 'absent' } $cron_ensure = $ensure if $suppress_cron_output { $croncommand = "${maincommand} > /dev/null 2>&1" } else { $croncommand = $maincommand } if $cron_before_command { $renewcommand = "(${cron_before_command}) && ${croncommand}" } else { $renewcommand = $croncommand } if $cron_success_command { $cron_cmd = "${renewcommand} && (${cron_success_command})" } else { $cron_cmd = $renewcommand } file { "${letsencrypt::cron_scripts_path}/renew-${title}.sh": ensure => $cron_script_ensure, mode => '0755', owner => 'root', group => $letsencrypt::cron_owner_group, content => template('letsencrypt/renew-script.sh.erb'), } cron { "letsencrypt renew cron ${title}": ensure => $cron_ensure, command => "\"${letsencrypt::cron_scripts_path}/renew-${title}.sh\"", user => root, hour => $cron_hour, minute => $cron_minute, monthday => $cron_monthday, } } } diff --git a/manifests/plugin/dns_route53.pp b/manifests/plugin/dns_route53.pp new file mode 100644 index 0000000..3d3d27b --- /dev/null +++ b/manifests/plugin/dns_route53.pp @@ -0,0 +1,22 @@ +# @summary Installs and configures the dns-route53 plugin +# +# This class installs and configures the Let's Encrypt dns-route53 plugin. +# https://certbot-dns-route53.readthedocs.io +# +# @param propagation_seconds Number of seconds to wait for the DNS server to propagate the DNS-01 challenge. +# @param manage_package Manage the plugin package. +# @param package_name The name of the package to install when $manage_package is true. +# +class letsencrypt::plugin::dns_route53 ( + String[1] $package_name, + Integer $propagation_seconds = 10, + Boolean $manage_package = true, +) { + require letsencrypt + + if $manage_package { + package { $package_name: + ensure => installed, + } + } +} diff --git a/spec/acceptance/letsencrypt_plugin_dns_route53_spec.rb b/spec/acceptance/letsencrypt_plugin_dns_route53_spec.rb new file mode 100644 index 0000000..5b26882 --- /dev/null +++ b/spec/acceptance/letsencrypt_plugin_dns_route53_spec.rb @@ -0,0 +1,40 @@ +require 'spec_helper_acceptance' + +describe 'letsencrypt::plugin::dns_route53' do + supported = case fact('os.family') + when 'Debian' + # Debian 9 has it in backports, Ubuntu started shipping in Bionic + fact('os.release.major') != '9' && fact('os.release.major') != '16.04' + when 'RedHat' + true + else + false + end + + context 'with defaults values' do + pp = <<-PUPPET + class { 'letsencrypt' : + email => 'letsregister@example.com', + config => { + 'server' => 'https://acme-staging-v02.api.letsencrypt.org/directory', + }, + } + class { 'letsencrypt::plugin::dns_route53': + } + PUPPET + + if supported + it 'installs letsencrypt and dns route53 plugin without error' do + apply_manifest(pp, catch_failures: true) + end + it 'installs letsencrypt and dns route53 idempotently' do + apply_manifest(pp, catch_changes: true) + end + + else + it 'fails to install' do + apply_manifest(pp, expect_failures: true) + end + end + end +end diff --git a/spec/classes/plugin/dns_route53_spec.rb b/spec/classes/plugin/dns_route53_spec.rb new file mode 100644 index 0000000..631fc5a --- /dev/null +++ b/spec/classes/plugin/dns_route53_spec.rb @@ -0,0 +1,57 @@ +require 'spec_helper' + +describe 'letsencrypt::plugin::dns_route53' do + on_supported_os.each do |os, facts| + context "on #{os} based operating systems" do + let(:facts) { facts } + let(:params) { {} } + let(:pre_condition) do + <<-PUPPET + class { 'letsencrypt': + email => 'foo@example.com', + } + PUPPET + end + let(:package_name) do + osname = facts[:os]['name'] + osrelease = facts[:os]['release']['major'] + osfull = "#{osname}-#{osrelease}" + case osfull + when 'Debian-10', 'Ubuntu-18.04', 'Fedora-30', 'Fedora-31' + 'python3-certbot-dns-route53' + when 'RedHat-7', 'CentOS-7' + 'python2-certbot-dns-route53' + end + end + + context 'with required parameters' do + it do + if package_name.nil? + is_expected.not_to compile + else + is_expected.to compile.with_all_deps + end + end + + describe 'with manage_package => true' do + let(:params) { super().merge(manage_package: true) } + + it do + if package_name.nil? + is_expected.not_to compile + else + is_expected.to contain_class('letsencrypt::plugin::dns_route53').with_package_name(package_name) + is_expected.to contain_package(package_name).with_ensure('installed') + end + end + end + + describe 'with manage_package => false' do + let(:params) { super().merge(manage_package: false, package_name: 'dns-route53-package') } + + it { is_expected.not_to contain_package('dns-route53-package') } + end + end + end + end +end diff --git a/spec/defines/letsencrypt_certonly_spec.rb b/spec/defines/letsencrypt_certonly_spec.rb index c219ade..63028b7 100644 --- a/spec/defines/letsencrypt_certonly_spec.rb +++ b/spec/defines/letsencrypt_certonly_spec.rb @@ -1,441 +1,461 @@ require 'spec_helper' describe 'letsencrypt::certonly' do on_supported_os.each do |os, facts| context "on #{os} based operating systems" do let :facts do facts end let(:pre_condition) { "class { letsencrypt: email => 'foo@example.com', package_command => 'letsencrypt' }" } # FreeBSD uses a different filesystem path pathprefix = facts[:kernel] == 'FreeBSD' ? '/usr/local' : '' context 'with a single domain' do let(:title) { 'foo.example.com' } it { is_expected.to compile.with_all_deps } it { is_expected.to contain_class('Letsencrypt::Install') } it { is_expected.to contain_class('Letsencrypt::Config') } if facts[:osfamily] == 'FreeBSD' it { is_expected.to contain_file('/usr/local/etc/letsencrypt') } it { is_expected.to contain_ini_setting('/usr/local/etc/letsencrypt/cli.ini email foo@example.com') } it { is_expected.to contain_ini_setting('/usr/local/etc/letsencrypt/cli.ini server https://acme-v02.api.letsencrypt.org/directory') } else it { is_expected.to contain_file('/etc/letsencrypt') } it { is_expected.to contain_package('letsencrypt') } it { is_expected.to contain_ini_setting('/etc/letsencrypt/cli.ini email foo@example.com') } it { is_expected.to contain_ini_setting('/etc/letsencrypt/cli.ini server https://acme-v02.api.letsencrypt.org/directory') } end it { is_expected.to contain_exec('initialize letsencrypt') } it { is_expected.to contain_exec('letsencrypt certonly foo.example.com') } it { is_expected.to contain_exec('letsencrypt certonly foo.example.com').with_unless "/usr/local/sbin/letsencrypt-domain-validation #{pathprefix}/etc/letsencrypt/live/foo.example.com/cert.pem 'foo.example.com'" } end context 'with ensure absent' do let(:title) { 'foo.example.com' } let(:params) { { ensure: 'absent' } } it { is_expected.to contain_exec('letsencrypt certonly foo.example.com') } it { is_expected.to contain_exec('letsencrypt certonly foo.example.com').with_command "letsencrypt --text --agree-tos --non-interactive delete --cert-name 'foo.example.com'" } it { is_expected.to contain_exec('letsencrypt certonly foo.example.com').with_onlyif "/usr/local/sbin/letsencrypt-domain-validation #{pathprefix}/etc/letsencrypt/live/foo.example.com/cert.pem 'foo.example.com'" } end context 'with multiple domains' do let(:title) { 'foo' } let(:params) { { domains: ['foo.example.com', 'bar.example.com', '*.example.com'] } } it { is_expected.to compile.with_all_deps } it { is_expected.to contain_exec('letsencrypt certonly foo').with_command "letsencrypt --text --agree-tos --non-interactive certonly --rsa-key-size 4096 -a standalone --cert-name 'foo' -d 'foo.example.com' -d 'bar.example.com' -d '*.example.com'" } end context 'with custom cert-name' do let(:title) { 'foo' } let(:params) { { cert_name: 'bar.example.com' } } it { is_expected.to compile.with_all_deps } it { is_expected.to contain_exec('letsencrypt certonly foo').with_command "letsencrypt --text --agree-tos --non-interactive certonly --rsa-key-size 4096 -a standalone --cert-name 'bar.example.com' -d 'foo'" } end context 'with custom command' do let(:title) { 'foo.example.com' } let(:params) { { letsencrypt_command: '/usr/lib/letsencrypt/letsencrypt-auto' } } it { is_expected.to compile.with_all_deps } it { is_expected.to contain_exec('letsencrypt certonly foo.example.com').with_command '/usr/lib/letsencrypt/letsencrypt-auto --text --agree-tos --non-interactive certonly --rsa-key-size 4096 -a standalone --cert-name \'foo.example.com\' -d \'foo.example.com\'' } end context 'with webroot plugin' do let(:title) { 'foo.example.com' } let(:params) do { plugin: 'webroot', webroot_paths: ['/var/www/foo'] } end it { is_expected.to compile.with_all_deps } it { is_expected.to contain_exec('letsencrypt certonly foo.example.com').with_command "letsencrypt --text --agree-tos --non-interactive certonly --rsa-key-size 4096 -a webroot --cert-name 'foo.example.com' --webroot-path /var/www/foo -d 'foo.example.com'" } end context 'with webroot plugin and multiple domains' do let(:title) { 'foo' } let(:params) do { domains: ['foo.example.com', 'bar.example.com'], plugin: 'webroot', webroot_paths: ['/var/www/foo', '/var/www/bar'] } end it { is_expected.to compile.with_all_deps } it { is_expected.to contain_exec('letsencrypt certonly foo').with_command "letsencrypt --text --agree-tos --non-interactive certonly --rsa-key-size 4096 -a webroot --cert-name 'foo' --webroot-path /var/www/foo -d 'foo.example.com' --webroot-path /var/www/bar -d 'bar.example.com'" } end context 'with webroot plugin, one webroot, and multiple domains' do let(:title) { 'foo' } let(:params) do { domains: ['foo.example.com', 'bar.example.com'], plugin: 'webroot', webroot_paths: ['/var/www/foo'] } end it { is_expected.to compile.with_all_deps } it { is_expected.to contain_exec('letsencrypt certonly foo').with_command "letsencrypt --text --agree-tos --non-interactive certonly --rsa-key-size 4096 -a webroot --cert-name 'foo' --webroot-path /var/www/foo -d 'foo.example.com' -d 'bar.example.com'" } end context 'with webroot plugin and no webroot_paths' do let(:title) { 'foo.example.com' } let(:params) { { plugin: 'webroot' } } it { is_expected.not_to compile.with_all_deps } it { is_expected.to raise_error Puppet::Error, %r{'webroot_paths' parameter must be specified} } end context 'with dns-rfc2136 plugin' do let(:title) { 'foo.example.com' } let(:params) { { plugin: 'dns-rfc2136', letsencrypt_command: 'letsencrypt' } } let(:pre_condition) do <<-PUPPET class { 'letsencrypt': email => 'foo@example.com', config_dir => '/etc/letsencrypt', } class { 'letsencrypt::plugin::dns_rfc2136': server => '192.0.2.1', key_name => 'certbot', key_secret => 'secret', package_name => 'irrelevant', } PUPPET end it { is_expected.to compile.with_all_deps } it { is_expected.to contain_class('letsencrypt::plugin::dns_rfc2136') } it { is_expected.to contain_exec('letsencrypt certonly foo.example.com').with_command "letsencrypt --text --agree-tos --non-interactive certonly --rsa-key-size 4096 -a dns-rfc2136 --cert-name 'foo.example.com' -d 'foo.example.com' --dns-rfc2136-credentials /etc/letsencrypt/dns-rfc2136.ini --dns-rfc2136-propagation-seconds 10" } end + context 'with dns-route53 plugin' do + let(:title) { 'foo.example.com' } + let(:params) { { plugin: 'dns-route53', letsencrypt_command: 'letsencrypt' } } + let(:pre_condition) do + <<-PUPPET + class { 'letsencrypt': + email => 'foo@example.com', + config_dir => '/etc/letsencrypt', + } + class { 'letsencrypt::plugin::dns_route53': + package_name => 'irrelevant', + } + PUPPET + end + + it { is_expected.to compile.with_all_deps } + it { is_expected.to contain_class('letsencrypt::plugin::dns_route53') } + it { is_expected.to contain_exec('letsencrypt certonly foo.example.com').with_command "letsencrypt --text --agree-tos --non-interactive certonly --rsa-key-size 4096 -a dns-route53 --cert-name 'foo.example.com' -d 'foo.example.com' --dns-route53-propagation-seconds 10" } + end + context 'with custom plugin' do let(:title) { 'foo.example.com' } let(:params) { { plugin: 'apache' } } it { is_expected.to compile.with_all_deps } it { is_expected.to contain_exec('letsencrypt certonly foo.example.com').with_command "letsencrypt --text --agree-tos --non-interactive certonly --rsa-key-size 4096 -a apache --cert-name 'foo.example.com' -d 'foo.example.com'" } end context 'with custom plugin and manage_cron' do let(:title) { 'foo.example.com' } let(:params) do { plugin: 'apache', manage_cron: true } end it { is_expected.to compile.with_all_deps } it { is_expected.to contain_cron('letsencrypt renew cron foo.example.com').with_command('"/var/lib/puppet/letsencrypt/renew-foo.example.com.sh"').with_ensure('present') } it { is_expected.to contain_file('/var/lib/puppet/letsencrypt/renew-foo.example.com.sh').with_ensure('file').with_content("#!/bin/sh\nexport VENV_PATH=/opt/letsencrypt/.venv\nletsencrypt --keep-until-expiring --text --agree-tos --non-interactive certonly --rsa-key-size 4096 -a apache --cert-name 'foo.example.com' -d 'foo.example.com'\n") } end context 'with hook' do context 'pre' do let(:title) { 'foo.example.com' } let(:params) { { config_dir: '/etc/letsencrypt', pre_hook_commands: ['FooBar'] } } it do is_expected.to compile.with_all_deps is_expected.to contain_letsencrypt__hook('foo.example.com-pre').with_hook_file('/etc/letsencrypt/renewal-hooks-puppet/foo.example.com-pre.sh') end end context 'pre with wildcard domain' do let(:title) { '*.example.com' } let(:params) { { config_dir: '/etc/letsencrypt', pre_hook_commands: ['FooBar'] } } it do is_expected.to compile.with_all_deps is_expected.to contain_letsencrypt__hook('*.example.com-pre').with_hook_file('/etc/letsencrypt/renewal-hooks-puppet/example.com-pre.sh') end end context 'post' do let(:title) { 'foo.example.com' } let(:params) { { config_dir: '/etc/letsencrypt', post_hook_commands: ['FooBar'] } } it do is_expected.to compile.with_all_deps is_expected.to contain_letsencrypt__hook('foo.example.com-post').with_hook_file('/etc/letsencrypt/renewal-hooks-puppet/foo.example.com-post.sh') end end context 'deploy' do let(:title) { 'foo.example.com' } let(:params) { { config_dir: '/etc/letsencrypt', deploy_hook_commands: ['FooBar'] } } it do is_expected.to compile.with_all_deps is_expected.to contain_letsencrypt__hook('foo.example.com-deploy').with_hook_file('/etc/letsencrypt/renewal-hooks-puppet/foo.example.com-deploy.sh') end end end # context 'with hook' context 'with manage_cron and defined cron_hour (integer)' do let(:title) { 'foo.example.com' } let(:params) do { cron_hour: 13, manage_cron: true } end it { is_expected.to compile.with_all_deps } it { is_expected.to contain_cron('letsencrypt renew cron foo.example.com').with_hour(13).with_ensure('present') } it { is_expected.to contain_file('/var/lib/puppet/letsencrypt/renew-foo.example.com.sh').with_ensure('file').with_content("#!/bin/sh\nexport VENV_PATH=/opt/letsencrypt/.venv\nletsencrypt --keep-until-expiring --text --agree-tos --non-interactive certonly --rsa-key-size 4096 -a standalone --cert-name 'foo.example.com' -d 'foo.example.com'\n") } end context 'with manage_cron and out of range defined cron_hour (integer)' do let(:title) { 'foo.example.com' } let(:params) do { cron_hour: 24, manage_cron: true } end it { is_expected.not_to compile.with_all_deps } it { is_expected.to raise_error Puppet::Error } end context 'with manage_cron and defined cron_hour (string)' do let(:title) { 'foo.example.com' } let(:params) do { cron_hour: '00', manage_cron: true } end it { is_expected.to compile.with_all_deps } it { is_expected.to contain_cron('letsencrypt renew cron foo.example.com').with_hour('00').with_ensure('present') } it { is_expected.to contain_file('/var/lib/puppet/letsencrypt/renew-foo.example.com.sh').with_ensure('file').with_content("#!/bin/sh\nexport VENV_PATH=/opt/letsencrypt/.venv\nletsencrypt --keep-until-expiring --text --agree-tos --non-interactive certonly --rsa-key-size 4096 -a standalone --cert-name 'foo.example.com' -d 'foo.example.com'\n") } end context 'with manage_cron and defined cron_hour (array)' do let(:title) { 'foo.example.com' } let(:params) do { cron_hour: [1, 13], manage_cron: true } end it { is_expected.to compile.with_all_deps } it { is_expected.to contain_cron('letsencrypt renew cron foo.example.com').with_hour([1, 13]).with_ensure('present') } it { is_expected.to contain_file('/var/lib/puppet/letsencrypt/renew-foo.example.com.sh').with_ensure('file').with_content("#!/bin/sh\nexport VENV_PATH=/opt/letsencrypt/.venv\nletsencrypt --keep-until-expiring --text --agree-tos --non-interactive certonly --rsa-key-size 4096 -a standalone --cert-name 'foo.example.com' -d 'foo.example.com'\n") } end context 'with manage_cron and defined cron_minute (integer)' do let(:title) { 'foo.example.com' } let(:params) do { cron_minute: 15, manage_cron: true } end it { is_expected.to compile.with_all_deps } it { is_expected.to contain_cron('letsencrypt renew cron foo.example.com').with_minute(15).with_ensure('present') } it { is_expected.to contain_file('/var/lib/puppet/letsencrypt/renew-foo.example.com.sh').with_ensure('file').with_content("#!/bin/sh\nexport VENV_PATH=/opt/letsencrypt/.venv\nletsencrypt --keep-until-expiring --text --agree-tos --non-interactive certonly --rsa-key-size 4096 -a standalone --cert-name 'foo.example.com' -d 'foo.example.com'\n") } end context 'with manage_cron and out of range defined cron_hour (integer)' do let(:title) { 'foo.example.com' } let(:params) do { cron_hour: 66, manage_cron: true } end it { is_expected.not_to compile.with_all_deps } it { is_expected.to raise_error Puppet::Error } end context 'with manage_cron and defined cron_minute (string)' do let(:title) { 'foo.example.com' } let(:params) do { cron_minute: '15', manage_cron: true } end it { is_expected.to compile.with_all_deps } it { is_expected.to contain_cron('letsencrypt renew cron foo.example.com').with_minute('15').with_ensure('present') } it { is_expected.to contain_file('/var/lib/puppet/letsencrypt/renew-foo.example.com.sh').with_ensure('file').with_content("#!/bin/sh\nexport VENV_PATH=/opt/letsencrypt/.venv\nletsencrypt --keep-until-expiring --text --agree-tos --non-interactive certonly --rsa-key-size 4096 -a standalone --cert-name 'foo.example.com' -d 'foo.example.com'\n") } end context 'with manage_cron and defined cron_minute (array)' do let(:title) { 'foo.example.com' } let(:params) do { cron_minute: [0, 30], manage_cron: true } end it { is_expected.to compile.with_all_deps } it { is_expected.to contain_cron('letsencrypt renew cron foo.example.com').with_minute([0, 30]).with_ensure('present') } it { is_expected.to contain_file('/var/lib/puppet/letsencrypt/renew-foo.example.com.sh').with_ensure('file').with_content("#!/bin/sh\nexport VENV_PATH=/opt/letsencrypt/.venv\nletsencrypt --keep-until-expiring --text --agree-tos --non-interactive certonly --rsa-key-size 4096 -a standalone --cert-name 'foo.example.com' -d 'foo.example.com'\n") } end context 'with manage_cron and ensure absent' do let(:title) { 'foo.example.com' } let(:params) do { ensure: 'absent', manage_cron: true } end it { is_expected.to compile.with_all_deps } it { is_expected.to contain_cron('letsencrypt renew cron foo.example.com').with_ensure('absent') } it { is_expected.to contain_file('/var/lib/puppet/letsencrypt/renew-foo.example.com.sh').with_ensure('absent') } end context 'with custom puppet_vardir path and manage_cron' do let :facts do super().merge(puppet_vardir: '/tmp/custom_vardir') end let(:title) { 'foo.example.com' } let(:params) do { plugin: 'apache', manage_cron: true } end it { is_expected.to compile.with_all_deps } it { is_expected.to contain_file('/tmp/custom_vardir/letsencrypt').with_ensure('directory') } it { is_expected.to contain_cron('letsencrypt renew cron foo.example.com').with_command '"/tmp/custom_vardir/letsencrypt/renew-foo.example.com.sh"' } it { is_expected.to contain_file('/tmp/custom_vardir/letsencrypt/renew-foo.example.com.sh').with_ensure('file').with_content("#!/bin/sh\nexport VENV_PATH=/opt/letsencrypt/.venv\nletsencrypt --keep-until-expiring --text --agree-tos --non-interactive certonly --rsa-key-size 4096 -a apache --cert-name 'foo.example.com' -d 'foo.example.com'\n") } end context 'with custom plugin and manage cron and cron_success_command' do let(:title) { 'foo.example.com' } let(:params) do { plugin: 'apache', manage_cron: true, cron_before_command: 'echo before', cron_success_command: 'echo success' } end it { is_expected.to compile.with_all_deps } it { is_expected.to contain_cron('letsencrypt renew cron foo.example.com').with_command '"/var/lib/puppet/letsencrypt/renew-foo.example.com.sh"' } it { is_expected.to contain_file('/var/lib/puppet/letsencrypt/renew-foo.example.com.sh').with_ensure('file').with_content("#!/bin/sh\nexport VENV_PATH=/opt/letsencrypt/.venv\n(echo before) && letsencrypt --keep-until-expiring --text --agree-tos --non-interactive certonly --rsa-key-size 4096 -a apache --cert-name 'foo.example.com' -d 'foo.example.com' && (echo success)\n") } end context 'without plugin' do let(:title) { 'foo.example.com' } let(:params) { { custom_plugin: true } } it { is_expected.to contain_exec('letsencrypt certonly foo.example.com').with_command "letsencrypt --text --agree-tos --non-interactive certonly --rsa-key-size 4096 --cert-name 'foo.example.com' -d 'foo.example.com'" } end context 'with invalid plugin' do let(:title) { 'foo.example.com' } let(:params) { { plugin: 'bad' } } it { is_expected.not_to compile.with_all_deps } it { is_expected.to raise_error Puppet::Error } end context 'when specifying additional arguments' do let(:title) { 'foo.example.com' } let(:params) { { additional_args: ['--foo bar', '--baz quux'] } } it { is_expected.to compile.with_all_deps } it { is_expected.to contain_exec('letsencrypt certonly foo.example.com').with_command "letsencrypt --text --agree-tos --non-interactive certonly --rsa-key-size 4096 -a standalone --cert-name 'foo.example.com' -d 'foo.example.com' --foo bar --baz quux" } end describe 'when specifying custom environment variables' do let(:title) { 'foo.example.com' } let(:params) { { environment: ['FOO=bar', 'FIZZ=buzz'] } } it { is_expected.to compile.with_all_deps } it { is_expected.to contain_exec('letsencrypt certonly foo.example.com').with_environment(['VENV_PATH=/opt/letsencrypt/.venv', 'FOO=bar', 'FIZZ=buzz']) } end context 'with custom environment variables and manage_cron' do let(:title) { 'foo.example.com' } let(:params) { { environment: ['FOO=bar', 'FIZZ=buzz'], manage_cron: true } } it { is_expected.to compile.with_all_deps } it { is_expected.to contain_file('/var/lib/puppet/letsencrypt/renew-foo.example.com.sh').with_content "#!/bin/sh\nexport VENV_PATH=/opt/letsencrypt/.venv\nexport FOO=bar\nexport FIZZ=buzz\nletsencrypt --keep-until-expiring --text --agree-tos --non-interactive certonly --rsa-key-size 4096 -a standalone --cert-name 'foo.example.com' -d 'foo.example.com'\n" } end context 'with manage cron and suppress_cron_output' do\ let(:title) { 'foo.example.com' } let(:params) do { manage_cron: true, suppress_cron_output: true } end it { is_expected.to compile.with_all_deps } it { is_expected.to contain_cron('letsencrypt renew cron foo.example.com').with_command('"/var/lib/puppet/letsencrypt/renew-foo.example.com.sh"').with_ensure('present') } it { is_expected.to contain_file('/var/lib/puppet/letsencrypt/renew-foo.example.com.sh').with_ensure('file').with_content("#!/bin/sh\nexport VENV_PATH=/opt/letsencrypt/.venv\nletsencrypt --keep-until-expiring --text --agree-tos --non-interactive certonly --rsa-key-size 4096 -a standalone --cert-name 'foo.example.com' -d 'foo.example.com' > /dev/null 2>&1\n") } end context 'with manage cron and custom day of month' do let(:title) { 'foo.example.com' } let(:params) do { manage_cron: true, cron_monthday: [1, 15] } end it { is_expected.to compile.with_all_deps } it { is_expected.to contain_cron('letsencrypt renew cron foo.example.com').with(monthday: [1, 15]).with_ensure('present') } it { is_expected.to contain_file('/var/lib/puppet/letsencrypt/renew-foo.example.com.sh').with_ensure('file').with_content("#!/bin/sh\nexport VENV_PATH=/opt/letsencrypt/.venv\nletsencrypt --keep-until-expiring --text --agree-tos --non-interactive certonly --rsa-key-size 4096 -a standalone --cert-name 'foo.example.com' -d 'foo.example.com'\n") } end context 'with custom config_dir' do let(:title) { 'foo.example.com' } let(:pre_condition) { "class { letsencrypt: email => 'foo@example.com', config_dir => '/foo/bar/baz', package_command => 'letsencrypt'}" } it { is_expected.to compile.with_all_deps } it { is_expected.to contain_file('/foo/bar/baz').with_ensure('directory') } it { is_expected.to contain_exec('letsencrypt certonly foo.example.com').with_unless '/usr/local/sbin/letsencrypt-domain-validation /foo/bar/baz/live/foo.example.com/cert.pem \'foo.example.com\'' } end context 'on FreeBSD', if: facts[:os]['name'] == 'FreeBSD' do let(:title) { 'foo.example.com' } let(:pre_condition) { "class { letsencrypt: email => 'foo@example.com'}" } it { is_expected.to compile.with_all_deps } it { is_expected.to contain_exec('letsencrypt certonly foo.example.com').with_command %r{^certbot} } it { is_expected.to contain_ini_setting('/usr/local/etc/letsencrypt/cli.ini email foo@example.com') } it { is_expected.to contain_ini_setting('/usr/local/etc/letsencrypt/cli.ini server https://acme-v02.api.letsencrypt.org/directory') } it { is_expected.to contain_file('/usr/local/etc/letsencrypt').with_ensure('directory') } it { is_expected.to contain_exec('letsencrypt certonly foo.example.com').with_unless '/usr/local/sbin/letsencrypt-domain-validation /usr/local/etc/letsencrypt/live/foo.example.com/cert.pem \'foo.example.com\'' } end end end end