pdk 1.9.0 → 3.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (163) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +744 -711
  3. data/README.md +23 -21
  4. data/lib/pdk/answer_file.rb +3 -112
  5. data/lib/pdk/bolt.rb +20 -0
  6. data/lib/pdk/cli/build.rb +51 -54
  7. data/lib/pdk/cli/bundle.rb +33 -29
  8. data/lib/pdk/cli/console.rb +148 -0
  9. data/lib/pdk/cli/convert.rb +46 -37
  10. data/lib/pdk/cli/env.rb +51 -0
  11. data/lib/pdk/cli/errors.rb +4 -3
  12. data/lib/pdk/cli/exec/command.rb +285 -0
  13. data/lib/pdk/cli/exec/interactive_command.rb +109 -0
  14. data/lib/pdk/cli/exec.rb +32 -201
  15. data/lib/pdk/cli/exec_group.rb +79 -43
  16. data/lib/pdk/cli/get/config.rb +26 -0
  17. data/lib/pdk/cli/get.rb +22 -0
  18. data/lib/pdk/cli/new/class.rb +20 -22
  19. data/lib/pdk/cli/new/defined_type.rb +21 -21
  20. data/lib/pdk/cli/new/fact.rb +27 -0
  21. data/lib/pdk/cli/new/function.rb +27 -0
  22. data/lib/pdk/cli/new/module.rb +40 -29
  23. data/lib/pdk/cli/new/provider.rb +18 -18
  24. data/lib/pdk/cli/new/task.rb +23 -22
  25. data/lib/pdk/cli/new/test.rb +52 -0
  26. data/lib/pdk/cli/new/transport.rb +27 -0
  27. data/lib/pdk/cli/new.rb +15 -9
  28. data/lib/pdk/cli/release/prep.rb +39 -0
  29. data/lib/pdk/cli/release/publish.rb +46 -0
  30. data/lib/pdk/cli/release.rb +185 -0
  31. data/lib/pdk/cli/remove/config.rb +83 -0
  32. data/lib/pdk/cli/remove.rb +22 -0
  33. data/lib/pdk/cli/set/config.rb +121 -0
  34. data/lib/pdk/cli/set.rb +22 -0
  35. data/lib/pdk/cli/test/unit.rb +71 -69
  36. data/lib/pdk/cli/test.rb +9 -8
  37. data/lib/pdk/cli/update.rb +38 -21
  38. data/lib/pdk/cli/util/command_redirector.rb +13 -3
  39. data/lib/pdk/cli/util/interview.rb +25 -9
  40. data/lib/pdk/cli/util/option_normalizer.rb +6 -6
  41. data/lib/pdk/cli/util/option_validator.rb +19 -9
  42. data/lib/pdk/cli/util/spinner.rb +13 -0
  43. data/lib/pdk/cli/util/update_manager_printer.rb +82 -0
  44. data/lib/pdk/cli/util.rb +105 -48
  45. data/lib/pdk/cli/validate.rb +96 -111
  46. data/lib/pdk/cli.rb +134 -87
  47. data/lib/pdk/config/errors.rb +5 -0
  48. data/lib/pdk/config/ini_file.rb +184 -0
  49. data/lib/pdk/config/ini_file_setting.rb +35 -0
  50. data/lib/pdk/config/json.rb +35 -0
  51. data/lib/pdk/config/json_schema_namespace.rb +137 -0
  52. data/lib/pdk/config/json_schema_setting.rb +51 -0
  53. data/lib/pdk/config/json_with_schema.rb +47 -0
  54. data/lib/pdk/config/namespace.rb +362 -0
  55. data/lib/pdk/config/setting.rb +134 -0
  56. data/lib/pdk/config/task_schema.json +116 -0
  57. data/lib/pdk/config/validator.rb +31 -0
  58. data/lib/pdk/config/yaml.rb +41 -0
  59. data/lib/pdk/config/yaml_with_schema.rb +51 -0
  60. data/lib/pdk/config.rb +304 -0
  61. data/lib/pdk/context/control_repo.rb +61 -0
  62. data/lib/pdk/context/module.rb +28 -0
  63. data/lib/pdk/context/none.rb +22 -0
  64. data/lib/pdk/context.rb +98 -0
  65. data/lib/pdk/control_repo.rb +89 -0
  66. data/lib/pdk/generate/defined_type.rb +27 -33
  67. data/lib/pdk/generate/fact.rb +26 -0
  68. data/lib/pdk/generate/function.rb +49 -0
  69. data/lib/pdk/generate/module.rb +160 -153
  70. data/lib/pdk/generate/provider.rb +16 -69
  71. data/lib/pdk/generate/puppet_class.rb +27 -32
  72. data/lib/pdk/generate/puppet_object.rb +100 -159
  73. data/lib/pdk/generate/task.rb +34 -51
  74. data/lib/pdk/generate/transport.rb +34 -0
  75. data/lib/pdk/generate.rb +21 -8
  76. data/lib/pdk/logger.rb +24 -6
  77. data/lib/pdk/module/build.rb +125 -37
  78. data/lib/pdk/module/convert.rb +146 -65
  79. data/lib/pdk/module/metadata.rb +72 -71
  80. data/lib/pdk/module/release.rb +255 -0
  81. data/lib/pdk/module/update.rb +48 -37
  82. data/lib/pdk/module/update_manager.rb +75 -39
  83. data/lib/pdk/module.rb +10 -2
  84. data/lib/pdk/monkey_patches.rb +268 -0
  85. data/lib/pdk/report/event.rb +36 -48
  86. data/lib/pdk/report.rb +35 -22
  87. data/lib/pdk/template/fetcher/git.rb +84 -0
  88. data/lib/pdk/template/fetcher/local.rb +29 -0
  89. data/lib/pdk/template/fetcher.rb +100 -0
  90. data/lib/pdk/template/renderer/v1/legacy_template_dir.rb +108 -0
  91. data/lib/pdk/template/renderer/v1/renderer.rb +131 -0
  92. data/lib/pdk/template/renderer/v1/template_file.rb +100 -0
  93. data/lib/pdk/template/renderer/v1.rb +25 -0
  94. data/lib/pdk/template/renderer.rb +97 -0
  95. data/lib/pdk/template/template_dir.rb +67 -0
  96. data/lib/pdk/template.rb +52 -0
  97. data/lib/pdk/tests/unit.rb +101 -51
  98. data/lib/pdk/util/bundler.rb +44 -42
  99. data/lib/pdk/util/changelog_generator.rb +138 -0
  100. data/lib/pdk/util/env.rb +48 -0
  101. data/lib/pdk/util/filesystem.rb +139 -2
  102. data/lib/pdk/util/git.rb +108 -8
  103. data/lib/pdk/util/json_finder.rb +86 -0
  104. data/lib/pdk/util/puppet_strings.rb +125 -0
  105. data/lib/pdk/util/puppet_version.rb +71 -87
  106. data/lib/pdk/util/ruby_version.rb +49 -25
  107. data/lib/pdk/util/template_uri.rb +283 -0
  108. data/lib/pdk/util/vendored_file.rb +34 -42
  109. data/lib/pdk/util/version.rb +11 -10
  110. data/lib/pdk/util/windows/api_types.rb +74 -44
  111. data/lib/pdk/util/windows/file.rb +31 -27
  112. data/lib/pdk/util/windows/process.rb +74 -0
  113. data/lib/pdk/util/windows/string.rb +19 -12
  114. data/lib/pdk/util/windows.rb +2 -0
  115. data/lib/pdk/util.rb +111 -124
  116. data/lib/pdk/validate/control_repo/control_repo_validator_group.rb +23 -0
  117. data/lib/pdk/validate/control_repo/environment_conf_validator.rb +98 -0
  118. data/lib/pdk/validate/external_command_validator.rb +213 -0
  119. data/lib/pdk/validate/internal_ruby_validator.rb +101 -0
  120. data/lib/pdk/validate/invokable_validator.rb +238 -0
  121. data/lib/pdk/validate/metadata/metadata_json_lint_validator.rb +84 -0
  122. data/lib/pdk/validate/metadata/metadata_syntax_validator.rb +76 -0
  123. data/lib/pdk/validate/metadata/metadata_validator_group.rb +20 -0
  124. data/lib/pdk/validate/puppet/puppet_epp_validator.rb +131 -0
  125. data/lib/pdk/validate/puppet/puppet_lint_validator.rb +66 -0
  126. data/lib/pdk/validate/puppet/puppet_plan_syntax_validator.rb +38 -0
  127. data/lib/pdk/validate/puppet/puppet_syntax_validator.rb +135 -0
  128. data/lib/pdk/validate/puppet/puppet_validator_group.rb +22 -0
  129. data/lib/pdk/validate/ruby/ruby_rubocop_validator.rb +79 -0
  130. data/lib/pdk/validate/ruby/ruby_validator_group.rb +19 -0
  131. data/lib/pdk/validate/tasks/tasks_metadata_lint_validator.rb +83 -0
  132. data/lib/pdk/validate/tasks/tasks_name_validator.rb +45 -0
  133. data/lib/pdk/validate/tasks/tasks_validator_group.rb +20 -0
  134. data/lib/pdk/validate/validator.rb +120 -0
  135. data/lib/pdk/validate/validator_group.rb +107 -0
  136. data/lib/pdk/validate/yaml/yaml_syntax_validator.rb +86 -0
  137. data/lib/pdk/validate/yaml/yaml_validator_group.rb +19 -0
  138. data/lib/pdk/validate.rb +86 -12
  139. data/lib/pdk/version.rb +2 -2
  140. data/lib/pdk.rb +60 -10
  141. metadata +138 -100
  142. data/lib/pdk/cli/module/build.rb +0 -14
  143. data/lib/pdk/cli/module/generate.rb +0 -45
  144. data/lib/pdk/cli/module.rb +0 -14
  145. data/lib/pdk/i18n.rb +0 -4
  146. data/lib/pdk/module/templatedir.rb +0 -321
  147. data/lib/pdk/template_file.rb +0 -95
  148. data/lib/pdk/validate/base_validator.rb +0 -215
  149. data/lib/pdk/validate/metadata/metadata_json_lint.rb +0 -86
  150. data/lib/pdk/validate/metadata/metadata_syntax.rb +0 -109
  151. data/lib/pdk/validate/metadata_validator.rb +0 -30
  152. data/lib/pdk/validate/puppet/puppet_lint.rb +0 -67
  153. data/lib/pdk/validate/puppet/puppet_syntax.rb +0 -112
  154. data/lib/pdk/validate/puppet_validator.rb +0 -30
  155. data/lib/pdk/validate/ruby/rubocop.rb +0 -77
  156. data/lib/pdk/validate/ruby_validator.rb +0 -29
  157. data/lib/pdk/validate/tasks/metadata_lint.rb +0 -126
  158. data/lib/pdk/validate/tasks/name.rb +0 -88
  159. data/lib/pdk/validate/tasks_validator.rb +0 -33
  160. data/lib/pdk/validate/yaml/syntax.rb +0 -109
  161. data/lib/pdk/validate/yaml_validator.rb +0 -31
  162. data/locales/config.yaml +0 -21
  163. data/locales/pdk.pot +0 -1291
