pdk 1.3.2 → 1.4.1

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.
@@ -0,0 +1,80 @@
1
+ require 'pdk/generate/puppet_object'
2
+
3
+ module PDK
4
+ module Generate
5
+ class Provider < PuppetObject
6
+ OBJECT_TYPE = :provider
7
+
8
+ # Prepares the data needed to render the new defined type template.
9
+ #
10
+ # @return [Hash{Symbol => Object}] a hash of information that will be
11
+ # provided to the defined type and defined type spec templates during
12
+ # rendering.
13
+ def template_data
14
+ data = {
15
+ name: object_name,
16
+ provider_class: Provider.class_name_from_object_name(object_name),
17
+ }
18
+
19
+ data
20
+ end
21
+
22
+ def raise_precondition_error(error)
23
+ raise PDK::CLI::ExitWithError, _('%{error}: Creating a provider needs some local configuration in your module.' \
24
+ ' Please follow the docs at https://github.com/puppetlabs/puppet-resource_api#getting-started.') % { error: error }
25
+ end
26
+
27
+ def check_preconditions
28
+ super
29
+ # These preconditions can be removed once the pdk-templates are carrying the puppet-resource_api gem by default, and have switched
30
+ # the default mock_with value.
31
+ sync_path = PDK::Util.find_upwards('.sync.yml')
32
+ if sync_path.nil?
33
+ raise_precondition_error(_('.sync.yml not found'))
34
+ end
35
+ sync = YAML.load_file(sync_path)
36
+ if !sync.is_a? Hash
37
+ raise_precondition_error(_('.sync.yml contents is not a Hash'))
38
+ elsif !sync.key? 'Gemfile'
39
+ raise_precondition_error(_('Gemfile configuration not found'))
40
+ elsif !sync['Gemfile'].key? 'optional'
41
+ raise_precondition_error(_('Gemfile.optional configuration not found'))
42
+ elsif !sync['Gemfile']['optional'].key? ':development'
43
+ raise_precondition_error(_('Gemfile.optional.:development configuration not found'))
44
+ elsif sync['Gemfile']['optional'][':development'].none? { |g| g['gem'] == 'puppet-resource_api' }
45
+ raise_precondition_error(_('puppet-resource_api not found in the Gemfile config'))
46
+ elsif !sync.key? 'spec/spec_helper.rb'
47
+ raise_precondition_error(_('spec/spec_helper.rb configuration not found'))
48
+ elsif !sync['spec/spec_helper.rb'].key? 'mock_with'
49
+ raise_precondition_error(_('spec/spec_helper.rb.mock_with configuration not found'))
50
+ elsif !sync['spec/spec_helper.rb']['mock_with'] == ':rspec'
51
+ raise_precondition_error(_('spec/spec_helper.rb.mock_with not set to \':rspec\''))
52
+ end
53
+ end
54
+
55
+ # @return [String] the path where the new type will be written.
56
+ def target_object_path
57
+ @target_object_path ||= File.join(module_dir, 'lib', 'puppet', 'type', object_name) + '.rb'
58
+ end
59
+
60
+ # @return [String] the path where the new provider will be written.
61
+ def target_addon_path
62
+ @target_addon_path ||= File.join(module_dir, 'lib', 'puppet', 'provider', object_name, object_name) + '.rb'
63
+ end
64
+
65
+ # Calculates the path to the file where the tests for the new defined
66
+ # type will be written.
67
+ #
68
+ # @return [String] the path where the tests for the new defined type
69
+ # will be written.
70
+ def target_spec_path
71
+ @target_spec_path ||= File.join(module_dir, 'spec', 'unit', 'puppet', 'provider', object_name, object_name) + '_spec.rb'
72
+ end
73
+
74
+ # transform a object name into a ruby class name
75
+ def self.class_name_from_object_name(object_name)
76
+ object_name.to_s.split('_').map(&:capitalize).join
77
+ end
78
+ end
79
+ end
80
+ end
@@ -58,6 +58,14 @@ module PDK
58
58
  raise NotImplementedError
59
59
  end
60
60
 
61
+ # @abstract Subclass and implement {#target_addon_path}. Implementations
62
+ # of this method should return a String containing the destination path
63
+ # of the additional object file being generated.
64
+ # @return [String] returns nil if there is no additional object file
65
+ def target_addon_path
66
+ nil
67
+ end
68
+
61
69
  # @abstract Subclass and implement {#target_spec_path}. Implementations
62
70
  # of this method should return a String containing the destination path
