diff --git a/README.md b/README.md
index 3ff8444..3964232 100644
--- a/README.md
+++ b/README.md
@@ -118,6 +118,26 @@ quadlets::quadlet { "centos.network":
}
```
+### Manage podman secrets with puppet
+The expected format for the title is 'username:secretname'. This allows manage the same secret for multiple
+users. To manage secrets for non root users, they need to have an active session (either enable linger or being logged in !).
+```puppet
+# a secret for the root user:
+quadlets_secret{ 'root:secretname':
+ secret => 'donotforget!',
+}
+# a secret for the xyz user, with labels:
+quadlets_secret{ 'xyz:secretname':
+ secret => 'ensuretorember',
+ labels => { 'label1' => 'one', 'label2' => 'two' },
+}
+
+# ensure to remove a secret:
+quadlets_secret{ 'root:secretname':
+ ensure => 'absent'
+}
+```
+
### Hiera Representation Of User setup and Quadlet deployment
```yaml
diff --git a/REFERENCE.md b/REFERENCE.md
index 68a5b37..e1b0a47 100644
--- a/REFERENCE.md
+++ b/REFERENCE.md
@@ -21,6 +21,10 @@
* [`quadlets::quadlet`](#quadlets--quadlet): Generate and manage podman quadlet definitions (podman > 4.4.0)
* [`quadlets::user`](#quadlets--user): Generate and manage podman quadlet user
+### Resource types
+
+* [`quadlets_secret`](#quadlets_secret): Type for managing podman secrets (per user)
+
### Data types
* [`Quadlets::Auth`](#Quadlets--Auth): custom datatype to specify username and password
@@ -70,6 +74,7 @@ The following parameters are available in the `quadlets` class:
* [`purge_quadlet_dir`](#-quadlets--purge_quadlet_dir)
* [`quadlets_hash`](#-quadlets--quadlets_hash)
* [`users_hash`](#-quadlets--users_hash)
+* [`secrets_hash`](#-quadlets--secrets_hash)
##### `manage_package`
@@ -195,6 +200,14 @@ a `Hash` of `quadlets::users` to deploy
Default value: `{}`
+##### `secrets_hash`
+
+Data type: `Stdlib::CreateResources`
+
+a `Hash` of `quadlets_secrets` to deploy
+
+Default value: `{}`
+
## Defined types
### `quadlets::quadlet`
@@ -636,6 +649,78 @@ Define additional parameters to be used to create the user.
Default value: `{}`
+## Resource types
+
+### `quadlets_secret`
+
+Type for managing podman secrets (per user)
+
+#### Examples
+
+##### Define a secret mysecret for user blah
+
+```puppet
+quadlets_secret{ 'blah:mysecret':
+ secret => '***secret***',
+}
+```
+
+#### Properties
+
+The following properties are available in the `quadlets_secret` type.
+
+##### `ensure`
+
+Valid values: `present`, `absent`
+
+The basic property that the resource should be in.
+
+Default value: `present`
+
+##### `labels`
+
+secret labels to set
+
+Default value: `{}`
+
+##### `secret`
+
+the secret himself
+
+#### Parameters
+
+The following parameters are available in the `quadlets_secret` type.
+
+* [`doptions`](#-quadlets_secret--doptions)
+* [`driver`](#-quadlets_secret--driver)
+* [`name`](#-quadlets_secret--name)
+* [`provider`](#-quadlets_secret--provider)
+
+##### `doptions`
+
+driver options used for secret creation
+
+Default value: `{}`
+
+##### `driver`
+
+driver to be used for secret creation
+
+Default value: `file`
+
+##### `name`
+
+Valid values: `%r{^\S+:\S+}`
+
+namevar
+
+combination of user:secretname of the secret to administrate
+
+##### `provider`
+
+The specific backend to use for this `quadlets_secret` resource. You will seldom need to specify this --- Puppet will
+usually discover the appropriate provider for your platform.
+
## Data types
### `Quadlets::Auth`
diff --git a/lib/puppet/provider/quadlets_secret/podman.rb b/lib/puppet/provider/quadlets_secret/podman.rb
new file mode 100644
index 0000000..b488b72
--- /dev/null
+++ b/lib/puppet/provider/quadlets_secret/podman.rb
@@ -0,0 +1,105 @@
+# frozen_string_literal: true
+
+#
+# This file contains a provider for the resource type `libvirt_nwfilter`,
+#
+
+require 'json'
+
+Puppet::Type.type(:quadlets_secret).provide(:podman) do
+ desc "@summary provider for the resource type `quadlets_secret`,
+ which manages quadlet secrets
+ using the podman command."
+
+ commands podman: 'podman'
+
+ def should_user
+ @should_user || @should_user = resource[:name].split(':')[0]
+ end
+
+ def should_secretname
+ @should_secretname || @should_secretname = resource[:name].split(':')[1]
+ end
+
+ def hash_to_array(inhash)
+ res = []
+ if inhash
+ inhash.keys.sort.each do |k|
+ res << "#{k}=#{inhash[k]}"
+ end
+ end
+ res
+ end
+
+ def run_podman(args, env = {})
+ user_info = Etc.getpwnam(should_user)
+ runenv = {
+ cwd: user_info.dir,
+ failonfail: true,
+ uid: user_info.uid,
+ gid: user_info.gid,
+ combine: false,
+ custom_environment: env.merge({ 'HOME' => user_info.dir, 'XDG_RUNTIME_DIR' => "/run/user/#{user_info.uid}" })
+ }
+ # test if user is logged in or lingering:
+ raise Puppet::Error, "user #{should_user} is not logged in, consider enable linger." unless File.directory?("/run/user/#{user_info.uid}")
+
+ execute([command('podman')] + args, runenv)
+ end
+
+ def create_secret(replace)
+ args = ['secret', 'create', should_secretname, '--env', 'psecret']
+
+ if replace
+ args << "-d=#{@result['Spec']['Driver']['Name']}"
+ hash_to_array(@result['Spec']['Driver']['Options']).each do |d|
+ args << "--driver-opts=#{d}"
+ end
+ args << '--replace'
+ else
+ args << "-d=#{@resource[:driver]}"
+ hash_to_array(resource[:doptions]).each do |d|
+ args << "--driver-opts=#{d}"
+ end
+ end
+
+ resource[:labels]&.split('|')&.each do |l|
+ args << "-l=#{l}"
+ end
+
+ run_podman(args, { 'psecret' => @resource[:secret] })
+ end
+
+ def create
+ create_secret(false)
+ end
+
+ def destroy
+ run_podman(['secret', 'rm', should_secretname])
+ end
+
+ def labels=(_label)
+ create_secret(true)
+ end
+
+ def labels
+ hash_to_array(@result['Spec']['Labels']).join('|') if @result
+ end
+
+ def secret=(_secret)
+ create_secret(true)
+ end
+
+ def secret
+ @result['SecretData'] if @result
+ end
+
+ def exists?
+ begin
+ res = run_podman(['secret', 'inspect', should_secretname, '--showsecret'])
+ rescue Puppet::ExecutionFailure
+ return false
+ end
+ @result = JSON.parse(res)[0]
+ end
+end
diff --git a/lib/puppet/type/quadlets_secret.rb b/lib/puppet/type/quadlets_secret.rb
new file mode 100644
index 0000000..984ecc5
--- /dev/null
+++ b/lib/puppet/type/quadlets_secret.rb
@@ -0,0 +1,81 @@
+# frozen_string_literal: true
+
+Puppet::Type.newtype(:quadlets_secret) do
+ desc <<~DESC
+ Type for managing podman secrets (per user)
+
+ @example Define a secret mysecret for user blah
+ quadlets_secret{ 'blah:mysecret':
+ secret => '***secret***',
+ }
+ DESC
+
+ ensurable
+
+ newparam(:name, namevar: true) do
+ desc 'combination of user:secretname of the secret to administrate'
+
+ newvalues(%r{^\S+:\S+})
+ end
+
+ newparam(:driver) do
+ desc 'driver to be used for secret creation'
+ defaultto 'file'
+ end
+
+ newparam(:doptions) do
+ desc 'driver options used for secret creation'
+ defaultto({})
+
+ validate do |value|
+ raise ArgumentError, 'needs to be a Hash' unless value.is_a?(Hash)
+ end
+ end
+
+ newproperty(:labels) do
+ desc 'secret labels to set'
+ defaultto({})
+
+ validate do |value|
+ raise ArgumentError, 'needs to be a Hash' unless value.is_a?(Hash)
+ end
+
+ munge do |value|
+ res = []
+ value.keys.sort.each do |k|
+ res << "#{k}=#{value[k]}"
+ end
+ return res.join('|')
+ end
+ end
+
+ newproperty(:secret) do
+ desc 'the secret himself'
+
+ validate do |value|
+ raise ArgumentError, 'needs to be a string' unless value.is_a?(String)
+ end
+
+ def should_to_s(_value)
+ '*****'
+ end
+
+ def is_to_s(_value)
+ '*****'
+ end
+ end
+
+ autorequire(:class) { 'quadlets' }
+
+ autorequire(:user) do
+ [self[:name].split(':')[0]]
+ end
+
+ autorequire(:loginctl_user) do
+ [self[:name].split(':')[0]]
+ end
+
+ autorequire(:package) do
+ ['podman']
+ end
+end
diff --git a/manifests/init.pp b/manifests/init.pp
index 92af5e3..46a106b 100644
--- a/manifests/init.pp
+++ b/manifests/init.pp
@@ -26,6 +26,9 @@
# @param quadlets_hash a `Hash` of `quadlets::quadlet` to deploy
# @param users_hash a `Hash` of `quadlets::users` to deploy
#
+# @param secrets_hash
+# a `Hash` of `quadlets_secrets` to deploy
+#
# @example Set up Podman for quadlets
# include quadlets
#
@@ -46,6 +49,7 @@
Boolean $purge_quadlet_dir = false,
Stdlib::CreateResources $quadlets_hash = {},
Stdlib::CreateResources $users_hash = {},
+ Stdlib::CreateResources $secrets_hash = {},
) {
$quadlet_dir = '/etc/containers/systemd'
$quadlet_system_user_dir = '/etc/containers/systemd/users'
@@ -68,4 +72,10 @@
* => $_v,
}
}
+
+ $secrets_hash.each |$_n, $_v| {
+ quadlets_secret { $_n:
+ * => $_v,
+ }
+ }
}
diff --git a/spec/acceptance/quadlets_secret_spec.rb b/spec/acceptance/quadlets_secret_spec.rb
new file mode 100644
index 0000000..1db9324
--- /dev/null
+++ b/spec/acceptance/quadlets_secret_spec.rb
@@ -0,0 +1,64 @@
+# frozen_string_literal: true
+
+require 'spec_helper_acceptance'
+
+describe 'quadlets_secret' do
+ context 'with a selection of secrets for root and auser' do
+ it_behaves_like 'an idempotent resource' do
+ let(:manifest) do
+ <<-PUPPET
+ include quadlets
+
+ quadlets_secret{'root:asecret':
+ secret => 'whoknows',
+ }
+
+ quadlets_secret{'root:anothersecret':
+ secret => 'justguess',
+ labels => {
+ label1 => 'one',
+ label2 => 'two',
+ },
+ }
+
+ quadlets_secret{'root:withpath':
+ secret => 'Idonottellyou;)',
+ doptions => {
+ path => '/tmp/whithpathsecret',
+ },
+ }
+ PUPPET
+ end
+ end
+
+ it 'root:asecret exists' do
+ result = command('podman secret ls --filter Name=asecret -n --format "{{.Name}}"')
+ expect(result.stdout.strip).to eq('asecret')
+ end
+
+ it 'root:anothersecret has labels' do
+ result = command('podman secret inspect anothersecret --format "{{.Spec.Labels}}"')
+ expect(result.stdout.strip).to eq('map[label1:one label2:two]')
+ end
+
+ describe user('auser') do
+ it { is_expected.to exist }
+ end
+
+ describe 'directories for secret with path set' do
+ describe file('/tmp/whithpathsecret') do
+ it { is_expected.to be_directory }
+ it { is_expected.to be_owned_by 'root' }
+ it { is_expected.to be_grouped_into 'root' }
+ end
+ end
+
+ describe 'file for secret with path set' do
+ describe file('/tmp/whithpathsecret/secretsdata.json') do
+ it { is_expected.to be_file }
+ it { is_expected.to be_owned_by 'root' }
+ it { is_expected.to be_grouped_into 'root' }
+ end
+ end
+ end
+end
diff --git a/spec/classes/init_spec.rb b/spec/classes/init_spec.rb
index 5c789da..1631d11 100644
--- a/spec/classes/init_spec.rb
+++ b/spec/classes/init_spec.rb
@@ -236,6 +236,18 @@
it { is_expected.to contain_quadlets__user('starmer').with_manage_user(true) }
it { is_expected.to have_quadlets__user_resource_count(2) }
end
+
+ context 'with secrets defined' do
+ let(:params) do
+ { secrets_hash: {
+ 'root:secret1': {
+ secret: '** hidden **',
+ }
+ } }
+ end
+
+ it { is_expected.to contain_quadlets_secret('root:secret1').with_secret('** hidden **') }
+ end
end
end
end
diff --git a/spec/types/quadlets_secret_spec.rb b/spec/types/quadlets_secret_spec.rb
new file mode 100644
index 0000000..b8b64a6
--- /dev/null
+++ b/spec/types/quadlets_secret_spec.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'quadlets_secret' do
+ let(:title) { 'root:asecret' }
+
+ context 'with default provider' do
+ it { is_expected.to be_valid_type.with_properties('ensure') }
+ it { is_expected.to be_valid_type.with_parameters('driver') }
+ it { is_expected.to be_valid_type.with_parameters('doptions') }
+ it { is_expected.to be_valid_type.with_properties('labels') }
+ it { is_expected.to be_valid_type.with_properties('secret') }
+ end
+end