pdk 1.10.0 → 1.11.0

Sign up to get free protection for your applications and to get access to all the features.
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