63
71
  # of the tests for the object being generated.
@@ -76,16 +84,14 @@ module PDK
76
84
  self.class::OBJECT_TYPE
77
85
  end
78
86
 
79
- # Check that the target files do not exist, find an appropriate template
80
- # and create the target files from the template. This is the main entry
81
- # point for the class.
87
+ # Check preconditions of this template group. By default this only makes sure that the target files do not
88
+ # already exist. Override this (and call super) to add your own preconditions.
82
89
  #
83
90
  # @raise [PDK::CLI::ExitWithError] if the target files already exist.
84
- # @raise [PDK::CLI::FatalError] (see #render_file)
85
91
  #
86
92
  # @api public
87
- def run
88
- [target_object_path, target_spec_path].compact.each do |target_file|
93
+ def check_preconditions
94
+ [target_object_path, target_addon_path, target_spec_path].compact.each do |target_file|
89
95
  next unless File.exist?(target_file)
90
96
 
91
97
  raise PDK::CLI::ExitWithError, _("Unable to generate %{object_type}; '%{file}' already exists.") % {
@@ -93,11 +99,24 @@ module PDK
93
99
  object_type: object_type,
94
100
  }
95
101
  end
102
+ end
103
+
104
+ # Check that the templates can be rendered. Find an appropriate template
105
+ # and create the target files from the template. This is the main entry
106
+ # point for the class.
107
+ #
108
+ # @raise [PDK::CLI::ExitWithError] if the target files already exist.
109
+ # @raise [PDK::CLI::FatalError] (see #render_file)
110
+ #
111
+ # @api public
112
+ def run
113
+ check_preconditions
96
114
 
97
115
  with_templates do |template_path, config_hash|
98
116
  data = template_data.merge(configs: config_hash)
99
117
 
100
118
  render_file(target_object_path, template_path[:object], data)
119
+ render_file(target_addon_path, template_path[:addon], data) if template_path[:addon]
101
120
  render_file(target_spec_path, template_path[:spec], data) if template_path[:spec]
102
121
  end
103
122
  end
