pdk 1.16.0 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (125) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +167 -11
  3. data/README.md +1 -1
  4. data/lib/pdk.rb +26 -19
  5. data/lib/pdk/answer_file.rb +2 -93
  6. data/lib/pdk/cli.rb +8 -6
  7. data/lib/pdk/cli/config.rb +3 -1
  8. data/lib/pdk/cli/config/get.rb +3 -1
  9. data/lib/pdk/cli/convert.rb +7 -9
  10. data/lib/pdk/cli/env.rb +52 -0
  11. data/lib/pdk/cli/exec/command.rb +13 -0
  12. data/lib/pdk/cli/exec_group.rb +78 -43
  13. data/lib/pdk/cli/get.rb +20 -0
  14. data/lib/pdk/cli/get/config.rb +24 -0
  15. data/lib/pdk/cli/new.rb +2 -0
  16. data/lib/pdk/cli/new/class.rb +2 -1
  17. data/lib/pdk/cli/new/defined_type.rb +2 -1
  18. data/lib/pdk/cli/new/fact.rb +29 -0
  19. data/lib/pdk/cli/new/function.rb +29 -0
  20. data/lib/pdk/cli/new/provider.rb +2 -1
  21. data/lib/pdk/cli/new/task.rb +2 -1
  22. data/lib/pdk/cli/new/test.rb +2 -1
  23. data/lib/pdk/cli/new/transport.rb +2 -1
  24. data/lib/pdk/cli/release/publish.rb +11 -1
  25. data/lib/pdk/cli/remove.rb +20 -0
  26. data/lib/pdk/cli/remove/config.rb +80 -0
  27. data/lib/pdk/cli/set.rb +20 -0
  28. data/lib/pdk/cli/set/config.rb +119 -0
  29. data/lib/pdk/cli/update.rb +6 -8
  30. data/lib/pdk/cli/util.rb +7 -3
  31. data/lib/pdk/cli/util/option_validator.rb +6 -0
  32. data/lib/pdk/cli/util/update_manager_printer.rb +82 -0
  33. data/lib/pdk/cli/validate.rb +26 -44
  34. data/lib/pdk/config.rb +264 -7
  35. data/lib/pdk/config/ini_file.rb +183 -0
  36. data/lib/pdk/config/ini_file_setting.rb +39 -0
  37. data/lib/pdk/config/namespace.rb +25 -5
  38. data/lib/pdk/config/setting.rb +3 -2
  39. data/lib/pdk/context.rb +99 -0
  40. data/lib/pdk/context/control_repo.rb +60 -0
  41. data/lib/pdk/context/module.rb +28 -0
  42. data/lib/pdk/context/none.rb +22 -0
  43. data/lib/pdk/control_repo.rb +40 -0
  44. data/lib/pdk/generate/defined_type.rb +25 -32
  45. data/lib/pdk/generate/fact.rb +25 -0
  46. data/lib/pdk/generate/function.rb +48 -0
  47. data/lib/pdk/generate/module.rb +14 -17
  48. data/lib/pdk/generate/provider.rb +15 -64
  49. data/lib/pdk/generate/puppet_class.rb +25 -31
  50. data/lib/pdk/generate/puppet_object.rb +83 -187
  51. data/lib/pdk/generate/task.rb +28 -46
  52. data/lib/pdk/generate/transport.rb +20 -74
  53. data/lib/pdk/module.rb +1 -1
  54. data/lib/pdk/module/convert.rb +43 -23
  55. data/lib/pdk/module/metadata.rb +6 -2
  56. data/lib/pdk/module/release.rb +3 -9
  57. data/lib/pdk/module/update.rb +7 -11
  58. data/lib/pdk/module/update_manager.rb +7 -0
  59. data/lib/pdk/report.rb +3 -3
  60. data/lib/pdk/report/event.rb +8 -2
  61. data/lib/pdk/template.rb +59 -0
  62. data/lib/pdk/template/fetcher.rb +98 -0
  63. data/lib/pdk/template/fetcher/git.rb +85 -0
  64. data/lib/pdk/template/fetcher/local.rb +28 -0
  65. data/lib/pdk/template/renderer.rb +96 -0
  66. data/lib/pdk/template/renderer/v1.rb +25 -0
  67. data/lib/pdk/template/renderer/v1/legacy_template_dir.rb +116 -0
  68. data/lib/pdk/template/renderer/v1/renderer.rb +132 -0
  69. data/lib/pdk/template/renderer/v1/template_file.rb +102 -0
  70. data/lib/pdk/template/template_dir.rb +67 -0
  71. data/lib/pdk/tests/unit.rb +8 -1
  72. data/lib/pdk/util.rb +38 -39
  73. data/lib/pdk/util/bundler.rb +2 -1
  74. data/lib/pdk/util/changelog_generator.rb +11 -2
  75. data/lib/pdk/util/json_finder.rb +84 -0
  76. data/lib/pdk/util/puppet_strings.rb +3 -3
  77. data/lib/pdk/util/puppet_version.rb +2 -2
  78. data/lib/pdk/util/ruby_version.rb +5 -1
  79. data/lib/pdk/util/template_uri.rb +13 -14
  80. data/lib/pdk/util/vendored_file.rb +1 -2
  81. data/lib/pdk/validate.rb +79 -25
  82. data/lib/pdk/validate/control_repo/control_repo_validator_group.rb +23 -0
  83. data/lib/pdk/validate/control_repo/environment_conf_validator.rb +98 -0
  84. data/lib/pdk/validate/external_command_validator.rb +208 -0
  85. data/lib/pdk/validate/internal_ruby_validator.rb +100 -0
  86. data/lib/pdk/validate/invokable_validator.rb +220 -0
  87. data/lib/pdk/validate/metadata/metadata_json_lint_validator.rb +86 -0
  88. data/lib/pdk/validate/metadata/metadata_syntax_validator.rb +78 -0
  89. data/lib/pdk/validate/metadata/metadata_validator_group.rb +20 -0
  90. data/lib/pdk/validate/puppet/puppet_epp_validator.rb +133 -0
  91. data/lib/pdk/validate/puppet/puppet_lint_validator.rb +66 -0
  92. data/lib/pdk/validate/puppet/puppet_syntax_validator.rb +137 -0
  93. data/lib/pdk/validate/puppet/puppet_validator_group.rb +21 -0
  94. data/lib/pdk/validate/ruby/ruby_rubocop_validator.rb +80 -0
  95. data/lib/pdk/validate/ruby/ruby_validator_group.rb +19 -0
  96. data/lib/pdk/validate/tasks/tasks_metadata_lint_validator.rb +88 -0
  97. data/lib/pdk/validate/tasks/tasks_name_validator.rb +50 -0
  98. data/lib/pdk/validate/tasks/tasks_validator_group.rb +20 -0
  99. data/lib/pdk/validate/validator.rb +118 -0
  100. data/lib/pdk/validate/validator_group.rb +104 -0
  101. data/lib/pdk/validate/yaml/yaml_syntax_validator.rb +95 -0
  102. data/lib/pdk/validate/yaml/yaml_validator_group.rb +19 -0
  103. data/lib/pdk/version.rb +1 -1
  104. data/locales/pdk.pot +477 -313
  105. metadata +77 -35
  106. data/lib/pdk/module/template_dir.rb +0 -115
  107. data/lib/pdk/module/template_dir/base.rb +0 -268
  108. data/lib/pdk/module/template_dir/git.rb +0 -91
  109. data/lib/pdk/module/template_dir/local.rb +0 -21
  110. data/lib/pdk/template_file.rb +0 -96
  111. data/lib/pdk/validate/base_validator.rb +0 -215
  112. data/lib/pdk/validate/metadata/metadata_json_lint.rb +0 -82
  113. data/lib/pdk/validate/metadata/metadata_syntax.rb +0 -111
  114. data/lib/pdk/validate/metadata_validator.rb +0 -26
  115. data/lib/pdk/validate/puppet/puppet_epp.rb +0 -135
  116. data/lib/pdk/validate/puppet/puppet_lint.rb +0 -64
  117. data/lib/pdk/validate/puppet/puppet_syntax.rb +0 -135
  118. data/lib/pdk/validate/puppet_validator.rb +0 -26
  119. data/lib/pdk/validate/ruby/rubocop.rb +0 -72
  120. data/lib/pdk/validate/ruby_validator.rb +0 -26
  121. data/lib/pdk/validate/tasks/metadata_lint.rb +0 -130
  122. data/lib/pdk/validate/tasks/name.rb +0 -90
  123. data/lib/pdk/validate/tasks_validator.rb +0 -29
  124. data/lib/pdk/validate/yaml/syntax.rb +0 -125
  125. data/lib/pdk/validate/yaml_validator.rb +0 -28
