pdk 1.16.0 → 1.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +24 -0
  3. data/lib/pdk.rb +25 -18
  4. data/lib/pdk/answer_file.rb +2 -93
  5. data/lib/pdk/cli.rb +1 -5
  6. data/lib/pdk/cli/config.rb +3 -1
  7. data/lib/pdk/cli/config/get.rb +3 -1
  8. data/lib/pdk/cli/convert.rb +1 -1
  9. data/lib/pdk/cli/exec/command.rb +13 -0
  10. data/lib/pdk/cli/exec_group.rb +78 -43
  11. data/lib/pdk/cli/get.rb +20 -0
  12. data/lib/pdk/cli/get/config.rb +24 -0
  13. data/lib/pdk/cli/util.rb +6 -3
  14. data/lib/pdk/cli/validate.rb +26 -44
  15. data/lib/pdk/config.rb +178 -4
  16. data/lib/pdk/config/ini_file.rb +183 -0
  17. data/lib/pdk/config/ini_file_setting.rb +39 -0
  18. data/lib/pdk/config/namespace.rb +25 -5
  19. data/lib/pdk/config/setting.rb +3 -2
  20. data/lib/pdk/context.rb +96 -0
  21. data/lib/pdk/context/control_repo.rb +60 -0
  22. data/lib/pdk/context/module.rb +28 -0
  23. data/lib/pdk/context/none.rb +22 -0
  24. data/lib/pdk/control_repo.rb +40 -0
  25. data/lib/pdk/generate/module.rb +8 -12
  26. data/lib/pdk/module/release.rb +2 -8
  27. data/lib/pdk/util.rb +35 -5
  28. data/lib/pdk/util/bundler.rb +1 -0
  29. data/lib/pdk/util/changelog_generator.rb +6 -1
  30. data/lib/pdk/util/template_uri.rb +4 -3
  31. data/lib/pdk/validate.rb +72 -25
  32. data/lib/pdk/validate/external_command_validator.rb +208 -0
  33. data/lib/pdk/validate/internal_ruby_validator.rb +100 -0
  34. data/lib/pdk/validate/invokable_validator.rb +216 -0
  35. data/lib/pdk/validate/metadata/metadata_json_lint_validator.rb +86 -0
  36. data/lib/pdk/validate/metadata/metadata_syntax_validator.rb +78 -0
  37. data/lib/pdk/validate/metadata/metadata_validator_group.rb +20 -0
  38. data/lib/pdk/validate/puppet/puppet_epp_validator.rb +133 -0
  39. data/lib/pdk/validate/puppet/puppet_lint_validator.rb +66 -0
  40. data/lib/pdk/validate/puppet/puppet_syntax_validator.rb +137 -0
  41. data/lib/pdk/validate/puppet/puppet_validator_group.rb +21 -0
  42. data/lib/pdk/validate/ruby/ruby_rubocop_validator.rb +80 -0
  43. data/lib/pdk/validate/ruby/ruby_validator_group.rb +19 -0
  44. data/lib/pdk/validate/tasks/tasks_metadata_lint_validator.rb +88 -0
  45. data/lib/pdk/validate/tasks/tasks_name_validator.rb +50 -0
  46. data/lib/pdk/validate/tasks/tasks_validator_group.rb +20 -0
  47. data/lib/pdk/validate/validator.rb +111 -0
  48. data/lib/pdk/validate/validator_group.rb +103 -0
  49. data/lib/pdk/validate/yaml/yaml_syntax_validator.rb +95 -0
  50. data/lib/pdk/validate/yaml/yaml_validator_group.rb +19 -0
  51. data/lib/pdk/version.rb +1 -1
  52. data/locales/pdk.pot +161 -125
  53. metadata +29 -17
  54. data/lib/pdk/validate/base_validator.rb +0 -215
  55. data/lib/pdk/validate/metadata/metadata_json_lint.rb +0 -82
  56. data/lib/pdk/validate/metadata/metadata_syntax.rb +0 -111
  57. data/lib/pdk/validate/metadata_validator.rb +0 -26
  58. data/lib/pdk/validate/puppet/puppet_epp.rb +0 -135
  59. data/lib/pdk/validate/puppet/puppet_lint.rb +0 -64
  60. data/lib/pdk/validate/puppet/puppet_syntax.rb +0 -135
  61. data/lib/pdk/validate/puppet_validator.rb +0 -26
  62. data/lib/pdk/validate/ruby/rubocop.rb +0 -72
  63. data/lib/pdk/validate/ruby_validator.rb +0 -26
  64. data/lib/pdk/validate/tasks/metadata_lint.rb +0 -130
  65. data/lib/pdk/validate/tasks/name.rb +0 -90
  66. data/lib/pdk/validate/tasks_validator.rb +0 -29
  67. data/lib/pdk/validate/yaml/syntax.rb +0 -125
  68. data/lib/pdk/validate/yaml_validator.rb +0 -28
