pdk 2.4.0 → 2.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (153) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +1336 -1329
  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 -278
  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 +2111 -2094
  153. metadata +3 -3
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