@@ -10,14 +10,12 @@ module PDK::CLI
10
10
  PDK::CLI.template_ref_option(self)
11
11
 
12
12
  run do |opts, _args, _cmd|
13
- require 'pdk/cli/util'
14
- require 'pdk/util'
15
- require 'pdk/module/update'
13
+ # Write the context information to the debug log
14
+ PDK.context.to_debug_log
16
15
 
17
- PDK::CLI::Util.ensure_in_module!(
18
- message: _('`pdk update` can only be run from inside a valid module directory.'),
19
- log_level: :info,
20
- )
16
+ unless PDK.context.is_a?(PDK::Context::Module)
17
+ raise PDK::CLI::ExitWithError, _('`pdk update` can only be run from inside a valid module directory.')
18
+ end
21
19
 
22
20
  raise PDK::CLI::ExitWithError, _('This module does not appear to be PDK compatible. To make the module compatible with PDK, run `pdk convert`.') unless PDK::Util.module_pdk_compatible?
23
21
 
@@ -46,7 +44,7 @@ module PDK::CLI
46
44
 
47
45
  PDK::CLI::Util.analytics_screen_view('update', opts)
48
46
 
49
- updater = PDK::Module::Update.new(PDK::Util.module_root, opts)
47
+ updater = PDK::Module::Update.new(PDK.context.root_path, opts)
50
48
 