@@ -0,0 +1,24 @@
1
+ module PDK::CLI
2
+ @get_config_cmd = @get_cmd.define_command do
3
+ name 'config'
4
+ usage _('config [name]')
5
+ summary _('Retrieve the configuration for <name>. If not specified, retrieve all configuration settings')
6
+
7
+ run do |_opts, args, _cmd|
8
+ item_name = args[0]
9
+ resolved_config = PDK.config.resolve(item_name)
10
+ # If the user wanted to know a setting but it doesn't exist, raise an error
11
+ if resolved_config.empty? && !item_name.nil?
12
+ PDK.logger.error(_("Configuration item '%{name}' does not exist") % { name: item_name })
13
+ exit 1
14
+ end
15
+ # If the user requested a setting and it's the only one resolved, then just output the value
16
+ if resolved_config.count == 1 && resolved_config.keys[0] == item_name
17
+ puts _('%{value}') % { value: resolved_config.values[0] }
18
+ exit 0
19
+ end
20
+ # Otherwise just output everything
21
+ resolved_config.keys.sort.each { |key| puts _('%{name}=%{value}') % { name: key, value: resolved_config[key] } }
22
+ end
23
+ end
24
+ end
@@ -129,13 +129,14 @@ module PDK
129
129
  end
130
130
  module_function :check_for_deprecated_puppet
131
131
 
132
- # @param opts [Hash] - the pdk options ot use, defaults to empty hash
132
+ # @param opts [Hash] - the pdk options to use, defaults to empty hash
133
133
  # @option opts [String] :'puppet-dev' Use the puppet development version, default to PDK_PUPPET_DEV env
134
134
  # @option opts [String] :'puppet-version' Puppet version to use, default to PDK_PUPPET_VERSION env
135
135
  # @option opts [String] :'pe-version' PE Puppet version to use, default to PDK_PE_VERSION env
136
136
  # @param logging_disabled [Boolean] - disable logging of PDK info
137
+ # @param context [PDK::Context::AbstractContext] - The context the PDK is running in
137
138
  # @return [Hash] - return hash of { gemset: <>, ruby_version: 2.x.x }
138
- def puppet_from_opts_or_env(opts, logging_disabled = false)
139
+ def puppet_from_opts_or_env(opts, logging_disabled = false, context = PDK.context)
139
140
  opts ||= {}
140
141
  use_puppet_dev = opts.fetch(:'puppet-dev', PDK::Util::Env['PDK_PUPPET_DEV'])
141
142
  desired_puppet_version = opts.fetch(:'puppet-version', PDK::Util::Env['PDK_PUPPET_VERSION'])
@@ -150,8 +151,10 @@ module PDK
150
151
  PDK::Util::PuppetVersion.find_gem_for(desired_puppet_version)
151
152
  elsif desired_pe_version
152
153
  PDK::Util::PuppetVersion.from_pe_version(desired_pe_version)
153
- else
154
+ elsif context.is_a?(PDK::Context::Module)
154
155
  PDK::Util::PuppetVersion.from_module_metadata || PDK::Util::PuppetVersion.latest_available
156
+ else
157
+ PDK::Util::PuppetVersion.latest_available
155
158
  end
156
159
  rescue ArgumentError => e