@@ -0,0 +1,208 @@
1
+ require 'fileutils'
2
+ require 'minitar'
3
+ require 'zlib'
4
+ require 'pathspec'
5
+ require 'find'
6
+
7
+ module PDK
8
+ module Module
9
+ class Build
10
+ def self.invoke(options = {})
11
+ new(options).build
12
+ end
13
+
14
+ attr_reader :module_dir
15
+ attr_reader :target_dir
16
+
17
+ def initialize(options = {})
18
+ @module_dir = File.expand_path(options[:module_dir] || Dir.pwd)
19
+ @target_dir = File.expand_path(options[:'target-dir'] || File.join(module_dir, 'pkg'))
20
+ end
21
+
22
+ # Read and parse the values from metadata.json for the module that is
23
+ # being built.
24
+ #
25
+ # @return [Hash{String => Object}] The hash of metadata values.
26
+ def metadata
27
+ @metadata ||= PDK::Module::Metadata.from_file(File.join(module_dir, 'metadata.json')).data
28
+ end
29
+
30
+ # Return the path where the built package file will be written to.
31
+ def package_file
32
+ @package_file ||= File.join(target_dir, "#{release_name}.tar.gz")
33
+ end
34
+
35
+ # Build a module package from a module directory.
36
+ #
37
+ # @return [String] The path to the built package file.
38
+ def build
39
+ create_build_dir
40
+
41
+ stage_module_in_build_dir
42
+ build_package
43
+
44
+ package_file
45
+ ensure
46
+ cleanup_build_dir
47
+ end
48
+
49
+ # Verify if there is an existing package in the target directory and prompts
50
+ # the user if they want to overwrite it.
51
+ def package_already_exists?
52
+ File.exist? package_file
53
+ end
54
+
55
+ # Check if the module is PDK Compatible. If not, then prompt the user if
56
+ # they want to run PDK Convert.
57
+ def module_pdk_compatible?
58
+ ['pdk-version', 'template-url'].any? { |key| metadata.key?(key) }
59
+ end
60
+
61
+ # Return the path to the temporary build directory, which will be placed
62
+ # inside the target directory and match the release name (see #release_name).
63
+ def build_dir
64
+ @build_dir ||= File.join(target_dir, release_name)
65
+ end
66
+
67
+ # Create a temporary build directory where the files to be included in
68
+ # the package will be staged before building the tarball.
69
+ #
70
+ # If the directory already exists, remove it first.
71
+ def create_build_dir
72
+ cleanup_build_dir
73
+
74
+ FileUtils.mkdir_p(build_dir)
75
+ end
76
+
77
+ # Remove the temporary build directory and all its contents from disk.
78
+ #
79
+ # @return nil.
80
+ def cleanup_build_dir
81
+ FileUtils.rm_rf(build_dir, secure: true)
82
+ end
83
+
84
+ # Combine the module name and version into a Forge-compatible dash
85
+ # separated string.
86
+ #
87
+ # @return [String] The module name and version, joined by a dash.
88
+ def release_name
89
+ @release_name ||= [
90
+ metadata['name'],
91
+ metadata['version'],
92
+ ].join('-')
93
+ end
94
+
95
+ # Iterate through all the files and directories in the module and stage
96
+ # them into the temporary build directory (unless ignored).
97
+ #
98
+ # @return nil
99
+ def stage_module_in_build_dir
100
+ Find.find(module_dir) do |path|
101
+ next if path == module_dir
102
+
103
+ ignored_path?(path) ? Find.prune : stage_path(path)
104
+ end
105
+ end
106
+
107
+ # Stage a file or directory from the module into the build directory.
108
+ #
109
+ # @param path [String] The path to the file or directory.
110
+ #
111
+ # @return nil.
112
+ def stage_path(path)
113
+ relative_path = Pathname.new(path).relative_path_from(Pathname.new(module_dir))
114
+ dest_path = File.join(build_dir, relative_path)
115
+
116
+ if File.directory?(path)
117
+ FileUtils.mkdir_p(dest_path, mode: File.stat(path).mode)
118
+ elsif File.symlink?(path)
119
+ warn_symlink(path)
120
+ else
121
+ FileUtils.cp(path, dest_path, preserve: true)
122
+ end
123
+ end
124
+
125
+ # Check if the given path matches one of the patterns listed in the
126
+ # ignore file.
127
+ #
128
+ # @param path [String] The path to be checked.
129
+ #
130
+ # @return [Boolean] true if the path matches and should be ignored.
131
+ def ignored_path?(path)
132
+ path = path.to_s + '/' if File.directory?(path)
133
+
134
+ !ignored_files.match_paths([path], module_dir).empty?
135
+ end
136
+
137
+ # Warn the user about a symlink that would have been included in the
138
+ # built package.
139
+ #
140
+ # @param path [String] The relative or absolute path to the symlink.
141
+ #
142
+ # @return nil.
143
+ def warn_symlink(path)
144
+ symlink_path = Pathname.new(path)
145
+ module_path = Pathname.new(module_dir)
146
+
147
+ PDK.logger.warn _('Symlinks in modules are not supported and will not be included in the package. Please investigate symlink %{from} -> %{to}.') % {
148
+ from: symlink_path.relative_path_from(module_path),
149
+ to: symlink_path.realpath.relative_path_from(module_path),
150
+ }
151
+ end
152
+
153
+ # Creates a gzip compressed tarball of the build directory.
154
+ #
155
+ # If the destination package already exists, it will be removed before
156
+ # creating the new tarball.
157
+ #
158
+ # @return nil.
159
+ def build_package
160
+ FileUtils.rm_f(package_file)
161
+
162
+ Dir.chdir(target_dir) do
163
+ Zlib::GzipWriter.open(package_file) do |package_fd|
164
+ Minitar.pack(release_name, package_fd)
165
+ end
166
+ end
167
+ end
168
+
169
+ # Select the most appropriate ignore file in the module directory.
170
+ #
171
+ # In order of preference, we first try `.pdkignore`, then `.pmtignore`
172
+ # and finally `.gitignore`.
173
+ #
174
+ # @return [String] The path to the file containing the patterns of file
175
+ # paths to ignore.
176
+ def ignore_file
177
+ @ignore_file ||= [
178
+ File.join(module_dir, '.pdkignore'),
179
+ File.join(module_dir, '.pmtignore'),
180
+ File.join(module_dir, '.gitignore'),
181
+ ].find { |file| File.file?(file) && File.readable?(file) }
182
+ end
183
+
184
+ # Instantiate a new PathSpec class and populate it with the pattern(s) of
185
+ # files to be ignored.
186
+ #
187
+ # @return [PathSpec] The populated ignore path matcher.
188
+ def ignored_files
189
+ @ignored_files ||= if ignore_file.nil?
190
+ PathSpec.new
191
+ else
192
+ fd = File.open(ignore_file, 'rb:UTF-8')
193
+ data = fd.read
194
+ fd.close
195
+
196
+ PathSpec.new(data)
197
+ end
198
+
199
+ # Also ignore the target directory if it is in the module dir and not already ignored
200
+ if Find.find(@module_dir).include?(target_dir) && !@ignored_files.match(File.basename(target_dir) + '/')
201
+ @ignored_files = @ignored_files.add("\/#{File.basename(target_dir)}\/")
202
+ end
203
+
204
+ @ignored_files
205
+ end
206
+ end
207
+ end
208
+ end
@@ -7,44 +7,30 @@ module PDK
7
7
  module Module
