diff --git a/lib/pdk/cli.rb b/lib/pdk/cli.rb index 80baf037a..5ea61b996 100644 --- a/lib/pdk/cli.rb +++ b/lib/pdk/cli.rb @@ -143,6 +143,7 @@ def self.puppet_dev_option(dsl) require 'pdk/cli/bundle' require 'pdk/cli/build' + require 'pdk/cli/config' require 'pdk/cli/convert' require 'pdk/cli/new' require 'pdk/cli/test' diff --git a/lib/pdk/cli/config.rb b/lib/pdk/cli/config.rb new file mode 100644 index 000000000..ef12ef418 --- /dev/null +++ b/lib/pdk/cli/config.rb @@ -0,0 +1,20 @@ +module PDK::CLI + @config_cmd = @base_cmd.define_command do + name 'config' + usage _('config [subcommand] [options]') + summary _('Configure the Puppet Development Kit.') + default_subcommand 'get' + + run do |_opts, args, _cmd| + if args == ['help'] + PDK::CLI.run(%w[config --help]) + exit 0 + end + + PDK::CLI.run(%w[config get]) if args.empty? + end + end + @config_cmd.add_command Cri::Command.new_basic_help +end + +require 'pdk/cli/config/get' diff --git a/lib/pdk/cli/config/get.rb b/lib/pdk/cli/config/get.rb new file mode 100644 index 000000000..a8b3c19ae --- /dev/null +++ b/lib/pdk/cli/config/get.rb @@ -0,0 +1,18 @@ +module PDK::CLI + @config_get_cmd = @config_cmd.define_command do + name 'get' + usage _('config get [name]') + summary _('Retrieve the configuration for . If not specified, retrieve all configuration settings') + + run do |_opts, args, _cmd| + item_name = args[0] + resolved_config = PDK::Config.new.resolve(item_name) + # If the user wanted to know a setting but it doesn't exist, raise an error + if resolved_config.empty? && !item_name.nil? + PDK.logger.error(_("Configuration item '%{name}' does not exist") % { name: item_name }) + exit 1 + end + resolved_config.keys.sort.each { |key| puts _('%{name}=%{value}') % { name: key, value: resolved_config[key] } } + end + end +end diff --git a/lib/pdk/config.rb b/lib/pdk/config.rb index 81339f8cb..4fed3bced 100644 --- a/lib/pdk/config.rb +++ b/lib/pdk/config.rb @@ -33,6 +33,14 @@ def user end end + # Resolves *all* filtered settings from all namespaces + # + # @param filter [String] Only resolve setting names which match the filter. See PDK::Config::Namespace.be_resolved? for matching rules + # @return [Hash{String => Object}] All resolved settings for example {'user.module_defaults.author' => 'johndoe'} + def resolve(filter = nil) + user.resolve(filter) + end + def self.bolt_analytics_config file = File.expand_path('~/.puppetlabs/bolt/analytics.yaml') PDK::Config::YAML.new(file: file) diff --git a/lib/pdk/config/namespace.rb b/lib/pdk/config/namespace.rb index 45cddc968..ddd91d427 100644 --- a/lib/pdk/config/namespace.rb +++ b/lib/pdk/config/namespace.rb @@ -138,6 +138,28 @@ def to_h end end + # Resolves all filtered settings, including child namespaces, fully namespaced and filling in default values. + # + # @param filter [String] Only resolve setting names which match the filter. See #be_resolved? for matching rules + # @return [Hash{String => Object}] All resolved settings for example {'user.module_defaults.author' => 'johndoe'} + def resolve(filter = nil) + # Explicitly force values to be loaded if they have not already + # done so. This will not cause them to be persisted to disk + (@values.keys - data.keys).each { |key_name| self[key_name] } + resolved = {} + data.each do |data_name, obj| + case obj + when PDK::Config::Namespace + # Query the child namespace + resolved.merge!(obj.resolve(filter)) + else + setting_name = [name, data_name.to_s].join('.') + resolved[setting_name] = self[data_name] if be_resolved?(setting_name, filter) + end + end + resolved + end + # @return [Boolean] true if the namespace has a parent, otherwise false. def child_namespace? !parent.nil? @@ -168,6 +190,21 @@ def include_in_parent? private + # Determines whether a setting name should be resolved using the filter + # Returns true when filter is nil. + # Returns true if the filter is exactly the same name as the setting. + # Returns true if the name is a sub-key of the filter e.g. + # Given a filter of user.module_defaults, `user.module_defaults.author` will return true, but `user.analytics.disabled` will return false. + # + # @param name [String] The setting name to test. + # @param filter [String] The filter used to test on the name. + # @return [Boolean] Whether the name passes the filter. + def be_resolved?(name, filter = nil) + return true if filter.nil? # If we're not filtering, this value should always be resolved + return true if name == filter # If it's exactly the same name then it should be resolved + name.start_with?(filter + '.') # If name is a subkey of the filter then it should be resolved + end + # @abstract Subclass and override {#parse_data} to implement parsing logic # for a particular config file format. # diff --git a/spec/acceptance/config_get_spec.rb b/spec/acceptance/config_get_spec.rb new file mode 100644 index 000000000..f45678f6b --- /dev/null +++ b/spec/acceptance/config_get_spec.rb @@ -0,0 +1,40 @@ +require 'spec_helper_acceptance' +require 'fileutils' + +describe 'pdk config get' do + include_context 'with a fake TTY' + + context 'when run outside of a module' do + describe command('pdk config get') do + its(:exit_status) { is_expected.to eq 0 } + # This setting should appear in all pdk versions + its(:stdout) { is_expected.to match(%r{user\.analytics\.user-id=}) } + its(:stderr) { is_expected.to be_empty } + end + + describe command('pdk config get user.analytics.disabled') do + its(:exit_status) { is_expected.to eq 0 } + # This setting, and only, this setting should appear in output + its(:stdout) { is_expected.to eq("user.analytics.disabled=true\n") } + its(:stderr) { is_expected.to be_empty } + end + + describe command('pdk config get user.analytics') do + its(:exit_status) { is_expected.to eq 0 } + # There should be two configuration items returned + its(:stdout) { expect(is_expected.target.split("\n").count).to eq(2) } + its(:stdout) do + result = is_expected.target.split("\n").sort + expect(result[0]).to match('user.analytics.disabled=true') + expect(result[1]).to match(%r{user.analytics.user-id=.+}) + end + its(:stderr) { is_expected.to be_empty } + end + + describe command('pdk config get does.not.exist') do + its(:exit_status) { is_expected.not_to eq(0) } + its(:stdout) { is_expected.to be_empty } + its(:stderr) { is_expected.to match(%r{does\.not\.exist}) } + end + end +end diff --git a/spec/acceptance/config_spec.rb b/spec/acceptance/config_spec.rb new file mode 100644 index 000000000..d6efcd745 --- /dev/null +++ b/spec/acceptance/config_spec.rb @@ -0,0 +1,15 @@ +require 'spec_helper_acceptance' +require 'fileutils' + +describe 'pdk config' do + include_context 'with a fake TTY' + + context 'when run outside of a module' do + describe command('pdk config') do + its(:exit_status) { is_expected.to eq 0 } + # This setting should appear in all pdk versions + its(:stdout) { is_expected.to match(%r{user\.analytics\.user-id=}) } + its(:stderr) { is_expected.to be_empty } + end + end +end diff --git a/spec/unit/pdk/config/namespace_spec.rb b/spec/unit/pdk/config/namespace_spec.rb index d95cda1bd..cb76111aa 100644 --- a/spec/unit/pdk/config/namespace_spec.rb +++ b/spec/unit/pdk/config/namespace_spec.rb @@ -139,6 +139,61 @@ end end + describe '#resolve' do + let(:config_options) { { persistent_defaults: false } } + + include_context :with_a_mounted_file_with_content, 'mounted', '{"setting1": "value1"}' + + before(:each) do + # Add a value with a default value + config.value('spec_test') do + default_to { 'spec_default' } + end + # The resolver should not trigger any saves unless persistent_defaults is set to true + expect(PDK::Util::Filesystem).to receive(:write_file).never + end + + context 'with an empty filter' do + let(:filter) { nil } + + it 'resolves all settings' do + result = config.resolve(filter) + expect(result.count).to eq(2) + expect(result['config.spec_test']).to eq('spec_default') + expect(result['config.mounted.setting1']).to eq('value1') + end + end + + context 'with a setting name' do + let(:filter) { 'config.spec_test' } + + it 'resolves only one setting' do + result = config.resolve(filter) + expect(result.count).to eq(1) + expect(result['config.spec_test']).to eq('spec_default') + end + end + + context 'with a tree name' do + let(:filter) { 'config.mounted' } + + it 'resolves only settings in the tree' do + result = config.resolve(filter) + expect(result.count).to eq(1) + expect(result['config.mounted.setting1']).to eq('value1') + end + end + + context 'with a name that cannot be resolved' do + let(:filter) { 'does.not.exist' } + + it 'returns an empty hash' do + result = config.resolve(filter) + expect(result).to eq({}) + end + end + end + describe '#namespace' do before(:each) do config.namespace('test') diff --git a/spec/unit/pdk/config_spec.rb b/spec/unit/pdk/config_spec.rb index 4aa9e82cf..54cb3c53c 100644 --- a/spec/unit/pdk/config_spec.rb +++ b/spec/unit/pdk/config_spec.rb @@ -96,6 +96,11 @@ def mock_file(path, content) end end + def uuid_regex(uuid) + # Depending on the YAML or JSON generator, it may, or may not have quotes + %r{user-id: (?:#{uuid}|'#{uuid}'|\"#{uuid}\")} + end + context 'default value' do context 'when there is no pre-existing bolt configuration' do it 'generates a new UUID' do @@ -107,7 +112,7 @@ def mock_file(path, content) new_id = SecureRandom.uuid expect(SecureRandom).to receive(:uuid).and_return(new_id) # Expect that the user-id is saved to the config file - expect(PDK::Util::Filesystem).to receive(:write_file).with(File.expand_path(described_class.analytics_config_path), %r{user-id: #{new_id}}) + expect(PDK::Util::Filesystem).to receive(:write_file).with(File.expand_path(described_class.analytics_config_path), uuid_regex(new_id)) # ... and that it returns the new id expect(config.user['analytics']['user-id']).to eq(new_id) end @@ -123,7 +128,7 @@ def mock_file(path, content) it 'saves the UUID to the analytics config file' do # Expect that the user-id is saved to the config file - expect(PDK::Util::Filesystem).to receive(:write_file).with(File.expand_path(described_class.analytics_config_path), %r{user-id: #{uuid}}) + expect(PDK::Util::Filesystem).to receive(:write_file).with(File.expand_path(described_class.analytics_config_path), uuid_regex(uuid)) config.user['analytics']['user-id'] end @@ -144,7 +149,7 @@ def mock_file(path, content) new_id = SecureRandom.uuid expect(SecureRandom).to receive(:uuid).and_return(new_id) # Expect that the user-id is saved to the config file - expect(PDK::Util::Filesystem).to receive(:write_file).with(File.expand_path(described_class.analytics_config_path), %r{user-id: #{new_id}}) + expect(PDK::Util::Filesystem).to receive(:write_file).with(File.expand_path(described_class.analytics_config_path), uuid_regex(new_id)) # ... and that it returns the new id expect(config.user['analytics']['user-id']).to eq(new_id) end