pdk 2.1.0 → 2.4.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 +1329 -1292
  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 -283
  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 -192
  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 -42
  70. data/lib/pdk/config/yaml_with_schema.rb +59 -59
  71. data/lib/pdk/config.rb +390 -384
  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 -254
  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 -124
  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 -84
  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 -220
  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 +19 -13
data/lib/pdk/config.rb CHANGED
@@ -1,384 +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
- end
98
- end
99
-
100
- # Resolves *all* filtered settings from all namespaces
101
- #
102
- # @param filter [String] Only resolve setting names which match the filter. See PDK::Config::Namespace.be_resolved? for matching rules
103
- # @return [Hash{String => Object}] All resolved settings for example {'user.module_defaults.author' => 'johndoe'}
104
- def resolve(filter = nil)
105
- all_scopes.values.reverse.reduce({}) do |result, method_name|
106
- result.merge(send(method_name).resolve(filter))
107
- end
108
- end
109
-
110
- # Returns a configuration setting by name. This name can either be a String, Array or parameters e.g. These are equivalent
111
- # - PDK.config.get('user.a.b.c')
112
- # - PDK.config.get(['user', 'a', 'b', 'c'])
113
- # - PDK.config.get('user', 'a', 'b', 'c')
114
- # @param root [Array[String], String] The root setting name or the entire setting name as a single string
115
- # @param keys [String] The child names of the setting
116
- # @return [PDK::Config::Namespace, Object, nil] The value of the configuration setting. Returns nil if it does no exist
117
- def get(root, *keys)
118
- return nil if root.nil? || root.empty?
119
-
120
- if keys.empty?
121
- if root.is_a?(Array)
122
- name = root
123
- elsif root.is_a?(String)
124
- name = split_key_string(root)
125
- else
126
- return nil
127
- end
128
- else
129
- name = [root].concat(keys)
130
- end
131
-
132
- get_within_scopes(name[1..-1], [name[0]])
133
- end
134
-
135
- # 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
136
- # @setting_name [String, Array[String]] The setting name to retrieve without the leading scope name e.g. Use 'setting' instead of 'system.setting'
137
- # @scopes [Nil, Array[String]] The list of scopes, in order, to query in turn for the setting_name. Invalid or missing scopes are ignored.
138
- # @return [PDK::Config::Namespace, Object, nil] The value of the configuration setting. Returns nil if it does no exist
139
- def get_within_scopes(setting_name, scopes = nil)
140
- raise ArgumentError, _('Expected an Array but got \'%{klass}\' for scopes') % { klass: scopes.class } unless scopes.nil? || scopes.is_a?(Array)
141
- 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)
142
-
143
- setting_arr = setting_name.is_a?(String) ? split_key_string(setting_name) : setting_name
144
- all_scope_names = all_scopes.keys
145
-
146
- # Use only valid scope names
147
- scopes = scopes.nil? ? all_scope_names : scopes & all_scope_names
148
-
149
- scopes.each do |scope_name|
150
- value = traverse_object(send(all_scopes[scope_name]), *setting_arr)
151
- return value unless value.nil?
152
- end
153
- nil
154
- end
155
-
156
- # 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
157
- # @setting_name [String, Array[String]] The setting name to retrieve without the leading scope name e.g. Use 'setting' instead of 'system.setting'
158
- # @scopes [Nil, Array[String]] The list of scopes, in order, to query in turn for the setting_name. Invalid or missing scopes are ignored.
159
- # @yield [PDK::Config::Namespace, Object] The value of the configuration setting. Does not yield if the setting does not exist or is nil
160
- def with_scoped_value(setting_name, scopes = nil)
161
- raise ArgumentError, _('must be passed a block') unless block_given?
162
- value = get_within_scopes(setting_name, scopes)
163
- yield value unless value.nil?
164
- end
165
-
166
- # Sets a configuration setting by name. This name can either be a String or an Array
167
- # - PDK.config.set('user.a.b.c', ...)
168
- # - PDK.config.set(['user', 'a', 'b', 'c'], ...)
169
- # @param key [String, Array[String]] The name of the configuration key to change
170
- # @param value [Object] The value to set the configuration setting to
171
- # @param options [Hash] Changes the behaviour of the setting process
172
- # @option options [Boolean] :force Disables any munging or array processing, and sets the value as it is. Default is false
173
- # @return [Object] The new value of the configuration setting
174
- def set(key, value, options = {})
175
- options = {
176
- force: false,
177
- }.merge(options)
178
-
179
- names = key.is_a?(String) ? split_key_string(key) : key
180
- raise ArgumentError, _('Invalid configuration names') if names.nil? || !names.is_a?(Array) || names.empty?
181
- scope_name = names[0]
182
- raise ArgumentError, _("Unknown configuration root '%{name}'") % { name: scope_name } if all_scopes[scope_name].nil?
183
- deep_set_object(value, options[:force], send(all_scopes[scope_name]), *names[1..-1])
184
- end
185
-
186
- def self.bolt_analytics_config
187
- file = PDK::Util::Filesystem.expand_path('~/.puppetlabs/bolt/analytics.yaml')
188
- PDK::Config::YAML.new(file: file)
189
- rescue PDK::Config::LoadError => e
190
- PDK.logger.debug _('Unable to load %{file}: %{message}') % {
191
- file: file,
192
- message: e.message,
193
- }
194
- PDK::Config::YAML.new
195
- end
196
-
197
- def self.analytics_config_path
198
- PDK::Util::Env['PDK_ANALYTICS_CONFIG'] || File.join(File.dirname(PDK::Util.configdir), 'puppet', 'analytics.yml')
199
- end
200
-
201
- def self.user_config_path
202
- File.join(PDK::Util.configdir, 'user_config.json')
203
- end
204
-
205
- def self.system_config_path
206
- File.join(PDK::Util.system_configdir, 'system_config.json')
207
- end
208
-
209
- def self.system_answers_path
210
- File.join(PDK::Util.system_configdir, 'answers.json')
211
- end
212
-
213
- def self.json_schemas_path
214
- File.join(__dir__, 'config')
215
- end
216
-
217
- # return nil if not exist
218
- def self.json_schema(name)
219
- File.join(json_schemas_path, name + '_schema.json')
220
- end
221
-
222
- def self.analytics_config_exist?
223
- PDK::Util::Filesystem.file?(analytics_config_path)
224
- end
225
-
226
- def self.analytics_config_interview!
227
- require 'pdk/cli/util'
228
-
229
- return unless PDK::CLI::Util.interactive?
230
-
231
- pre_message = _(
232
- 'PDK collects anonymous usage information to help us understand how ' \
233
- 'it is being used and make decisions on how to improve it. You can ' \
234
- 'find out more about what data we collect and how it is used in the ' \
235
- "PDK documentation at %{url}.\n",
236
- ) % { url: 'https://puppet.com/docs/pdk/latest/pdk_install.html' }
237
- post_message = _(
238
- 'You can opt in or out of the usage data collection at any time by ' \
239
- 'editing the analytics configuration file at %{path} and changing ' \
240
- "the '%{key}' value.",
241
- ) % {
242
- path: PDK::Config.analytics_config_path,
243
- key: 'disabled',
244
- }
245
-
246
- questions = [
247
- {
248
- name: 'enabled',
249
- question: _('Do you consent to the collection of anonymous PDK usage information?'),
250
- type: :yes,
251
- },
252
- ]
253
-
254
- require 'pdk/cli/util/interview'
255
-
256
- PDK.logger.info(text: pre_message, wrap: true)
257
- prompt = TTY::Prompt.new(help_color: :cyan)
258
- interview = PDK::CLI::Util::Interview.new(prompt)
259
- interview.add_questions(questions)
260
- answers = interview.run
261
-
262
- if answers.nil?
263
- PDK.logger.info _('No answer given, opting out of analytics collection.')
264
- PDK.config.set(%w[user analytics disabled], true)
265
- else
266
- PDK.config.set(%w[user analytics disabled], !answers['enabled'])
267
- end
268
-
269
- PDK.logger.info(text: post_message, wrap: true)
270
- end
271
-
272
- private
273
-
274
- #:nocov: This is a private method and is tested elsewhere
275
- def traverse_object(object, *names)
276
- return nil if object.nil? || !object.respond_to?(:[])
277
- return nil if names.nil?
278
- # It's possible to pass in empty names at the root traversal layer
279
- # but this should _only_ happen at the root namespace level
280
- if names.empty?
281
- return (object.is_a?(PDK::Config::Namespace) ? object : nil)
282
- end
283
-
284
- name = names.shift
285
- value = object[name]
286
- if names.empty?
287
- return value if value.is_a?(PDK::Config::Namespace)
288
- # Duplicate arrays and hashes so that they are isolated from changes being made
289
- (value.is_a?(Hash) || value.is_a?(Array)) ? value.dup : value
290
- else
291
- traverse_object(value, *names)
292
- end
293
- end
294
- #:nocov:
295
-
296
- #:nocov: This is a private method and is tested elsewhere
297
- # Takes a string representation of a setting and splits into its constituent setting parts e.g.
298
- # 'user.a.b.c' becomes ['user', 'a', 'b', 'c']
299
- # @return [Array[String]] The string split into each setting name as an array
300
- def split_key_string(key)
301
- raise ArgumentError, _('Expected a String but got \'%{klass}\'') % { klass: key.class } unless key.is_a?(String)
302
- key.split('.')
303
- end
304
- #:nocov:
305
-
306
- #:nocov:
307
- # Returns all known scope names and their associated method name to call, to query the scope
308
- # Note - Order is important. This dictates the resolution precedence order (topmost is processed first)
309
- # @return [Hash[String, Symbol]] A hash of the scope name then method name to call to query the scope (as a Symbol)
310
- def all_scopes
311
- # Note - Order is important. This dictates the resolution precedence order (topmost is processed first)
312
- {
313
- 'project' => :project_config,
314
- 'user' => :user_config,
315
- 'system' => :system_config,
316
- }.freeze
317
- end
318
-
319
- #:nocov: This is a private method and is tested elsewhere
320
- # Deeply traverses an object tree via `[]` and sets the last
321
- # element to the value specified.
322
- #
323
- # Creating any missing parent hashes during the traversal
324
- def deep_set_object(value, force, namespace, *names)
325
- raise ArgumentError, _('Missing or invalid namespace') unless namespace.is_a?(PDK::Config::Namespace)
326
- raise ArgumentError, _('Missing a name to set') if names.nil? || names.empty?
327
-
328
- name = names.shift
329
- current_value = namespace[name]
330
-
331
- # If the next thing in the traversal chain is another namespace, set the value using that child namespace.
332
- if current_value.is_a?(PDK::Config::Namespace)
333
- return deep_set_object(value, force, current_value, *names)
334
- end
335
-
336
- # We're at the end of the name traversal
337
- if names.empty?
338
- if force || !current_value.is_a?(Array)
339
- namespace[name] = value
340
- return value
341
- end
342
-
343
- # Arrays are a special case if we're not forcing the value
344
- namespace[name] = current_value << value unless current_value.include?(value)
345
- return value
346
- end
347
-
348
- # Need to generate a deep hash using the current remaining names
349
- # So given an origin *names of ['a', 'b', 'c', 'd'] and a value 'foo',
350
- # we eventually want a hash of `{"b"=>{"c"=>{"d"=>"foo"}}}`
351
- #
352
- # The code above has already shifted the first element so we currently have
353
- # name : 'a'
354
- # names: ['b', 'c', 'd']
355
- #
356
- #
357
- # First we need to pop off the last element ('d') in this case as we need to set that in the `reduce` call below
358
- # So now we have:
359
- # name : 'a'
360
- # names: ['b', 'c']
361
- # last_name : 'd'
362
- last_name = names.pop
363
- # Using reduce and an accumulator, we create the nested hash from the deepest value first. In this case the deepest value
364
- # is the last_name, so the starting condition is {"d"=>"foo"}
365
- # After the first iteration ('c'), the accumulator has {"c"=>{"d"=>"foo"}}}
366
- # After the last iteration ('b'), the accumulator has {"b"=>{"c"=>{"d"=>"foo"}}}
367
- hash_value = names.reverse.reduce(last_name => value) { |accumulator, item| { item => accumulator } }
368
-
369
- # If the current value is nil, then it can't be a namespace or an existing value
370
- # or
371
- # If the current value is not a Hash and are forcing the change.
372
- if current_value.nil? || (force && !current_value.is_a?(Hash))
373
- namespace[name] = hash_value
374
- return value
375
- end
376
-
377
- 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)
378
-
379
- namespace[name] = current_value.merge(hash_value)
380
- value
381
- end
382
- #:nocov:
383
- end
384
- 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