8
8
  class Convert
9
9
  def self.invoke(options)
10
- update_manager = PDK::Module::UpdateManager.new
11
- template_url = options.fetch(:'template-url', PDK::Util.default_template_url)
10
+ new(options).run
11
+ end
12
12
 
13
- PDK::Module::TemplateDir.new(template_url, nil, false) do |templates|
14
- new_metadata = update_metadata('metadata.json', templates.metadata, options)
13
+ attr_reader :options
15
14
 
16
- if options[:noop] && new_metadata.nil?
17
- update_manager.add_file('metadata.json', '')
18
- elsif File.file?('metadata.json')
19
- update_manager.modify_file('metadata.json', new_metadata)
20
- else
21
- update_manager.add_file('metadata.json', new_metadata)
22
- end
15
+ def initialize(options = {})
16
+ @options = options
17
+ end
23
18
 
24
- templates.render do |file_path, file_content|
25
- if File.exist? file_path
26
- update_manager.modify_file(file_path, file_content)
27
- else
28
- update_manager.add_file(file_path, file_content)
29
- end
30
- end
31
- end
19
+ def run
20
+ stage_changes!
32
21
 
33
22
  unless update_manager.changes?
34
23
  PDK::Report.default_target.puts(_('No changes required.'))
35
24
  return
36
25
  end
37
26
 
38
- # Print the summary to the default target of reports
39
- summary = get_summary(update_manager)
40
- print_summary(summary)
27
+ print_summary
41
28
 
42
- # Generates the full convert report
43
- full_report(update_manager) unless update_manager.changes[:modified].empty?
29
+ full_report('convert_report.txt') unless update_manager.changes[:modified].empty?
44
30
 
45
- return if options[:noop]
31
+ return if noop?
46
32
 
