commit 331b8d85f7c38b6dbfe602f75d6c9831b1534694
Author: Steve Traylen <steve.traylen@cern.ch>
Date:   Wed Aug 31 17:19:42 2022 +0200

    New nftables::file type to include raw file
    
    For example:
    
    ```puppet
    nftables::file{'geoip':
      content => "include \"/files/geoipsets/dbip/*.ipv4\"\n",
    }
    ```
    
    will right a file or content into the nftables configuration.
    
    The file written will be included in configuration.
    
    Fixes #146

diff --git a/REFERENCE.md b/REFERENCE.md
index 73fc306..c2e4903 100644
--- a/REFERENCE.md
+++ b/REFERENCE.md
@@ -78,6 +78,7 @@ and Manager Daemons (MGR).
 
 * [`nftables::chain`](#nftableschain): manage a chain
 * [`nftables::config`](#nftablesconfig): manage a config snippet
+* [`nftables::file`](#nftablesfile): Insert a file into the nftables configuration
 * [`nftables::rule`](#nftablesrule): Provides an interface to create a firewall rule
 * [`nftables::rules::dnat4`](#nftablesrulesdnat4): manage a ipv4 dnat rule
 * [`nftables::rules::masquerade`](#nftablesrulesmasquerade): masquerade all outgoing traffic
@@ -1233,6 +1234,65 @@ Data type: `String`
 
 Default value: `'custom-'`
 
+### <a name="nftablesfile"></a>`nftables::file`
+
+Insert a file into the nftables configuration
+
+#### Examples
+
+##### Include a file that includes other files
+
+```puppet
+nftables::file{'geoip':
+  content => @(EOT)
+    include "/var/local/geoipsets/dbip/nftset/ipv4/*.ipv4"
+    include "/var/local/geoipsets/dbip/nftset/ipv6/*.ipv6"
+    |EOT,
+}
+```
+
+#### Parameters
+
+The following parameters are available in the `nftables::file` defined type:
+
+* [`label`](#label)
+* [`content`](#content)
+* [`source`](#source)
+* [`prefix`](#prefix)
+
+##### <a name="label"></a>`label`
+
+Data type: `String[1]`
+
+Unique name to include in filename.
+
+Default value: `$title`
+
+##### <a name="content"></a>`content`
+
+Data type: `Optional[String]`
+
+The content to place in the file.
+
+Default value: ``undef``
+
+##### <a name="source"></a>`source`
+
+Data type: `Optional[Variant[String,Array[String,1]]]`
+
+A source to obtain the file content from.
+
+Default value: ``undef``
+
+##### <a name="prefix"></a>`prefix`
+
+Data type: `String`
+
+Prefix of file name to be created, if left as `file-` it will be
+auto included in the main nft configuration
+
+Default value: `'file-'`
+
 ### <a name="nftablesrule"></a>`nftables::rule`
 
 Provides an interface to create a firewall rule
diff --git a/manifests/file.pp b/manifests/file.pp
new file mode 100644
index 0000000..77e0385
--- /dev/null
+++ b/manifests/file.pp
@@ -0,0 +1,44 @@
+# @summary Insert a file into the nftables configuration
+# @example Include a file that includes other files
+#   nftables::file{'geoip':
+#     content => @(EOT)
+#       include "/var/local/geoipsets/dbip/nftset/ipv4/*.ipv4"
+#       include "/var/local/geoipsets/dbip/nftset/ipv6/*.ipv6"
+#       |EOT,
+#   }
+#
+# @param label Unique name to include in filename.
+# @param content The content to place in the file.
+# @param source A source to obtain the file content from.
+# @param prefix
+#   Prefix of file name to be created, if left as `file-` it will be
+#   auto included in the main nft configuration
+#
+define nftables::file (
+  String[1] $label = $title,
+  Optional[String] $content = undef,
+  Optional[Variant[String,Array[String,1]]] $source = undef,
+  String $prefix = 'file-',
+) {
+  if $content and $source {
+    fail('Please pass only $content or $source, not both.')
+  }
+
+  $concat_name = "nftables-${name}"
+
+  Package['nftables'] -> file { "/etc/nftables/puppet-preflight/${prefix}${label}.nft":
+    ensure  => file,
+    owner   => root,
+    group   => root,
+    mode    => '0640',
+    content => $content,
+    source  => $source,
+  } ~> Exec['nft validate'] -> file { "/etc/nftables/puppet/${prefix}${label}.nft":
+    ensure  => file,
+    owner   => root,
+    group   => root,
+    mode    => '0640',
+    content => $content,
+    source  => $source,
+  } ~> Service['nftables']
+}
diff --git a/spec/acceptance/file_spec.rb b/spec/acceptance/file_spec.rb
new file mode 100644
index 0000000..771e6a8
--- /dev/null
+++ b/spec/acceptance/file_spec.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+
+require 'spec_helper_acceptance'
+
+describe 'nftables class' do
+  context 'configure a nftables::file raw file' do
+    it 'works idempotently with no errors' do
+      pp = <<-EOS
+      # default mask of firewalld service fails if service is not installed.
+      # https://tickets.puppetlabs.com/browse/PUP-10814
+      class { 'nftables':
+        firewalld_enable => false,
+      }
+      nftables::file{'geoip':
+        content => "# A comment should not fail\n",
+      }
+      $config_path = $facts['os']['family'] ? {
+        'Archlinux' => '/etc/nftables.conf',
+        'Debian' => '/etc/nftables.conf',
+        default => '/etc/sysconfig/nftables.conf',
+      }
+      $nft_path = $facts['os']['family'] ? {
+        'Archlinux' => '/usr/bin/nft',
+        default => '/usr/sbin/nft',
+      }
+      # nftables cannot be started in docker so replace service with a validation only.
+      systemd::dropin_file{"zzz_docker_nft.conf":
+        ensure  => present,
+        unit    => "nftables.service",
+        content => [
+          "[Service]",
+          "ExecStart=",
+          "ExecStart=${nft_path} -c -I /etc/nftables/puppet -f ${config_path}",
+          "ExecReload=",
+          "ExecReload=${nft_path} -c -I /etc/nftables/puppet -f ${config_path}",
+          "",
+          ].join("\n"),
+        notify  => Service["nftables"],
+      }
+      EOS
+      # Run it twice and test for idempotency
+      apply_manifest(pp, catch_failures: true)
+      apply_manifest(pp, catch_changes: true)
+    end
+
+    describe package('nftables') do
+      it { is_expected.to be_installed }
+    end
+
+    describe service('nftables') do
+      it { is_expected.to be_running }
+      it { is_expected.to be_enabled }
+    end
+
+    describe file('/etc/nftables/puppet/file-geoip.nft', '/etc/nftables/puppet/file-geoip.nft') do
+      it { is_expected.to be_file }
+    end
+  end
+end
diff --git a/spec/classes/nftables_spec.rb b/spec/classes/nftables_spec.rb
index 1938f58..c8b13e8 100644
--- a/spec/classes/nftables_spec.rb
+++ b/spec/classes/nftables_spec.rb
@@ -46,6 +46,12 @@ describe 'nftables' do
       }
 
       it {
+        expect(subject).to contain_file('/etc/nftables/puppet.nft').with(
+          content: %r{^include "file-\*\.nft"$}
+        )
+      }
+
+      it {
         expect(subject).to contain_file('/etc/nftables/puppet').with(
           ensure: 'directory',
           owner: 'root',
@@ -68,6 +74,12 @@ describe 'nftables' do
       }
 
       it {
+        expect(subject).to contain_file('/etc/nftables/puppet-preflight.nft').with(
+          content: %r{^include "file-\*\.nft"$}
+        )
+      }
+
+      it {
         expect(subject).to contain_file('/etc/nftables/puppet-preflight').with(
           ensure: 'directory',
           owner: 'root',
diff --git a/spec/defines/file_spec.rb b/spec/defines/file_spec.rb
new file mode 100644
index 0000000..1278ae1
--- /dev/null
+++ b/spec/defines/file_spec.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'nftables::file' do
+  let(:pre_condition) { 'include nftables' }
+  let(:title) { 'FOO' }
+
+  on_supported_os.each do |os, facts|
+    context "on #{os}" do
+      let(:facts) do
+        facts
+      end
+
+      context 'with source and content both unset' do
+        it { is_expected.to compile }
+        it { is_expected.to contain_file('/etc/nftables/puppet-preflight/file-FOO.nft').without_source }
+        it { is_expected.to contain_file('/etc/nftables/puppet-preflight/file-FOO.nft').without_content }
+        it { is_expected.to contain_file('/etc/nftables/puppet/file-FOO.nft').without_source }
+        it { is_expected.to contain_file('/etc/nftables/puppet/file-FOO.nft').without_content }
+        it { is_expected.to contain_exec('nft validate') }
+        it { is_expected.to contain_service('nftables') }
+        it { is_expected.to contain_package('nftables').that_comes_before('File[/etc/nftables/puppet-preflight/file-FOO.nft]') }
+        it { is_expected.to contain_file('/etc/nftables/puppet-preflight/file-FOO.nft').that_notifies('Exec[nft validate]') }
+        it { is_expected.to contain_exec('nft validate').that_comes_before('File[/etc/nftables/puppet/file-FOO.nft]') }
+        it { is_expected.to contain_file('/etc/nftables/puppet/file-FOO.nft').that_notifies('Service[nftables]') }
+      end
+
+      context 'with source set only' do
+        let(:params) do
+          { source: 'puppet:///module/foobar.nft' }
+        end
+
+        it { is_expected.to contain_file('/etc/nftables/puppet-preflight/file-FOO.nft').without_content }
+        it { is_expected.to contain_file('/etc/nftables/puppet-preflight/file-FOO.nft').with_source('puppet:///module/foobar.nft') }
+        it { is_expected.to contain_file('/etc/nftables/puppet/file-FOO.nft').without_content }
+        it { is_expected.to contain_file('/etc/nftables/puppet/file-FOO.nft').with_source('puppet:///module/foobar.nft') }
+      end
+
+      context 'with content set only' do
+        let(:params) do
+          { content: '# my rules' }
+        end
+
+        it { is_expected.to contain_file('/etc/nftables/puppet-preflight/file-FOO.nft').without_source }
+        it { is_expected.to contain_file('/etc/nftables/puppet-preflight/file-FOO.nft').with_content('# my rules') }
+        it { is_expected.to contain_file('/etc/nftables/puppet/file-FOO.nft').without_source }
+        it { is_expected.to contain_file('/etc/nftables/puppet/file-FOO.nft').with_content('# my rules') }
+      end
+
+      context 'with content and source set' do
+        let(:params) do
+          { content: '# my rules', source: 'puppet:///modules/foobar.nft' }
+        end
+
+        it { is_expected.not_to compile }
+      end
+    end
+  end
+end
diff --git a/templates/config/puppet.nft.epp b/templates/config/puppet.nft.epp
index ff2297a..11bb545 100644
--- a/templates/config/puppet.nft.epp
+++ b/templates/config/puppet.nft.epp
@@ -21,6 +21,7 @@ if $noflush and $facts['nftables'] and $facts['nftables']['tables'] {
 # drop any existing nftables ruleset, ensure tables are initialized
 <%= $_flush_command.join("\n") %>
 
+include "file-*.nft"
 include "custom-*.nft"
 <% if $inet_filter { -%>
 include "inet-filter.nft"