157
160
  raise PDK::CLI::ExitWithError, e.message
@@ -19,6 +19,9 @@ module PDK::CLI
19
19
  flag nil, :parallel, _('Run validations in parallel.')
20
20
 
21
21
  run do |opts, args, _cmd|
22
+ # Write the context information to the debug log
23
+ PDK.context.to_debug_log
24
+
22
25
  if args == ['help']
23
26
  PDK::CLI.run(['validate', '--help'])
24
27
  exit 0
@@ -26,42 +29,42 @@ module PDK::CLI
26
29
 
27
30
  require 'pdk/validate'
28
31
 
29
- validator_names = PDK::Validate.validators.map { |v| v.name }
30
- validators = PDK::Validate.validators
31
- targets = []
32
-
33
32
  if opts[:list]
34
33
  PDK::CLI::Util.analytics_screen_view('validate', opts)
35
- PDK.logger.info(_('Available validators: %{validator_names}') % { validator_names: validator_names.join(', ') })
34
+ PDK.logger.info(_('Available validators: %{validator_names}') % { validator_names: PDK::Validate.validator_names.join(', ') })
36
35
  exit 0
37
36
  end
38
37
 
39
38
  PDK::CLI::Util.validate_puppet_version_opts(opts)
39
+ unless PDK.feature_flag?('controlrepo') || PDK.context.is_a?(PDK::Context::Module)
40
+ raise PDK::CLI::ExitWithError.new(_('Code validation can only be run from inside a valid module directory'), log_level: :error)
41
+ end
40
42
 
41
- PDK::CLI::Util.ensure_in_module!(
42
- message: _('Code validation can only be run from inside a valid module directory'),
43
- log_level: :info,
44
- )
43
+ PDK::CLI::Util.module_version_check if PDK.context.is_a?(PDK::Context::Module)
45
44
 
46
- PDK::CLI::Util.module_version_check
45
+ # Set the ruby version we're going to use early. Must be set before the validators are created.
46
+ # Note that this is a bit of code-smell and should be fixed
47
+ puppet_env = PDK::CLI::Util.puppet_from_opts_or_env(opts)
48
+ PDK::Util::RubyVersion.use(puppet_env[:ruby_version])
47
49
 
50
+ targets = []
51
+ validators_to_run = nil
48
52
  if args[0]
49
53
  # This may be a single validator, a list of validators, or a target.
50
54
  if Util::OptionValidator.comma_separated_list?(args[0])
51
55
  # This is a comma separated list. Treat each item as a validator.
52
-
53
56
  vals = Util::OptionNormalizer.comma_separated_list_to_array(args[0])
54
- validators = PDK::Validate.validators.select { |v| vals.include?(v.name) }
57
+ validators_to_run = PDK::Validate.validator_names.select { |name| vals.include?(name) }
55
58
 
56
- invalid = vals.reject { |v| validator_names.include?(v) }
57
- invalid.each do |v|
58
- PDK.logger.warn(_("Unknown validator '%{v}'. Available validators: %{validators}.") % { v: v, validators: validator_names.join(', ') })
59
+ vals.reject { |v| PDK::Validate.validator_names.include?(v) }
60
+ .each do |v|
61
+ PDK.logger.warn(_("Unknown validator '%{v}'. Available validators: %{validators}.") % { v: v, validators: PDK::Validate.validator_names.join(', ') })
59
62
  end
60
63
  else
61
64
  # This is a single item. Check if it's a known validator, or otherwise treat it as a target.
62
- val = PDK::Validate.validators.find { |v| args[0] == v.name }
65
+ val = PDK::Validate.validator_names.find { |name| args[0] == name }
63
66
  if val
64
- validators = [val]
67
+ validators_to_run = [val]
65
68
  else
66
69
  targets = [args[0]]
67
70
  # We now know that no validators were passed, so let the user know we're using all of them by default.
@@ -71,11 +74,12 @@ module PDK::CLI
71
74
  else
72
75
  PDK.logger.info(_('Running all available validators...'))
73
76
  end
77
+ validators_to_run = PDK::Validate.validator_names if validators_to_run.nil?
74
78
 
