diff --git a/README.md b/README.md index 44cd89d05c..feb82889fe 100644 --- a/README.md +++ b/README.md @@ -225,6 +225,27 @@ apt::source { 'puppetlabs': +### Generating a DEB822 .sources file + +You can also generate a DEB822 format .sources file. This example covers most of the available options. + +Use the `source_format` parameter to choose between 'list' and 'sources' (DEB822) formats. +```puppet +apt::source { 'debian': + source_format => 'sources' + comment => 'Official Debian Repository', + enabled => true, + types => ['deb', 'deb-src'], + location => ['http://fr.debian.org/debian', 'http://de.debian.org/debian'] + release => ['stable', 'stable-updates', 'stable-backports'], + repos => ['main', 'contrib', 'non-free'], + architecture => ['amd64', 'i386'], + allow_unsigned => true, + keyring => '/etc/apt/keyrings/debian.gpg' + notify_update => false +} +``` + ### Configure Apt from Hiera Instead of specifying your sources directly as resources, you can instead just include the `apt` class, which will pick up the values automatically from hiera. diff --git a/REFERENCE.md b/REFERENCE.md index 980848ac4e..08dd08a356 100644 --- a/REFERENCE.md +++ b/REFERENCE.md @@ -402,6 +402,10 @@ Default value: 'path' => $sources_list_d, 'ext' => '.list', }, + 'sources' => { + 'path' => $sources_list_d, + 'ext' => '.sources', + }, } ``` @@ -1088,11 +1092,25 @@ apt::source { 'puppetlabs': } ``` +##### Install the puppetlabs apt source (deb822 format) + +```puppet +apt::source { 'puppetlabs': + source_format => 'sources' + location => ['http://apt.puppetlabs.com'], + repos => ['puppet8'], + keyring => '/etc/apt/keyrings/puppetlabs.gpg', +} +``` + #### Parameters The following parameters are available in the `apt::source` defined type: +* [`source_format`](#-apt--source--source_format) * [`location`](#-apt--source--location) +* [`types`](#-apt--source--types) +* [`enabled`](#-apt--source--enabled) * [`comment`](#-apt--source--comment) * [`ensure`](#-apt--source--ensure) * [`release`](#-apt--source--release) @@ -1107,14 +1125,39 @@ The following parameters are available in the `apt::source` defined type: * [`notify_update`](#-apt--source--notify_update) * [`check_valid_until`](#-apt--source--check_valid_until) +##### `source_format` + +Data type: `Enum['list', 'sources']` + +The file format to use for the apt source. See https://wiki.debian.org/SourcesList + +Default value: `'list'` + ##### `location` -Data type: `Optional[String[1]]` +Data type: `Optional[Variant[String[1], Array[String[1]]]]` -Required, unless ensure is set to 'absent'. Specifies an Apt repository. +Required, unless ensure is set to 'absent'. Specifies an Apt repository. Valid options: a string containing a repository URL. +DEB822: Supports an array of URL values Default value: `undef` +##### `types` + +Data type: `Array[Enum['deb','deb-src'], 1, 2]` + +DEB822: The package types this source manages. + +Default value: `['deb']` + +##### `enabled` + +Data type: `Boolean` + +DEB822: Enable or Disable the APT source. + +Default value: `true` + ##### `comment` Data type: `String[1]` @@ -1133,17 +1176,19 @@ Default value: `present` ##### `release` -Data type: `Optional[String[0]]` +Data type: `Optional[Variant[String[0], Array[String[0]]]]` Specifies a distribution of the Apt repository. +DEB822: Supports an array of values Default value: `undef` ##### `repos` -Data type: `String[1]` +Data type: `Variant[String[1], Array[String[1]]]` Specifies a component of the Apt repository. +DEB822: Supports an array of values Default value: `'main'` @@ -1194,29 +1239,30 @@ Default value: `undef` ##### `architecture` -Data type: `Optional[String[1]]` +Data type: `Optional[Variant[String[1], Array[String[1]]]]` Tells Apt to only download information for specified architectures. Valid options: a string containing one or more architecture names, separated by commas (e.g., 'i386' or 'i386,alpha,powerpc'). (if unspecified, Apt downloads information for all architectures defined in the Apt::Architectures option) +DEB822: Supports an array of values Default value: `undef` ##### `allow_unsigned` -Data type: `Boolean` +Data type: `Optional[Boolean]` Specifies whether to authenticate packages from this release, even if the Release file is not signed or the signature can't be checked. -Default value: `false` +Default value: `undef` ##### `allow_insecure` -Data type: `Boolean` +Data type: `Optional[Boolean]` Specifies whether to allow downloads from insecure repositories. -Default value: `false` +Default value: `undef` ##### `notify_update` @@ -1228,11 +1274,11 @@ Default value: `true` ##### `check_valid_until` -Data type: `Boolean` +Data type: `Optional[Boolean]` Specifies whether to check if the package release date is valid. -Default value: `true` +Default value: `undef` ## Data types diff --git a/manifests/init.pp b/manifests/init.pp index a1b06fbb81..55b38ed085 100644 --- a/manifests/init.pp +++ b/manifests/init.pp @@ -213,6 +213,10 @@ 'path' => $sources_list_d, 'ext' => '.list', }, + 'sources' => { + 'path' => $sources_list_d, + 'ext' => '.sources', + }, }, Boolean $sources_list_force = false, Hash $source_key_defaults = { diff --git a/manifests/setting.pp b/manifests/setting.pp index 46b347d96a..f057d1aa2a 100644 --- a/manifests/setting.pp +++ b/manifests/setting.pp @@ -40,8 +40,8 @@ $setting_type = $title_array[0] $base_name = join(delete_at($title_array, 0), '-') - assert_type(Pattern[/\Aconf\z/, /\Apref\z/, /\Alist\z/], $setting_type) |$a, $b| { - fail("apt::setting resource name/title must start with either 'conf-', 'pref-' or 'list-'") + assert_type(Pattern[/\Aconf\z/, /\Apref\z/, /\Alist\z/, /\Asources\z/], $setting_type) |$a, $b| { + fail("apt::setting resource name/title must start with either 'conf-', 'pref-', 'list-', or 'sources-'") } if $priority !~ Integer { @@ -51,7 +51,7 @@ } } - if ($setting_type == 'list') or ($setting_type == 'pref') { + if $setting_type in ['list', 'pref', 'sources'] { $_priority = '' } else { $_priority = $priority diff --git a/manifests/source.pp b/manifests/source.pp index e7986697c3..70191993f7 100644 --- a/manifests/source.pp +++ b/manifests/source.pp @@ -21,8 +21,26 @@ # }, # } # +# @example Install the puppetlabs apt source (deb822 format) +# apt::source { 'puppetlabs': +# source_format => 'sources' +# location => ['http://apt.puppetlabs.com'], +# repos => ['puppet8'], +# keyring => '/etc/apt/keyrings/puppetlabs.gpg', +# } +# +# @param source_format +# The file format to use for the apt source. See https://wiki.debian.org/SourcesList +# # @param location -# Required, unless ensure is set to 'absent'. Specifies an Apt repository. +# Required, unless ensure is set to 'absent'. Specifies an Apt repository. Valid options: a string containing a repository URL. +# DEB822: Supports an array of URL values +# +# @param types +# DEB822: The package types this source manages. +# +# @param enabled +# DEB822: Enable or Disable the APT source. # # @param comment # Supplies a comment for adding to the Apt source file. @@ -32,9 +50,11 @@ # # @param release # Specifies a distribution of the Apt repository. +# DEB822: Supports an array of values # # @param repos # Specifies a component of the Apt repository. +# DEB822: Supports an array of values # # @param include # Configures include options. Valid options: a hash of available keys. @@ -66,6 +86,7 @@ # Tells Apt to only download information for specified architectures. Valid options: a string containing one or more architecture names, # separated by commas (e.g., 'i386' or 'i386,alpha,powerpc'). # (if unspecified, Apt downloads information for all architectures defined in the Apt::Architectures option) +# DEB822: Supports an array of values # # @param allow_unsigned # Specifies whether to authenticate packages from this release, even if the Release file is not signed or the signature can't be checked. @@ -80,177 +101,269 @@ # Specifies whether to check if the package release date is valid. # define apt::source ( - Optional[String[1]] $location = undef, + Enum['list', 'sources'] $source_format = 'list', + Array[Enum['deb','deb-src'], 1, 2] $types = ['deb'], + Optional[Variant[String[1], Array[String[1]]]] $location = undef, String[1] $comment = $name, + Boolean $enabled = true, # deb822 Enum['present', 'absent'] $ensure = present, - Optional[String[0]] $release = undef, - String[1] $repos = 'main', + Optional[Variant[String[0], Array[String[0]]]] $release = undef, + Variant[String[1], Array[String[1]]] $repos = 'main', Hash $include = {}, Optional[Variant[String[1], Hash]] $key = undef, Optional[Stdlib::AbsolutePath] $keyring = undef, Optional[Variant[Hash, Integer, String[1]]] $pin = undef, - Optional[String[1]] $architecture = undef, - Boolean $allow_unsigned = false, - Boolean $allow_insecure = false, + Optional[Variant[String[1], Array[String[1]]]] $architecture = undef, + Optional[Boolean] $allow_unsigned = undef, + Optional[Boolean] $allow_insecure = undef, + Optional[Boolean] $check_valid_until = undef, Boolean $notify_update = true, - Boolean $check_valid_until = true, ) { include apt $_before = Apt::Setting["list-${title}"] - if !$release { - if fact('os.distro.codename') { - $_release = fact('os.distro.codename') - } else { - fail('os.distro.codename fact not available: release parameter required') - } - } else { - $_release = $release - } + case $source_format { + 'list': { + $_file_suffix = $source_format - if $release =~ Pattern[/\/$/] { - $_components = $_release - } else { - $_components = "${_release} ${repos}" - } + if !$release { + if fact('os.distro.codename') { + $_release = fact('os.distro.codename') + } else { + fail('os.distro.codename fact not available: release parameter required') + } + } else { + $_release = $release + } - if $ensure == 'present' { - if ! $location { - fail('cannot create a source entry without specifying a location') - } - elsif ($apt::proxy['https_acng']) and ($location =~ /(?i:^https:\/\/)/) { - $_location = regsubst($location, 'https://','http://HTTPS///') - } - else { - $_location = $location - } - } else { - $_location = undef - } + if $release =~ Pattern[/\/$/] { + $_components = $_release + } else { + $_components = "${_release} ${repos}" + } - $includes = $apt::include_defaults + $include + if $ensure == 'present' { + if ! $location { + fail('cannot create a source entry without specifying a location') + } + elsif ($apt::proxy['https_acng']) and ($location =~ /(?i:^https:\/\/)/) { + $_location = regsubst($location, 'https://','http://HTTPS///') + } + else { + $_location = $location + } + } else { + $_location = undef + } - if $keyring { - if $key { - fail('parameters key and keyring are mutually exclusive') - } else { - $_list_keyring = $keyring - } - } elsif $key { - if $key =~ Hash { - unless $key['name'] or $key['id'] { - fail('key hash must contain a key name (for apt::keyring) or an id (for apt::key)') + $includes = $apt::include_defaults + $include + + if $keyring { + if $key { + fail('parameters key and keyring are mutually exclusive') + } else { + $_list_keyring = $keyring + } + } elsif $key { + if $key =~ Hash { + unless $key['name'] or $key['id'] { + fail('key hash must contain a key name (for apt::keyring) or an id (for apt::key)') + } + if $key['id'] { + # defaults like keyserver are only relevant to apt::key + $_key = $apt::source_key_defaults + $key + } else { + $_key = $key + } + } else { + $_key = { 'id' => assert_type(String[1], $key) } + } + if $_key['ensure'] { + $_key_ensure = $_key['ensure'] + } else { + $_key_ensure = $ensure + } + + # Old keyserver keys handled by apt-key + if $_key =~ Hash and $_key['id'] { + # We do not want to remove keys when the source is absent. + if $ensure == 'present' { + apt::key { "Add key: ${$_key['id']} from Apt::Source ${title}": + ensure => $_key_ensure, + id => $_key['id'], + server => $_key['server'], + content => $_key['content'], + source => $_key['source'], + options => $_key['options'], + weak_ssl => $_key['weak_ssl'], + before => $_before, + } + } + $_list_keyring = undef + } + # Modern apt keyrings + elsif $_key =~ Hash and $_key['name'] { + apt::keyring { $_key['name']: + ensure => $_key_ensure, + content => $_key['content'], + source => $_key['source'], + dir => $_key['dir'], + filename => $_key['filename'], + mode => $_key['mode'], + before => $_before, + } + + $_list_keyring = if $_key['dir'] and $_key['filename'] { + "${_key['dir']}${_key['filename']}" + } elsif $_key['filename'] { + "/etc/apt/keyrings/${_key['filename']}" + } elsif $_key['dir'] { + "${_key['dir']}${_key['name']}" + } else { + "/etc/apt/keyrings/${_key['name']}" + } + } + } else { + # No `key` nor `keyring` provided + $_list_keyring = undef } - if $key['id'] { - # defaults like keyserver are only relevant to apt::key - $_key = $apt::source_key_defaults + $key + + $header = epp('apt/_header.epp') + + if $architecture { + $_architecture = regsubst($architecture, '\baarch64\b', 'arm64') } else { - $_key = $key + $_architecture = undef + } + + $source_content = epp('apt/source.list.epp', { + 'comment' => $comment, + 'includes' => $includes, + 'options' => delete_undef_values({ + 'arch' => $_architecture, + 'trusted' => $allow_unsigned ? { true => 'yes', false => undef, default => undef }, + 'allow-insecure' => $allow_insecure ? { true => 'yes', false => undef, default => undef }, + 'signed-by' => $_list_keyring, + 'check-valid-until' => $check_valid_until? { true => undef, false => 'false', default => undef }, + }, + ), + 'location' => $_location, + 'components' => $_components, + } + ) + + if $pin { + if $pin =~ Hash { + $_pin = $pin + { 'ensure' => $ensure, 'before' => $_before } + } elsif ($pin =~ Numeric or $pin =~ String) { + $url_split = split($location, '[:\/]+') + $host = $url_split[1] + $_pin = { + 'ensure' => $ensure, + 'priority' => $pin, + 'before' => $_before, + 'origin' => $host, + } + } else { + fail('Received invalid value for pin parameter') + } + + apt::pin { $name: + * => $_pin, + } } - } else { - $_key = { 'id' => assert_type(String[1], $key) } - } - if $_key['ensure'] { - $_key_ensure = $_key['ensure'] - } else { - $_key_ensure = $ensure } + 'sources': { + $_file_suffix = $source_format - # Old keyserver keys handled by apt-key - if $_key =~ Hash and $_key['id'] { - # We do not want to remove keys when the source is absent. + if $pin { + warning("'pin' parameter is not supported with deb822 format.") + } + if $key { + warning("'key' parameter is not supported with deb822 format.") + } if $ensure == 'present' { - apt::key { "Add key: ${$_key['id']} from Apt::Source ${title}": - ensure => $_key_ensure, - id => $_key['id'], - server => $_key['server'], - content => $_key['content'], - source => $_key['source'], - options => $_key['options'], - weak_ssl => $_key['weak_ssl'], - before => $_before, + if ! $location { + fail('cannot create a source entry without specifying a location') } } - $_list_keyring = undef - } - # Modern apt keyrings - elsif $_key =~ Hash and $_key['name'] { - apt::keyring { $_key['name']: - ensure => $_key_ensure, - content => $_key['content'], - source => $_key['source'], - dir => $_key['dir'], - filename => $_key['filename'], - mode => $_key['mode'], - before => $_before, + if (type($location, 'generalized') !~ Type[Array]) { + warning('For deb822 sources, location must be specified as an array.') + $_location = [$location] + } + else { + $_location = $location } - $_list_keyring = if $_key['dir'] and $_key['filename'] { - "${_key['dir']}${_key['filename']}" - } elsif $_key['filename'] { - "/etc/apt/keyrings/${_key['filename']}" - } elsif $_key['dir'] { - "${_key['dir']}${_key['name']}" + if !$release { + if fact('os.distro.codename') { + $_release = [fact('os.distro.codename')] + } else { + fail('os.distro.codename fact not available: release parameter required') + } } else { - "/etc/apt/keyrings/${_key['name']}" + if (type($release, 'generalized') !~ Type[Array]) { + warning("For deb822 sources, 'release' must be specified as an array. Converting to array.") + $_release = [$release] + } else { + $_release = $release + } } - } - } else { - # No `key` nor `keyring` provided - $_list_keyring = undef - } - - $header = epp('apt/_header.epp') - - if $architecture { - $_architecture = regsubst($architecture, '\baarch64\b', 'arm64') - } else { - $_architecture = undef - } - - $sourcelist = epp('apt/source.list.epp', - { - 'comment' => $comment, - 'includes' => $includes, - 'options' => delete_undef_values( - { - 'arch' => $_architecture, - 'trusted' => $allow_unsigned ? { true => 'yes', false => undef }, - 'allow-insecure' => $allow_insecure ? { true => 'yes', false => undef }, - 'signed-by' => $_list_keyring, - 'check-valid-until' => $check_valid_until? { true => undef, false => 'false' }, - }, - ), - 'location' => $_location, - 'components' => $_components, - }, - ) - apt::setting { "list-${name}": - ensure => $ensure, - content => "${header}${sourcelist}", - notify_update => $notify_update, - } + if (type($repos, 'generalized') !~ Type[Array]) { + warning("For deb822 sources, 'repos' must be specified as an array. Converting to array.") + $_repos = split($repos, /\s+/) + } else { + $_repos = $repos + } - if $pin { - if $pin =~ Hash { - $_pin = $pin + { 'ensure' => $ensure, 'before' => $_before } - } elsif ($pin =~ Numeric or $pin =~ String) { - $url_split = split($location, '[:\/]+') - $host = $url_split[1] - $_pin = { - 'ensure' => $ensure, - 'priority' => $pin, - 'before' => $_before, - 'origin' => $host, + if $architecture != undef { + if (type($architecture, 'generalized') !~ Type[Array]) { + warning("For deb822 sources, 'architecture' must be specified as an array. Converting to array.") + $_architecture = split($architecture, '[,]') + } + else { + $_architecture = $architecture + } + } else { + $_architecture = $architecture + } + case $ensure { + 'present': { + $header = epp('apt/_header.epp') + $source_content = epp('apt/source_deb822.epp', delete_undef_values({ + 'uris' => $_location, + 'suites' => $_release, + 'components' => $_repos, + 'types' => $types, + 'comment' => $comment, + 'enabled' => $enabled ? { true => 'yes', false => 'no' }, + 'architectures' => $_architecture, + 'allow_insecure' => $allow_insecure ? { true => 'yes', false => 'no', default => undef }, + 'repo_trusted' => $allow_unsigned ? { true => 'yes', false => 'no', default => undef }, + 'check_valid_until' => $check_valid_until ? { true => 'yes', false => 'no', default => undef }, + 'signed_by' => $keyring, + } + ) + ) + } + 'absent': { + $header = undef + $source_content = undef + } + default: { + fail('Unexpected value for $ensure parameter.') + } } - } else { - fail('Received invalid value for pin parameter') } - - apt::pin { $name: - * => $_pin, + default: { + fail("Unexpected APT source format: ${source_format}") } } + apt::setting { "${_file_suffix}-${name}": + ensure => $ensure, + content => "${header}${source_content}", + notify_update => $notify_update, + } } diff --git a/spec/defines/source_spec.rb b/spec/defines/source_spec.rb index 5268c03a04..9c619439c0 100644 --- a/spec/defines/source_spec.rb +++ b/spec/defines/source_spec.rb @@ -416,4 +416,69 @@ it { is_expected.to contain_apt__setting("list-#{title}").with_notify_update(false) } end end + + describe 'deb822 sources' do + let :params do + { + source_format: 'sources', + } + end + + context 'basic deb822 source' do + let :params do + super().merge( + { + location: ['http://debian.mirror.iweb.ca/debian/'], + repos: ['main', 'contrib', 'non-free'] + }, + ) + end + + it { is_expected.to contain_apt__setting("sources-#{title}").with_notify_update(true) } + end + + context 'complex deb822 source' do + let :params do + super().merge( + { + types: ['deb', 'deb-src'], + location: ['http://fr.debian.org/debian', 'http://de.debian.org/debian'], + release: ['stable', 'stable-updates', 'stable-backports'], + repos: ['main', 'contrib', 'non-free'], + architecture: ['amd64', 'i386'], + allow_unsigned: true, + notify_update: false + }, + ) + end + + it { is_expected.to contain_apt__setting("sources-#{title}").with_notify_update(false) } + it { is_expected.to contain_apt__setting("sources-#{title}").with_content(%r{Enabled: yes}) } + it { is_expected.to contain_apt__setting("sources-#{title}").with_content(%r{Types: deb deb-src}) } + it { is_expected.to contain_apt__setting("sources-#{title}").with_content(%r{URIs: http://fr.debian.org/debian http://de.debian.org/debian}) } + it { is_expected.to contain_apt__setting("sources-#{title}").with_content(%r{Suites: stable stable-updates stable-backports}) } + it { is_expected.to contain_apt__setting("sources-#{title}").with_content(%r{Components: main contrib non-free}) } + it { is_expected.to contain_apt__setting("sources-#{title}").with_content(%r{Architectures: amd64 i386}) } + it { is_expected.to contain_apt__setting("sources-#{title}").with_content(%r{Trusted: yes}) } + end + + context '.list backwards compatibility' do + let :params do + super().merge( + { + location: 'http://debian.mirror.iweb.ca/debian/', + release: 'unstable', + repos: 'main contrib non-free', + key: { + id: 'A1BD8E9D78F7FE5C3E65D8AF8B48AD6246925553', + server: 'keyserver.ubuntu.com', + }, + pin: '-10' + }, + ) + end + + it { is_expected.to contain_apt__setting("sources-#{title}").with_notify_update(true) } + end + end end diff --git a/templates/source_deb822.epp b/templates/source_deb822.epp new file mode 100644 index 0000000000..10802b05d5 --- /dev/null +++ b/templates/source_deb822.epp @@ -0,0 +1,36 @@ +<% | String $comment, + Enum['yes','no'] $enabled, + Array[String] $types, + Array[String] $uris, + Array[String] $suites, + Array[String] $components, + Optional[Array] $architectures = undef, + Optional[Enum['yes','no']] $allow_insecure = undef, + Optional[Enum['yes','no']] $repo_trusted = undef, + Optional[Enum['yes','no']] $check_valid_until = undef, + Optional[Variant[Stdlib::AbsolutePath,Array[String]]] $signed_by = undef, +| -%> +# <%= $comment %> +Enabled: <%= $enabled %> +Types: <% $types.each |String $type| { -%> <%= $type %> <% } %> +URIs: <% $uris.each | String $uri | { -%> <%= $uri %> <% } %> +Suites: <% $suites.each | String $suite | { -%> <%= $suite %> <% } %> +Components: <% $components.each | String $component | { -%> <%= $component %> <% } %> +<% if $architectures { -%> +Architectures:<% $architectures.each | String $arch | { %> <%= $arch %><% } %> +<%- } -%> +<% if $allow_insecure { -%> +Allow-Insecure: <%= $allow_insecure %> +<% } -%> +<% if $repo_trusted { -%> +Trusted: <%= $repo_trusted %> +<% } -%> +<% if $check_valid_until { -%> +Check-Valid-Until: <%= $check_valid_until %> +<% } -%> +<% if $signed_by { -%> +Signed-By: <% if type($signed_by) =~ Type[Array] { -%><%- $signed_by.each |String $keyring| { -%><%= $keyring %> <% } -%> +<%- } -%> +<%- elsif type($signed_by) =~ Type[String] { -%> +<%= $signed_by -%> +<%- }} %>