pdk 2.3.0 → 2.4.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 (153) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +1329 -1321
  3. data/LICENSE +201 -201
  4. data/README.md +163 -163
  5. data/exe/pdk +10 -10
  6. data/lib/pdk/analytics/client/google_analytics.rb +143 -143
  7. data/lib/pdk/analytics/client/noop.rb +25 -25
  8. data/lib/pdk/analytics/util.rb +19 -19
  9. data/lib/pdk/analytics.rb +30 -30
  10. data/lib/pdk/answer_file.rb +12 -12
  11. data/lib/pdk/bolt.rb +19 -19
  12. data/lib/pdk/cli/build.rb +82 -82
  13. data/lib/pdk/cli/bundle.rb +48 -48
  14. data/lib/pdk/cli/config/get.rb +26 -26
  15. data/lib/pdk/cli/config.rb +22 -22
  16. data/lib/pdk/cli/console.rb +148 -148
  17. data/lib/pdk/cli/convert.rb +52 -52
  18. data/lib/pdk/cli/env.rb +52 -52
  19. data/lib/pdk/cli/errors.rb +25 -25
  20. data/lib/pdk/cli/exec/command.rb +293 -293
  21. data/lib/pdk/cli/exec/interactive_command.rb +114 -114
  22. data/lib/pdk/cli/exec.rb +84 -84
  23. data/lib/pdk/cli/exec_group.rb +104 -104
  24. data/lib/pdk/cli/get/config.rb +24 -24
  25. data/lib/pdk/cli/get.rb +20 -20
  26. data/lib/pdk/cli/module/build.rb +12 -12
  27. data/lib/pdk/cli/module/generate.rb +47 -47
  28. data/lib/pdk/cli/module.rb +14 -14
  29. data/lib/pdk/cli/new/class.rb +32 -32
  30. data/lib/pdk/cli/new/defined_type.rb +32 -32
  31. data/lib/pdk/cli/new/fact.rb +29 -29
  32. data/lib/pdk/cli/new/function.rb +29 -29
  33. data/lib/pdk/cli/new/module.rb +53 -53
  34. data/lib/pdk/cli/new/provider.rb +29 -29
  35. data/lib/pdk/cli/new/task.rb +34 -34
  36. data/lib/pdk/cli/new/test.rb +52 -52
  37. data/lib/pdk/cli/new/transport.rb +27 -27
  38. data/lib/pdk/cli/new.rb +21 -21
  39. data/lib/pdk/cli/release/prep.rb +39 -39
  40. data/lib/pdk/cli/release/publish.rb +50 -50
  41. data/lib/pdk/cli/release.rb +194 -194
  42. data/lib/pdk/cli/remove/config.rb +80 -80
  43. data/lib/pdk/cli/remove.rb +20 -20
  44. data/lib/pdk/cli/set/config.rb +119 -119
  45. data/lib/pdk/cli/set.rb +20 -20
  46. data/lib/pdk/cli/test/unit.rb +90 -90
  47. data/lib/pdk/cli/test.rb +11 -11
  48. data/lib/pdk/cli/update.rb +64 -64
  49. data/lib/pdk/cli/util/command_redirector.rb +27 -27
  50. data/lib/pdk/cli/util/interview.rb +72 -72
  51. data/lib/pdk/cli/util/option_normalizer.rb +55 -55
  52. data/lib/pdk/cli/util/option_validator.rb +68 -68
  53. data/lib/pdk/cli/util/spinner.rb +13 -13
  54. data/lib/pdk/cli/util/update_manager_printer.rb +82 -82
  55. data/lib/pdk/cli/util.rb +305 -305
  56. data/lib/pdk/cli/validate.rb +116 -116
  57. data/lib/pdk/cli.rb +175 -175
  58. data/lib/pdk/config/analytics_schema.json +26 -26
  59. data/lib/pdk/config/errors.rb +5 -5
  60. data/lib/pdk/config/ini_file.rb +183 -183
  61. data/lib/pdk/config/ini_file_setting.rb +39 -39
  62. data/lib/pdk/config/json.rb +34 -34
  63. data/lib/pdk/config/json_schema_namespace.rb +142 -142
  64. data/lib/pdk/config/json_schema_setting.rb +53 -53
  65. data/lib/pdk/config/json_with_schema.rb +49 -49
  66. data/lib/pdk/config/namespace.rb +354 -354
  67. data/lib/pdk/config/setting.rb +135 -135
  68. data/lib/pdk/config/validator.rb +31 -31
  69. data/lib/pdk/config/yaml.rb +46 -46
  70. data/lib/pdk/config/yaml_with_schema.rb +59 -59
  71. data/lib/pdk/config.rb +390 -390
  72. data/lib/pdk/context/control_repo.rb +60 -60
  73. data/lib/pdk/context/module.rb +28 -28
  74. data/lib/pdk/context/none.rb +22 -22
  75. data/lib/pdk/context.rb +99 -99
  76. data/lib/pdk/control_repo.rb +90 -90
  77. data/lib/pdk/generate/defined_type.rb +43 -43
  78. data/lib/pdk/generate/fact.rb +25 -25
  79. data/lib/pdk/generate/function.rb +48 -48
  80. data/lib/pdk/generate/module.rb +352 -352
  81. data/lib/pdk/generate/provider.rb +28 -28
  82. data/lib/pdk/generate/puppet_class.rb +43 -43
  83. data/lib/pdk/generate/puppet_object.rb +232 -232
  84. data/lib/pdk/generate/task.rb +68 -68
  85. data/lib/pdk/generate/transport.rb +33 -33
  86. data/lib/pdk/generate.rb +24 -24
  87. data/lib/pdk/i18n.rb +4 -4
  88. data/lib/pdk/logger.rb +45 -45
  89. data/lib/pdk/module/build.rb +322 -322
  90. data/lib/pdk/module/convert.rb +296 -296
  91. data/lib/pdk/module/metadata.rb +202 -202
  92. data/lib/pdk/module/release.rb +260 -260
  93. data/lib/pdk/module/update.rb +131 -131
  94. data/lib/pdk/module/update_manager.rb +227 -227
  95. data/lib/pdk/module.rb +30 -30
  96. data/lib/pdk/report/event.rb +370 -370
  97. data/lib/pdk/report.rb +121 -121
  98. data/lib/pdk/template/fetcher/git.rb +85 -85
  99. data/lib/pdk/template/fetcher/local.rb +28 -28
  100. data/lib/pdk/template/fetcher.rb +98 -98
  101. data/lib/pdk/template/renderer/v1/legacy_template_dir.rb +116 -116
  102. data/lib/pdk/template/renderer/v1/renderer.rb +132 -132
  103. data/lib/pdk/template/renderer/v1/template_file.rb +102 -102
  104. data/lib/pdk/template/renderer/v1.rb +25 -25
  105. data/lib/pdk/template/renderer.rb +96 -96
  106. data/lib/pdk/template/template_dir.rb +67 -67
  107. data/lib/pdk/template.rb +59 -59
  108. data/lib/pdk/tests/unit.rb +252 -252
  109. data/lib/pdk/util/bundler.rb +259 -259
  110. data/lib/pdk/util/changelog_generator.rb +137 -137
  111. data/lib/pdk/util/env.rb +47 -47
  112. data/lib/pdk/util/filesystem.rb +138 -138
  113. data/lib/pdk/util/git.rb +179 -179
  114. data/lib/pdk/util/json_finder.rb +85 -85
  115. data/lib/pdk/util/puppet_strings.rb +125 -125
  116. data/lib/pdk/util/puppet_version.rb +266 -266
  117. data/lib/pdk/util/ruby_version.rb +179 -179
  118. data/lib/pdk/util/template_uri.rb +295 -295
  119. data/lib/pdk/util/vendored_file.rb +93 -93
  120. data/lib/pdk/util/version.rb +43 -43
  121. data/lib/pdk/util/windows/api_types.rb +82 -82
  122. data/lib/pdk/util/windows/file.rb +36 -36
  123. data/lib/pdk/util/windows/process.rb +79 -79
  124. data/lib/pdk/util/windows/string.rb +16 -16
  125. data/lib/pdk/util/windows.rb +15 -15
  126. data/lib/pdk/util.rb +278 -277
  127. data/lib/pdk/validate/control_repo/control_repo_validator_group.rb +23 -23
  128. data/lib/pdk/validate/control_repo/environment_conf_validator.rb +98 -98
  129. data/lib/pdk/validate/external_command_validator.rb +208 -208
  130. data/lib/pdk/validate/internal_ruby_validator.rb +100 -100
  131. data/lib/pdk/validate/invokable_validator.rb +228 -228
  132. data/lib/pdk/validate/metadata/metadata_json_lint_validator.rb +86 -86
  133. data/lib/pdk/validate/metadata/metadata_syntax_validator.rb +78 -78
  134. data/lib/pdk/validate/metadata/metadata_validator_group.rb +20 -20
  135. data/lib/pdk/validate/puppet/puppet_epp_validator.rb +133 -133
  136. data/lib/pdk/validate/puppet/puppet_lint_validator.rb +66 -66
  137. data/lib/pdk/validate/puppet/puppet_syntax_validator.rb +137 -137
  138. data/lib/pdk/validate/puppet/puppet_validator_group.rb +21 -21
  139. data/lib/pdk/validate/ruby/ruby_rubocop_validator.rb +80 -80
  140. data/lib/pdk/validate/ruby/ruby_validator_group.rb +19 -19
  141. data/lib/pdk/validate/tasks/tasks_metadata_lint_validator.rb +88 -88
  142. data/lib/pdk/validate/tasks/tasks_name_validator.rb +50 -50
  143. data/lib/pdk/validate/tasks/tasks_validator_group.rb +20 -20
  144. data/lib/pdk/validate/validator.rb +118 -118
  145. data/lib/pdk/validate/validator_group.rb +104 -104
  146. data/lib/pdk/validate/yaml/yaml_syntax_validator.rb +95 -95
  147. data/lib/pdk/validate/yaml/yaml_validator_group.rb +19 -19
  148. data/lib/pdk/validate.rb +94 -94
  149. data/lib/pdk/version.rb +4 -4
  150. data/lib/pdk.rb +76 -76
  151. data/locales/config.yaml +21 -21
  152. data/locales/pdk.pot +2094 -2094
  153. metadata +5 -6
