pdk 1.10.0 → 1.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +50 -1
  3. data/lib/pdk.rb +16 -1
  4. data/lib/pdk/analytics.rb +28 -0
  5. data/lib/pdk/analytics/client/google_analytics.rb +138 -0
  6. data/lib/pdk/analytics/client/noop.rb +23 -0
  7. data/lib/pdk/analytics/util.rb +17 -0
  8. data/lib/pdk/cli.rb +37 -0
  9. data/lib/pdk/cli/build.rb +2 -0
  10. data/lib/pdk/cli/bundle.rb +2 -1
  11. data/lib/pdk/cli/convert.rb +2 -0
  12. data/lib/pdk/cli/exec.rb +28 -1
  13. data/lib/pdk/cli/new/class.rb +2 -0
  14. data/lib/pdk/cli/new/defined_type.rb +2 -0
  15. data/lib/pdk/cli/new/module.rb +2 -0
  16. data/lib/pdk/cli/new/provider.rb +2 -0
  17. data/lib/pdk/cli/new/task.rb +2 -0
  18. data/lib/pdk/cli/test.rb +0 -1
  19. data/lib/pdk/cli/test/unit.rb +13 -10
  20. data/lib/pdk/cli/update.rb +21 -0
  21. data/lib/pdk/cli/util.rb +35 -0
  22. data/lib/pdk/cli/util/interview.rb +7 -1
  23. data/lib/pdk/cli/validate.rb +9 -2
  24. data/lib/pdk/config.rb +94 -0
  25. data/lib/pdk/config/errors.rb +5 -0
  26. data/lib/pdk/config/json.rb +23 -0
  27. data/lib/pdk/config/namespace.rb +273 -0
  28. data/lib/pdk/config/validator.rb +31 -0
  29. data/lib/pdk/config/value.rb +94 -0
  30. data/lib/pdk/config/yaml.rb +31 -0
  31. data/lib/pdk/generate/module.rb +3 -2
  32. data/lib/pdk/logger.rb +21 -1
  33. data/lib/pdk/module/build.rb +58 -0
  34. data/lib/pdk/module/convert.rb +1 -1
  35. data/lib/pdk/module/metadata.rb +1 -0
  36. data/lib/pdk/module/templatedir.rb +24 -5
  37. data/lib/pdk/module/update_manager.rb +2 -2
  38. data/lib/pdk/report/event.rb +3 -3
  39. data/lib/pdk/template_file.rb +1 -1
  40. data/lib/pdk/tests/unit.rb +10 -12
  41. data/lib/pdk/util.rb +9 -0
  42. data/lib/pdk/util/bundler.rb +5 -9
  43. data/lib/pdk/util/filesystem.rb +37 -0
  44. data/lib/pdk/util/puppet_version.rb +1 -1
  45. data/lib/pdk/util/ruby_version.rb +16 -6
  46. data/lib/pdk/util/template_uri.rb +72 -43
  47. data/lib/pdk/util/version.rb +1 -1
  48. data/lib/pdk/util/windows.rb +1 -0
  49. data/lib/pdk/util/windows/api_types.rb +0 -7
  50. data/lib/pdk/util/windows/file.rb +1 -1
  51. data/lib/pdk/util/windows/string.rb +1 -1
  52. data/lib/pdk/validate/base_validator.rb +8 -6
  53. data/lib/pdk/validate/puppet/puppet_syntax.rb +1 -1
  54. data/lib/pdk/validate/ruby/rubocop.rb +1 -1
  55. data/lib/pdk/version.rb +1 -1
  56. data/locales/pdk.pot +223 -114
  57. metadata +103 -50
