diff --git a/REFERENCE.md b/REFERENCE.md
index a2e51bb9..e78c9249 100644
--- a/REFERENCE.md
+++ b/REFERENCE.md
@@ -40,6 +40,7 @@
* [`systemd::tmpfile`](#systemd--tmpfile): Creates a systemd tmpfile
* [`systemd::udev::rule`](#systemd--udev--rule): Adds a custom udev rule
* [`systemd::unit_file`](#systemd--unit_file): Creates a systemd unit file
+* [`systemd::user_service`](#systemd--user_service): Manage a user service running under systemd --user
### Resource types
@@ -2251,6 +2252,104 @@ restart (notify) the service when unit file changed
Default value: `true`
+### `systemd::user_service`
+
+Manage a user service running under systemd --user
+
+#### Examples
+
+##### Enable a service for all users
+
+```puppet
+systemd::user_service { 'systemd-tmpfiles-clean.timer':
+ enable => true,
+ global => true,
+}
+```
+
+##### Enable a particular user's service
+
+```puppet
+systemd::user_service { 'podman-auto-update.timer':
+ ensure => true,
+ enable => true,
+ user => 'steve',
+}
+```
+
+##### Notify a user's service to restart it
+
+```puppet
+file{ '/home/steve/.config/systemd/user/podman-auto-update.timer':
+ ensure => file,
+ content => ...,
+ notify => Systemd::User_service['steve-podman-auto-update.timer']
+}
+
+systemd::user_service { 'steve-podman-auto-update.timer':
+ ensure => true,
+ enable => true,
+ unit => 'podman-auto-update.timer',
+ user => 'steve',
+}
+
+@param unit Unit name to work on
+@param ensure Should the unit be started or stopped. Can only be true if user is specified.
+@param enable Should the unit be enabled or disabled
+@param user User name of user whose unit should be acted upon. Mutually exclusive with
+@param global Act globally for all users. Mutually exclusibe with `user`.
+```
+
+#### Parameters
+
+The following parameters are available in the `systemd::user_service` defined type:
+
+* [`unit`](#-systemd--user_service--unit)
+* [`ensure`](#-systemd--user_service--ensure)
+* [`enable`](#-systemd--user_service--enable)
+* [`global`](#-systemd--user_service--global)
+* [`user`](#-systemd--user_service--user)
+
+##### `unit`
+
+Data type: `Systemd::Unit`
+
+
+
+Default value: `$title`
+
+##### `ensure`
+
+Data type: `Variant[Boolean,Enum['stopped','running']]`
+
+
+
+Default value: `false`
+
+##### `enable`
+
+Data type: `Boolean`
+
+
+
+Default value: `false`
+
+##### `global`
+
+Data type: `Boolean`
+
+
+
+Default value: `false`
+
+##### `user`
+
+Data type: `Optional[String[1]]`
+
+
+
+Default value: `undef`
+
## Resource types
### `loginctl_user`
diff --git a/manifests/user_service.pp b/manifests/user_service.pp
new file mode 100644
index 00000000..b1fc930f
--- /dev/null
+++ b/manifests/user_service.pp
@@ -0,0 +1,136 @@
+# @summary Manage a user service running under systemd --user
+#
+# @example Enable a service for all users
+# systemd::user_service { 'systemd-tmpfiles-clean.timer':
+# enable => true,
+# global => true,
+# }
+#
+# @example Enable a particular user's service
+# systemd::user_service { 'podman-auto-update.timer':
+# ensure => true,
+# enable => true,
+# user => 'steve',
+# }
+#
+# @example Notify a user's service to restart it
+# file{ '/home/steve/.config/systemd/user/podman-auto-update.timer':
+# ensure => file,
+# content => ...,
+# notify => Systemd::User_service['steve-podman-auto-update.timer']
+# }
+#
+# systemd::user_service { 'steve-podman-auto-update.timer':
+# ensure => true,
+# enable => true,
+# unit => 'podman-auto-update.timer',
+# user => 'steve',
+# }
+#
+# @param unit Unit name to work on
+# @param ensure Should the unit be started or stopped. Can only be true if user is specified.
+# @param enable Should the unit be enabled or disabled
+# @param user User name of user whose unit should be acted upon. Mutually exclusive with
+# @param global Act globally for all users. Mutually exclusibe with `user`.
+#
+define systemd::user_service (
+ Systemd::Unit $unit = $title,
+ Variant[Boolean,Enum['stopped','running']] $ensure = false,
+ Boolean $enable = false,
+ Boolean $global = false,
+ Optional[String[1]] $user = undef,
+) {
+ $_ensure = $ensure ? {
+ 'stopped' => false,
+ 'running' => true,
+ default => $ensure
+ }
+
+ if ( $global and $user ) or ( ! $global and ! $user) {
+ fail('Exactly one of the "user" or "global" parameters must be defined')
+ }
+
+ if $global and $_ensure {
+ fail('Cannot ensure a service is running for all users globally')
+ }
+
+ if $global {
+ if $enable {
+ $_title = "Enable user service ${unit} globally"
+ $_command = ['systemctl', '--global', 'enable', $unit]
+ $_unless = [['systemctl', '--global', 'is-enabled', $unit]]
+ $_onlyif = undef
+ } else {
+ $_title = "Disable user service ${unit} globally"
+ $_command = ['systemctl', '--global', 'disable', $unit]
+ $_unless = undef
+ $_onlyif = [['systemctl', '--global', 'is-enabled', $unit]]
+ }
+ exec { $_title:
+ command => $_command,
+ unless => $_unless,
+ onlyif => $_onlyif,
+ path => $facts['path'],
+ }
+ } else { # per user services
+
+ $_systemctl_user = [
+ 'systemd-run', '--pipe', '--wait', '--user', '--machine', "${user}@.host",
+ 'systemctl', '--user',
+ ]
+
+ # To accept notifies of this type.
+ exec { "try-reload-or-restart-${user}-${unit}":
+ command => $_systemctl_user + ['try-reload-or-restart', $unit],
+ refreshonly => true,
+ path => $facts['path'],
+ }
+
+ if $_ensure {
+ $_ensure_title = "Start user service ${unit} for user ${user}"
+ $_ensure_command = $_systemctl_user + ['start', $unit]
+ $_ensure_unless = [$_systemctl_user + ['is-active', $unit]]
+ $_ensure_onlyif = undef
+
+ # Don't reload just after starting
+ Exec["try-reload-or-restart-${user}-${unit}"] -> Exec[$_ensure_title]
+ } else {
+ $_ensure_title = "Stop user service ${unit} for user ${user}"
+ $_ensure_command = $_systemctl_user + ['stop', $unit]
+ $_ensure_unless = undef
+ $_ensure_onlyif = [$_systemctl_user + ['is-active', $unit]]
+ }
+
+ exec { $_ensure_title:
+ command => $_ensure_command,
+ unless => $_ensure_unless,
+ onlyif => $_ensure_onlyif,
+ path => $facts['path'],
+ }
+
+ if $enable {
+ $_enable_title = "Enable user service ${unit} for user ${user}"
+ $_enable_command = $_systemctl_user + ['enable', $unit]
+ $_enable_unless = [$_systemctl_user + ['is-enabled', $unit]]
+ $_enable_onlyif = undef
+
+ # Puppet does this for services so lets copy that logic
+ # don't enable if you can't start.
+ if $_ensure {
+ Exec[$_ensure_title] -> Exec[$_enable_title]
+ }
+ } else {
+ $_enable_title = "Disable user service ${unit} for user ${user}"
+ $_enable_command = $_systemctl_user + ['disable', $unit]
+ $_enable_unless = undef
+ $_enable_onlyif = [$_systemctl_user + ['is-enabled', $unit]]
+ }
+
+ exec { $_enable_title:
+ command => $_enable_command,
+ unless => $_enable_unless,
+ onlyif => $_enable_onlyif,
+ path => $facts['path'],
+ }
+ }
+}
diff --git a/metadata.json b/metadata.json
index 9e1f8837..edde66d2 100644
--- a/metadata.json
+++ b/metadata.json
@@ -100,7 +100,7 @@
"requirements": [
{
"name": "puppet",
- "version_requirement": ">= 7.0.0 < 9.0.0"
+ "version_requirement": ">= 7.9.0 < 9.0.0"
}
]
}
diff --git a/spec/defines/user_service_spec.rb b/spec/defines/user_service_spec.rb
new file mode 100644
index 00000000..81e2f70c
--- /dev/null
+++ b/spec/defines/user_service_spec.rb
@@ -0,0 +1,154 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'systemd::user_service' do
+ context 'supported operating systems' do
+ on_supported_os.each do |os, facts|
+ context "on #{os}" do
+ let(:facts) { facts }
+ let(:title) { 'mine.timer' }
+
+ context 'with defaults' do
+ it { is_expected.to compile.and_raise_error(%r{"user" or "global"}) }
+ end
+
+ context 'with user and global set' do
+ let(:params) do
+ { global: true, user: 'steve' }
+ end
+
+ it { is_expected.to compile.and_raise_error(%r{"user" or "global"}) }
+ end
+
+ context 'with global and ensure' do
+ let(:params) do
+ { global: true, ensure: 'running' }
+ end
+
+ it { is_expected.to compile.and_raise_error(%r{Cannot ensure a service is running for all users globally}) }
+ end
+
+ context 'with global enable' do
+ let(:params) do
+ { global: true, enable: true }
+ end
+
+ it {
+ is_expected.to contain_exec('Enable user service mine.timer globally')
+ }
+
+ it {
+ is_expected.to contain_exec('Enable user service mine.timer globally').
+ with_command(['systemctl', '--global', 'enable', 'mine.timer']).
+ with_unless([['systemctl', '--global', 'is-enabled', 'mine.timer']]).
+ without_onlyif
+ }
+ end
+
+ context 'with global disable' do
+ let(:params) do
+ { global: true, enable: false }
+ end
+
+ it {
+ is_expected.to contain_exec('Disable user service mine.timer globally').
+ with_command(['systemctl', '--global', 'disable', 'mine.timer']).
+ without_unless.
+ with_onlyif([['systemctl', '--global', 'is-enabled', 'mine.timer']])
+ }
+ end
+
+ context 'with a user specified' do
+ let(:params) do
+ { user: 'steve' }
+ end
+
+ it { is_expected.to contain_exec('try-reload-or-restart-steve-mine.timer') }
+
+ context 'with enable and ensure false' do
+ let(:params) do
+ super().merge(enable: false, ensure: 'stopped')
+ end
+
+ it {
+ is_expected.to contain_exec('Stop user service mine.timer for user steve').
+ with_command([
+ 'systemd-run', '--pipe', '--wait', '--user', '--machine', 'steve@.host',
+ 'systemctl', '--user', 'stop', 'mine.timer',
+ ]).
+ with_onlyif([[
+ 'systemd-run', '--pipe', '--wait', '--user', '--machine', 'steve@.host',
+ 'systemctl', '--user', 'is-active', 'mine.timer',
+ ]]).
+ without_unless
+ }
+
+ it {
+ is_expected.to contain_exec('Disable user service mine.timer for user steve').
+ with_command([
+ 'systemd-run', '--pipe', '--wait', '--user', '--machine', 'steve@.host',
+ 'systemctl', '--user', 'disable', 'mine.timer',
+ ]).
+ with_onlyif([[
+ 'systemd-run', '--pipe', '--wait', '--user', '--machine', 'steve@.host',
+ 'systemctl', '--user', 'is-enabled', 'mine.timer',
+ ]]).
+ without_unless
+ }
+ end
+
+ context 'with enable and ensure true' do
+ let(:params) do
+ super().merge(enable: true, ensure: 'running')
+ end
+
+ it {
+ is_expected.to contain_exec('Start user service mine.timer for user steve').
+ with_command([
+ 'systemd-run', '--pipe', '--wait', '--user', '--machine', 'steve@.host',
+ 'systemctl', '--user', 'start', 'mine.timer',
+ ]).
+ without_onlyif.
+ with_unless([[
+ 'systemd-run', '--pipe', '--wait', '--user', '--machine', 'steve@.host',
+ 'systemctl', '--user', 'is-active', 'mine.timer',
+ ]])
+ }
+
+ it {
+ is_expected.to contain_exec('Enable user service mine.timer for user steve').
+ with_command([
+ 'systemd-run', '--pipe', '--wait', '--user', '--machine', 'steve@.host',
+ 'systemctl', '--user', 'enable', 'mine.timer',
+ ]).
+ without_onlif.
+ with_unless([[
+ 'systemd-run', '--pipe', '--wait', '--user', '--machine', 'steve@.host',
+ 'systemctl', '--user', 'is-enabled', 'mine.timer',
+ ]])
+ }
+ end
+
+ context 'with enable true and ensure false' do
+ let(:params) do
+ super().merge(enable: true, ensure: false)
+ end
+
+ it { is_expected.to contain_exec('Stop user service mine.timer for user steve') }
+ it { is_expected.to contain_exec('Enable user service mine.timer for user steve') }
+ end
+
+ context 'with enable false and ensure true' do
+ let(:params) do
+ super().merge(enable: false, ensure: true)
+ end
+
+ it { is_expected.to contain_exec('Start user service mine.timer for user steve') }
+ it { is_expected.to contain_exec('Disable user service mine.timer for user steve') }
+ end
+ end
+ end
+ end
+ end
+end