data/lib/pdk/config.rb CHANGED
@@ -1,390 +1,390 @@
1
- require 'pdk'
2
-
3
- module PDK
4
- class Config
5
- autoload :IniFile, 'pdk/config/ini_file'
6
- autoload :IniFileSetting, 'pdk/config/ini_file_setting'
7
- autoload :JSON, 'pdk/config/json'
8
- autoload :JSONSchemaNamespace, 'pdk/config/json_schema_namespace'
9
- autoload :JSONSchemaSetting, 'pdk/config/json_schema_setting'
10
- autoload :LoadError, 'pdk/config/errors'
11
- autoload :Namespace, 'pdk/config/namespace'
12
- autoload :Setting, 'pdk/config/setting'
13
- autoload :Validator, 'pdk/config/validator'
14
- autoload :YAML, 'pdk/config/yaml'
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 system level configuration settings.
37
- # @return [PDK::Config::Namespace]
38
- # @api private
39
- def system_config
40
- local_options = @config_options
41
- @system_config ||= PDK::Config::JSON.new('system', file: local_options['system.path']) do
42
- mount :module_defaults, PDK::Config::JSON.new(file: local_options['system.module_defaults.path'])
43
- end
44
- end
45
-
46
- # The user level configuration settings.
47
- # @return [PDK::Config::Namespace]
48
- # @api private
49
- def user_config
50
- local_options = @config_options
51
- @user_config ||= PDK::Config::JSON.new('user', file: local_options['user.path']) do
52
- mount :module_defaults, PDK::Config::JSON.new(file: local_options['user.module_defaults.path'])
53
-
54
- # Due to the json-schema gem having issues with Windows based paths, and only supporting Draft 05 (or less) do
55
- # not use JSON validation yet. Once PDK drops support for EOL rubies, we will be able to use the json_schemer gem
56
- # Which has much more modern support
57
- # Reference - https://github.com/puppetlabs/pdk/pull/777
58
- # Reference - https://tickets.puppetlabs.com/browse/PDK-1526
59
- mount :analytics, PDK::Config::YAML.new(file: local_options['user.analytics.path'], persistent_defaults: true) do
60
- setting :disabled do
61
- validate PDK::Config::Validator.boolean
62
- default_to { PDK::Config.bolt_analytics_config.fetch('disabled', true) }
63
- end
64
-
65
- setting 'user-id' do
66
- validate PDK::Config::Validator.uuid
67
- default_to do
68
- require 'securerandom'
69
-
70
- PDK::Config.bolt_analytics_config.fetch('user-id', SecureRandom.uuid)
71
- end
72
- end
73
- end
74
-
75
- # Display the feature flags
76
- mount :pdk_feature_flags, PDK::Config::Namespace.new('pdk_feature_flags') do
77
- setting 'available' do
78
- default_to { PDK.available_feature_flags }
79
- end
80
-
81
- setting 'requested' do
82
- default_to { PDK.requested_feature_flags }
83
- end
84
- end
85
- end
86
- end
87
-
88
- # The project level configuration settings.
89
- # @return [PDK::Config::Namespace]
90
- # @api private
91
- def project_config
92
- context = @config_options['context']
93
- @project ||= PDK::Config::Namespace.new('project') do
94
- if context.is_a?(PDK::Context::ControlRepo)
95
- mount :environment, PDK::ControlRepo.environment_conf_as_config(File.join(context.root_path, 'environment.conf'))
96
- end
97
-
98
- mount :validate, PDK::Config::YAML.new('validate', file: File.join(context.root_path, 'pdk.yaml'), persistent_defaults: true) do
99
- setting 'ignore' do
100
- default_to { [] }
101
- end
102
- end
103
- end
104
- end
105
-
106
- # Resolves *all* filtered settings from all namespaces
107
- #
108
- # @param filter [String] Only resolve setting names which match the filter. See PDK::Config::Namespace.be_resolved? for matching rules
109
- # @return [Hash{String => Object}] All resolved settings for example {'user.module_defaults.author' => 'johndoe'}
110
- def resolve(filter = nil)
111
- all_scopes.values.reverse.reduce({}) do |result, method_name|
112
- result.merge(send(method_name).resolve(filter))
113
- end
114
- end
115
-
116
- # Returns a configuration setting by name. This name can either be a String, Array or parameters e.g. These are equivalent
117
- # - PDK.config.get('user.a.b.c')
118
- # - PDK.config.get(['user', 'a', 'b', 'c'])
119
- # - PDK.config.get('user', 'a', 'b', 'c')
120
- # @param root [Array[String], String] The root setting name or the entire setting name as a single string
121
- # @param keys [String] The child names of the setting
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?
170
- end
171
-
172
- # Sets a configuration setting by name. This name can either be a String or an Array
173
- # - PDK.config.set('user.a.b.c', ...)
174
- # - PDK.config.set(['user', 'a', 'b', 'c'], ...)
175
- # @param key [String, Array[String]] The name of the configuration key to change
176
- # @param value [Object] The value to set the configuration setting to
177
- # @param options [Hash] Changes the behaviour of the setting process
178
- # @option options [Boolean] :force Disables any munging or array processing, and sets the value as it is. Default is false
179
- # @return [Object] The new value of the configuration setting
180
- def set(key, value, options = {})
181
- options = {
182
- force: false,
183
- }.merge(options)
184
-
185
- names = key.is_a?(String) ? split_key_string(key) : key
186
- raise ArgumentError, _('Invalid configuration names') if names.nil? || !names.is_a?(Array) || names.empty?
187
- scope_name = names[0]
188
- raise ArgumentError, _("Unknown configuration root '%{name}'") % { name: scope_name } if all_scopes[scope_name].nil?
189
- deep_set_object(value, options[:force], send(all_scopes[scope_name]), *names[1..-1])
190
- end
191
-
192
- def self.bolt_analytics_config
193
- file = PDK::Util::Filesystem.expand_path('~/.puppetlabs/bolt/analytics.yaml')
194
- PDK::Config::YAML.new(file: file)
195
- rescue PDK::Config::LoadError => e
196
- PDK.logger.debug _('Unable to load %{file}: %{message}') % {
197
- file: file,
198
- message: e.message,
199
- }
200
- PDK::Config::YAML.new
201
- end
202
-
203
- def self.analytics_config_path
204
- PDK::Util::Env['PDK_ANALYTICS_CONFIG'] || File.join(File.dirname(PDK::Util.configdir), 'puppet', 'analytics.yml')
205
- end
206
-
207
- def self.user_config_path
208
- File.join(PDK::Util.configdir, 'user_config.json')
209
- end
210
-
211
- def self.system_config_path
212
- File.join(PDK::Util.system_configdir, 'system_config.json')
213
- end
214
-
215
- def self.system_answers_path
216
- File.join(PDK::Util.system_configdir, 'answers.json')
217
- end
218
-
219
- def self.json_schemas_path
220
- File.join(__dir__, 'config')
221
- end
222
-
223
- # return nil if not exist
224
- def self.json_schema(name)
225
- File.join(json_schemas_path, name + '_schema.json')
226
- end
227
-
228
- def self.analytics_config_exist?
229
- PDK::Util::Filesystem.file?(analytics_config_path)
230
- end
231
-
232
- def self.analytics_config_interview!
233
- require 'pdk/cli/util'
234
-
235
- return unless PDK::CLI::Util.interactive?
236
-
237
- pre_message = _(
238
- 'PDK collects anonymous usage information to help us understand how ' \
239
- 'it is being used and make decisions on how to improve it. You can ' \
240
- 'find out more about what data we collect and how it is used in the ' \
241
- "PDK documentation at %{url}.\n",
242
- ) % { url: 'https://puppet.com/docs/pdk/latest/pdk_install.html' }
243
- post_message = _(
244
- 'You can opt in or out of the usage data collection at any time by ' \
245
- 'editing the analytics configuration file at %{path} and changing ' \
246
- "the '%{key}' value.",
247
- ) % {
248
- path: PDK::Config.analytics_config_path,
249
- key: 'disabled',
250
- }
251
-
252
- questions = [
253
- {
254
- name: 'enabled',
255
- question: _('Do you consent to the collection of anonymous PDK usage information?'),
256
- type: :yes,
257
- },
258
- ]
259
-
260
- require 'pdk/cli/util/interview'
261
-
262
- PDK.logger.info(text: pre_message, wrap: true)
263
- prompt = TTY::Prompt.new(help_color: :cyan)
264
- interview = PDK::CLI::Util::Interview.new(prompt)
265
- interview.add_questions(questions)
266
- answers = interview.run
267
-
268
- if answers.nil?
269
- PDK.logger.info _('No answer given, opting out of analytics collection.')
270
- PDK.config.set(%w[user analytics disabled], true)
271
- else
272
- PDK.config.set(%w[user analytics disabled], !answers['enabled'])
273
- end
274
-
275
- PDK.logger.info(text: post_message, wrap: true)
276
- end
277
-
278
- private
279
-
280
- #:nocov: This is a private method and is tested elsewhere
281
- def traverse_object(object, *names)
282
- return nil if object.nil? || !object.respond_to?(:[])
283
- return nil if names.nil?
284
- # It's possible to pass in empty names at the root traversal layer
285
- # but this should _only_ happen at the root namespace level
286
- if names.empty?
287
- return (object.is_a?(PDK::Config::Namespace) ? object : nil)
288
- end
289
-
290
- name = names.shift
291
- value = object[name]
292
- if names.empty?
293
- return value if value.is_a?(PDK::Config::Namespace)
294
- # Duplicate arrays and hashes so that they are isolated from changes being made
295
- (value.is_a?(Hash) || value.is_a?(Array)) ? value.dup : value
296
- else
297
- traverse_object(value, *names)
298
- end
299
- end
300
- #:nocov:
301
-
302
- #:nocov: This is a private method and is tested elsewhere
303
- # Takes a string representation of a setting and splits into its constituent setting parts e.g.
304
- # 'user.a.b.c' becomes ['user', 'a', 'b', 'c']
305
- # @return [Array[String]] The string split into each setting name as an array
306
- def split_key_string(key)
307
- raise ArgumentError, _('Expected a String but got \'%{klass}\'') % { klass: key.class } unless key.is_a?(String)
308
- key.split('.')
309
- end
310
- #:nocov:
311
-
312
- #:nocov:
313
- # Returns all known scope names and their associated method name to call, to query the scope
314
- # Note - Order is important. This dictates the resolution precedence order (topmost is processed first)
315
- # @return [Hash[String, Symbol]] A hash of the scope name then method name to call to query the scope (as a Symbol)
316
- def all_scopes
317
- # Note - Order is important. This dictates the resolution precedence order (topmost is processed first)
318
- {
319
- 'project' => :project_config,
320
- 'user' => :user_config,
321
- 'system' => :system_config,
322
- }.freeze
323
- end
324
-
325
- #:nocov: This is a private method and is tested elsewhere
326
- # Deeply traverses an object tree via `[]` and sets the last
327
- # element to the value specified.
328
- #
329
- # Creating any missing parent hashes during the traversal
330
- def deep_set_object(value, force, namespace, *names)
331
- raise ArgumentError, _('Missing or invalid namespace') unless namespace.is_a?(PDK::Config::Namespace)
332
- raise ArgumentError, _('Missing a name to set') if names.nil? || names.empty?
333
-
334
- name = names.shift
335
- current_value = namespace[name]
336
-
337
- # If the next thing in the traversal chain is another namespace, set the value using that child namespace.
338
- if current_value.is_a?(PDK::Config::Namespace)
339
- return deep_set_object(value, force, current_value, *names)
340
- end
341
-
342
- # We're at the end of the name traversal
343
- if names.empty?
344
- if force || !current_value.is_a?(Array)
345
- namespace[name] = value
346
- return value
347
- end
348
-
349
- # Arrays are a special case if we're not forcing the value
350
- namespace[name] = current_value << value unless current_value.include?(value)
351
- return value
352
- end
353
-
354
- # Need to generate a deep hash using the current remaining names
355
- # So given an origin *names of ['a', 'b', 'c', 'd'] and a value 'foo',
356
- # we eventually want a hash of `{"b"=>{"c"=>{"d"=>"foo"}}}`
357
- #
358
- # The code above has already shifted the first element so we currently have
359
- # name : 'a'
360
- # names: ['b', 'c', 'd']
361
- #
362
- #
363
- # First we need to pop off the last element ('d') in this case as we need to set that in the `reduce` call below
364
- # So now we have:
365
- # name : 'a'
366
- # names: ['b', 'c']
367
- # last_name : 'd'
368
- last_name = names.pop
369
- # Using reduce and an accumulator, we create the nested hash from the deepest value first. In this case the deepest value
370
- # is the last_name, so the starting condition is {"d"=>"foo"}
371
- # After the first iteration ('c'), the accumulator has {"c"=>{"d"=>"foo"}}}
372
- # After the last iteration ('b'), the accumulator has {"b"=>{"c"=>{"d"=>"foo"}}}
373
- hash_value = names.reverse.reduce(last_name => value) { |accumulator, item| { item => accumulator } }
374
-
375
- # If the current value is nil, then it can't be a namespace or an existing value
376
- # or
377
- # If the current value is not a Hash and are forcing the change.
378
- if current_value.nil? || (force && !current_value.is_a?(Hash))
379
- namespace[name] = hash_value
380
- return value
381
- end
382
-
383
- raise ArgumentError, _("Unable to set '%{key}' to '%{value}' as it is not a Hash") % { key: namespace.name + '.' + name, value: hash_value } unless current_value.is_a?(Hash)
384
-
385
- namespace[name] = current_value.merge(hash_value)
386
- value
387
- end
388
- #:nocov:
389
- end
390
- end
1
+ require 'pdk'
2
+
3
+ module PDK
4
+ class Config
5
+ autoload :IniFile, 'pdk/config/ini_file'
6
+ autoload :IniFileSetting, 'pdk/config/ini_file_setting'
7
+ autoload :JSON, 'pdk/config/json'
8
+ autoload :JSONSchemaNamespace, 'pdk/config/json_schema_namespace'
9
+ autoload :JSONSchemaSetting, 'pdk/config/json_schema_setting'
10
+ autoload :LoadError, 'pdk/config/errors'
11
+ autoload :Namespace, 'pdk/config/namespace'
12
+ autoload :Setting, 'pdk/config/setting'
13
+ autoload :Validator, 'pdk/config/validator'
14
+ autoload :YAML, 'pdk/config/yaml'
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 system level configuration settings.
37
+ # @return [PDK::Config::Namespace]
38
+ # @api private
39
+ def system_config
40
+ local_options = @config_options
41
+ @system_config ||= PDK::Config::JSON.new('system', file: local_options['system.path']) do
42
+ mount :module_defaults, PDK::Config::JSON.new(file: local_options['system.module_defaults.path'])
43
+ end
44
+ end
45
+
46
+ # The user level configuration settings.
47
+ # @return [PDK::Config::Namespace]
48
+ # @api private
49
+ def user_config
50
+ local_options = @config_options
51
+ @user_config ||= PDK::Config::JSON.new('user', file: local_options['user.path']) do
52
+ mount :module_defaults, PDK::Config::JSON.new(file: local_options['user.module_defaults.path'])
53
+
54
+ # Due to the json-schema gem having issues with Windows based paths, and only supporting Draft 05 (or less) do
55
+ # not use JSON validation yet. Once PDK drops support for EOL rubies, we will be able to use the json_schemer gem
56
+ # Which has much more modern support
57
+ # Reference - https://github.com/puppetlabs/pdk/pull/777
58
+ # Reference - https://tickets.puppetlabs.com/browse/PDK-1526
59
+ mount :analytics, PDK::Config::YAML.new(file: local_options['user.analytics.path'], persistent_defaults: true) do
60
+ setting :disabled do
61
+ validate PDK::Config::Validator.boolean
62
+ default_to { PDK::Config.bolt_analytics_config.fetch('disabled', true) }
63
+ end
64
+
65
+ setting 'user-id' do
66
+ validate PDK::Config::Validator.uuid
67
+ default_to do
68
+ require 'securerandom'
69
+
70
+ PDK::Config.bolt_analytics_config.fetch('user-id', SecureRandom.uuid)
71
+ end
72
+ end
73
+ end
74
+
75
+ # Display the feature flags
76
+ mount :pdk_feature_flags, PDK::Config::Namespace.new('pdk_feature_flags') do
77
+ setting 'available' do
78
+ default_to { PDK.available_feature_flags }
79
+ end
80
+
81
+ setting 'requested' do
82
+ default_to { PDK.requested_feature_flags }
83
+ end
84
+ end
85
+ end
86
+ end
87
+
88
+ # The project level configuration settings.
89
+ # @return [PDK::Config::Namespace]
90
+ # @api private
91
+ def project_config
92
+ context = @config_options['context']
93
+ @project ||= PDK::Config::Namespace.new('project') do
94
+ if context.is_a?(PDK::Context::ControlRepo)
95
+ mount :environment, PDK::ControlRepo.environment_conf_as_config(File.join(context.root_path, 'environment.conf'))
96
+ end
97
+
98
+ mount :validate, PDK::Config::YAML.new('validate', file: File.join(context.root_path, 'pdk.yaml'), persistent_defaults: true) do
99
+ setting 'ignore' do
100
+ default_to { [] }
101
+ end
102
+ end
103
+ end
104
+ end
105
+
106
+ # Resolves *all* filtered settings from all namespaces
107
+ #
108
+ # @param filter [String] Only resolve setting names which match the filter. See PDK::Config::Namespace.be_resolved? for matching rules
109
+ # @return [Hash{String => Object}] All resolved settings for example {'user.module_defaults.author' => 'johndoe'}
110
+ def resolve(filter = nil)
111
+ all_scopes.values.reverse.reduce({}) do |result, method_name|
112
+ result.merge(send(method_name).resolve(filter))
113
+ end
114
+ end
115
+
116
+ # Returns a configuration setting by name. This name can either be a String, Array or parameters e.g. These are equivalent
117
+ # - PDK.config.get('user.a.b.c')
118
+ # - PDK.config.get(['user', 'a', 'b', 'c'])
119
+ # - PDK.config.get('user', 'a', 'b', 'c')
120
+ # @param root [Array[String], String] The root setting name or the entire setting name as a single string
121
+ # @param keys [String] The child names of the setting
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?
170
+ end
171
+
172
+ # Sets a configuration setting by name. This name can either be a String or an Array
173
+ # - PDK.config.set('user.a.b.c', ...)
174
+ # - PDK.config.set(['user', 'a', 'b', 'c'], ...)
175
+ # @param key [String, Array[String]] The name of the configuration key to change
176
+ # @param value [Object] The value to set the configuration setting to
177
+ # @param options [Hash] Changes the behaviour of the setting process
178
+ # @option options [Boolean] :force Disables any munging or array processing, and sets the value as it is. Default is false
179
+ # @return [Object] The new value of the configuration setting
180
+ def set(key, value, options = {})
181
+ options = {
182
+ force: false,
183
+ }.merge(options)
184
+
185
+ names = key.is_a?(String) ? split_key_string(key) : key
186
+ raise ArgumentError, _('Invalid configuration names') if names.nil? || !names.is_a?(Array) || names.empty?
187
+ scope_name = names[0]
188
+ raise ArgumentError, _("Unknown configuration root '%{name}'") % { name: scope_name } if all_scopes[scope_name].nil?
189
+ deep_set_object(value, options[:force], send(all_scopes[scope_name]), *names[1..-1])
190
+ end
191
+
192
+ def self.bolt_analytics_config
193
+ file = PDK::Util::Filesystem.expand_path('~/.puppetlabs/bolt/analytics.yaml')
194
+ PDK::Config::YAML.new(file: file)
195
+ rescue PDK::Config::LoadError => e
196
+ PDK.logger.debug _('Unable to load %{file}: %{message}') % {
197
+ file: file,
198
+ message: e.message,
199
+ }
200
+ PDK::Config::YAML.new
201
+ end
202
+
203
+ def self.analytics_config_path
204
+ PDK::Util::Env['PDK_ANALYTICS_CONFIG'] || File.join(File.dirname(PDK::Util.configdir), 'puppet', 'analytics.yml')
205
+ end
206
+
207
+ def self.user_config_path
208
+ File.join(PDK::Util.configdir, 'user_config.json')
209
+ end
210
+
211
+ def self.system_config_path
212
+ File.join(PDK::Util.system_configdir, 'system_config.json')
213
+ end
214
+
215
+ def self.system_answers_path
216
+ File.join(PDK::Util.system_configdir, 'answers.json')
217
+ end
218
+
219
+ def self.json_schemas_path
220
+ File.join(__dir__, 'config')
221
+ end
222
+
223
+ # return nil if not exist
224
+ def self.json_schema(name)
225
+ File.join(json_schemas_path, name + '_schema.json')
226
+ end
227
+
228
+ def self.analytics_config_exist?
229
+ PDK::Util::Filesystem.file?(analytics_config_path)
230
+ end
231
+
232
+ def self.analytics_config_interview!
233
+ require 'pdk/cli/util'
234
+
235
+ return unless PDK::CLI::Util.interactive?
236
+
237
+ pre_message = _(
238
+ 'PDK collects anonymous usage information to help us understand how ' \
239
+ 'it is being used and make decisions on how to improve it. You can ' \
240
+ 'find out more about what data we collect and how it is used in the ' \
241
+ "PDK documentation at %{url}.\n",
242
+ ) % { url: 'https://puppet.com/docs/pdk/latest/pdk_install.html' }
243
+ post_message = _(
244
+ 'You can opt in or out of the usage data collection at any time by ' \
245
+ 'editing the analytics configuration file at %{path} and changing ' \
246
+ "the '%{key}' value.",
247
+ ) % {
248
+ path: PDK::Config.analytics_config_path,
249
+ key: 'disabled',
250
+ }
251
+
252
+ questions = [
253
+ {
254
+ name: 'enabled',
255
+ question: _('Do you consent to the collection of anonymous PDK usage information?'),
256
+ type: :yes,
257
+ },
258
+ ]
259
+
260
+ require 'pdk/cli/util/interview'
261
+
262
+ PDK.logger.info(text: pre_message, wrap: true)
263
+ prompt = TTY::Prompt.new(help_color: :cyan)
264
+ interview = PDK::CLI::Util::Interview.new(prompt)
265
+ interview.add_questions(questions)
266
+ answers = interview.run
267
+
268
+ if answers.nil?
269
+ PDK.logger.info _('No answer given, opting out of analytics collection.')
270
+ PDK.config.set(%w[user analytics disabled], true)
271
+ else
272
+ PDK.config.set(%w[user analytics disabled], !answers['enabled'])
273
+ end
274
+
275
+ PDK.logger.info(text: post_message, wrap: true)
276
+ end
277
+
278
+ private
279
+
280
+ #:nocov: This is a private method and is tested elsewhere
281
+ def traverse_object(object, *names)
282
+ return nil if object.nil? || !object.respond_to?(:[])
283
+ return nil if names.nil?
284
+ # It's possible to pass in empty names at the root traversal layer
285
+ # but this should _only_ happen at the root namespace level
286
+ if names.empty?
287
+ return (object.is_a?(PDK::Config::Namespace) ? object : nil)
288
+ end
289
+
290
+ name = names.shift
291
+ value = object[name]
292
+ if names.empty?
293
+ return value if value.is_a?(PDK::Config::Namespace)
294
+ # Duplicate arrays and hashes so that they are isolated from changes being made
295
+ (value.is_a?(Hash) || value.is_a?(Array)) ? value.dup : value
296
+ else
297
+ traverse_object(value, *names)
298
+ end
299
+ end
300
+ #:nocov:
301
+
302
+ #:nocov: This is a private method and is tested elsewhere
303
+ # Takes a string representation of a setting and splits into its constituent setting parts e.g.
304
+ # 'user.a.b.c' becomes ['user', 'a', 'b', 'c']
305
+ # @return [Array[String]] The string split into each setting name as an array
306
+ def split_key_string(key)
307
+ raise ArgumentError, _('Expected a String but got \'%{klass}\'') % { klass: key.class } unless key.is_a?(String)
308
+ key.split('.')
309
+ end
310
+ #:nocov:
311
+
312
+ #:nocov:
313
+ # Returns all known scope names and their associated method name to call, to query the scope
314
+ # Note - Order is important. This dictates the resolution precedence order (topmost is processed first)
315
+ # @return [Hash[String, Symbol]] A hash of the scope name then method name to call to query the scope (as a Symbol)
316
+ def all_scopes
317
+ # Note - Order is important. This dictates the resolution precedence order (topmost is processed first)
318
+ {
319
+ 'project' => :project_config,
320
+ 'user' => :user_config,
321
+ 'system' => :system_config,
322
+ }.freeze
323
+ end
324
+
325
+ #:nocov: This is a private method and is tested elsewhere
326
+ # Deeply traverses an object tree via `[]` and sets the last
327
+ # element to the value specified.
328
+ #
329
+ # Creating any missing parent hashes during the traversal
330
+ def deep_set_object(value, force, namespace, *names)
331
+ raise ArgumentError, _('Missing or invalid namespace') unless namespace.is_a?(PDK::Config::Namespace)
332
+ raise ArgumentError, _('Missing a name to set') if names.nil? || names.empty?
333
+
334
+ name = names.shift
335
+ current_value = namespace[name]
336
+
337
+ # If the next thing in the traversal chain is another namespace, set the value using that child namespace.
338
+ if current_value.is_a?(PDK::Config::Namespace)
339
+ return deep_set_object(value, force, current_value, *names)
340
+ end
341
+
342
+ # We're at the end of the name traversal
343
+ if names.empty?
344
+ if force || !current_value.is_a?(Array)
345
+ namespace[name] = value
346
+ return value
347
+ end
348
+
349
+ # Arrays are a special case if we're not forcing the value
350
+ namespace[name] = current_value << value unless current_value.include?(value)
351
+ return value
352
+ end
353
+
354
+ # Need to generate a deep hash using the current remaining names
355
+ # So given an origin *names of ['a', 'b', 'c', 'd'] and a value 'foo',
356
+ # we eventually want a hash of `{"b"=>{"c"=>{"d"=>"foo"}}}`
357
+ #
358
+ # The code above has already shifted the first element so we currently have
359
+ # name : 'a'
360
+ # names: ['b', 'c', 'd']
361
+ #
362
+ #
363
+ # First we need to pop off the last element ('d') in this case as we need to set that in the `reduce` call below
364
+ # So now we have:
365
+ # name : 'a'
366
+ # names: ['b', 'c']
367
+ # last_name : 'd'
368
+ last_name = names.pop
369
+ # Using reduce and an accumulator, we create the nested hash from the deepest value first. In this case the deepest value
370
+ # is the last_name, so the starting condition is {"d"=>"foo"}
371
+ # After the first iteration ('c'), the accumulator has {"c"=>{"d"=>"foo"}}}
372
+ # After the last iteration ('b'), the accumulator has {"b"=>{"c"=>{"d"=>"foo"}}}
373
+ hash_value = names.reverse.reduce(last_name => value) { |accumulator, item| { item => accumulator } }
374
+
375
+ # If the current value is nil, then it can't be a namespace or an existing value
376
+ # or
377
+ # If the current value is not a Hash and are forcing the change.
378
+ if current_value.nil? || (force && !current_value.is_a?(Hash))
379
+ namespace[name] = hash_value
380
+ return value
381
+ end
382
+
383
+ raise ArgumentError, _("Unable to set '%{key}' to '%{value}' as it is not a Hash") % { key: namespace.name + '.' + name, value: hash_value } unless current_value.is_a?(Hash)
384
+
385
+ namespace[name] = current_value.merge(hash_value)
386
+ value
387
+ end
388
+ #:nocov:
389
+ end
390
+ end