commit 5dedf86cb36beb4043c7e2d34724d8439479dc48
Author: Steve Traylen <steve.traylen@cern.ch>
Date:   Thu Mar 6 12:18:35 2025 +0100

    Add ruleset for a Nomad cluster
    
    Nomad clusters typically have single public API
    port as well as rpc and serf ports for inter cluster
    communication.
    
    Example:
    
    ```puppet
    class{ 'nftables::rules::nomad':
      cluster_elements = [
        '10.0.0.1','10.0.0.2',
        '::1', '::2'',
      ],
    }
    ```
    
    The default ports can be overridden with parameters `http`, `rpc` and
    `surf`.
    
    * https://developer.hashicorp.com/nomad/docs/install/production/requirements#ports-used

diff --git a/REFERENCE.md b/REFERENCE.md
index d2694e3..248a13f 100644
--- a/REFERENCE.md
+++ b/REFERENCE.md
@@ -33,6 +33,7 @@ Enable this option to support Ceph's Monitor Daemon.
 * [`nftables::rules::nfs`](#nftables--rules--nfs): manage in nfs4
 * [`nftables::rules::nfs3`](#nftables--rules--nfs3): manage in nfs3
 * [`nftables::rules::node_exporter`](#nftables--rules--node_exporter): manage in node exporter
+* [`nftables::rules::nomad`](#nftables--rules--nomad): manage port openings for a nomad cluster
 * [`nftables::rules::ospf`](#nftables--rules--ospf): manage in ospf
 * [`nftables::rules::ospf3`](#nftables--rules--ospf3): manage in ospf3
 * [`nftables::rules::out::active_directory`](#nftables--rules--out--active_directory): manage outgoing active diectory
@@ -887,6 +888,64 @@ Specify port to open
 
 Default value: `9100`
 
+### <a name="nftables--rules--nomad"></a>`nftables::rules::nomad`
+
+manage port openings for a nomad cluster
+
+#### Examples
+
+##### Simple two node nomad cluster
+
+```puppet
+class{ 'nftables::rules::nomad':
+  cluster_elements = [
+    '10.0.0.1','10.0.0.2',
+    '::1', '::2'',
+  ],
+}
+```
+
+#### Parameters
+
+The following parameters are available in the `nftables::rules::nomad` class:
+
+* [`cluster_elements`](#-nftables--rules--nomad--cluster_elements)
+* [`http`](#-nftables--rules--nomad--http)
+* [`rpc`](#-nftables--rules--nomad--rpc)
+* [`serf`](#-nftables--rules--nomad--serf)
+
+##### <a name="-nftables--rules--nomad--cluster_elements"></a>`cluster_elements`
+
+Data type: `Array[Stdlib::IP::Address,1]`
+
+IP addreses of nomad cluster nodes
+
+Default value: `['127.0.0.1','::1']`
+
+##### <a name="-nftables--rules--nomad--http"></a>`http`
+
+Data type: `Stdlib::Port`
+
+Specify http api port to open to the world.
+
+Default value: `4646`
+
+##### <a name="-nftables--rules--nomad--rpc"></a>`rpc`
+
+Data type: `Stdlib::Port`
+
+Specify rpc port to open within the nomad cluster
+
+Default value: `4647`
+
+##### <a name="-nftables--rules--nomad--serf"></a>`serf`
+
+Data type: `Stdlib::Port`
+
+Specify serf port to open within the nomad cluster
+
+Default value: `4648`
+
 ### <a name="nftables--rules--ospf"></a>`nftables::rules::ospf`
 
 manage in ospf
diff --git a/manifests/rules/nomad.pp b/manifests/rules/nomad.pp
new file mode 100644
index 0000000..af51e96
--- /dev/null
+++ b/manifests/rules/nomad.pp
@@ -0,0 +1,59 @@
+# @summary manage port openings for a nomad cluster
+#
+# @param cluster_elements IP addreses of nomad cluster nodes
+# @param http Specify http api port to open to the world.
+# @param rpc Specify rpc port to open within the nomad cluster
+# @param serf Specify serf port to open within the nomad cluster
+#
+# @example Simple two node nomad cluster
+#  class{ 'nftables::rules::nomad':
+#    cluster_elements = [
+#      '10.0.0.1','10.0.0.2',
+#      '::1', '::2'',
+#    ],
+#  }
+#
+class nftables::rules::nomad (
+  Stdlib::Port $http = 4646,
+  Stdlib::Port $rpc  = 4647,
+  Stdlib::Port $serf = 4648,
+  Array[Stdlib::IP::Address,1] $cluster_elements = ['127.0.0.1','::1'],
+) {
+  # Open http api port to everything.
+  #
+  nftables::rule { 'default_in-nomad_http':
+    content => "tcp dport ${http}",
+  }
+
+  ['ip','ip6'].each | $_family | {
+    $_ip_type = $_family ? {
+      'ip'    => Stdlib::IP::Address::V4,
+      default => Stdlib::IP::Address::V6,
+    }
+    $_set_type = $_family ? {
+      'ip'    => 'ipv4_addr',
+      default => 'ipv6_addr',
+    }
+
+    $_elements = $cluster_elements.filter | $_ip | { $_ip =~ $_ip_type }
+
+    if $_elements.length > 0 {
+      nftables::set { "nomad_${_family}":
+        elements => $_elements,
+        type     => $_set_type,
+      }
+
+      nftables::rule { "default_in-nomad_rpc_${_family}":
+        content => "tcp dport ${rpc} ${_family} saddr @nomad_${_family} accept",
+      }
+
+      nftables::rule { "default_in-nomad_serf_udp_${_family}":
+        content => "udp dport ${serf} ${_family} saddr @nomad_${_family} accept",
+      }
+
+      nftables::rule { "default_in-nomad_serf_tcp_${_family}":
+        content => "tcp dport ${serf} ${_family} saddr @nomad_${_family} accept",
+      }
+    }
+  }
+}
diff --git a/spec/acceptance/all_rules_spec.rb b/spec/acceptance/all_rules_spec.rb
index 9ca86bb..7d325ba 100644
--- a/spec/acceptance/all_rules_spec.rb
+++ b/spec/acceptance/all_rules_spec.rb
@@ -48,6 +48,7 @@ describe 'nftables class' do
       include nftables::rules::smtp_submission
       include nftables::rules::https
       include nftables::rules::nfs
+      include nftables::rules::nomad
       include nftables::rules::smtps
       include nftables::rules::smtp
       include nftables::rules::ceph
diff --git a/spec/classes/rules/nomad_spec.rb b/spec/classes/rules/nomad_spec.rb
new file mode 100644
index 0000000..f5fd063
--- /dev/null
+++ b/spec/classes/rules/nomad_spec.rb
@@ -0,0 +1,99 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'nftables::rules::nomad' do
+  on_supported_os.each do |os, os_facts|
+    context "on #{os}" do
+      let(:facts) { os_facts }
+
+      context 'default options' do
+        it { is_expected.to compile }
+
+        it {
+          is_expected.to contain_nftables__set('nomad_ip').with(
+            {
+              type: 'ipv4_addr',
+              elements: ['127.0.0.1'],
+            }
+          )
+        }
+
+        it {
+          is_expected.to contain_nftables__set('nomad_ip6').with(
+            {
+              type: 'ipv6_addr',
+              elements: ['::1'],
+            }
+          )
+        }
+
+        it {
+          is_expected.to contain_nftables__rule('default_in-nomad_http').with_content('tcp dport 4646')
+          is_expected.to contain_nftables__rule('default_in-nomad_rpc_ip6').with_content('tcp dport 4647 ip6 saddr @nomad_ip6 accept')
+          is_expected.to contain_nftables__rule('default_in-nomad_rpc_ip').with_content('tcp dport 4647 ip saddr @nomad_ip accept')
+          is_expected.to contain_nftables__rule('default_in-nomad_serf_tcp_ip6').with_content('tcp dport 4648 ip6 saddr @nomad_ip6 accept')
+          is_expected.to contain_nftables__rule('default_in-nomad_serf_tcp_ip').with_content('tcp dport 4648 ip saddr @nomad_ip accept')
+          is_expected.to contain_nftables__rule('default_in-nomad_serf_udp_ip6').with_content('udp dport 4648 ip6 saddr @nomad_ip6 accept')
+          is_expected.to contain_nftables__rule('default_in-nomad_serf_udp_ip').with_content('udp dport 4648 ip saddr @nomad_ip accept')
+        }
+      end
+
+      context 'with ports set' do
+        let(:params) do
+          {
+            http: 1000,
+            rpc: 2000,
+            serf: 3000,
+          }
+        end
+
+        it { is_expected.to compile }
+
+        it {
+          is_expected.to contain_nftables__set('nomad_ip')
+          is_expected.to contain_nftables__set('nomad_ip6')
+        }
+
+        it {
+          is_expected.to contain_nftables__rule('default_in-nomad_http').with_content('tcp dport 1000')
+          is_expected.to contain_nftables__rule('default_in-nomad_rpc_ip6').with_content('tcp dport 2000 ip6 saddr @nomad_ip6 accept')
+          is_expected.to contain_nftables__rule('default_in-nomad_rpc_ip').with_content('tcp dport 2000 ip saddr @nomad_ip accept')
+          is_expected.to contain_nftables__rule('default_in-nomad_serf_tcp_ip6').with_content('tcp dport 3000 ip6 saddr @nomad_ip6 accept')
+          is_expected.to contain_nftables__rule('default_in-nomad_serf_tcp_ip').with_content('tcp dport 3000 ip saddr @nomad_ip accept')
+          is_expected.to contain_nftables__rule('default_in-nomad_serf_udp_ip6').with_content('udp dport 3000 ip6 saddr @nomad_ip6 accept')
+          is_expected.to contain_nftables__rule('default_in-nomad_serf_udp_ip').with_content('udp dport 3000 ip saddr @nomad_ip accept')
+        }
+      end
+
+      context 'with ipv4 hosts only' do
+        let(:params) do
+          {
+            cluster_elements: ['127.0.0.1', '127.0.0.2']
+          }
+        end
+
+        it {
+          is_expected.to contain_nftables__set('nomad_ip').with(
+            {
+              type: 'ipv4_addr',
+              elements: ['127.0.0.1', '127.0.0.2'],
+            }
+          )
+        }
+
+        it { is_expected.not_to contain_nftables__set('nomad_ip6') }
+
+        it {
+          is_expected.to contain_nftables__rule('default_in-nomad_http').with_content('tcp dport 4646')
+          is_expected.not_to contain_nftables__rule('default_in-nomad_rpc_ip6')
+          is_expected.to contain_nftables__rule('default_in-nomad_rpc_ip').with_content('tcp dport 4647 ip saddr @nomad_ip accept')
+          is_expected.not_to contain_nftables__rule('default_in-nomad_serf_tcp_ip6')
+          is_expected.to contain_nftables__rule('default_in-nomad_serf_tcp_ip').with_content('tcp dport 4648 ip saddr @nomad_ip accept')
+          is_expected.not_to contain_nftables__rule('default_in-nomad_serf_udp_ip6')
+          is_expected.to contain_nftables__rule('default_in-nomad_serf_udp_ip').with_content('udp dport 4648 ip saddr @nomad_ip accept')
+        }
+      end
+    end
+  end
+end