75
- if validators == PDK::Validate.validators
79
+ if validators_to_run.sort == PDK::Validate.validator_names.sort
76
80
  PDK::CLI::Util.analytics_screen_view('validate', opts)
77
81
  else
78
- PDK::CLI::Util.analytics_screen_view(['validate', validators.map(&:name).sort].flatten.join('_'), opts)
82
+ PDK::CLI::Util.analytics_screen_view(['validate', validators_to_run.sort].flatten.join('_'), opts)
79
83
  end
80
84
 
81
85
  # Subsequent arguments are targets.
@@ -93,36 +97,14 @@ module PDK::CLI
93
97
 
94
98
  options = targets.empty? ? {} : { targets: targets }
95
99
  options[:auto_correct] = true if opts[:'auto-correct']
96
-
97
- # Ensure that the bundled gems are up to date and correct Ruby is activated before running any validations.
98
- puppet_env = PDK::CLI::Util.puppet_from_opts_or_env(opts)
99
- PDK::Util::RubyVersion.use(puppet_env[:ruby_version])
100
-
101
100
  options.merge!(puppet_env[:gemset])
102
101
 
102
+ # Ensure that the bundled gems are up to date and correct Ruby is activated before running any validations.
103
+ # Note that if no Gemfile exists, then ensure_bundle! will log a debug message and exit gracefully
103
104
  require 'pdk/util/bundler'
104
-
105
105
  PDK::Util::Bundler.ensure_bundle!(puppet_env[:gemset])
106
106
 
107
- exit_code = 0
108
- if opts[:parallel]
109
- require 'pdk/cli/exec_group'
110
-
111
- exec_group = PDK::CLI::ExecGroup.new(_('Validating module using %{num_of_threads} threads' % { num_of_threads: validators.count }), opts)
112
-
113
- validators.each do |validator|
114
- exec_group.register do
115
- validator.invoke(report, options.merge(exec_group: exec_group))
116
- end
117
- end
118
-
119
- exit_code = exec_group.exit_code
120
- else
121
- validators.each do |validator|
122
- validator_exit_code = validator.invoke(report, options.dup)
123
- exit_code = validator_exit_code if validator_exit_code != 0
124
- end
125
- end
107
+ exit_code, report = PDK::Validate.invoke_validators_by_name(PDK.context, validators_to_run, opts.fetch(:parallel, false), options)
126
108
 
127
109
  report_formats.each do |format|
128
110
  report.send(format[:method], format[:target])
@@ -2,6 +2,8 @@ require 'pdk'
2
2
 
3
3
  module PDK
4
4
  class Config
5
+ autoload :IniFile, 'pdk/config/ini_file'
6
+ autoload :IniFileSetting, 'pdk/config/ini_file_setting'
5
7
  autoload :JSON, 'pdk/config/json'
6
8
  autoload :JSONSchemaNamespace, 'pdk/config/json_schema_namespace'
7
9
  autoload :JSONSchemaSetting, 'pdk/config/json_schema_setting'
@@ -11,16 +13,58 @@ module PDK
11
13
  autoload :Validator, 'pdk/config/validator'
12
14
  autoload :YAML, 'pdk/config/yaml'
13
15
 
16
+ # Create a new instance of the PDK Configuration
17
+ # @param options [Hash[String => Object]] Optional hash to override configuration options
18
+ # @option options [String] 'system.path' Path to the system PDK configuration file
19
+ # @option options [String] 'system.module_defaults.path' Path to the system module answers PDK configuration file
20
+ # @option options [String] 'user.path' Path to the user PDK configuration file
21
+ # @option options [String] 'user.module_defaults.path' Path to the user module answers PDK configuration file
22
+ # @option options [String] 'user.analytics.path' Path to the user analytics PDK configuration file
23
+ # @option options [PDK::Context::AbstractContext] 'context' The context that the configuration should be created in
24
+ def initialize(options = nil)
25
+ options = {} if options.nil?
26
+ @config_options = {
27
+ 'system.path' => PDK::Config.system_config_path,
28
+ 'system.module_defaults.path' => PDK::Config.system_answers_path,
29
+ 'user.path' => PDK::Config.user_config_path,
30
+ 'user.module_defaults.path' => PDK::AnswerFile.default_answer_file_path,
31
+ 'user.analytics.path' => PDK::Config.analytics_config_path,
32
+ 'context' => PDK.context,
33
+ }.merge(options)
34
+ end
35
+
36
+ # The user configuration settings.
37
+ # @deprecated This method is only provided as a courtesy until the `pdk set config` CLI and associated changes in this class, are completed.
38
+ # Any read-only operations should be using `.get` or `.get_within_scopes`
39
+ # @return [PDK::Config::Namespace]
14
40
  def user