@@ -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(%r{\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,94 @@
1
+ module PDK
2
+ class Config
3
+ # A class for describing the value of a {PDK::Config} setting.
4
+ #
5
+ # Generally, this is never instantiated manually, but is instead
6
+ # instantiated by passing a block to {PDK::Config::Namespace#value}.
7
+ #
8
+ # @example
9
+ #
10
+ # PDK::Config::Namespace.new('analytics') do
11
+ # value :disabled do
12
+ # validate PDK::Config::Validator.boolean
13
+ # default_to { false }
14
+ # end
15
+ # end
16
+ class Value
17
+ # Initialises an empty value definition.
18
+ #
19
+ # @param name [String,Symbol] the name of the value.
20
+ def initialize(name)
21
+ @name = name
22
+ @validators = []
23
+ end
24
+
25
+ # Assign a validator to the value.
26
+ #
27
+ # @param validator [Hash{Symbol => [Proc,String]}]
28
+ # @option validator [Proc] :proc a lambda that takes the value to be
29
+ # validated as the argument and returns `true` if the value is valid.
30
+ # @option validator [String] :message a description of what the validator
31
+ # is testing for, that is displayed to the user as part of the error
32
+ # message for invalid values.
33
+ #
34
+ # @raise [ArgumentError] if not passed a Hash.
35
+ # @raise [ArgumentError] if the Hash doesn't have a `:proc` key that
36
+ # contains a Proc.
37
+ # @raise [ArgumentError] if the Hash doesn't have a `:message` key that
38
+ # contains a String.
39
+ #
40
+ # @return [nil]
41
+ def validate(validator)
42
+ raise ArgumentError, _('validator must be a Hash') unless validator.is_a?(Hash)
43
+ raise ArgumentError, _('the :proc key must contain a Proc') unless validator.key?(:proc) && validator[:proc].is_a?(Proc)
44
+ raise ArgumentError, _('the :message key must contain a String') unless validator.key?(:message) && validator[:message].is_a?(String)
45
+
46
+ @validators << validator
47
+ end
48
+
49
+ # Validate a value against the assigned validators.
50
+ #
51
+ # @param key [String] the name of the value being validated.
52
+ # @param value [Object] the value being validated.
53
+ #
54
+ # @raise [ArgumentError] if any of the assigned validators fail to
55
+ # validate the value.
56
+ #
57
+ # @return [nil]
58
+ def validate!(key, value)
59
+ @validators.each do |validator|
60
+ next if validator[:proc].call(value)
61
+
62
+ raise ArgumentError, _('%{key} %{message}') % {
63
+ key: key,
64
+ message: validator[:message],
65
+ }
66
+ end
67
+ end
68
+
69
+ # Assign a default value.
70
+ #
71
+ # @param block [Proc] a block that is lazy evaluated when necessary in
72
+ # order to determine the default value.
73
+ #
74
+ # @return [nil]
75
+ def default_to(&block)
76
+ raise ArgumentError, _('must be passed a block') unless block_given?
77
+ @default_to = block
78
+ end
79
+
80
+ # Evaluate the default value block.
81
+ #
82
+ # @return [Object,nil] the result of evaluating the block given to
83
+ # {#default_to}, or `nil` if the value has no default.
84
+ def default
85
+ default? ? @default_to.call : nil
86
+ end
87
+
88
+ # @return [Boolean] true if the value has a default value block.
89
+ def default?
90
+ !@default_to.nil?
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,31 @@
1
+ require 'pdk/config/namespace'
2
+
3
+ module PDK
4
+ class Config
5
+ class YAML < Namespace
6
+ def parse_data(data, filename)
7
+ return {} if data.nil? || data.empty?
8
+
9
+ require 'yaml'
10
+
11
+ ::YAML.safe_load(data, [Symbol], [], true)
12
+ rescue Psych::SyntaxError => e
13
+ raise PDK::Config::LoadError, _('Syntax error when loading %{file}: %{error}') % {
14
+ file: filename,
15
+ error: "#{e.problem} #{e.context}",
16
+ }
17
+ rescue Psych::DisallowedClass => e
18
+ raise PDK::Config::LoadError, _('Unsupported class in %{file}: %{error}') % {
19
+ file: filename,
20
+ error: e.message,
21
+ }
22
+ end
23
+
24
+ def serialize_data(data)
25
+ require 'yaml'
26
+
27
+ ::YAML.dump(data)
28
+ end
29
+ end
30
+ end
31
+ end
@@ -124,7 +124,7 @@ module PDK
124
124
  defaults['name'] = "#{opts[:username]}-#{opts[:module_name]}" unless opts[:module_name].nil?
125
125
  defaults['author'] = PDK.answers['author'] unless PDK.answers['author'].nil?
126
126
  defaults['license'] = PDK.answers['license'] unless PDK.answers['license'].nil?
127
- defaults['license'] = opts[:license] if opts.key? :license
127
+ defaults['license'] = opts[:license] if opts.key?(:license)
128
128
 
129
129
  metadata = PDK::Module::Metadata.new(defaults)
130
130
  module_interview(metadata, opts) unless opts[:'skip-interview']
@@ -199,6 +199,7 @@ module PDK
199
199
  question: _('What operating systems does this module support?'),
200
200
  help: _('Use the up and down keys to move between the choices, space to select and enter to continue.'),
201
201
  required: true,
202
+ type: :multi_select,
202
203
  choices: PDK::Module::Metadata::OPERATING_SYSTEMS,
203
204
  default: PDK::Module::Metadata::DEFAULT_OPERATING_SYSTEMS.map do |os_name|
204
205
  # tty-prompt uses a 1-index
@@ -252,7 +253,7 @@ module PDK
252
253
  else
253
254
  questions.reject! { |q| q[:name] == 'module_name' } if opts.key?(:module_name)
254
255
  questions.reject! { |q| q[:name] == 'license' } if opts.key?(:license)
255
- questions.reject! { |q| q[:forge_only] } unless opts.key?(:'full-interview')
256
+ questions.reject! { |q| q[:forge_only] } unless opts[:'full-interview']
256
257
  end
257
258
 
258
259
  interview.add_questions(questions)
data/lib/pdk/logger.rb CHANGED
@@ -6,17 +6,37 @@ module PDK
6
6
  end
7
7
 
8
8
  class Logger < ::Logger
9
+ WRAP_COLUMN_LIMIT = 78
10
+
9
11
  def initialize
10
12
  super(STDERR)
13
+ @sent_messages = {}
11
14
 
12
15
  # TODO: Decide on output format.
13
16
  self.formatter = proc do |severity, _datetime, _progname, msg|
14
- "pdk (#{severity}): #{msg}\n"
17
+ prefix = "pdk (#{severity}): "
18
+ if msg.is_a?(Hash)
19
+ if msg.fetch(:wrap, false)
20
+ wrap_pattern = %r{(.{1,#{WRAP_COLUMN_LIMIT - prefix.length}})(\s+|\Z)}
21
+ "#{prefix}#{msg[:text].gsub(wrap_pattern, "\\1\n#{' ' * prefix.length}")}\n"
22
+ else
23
+ "#{prefix}#{msg[:text]}\n"
24
+ end
25
+ else
26
+ "#{prefix}#{msg}\n"
27
+ end
15
28
  end
16
29
 
17
30
  self.level = ::Logger::INFO
18
31
  end
19
32
 
33
+ def warn_once(*args)
34
+ hash = args.inspect.hash
35
+ return if (@sent_messages[::Logger::WARN] ||= {}).key?(hash)
36
+ @sent_messages[::Logger::WARN][hash] = true
37
+ warn(*args)
38
+ end
39
+
20
40
  def enable_debug_output
21
41
  self.level = ::Logger::DEBUG
22
42
  end
@@ -120,8 +120,14 @@ module PDK
120
120
  elsif File.symlink?(path)
121
121
  warn_symlink(path)
122
122
  else
123
+ validate_ustar_path!(relative_path.to_path)
123
124
  FileUtils.cp(path, dest_path, preserve: true)
124
125
  end
126
+ rescue ArgumentError => e
127
+ raise PDK::CLI::ExitWithError, _(
128
+ '%{message} Please rename the file or exclude it from the package ' \
129
+ 'by adding it to the .pdkignore file in your module.',
130
+ ) % { message: e.message }
125
131
  end
126
132
 
127
133
  # Check if the given path matches one of the patterns listed in the
@@ -152,6 +158,58 @@ module PDK
152
158
  }
153
159
  end
154
160
 
161
+ # Checks if the path length will fit into the POSIX.1-1998 (ustar) tar
162
+ # header format.
163
+ #
164
+ # POSIX.1-2001 (which allows paths of infinite length) was adopted by GNU
165
+ # tar in 2004 and is supported by minitar 0.7 and above. Unfortunately
166
+ # much of the Puppet ecosystem still uses minitar 0.6.1.
167
+ #
168
+ # POSIX.1-1998 tar format does not allow for paths greater than 256 bytes,
169
+ # or paths that can't be split into a prefix of 155 bytes (max) and
170
+ # a suffix of 100 bytes (max).
171
+ #
172
+ # This logic was pretty much copied from the private method
173
+ # {Archive::Tar::Minitar::Writer#split_name}.
174
+ #
175
+ # @param path [String] the relative path to be added to the tar file.
176
+ #
177
+ # @raise [ArgumentError] if the path is too long or could not be split.
178
+ #
179
+ # @return [nil]
180
+ def validate_ustar_path!(path)
181
+ if path.bytesize > 256
182
+ raise ArgumentError, _("The path '%{path}' is longer than 256 bytes.") % {
183
+ path: path,
184
+ }
185
+ end
186
+
187
+ if path.bytesize <= 100
188
+ prefix = ''
189
+ else
190
+ parts = path.split(File::SEPARATOR)
191
+ newpath = parts.pop
192
+ nxt = ''
193
+
194
+ loop do
195
+ nxt = parts.pop || ''
196
+ break if newpath.bytesize + 1 + nxt.bytesize >= 100
197
+ newpath = File.join(nxt, newpath)
198
+ end
199
+
200
+ prefix = File.join(*parts, nxt)
201
+ path = newpath
202
+ end
203
+
204
+ return unless path.bytesize > 100 || prefix.bytesize > 155
205
+
206
+ raise ArgumentError, _(
207
+ "'%{path}' could not be split at a directory separator into two " \
208
+ 'parts, the first having a maximum length of 155 bytes and the ' \
209
+ 'second having a maximum length of 100 bytes.',
210
+ ) % { path: path }
211
+ end
212
+
155
213
  # Creates a gzip compressed tarball of the build directory.
156
214
  #
157
215
  # If the destination package already exists, it will be removed before
@@ -129,7 +129,7 @@ module PDK
129
129
  path: metadata_path,
130
130
  }
131
131
  else
132
- return nil if options[:noop]
132
+ return if options[:noop]
133
133
 
134
134
  project_dir = File.basename(Dir.pwd)
135
135
  options[:module_name] = project_dir.split('-', 2).compact[-1]
@@ -108,6 +108,7 @@ module PDK
108
108
  raise ArgumentError, _('Invalid JSON in metadata.json: %{msg}') % { msg: e.message }
109
109
  end
110
110
 
111
+ data['template-url'] = PDK::Util::TemplateURI.default_template_uri.metadata_format if PDK::Util.package_install? && data['template-url'] == PDK::Util::TemplateURI::PACKAGED_TEMPLATE_KEYWORD
111
112
  new(data)
112
113
  end
113
114
 
@@ -9,6 +9,7 @@ module PDK
9
9
  module Module
10
10
  class TemplateDir
11
11
  attr_accessor :module_metadata
12
+ attr_reader :uri
12
13
 
13
14
  # Initialises the TemplateDir object with the path or URL to the template
14
15
  # and the block of code to run to be run while the template is available.
@@ -61,7 +62,7 @@ module PDK
61
62
  }
62
63
  end
63
64
  end
64
- @cloned_from = uri.metadata_format
65
+ @uri = uri
65
66
 
66
67
  @init = init
67
68
  @moduleroot_dir = File.join(@path, 'moduleroot')
@@ -74,6 +75,9 @@ module PDK
74
75
 
75
76
  @module_metadata = module_metadata
76
77
 
78
+ template_type = uri.default? ? 'default' : 'custom'
79
+ PDK.analytics.event('TemplateDir', 'initialize', label: template_type)
80
+
77
81
  yield self
78
82
  ensure
79
83
  # If we cloned a git repo to get the template, remove the clone once
@@ -94,7 +98,7 @@ module PDK
94
98
  def metadata
95
99
  {
96
100
  'pdk-version' => PDK::Util::Version.version_string,
97
- 'template-url' => @cloned_from,
101
+ 'template-url' => uri.metadata_format,
98
102
  'template-ref' => cache_template_ref(@path),
99
103
  }
100
104
  end
@@ -212,6 +216,7 @@ module PDK
212
216
  raise ArgumentError, _("The template at '%{path}' does not contain a 'moduleroot_init/' directory, which indicates you are using an older style of template. Before continuing please use the --template-url flag when running the pdk new commands to pass a new style template.") % { path: @path }
213
217
  # rubocop:enable Metrics/LineLength Style/GuardClause
214
218
  end
219
+ # rubocop:enable Style/GuardClause
215
220
  end
216
221
 
217
222
  # Get a list of template files in the template directory.
@@ -254,14 +259,28 @@ module PDK
254
259
 
255
260
  if @config.nil?
256
261
  conf_defaults = read_config(config_path)
257
- sync_config = read_config(sync_config_path) unless sync_config_path.nil?
262
+ @sync_config = read_config(sync_config_path) unless sync_config_path.nil?
258
263
  @config = conf_defaults
259
- @config.deep_merge!(sync_config, knockout_prefix: '---') unless sync_config.nil?
264
+ @config.deep_merge!(@sync_config, knockout_prefix: '---') unless @sync_config.nil?
260
265
  end
261
266
  file_config = @config.fetch(:global, {})
262
267
  file_config['module_metadata'] = @module_metadata
263
268
  file_config.merge!(@config.fetch(dest_path, {})) unless dest_path.nil?
264
- file_config.merge!(@config)
269
+ file_config.merge!(@config).tap do |c|
270
+ if uri.default?
271
+ file_value = if c['unmanaged']
272
+ 'unmanaged'
273
+ elsif c['delete']
274
+ 'deleted'
275
+ elsif @sync_config && @sync_config.key?(dest_path)
276
+ 'customized'
277
+ else
278
+ 'default'
279
+ end
280
+
281
+ PDK.analytics.event('TemplateDir', 'file', label: dest_path, value: file_value)
282
+ end
283
+ end
265
284
  end
266
285
 
267
286
  # Generates a hash of data from a given yaml file location.
@@ -73,7 +73,7 @@ module PDK
73
73
  def changed?(path)
74
74
  changes[:added].any? { |add| add[:path] == path } ||
75
75
  changes[:removed].include?(path) ||
76
- changes[:modified].keys.include?(path)
76
+ changes[:modified].key?(path)
77
77
  end
78
78
 
79
79
  # Apply any pending changes stored in the UpdateManager to the module.
@@ -174,7 +174,7 @@ module PDK
174
174
 
175
175
  diffs = Diff::LCS.diff(old_lines, new_lines)
176
176
 
177
- return nil if diffs.empty?
177
+ return if diffs.empty?
178
178
 
179
179
  file_mtime = File.stat(path).mtime.localtime.strftime('%Y-%m-%d %H:%M:%S.%N %z')
180
180
  now = Time.now.localtime.strftime('%Y-%m-%d %H:%M:%S.%N %z')
@@ -267,7 +267,7 @@ module PDK
267
267
  # @return [Integer] the provided value, converted into an Integer if
268
268
  # necessary.
269
269
  def sanitise_line(value)
270
- return nil if value.nil?
270
+ return if value.nil?
271
271
 
272
272
  valid_types = [String, Integer]
273
273
  if RUBY_VERSION.split('.')[0..1].join('.').to_f < 2.4
@@ -292,7 +292,7 @@ module PDK
292
292
  # @return [Integer] the provided value, converted into an Integer if
293
293
  # necessary.
294
294
  def sanitise_column(value)
295
- return nil if value.nil?
295
+ return if value.nil?
296
296
 
297
297
  valid_types = [String, Integer]
298
298
  if RUBY_VERSION.split('.')[0..1].join('.').to_f < 2.4
@@ -317,7 +317,7 @@ module PDK
317
317
  #
318
318
  # @return [Array] Array of stack trace lines with less relevant lines excluded
319
319
  def sanitise_trace(value)
320
- return nil if value.nil?
320
+ return if value.nil?
321
321
 
322
322
  valid_types = [Array]
323
323
 
@@ -45,7 +45,7 @@ module PDK
45
45
  end
46
46
 
47
47
  def config_for(path)
48
- return nil unless respond_to?(:template_dir)
48
+ return unless respond_to?(:template_dir)
49
49
 
50
50
  template_dir.config_for(path)
51
51
  end