@@ -0,0 +1,116 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-06/schema#",
3
+ "title": "Puppet Task Metadata",
4
+ "description": "The metadata format for Puppet Tasks",
5
+ "type": "object",
6
+ "properties": {
7
+ "description": {
8
+ "type": "string",
9
+ "description": "A longer description(one paragraph) of how to use the task"
10
+ },
11
+ "puppet_task_version": {
12
+ "type": "integer",
13
+ "description": "The version of this spec used",
14
+ "default": 1
15
+ },
16
+ "supports_noop": {
17
+ "type": "boolean",
18
+ "default": false,
19
+ "description": "This task respects the '_noop' metaparam. If this false or absent the task runner will refuse to run this task if noop is specified."
20
+ },
21
+ "remote": {
22
+ "type": "boolean",
23
+ "default": false,
24
+ "description": "This task is capable of operating on a remote target using connection information in the '_target' metaparam."
25
+ },
26
+ "input_method": {
27
+ "type": "string",
28
+ "enum": ["stdin", "environment", "both", "powershell"],
29
+ "description": "What input method should be used to pass params to the task"
30
+ },
31
+ "parameters": {
32
+ "$ref": "#/definitions/paramsObject",
33
+ "description": "An object mapping valid parameter names to corresponding json-schemas"
34
+ },
35
+ "implementations": {
36
+ "type": "array",
37
+ "items": {
38
+ "type": "object",
39
+ "required": ["name"],
40
+ "properties": {
41
+ "name": {
42
+ "type": "string",
43
+ "description": "Name of task executable file"
44
+ },
45
+ "requirements": {
46
+ "type": "array",
47
+ "additionalItems": {
48
+ "type": "string"
49
+ },
50
+ "description": "Features required on target to execute task"
51
+ },
52
+ "files": {
53
+ "type": "array",
54
+ "additionalItems": {
55
+ "type": "string"
56
+ },
57
+ "description": "File resources required by task"
58
+ }
59
+ }
60
+ },
61
+ "description": "Rules for selecting implementation resources based on features available on target"
62
+ },
63
+ "files": {
64
+ "type": "array",
65
+ "items": {
66
+ "type": "string"
67
+ },
68
+ "description": "Path to file resources saved in valid module directory to be provided to task"
69
+ },
70
+ "private": {
71
+ "type": "boolean",
72
+ "description": "Should this task appear by default in UI lists of tasks"
73
+ },
74
+ "extensions": {
75
+ "type": "object",
76
+ "description": "Task Runner specific metadata extensions",
77
+ "items": {
78
+ "type": "object"
79
+ }
80
+ }
81
+ },
82
+ "definitions": {
83
+ "parameterName": {
84
+ "description": "Valid names for parameter keys",
85
+ "type": "string",
86
+ "pattern": "^[a-z][a-z0-9_]*$"
87
+ },
88
+ "paramsObject": {
89
+ "type": "object",
90
+ "description": "An object with restricted keys and enumData support",
91
+ "propertyNames": {"$ref": "#/definitions/parameterName"},
92
+ "additionalProperties": {
93
+ "type": "object",
94
+ "description": "Extend Normal JSON schema to require an object and describe 'enumData' to map enum values to descriptions",
95
+ "properties": {
96
+ "description": {
97
+ "description": "A description of the parameter",
98
+ "type": "string"
99
+ },
100
+ "type": {
101
+ "description": "A puppet type string that describes a data type that will match the parameter value",
102
+ "type": "string"
103
+ },
104
+ "sensitive": {
105
+ "description": "Whether the task runner should treat the parameter value as sensitive",
106
+ "type": "boolean",
107
+ "default": false
108
+ },
109
+ "default": {
110
+ "description": "The default value to pass to the task implementation if the parameter isn't provided"
111
+ }
112
+ }
113
+ }
114
+ }
115
+ }
116
+ }
@@ -0,0 +1,31 @@
1
+ module PDK
2
+ class Config
3
+ # A collection of predefined validators for use with {PDK::Config::Value}.
4
+ #
5
+ # @example
6
+ # value :enabled do
7
+ # validate PDK::Config::Validator.boolean
8
+ # end
9
+ module Validator
10
+ # @return [Hash{Symbol => [Proc,String]}] a {PDK::Config::Value}
11
+ # validator that ensures that the value is either a TrueClass or
12
+ # FalseClass.
13
+ def self.boolean
14
+ {
15
+ proc: ->(value) { [true, false].include?(value) },
16
+ message: 'must be a boolean: true or false'
17
+ }
18
+ end
19
+
20
+ # @return [Hash{Symbol => [Proc,String]}] a {PDK::Config::Value}
21
+ # validator that ensures that the value is a String that matches the
22
+ # regex for a version 4 UUID.
23
+ def self.uuid
24
+ {
25
+ proc: ->(value) { value.match(/\A\h{8}(?:-\h{4}){3}-\h{12}\z/) },
26
+ message: 'must be a version 4 UUID'
27
+ }
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,41 @@
1
+ require 'pdk'
2
+
3
+ module PDK
4
+ class Config
5
+ # Parses a YAML document.
6
+ #
7
+ # @see PDK::Config::Namespace.parse_file
8
+ class YAML < Namespace
9
+ def parse_file(filename)
10
+ raise unless block_given?
11
+
12
+ data = load_data(filename)
13
+ return if data.nil? || data.empty?
14
+
15
+ require 'yaml'
16
+
17
+ data = if Gem::Version.new(Psych::VERSION) >= Gem::Version.new('3.1.0.pre1')
18
+ ::YAML.safe_load(data, permitted_classes: [Symbol], permitted_symbols: [], aliases: true)
19
+ else
20
+ ::YAML.safe_load(data, [Symbol], [], true)
21
+ end
22
+ return if data.nil?
23
+
24
+ data.each { |k, v| yield k, PDK::Config::Setting.new(k, self, v) }
25
+ rescue Psych::SyntaxError => e
26
+ raise PDK::Config::LoadError, format('Syntax error when loading %{file}: %{error}', file: filename, error: "#{e.problem} #{e.context}")
27
+ rescue Psych::DisallowedClass => e
28
+ raise PDK::Config::LoadError, format('Unsupported class in %{file}: %{error}', file: filename, error: e.message)
29
+ end
30
+
31
+ # Serializes object data into a YAML string.
32
+ #
33
+ # @see PDK::Config::Namespace.serialize_data
34
+ def serialize_data(data)
35
+ require 'yaml'
36
+
37
+ ::YAML.dump(data)
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,51 @@
1
+ require 'pdk'
2
+
3
+ module PDK
4
+ class Config
5
+ # Parses a YAML document with a JSON schema.
6
+ #
7
+ # @see PDK::Config::Namespace.parse_file
8
+ class YAMLWithSchema < JSONSchemaNamespace
9
+ def parse_file(filename)
10
+ raise unless block_given?
11
+
12
+ data = load_data(filename)
13
+ data = '' if data.nil?
14
+ require 'yaml'
15
+ require 'json-schema'
16
+
17
+ @raw_data = ::YAML.safe_load(data, [Symbol], [], true)
18
+ @raw_data = {} if @raw_data.nil?
19
+
20
+ begin
21
+ # Ensure the parsed document is actually valid
22
+ validate_document!(@raw_data)
23
+ rescue ::JSON::Schema::ValidationError => e
24
+ raise PDK::Config::LoadError, format('The configuration file %{filename} is not valid: %{message}', filename: filename, message: e.message)
25
+ end
26
+
27
+ require 'pdk/config/json_schema_setting'
28
+
29
+ schema_property_names.each do |key|
30
+ yield key, PDK::Config::JSONSchemaSetting.new(key, self, @raw_data[key])
31
+ end
32
+
33
+ # Remove all of the "known" settings from the schema and
34
+ # we're left with the settings that we don't manage.
35
+ self.unmanaged_settings = @raw_data.reject { |k, _| schema_property_names.include?(k) }
36
+ rescue Psych::SyntaxError => e
37
+ raise PDK::Config::LoadError, format('Syntax error when loading %{file}: %{error}', file: filename, error: "#{e.problem} #{e.context}")
38
+ rescue Psych::DisallowedClass => e
39
+ raise PDK::Config::LoadError, format('Unsupported class in %{file}: %{error}', file: filename, error: e.message)
40
+ end
41
+
42
+ # Serializes object data into a YAML string.
43
+ #
44
+ # @see PDK::Config::Namespace.serialize_data
45
+ def serialize_data(data)
46
+ require 'yaml'
47
+ ::YAML.dump(data)
48
+ end
49
+ end
50
+ end
51
+ end
data/lib/pdk/config.rb ADDED
@@ -0,0 +1,304 @@
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 [PDK::Context::AbstractContext] 'context' The context that the configuration should be created in
23
+ def initialize(options = nil)
24
+ options = {} if options.nil?
25
+ @config_options = {
26
+ 'system.path' => PDK::Config.system_config_path,
27
+ 'system.module_defaults.path' => PDK::Config.system_answers_path,
28
+ 'user.path' => PDK::Config.user_config_path,
29
+ 'user.module_defaults.path' => PDK::AnswerFile.default_answer_file_path,
30
+ 'context' => PDK.context
31
+ }.merge(options)
32
+ end
33
+
34
+ # The system level configuration settings.
35
+ # @return [PDK::Config::Namespace]
36
+ # @api private
37
+ def system_config
38
+ local_options = @config_options
39
+ @system_config ||= PDK::Config::JSON.new('system', file: local_options['system.path']) do
40
+ mount :module_defaults, PDK::Config::JSON.new(file: local_options['system.module_defaults.path'])
41
+ end
42
+ end
43
+
44
+ # The user level configuration settings.
45
+ # @return [PDK::Config::Namespace]
46
+ # @api private
47
+ def user_config
48
+ local_options = @config_options
49
+ @user_config ||= PDK::Config::JSON.new('user', file: local_options['user.path']) do
50
+ mount :module_defaults, PDK::Config::JSON.new(file: local_options['user.module_defaults.path'])
51
+
52
+ # Display the feature flags
53
+ mount :pdk_feature_flags, PDK::Config::Namespace.new('pdk_feature_flags') do
54
+ setting 'available' do
55
+ default_to { PDK.available_feature_flags }
56
+ end
57
+
58
+ setting 'requested' do
59
+ default_to { PDK.requested_feature_flags }
60
+ end
61
+ end
62
+ end
63
+ end
64
+
65
+ # The project level configuration settings.
66
+ # @return [PDK::Config::Namespace]
67
+ # @api private
68
+ def project_config
69
+ context = @config_options['context']
70
+ @project ||= PDK::Config::Namespace.new('project') do
71
+ mount :environment, PDK::ControlRepo.environment_conf_as_config(File.join(context.root_path, 'environment.conf')) if context.is_a?(PDK::Context::ControlRepo)
72
+
73
+ mount :validate, PDK::Config::YAML.new('validate', file: File.join(context.root_path, 'pdk.yaml'), persistent_defaults: true) do
74
+ setting 'ignore' do
75
+ default_to { [] }
76
+ end
77
+ end
78
+ end
79
+ end
80
+
81
+ # Resolves *all* filtered settings from all namespaces
82
+ #
83
+ # @param filter [String] Only resolve setting names which match the filter. See PDK::Config::Namespace.be_resolved? for matching rules
84
+ # @return [Hash{String => Object}] All resolved settings for example {'user.module_defaults.author' => 'johndoe'}
85
+ def resolve(filter = nil)
86
+ all_scopes.values.reverse.reduce({}) do |result, method_name|
87
+ result.merge(send(method_name).resolve(filter))
88
+ end
89
+ end
90
+
91
+ # Returns a configuration setting by name. This name can either be a String, Array or parameters e.g. These are equivalent
92
+ # - PDK.config.get('user.a.b.c')
93
+ # - PDK.config.get(['user', 'a', 'b', 'c'])
94
+ # - PDK.config.get('user', 'a', 'b', 'c')
95
+ # @param root [Array[String], String] The root setting name or the entire setting name as a single string
96
+ # @param keys [String] The child names of the setting
97
+ # @return [PDK::Config::Namespace, Object, nil] The value of the configuration setting. Returns nil if it does no exist
98
+ def get(root, *keys)
99
+ return nil if root.nil? || root.empty?
100
+
101
+ if keys.empty?
102
+ case root
103
+ when Array
104
+ name = root
105
+ when String
106
+ name = split_key_string(root)
107
+ else
108
+ return nil
109
+ end
110
+ else
111
+ name = [root].concat(keys)
112
+ end
113
+
114
+ get_within_scopes(name[1..], [name[0]])
115
+ end
116
+
117
+ # 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
118
+ # @setting_name [String, Array[String]] The setting name to retrieve without the leading scope name e.g. Use 'setting' instead of 'system.setting'
119
+ # @scopes [Nil, Array[String]] The list of scopes, in order, to query in turn for the setting_name. Invalid or missing scopes are ignored.
120
+ # @return [PDK::Config::Namespace, Object, nil] The value of the configuration setting. Returns nil if it does no exist
121
+ def get_within_scopes(setting_name, scopes = nil)
122
+ raise ArgumentError, format('Expected an Array but got \'%{klass}\' for scopes', klass: scopes.class) unless scopes.nil? || scopes.is_a?(Array)
123
+ raise ArgumentError, format('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)
124
+
125
+ setting_arr = setting_name.is_a?(String) ? split_key_string(setting_name) : setting_name
126
+ all_scope_names = all_scopes.keys
127
+
128
+ # Use only valid scope names
129
+ scopes = scopes.nil? ? all_scope_names : scopes & all_scope_names
130
+
131
+ scopes.each do |scope_name|
132
+ value = traverse_object(send(all_scopes[scope_name]), *setting_arr)
133
+ return value unless value.nil?
134
+ end
135
+ nil
136
+ end
137
+
138
+ # 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
139
+ # @setting_name [String, Array[String]] The setting name to retrieve without the leading scope name e.g. Use 'setting' instead of 'system.setting'
140
+ # @scopes [Nil, Array[String]] The list of scopes, in order, to query in turn for the setting_name. Invalid or missing scopes are ignored.
141
+ # @yield [PDK::Config::Namespace, Object] The value of the configuration setting. Does not yield if the setting does not exist or is nil
142
+ def with_scoped_value(setting_name, scopes = nil)
143
+ raise ArgumentError, 'must be passed a block' unless block_given?
144
+
145
+ value = get_within_scopes(setting_name, scopes)
146
+ yield value unless value.nil?
147
+ end
148
+
149
+ # Sets a configuration setting by name. This name can either be a String or an Array
150
+ # - PDK.config.set('user.a.b.c', ...)
151
+ # - PDK.config.set(['user', 'a', 'b', 'c'], ...)
152
+ # @param key [String, Array[String]] The name of the configuration key to change
153
+ # @param value [Object] The value to set the configuration setting to
154
+ # @param options [Hash] Changes the behaviour of the setting process
155
+ # @option options [Boolean] :force Disables any munging or array processing, and sets the value as it is. Default is false
156
+ # @return [Object] The new value of the configuration setting
157
+ def set(key, value, options = {})
158
+ options = {
159
+ force: false
160
+ }.merge(options)
161
+
162
+ names = key.is_a?(String) ? split_key_string(key) : key
163
+ raise ArgumentError, 'Invalid configuration names' if names.nil? || !names.is_a?(Array) || names.empty?
164
+
165
+ scope_name = names[0]
166
+ raise ArgumentError, format("Unknown configuration root '%{name}'", name: scope_name) if all_scopes[scope_name].nil?
167
+
168
+ deep_set_object(value, options[:force], send(all_scopes[scope_name]), *names[1..])
169
+ end
170
+
171
+ def self.user_config_path
172
+ File.join(PDK::Util.configdir, 'user_config.json')
173
+ end
174
+
175
+ def self.system_config_path
176
+ File.join(PDK::Util.system_configdir, 'system_config.json')
177
+ end
178
+
179
+ def self.system_answers_path
180
+ File.join(PDK::Util.system_configdir, 'answers.json')
181
+ end
182
+
183
+ def self.json_schemas_path
184
+ File.join(__dir__, 'config')
185
+ end
186
+
187
+ # return nil if not exist
188
+ def self.json_schema(name)
189
+ File.join(json_schemas_path, "#{name}_schema.json")
190
+ end
191
+
192
+ private
193
+
194
+ # :nocov: This is a private method and is tested elsewhere
195
+ def traverse_object(object, *names)
196
+ return nil if object.nil? || !object.respond_to?(:[])
197
+ return nil if names.nil?
198
+ # It's possible to pass in empty names at the root traversal layer
199
+ # but this should _only_ happen at the root namespace level
200
+ if names.empty?
201
+ return (object.is_a?(PDK::Config::Namespace) ? object : nil)
202
+ end
203
+
204
+ name = names.shift
205
+ value = object[name]
206
+ if names.empty?
207
+ return value if value.is_a?(PDK::Config::Namespace)
208
+
209
+ # Duplicate arrays and hashes so that they are isolated from changes being made
210
+ value.is_a?(Hash) || value.is_a?(Array) ? value.dup : value
211
+ else
212
+ traverse_object(value, *names)
213
+ end
214
+ end
215
+ # :nocov:
216
+
217
+ # :nocov: This is a private method and is tested elsewhere
218
+ # Takes a string representation of a setting and splits into its constituent setting parts e.g.
219
+ # 'user.a.b.c' becomes ['user', 'a', 'b', 'c']
220
+ # @return [Array[String]] The string split into each setting name as an array
221
+ def split_key_string(key)
222
+ raise ArgumentError, format('Expected a String but got \'%{klass}\'', klass: key.class) unless key.is_a?(String)
223
+
224
+ key.split('.')
225
+ end
226
+ # :nocov:
227
+
228
+ # :nocov:
229
+ # Returns all known scope names and their associated method name to call, to query the scope
230
+ # Note - Order is important. This dictates the resolution precedence order (topmost is processed first)
231
+ # @return [Hash[String, Symbol]] A hash of the scope name then method name to call to query the scope (as a Symbol)
232
+ def all_scopes
233
+ # Note - Order is important. This dictates the resolution precedence order (topmost is processed first)
234
+ {
235
+ 'project' => :project_config,
236
+ 'user' => :user_config,
237
+ 'system' => :system_config
238
+ }.freeze
239
+ end
240
+
241
+ # :nocov: This is a private method and is tested elsewhere
242
+ # Deeply traverses an object tree via `[]` and sets the last
243
+ # element to the value specified.
244
+ #
245
+ # Creating any missing parent hashes during the traversal
246
+ def deep_set_object(value, force, namespace, *names)
247
+ raise ArgumentError, 'Missing or invalid namespace' unless namespace.is_a?(PDK::Config::Namespace)
248
+ raise ArgumentError, 'Missing a name to set' if names.nil? || names.empty?
249
+
250
+ name = names.shift
251
+ current_value = namespace[name]
252
+
253
+ # If the next thing in the traversal chain is another namespace, set the value using that child namespace.
254
+ return deep_set_object(value, force, current_value, *names) if current_value.is_a?(PDK::Config::Namespace)
255
+
256
+ # We're at the end of the name traversal
257
+ if names.empty?
258
+ if force || !current_value.is_a?(Array)
259
+ namespace[name] = value
260
+ return value
261
+ end
262
+
263
+ # Arrays are a special case if we're not forcing the value
264
+ namespace[name] = current_value << value unless current_value.include?(value)
265
+ return value
266
+ end
267
+
268
+ # Need to generate a deep hash using the current remaining names
269
+ # So given an origin *names of ['a', 'b', 'c', 'd'] and a value 'foo',
270
+ # we eventually want a hash of `{"b"=>{"c"=>{"d"=>"foo"}}}`
271
+ #
272
+ # The code above has already shifted the first element so we currently have
273
+ # name : 'a'
274
+ # names: ['b', 'c', 'd']
275
+ #
276
+ #
277
+ # First we need to pop off the last element ('d') in this case as we need to set that in the `reduce` call below
278
+ # So now we have:
279
+ # name : 'a'
280
+ # names: ['b', 'c']
281
+ # last_name : 'd'
282
+ last_name = names.pop
283
+ # Using reduce and an accumulator, we create the nested hash from the deepest value first. In this case the deepest value
284
+ # is the last_name, so the starting condition is {"d"=>"foo"}
285
+ # After the first iteration ('c'), the accumulator has {"c"=>{"d"=>"foo"}}}
286
+ # After the last iteration ('b'), the accumulator has {"b"=>{"c"=>{"d"=>"foo"}}}
287
+ hash_value = names.reverse.reduce(last_name => value) { |accumulator, item| { item => accumulator } }
288
+
289
+ # If the current value is nil, then it can't be a namespace or an existing value
290
+ # or
291
+ # If the current value is not a Hash and are forcing the change.
292
+ if current_value.nil? || (force && !current_value.is_a?(Hash))
293
+ namespace[name] = hash_value
294
+ return value
295
+ end
296
+
297
+ raise ArgumentError, format("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)
298
+
299
+ namespace[name] = current_value.merge(hash_value)
300
+ value
301
+ end
302
+ # :nocov:
303
+ end
304
+ end
@@ -0,0 +1,61 @@
1
+ require 'pdk'
2
+
3
+ module PDK
4
+ module Context
5
+ # Represents a context for a directory based Control Repository
6
+ class ControlRepo < PDK::Context::AbstractContext
7
+ # @param repo_root [String] The root path for the control repo.
8
+ # @param context_path [String] The path where this context was created from e.g. Dir.pwd
9
+ # @see PDK::Context::AbstractContext
10
+ def initialize(repo_root, context_path)
11
+ super(context_path)
12
+ @root_path = repo_root
13
+ @environment_conf = nil
14
+ end
15
+
16
+ # @see PDK::Context::AbstractContext.pdk_compatible?
17
+ def pdk_compatible?
18
+ # Currently there is nothing to determine compatibility with the PDK for a
19
+ # Control Repo. For now assume everything is compatible
20
+ true
21
+ end
22
+
23
+ # The modulepath setting for this control repository as an array of strings. These paths are relative
24
+ # and may contain interpolation strings (e.g. $basemodulepath)
25
+ # @see https://puppet.com/docs/puppet/latest/config_file_environment.html#allowed-settings
26
+ # @return [Array[String]] The modulepath setting for this control repository
27
+ def module_paths
28
+ return @module_paths unless @module_paths.nil?
29
+
30
+ value = environment_conf['modulepath'] || ''
31
+ # We have to use a hardcoded value here because File::PATH_SEPARATOR is ';' on Windows.
32
+ # As the environment.conf is only used on Puppet Server, it's always ':'
33
+ # Based on - https://github.com/puppetlabs/puppet/blob/f3e6d7e6d87f46408943a8e2176afb82ff6ea096/lib/puppet/settings/environment_conf.rb#L98-L106
34
+ @module_paths = value.split(':')
35
+ end
36
+
37
+ # The relative module_paths that exist on disk.
38
+ # @see https://puppet.com/docs/puppet/latest/config_file_environment.html#allowed-settings
39
+ # @return [Array[String]] The relative module paths on disk
40
+ def actualized_module_paths
41
+ @actualized_module_paths ||= module_paths.reject { |path| path.start_with?('$') }
42
+ .select { |path| PDK::Util::Filesystem.directory?(PDK::Util::Filesystem.expand_path(File.join(root_path, path))) }
43
+ end
44
+
45
+ # :nocov:
46
+ # @see PDK::Context::AbstractContext.display_name
47
+ def display_name
48
+ 'a Control Repository context'
49
+ end
50
+ # :nocov:
51
+
52
+ private
53
+
54
+ # Memoization helper to read the environment.conf file.
55
+ # @return [PDK::Config::IniFile]
56
+ def environment_conf
57
+ @environment_conf ||= PDK::ControlRepo.environment_conf_as_config(File.join(root_path, 'environment.conf'))
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,28 @@
1
+ require 'pdk'
2
+
3
+ module PDK
4
+ module Context
5
+ # Represents a context for a Puppet Module
6
+ class Module < PDK::Context::AbstractContext
7
+ # @param module_root [String] The root path for the module.
8
+ # @param context_path [String] The path where this context was created from e.g. Dir.pwd
9
+ # @see PDK::Context::AbstractContext
10
+ def initialize(module_root, context_path)
11
+ super(context_path)
12
+ @root_path = module_root
13
+ end
14
+
15
+ # @see PDK::Context::AbstractContext.pdk_compatible?
16
+ def pdk_compatible?
17
+ PDK::Util.module_pdk_compatible?(root_path)
18
+ end
19
+
20
+ # :nocov:
21
+ # @see PDK::Context::AbstractContext.display_name
22
+ def display_name
23
+ 'a Puppet Module context'
24
+ end
25
+ # :nocov:
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,22 @@
1
+ require 'pdk'
2
+
3
+ module PDK
4
+ module Context
5
+ # Represents a context which the PDK does not know. For example
6
+ # an empty directory
7
+ class None < PDK::Context::AbstractContext
8
+ # :nocov:
9
+ # @see PDK::Context::AbstractContext.display_name
10
+ def display_name
11
+ 'an unknown context'
12
+ end
13
+ # :nocov:
14
+
15
+ # @see PDK::Context::AbstractContext.parent_context
16
+ def parent_context
17
+ # An unknown context has no parent
18
+ nil
19
+ end
20
+ end
21
+ end
22
+ end