15
- @user ||= PDK::Config::JSON.new('user', file: PDK::Config.user_config_path) do
16
- mount :module_defaults, PDK::Config::JSON.new(file: PDK.answers.answer_file_path)
41
+ user_config
42
+ end
43
+
44
+ # The system level configuration settings.
45
+ # @return [PDK::Config::Namespace]
46
+ # @api private
47
+ def system_config
48
+ local_options = @config_options
49
+ @system ||= PDK::Config::JSON.new('system', file: local_options['system.path']) do
50
+ mount :module_defaults, PDK::Config::JSON.new(file: local_options['system.module_defaults.path'])
51
+ end
52
+ end
53
+
54
+ # The user level configuration settings.
55
+ # @return [PDK::Config::Namespace]
56
+ # @api private
57
+ def user_config
58
+ local_options = @config_options
59
+ @user ||= PDK::Config::JSON.new('user', file: local_options['user.path']) do
60
+ mount :module_defaults, PDK::Config::JSON.new(file: local_options['user.module_defaults.path'])
17
61
 
18
62
  # Due to the json-schema gem having issues with Windows based paths, and only supporting Draft 05 (or less) do
19
63
  # not use JSON validation yet. Once PDK drops support for EOL rubies, we will be able to use the json_schemer gem
20
64
  # Which has much more modern support
21
65
  # Reference - https://github.com/puppetlabs/pdk/pull/777
22
66
  # Reference - https://tickets.puppetlabs.com/browse/PDK-1526
23
- mount :analytics, PDK::Config::YAML.new(file: PDK::Config.analytics_config_path, persistent_defaults: true) do
67
+ mount :analytics, PDK::Config::YAML.new(file: local_options['user.analytics.path'], persistent_defaults: true) do
24
68
  setting :disabled do
25
69
  validate PDK::Config::Validator.boolean
26
70
  default_to { PDK::Config.bolt_analytics_config.fetch('disabled', true) }
@@ -35,6 +79,29 @@ module PDK
35
79
  end
36
80
  end
37
81
  end
82
+
83
+ # Display the feature flags
84
+ mount :pdk_feature_flags, PDK::Config::Namespace.new('pdk_feature_flags') do
85
+ setting 'available' do
86
+ default_to { PDK.available_feature_flags }
87
+ end
88
+
89
+ setting 'requested' do
90
+ default_to { PDK.requested_feature_flags }
91
+ end
92
+ end
93
+ end
94
+ end
95
+
96
+ # The project level configuration settings.
97
+ # @return [PDK::Config::Namespace]
98
+ # @api private
99
+ def project_config
100
+ context = @config_options['context']
101
+ @project ||= PDK::Config::Namespace.new('project') do
102
+ if context.is_a?(PDK::Context::ControlRepo)
103
+ mount :environment, PDK::ControlRepo.environment_conf_as_config(File.join(context.root_path, 'environment.conf'))
104
+ end
38
105
  end
39
106
  end
40
107
 
@@ -43,7 +110,63 @@ module PDK
43
110
  # @param filter [String] Only resolve setting names which match the filter. See PDK::Config::Namespace.be_resolved? for matching rules
44
111
  # @return [Hash{String => Object}] All resolved settings for example {'user.module_defaults.author' => 'johndoe'}
45
112
  def resolve(filter = nil)
