commit 30462da12b2eca072bdd9c13b1970958ff962745
Author: Steve Traylen <steve.traylen@cern.ch>
Date:   Wed Nov 18 08:43:18 2020 +0100

    Reload rules atomically
    
    Background: The unit file for nftables on CentOS 8 contains:
    
    ```
    ExecStart=/sbin/nft -f /etc/sysconfig/nftables.conf
    ExecReload=/sbin/nft 'flush ruleset; include "/etc/sysconfig/nftables.conf";'
    ExecStop=/sbin/nft flush ruleset
    ```
    
    As things stood on config modication `systemctl stop nftables ; systemctl start nftables` was being
    called resulting in:
    
    ```
    nft flush ruleset
    nft -f /etc/sysconfig/nftables.conf
    ```
    
    as distinct commands so a non-atomic flush and load of ruleset.
    
    With this change it is now.
    
    ```
    /sbin/nft 'flush ruleset; include "/etc/sysconfig/nftables.conf";'
    ```
    
    There is subsequently a redundant extra 'flush ruleset' in there to be followed
    up in a seperate patch as I have desire to make that tunable.
    
    To verify what happens when broken rules have been applied, e.g
    
    ```puppet
    nftables::rule{'default_in-junk':
      content =>  'junk',
    }
    ```
    
    This results in puppet run of
    
    ```
    Error: /Stage[main]/Nftables/Service[nftables]: Failed to call refresh: Systemd restart for nftables failed!
    journalctl log for nftables:
    ```
    
    and the existing rules are left live, previously the flush from the stop was occuring.
    
    The reload attempt would only happen once however leaving a time bomb at reboot.
    To resvole this the configuration is modified to force a reconfig and reload every puppet run.

diff --git a/.fixtures.yml b/.fixtures.yml
index 039f70f..88fc90d 100644
--- a/.fixtures.yml
+++ b/.fixtures.yml
@@ -5,3 +5,4 @@ fixtures:
   forge_modules:
     concat: "puppetlabs/concat"
     stdlib: "puppetlabs/stdlib"
+    systemd: "camptocamp/systemd"
diff --git a/README.md b/README.md
index 9c1a126..013d94b 100644
--- a/README.md
+++ b/README.md
@@ -42,6 +42,14 @@ for all masterchains.
 INPUT and OUTPUT to the loopback device is allowed by
 default, though you could restrict it later.
 
+### Rules Validation
+Initially puppet deploys all configuration to
+`/etc/nftables/puppet-preflight/` and
+`/etc/nftables/puppet-preflight.nft`. This is validated with
+`nfc -c -L /etc/nftables/puppet-preflight/ -f /etc/nftables/puppet-preflight.nft`.
+If and only if successful the configuration will be copied to
+the real locations before the service is reloaded.
+
 ### nftables::config
 
 Manages a raw file in `/etc/nftables/puppet/${name}.nft`
