diff --git a/REFERENCE.md b/REFERENCE.md
index cff5ef1c..f2caa5ac 100644
--- a/REFERENCE.md
+++ b/REFERENCE.md
@@ -35,6 +35,7 @@
* [`systemd::modules_load`](#systemd--modules_load): Creates a modules-load.d drop file
* [`systemd::network`](#systemd--network): Creates network config for systemd-networkd
* [`systemd::service_limits`](#systemd--service_limits): Adds a set of custom limits to the service
+* [`systemd::socket_service`](#systemd--socket_service): Create a systemd socket activated service
* [`systemd::timer`](#systemd--timer): Create a timer and optionally a service unit to execute with the timer unit
* [`systemd::tmpfile`](#systemd--tmpfile): Creates a systemd tmpfile
* [`systemd::udev::rule`](#systemd--udev--rule): Adds a custom udev rule
@@ -1333,6 +1334,62 @@ Restart the managed service after setting the limits
Default value: `true`
+### `systemd::socket_service`
+
+Systemd socket activated services have their own dependencies. This is a
+convenience wrapper around systemd::unit_file.
+
+#### Parameters
+
+The following parameters are available in the `systemd::socket_service` defined type:
+
+* [`name`](#-systemd--socket_service--name)
+* [`ensure`](#-systemd--socket_service--ensure)
+* [`socket_content`](#-systemd--socket_service--socket_content)
+* [`service_content`](#-systemd--socket_service--service_content)
+* [`enable`](#-systemd--socket_service--enable)
+
+##### `name`
+
+Data type: `Pattern['^[^/]+$']`
+
+The target unit file to create
+
+##### `ensure`
+
+Data type: `Enum['running', 'stopped', 'present', 'absent']`
+
+State of the socket service to ensure. Present means it ensures it's
+present, but doesn't ensure the service state.
+
+Default value: `'running'`
+
+##### `socket_content`
+
+Data type: `Optional[String[1]]`
+
+The content for the socket unit file. Required if ensure isn't absent.
+
+Default value: `undef`
+
+##### `service_content`
+
+Data type: `Optional[String[1]]`
+
+The content for the service unit file. Required if ensure isn't absent.
+
+Default value: `undef`
+
+##### `enable`
+
+Data type: `Optional[Boolean]`
+
+Whether to enable or disable the service. By default this is derived from
+$ensure but can be overridden for advanced use cases where the service is
+running during a migration but shouldn't be enabled on boot.
+
+Default value: `undef`
+
### `systemd::timer`
Create a timer and optionally a service unit to execute with the timer unit
diff --git a/manifests/socket_service.pp b/manifests/socket_service.pp
new file mode 100644
index 00000000..4c6ff980
--- /dev/null
+++ b/manifests/socket_service.pp
@@ -0,0 +1,69 @@
+# @summary Create a systemd socket activated service
+# @api public
+#
+# Systemd socket activated services have their own dependencies. This is a
+# convenience wrapper around systemd::unit_file.
+#
+# @param name [Pattern['^[^/]+$']]
+# The target unit file to create
+# @param ensure
+# State of the socket service to ensure. Present means it ensures it's
+# present, but doesn't ensure the service state.
+# @param socket_content
+# The content for the socket unit file. Required if ensure isn't absent.
+# @param service_content
+# The content for the service unit file. Required if ensure isn't absent.
+# @param enable
+# Whether to enable or disable the service. By default this is derived from
+# $ensure but can be overridden for advanced use cases where the service is
+# running during a migration but shouldn't be enabled on boot.
+define systemd::socket_service (
+ Enum['running', 'stopped', 'present', 'absent'] $ensure = 'running',
+ Optional[String[1]] $socket_content = undef,
+ Optional[String[1]] $service_content = undef,
+ Optional[Boolean] $enable = undef,
+) {
+ assert_type(Pattern['^[^/]+$'], $name)
+
+ if $ensure != 'absent' {
+ assert_type(NotUndef, $socket_content)
+ assert_type(NotUndef, $service_content)
+ }
+
+ $active = $ensure ? {
+ 'running' => true,
+ 'stopped' => false,
+ 'absent' => false,
+ default => undef,
+ }
+ # https://tickets.puppetlabs.com/browse/MODULES-11018
+ if $enable == undef and $active == undef {
+ $real_enable = undef
+ } else {
+ $real_enable = pick($enable, $active)
+ }
+
+ $unit_file_ensure = bool2str($ensure == 'absent', 'absent', 'present')
+
+ systemd::unit_file { "${name}.socket":
+ ensure => $unit_file_ensure,
+ content => $socket_content,
+ active => $active,
+ enable => $real_enable,
+ }
+
+ systemd::unit_file { "${name}.service":
+ ensure => $unit_file_ensure,
+ content => $service_content,
+ active => $active,
+ enable => $real_enable,
+ }
+
+ if $active != undef or $real_enable != undef {
+ # Systemd needs both .socket and .service to be loaded when starting the
+ # service. The unit_file takes care of matching, this ensures the
+ # non-matching order.
+ File["/etc/systemd/system/${name}.socket"] -> Service["${name}.service"]
+ File["/etc/systemd/system/${name}.service"] -> Service["${name}.socket"]
+ }
+}
diff --git a/spec/defines/socket_service_spec.rb b/spec/defines/socket_service_spec.rb
new file mode 100644
index 00000000..aa076c2e
--- /dev/null
+++ b/spec/defines/socket_service_spec.rb
@@ -0,0 +1,211 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'systemd::socket_service' do
+ let(:title) { 'myservice' }
+
+ on_supported_os.each do |os, os_facts|
+ context "on #{os}" do
+ let(:facts) { os_facts }
+
+ context 'ensure => running' do
+ let(:params) do
+ {
+ ensure: 'running',
+ socket_content: "[Socket]\nListenStream=/run/myservice.socket\n",
+ service_content: "[Service]\nType=notify\n",
+ }
+ end
+
+ it { is_expected.to compile.with_all_deps }
+
+ it 'sets up the socket unit file' do
+ is_expected.to contain_file('/etc/systemd/system/myservice.socket').
+ with_ensure('file').
+ with_content(%r{\[Socket\]}).
+ that_comes_before(['Service[myservice.socket]', 'Service[myservice.service]'])
+ end
+
+ it 'sets up the socket service' do
+ is_expected.to contain_service('myservice.socket').
+ with_ensure(true).
+ with_enable(true)
+ end
+
+ it 'sets up the service unit file' do
+ is_expected.to contain_file('/etc/systemd/system/myservice.service').
+ with_ensure('file').
+ with_content(%r{\[Service\]}).
+ that_comes_before('Service[myservice.service]')
+ end
+
+ it 'sets up the service service' do
+ is_expected.to contain_service('myservice.service').
+ with_ensure(true).
+ with_enable(true)
+ end
+
+ context 'enable => false' do
+ let(:params) { super().merge(enable: false) }
+
+ it { is_expected.to compile.with_all_deps }
+
+ it 'sets up the socket service' do
+ is_expected.to contain_service('myservice.socket').
+ with_ensure(true).
+ with_enable(false)
+ end
+
+ it 'sets up the service service' do
+ is_expected.to contain_service('myservice.service').
+ with_ensure(true).
+ with_enable(false)
+ end
+ end
+ end
+
+ context 'ensure => stopped' do
+ let(:params) do
+ {
+ ensure: 'stopped',
+ socket_content: "[Socket]\nListenStream=/run/myservice.socket\n",
+ service_content: "[Service]\nType=notify\n",
+ }
+ end
+
+ it { is_expected.to compile.with_all_deps }
+
+ it 'sets up the socket unit file' do
+ is_expected.to contain_file('/etc/systemd/system/myservice.socket').
+ with_ensure('file').
+ with_content(%r{\[Socket\]}).
+ that_comes_before(['Service[myservice.socket]', 'Service[myservice.service]'])
+ end
+
+ it 'sets up the socket service' do
+ is_expected.to contain_service('myservice.socket').
+ with_ensure(false).
+ with_enable(false)
+ end
+
+ it 'sets up the service unit file' do
+ is_expected.to contain_file('/etc/systemd/system/myservice.service').
+ with_ensure('file').
+ with_content(%r{\[Service\]}).
+ that_comes_before('Service[myservice.service]')
+ end
+
+ it 'sets up the service service' do
+ is_expected.to contain_service('myservice.service').
+ with_ensure(false).
+ with_enable(false)
+ end
+
+ context 'enable => true' do
+ let(:params) { super().merge(enable: true) }
+
+ it { is_expected.to compile.with_all_deps }
+
+ it 'sets up the socket service' do
+ is_expected.to contain_service('myservice.socket').
+ with_ensure(false).
+ with_enable(true)
+ end
+
+ it 'sets up the service service' do
+ is_expected.to contain_service('myservice.service').
+ with_ensure(false).
+ with_enable(true)
+ end
+ end
+ end
+
+ context 'ensure => present' do
+ let(:params) do
+ {
+ ensure: 'present',
+ socket_content: "[Socket]\nListenStream=/run/myservice.socket\n",
+ service_content: "[Service]\nType=notify\n",
+ }
+ end
+
+ it { is_expected.to compile.with_all_deps }
+
+ it 'sets up the socket unit file' do
+ is_expected.to contain_file('/etc/systemd/system/myservice.socket').
+ with_ensure('file').
+ with_content(%r{\[Socket\]})
+ end
+
+ it "doesn't set up the socket service" do
+ is_expected.not_to contain_service('myservice.socket')
+ end
+
+ it 'sets up the service unit file' do
+ is_expected.to contain_file('/etc/systemd/system/myservice.service').
+ with_ensure('file').
+ with_content(%r{\[Service\]})
+ end
+
+ it "doesn't set up the service service" do
+ is_expected.not_to contain_service('myservice.service')
+ end
+
+ context 'enable => true' do
+ let(:params) { super().merge(enable: true) }
+
+ it { is_expected.to compile.with_all_deps }
+
+ it 'sets up the socket service' do
+ is_expected.to contain_service('myservice.socket').
+ without_ensure.
+ with_enable(true)
+ end
+
+ it 'sets up the service service' do
+ is_expected.to contain_service('myservice.service').
+ without_ensure.
+ with_enable(true)
+ end
+ end
+ end
+
+ context 'ensure => absent' do
+ let(:params) do
+ {
+ ensure: 'absent',
+ }
+ end
+
+ it { is_expected.to compile.with_all_deps }
+
+ it 'sets up the socket unit file' do
+ is_expected.to contain_file('/etc/systemd/system/myservice.socket').
+ with_ensure('absent').
+ without_content.
+ that_requires('Service[myservice.socket]')
+ end
+
+ it 'sets up the socket service' do
+ is_expected.to contain_service('myservice.socket').
+ with_ensure(false).
+ with_enable(false)
+ end
+
+ it 'sets up the service unit file' do
+ is_expected.to contain_file('/etc/systemd/system/myservice.service').
+ with_ensure('absent').
+ without_content.
+ that_requires('Service[myservice.service]')
+ end
+
+ it 'sets up the service service' do
+ is_expected.to contain_service('myservice.service').
+ with_ensure(false).
+ with_enable(false)
+ end
+ end
+ end
+ end
+end