12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,18 @@ All notable changes to this project will be documented in this file.
Each new release typically also includes the latest modulesync defaults.
These should not affect the functionality of the module.

## [v13.2.0](https://github.com/voxpupuli/puppet-rabbitmq/tree/v13.2.0) (2023-12-10)

[Full Changelog](https://github.com/voxpupuli/puppet-rabbitmq/compare/v13.1.2...v13.2.0)

**Implemented enhancements:**

- Add support for Vhost metadata [\#964](https://github.com/voxpupuli/puppet-rabbitmq/pull/964) ([jimmybigcommerce](https://github.com/jimmybigcommerce))

**Fixed bugs:**

- resources fail to prefetch when rabbitmq is not intended to be installed \(via --noop or --tags\) [\#961](https://github.com/voxpupuli/puppet-rabbitmq/issues/961)

## [v13.1.2](https://github.com/voxpupuli/puppet-rabbitmq/tree/v13.1.2) (2023-11-05)

[Full Changelog](https://github.com/voxpupuli/puppet-rabbitmq/compare/v13.1.1...v13.1.2)
Expand Down
21 changes: 20 additions & 1 deletion REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -2080,14 +2080,27 @@ $ puppet resource rabbitmq_vhost`

```puppet
rabbitmq_vhost { 'myvhost':
ensure => present,
ensure => present,
description => 'myvhost description',
tags => ['tag1', 'tag2'],
default_queue_type => 'quorum',
}
```

#### Properties

The following properties are available in the `rabbitmq_vhost` type.

##### `default_queue_type`

Valid values: `classic`, `quorum`, `stream`

The default queue type for queues in this vhost

##### `description`

A description of the vhost

##### `ensure`

Valid values: `present`, `absent`
Expand All @@ -2096,6 +2109,12 @@ The basic property that the resource should be in.

Default value: `present`

##### `tags`

additional tags for the vhost

Default value: `[]`

#### Parameters

The following parameters are available in the `rabbitmq_vhost` type.
Expand Down
8 changes: 8 additions & 0 deletions lib/puppet/provider/rabbitmq_cli.rb
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,14 @@ def self.rabbitmqctl_list(resource, *opts)
['-q']
end
rabbitmqctl("list_#{resource}", *list_opts, *opts)
rescue Puppet::MissingCommand
# rabbitmqctl is not present. Normally we would have installed a package
# that provides rabbitmqctl by now, but if we're running under --noop or
# with a restrictive set of tags, the package may not have been installed.
# Return an empty string to avoid error. This may give false positives for
# resources under --noop!
Puppet.debug('rabbitmqctl command not found; assuming rabbitmq is not installed')
''
end

def self.rabbitmq_running
Expand Down
5 changes: 5 additions & 0 deletions lib/puppet/provider/rabbitmq_plugin/rabbitmqplugins.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ def self.plugin_list
# To preserve idempotency we should get all enabled plugins regardless of implicitly or
# explicitly enabled.
rabbitmqplugins('list', '-e', '-m')
rescue Puppet::MissingCommand
# See note about Puppet::MissingCommand in:
# lib/puppet/provider/rabbitmq_cli.rb
Puppet.debug('rabbitmqplugins command not found; assuming rabbitmq is not installed')
''
end
# Split by newline.
lines = list_str.split(%r{\n})
Expand Down
95 changes: 88 additions & 7 deletions lib/puppet/provider/rabbitmq_vhost/rabbitmqctl.rb
Original file line number Diff line number Diff line change
@@ -1,23 +1,104 @@
# frozen_string_literal: true

require File.expand_path(File.join(File.dirname(__FILE__), '..', 'rabbitmq_cli'))
Puppet::Type.type(:rabbitmq_vhost).provide(:rabbitmqctl, parent: Puppet::Provider::RabbitmqCli) do
Puppet::Type.type(:rabbitmq_vhost).provide(
:rabbitmqctl,
parent: Puppet::Provider::RabbitmqCli
) do
confine feature: :posix

def self.instances
vhost_list = run_with_retries do
rabbitmqctl_list('vhosts')
def self.prefetch(resources)
instances.each do |prov|
if (resource = resources[prov.name])
resource.provider = prov
end
end
end

# To maintain compatibility with older versions of RabbitMQ,
# we only deal with vhost metadata >= version 3.11.0
def self.supports_metadata?
Puppet::Util::Package.versioncmp(rabbitmq_version, '3.11') >= 0
end

def supports_metadata?
self.class.supports_metadata?
end

def self.vhost_list
run_with_retries do
if supports_metadata?
rabbitmqctl_list('vhosts', 'name,description,default_queue_type,tags', '-s')
else
rabbitmqctl_list('vhosts')
end
end
end

def self.instances
vhost_list.split(%r{\n}).map do |line|
raise Puppet::Error, "Cannot parse invalid vhost line: #{line}" unless line =~ %r{^(\S+)$}
if supports_metadata?
raise Puppet::Error, "Cannot parse invalid vhost line: #{line}" unless \
(matches = line.match(%r{^(\S+)\t+(.*?)\t+(undefined|quorum|classic|stream)?\t+\[(.*?)\]$}i))

new(name: Regexp.last_match(1))
name, description, default_queue_type, tags = matches.captures
# RMQ returns 'undefined' as default_queue_type if it has never been set
default_queue_type = nil if default_queue_type == 'undefined'
new(ensure: :present, name: name, description: description, default_queue_type: default_queue_type, tags: tags.split(%r{,\s*}))
else
raise Puppet::Error, "Cannot parse invalid vhost line: #{line}" unless line =~ %r{^(\S+)$}

new(ensure: :present, name: Regexp.last_match(1))
end
end
end

def create
rabbitmqctl('add_vhost', resource[:name])
rabbitmqctl('add_vhost', *params)
end

def params
params = [resource[:name]]
if supports_metadata?
params << ['--description', resource[:description]] if resource[:description]
params << ['--default-queue-type', resource[:default_queue_type]] if resource[:default_queue_type] && resource[:default_queue_type] != 'undefined'
params << ['--tags', resource[:tags].join(',')] if resource[:tags]
end
params
end

def description
@property_hash[:description]
end

def tags
@property_hash[:tags]
end

def default_queue_type
@property_hash[:default_queue_type]
end

def tags=(tags)
@property_hash[:tags] = tags
end

def description=(value)
@property_hash[:description] = value
end

def default_queue_type=(value)
@property_hash[:default_queue_type] = value
end

def flush
return if @property_hash.empty? || !supports_metadata? || !exists?

params = [resource[:name]]
params << ['--description', @property_hash[:description]] if @property_hash[:description]
params << ['--default-queue-type', @property_hash[:default_queue_type]] if @property_hash[:default_queue_type]
params << ['--tags', @property_hash[:tags].join(',')] if @property_hash[:tags]
rabbitmqctl('update_vhost_metadata', *params)
end

def destroy
Expand Down
23 changes: 22 additions & 1 deletion lib/puppet/type/rabbitmq_vhost.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@
@example Create a rabbitmq_vhost
rabbitmq_vhost { 'myvhost':
ensure => present,
ensure => present,
description => 'myvhost description',
tags => ['tag1', 'tag2'],
default_queue_type => 'quorum',
}
DESC

Expand All @@ -29,4 +32,22 @@
desc 'The name of the vhost to add'
newvalues(%r{^\S+$})
end

newproperty(:description) do
desc 'A description of the vhost'
end

newproperty(:default_queue_type) do
desc 'The default queue type for queues in this vhost'
newvalues(:classic, :quorum, :stream)
munge(&:to_s)
end

newproperty(:tags, array_matching: :all) do
desc 'additional tags for the vhost'
validate do |value|
raise ArgumentError, "Invalid tag: #{value.inspect}" unless value =~ %r{^\S+$}
end
defaultto []
end
end
2 changes: 1 addition & 1 deletion metadata.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "puppet-rabbitmq",
"version": "13.1.2",
"version": "13.2.0",
"author": "voxpupuli",
"summary": "Installs, configures, and manages RabbitMQ.",
"license": "Apache-2.0",
Expand Down
103 changes: 94 additions & 9 deletions spec/unit/puppet/provider/rabbitmq_vhost/rabbitmqctl_spec.rb
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
# frozen_string_literal: true

require 'spec_helper'

provider_class = Puppet::Type.type(:rabbitmq_vhost).provider(:rabbitmqctl)
describe provider_class do
describe Puppet::Type.type(:rabbitmq_vhost).provider(:rabbitmqctl) do
let(:resource) do
Puppet::Type::Rabbitmq_vhost.new(
name: 'foo'
name: 'foo',
description: 'foo description',
default_queue_type: 'quorum',
tags: %w[foo bar]
)
end
let(:provider) { provider_class.new(resource) }
let(:provider) { described_class.new(resource) }

it 'matches vhost names' do
provider.expects(:rabbitmqctl_list).with('vhosts').returns <<~EOT
Expand Down Expand Up @@ -37,12 +38,96 @@
expect(provider.exists?).to eq(false)
end

it 'calls rabbitmqctl to create' do
provider.expects(:rabbitmqctl).with('add_vhost', 'foo')
provider.create
context 'with RabbitMQ version <3.11.0 (no metadata support)' do
it 'calls rabbitmqctl to create' do
provider.expects(:supports_metadata?).at_least_once.returns false
provider.expects(:rabbitmqctl).with('add_vhost', 'foo')
provider.create
end
end

context 'with RabbitMQ version >=3.11.0 (metadata support)' do
it 'parses vhost list with valid metadata' do
provider.class.expects(:supports_metadata?).at_least_once.returns true
provider.class.expects(:vhost_list).returns <<~EOT
inventory classic []
/ Default virtual host undefined []
search quorum []
testing My cool vhost undefined [tag1, tag2]
EOT
instances = provider.class.instances
expect(instances.size).to eq(4)
expect(instances.map do |prov|
{
name: prov.get(:name),
description: prov.get(:description),
default_queue_type: prov.get(:default_queue_type),
tags: prov.get(:tags)
}
end).to eq(
[
{
name: 'inventory',
description: '',
default_queue_type: 'classic',
tags: []
},
{
name: '/',
description: 'Default virtual host',
default_queue_type: :absent,
tags: []
},
{
name: 'search',
description: '',
default_queue_type: 'quorum',
tags: []
},
{
name: 'testing',
description: 'My cool vhost',
default_queue_type: :absent,
tags: %w[tag1 tag2]
}
]
)
end

it 'throws error when parsing invalid vhost metadata' do
provider.class.expects(:supports_metadata?).at_least_once.returns true
provider.class.expects(:vhost_list).returns <<~EOT
inventory undefined []
/ Default virtual host undefined
EOT
expect { print provider.class.instances }.to raise_error(Puppet::Error, %r{Cannot parse invalid vhost line: / Default virtual host undefined})
end

it 'calls rabbitmqctl to create with metadata' do
provider.expects(:supports_metadata?).at_least_once.returns true
provider.expects(:rabbitmqctl).with('add_vhost', 'foo', ['--description', 'foo description'], \
['--default-queue-type', 'quorum'], ['--tags', 'foo,bar'])
provider.create
end

it 'updates tags' do
provider.set(tags: %w[tag1 tag2])
provider.expects(:exists?).at_least_once.returns true
provider.expects(:supports_metadata?).at_least_once.returns true
provider.expects(:rabbitmqctl).with('update_vhost_metadata', 'foo', ['--tags', 'tag1,tag2'])
provider.flush
end

it 'updates description' do
provider.set(description: 'this is the new description')
provider.expects(:exists?).at_least_once.returns true
provider.expects(:supports_metadata?).at_least_once.returns true
provider.expects(:rabbitmqctl).with('update_vhost_metadata', 'foo', ['--description', 'this is the new description'])
provider.flush
end
end

it 'calls rabbitmqctl to create' do
it 'calls rabbitmqctl to delete' do
provider.expects(:rabbitmqctl).with('delete_vhost', 'foo')
provider.destroy
end
Expand Down