diff --git a/files/config/puppet-inet-filter.nft b/files/config/puppet-inet-filter.nft
index 50681d2..f2d0157 100644
--- a/files/config/puppet-inet-filter.nft
+++ b/files/config/puppet-inet-filter.nft
@@ -1,4 +1,4 @@
-  include "/etc/nftables/puppet/inet-filter-chain-*.nft"
+  include "inet-filter-chain-*.nft"
 
   # something we want for all
   chain global {
diff --git a/files/config/puppet-ip-nat.nft b/files/config/puppet-ip-nat.nft
index a08867e..7f274eb 100644
--- a/files/config/puppet-ip-nat.nft
+++ b/files/config/puppet-ip-nat.nft
@@ -1 +1 @@
-  include "/etc/nftables/puppet/ip-nat-chain-*.nft"
+  include "ip-nat-chain-*.nft"
diff --git a/files/config/puppet-ip6-nat.nft b/files/config/puppet-ip6-nat.nft
index 2202b46..ce3aea6 100644
--- a/files/config/puppet-ip6-nat.nft
+++ b/files/config/puppet-ip6-nat.nft
@@ -1 +1 @@
-  include "/etc/nftables/puppet/ip6-nat-chain-*.nft"
+  include "ip6-nat-chain-*.nft"
diff --git a/files/config/puppet.nft b/files/config/puppet.nft
index fa394db..1d4065f 100644
--- a/files/config/puppet.nft
+++ b/files/config/puppet.nft
@@ -1,7 +1,13 @@
+# puppet-preflight.nft is only used by puppet for validating new configs
+# puppet.nft is real configuration that the nftables services uses.
+# To process either the -I flag must be specified.
+# nft -c -I /etc/nftables/puppet -f /etc/nftables/puppet.nft
+# nft -c -I /etc/nftables/puppet-preflight -f /etc/nftables/puppet-preflight.nft
+
 # drop any existing nftables ruleset
 flush ruleset
 
-include "/etc/nftables/puppet/custom-*.nft"
-include "/etc/nftables/puppet/inet-filter.nft"
-include "/etc/nftables/puppet/ip-nat.nft"
-include "/etc/nftables/puppet/ip6-nat.nft"
+include "custom-*.nft"
+include "inet-filter.nft"
+include "ip-nat.nft"
+include "ip6-nat.nft"
diff --git a/files/systemd/puppet_nft.conf b/files/systemd/puppet_nft.conf
new file mode 100644
index 0000000..fa66c53
--- /dev/null
+++ b/files/systemd/puppet_nft.conf
@@ -0,0 +1,7 @@
+# Specify directory to look for relative includes
+[Service]
+ExecStart=
+ExecStart=/sbin/nft -I /etc/nftables/puppet -f /etc/sysconfig/nftables.conf
+ExecReload=
+ExecReload=/sbin/nft -I /etc/nftables/puppet 'flush ruleset; include "/etc/sysconfig/nftables.conf";'
+
diff --git a/manifests/chain.pp b/manifests/chain.pp
index 26dbe51..29bfbfe 100644
--- a/manifests/chain.pp
+++ b/manifests/chain.pp
@@ -15,14 +15,20 @@ define nftables::chain(
 
   concat{
     $concat_name:
-      path           => "/etc/nftables/puppet/${table}-chain-${chain}.nft",
+      path           => "/etc/nftables/puppet-preflight/${table}-chain-${chain}.nft",
       owner          => root,
       group          => root,
       mode           => '0640',
       ensure_newline => true,
       require        => Package['nftables'],
-      notify         => Service['nftables'],
-  }
+  } ~> Exec['nft validate'] -> file{
+    "/etc/nftables/puppet/${table}-chain-${chain}.nft":
+    ensure => file,
+    source => "/etc/nftables/puppet-preflight/${table}-chain-${chain}.nft",
+    owner  => root,
+    group  => root,
+    mode   => '0640',
+  } ~> Service['nftables']
 
   concat::fragment{
     default:
diff --git a/manifests/config.pp b/manifests/config.pp
index 036aa45..f07b1f0 100644
--- a/manifests/config.pp
+++ b/manifests/config.pp
@@ -9,11 +9,18 @@ define nftables::config(
 
   Package['nftables'] -> concat{
     $concat_name:
-      path           => "/etc/nftables/puppet/${name}.nft",
+      path           => "/etc/nftables/puppet-preflight/${name}.nft",
       ensure_newline => true,
       owner          => root,
       group          => root,
       mode           => '0640',
+  } ~> Exec['nft validate'] -> file{
+    "/etc/nftables/puppet/${name}.nft":
+    ensure => file,
+    source => "/etc/nftables/puppet-preflight/${name}.nft",
+    owner  => root,
+    group  => root,
+    mode   => '0640',
   } ~> Service['nftables']
 
   $data = split($name, '-')
diff --git a/manifests/init.pp b/manifests/init.pp
index 837db61..9e49eaf 100644
--- a/manifests/init.pp
+++ b/manifests/init.pp
@@ -84,6 +84,24 @@ class nftables (
       owner => 'root',
       group => 'root',
       mode  => '0640';
+    '/etc/nftables/puppet-preflight':
+      ensure  => directory,
+      mode    => '0750',
+      purge   => true,
+      force   => true,
+      recurse => true;
+    '/etc/nftables/puppet-preflight.nft':
+      ensure => file,
+      source => 'puppet:///modules/nftables/config/puppet.nft';
+  } ~> exec{
+    'nft validate':
+      refreshonly => true,
+      command     => '/usr/sbin/nft -I /etc/nftables/puppet-preflight -c -f /etc/nftables/puppet-preflight.nft || ( /usr/bin/echo "#CONFIG BROKEN" >> /etc/nftables/puppet-preflight.nft && /bin/false)';
+  } -> file{
+    default:
+      owner => 'root',
+      group => 'root',
+      mode  => '0640';
     '/etc/nftables/puppet.nft':
       ensure => file,
       source => 'puppet:///modules/nftables/config/puppet.nft';
@@ -94,8 +112,17 @@ class nftables (
       force   => true,
       recurse => true;
   } ~> service{'nftables':
-    ensure => running,
-    enable => true,
+    ensure     => running,
+    enable     => true,
+    hasrestart => true,
+    restart    => '/usr/bin/systemctl reload nftables',
+  }
+
+  systemd::dropin_file{'puppet_nft.conf':
+    ensure => present,
+    unit   => 'nftables.service',
+    source => 'puppet:///modules/nftables/systemd/puppet_nft.conf',
+    notify => Service['nftables'],
   }
 
   service{'firewalld':
diff --git a/metadata.json b/metadata.json
index 5a64901..6e08dcc 100644
--- a/metadata.json
+++ b/metadata.json
@@ -12,6 +12,10 @@
       "version_requirement": ">= 6.2.0 < 7.0.0"
     },
     {
+      "name": "camptocamp/systemd",
+      "version_requirement": ">= 2.0.0 < 3.0.0"
+    },
+    {
       "name": "puppetlabs/stdlib",
       "version_requirement": ">= 4.13.1 < 7.0.0"
     }
diff --git a/spec/classes/bridges_spec.rb b/spec/classes/bridges_spec.rb
index 0ff6911..dbbc852 100644
--- a/spec/classes/bridges_spec.rb
+++ b/spec/classes/bridges_spec.rb
@@ -11,7 +11,7 @@ describe 'nftables' do
 
       it {
         is_expected.to contain_concat('nftables-inet-filter-chain-default_fwd').with(
-          path:           '/etc/nftables/puppet/inet-filter-chain-default_fwd.nft',
+          path:           '/etc/nftables/puppet-preflight/inet-filter-chain-default_fwd.nft',
           owner:          'root',
           group:          'root',
           mode:           '0640',
diff --git a/spec/classes/dnat4_spec.rb b/spec/classes/dnat4_spec.rb
index 7d9675a..5e9bdf9 100644
--- a/spec/classes/dnat4_spec.rb
+++ b/spec/classes/dnat4_spec.rb
@@ -52,7 +52,7 @@ describe 'nftables' do
 
         it {
           is_expected.to contain_concat('nftables-inet-filter-chain-default_fwd').with(
-            path:           '/etc/nftables/puppet/inet-filter-chain-default_fwd.nft',
+            path:           '/etc/nftables/puppet-preflight/inet-filter-chain-default_fwd.nft',
             owner:          'root',
             group:          'root',
             mode:           '0640',
@@ -126,7 +126,7 @@ describe 'nftables' do
 
         it {
           is_expected.to contain_concat('nftables-ip-nat-chain-PREROUTING').with(
-            path:           '/etc/nftables/puppet/ip-nat-chain-PREROUTING.nft',
+            path:           '/etc/nftables/puppet-preflight/ip-nat-chain-PREROUTING.nft',
             owner:          'root',
             group:          'root',
             mode:           '0640',
diff --git a/spec/classes/inet_filter_spec.rb b/spec/classes/inet_filter_spec.rb
index 187da7d..53920c3 100644
--- a/spec/classes/inet_filter_spec.rb
+++ b/spec/classes/inet_filter_spec.rb
@@ -11,7 +11,7 @@ describe 'nftables' do
 
       it {
         is_expected.to contain_concat('nftables-inet-filter').with(
-          path:   '/etc/nftables/puppet/inet-filter.nft',
+          path:   '/etc/nftables/puppet-preflight/inet-filter.nft',
           ensure: 'present',
           owner:  'root',
           group:  'root',
@@ -45,7 +45,7 @@ describe 'nftables' do
       context 'chain input' do
         it {
           is_expected.to contain_concat('nftables-inet-filter-chain-INPUT').with(
-            path:           '/etc/nftables/puppet/inet-filter-chain-INPUT.nft',
+            path:           '/etc/nftables/puppet-preflight/inet-filter-chain-INPUT.nft',
             owner:          'root',
             group:          'root',
             mode:           '0640',
@@ -132,7 +132,7 @@ describe 'nftables' do
 
         it {
           is_expected.to contain_concat('nftables-inet-filter-chain-default_in').with(
-            path:           '/etc/nftables/puppet/inet-filter-chain-default_in.nft',
+            path:           '/etc/nftables/puppet-preflight/inet-filter-chain-default_in.nft',
             owner:          'root',
             group:          'root',
             mode:           '0640',
@@ -168,7 +168,7 @@ describe 'nftables' do
       context 'chain output' do
         it {
           is_expected.to contain_concat('nftables-inet-filter-chain-OUTPUT').with(
-            path:           '/etc/nftables/puppet/inet-filter-chain-OUTPUT.nft',
+            path:           '/etc/nftables/puppet-preflight/inet-filter-chain-OUTPUT.nft',
             owner:          'root',
             group:          'root',
             mode:           '0640',
@@ -255,7 +255,7 @@ describe 'nftables' do
 
         it {
           is_expected.to contain_concat('nftables-inet-filter-chain-default_out').with(
-            path:           '/etc/nftables/puppet/inet-filter-chain-default_out.nft',
+            path:           '/etc/nftables/puppet-preflight/inet-filter-chain-default_out.nft',
             owner:          'root',
             group:          'root',
             mode:           '0640',
@@ -319,7 +319,7 @@ describe 'nftables' do
       context 'chain forward' do
         it {
           is_expected.to contain_concat('nftables-inet-filter-chain-FORWARD').with(
-            path:           '/etc/nftables/puppet/inet-filter-chain-FORWARD.nft',
+            path:           '/etc/nftables/puppet-preflight/inet-filter-chain-FORWARD.nft',
             owner:          'root',
             group:          'root',
             mode:           '0640',
@@ -391,7 +391,7 @@ describe 'nftables' do
 
         it {
           is_expected.to contain_concat('nftables-inet-filter-chain-default_fwd').with(
-            path:           '/etc/nftables/puppet/inet-filter-chain-default_fwd.nft',
+            path:           '/etc/nftables/puppet-preflight/inet-filter-chain-default_fwd.nft',
             owner:          'root',
             group:          'root',
             mode:           '0640',
diff --git a/spec/classes/ip_nat_spec.rb b/spec/classes/ip_nat_spec.rb
index 69b3af7..a1f5d2e 100644
--- a/spec/classes/ip_nat_spec.rb
+++ b/spec/classes/ip_nat_spec.rb
@@ -11,7 +11,7 @@ describe 'nftables' do
 
       it {
         is_expected.to contain_concat('nftables-ip-nat').with(
-          path:   '/etc/nftables/puppet/ip-nat.nft',
+          path:   '/etc/nftables/puppet-preflight/ip-nat.nft',
           ensure: 'present',
           owner:  'root',
           group:  'root',
@@ -44,7 +44,7 @@ describe 'nftables' do
 
       it {
         is_expected.to contain_concat('nftables-ip6-nat').with(
-          path:   '/etc/nftables/puppet/ip6-nat.nft',
+          path:   '/etc/nftables/puppet-preflight/ip6-nat.nft',
           ensure: 'present',
           owner:  'root',
           group:  'root',
@@ -78,7 +78,7 @@ describe 'nftables' do
       context 'table ip nat chain prerouting' do
         it {
           is_expected.to contain_concat('nftables-ip-nat-chain-PREROUTING').with(
-            path:           '/etc/nftables/puppet/ip-nat-chain-PREROUTING.nft',
+            path:           '/etc/nftables/puppet-preflight/ip-nat-chain-PREROUTING.nft',
             owner:          'root',
             group:          'root',
             mode:           '0640',
@@ -118,7 +118,7 @@ describe 'nftables' do
       context 'table ip nat chain postrouting' do
         it {
           is_expected.to contain_concat('nftables-ip-nat-chain-POSTROUTING').with(
-            path:           '/etc/nftables/puppet/ip-nat-chain-POSTROUTING.nft',
+            path:           '/etc/nftables/puppet-preflight/ip-nat-chain-POSTROUTING.nft',
             owner:          'root',
             group:          'root',
             mode:           '0640',
@@ -158,7 +158,7 @@ describe 'nftables' do
       context 'table ip6 nat chain prerouting' do
         it {
           is_expected.to contain_concat('nftables-ip6-nat-chain-PREROUTING6').with(
-            path:           '/etc/nftables/puppet/ip6-nat-chain-PREROUTING6.nft',
+            path:           '/etc/nftables/puppet-preflight/ip6-nat-chain-PREROUTING6.nft',
             owner:          'root',
             group:          'root',
             mode:           '0640',
@@ -198,7 +198,7 @@ describe 'nftables' do
       context 'table ip nat chain postrouting' do
         it {
           is_expected.to contain_concat('nftables-ip6-nat-chain-POSTROUTING6').with(
-            path:           '/etc/nftables/puppet/ip6-nat-chain-POSTROUTING6.nft',
+            path:           '/etc/nftables/puppet-preflight/ip6-nat-chain-POSTROUTING6.nft',
             owner:          'root',
             group:          'root',
             mode:           '0640',
diff --git a/spec/classes/masquerade_spec.rb b/spec/classes/masquerade_spec.rb
index b9749c2..31195cc 100644
--- a/spec/classes/masquerade_spec.rb
+++ b/spec/classes/masquerade_spec.rb
@@ -36,7 +36,7 @@ describe 'nftables' do
 
         it {
           is_expected.to contain_concat('nftables-ip-nat-chain-POSTROUTING').with(
-            path:           '/etc/nftables/puppet/ip-nat-chain-POSTROUTING.nft',
+            path:           '/etc/nftables/puppet-preflight/ip-nat-chain-POSTROUTING.nft',
             owner:          'root',
             group:          'root',
             mode:           '0640',
diff --git a/spec/classes/nftables_spec.rb b/spec/classes/nftables_spec.rb
index 62a03d1..7102332 100644
--- a/spec/classes/nftables_spec.rb
+++ b/spec/classes/nftables_spec.rb
@@ -34,9 +34,40 @@ describe 'nftables' do
       }
 
       it {
+        is_expected.to contain_file('/etc/nftables/puppet-preflight.nft').with(
+          ensure: 'file',
+          owner:  'root',
+          group:  'root',
+          mode:   '0640',
+          source: 'puppet:///modules/nftables/config/puppet.nft',
+        )
+      }
+
+      it {
+        is_expected.to contain_file('/etc/nftables/puppet-preflight').with(
+          ensure:  'directory',
+          owner:   'root',
+          group:   'root',
+          mode:    '0750',
+          purge:   true,
+          force:   true,
+          recurse: true,
+        )
+      }
+
+      it {
+        is_expected.to contain_exec('nft validate').with(
+          refreshonly: true,
+          command: %r{^/usr/sbin/nft -I /etc/nftables/puppet-preflight -c -f /etc/nftables/puppet-preflight.nft.*},
+        )
+      }
+
+      it {
         is_expected.to contain_service('nftables').with(
           ensure: 'running',
           enable: true,
+          hasrestart: true,
+          restart: %r{/usr/bin/systemctl reload nft.*},
         )
       }
 
diff --git a/spec/classes/router_spec.rb b/spec/classes/router_spec.rb
index 49cda5a..d3ccf81 100644
--- a/spec/classes/router_spec.rb
+++ b/spec/classes/router_spec.rb
@@ -32,7 +32,7 @@ describe 'nftables' do
 
         it {
           is_expected.to contain_concat('nftables-inet-filter-chain-default_fwd').with(
-            path:           '/etc/nftables/puppet/inet-filter-chain-default_fwd.nft',
+            path:           '/etc/nftables/puppet-preflight/inet-filter-chain-default_fwd.nft',
             owner:          'root',
             group:          'root',
             mode:           '0640',
@@ -70,7 +70,7 @@ describe 'nftables' do
 
         it {
           is_expected.to contain_concat('nftables-ip-nat-chain-PREROUTING').with(
-            path:           '/etc/nftables/puppet/ip-nat-chain-PREROUTING.nft',
+            path:           '/etc/nftables/puppet-preflight/ip-nat-chain-PREROUTING.nft',
             owner:          'root',
             group:          'root',
             mode:           '0640',
@@ -108,7 +108,7 @@ describe 'nftables' do
 
         it {
           is_expected.to contain_concat('nftables-ip-nat-chain-POSTROUTING').with(
-            path:           '/etc/nftables/puppet/ip-nat-chain-POSTROUTING.nft',
+            path:           '/etc/nftables/puppet-preflight/ip-nat-chain-POSTROUTING.nft',
             owner:          'root',
             group:          'root',
             mode:           '0640',
diff --git a/spec/classes/snat4_spec.rb b/spec/classes/snat4_spec.rb
index 75249ef..8f8abb0 100644
--- a/spec/classes/snat4_spec.rb
+++ b/spec/classes/snat4_spec.rb
@@ -37,7 +37,7 @@ describe 'nftables' do
 
         it {
           is_expected.to contain_concat('nftables-ip-nat-chain-POSTROUTING').with(
-            path:           '/etc/nftables/puppet/ip-nat-chain-POSTROUTING.nft',
+            path:           '/etc/nftables/puppet-preflight/ip-nat-chain-POSTROUTING.nft',
             owner:          'root',
             group:          'root',
             mode:           '0640',
diff --git a/spec/defines/chain_spec.rb b/spec/defines/chain_spec.rb
new file mode 100644
index 0000000..37660c2
--- /dev/null
+++ b/spec/defines/chain_spec.rb
@@ -0,0 +1,121 @@
+require 'spec_helper'
+
+describe 'nftables::chain' do
+  let(:title) { 'MYCHAIN' }
+  let(:pre_condition) { 'include nftables' }
+
+  on_supported_os.each do |os, facts|
+    context "on #{os}" do
+      let(:facts) do
+        facts
+      end
+
+      it { is_expected.to compile }
+
+      it { is_expected.to contain_concat('nftables-inet-filter-chain-MYCHAIN').that_notifies('Exec[nft validate]') }
+      it { is_expected.to contain_exec('nft validate').that_comes_before('File[/etc/nftables/puppet/inet-filter-chain-MYCHAIN.nft]') }
+      it { is_expected.to contain_file('/etc/nftables/puppet/inet-filter-chain-MYCHAIN.nft').that_comes_before('Service[nftables]') }
+
+      it {
+        is_expected.to contain_concat('nftables-inet-filter-chain-MYCHAIN').with(
+          path: '/etc/nftables/puppet-preflight/inet-filter-chain-MYCHAIN.nft',
+          owner: 'root',
+          group: 'root',
+          mode: '0640',
+          ensure_newline: true,
+        )
+      }
+      it {
+        is_expected.to contain_file('/etc/nftables/puppet/inet-filter-chain-MYCHAIN.nft').with(
+          ensure: 'file',
+          source: '/etc/nftables/puppet-preflight/inet-filter-chain-MYCHAIN.nft',
+          mode: '0640',
+          owner: 'root',
+          group: 'root',
+        )
+      }
+      it {
+        is_expected.to contain_concat__fragment('nftables-inet-filter-chain-MYCHAIN-header').with(
+          order: '00',
+          content: "# Start of fragment order:00 MYCHAIN header\nchain MYCHAIN {",
+          target: 'nftables-inet-filter-chain-MYCHAIN',
+        )
+      }
+      it {
+        is_expected.to contain_concat__fragment('nftables-inet-filter-chain-MYCHAIN-footer').with(
+          order: '99',
+          content: "# Start of fragment order:99 MYCHAIN footer\n}",
+          target: 'nftables-inet-filter-chain-MYCHAIN',
+        )
+      }
+
+      context('with table set to ip6-foo') do
+        let(:params) do
+          {
+            table: 'ip6-foo',
+          }
+        end
+
+        it {
+          is_expected.to contain_concat('nftables-ip6-foo-chain-MYCHAIN').with(
+            path: '/etc/nftables/puppet-preflight/ip6-foo-chain-MYCHAIN.nft',
+            owner: 'root',
+            group: 'root',
+            mode: '0640',
+            ensure_newline: true,
+          )
+        }
+        it {
+          is_expected.to contain_file('/etc/nftables/puppet/ip6-foo-chain-MYCHAIN.nft').with(
+            ensure: 'file',
+            source: '/etc/nftables/puppet-preflight/ip6-foo-chain-MYCHAIN.nft',
+            mode: '0640',
+            owner: 'root',
+            group: 'root',
+          )
+        }
+        it {
+          is_expected.to contain_concat__fragment('nftables-ip6-foo-chain-MYCHAIN-header').with(
+            order: '00',
+            content: "# Start of fragment order:00 MYCHAIN header\nchain MYCHAIN {",
+            target: 'nftables-ip6-foo-chain-MYCHAIN',
+          )
+        }
+        it {
+          is_expected.to contain_concat__fragment('nftables-ip6-foo-chain-MYCHAIN-footer').with(
+            order: '99',
+            content: "# Start of fragment order:99 MYCHAIN footer\n}",
+            target: 'nftables-ip6-foo-chain-MYCHAIN',
+          )
+        }
+      end
+      context 'with inject set to 22-foobar' do
+        let(:params) do
+          {
+            inject: '22-foobar',
+          }
+        end
+
+        it { is_expected.to contain_nftables__rule('foobar-jump_MYCHAIN') }
+        it {
+          is_expected.to contain_nftables__rule('foobar-jump_MYCHAIN').with(
+            order: '22',
+            content: 'jump MYCHAIN',
+          )
+        }
+        context 'with inject_oif set to alpha and inject_oif set to beta' do
+          let(:params) do
+            super().merge(inject_iif: 'alpha', inject_oif: 'beta')
+          end
+
+          it {
+            is_expected.to contain_nftables__rule('foobar-jump_MYCHAIN').with(
+              order: '22',
+              content: 'iifname alpha oifname beta jump MYCHAIN',
+            )
+          }
+        end
+      end
+    end
+  end
+end
diff --git a/spec/defines/config_spec.rb b/spec/defines/config_spec.rb
new file mode 100644
index 0000000..43f2a6a
--- /dev/null
+++ b/spec/defines/config_spec.rb
@@ -0,0 +1,93 @@
+require 'spec_helper'
+
+describe 'nftables::config' do
+  let(:pre_condition) { 'include nftables' }
+
+  on_supported_os.each do |os, facts|
+    context "on #{os}" do
+      let(:title) { 'FOO-BAR' }
+      let(:facts) do
+        facts
+      end
+
+      context 'with source and content both unset' do
+        it { is_expected.not_to compile }
+      end
+
+      context 'with a non hyphenated title' do
+        let(:title) { 'STRING' }
+
+        it { is_expected.not_to compile }
+      end
+
+      context 'with source and content both set' do
+        let(:params) do
+          {
+            source: 'foo',
+            content: 'puppet:///modules/foo/bar',
+          }
+        end
+
+        it {
+          pending('Setting source and content should be made to fail')
+          is_expected.not_to compile
+        }
+      end
+      context 'with content set' do
+        let(:params) do
+          {
+            content: 'strange content',
+          }
+        end
+
+        it { is_expected.to compile }
+        it { is_expected.to contain_concat('nftables-FOO-BAR') }
+        it {
+          is_expected.to contain_concat('nftables-FOO-BAR').with(
+            path: '/etc/nftables/puppet-preflight/FOO-BAR.nft',
+            ensure_newline: true,
+            mode: '0640',
+          )
+        }
+        it { is_expected.to contain_file('/etc/nftables/puppet/FOO-BAR.nft') }
+        it {
+          is_expected.to contain_file('/etc/nftables/puppet/FOO-BAR.nft').with(
+            ensure: 'file',
+            source: '/etc/nftables/puppet-preflight/FOO-BAR.nft',
+            mode: '0640',
+          )
+        }
+        it { is_expected.to contain_concat_fragment('nftables-FOO-BAR-header') }
+        it {
+          is_expected.to contain_concat_fragment('nftables-FOO-BAR-header').with(
+            target: 'nftables-FOO-BAR',
+            order: '00',
+            content: 'table FOO BAR {',
+          )
+        }
+        it {
+          is_expected.to contain_concat_fragment('nftables-FOO-BAR-body').with(
+            target: 'nftables-FOO-BAR',
+            order: '98',
+            content: 'strange content',
+          )
+        }
+      end
+      context 'with content set' do
+        let(:params) do
+          {
+            source: 'puppet:///modules/foo',
+          }
+        end
+
+        it {
+          is_expected.to contain_concat_fragment('nftables-FOO-BAR-body').with(
+            target: 'nftables-FOO-BAR',
+            order: '98',
+            source: 'puppet:///modules/foo',
+          )
+        }
+      end
+    end
+  end
+end
