pdk 1.3.2 → 1.4.1

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