Skip to content

Make ensure_packages work with ensure => present #1300

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 37 additions & 15 deletions lib/puppet/functions/ensure_packages.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,34 +6,56 @@
# third argument to the ensure_resource() function.
Puppet::Functions.create_function(:ensure_packages, Puppet::Functions::InternalFunction) do
# @param packages
# The packages to ensure are installed. If it's a Hash it will be passed to `ensure_resource`
# The packages to ensure are installed.
# @param default_attributes
# Default attributes to be passed to the `ensure_resource()` function
# @return [Undef] Returns nothing.
dispatch :ensure_packages do
scope_param
param 'Variant[String[1], Array[String[1]], Hash[String[1], Any]]', :packages
param 'Variant[String[1], Array[String[1]]]', :packages
optional_param 'Hash', :default_attributes
return_type 'Undef'
end

def ensure_packages(scope, packages, default_attributes = nil)
if default_attributes
# @param packages
# The packages to ensure are installed. The keys are packages and values are the attributes specific to that package.
# @param default_attributes
# Default attributes. Package specific attributes from the `packages` parameter will take precedence.
# @return [Undef] Returns nothing.
dispatch :ensure_packages_hash do
scope_param
param 'Hash[String[1], Any]', :packages
optional_param 'Hash', :default_attributes
return_type 'Undef'
end

def ensure_packages(scope, packages, default_attributes = {})
Array(packages).each do |package_name|
defaults = { 'ensure' => 'installed' }.merge(default_attributes)
if defaults['ensure'] == 'present'
defaults['ensure'] = 'installed'
end
else
defaults = { 'ensure' => 'installed' }

# `present` and `installed` are aliases for the `ensure` attribute. If `ensure` is set to either of these values replace
# with `installed` by default but `present` if this package is already in the catalog with `ensure => present`
defaults['ensure'] = default_ensure(package_name) if ['present', 'installed'].include?(defaults['ensure'])

scope.call_function('ensure_resource', ['package', package_name, defaults])
end
nil
end

if packages.is_a?(Hash)
scope.call_function('ensure_resources', ['package', packages.dup, defaults])
else
Array(packages).each do |package_name|
scope.call_function('ensure_resource', ['package', package_name, defaults])
end
def ensure_packages_hash(scope, packages, default_attributes = {})
packages.each do |package, attributes|
ensure_packages(scope, package, default_attributes.merge(attributes))
end
nil
end

private

def default_ensure(package_name)
if call_function('defined_with_params', "Package[#{package_name}]", { 'ensure' => 'present' })
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The root cause of many problems is that defined_with_params doesn't understand this bit: https://github.com/puppetlabs/puppet/blob/523d881ecdee777d7bec46cea5b26fd6621f558c/lib/puppet/type/package.rb#L113-L114

I was thinking about this too and wondering about teaching defined_with_params about aliasvalue. Though I really wonder how you'd implement that, given a lot of those mechanics are private APIs.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, I did try and figure out a more generic solution, but also figured the less I touch, the less chance of subtly breaking anything!

This is effectively my 2nd PR on the way to fixing this issue. The first one was a while back and turned the function into a modern API version.

Whilst I'm not super happy with the hackiness of this solution, as a whole with the refactoring in this PR combined with the work done when moving from the old API I think the final result is still a lot nicer than what we originally had.

The breaking change in 8.0.0 has been a blocker for several people in upgrading stdlib, so hopefully this is a good enough fix and can be merged.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also ran into it myself, so I think this would be good start.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I drilled into this for a bit and got closer, but not completely there. I got stuck trying to figure out how to get a list of known aliases and couldn't get there in this context. What I'd like to do is update this check,

found_match = (res_is_undef && value_is_undef) || (res[key] == value)

to do something smarter, like res[key].aliases.include? value instead of the straight equality check it does now.

In other contexts, I can get a list of aliases using Puppet::Parameter::Value.aliases but I can't quite get there from here yet.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@binford2k that's exactly what my line of thought was, except I gave up on figuring out the .aliases bit. As I said: those parts look like private APIs.

'present'
else
'installed'
end
end
end
38 changes: 38 additions & 0 deletions spec/functions/ensure_packages_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,14 @@
subject.execute({ 'foo' => { 'provider' => 'rpm' }, 'bar' => { 'provider' => 'gem' } }, 'ensure' => 'present')
subject.execute('パッケージ' => { 'ensure' => 'absent' })
subject.execute('ρǻ¢κầģẻ' => { 'ensure' => 'absent' })
subject.execute(
{
'package_one' => {},
'package_two' => {},
'package_three' => { 'provider' => 'puppetserver_gem' },
},
{ 'provider' => 'puppet_gem' },
)
end

# this lambda is required due to strangeness within rspec-puppet's expectation handling
Expand All @@ -42,6 +50,14 @@
it { expect(-> { catalogue }).to contain_package('パッケージ').with('ensure' => 'absent') }
it { expect(-> { catalogue }).to contain_package('ρǻ¢κầģẻ').with('ensure' => 'absent') }
end

describe 'default attributes' do
it 'package specific attributes take precedence' do
expect(-> { catalogue }).to contain_package('package_one').with('provider' => 'puppet_gem')
expect(-> { catalogue }).to contain_package('package_two').with('provider' => 'puppet_gem')
expect(-> { catalogue }).to contain_package('package_three').with('provider' => 'puppetserver_gem')
end
end
end

context 'when given a catalog with "package { puppet: ensure => installed }"' do
Expand All @@ -54,4 +70,26 @@
it { expect(-> { catalogue }).to contain_package('puppet').with_ensure('installed') }
end
end

context 'when given a catalog with "package { puppet: ensure => present }"' do
let(:pre_condition) { 'package { puppet: ensure => present }' }

describe 'after running ensure_package("puppet", { "ensure" => "present" })' do
before(:each) { subject.execute('puppet', 'ensure' => 'present') }

it { expect(-> { catalogue }).to contain_package('puppet').with_ensure('present') }
end

describe 'after running ensure_package("puppet", { "ensure" => "installed" })' do
before(:each) { subject.execute('puppet', 'ensure' => 'installed') }

it { expect(-> { catalogue }).to contain_package('puppet').with_ensure('present') }
end

describe 'after running ensure_package(["puppet"])' do
before(:each) { subject.execute(['puppet']) }

it { expect(-> { catalogue }).to contain_package('puppet').with_ensure('present') }
end
end
end