46
- user.resolve(filter)
113
+ all_scopes.values.reverse.reduce({}) do |result, method_name|
114
+ result.merge(send(method_name).resolve(filter))
115
+ end
116
+ end
117
+
118
+ # Returns a configuration setting by name. This name can either be a String, Array or parameters e.g. These are equivalent
119
+ # - PDK.config.get('user.a.b.c')
120
+ # - PDK.config.get(['user', 'a', 'b', 'c'])
121
+ # - PDK.config.get('user', 'a', 'b', 'c')
122
+ # @return [PDK::Config::Namespace, Object, nil] The value of the configuration setting. Returns nil if it does no exist
123
+ def get(root, *keys)
124
+ return nil if root.nil? || root.empty?
125
+
126
+ if keys.empty?
127
+ if root.is_a?(Array)
128
+ name = root
129
+ elsif root.is_a?(String)
130
+ name = split_key_string(root)
131
+ else
132
+ return nil
133
+ end
134
+ else
135
+ name = [root].concat(keys)
136
+ end
137
+
138
+ get_within_scopes(name[1..-1], [name[0]])
139
+ end
140
+
141
+ # Returns a configuration setting by name, using scope precedence rules. If no scopes are passed, then all scopes are queried using the default precedence rules
142
+ # @setting_name [String, Array[String]] The setting name to retrieve without the leading scope name e.g. Use 'setting' instead of 'system.setting'
143
+ # @scopes [Nil, Array[String]] The list of scopes, in order, to query in turn for the setting_name. Invalid or missing scopes are ignored.
144
+ # @return [PDK::Config::Namespace, Object, nil] The value of the configuration setting. Returns nil if it does no exist
145
+ def get_within_scopes(setting_name, scopes = nil)
146
+ raise ArgumentError, _('Expected an Array but got \'%{klass}\' for scopes') % { klass: scopes.class } unless scopes.nil? || scopes.is_a?(Array)
147
+ raise ArgumentError, _('Expected an Array or String but got \'%{klass}\' for setting_name') % { klass: setting_name.class } unless setting_name.is_a?(Array) || setting_name.is_a?(String)
148
+
149
+ setting_arr = setting_name.is_a?(String) ? split_key_string(setting_name) : setting_name
150
+ all_scope_names = all_scopes.keys
151
+
152
+ # Use only valid scope names
153
+ scopes = scopes.nil? ? all_scope_names : scopes & all_scope_names
154
+
155
+ scopes.each do |scope_name|
156
+ value = traverse_object(send(all_scopes[scope_name]), *setting_arr)
157
+ return value unless value.nil?
158
+ end
159
+ nil
160
+ end
161
+
162
+ # Yields a configuration setting value by name, using scope precedence rules. If no scopes are passed, then all scopes are queried using the default precedence rules
163
+ # @setting_name [String, Array[String]] The setting name to retrieve without the leading scope name e.g. Use 'setting' instead of 'system.setting'
164
+ # @scopes [Nil, Array[String]] The list of scopes, in order, to query in turn for the setting_name. Invalid or missing scopes are ignored.
165
+ # @yield [PDK::Config::Namespace, Object] The value of the configuration setting. Does not yield if the setting does not exist or is nil
166
+ def with_scoped_value(setting_name, scopes = nil)
167
+ raise ArgumentError, _('must be passed a block') unless block_given?
168
+ value = get_within_scopes(setting_name, scopes)
169
+ yield value unless value.nil?
47
170
  end
48
171
 
49
172
  def self.bolt_analytics_config
@@ -65,6 +188,14 @@ module PDK
65
188
  File.join(PDK::Util.configdir, 'user_config.json')
66
189
  end
67
190
 
191
+ def self.system_config_path
192
+ File.join(PDK::Util.system_configdir, 'system_config.json')
193
+ end
194
+
195
+ def self.system_answers_path
196
+ File.join(PDK::Util.system_configdir, 'answers.json')
197
+ end
198
+
68
199
  def self.json_schemas_path
69
200
  File.join(__dir__, 'config')
70
201
  end
@@ -123,5 +254,48 @@ module PDK
123
254
 
