diff --git a/REFERENCE.md b/REFERENCE.md
index 4184f8bf..276b4607 100644
--- a/REFERENCE.md
+++ b/REFERENCE.md
@@ -10,6 +10,7 @@
* [`letsencrypt`](#letsencrypt): Install and configure Certbot, the LetsEncrypt client
* [`letsencrypt::plugin::dns_cloudflare`](#letsencrypt--plugin--dns_cloudflare): Installs and configures the dns-cloudflare plugin
+* [`letsencrypt::plugin::dns_gandi`](#letsencrypt--plugin--dns_gandi): Installs and configures the dns-gandi plugin
* [`letsencrypt::plugin::dns_linode`](#letsencrypt--plugin--dns_linode): Installs and configures the dns-linode plugin
* [`letsencrypt::plugin::dns_rfc2136`](#letsencrypt--plugin--dns_rfc2136): Installs and configures the dns-rfc2136 plugin
* [`letsencrypt::plugin::dns_route53`](#letsencrypt--plugin--dns_route53): Installs and configures the dns-route53 plugin
@@ -412,6 +413,59 @@ Number of seconds to wait for the DNS server to propagate the DNS-01 challenge.
Default value: `10`
+### `letsencrypt::plugin::dns_gandi`
+
+This class installs and configures the Let's Encrypt dns-gandi plugin.
+https://pypi.org/project/certbot-plugin-gandi/
+
+#### Parameters
+
+The following parameters are available in the `letsencrypt::plugin::dns_gandi` class:
+
+* [`api_key`](#-letsencrypt--plugin--dns_gandi--api_key)
+* [`personal_access_token`](#-letsencrypt--plugin--dns_gandi--personal_access_token)
+* [`package_name`](#-letsencrypt--plugin--dns_gandi--package_name)
+* [`config_file`](#-letsencrypt--plugin--dns_gandi--config_file)
+* [`manage_package`](#-letsencrypt--plugin--dns_gandi--manage_package)
+
+##### `api_key`
+
+Data type: `Optional[String[1]]`
+
+Gandi production api key secret. You can get it in your security tab of your account
+
+Default value: `undef`
+
+##### `personal_access_token`
+
+Data type: `Optional[String[1]]`
+
+Gandi personal access token(PAT). You can get it in your security tab of your account
+
+Default value: `undef`
+
+##### `package_name`
+
+Data type: `String[1]`
+
+The name of the package to install when $manage_package is true.
+
+##### `config_file`
+
+Data type: `Stdlib::Absolutepath`
+
+The path to the configuration file.
+
+Default value: `"${letsencrypt::config_dir}/dns-gandi.ini"`
+
+##### `manage_package`
+
+Data type: `Boolean`
+
+Manage the plugin package.
+
+Default value: `true`
+
### `letsencrypt::plugin::dns_linode`
This class installs and configures the Let's Encrypt dns-linode plugin.
@@ -1135,5 +1189,5 @@ Variant[Integer[0,31], String[1], Array[
List of accepted plugins
-Alias of `Enum['apache', 'standalone', 'webroot', 'nginx', 'dns-azure', 'dns-route53', 'dns-google', 'dns-cloudflare', 'dns-linode', 'dns-rfc2136', 'manual']`
+Alias of `Enum['apache', 'standalone', 'webroot', 'nginx', 'dns-azure', 'dns-route53', 'dns-google', 'dns-cloudflare', 'dns-linode', 'dns-rfc2136', 'dns-gandi', 'manual']`
diff --git a/data/os/Debian/11.yaml b/data/os/Debian/11.yaml
new file mode 100644
index 00000000..78ee20ed
--- /dev/null
+++ b/data/os/Debian/11.yaml
@@ -0,0 +1,2 @@
+---
+letsencrypt::plugin::dns_gandi::package_name: python3-certbot-dns-gandi
diff --git a/data/os/Ubuntu/20.04.yaml b/data/os/Ubuntu/20.04.yaml
new file mode 100644
index 00000000..78ee20ed
--- /dev/null
+++ b/data/os/Ubuntu/20.04.yaml
@@ -0,0 +1,2 @@
+---
+letsencrypt::plugin::dns_gandi::package_name: python3-certbot-dns-gandi
diff --git a/data/os/Ubuntu/22.04.yaml b/data/os/Ubuntu/22.04.yaml
new file mode 100644
index 00000000..78ee20ed
--- /dev/null
+++ b/data/os/Ubuntu/22.04.yaml
@@ -0,0 +1,2 @@
+---
+letsencrypt::plugin::dns_gandi::package_name: python3-certbot-dns-gandi
diff --git a/data/os/Ubuntu/24.04.yaml b/data/os/Ubuntu/24.04.yaml
new file mode 100644
index 00000000..78ee20ed
--- /dev/null
+++ b/data/os/Ubuntu/24.04.yaml
@@ -0,0 +1,2 @@
+---
+letsencrypt::plugin::dns_gandi::package_name: python3-certbot-dns-gandi
diff --git a/manifests/certonly.pp b/manifests/certonly.pp
index 78c78cdd..495d9689 100644
--- a/manifests/certonly.pp
+++ b/manifests/certonly.pp
@@ -226,6 +226,16 @@
}
}
+ 'dns-gandi': {
+ require letsencrypt::plugin::dns_gandi
+ $_domains = join($domains, '\' -d \'')
+ $plugin_args = [
+ "--cert-name '${cert_name}' -d",
+ "'${_domains}'",
+ "--dns-gandi-credentials ${letsencrypt::config_dir}/dns-gandi.ini",
+ ]
+ }
+
default: {
if $ensure == 'present' {
$_domains = join($domains, '\' -d \'')
diff --git a/manifests/plugin/dns_gandi.pp b/manifests/plugin/dns_gandi.pp
new file mode 100644
index 00000000..e8402d81
--- /dev/null
+++ b/manifests/plugin/dns_gandi.pp
@@ -0,0 +1,49 @@
+# @summary Installs and configures the dns-gandi plugin
+#
+# This class installs and configures the Let's Encrypt dns-gandi plugin.
+# https://pypi.org/project/certbot-plugin-gandi/
+#
+# @param api_key Gandi production api key secret. You can get it in your security tab of your account
+# @param personal_access_token Gandi personal access token(PAT). You can get it in your security tab of your account
+# @param package_name The name of the package to install when $manage_package is true.
+# @param config_file The path to the configuration file.
+# @param manage_package Manage the plugin package.
+#
+class letsencrypt::plugin::dns_gandi (
+ String[1] $package_name,
+ Optional[String[1]] $api_key = undef,
+ Optional[String[1]] $personal_access_token = undef,
+ Stdlib::Absolutepath $config_file = "${letsencrypt::config_dir}/dns-gandi.ini",
+ Boolean $manage_package = true,
+) {
+ require letsencrypt
+
+ if $manage_package {
+ package { $package_name:
+ ensure => installed,
+ before => File[$config_file],
+ }
+ }
+
+ if $api_key != undef {
+ $ini_vars = {
+ 'dns_gandi_api_key' => $api_key,
+ }
+ } elsif $personal_access_token != undef {
+ $ini_vars = {
+ 'dns_gandi_token' => $personal_access_token,
+ }
+ } else {
+ fail("expects a value for parameter 'api_key' or 'personal_access_token'")
+ }
+
+ file { $config_file:
+ ensure => file,
+ owner => 'root',
+ group => 'root',
+ mode => '0400',
+ content => epp('letsencrypt/ini.epp', {
+ vars => { '' => $ini_vars },
+ }),
+ }
+}
diff --git a/spec/acceptance/letsencrypt_plugin_dns_gandi_spec.rb b/spec/acceptance/letsencrypt_plugin_dns_gandi_spec.rb
new file mode 100644
index 00000000..8f026857
--- /dev/null
+++ b/spec/acceptance/letsencrypt_plugin_dns_gandi_spec.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require 'spec_helper_acceptance'
+
+describe 'letsencrypt::plugin::dns_gandi', if: supported_os_gandi(fact('os')) do
+ it_behaves_like 'an idempotent resource' do
+ let(:manifest) do
+ <<-PUPPET
+ include letsencrypt
+ class { 'letsencrypt::plugin::dns_gandi':
+ api_key => 'dummy-gandi-api-token',
+ }
+ PUPPET
+ end
+ end
+
+ describe file('/etc/letsencrypt/dns-gandi.ini') do
+ it { is_expected.to be_file }
+ it { is_expected.to be_owned_by 'root' }
+ it { is_expected.to be_grouped_into 'root' }
+ it { is_expected.to be_mode 400 }
+ end
+end
diff --git a/spec/classes/plugin/dns_gandi_spec.rb b/spec/classes/plugin/dns_gandi_spec.rb
new file mode 100644
index 00000000..701e57c4
--- /dev/null
+++ b/spec/classes/plugin/dns_gandi_spec.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'letsencrypt::plugin::dns_gandi' do
+ on_supported_os.each do |os, facts|
+ next unless supported_os_gandi(os)
+
+ context "on #{os} based operating systems" do
+ let(:facts) { facts }
+ let(:params) { { 'api_key' => 'dummy-gandi-api-token' } }
+ let(:pre_condition) do
+ <<-PUPPET
+ class { 'letsencrypt':
+ email => 'foo@example.com',
+ }
+ PUPPET
+ end
+ let(:package_name) do
+ 'python3-certbot-dns-gandi'
+ end
+
+ context 'with required parameters' do
+ it do
+ is_expected.to compile.with_all_deps
+ end
+
+ describe 'with manage_package => true' do
+ let(:params) { super().merge(manage_package: true) }
+
+ it do
+ is_expected.to contain_class('letsencrypt::plugin::dns_gandi').with_package_name(package_name)
+ is_expected.to contain_package(package_name).with_ensure('installed')
+ end
+ end
+
+ describe 'with manage_package => false' do
+ let(:params) { super().merge(manage_package: false, package_name: 'dns-gandi-package') }
+
+ it { is_expected.not_to contain_package('dns-gandi-package') }
+ end
+ end
+ end
+ end
+end
diff --git a/spec/defines/letsencrypt_certonly_spec.rb b/spec/defines/letsencrypt_certonly_spec.rb
index 2c616480..8adc21cc 100644
--- a/spec/defines/letsencrypt_certonly_spec.rb
+++ b/spec/defines/letsencrypt_certonly_spec.rb
@@ -227,6 +227,66 @@ class { 'letsencrypt::plugin::dns_linode':
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-linode --cert-name 'foo.example.com' -d 'foo.example.com' --dns-linode --dns-linode-credentials /etc/letsencrypt/dns-linode.ini --dns-linode-propagation-seconds 120" }
end
+ context 'with dns-gandi plugin with api_key' do
+ let(:title) { 'foo.example.com' }
+ let(:params) { { plugin: 'dns-gandi', letsencrypt_command: 'letsencrypt' } }
+ let(:pre_condition) do
+ <<-PUPPET
+ class { 'letsencrypt':
+ email => 'foo@example.com',
+ config_dir => '/etc/letsencrypt',
+ }
+ class { 'letsencrypt::plugin::dns_gandi':
+ package_name => 'irrelevant',
+ api_key => 'dummy-gandi-api-token',
+ }
+ PUPPET
+ end
+
+ it { is_expected.to compile.with_all_deps }
+ it { is_expected.to contain_class('letsencrypt::plugin::dns_gandi') }
+ 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-gandi --cert-name 'foo.example.com' -d 'foo.example.com' --dns-gandi-credentials /etc/letsencrypt/dns-gandi.ini" }
+ end
+
+ context 'with dns-gandi plugin with personal_access_token' do
+ let(:title) { 'foo.example.com' }
+ let(:params) { { plugin: 'dns-gandi', letsencrypt_command: 'letsencrypt' } }
+ let(:pre_condition) do
+ <<-PUPPET
+ class { 'letsencrypt':
+ email => 'foo@example.com',
+ config_dir => '/etc/letsencrypt',
+ }
+ class { 'letsencrypt::plugin::dns_gandi':
+ package_name => 'irrelevant',
+ personal_access_token => 'dummy-pat',
+ }
+ PUPPET
+ end
+
+ it { is_expected.to compile.with_all_deps }
+ it { is_expected.to contain_class('letsencrypt::plugin::dns_gandi') }
+ 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-gandi --cert-name 'foo.example.com' -d 'foo.example.com' --dns-gandi-credentials /etc/letsencrypt/dns-gandi.ini" }
+ end
+
+ context 'with dns-gandi plugin without api_key or personal_access_token' do
+ let(:title) { 'foo.example.com' }
+ let(:params) { { plugin: 'dns-gandi', letsencrypt_command: 'letsencrypt' } }
+ let(:pre_condition) do
+ <<-PUPPET
+ class { 'letsencrypt':
+ email => 'foo@example.com',
+ config_dir => '/etc/letsencrypt',
+ }
+ class { 'letsencrypt::plugin::dns_gandi':
+ package_name => 'irrelevant',
+ }
+ PUPPET
+ end
+
+ it { is_expected.to compile.and_raise_error(%r{expects a value for parameter 'api_key' or 'personal_access_token'}) }
+ end
+
context 'with custom plugin' do
let(:title) { 'foo.example.com' }
let(:params) { { plugin: 'apache' } }
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index d9a3f293..78a1be70 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -8,6 +8,7 @@
ENV['COVERAGE'] ||= 'yes' if Dir.exist?(File.expand_path('../lib', __dir__))
require 'voxpupuli/test/spec_helper'
+require 'spec_helper_local'
RSpec.configure do |c|
c.facterdb_string_keys = true
diff --git a/spec/spec_helper_acceptance.rb b/spec/spec_helper_acceptance.rb
index b4f352d4..6421f5ba 100644
--- a/spec/spec_helper_acceptance.rb
+++ b/spec/spec_helper_acceptance.rb
@@ -1,6 +1,7 @@
# frozen_string_literal: true
require 'voxpupuli/acceptance/spec_helper_acceptance'
+require 'spec_helper_local'
configure_beaker do |host|
# docker image does not provide cron in all cases
diff --git a/spec/spec_helper_local.rb b/spec/spec_helper_local.rb
new file mode 100644
index 00000000..ae675a9b
--- /dev/null
+++ b/spec/spec_helper_local.rb
@@ -0,0 +1,6 @@
+# frozen_string_literal: true
+
+def supported_os_gandi(os)
+ # Gandi plugin is only supported on debian 11 and ubuntu 20.04 and superiors
+ (os['name'] == 'Debian' && os['release']['major'].to_i >= 11) || (os['name'] == 'Ubuntu' && os['release']['major'].to_i >= 20)
+end
diff --git a/types/plugin.pp b/types/plugin.pp
index 6083293a..07c7a50e 100644
--- a/types/plugin.pp
+++ b/types/plugin.pp
@@ -10,5 +10,6 @@
'dns-cloudflare',
'dns-linode',
'dns-rfc2136',
+ 'dns-gandi',
'manual',
]