51
49
  if updater.pinned_to_puppetlabs_template_tag?
52
50
  PDK.logger.info _(
data/lib/pdk/cli/util.rb CHANGED
@@ -8,6 +8,7 @@ module PDK
8
8
  autoload :OptionValidator, 'pdk/cli/util/option_validator'
9
9
  autoload :Interview, 'pdk/cli/util/interview'
10
10
  autoload :Spinner, 'pdk/cli/util/spinner'
11
+ autoload :UpdateManagerPrinter, 'pdk/cli/util/update_manager_printer'
11
12
 
12
13
  # Ensures the calling code is being run from inside a module directory.
13
14
  #
@@ -129,13 +130,14 @@ module PDK
129
130
  end
130
131
  module_function :check_for_deprecated_puppet
131
132
 
132
- # @param opts [Hash] - the pdk options ot use, defaults to empty hash
133
+ # @param opts [Hash] - the pdk options to use, defaults to empty hash
133
134
  # @option opts [String] :'puppet-dev' Use the puppet development version, default to PDK_PUPPET_DEV env
134
135
  # @option opts [String] :'puppet-version' Puppet version to use, default to PDK_PUPPET_VERSION env
135
136
  # @option opts [String] :'pe-version' PE Puppet version to use, default to PDK_PE_VERSION env
136
137
  # @param logging_disabled [Boolean] - disable logging of PDK info
138
+ # @param context [PDK::Context::AbstractContext] - The context the PDK is running in
137
139
  # @return [Hash] - return hash of { gemset: <>, ruby_version: 2.x.x }
138
- def puppet_from_opts_or_env(opts, logging_disabled = false)
140
+ def puppet_from_opts_or_env(opts, logging_disabled = false, context = PDK.context)
139
141
  opts ||= {}
140
142
  use_puppet_dev = opts.fetch(:'puppet-dev', PDK::Util::Env['PDK_PUPPET_DEV'])
141
143
  desired_puppet_version = opts.fetch(:'puppet-version', PDK::Util::Env['PDK_PUPPET_VERSION'])
@@ -150,8 +152,10 @@ module PDK
150
152
  PDK::Util::PuppetVersion.find_gem_for(desired_puppet_version)
151
153
  elsif desired_pe_version
152
154
  PDK::Util::PuppetVersion.from_pe_version(desired_pe_version)
153
- else
155
+ elsif context.is_a?(PDK::Context::Module)
154
156
  PDK::Util::PuppetVersion.from_module_metadata || PDK::Util::PuppetVersion.latest_available
157
+ else
158
+ PDK::Util::PuppetVersion.latest_available
155
159
  end
156
160
  rescue ArgumentError => e
157
161
  raise PDK::CLI::ExitWithError, e.message
@@ -8,6 +8,11 @@ module PDK
8
8
  (list =~ %r{^[\w\-]+(?:,[\w\-]+)+$}) ? true : false
9
9
  end
10
10
 
11
+ # @return [Boolean] true if the fact name is valid
12
+ def self.valid_fact_name?(name)
13
+ name.length > 1
14
+ end
15
+
11
16
  def self.enum(val, valid_entries, _options = {})
12
17
  vals = val.is_a?(Array) ? val : [val]
13
18
  invalid_entries = vals.reject { |e| valid_entries.include?(e) }
@@ -41,6 +46,7 @@ module PDK
41
46
 
42
47
  !(string =~ %r{\A([a-z][a-z0-9_]*)(::[a-z][a-z0-9_]*)*\Z}).nil?
43
48
  end
49
+ singleton_class.send(:alias_method, :valid_function_name?, :valid_namespace?)
44
50
 
45
51
  singleton_class.send(:alias_method, :valid_class_name?, :valid_namespace?)
46
52
  singleton_class.send(:alias_method, :valid_defined_type_name?, :valid_namespace?)
@@ -0,0 +1,82 @@
1
+ require 'pdk'
2
+
3
+ module PDK
4
+ module CLI
5
+ module Util
6
+ module UpdateManagerPrinter
7
+ # Prints the summary for a PDK::Module::UpdateManager Object
8
+ # @param update_manager [PDK::Module::UpdateManager] The object to print a summary of
9
+ # @param options [Hash{Object => Object}] A list of options when printing
10
+ # @option options [Boolean] :tense Whether to use future (:future) or past (:past) tense when printing the summary ("Files to be added" versus "Files added"). Default is :future
11
+ #
12
+ # @return [void]
13
+ def self.print_summary(update_manager, options = {})
14
+ require 'pdk/report'
15
+
16
+ options = {
17
+ tense: :future,
18
+ }.merge(options)
19
+
20
+ footer = false
21
+
22
+ summary(update_manager).each do |category, files|
23
+ next if files.empty?
24
+
25
+ PDK::Report.default_target.puts('')
26
+ PDK::Report.default_target.puts(generate_banner("Files #{(options[:tense] == :future) ? 'to be ' : ''}#{category}", 40))
27
+ PDK::Report.default_target.puts(files.map(&:to_s).join("\n"))
28
+ footer = true
29
+ end
30
+
31
+ if footer # rubocop:disable Style/GuardClause No.
32
+ PDK::Report.default_target.puts('')
33
+ PDK::Report.default_target.puts(generate_banner('', 40))
34
+ end
35
+ end
36
+
37
+ #:nocov: Tested as part of the public methods
38
+ # Returns a hash, summarizing the contents of the Update Manager object
39
+ # @param update_manager [PDK::Module::UpdateManager] The object to create a summary of
40
+ #
41
+ # @return [Hash{Symbol => Array[String]}] A hash of each category and the file paths in each category
42
+ def self.summary(update_manager)
43
+ summary = {}
44
+ update_manager.changes.each do |category, update_category|
45
+ if update_category.respond_to?(:keys)
46
+ updated_files = update_category.keys
47
+ else
48
+ begin
49
+ updated_files = update_category.map { |file| file[:path] }
50
+ rescue TypeError
51
+ updated_files = update_category.to_a
52
+ end
53
+ end
54
+
55
+ summary[category] = updated_files
56
+ end
57
+
58
+ summary
59
+ end
60
+ private_class_method :summary
61
+
62
+ # Creates a line of text, with `text` centered in the middle
63
+ # @param text [String] The text to put in the middle of the banner
64
+ # @param width [Integer] The width of the banner in characters. Default is 80
65
+ # @return [String] The generated banner
66
+ def self.generate_banner(text, width = 80)
67
+ padding = width - text.length
68
+ banner = ''
69
+ padding_char = '-'
70
+
71
+ (padding / 2.0).ceil.times { banner << padding_char }
72
+ banner << text
73
+ (padding / 2.0).floor.times { banner << padding_char }
74
+
75
+ banner
76
+ end
77
+ private_class_method :generate_banner
78
+ #:nocov:
79
+ end
80
+ end
81
+ end
82
+ end
@@ -19,6 +19,9 @@ module PDK::CLI
19
19
  flag nil, :parallel, _('Run validations in parallel.')
20
20
 
21
21
  run do |opts, args, _cmd|
22
+ # Write the context information to the debug log
23
+ PDK.context.to_debug_log
24
+
22
25
  if args == ['help']
23
26
  PDK::CLI.run(['validate', '--help'])
24
27
  exit 0
@@ -26,42 +29,42 @@ module PDK::CLI
26
29
 
27
30
  require 'pdk/validate'
28
31
 
29
- validator_names = PDK::Validate.validators.map { |v| v.name }
30
- validators = PDK::Validate.validators
31
- targets = []
32
-
33
32
  if opts[:list]
34
33
  PDK::CLI::Util.analytics_screen_view('validate', opts)
35
- PDK.logger.info(_('Available validators: %{validator_names}') % { validator_names: validator_names.join(', ') })
34
+ PDK.logger.info(_('Available validators: %{validator_names}') % { validator_names: PDK::Validate.validator_names.join(', ') })
36
35
  exit 0
37
36
  end
38
37
 
39
38
  PDK::CLI::Util.validate_puppet_version_opts(opts)
39
+ unless PDK.feature_flag?('controlrepo') || PDK.context.is_a?(PDK::Context::Module)
40
+ raise PDK::CLI::ExitWithError.new(_('Code validation can only be run from inside a valid module directory'), log_level: :error)
41
+ end
40
42
 
41
- PDK::CLI::Util.ensure_in_module!(
42
- message: _('Code validation can only be run from inside a valid module directory'),
43
- log_level: :info,
44
- )
43
+ PDK::CLI::Util.module_version_check if PDK.context.is_a?(PDK::Context::Module)
45
44
 
46
- PDK::CLI::Util.module_version_check
45
+ # Set the ruby version we're going to use early. Must be set before the validators are created.
46
+ # Note that this is a bit of code-smell and should be fixed
47
+ puppet_env = PDK::CLI::Util.puppet_from_opts_or_env(opts)
48
+ PDK::Util::RubyVersion.use(puppet_env[:ruby_version])
47
49
 
50
+ targets = []
51
+ validators_to_run = nil
48
52
  if args[0]
49
53
  # This may be a single validator, a list of validators, or a target.
50
54
  if Util::OptionValidator.comma_separated_list?(args[0])
51
55
  # This is a comma separated list. Treat each item as a validator.
52
-
53
56
  vals = Util::OptionNormalizer.comma_separated_list_to_array(args[0])
54
- validators = PDK::Validate.validators.select { |v| vals.include?(v.name) }
57
+ validators_to_run = PDK::Validate.validator_names.select { |name| vals.include?(name) }
55
58
 
56
- invalid = vals.reject { |v| validator_names.include?(v) }
57
- invalid.each do |v|
58
- PDK.logger.warn(_("Unknown validator '%{v}'. Available validators: %{validators}.") % { v: v, validators: validator_names.join(', ') })
59
+ vals.reject { |v| PDK::Validate.validator_names.include?(v) }
60
+ .each do |v|
61
+ PDK.logger.warn(_("Unknown validator '%{v}'. Available validators: %{validators}.") % { v: v, validators: PDK::Validate.validator_names.join(', ') })
59
62
  end
60
63
  else
61
64
  # This is a single item. Check if it's a known validator, or otherwise treat it as a target.
62
- val = PDK::Validate.validators.find { |v| args[0] == v.name }
65
+ val = PDK::Validate.validator_names.find { |name| args[0] == name }
63
66
  if val
64
- validators = [val]
67
+ validators_to_run = [val]
65
68
  else
66
69
  targets = [args[0]]
67
70
  # We now know that no validators were passed, so let the user know we're using all of them by default.
@@ -71,11 +74,12 @@ module PDK::CLI
71
74
  else
72
75
  PDK.logger.info(_('Running all available validators...'))
73
76
  end
77
+ validators_to_run = PDK::Validate.validator_names if validators_to_run.nil?
74
78
 
75
- if validators == PDK::Validate.validators
79
+ if validators_to_run.sort == PDK::Validate.validator_names.sort
76
80
  PDK::CLI::Util.analytics_screen_view('validate', opts)
77
81
  else
78
- PDK::CLI::Util.analytics_screen_view(['validate', validators.map(&:name).sort].flatten.join('_'), opts)
82
+ PDK::CLI::Util.analytics_screen_view(['validate', validators_to_run.sort].flatten.join('_'), opts)
79
83
  end
80
84
 
81
85
  # Subsequent arguments are targets.
@@ -93,36 +97,14 @@ module PDK::CLI
93
97
 
94
98
  options = targets.empty? ? {} : { targets: targets }
95
99
  options[:auto_correct] = true if opts[:'auto-correct']
96
-
97
- # Ensure that the bundled gems are up to date and correct Ruby is activated before running any validations.
98
- puppet_env = PDK::CLI::Util.puppet_from_opts_or_env(opts)
99
- PDK::Util::RubyVersion.use(puppet_env[:ruby_version])
100
-
101
100
  options.merge!(puppet_env[:gemset])
102
101
 
102
+ # Ensure that the bundled gems are up to date and correct Ruby is activated before running any validations.
103
+ # Note that if no Gemfile exists, then ensure_bundle! will log a debug message and exit gracefully
103
104
  require 'pdk/util/bundler'
104
-
105
105
  PDK::Util::Bundler.ensure_bundle!(puppet_env[:gemset])
106
106
 
107
- exit_code = 0
108
- if opts[:parallel]
109
- require 'pdk/cli/exec_group'
110
-
111
- exec_group = PDK::CLI::ExecGroup.new(_('Validating module using %{num_of_threads} threads' % { num_of_threads: validators.count }), opts)
112
-
113
- validators.each do |validator|
114
- exec_group.register do
115
- validator.invoke(report, options.merge(exec_group: exec_group))
116
- end
117
- end
118
-
119
- exit_code = exec_group.exit_code
120
- else
121
- validators.each do |validator|
122
- validator_exit_code = validator.invoke(report, options.dup)
123
- exit_code = validator_exit_code if validator_exit_code != 0
124
- end
125
- end
107
+ exit_code, report = PDK::Validate.invoke_validators_by_name(PDK.context, validators_to_run, opts.fetch(:parallel, false), options)
126
108
 
127
109
  report_formats.each do |format|
128
110
  report.send(format[:method], format[:target])
data/lib/pdk/config.rb CHANGED
@@ -2,6 +2,8 @@ require 'pdk'
2
2
 
3
3
  module PDK
4
4
  class Config
5
+ autoload :IniFile, 'pdk/config/ini_file'
6
+ autoload :IniFileSetting, 'pdk/config/ini_file_setting'
5
7
  autoload :JSON, 'pdk/config/json'
6
8
  autoload :JSONSchemaNamespace, 'pdk/config/json_schema_namespace'
7
9
  autoload :JSONSchemaSetting, 'pdk/config/json_schema_setting'
@@ -11,16 +13,50 @@ module PDK
11
13
  autoload :Validator, 'pdk/config/validator'
12
14
  autoload :YAML, 'pdk/config/yaml'
13
15
 
14
- def user
15
- @user ||= PDK::Config::JSON.new('user', file: PDK::Config.user_config_path) do
16
- mount :module_defaults, PDK::Config::JSON.new(file: PDK.answers.answer_file_path)
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'])
17
53
 
18
54
  # Due to the json-schema gem having issues with Windows based paths, and only supporting Draft 05 (or less) do
19
55
  # not use JSON validation yet. Once PDK drops support for EOL rubies, we will be able to use the json_schemer gem
20
56
  # Which has much more modern support
21
57
  # Reference - https://github.com/puppetlabs/pdk/pull/777
22
58
  # Reference - https://tickets.puppetlabs.com/browse/PDK-1526
23
- mount :analytics, PDK::Config::YAML.new(file: PDK::Config.analytics_config_path, persistent_defaults: true) do
59
+ mount :analytics, PDK::Config::YAML.new(file: local_options['user.analytics.path'], persistent_defaults: true) do
24
60
  setting :disabled do
25
61
  validate PDK::Config::Validator.boolean
26
62
  default_to { PDK::Config.bolt_analytics_config.fetch('disabled', true) }
@@ -35,6 +71,29 @@ module PDK
35
71
  end
36
72
  end
37
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
38
97
  end
39
98
  end
40
99
 
@@ -43,7 +102,85 @@ module PDK
43
102
  # @param filter [String] Only resolve setting names which match the filter. See PDK::Config::Namespace.be_resolved? for matching rules
44
103
  # @return [Hash{String => Object}] All resolved settings for example {'user.module_defaults.author' => 'johndoe'}
45
104
  def resolve(filter = nil)
46
- user.resolve(filter)
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])
47
184
  end
48
185
 
49
186
  def self.bolt_analytics_config
@@ -65,6 +202,14 @@ module PDK
65
202
  File.join(PDK::Util.configdir, 'user_config.json')
66
203
  end
67
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
+
68
213
  def self.json_schemas_path
69
214
  File.join(__dir__, 'config')
70
215
  end
@@ -116,12 +261,124 @@ module PDK
116
261
 
117
262
  if answers.nil?
118
263
  PDK.logger.info _('No answer given, opting out of analytics collection.')
119
- PDK.config.user['analytics']['disabled'] = true
264
+ PDK.config.set(%w[user analytics disabled], true)
120
265
  else
121
- PDK.config.user['analytics']['disabled'] = !answers['enabled']
266
+ PDK.config.set(%w[user analytics disabled], !answers['enabled'])
122
267
  end
123
268
 
124
269
  PDK.logger.info(text: post_message, wrap: true)
125
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:
126
383
  end
127
384
  end