124
255
  PDK.logger.info(text: post_message, wrap: true)
125
256
  end
257
+
258
+ private
259
+
260
+ #:nocov: This is a private method and is tested elsewhere
261
+ def traverse_object(object, *names)
262
+ return nil if object.nil? || !object.respond_to?(:[])
263
+ return nil if names.nil? || names.empty?
264
+
265
+ name = names.shift
266
+ value = object[name]
267
+ if names.empty?
268
+ return value if value.is_a?(PDK::Config::Namespace)
269
+ # Duplicate arrays and hashes so that they are isolated from changes being made
270
+ (value.is_a?(Hash) || value.is_a?(Array)) ? value.dup : value
271
+ else
272
+ traverse_object(value, *names)
273
+ end
274
+ end
275
+ #:nocov:
276
+
277
+ #:nocov: This is a private method and is tested elsewhere
278
+ # Takes a string representation of a setting and splits into its constituent setting parts e.g.
279
+ # 'user.a.b.c' becomes ['user', 'a', 'b', 'c']
280
+ # @return [Array[String]] The string split into each setting name as an array
281
+ def split_key_string(key)
282
+ raise ArgumentError, _('Expected a String but got \'%{klass}\'') % { klass: key.class } unless key.is_a?(String)
283
+ key.split('.')
284
+ end
285
+ #:nocov:
286
+
287
+ #:nocov:
288
+ # Returns all known scope names and their associated method name to call, to query the scope
289
+ # Note - Order is important. This dictates the resolution precedence order (topmost is processed first)
290
+ # @return [Hash[String, Symbol]] A hash of the scope name then method name to call to query the scope (as a Symbol)
291
+ def all_scopes
292
+ # Note - Order is important. This dictates the resolution precedence order (topmost is processed first)
293
+ {
294
+ 'project' => :project_config,
295
+ 'user' => :user_config,
296
+ 'system' => :system_config,
297
+ }.freeze
298
+ end
299
+ #:nocov:
126
300
  end
127
301
  end