47
- unless options[:force]
33
+ unless force?
48
34
  PDK.logger.info _(
49
35
  'Module conversion is a potentially destructive action. ' \
50
36
  'Ensure that you have committed your module to a version control ' \
@@ -54,21 +40,62 @@ module PDK
54
40
  return unless continue
55
41
  end
56
42
 
57
- # Mark these files for removal after generating the report as these
58
- # changes are not something that the user needs to review.
59
- if update_manager.changed?('Gemfile')
60
- update_manager.remove_file('Gemfile.lock')
61
- update_manager.remove_file(File.join('.bundle', 'config'))
43
+ # Remove these files straight away as these changes are not something that the user needs to review.
44
+ if needs_bundle_update?
45
+ update_manager.unlink_file('Gemfile.lock')
46
+ update_manager.unlink_file(File.join('.bundle', 'config'))
62
47
  end
63
48
 
64
49
  update_manager.sync_changes!
65
50
 
66
- PDK::Util::Bundler.ensure_bundle! if update_manager.changed?('Gemfile')
51
+ PDK::Util::Bundler.ensure_bundle! if needs_bundle_update?
52
+
53
+ print_result 'Convert completed'
54
+ end
55
+
56
+ def noop?
57
+ options[:noop]
58
+ end
59
+
60
+ def force?
61
+ options[:force]
62
+ end
63
+
64
+ def needs_bundle_update?
65
+ update_manager.changed?('Gemfile')
66
+ end
67
+
68
+ def stage_changes!
69
+ PDK::Module::TemplateDir.new(template_url, nil, false) do |templates|
70
+ new_metadata = update_metadata('metadata.json', templates.metadata)
71
+
72
+ if options[:noop] && new_metadata.nil?
73
+ update_manager.add_file('metadata.json', '')
74
+ elsif File.file?('metadata.json')
75
+ update_manager.modify_file('metadata.json', new_metadata)
76
+ else
77
+ update_manager.add_file('metadata.json', new_metadata)
78
+ end
79
+
80
+ templates.render do |file_path, file_content|
81
+ if File.exist? file_path
82
+ update_manager.modify_file(file_path, file_content)
83
+ else
84
+ update_manager.add_file(file_path, file_content)
85
+ end
86
+ end
87
+ end
88
+ end
89
+
90
+ def update_manager
91
+ @update_manager ||= PDK::Module::UpdateManager.new
92
+ end
67
93
 
68
- print_result(summary)
94
+ def template_url
95
+ @template_url ||= options.fetch(:'template-url', PDK::Util.default_template_url)
69
96
  end
70
97
 
71
- def self.update_metadata(metadata_path, template_metadata, options = {})
98
+ def update_metadata(metadata_path, template_metadata)
72
99
  if File.file?(metadata_path)
73
100
  if File.readable?(metadata_path)
74
101
  begin
@@ -79,12 +106,12 @@ module PDK
79
106
  metadata = PDK::Generate::Module.prepare_metadata(options) unless options[:noop] # rubocop:disable Metrics/BlockNesting
80
107
  end
81
108
  else
82
- raise PDK::CLI::ExitWithError, _('Unable to convert module metadata; %{path} exists but it is not readable.') % {
109
+ raise PDK::CLI::ExitWithError, _('Unable to update module metadata; %{path} exists but it is not readable.') % {
83
110
  path: metadata_path,
84
111
  }
85
112
  end
86
113
  elsif File.exist?(metadata_path)
87
- raise PDK::CLI::ExitWithError, _('Unable to convert module metadata; %{path} exists but it is not a file.') % {
114
+ raise PDK::CLI::ExitWithError, _('Unable to update module metadata; %{path} exists but it is not a file.') % {
88
115
  path: metadata_path,
89
116
  }
90
117
  else
@@ -102,14 +129,18 @@ module PDK
102
129
  metadata.to_json
103
130
  end
104
131
 
105
- def self.get_summary(update_manager)
132
+ def summary
106
133
  summary = {}
107
134
  update_manager.changes.each do |category, update_category|
108
- updated_files = if update_category.respond_to?(:keys)
109
- update_category.keys
110
- else
111
- update_category.map { |file| file[:path] }
112
- end
135
+ if update_category.respond_to?(:keys)
136
+ updated_files = update_category.keys
137
+ else
138
+ begin
139
+ updated_files = update_category.map { |file| file[:path] }
140
+ rescue TypeError
141
+ updated_files = update_category.to_a
142
+ end
143
+ end
113
144
 
114
145
  summary[category] = updated_files
115
146
  end
@@ -117,7 +148,7 @@ module PDK
117
148
  summary
118
149
  end
119
150
 
120
- def self.print_summary(summary)
151
+ def print_summary
121
152
  footer = false
122
153
 
123
154
  summary.keys.each do |category|
@@ -131,23 +162,23 @@ module PDK
131
162
  PDK::Report.default_target.puts(_("\n%{banner}") % { banner: generate_banner('', 40) }) if footer
132
163
  end
133
164
 
134
- def self.print_result(summary)
135
- PDK::Report.default_target.puts(_("\n%{banner}") % { banner: generate_banner('Convert completed', 40) })
165
+ def print_result(banner_text)
166
+ PDK::Report.default_target.puts(_("\n%{banner}") % { banner: generate_banner(banner_text, 40) })
136
167
  summary_to_print = summary.map { |k, v| "#{v.length} files #{k}" unless v.empty? }.compact
137
168
  PDK::Report.default_target.puts(_("\n%{summary}\n\n") % { summary: "#{summary_to_print.join(', ')}." })
138
169
  end
139
170
 
140
- def self.full_report(update_manager)
141
- File.open('convert_report.txt', 'w') do |f|
142
- f.write("/* Convert Report generated by PDK at #{Time.now} */")
171
+ def full_report(path)
172
+ File.open(path, 'w') do |f|
173
+ f.write("/* Report generated by PDK at #{Time.now} */")
143
174
  update_manager.changes[:modified].each do |_, diff|
144
175
  f.write("\n\n\n" + diff)
145
176
  end
146
177
  end
147
- PDK::Report.default_target.puts(_("\nYou can find a report of differences in convert_report.txt.\n\n"))
178
+ PDK::Report.default_target.puts(_("\nYou can find a report of differences in %{path}.\n\n") % { path: path })
148
179
  end
149
180
 
150
- def self.generate_banner(text, width = 80)
181
+ def generate_banner(text, width = 80)
151
182
  padding = width - text.length
152
183
  banner = ''
153
184
  padding_char = '-'