@@ -0,0 +1,183 @@
1
+ require 'pdk'
2
+
3
+ module PDK
4
+ class Config
5
+ # Represents a configuration file using the INI file format
6
+ class IniFile < Namespace
7
+ # Ini Files have very strict valdiation rules which are set in the IniFileSetting class
8
+ # @see PDK::Config::Namespace.default_setting_class
9
+ def default_setting_class
10
+ PDK::Config::IniFileSetting
11
+ end
12
+
13
+ # Parses an IniFile document.
14
+ #
15
+ # @see PDK::Config::Namespace.parse_file
16
+ def parse_file(filename)
17
+ raise unless block_given?
18
+ data = load_data(filename)
19
+ return if data.nil? || data.empty?
20
+
21
+ ini_file = IniFileImpl.parse(data)
22
+ ini_file.to_hash.each do |name, value|
23
+ begin
24
+ new_setting = PDK::Config::IniFileSetting.new(name, self, value)
25
+ rescue StandardError
26
+ # We just ignore invalid initial settings
27
+ new_setting = PDK::Config::IniFileSetting.new(name, self, nil)
28
+ end
29
+
30
+ yield name, new_setting
31
+ end
32
+ end
33
+
34
+ # Serializes object data into an INI file string.
35
+ #
36
+ # @see PDK::Config::Namespace.serialize_data
37
+ def serialize_data(data)
38
+ default_lines = ''
39
+ lines = ''
40
+ data.each do |name, value|
41
+ next if value.nil?
42
+ if value.is_a?(Hash)
43
+ # Hashes are an INI section
44
+ lines += "\n[#{name}]\n"
45
+ value.each do |child_name, child_value|
46
+ next if child_value.nil?
47
+ lines += "#{child_name} = #{munge_serialized_value(child_value)}\n"
48
+ end
49
+ else
50
+ default_lines += "#{name} = #{munge_serialized_value(value)}\n"
51
+ end
52
+ end
53
+
54
+ default_lines + lines
55
+ end
56
+
57
+ private
58
+
59
+ def munge_serialized_value(value)
60
+ value = value.to_s unless value.is_a?(String)
61
+ # Add enclosing quotes if there's a space in the value
62
+ value = '"' + value + '"' if value.include?(' ')
63
+ value
64
+ end
65
+
66
+ # Adapted from https://raw.githubusercontent.com/puppetlabs/puppet/6c257fc7827989c2af2901f974666f0f23611153/lib/puppet/settings/ini_file.rb
67
+ # rubocop:disable Style/RegexpLiteral
68
+ # rubocop:disable Style/PerlBackrefs
69
+ # rubocop:disable Style/RedundantSelf
70
+ # rubocop:disable Style/StringLiterals
71
+ class IniFileImpl
72
+ DEFAULT_SECTION_NAME = 'default_section_name'.freeze
73
+
74
+ def self.parse(config_fh)
75
+ config = new([DefaultSection.new])
76
+ config_fh.each_line do |line|
77
+ case line.chomp
78
+ when /^(\s*)\[([[:word:]]+)\](\s*)$/
79
+ config.append(SectionLine.new($1, $2, $3))
80
+ when /^(\s*)([[:word:]]+)(\s*=\s*)(.*?)(\s*)$/
81
+ config.append(SettingLine.new($1, $2, $3, $4, $5))
82
+ else
83
+ config.append(Line.new(line))
84
+ end
85
+ end
86
+
87
+ config
88
+ end
89
+
90
+ def initialize(lines = [])
91
+ @lines = lines
92
+ end
93
+
94
+ def to_hash
95
+ result = {}
96
+
97
+ current_section_name = nil
98
+ @lines.each do |line|
99
+ if line.instance_of?(SectionLine)
100
+ current_section_name = line.name
101
+ result[current_section_name] = {}
102
+ elsif line.instance_of?(SettingLine)
103
+ if current_section_name.nil?
104
+ result[line.name] = munge_value(line.value)
105
+ else
106
+ result[current_section_name][line.name] = munge_value(line.value)
107
+ end
108
+ end
109
+ end
110
+
111
+ result
112
+ end
113
+
114
+ def munge_value(value)
115
+ value = value.to_s unless value.is_a?(String)
116
+ # Strip enclosing quotes
117
+ value = value.slice(1...-1) if value.start_with?('"') && value.end_with?('"')
118
+ value
119
+ end
120
+
121
+ def append(line)
122
+ line.previous = @lines.last
123
+ @lines << line
124
+ end
125
+
126
+ module LineNumber
127
+ attr_accessor :previous
128
+
129
+ def line_number
130
+ line = 0
131
+ previous_line = previous
132
+ while previous_line
133
+ line += 1
134
+ previous_line = previous_line.previous
135
+ end
136
+ line
137
+ end
138
+ end
139
+
140
+ Line = Struct.new(:text) do
141
+ include LineNumber
142
+
143
+ def to_s
144
+ text
145
+ end
146
+ end
147
+
148
+ SettingLine = Struct.new(:prefix, :name, :infix, :value, :suffix) do
149
+ include LineNumber
150
+
151
+ def to_s
152
+ "#{prefix}#{name}#{infix}#{value}#{suffix}"
153
+ end
154
+
155
+ def ==(other)
156
+ super(other) && self.line_number == other.line_number
157
+ end
158
+ end
159
+
160
+ SectionLine = Struct.new(:prefix, :name, :suffix) do
161
+ include LineNumber
162
+
163
+ def to_s
164
+ "#{prefix}[#{name}]#{suffix}"
165
+ end
166
+ end
167
+
168
+ class DefaultSection < SectionLine
169
+ attr_accessor :write_sectionline
170
+
171
+ def initialize
172
+ @write_sectionline = false
173
+ super("", DEFAULT_SECTION_NAME, "")
174
+ end
175
+ end
176
+ end
177
+ # rubocop:enable Style/StringLiterals
178
+ # rubocop:enable Style/RedundantSelf
179
+ # rubocop:enable Style/PerlBackrefs
180
+ # rubocop:enable Style/RegexpLiteral
181
+ end
182
+ end
183
+ end