pdk-akerl 1.8.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (81) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +826 -0
  3. data/LICENSE +201 -0
  4. data/README.md +133 -0
  5. data/exe/pdk +10 -0
  6. data/lib/pdk.rb +10 -0
  7. data/lib/pdk/answer_file.rb +121 -0
  8. data/lib/pdk/cli.rb +113 -0
  9. data/lib/pdk/cli/build.rb +76 -0
  10. data/lib/pdk/cli/bundle.rb +42 -0
  11. data/lib/pdk/cli/convert.rb +41 -0
  12. data/lib/pdk/cli/errors.rb +23 -0
  13. data/lib/pdk/cli/exec.rb +246 -0
  14. data/lib/pdk/cli/exec_group.rb +67 -0
  15. data/lib/pdk/cli/module.rb +14 -0
  16. data/lib/pdk/cli/module/build.rb +14 -0
  17. data/lib/pdk/cli/module/generate.rb +45 -0
  18. data/lib/pdk/cli/new.rb +17 -0
  19. data/lib/pdk/cli/new/class.rb +32 -0
  20. data/lib/pdk/cli/new/defined_type.rb +30 -0
  21. data/lib/pdk/cli/new/module.rb +41 -0
  22. data/lib/pdk/cli/new/provider.rb +27 -0
  23. data/lib/pdk/cli/new/task.rb +31 -0
  24. data/lib/pdk/cli/test.rb +12 -0
  25. data/lib/pdk/cli/test/unit.rb +88 -0
  26. data/lib/pdk/cli/update.rb +32 -0
  27. data/lib/pdk/cli/util.rb +193 -0
  28. data/lib/pdk/cli/util/command_redirector.rb +26 -0
  29. data/lib/pdk/cli/util/interview.rb +63 -0
  30. data/lib/pdk/cli/util/option_normalizer.rb +53 -0
  31. data/lib/pdk/cli/util/option_validator.rb +56 -0
  32. data/lib/pdk/cli/validate.rb +124 -0
  33. data/lib/pdk/generate.rb +11 -0
  34. data/lib/pdk/generate/defined_type.rb +49 -0
  35. data/lib/pdk/generate/module.rb +318 -0
  36. data/lib/pdk/generate/provider.rb +82 -0
  37. data/lib/pdk/generate/puppet_class.rb +48 -0
  38. data/lib/pdk/generate/puppet_object.rb +288 -0
  39. data/lib/pdk/generate/task.rb +86 -0
  40. data/lib/pdk/i18n.rb +4 -0
  41. data/lib/pdk/logger.rb +28 -0
  42. data/lib/pdk/module.rb +21 -0
  43. data/lib/pdk/module/build.rb +214 -0
  44. data/lib/pdk/module/convert.rb +209 -0
  45. data/lib/pdk/module/metadata.rb +193 -0
  46. data/lib/pdk/module/templatedir.rb +313 -0
  47. data/lib/pdk/module/update.rb +111 -0
  48. data/lib/pdk/module/update_manager.rb +210 -0
  49. data/lib/pdk/report.rb +112 -0
  50. data/lib/pdk/report/event.rb +357 -0
  51. data/lib/pdk/template_file.rb +89 -0
  52. data/lib/pdk/tests/unit.rb +213 -0
  53. data/lib/pdk/util.rb +271 -0
  54. data/lib/pdk/util/bundler.rb +253 -0
  55. data/lib/pdk/util/filesystem.rb +12 -0
  56. data/lib/pdk/util/git.rb +74 -0
  57. data/lib/pdk/util/puppet_version.rb +242 -0
  58. data/lib/pdk/util/ruby_version.rb +147 -0
  59. data/lib/pdk/util/vendored_file.rb +88 -0
  60. data/lib/pdk/util/version.rb +42 -0
  61. data/lib/pdk/util/windows.rb +13 -0
  62. data/lib/pdk/util/windows/api_types.rb +57 -0
  63. data/lib/pdk/util/windows/file.rb +36 -0
  64. data/lib/pdk/util/windows/string.rb +16 -0
  65. data/lib/pdk/validate.rb +14 -0
  66. data/lib/pdk/validate/base_validator.rb +209 -0
  67. data/lib/pdk/validate/metadata/metadata_json_lint.rb +86 -0
  68. data/lib/pdk/validate/metadata/metadata_syntax.rb +109 -0
  69. data/lib/pdk/validate/metadata_validator.rb +30 -0
  70. data/lib/pdk/validate/puppet/puppet_lint.rb +67 -0
  71. data/lib/pdk/validate/puppet/puppet_syntax.rb +112 -0
  72. data/lib/pdk/validate/puppet_validator.rb +30 -0
  73. data/lib/pdk/validate/ruby/rubocop.rb +77 -0
  74. data/lib/pdk/validate/ruby_validator.rb +29 -0
  75. data/lib/pdk/validate/tasks/metadata_lint.rb +126 -0
  76. data/lib/pdk/validate/tasks/name.rb +88 -0
  77. data/lib/pdk/validate/tasks_validator.rb +33 -0
  78. data/lib/pdk/version.rb +4 -0
  79. data/locales/config.yaml +21 -0
  80. data/locales/pdk.pot +1283 -0
  81. metadata +304 -0
data/lib/pdk/module.rb ADDED
@@ -0,0 +1,21 @@
1
+ require 'pathspec'
2
+
3
+ module PDK
4
+ module Module
5
+ DEFAULT_IGNORED = [
6
+ '/pkg/',
7
+ '.*',
8
+ '~*',
9
+ '/coverage',
10
+ '/checksums.json',
11
+ '/REVISION',
12
+ '/spec/fixtures/modules/',
13
+ '/vendor/',
14
+ ].freeze
15
+
16
+ def default_ignored_pathspec
17
+ @default_ignored_pathspec ||= PathSpec.new(DEFAULT_IGNORED)
18
+ end
19
+ module_function :default_ignored_pathspec
20
+ end
21
+ end
@@ -0,0 +1,214 @@
1
+ require 'fileutils'
2
+ require 'minitar'
3
+ require 'zlib'
4
+ require 'pathspec'
5
+ require 'find'
6
+ require 'pdk/module'
7
+ require 'pdk/tests/unit'
8
+
9
+ module PDK
10
+ module Module
11
+ class Build
12
+ def self.invoke(options = {})
13
+ new(options).build
14
+ end
15
+
16
+ attr_reader :module_dir
17
+ attr_reader :target_dir
18
+
19
+ def initialize(options = {})
20
+ @module_dir = File.expand_path(options[:module_dir] || Dir.pwd)
21
+ @target_dir = File.expand_path(options[:'target-dir'] || File.join(module_dir, 'pkg'))
22
+ end
23
+
24
+ # Read and parse the values from metadata.json for the module that is
25
+ # being built.
26
+ #
27
+ # @return [Hash{String => Object}] The hash of metadata values.
28
+ def metadata
29
+ @metadata ||= PDK::Module::Metadata.from_file(File.join(module_dir, 'metadata.json')).data
30
+ end
31
+
32
+ # Return the path where the built package file will be written to.
33
+ def package_file
34
+ @package_file ||= File.join(target_dir, "#{release_name}.tar.gz")
35
+ end
36
+
37
+ # Build a module package from a module directory.
38
+ #
39
+ # @return [String] The path to the built package file.
40
+ def build
41
+ create_build_dir
42
+
43
+ stage_module_in_build_dir
44
+ build_package
45
+
46
+ package_file
47
+ ensure
48
+ cleanup_build_dir
49
+ end
50
+
51
+ # Verify if there is an existing package in the target directory and prompts
52
+ # the user if they want to overwrite it.
53
+ def package_already_exists?
54
+ File.exist? package_file
55
+ end
56
+
57
+ # Check if the module is PDK Compatible. If not, then prompt the user if
58
+ # they want to run PDK Convert.
59
+ def module_pdk_compatible?
60
+ ['pdk-version', 'template-url'].any? { |key| metadata.key?(key) }
61
+ end
62
+
63
+ # Return the path to the temporary build directory, which will be placed
64
+ # inside the target directory and match the release name (see #release_name).
65
+ def build_dir
66
+ @build_dir ||= File.join(target_dir, release_name)
67
+ end
68
+
69
+ # Create a temporary build directory where the files to be included in
70
+ # the package will be staged before building the tarball.
71
+ #
72
+ # If the directory already exists, remove it first.
73
+ def create_build_dir
74
+ cleanup_build_dir
75
+
76
+ FileUtils.mkdir_p(build_dir)
77
+ end
78
+
79
+ # Remove the temporary build directory and all its contents from disk.
80
+ #
81
+ # @return nil.
82
+ def cleanup_build_dir
83
+ FileUtils.rm_rf(build_dir, secure: true)
84
+ end
85
+
86
+ # Combine the module name and version into a Forge-compatible dash
87
+ # separated string.
88
+ #
89
+ # @return [String] The module name and version, joined by a dash.
90
+ def release_name
91
+ @release_name ||= [
92
+ metadata['name'],
93
+ metadata['version'],
94
+ ].join('-')
95
+ end
96
+
97
+ # Iterate through all the files and directories in the module and stage
98
+ # them into the temporary build directory (unless ignored).
99
+ #
100
+ # @return nil
101
+ def stage_module_in_build_dir
102
+ Find.find(module_dir) do |path|
103
+ next if path == module_dir
104
+
105
+ ignored_path?(path) ? Find.prune : stage_path(path)
106
+ end
107
+ end
108
+
109
+ # Stage a file or directory from the module into the build directory.
110
+ #
111
+ # @param path [String] The path to the file or directory.
112
+ #
113
+ # @return nil.
114
+ def stage_path(path)
115
+ relative_path = Pathname.new(path).relative_path_from(Pathname.new(module_dir))
116
+ dest_path = File.join(build_dir, relative_path)
117
+
118
+ if File.directory?(path)
119
+ FileUtils.mkdir_p(dest_path, mode: File.stat(path).mode)
120
+ elsif File.symlink?(path)
121
+ warn_symlink(path)
122
+ else
123
+ FileUtils.cp(path, dest_path, preserve: true)
124
+ end
125
+ end
126
+
127
+ # Check if the given path matches one of the patterns listed in the
128
+ # ignore file.
129
+ #
130
+ # @param path [String] The path to be checked.
131
+ #
132
+ # @return [Boolean] true if the path matches and should be ignored.
133
+ def ignored_path?(path)
134
+ path = path.to_s + '/' if File.directory?(path)
135
+
136
+ !ignored_files.match_paths([path], module_dir).empty?
137
+ end
138
+
139
+ # Warn the user about a symlink that would have been included in the
140
+ # built package.
141
+ #
142
+ # @param path [String] The relative or absolute path to the symlink.
143
+ #
144
+ # @return nil.
145
+ def warn_symlink(path)
146
+ symlink_path = Pathname.new(path)
147
+ module_path = Pathname.new(module_dir)
148
+
149
+ PDK.logger.warn _('Symlinks in modules are not supported and will not be included in the package. Please investigate symlink %{from} -> %{to}.') % {
150
+ from: symlink_path.relative_path_from(module_path),
151
+ to: symlink_path.realpath.relative_path_from(module_path),
152
+ }
153
+ end
154
+
155
+ # Creates a gzip compressed tarball of the build directory.
156
+ #
157
+ # If the destination package already exists, it will be removed before
158
+ # creating the new tarball.
159
+ #
160
+ # @return nil.
161
+ def build_package
162
+ FileUtils.rm_f(package_file)
163
+
164
+ Dir.chdir(target_dir) do
165
+ Zlib::GzipWriter.open(package_file) do |package_fd|
166
+ Minitar.pack(release_name, package_fd)
167
+ end
168
+ end
169
+ end
170
+
171
+ # Select the most appropriate ignore file in the module directory.
172
+ #
173
+ # In order of preference, we first try `.pdkignore`, then `.pmtignore`
174
+ # and finally `.gitignore`.
175
+ #
176
+ # @return [String] The path to the file containing the patterns of file
177
+ # paths to ignore.
178
+ def ignore_file
179
+ @ignore_file ||= [
180
+ File.join(module_dir, '.pdkignore'),
181
+ File.join(module_dir, '.pmtignore'),
182
+ File.join(module_dir, '.gitignore'),
183
+ ].find { |file| File.file?(file) && File.readable?(file) }
184
+ end
185
+
186
+ # Instantiate a new PathSpec class and populate it with the pattern(s) of
187
+ # files to be ignored.
188
+ #
189
+ # @return [PathSpec] The populated ignore path matcher.
190
+ def ignored_files
191
+ @ignored_files ||=
192
+ begin
193
+ ignored = if ignore_file.nil?
194
+ PathSpec.new
195
+ else
196
+ fd = File.open(ignore_file, 'rb:UTF-8')
197
+ data = fd.read
198
+ fd.close
199
+
200
+ PathSpec.new(data)
201
+ end
202
+
203
+ if File.realdirpath(target_dir).start_with?(File.realdirpath(module_dir))
204
+ ignored = ignored.add("\/#{File.basename(target_dir)}\/")
205
+ end
206
+
207
+ PDK::Module::DEFAULT_IGNORED.each { |r| ignored.add(r) }
208
+
209
+ ignored
210
+ end
211
+ end
212
+ end
213
+ end
214
+ end
@@ -0,0 +1,209 @@
1
+ require 'pdk/generate/module'
2
+ require 'pdk/module/update_manager'
3
+ require 'pdk/util'
4
+ require 'pdk/report'
5
+
6
+ module PDK
7
+ module Module
8
+ class Convert
9
+ def self.invoke(options)
10
+ new(options).run
11
+ end
12
+
13
+ attr_reader :options
14
+
15
+ def initialize(options = {})
16
+ @options = options
17
+ end
18
+
19
+ def run
20
+ stage_changes!
21
+
22
+ unless update_manager.changes?
23
+ PDK::Report.default_target.puts(_('No changes required.'))
24
+ return
25
+ end
26
+
27
+ print_summary
28
+
29
+ full_report('convert_report.txt') unless update_manager.changes[:modified].empty?
30
+
31
+ return if noop?
32
+
33
+ unless force?
34
+ PDK.logger.info _(
35
+ 'Module conversion is a potentially destructive action. ' \
36
+ 'Ensure that you have committed your module to a version control ' \
37
+ 'system or have a backup, and review the changes above before continuing.',
38
+ )
39
+ continue = PDK::CLI::Util.prompt_for_yes(_('Do you want to continue and make these changes to your module?'))
40
+ return unless continue
41
+ end
42
+
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'))
47
+ end
48
+
49
+ update_manager.sync_changes!
50
+
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
+ metadata_path = 'metadata.json'
70
+
71
+ PDK::Module::TemplateDir.new(template_url, nil, false) do |templates|
72
+ new_metadata = update_metadata(metadata_path, templates.metadata)
73
+ templates.module_metadata = new_metadata.data unless new_metadata.nil?
74
+
75
+ if options[:noop] && new_metadata.nil?
76
+ update_manager.add_file(metadata_path, '')
77
+ elsif File.file?(metadata_path)
78
+ update_manager.modify_file(metadata_path, new_metadata.to_json)
79
+ else
80
+ update_manager.add_file(metadata_path, new_metadata.to_json)
81
+ end
82
+
83
+ templates.render do |file_path, file_content, file_status|
84
+ if file_status == :unmanage
85
+ PDK.logger.debug(_("skipping '%{path}'") % { path: file_path })
86
+ elsif file_status == :delete
87
+ update_manager.remove_file(file_path)
88
+ elsif file_status == :manage
89
+ if File.exist? file_path
90
+ update_manager.modify_file(file_path, file_content)
91
+ else
92
+ update_manager.add_file(file_path, file_content)
93
+ end
94
+ end
95
+ end
96
+ end
97
+ rescue ArgumentError => e
98
+ raise PDK::CLI::ExitWithError, e
99
+ end
100
+
101
+ def update_manager
102
+ @update_manager ||= PDK::Module::UpdateManager.new
103
+ end
104
+
105
+ def template_url
106
+ @template_url ||= options.fetch(:'template-url', PDK::Util.default_template_url)
107
+ end
108
+
109
+ def update_metadata(metadata_path, template_metadata)
110
+ if File.file?(metadata_path)
111
+ unless File.readable?(metadata_path)
112
+ raise PDK::CLI::ExitWithError, _('Unable to update module metadata; %{path} exists but it is not readable.') % {
113
+ path: metadata_path,
114
+ }
115
+ end
116
+
117
+ begin
118
+ metadata = PDK::Module::Metadata.from_file(metadata_path)
119
+ new_values = PDK::Module::Metadata::DEFAULTS.select do |key, _|
120
+ !metadata.data.key?(key) || metadata.data[key].nil? ||
121
+ (key == 'requirements' && metadata.data[key].empty?)
122
+ end
123
+ metadata.update!(new_values)
124
+ rescue ArgumentError
125
+ metadata = PDK::Generate::Module.prepare_metadata(options) unless options[:noop]
126
+ end
127
+ elsif File.exist?(metadata_path)
128
+ raise PDK::CLI::ExitWithError, _('Unable to update module metadata; %{path} exists but it is not a file.') % {
129
+ path: metadata_path,
130
+ }
131
+ else
132
+ return nil if options[:noop]
133
+
134
+ project_dir = File.basename(Dir.pwd)
135
+ options[:module_name] = project_dir.split('-', 2).compact[-1]
136
+ options[:prompt] = false
137
+ options[:'skip-interview'] = true if options[:force]
138
+
139
+ metadata = PDK::Generate::Module.prepare_metadata(options)
140
+ end
141
+
142
+ metadata.update!(template_metadata)
143
+ metadata
144
+ end
145
+
146
+ def summary
147
+ summary = {}
148
+ update_manager.changes.each do |category, update_category|
149
+ if update_category.respond_to?(:keys)
150
+ updated_files = update_category.keys
151
+ else
152
+ begin
153
+ updated_files = update_category.map { |file| file[:path] }
154
+ rescue TypeError
155
+ updated_files = update_category.to_a
156
+ end
157
+ end
158
+
159
+ summary[category] = updated_files
160
+ end
161
+
162
+ summary
163
+ end
164
+
165
+ def print_summary
166
+ footer = false
167
+
168
+ summary.keys.each do |category|
169
+ next if summary[category].empty?
170
+
171
+ PDK::Report.default_target.puts(_("\n%{banner}") % { banner: generate_banner("Files to be #{category}", 40) })
172
+ PDK::Report.default_target.puts(summary[category])
173
+ footer = true
174
+ end
175
+
176
+ PDK::Report.default_target.puts(_("\n%{banner}") % { banner: generate_banner('', 40) }) if footer
177
+ end
178
+
179
+ def print_result(banner_text)
180
+ PDK::Report.default_target.puts(_("\n%{banner}") % { banner: generate_banner(banner_text, 40) })
181
+ summary_to_print = summary.map { |k, v| "#{v.length} files #{k}" unless v.empty? }.compact
182
+ PDK::Report.default_target.puts(_("\n%{summary}\n\n") % { summary: "#{summary_to_print.join(', ')}." })
183
+ end
184
+
185
+ def full_report(path)
186
+ File.open(path, 'w') do |f|
187
+ f.write("/* Report generated by PDK at #{Time.now} */")
188
+ update_manager.changes[:modified].each do |_, diff|
189
+ f.write("\n\n\n" + diff)
190
+ end
191
+ f.write("\n")
192
+ end
193
+ PDK::Report.default_target.puts(_("\nYou can find a report of differences in %{path}.\n\n") % { path: path })
194
+ end
195
+
196
+ def generate_banner(text, width = 80)
197
+ padding = width - text.length
198
+ banner = ''
199
+ padding_char = '-'
200
+
201
+ (padding / 2.0).ceil.times { banner << padding_char }
202
+ banner << text
203
+ (padding / 2.0).floor.times { banner << padding_char }
204
+
205
+ banner
206
+ end
207
+ end
208
+ end
209
+ end
@@ -0,0 +1,193 @@
1
+ require 'json'
2
+ require 'pdk/util/filesystem'
3
+
4
+ module PDK
5
+ module Module
6
+ class Metadata
7
+ attr_accessor :data
8
+
9
+ include PDK::Util::Filesystem
10
+
11
+ OPERATING_SYSTEMS = {
12
+ 'RedHat based Linux' => [
13
+ {
14
+ 'operatingsystem' => 'CentOS',
15
+ 'operatingsystemrelease' => ['7'],
16
+ },
17
+ {
18
+ 'operatingsystem' => 'OracleLinux',
19
+ 'operatingsystemrelease' => ['7'],
20
+ },
21
+ {
22
+ 'operatingsystem' => 'RedHat',
23
+ 'operatingsystemrelease' => ['7'],
24
+ },
25
+ {
26
+ 'operatingsystem' => 'Scientific',
27
+ 'operatingsystemrelease' => ['7'],
28
+ },
29
+ ],
30
+ 'Debian based Linux' => [
31
+ {
32
+ 'operatingsystem' => 'Debian',
33
+ 'operatingsystemrelease' => ['8'],
34
+ },
35
+ {
36
+ 'operatingsystem' => 'Ubuntu',
37
+ 'operatingsystemrelease' => ['16.04'],
38
+ },
39
+ ],
40
+ 'Fedora' => {
41
+ 'operatingsystem' => 'Fedora',
42
+ 'operatingsystemrelease' => ['25'],
43
+ },
44
+ 'OSX' => {
45
+ 'operatingsystem' => 'Darwin',
46
+ 'operatingsystemrelease' => ['16'],
47
+ },
48
+ 'SLES' => {
49
+ 'operatingsystem' => 'SLES',
50
+ 'operatingsystemrelease' => ['12'],
51
+ },
52
+ 'Solaris' => {
53
+ 'operatingsystem' => 'Solaris',
54
+ 'operatingsystemrelease' => ['11'],
55
+ },
56
+ 'Windows' => {
57
+ 'operatingsystem' => 'windows',
58
+ 'operatingsystemrelease' => ['2008 R2', '2012 R2', '10'],
59
+ },
60
+ }.freeze
61
+
62
+ DEFAULT_OPERATING_SYSTEMS = [
63
+ 'RedHat based Linux',
64
+ 'Debian based Linux',
65
+ 'Windows',
66
+ ].freeze
67
+
68
+ DEFAULTS = {
69
+ 'name' => nil,
70
+ 'version' => '0.1.0',
71
+ 'author' => nil,
72
+ 'summary' => '',
73
+ 'license' => 'Apache-2.0',
74
+ 'source' => '',
75
+ 'project_page' => nil,
76
+ 'issues_url' => nil,
77
+ 'dependencies' => [],
78
+ 'data_provider' => nil,
79
+ 'operatingsystem_support' => DEFAULT_OPERATING_SYSTEMS.map { |os_name|
80
+ OPERATING_SYSTEMS[os_name]
81
+ }.flatten,
82
+ 'requirements' => [
83
+ { 'name' => 'puppet', 'version_requirement' => '>= 4.10.0 < 7.0.0' },
84
+ ],
85
+ }.freeze
86
+
87
+ def initialize(params = {})
88
+ @data = DEFAULTS.dup
89
+ update!(params) if params
90
+ end
91
+
92
+ def self.from_file(metadata_json_path)
93
+ if metadata_json_path.nil?
94
+ raise ArgumentError, _('Cannot read metadata from file: no path to file was given.')
95
+ end
96
+
97
+ unless File.file?(metadata_json_path)
98
+ raise ArgumentError, _("'%{file}' does not exist or is not a file.") % { file: metadata_json_path }
99
+ end
100
+
101
+ unless File.readable?(metadata_json_path)
102
+ raise ArgumentError, _("Unable to open '%{file}' for reading.") % { file: metadata_json_path }
103
+ end
104
+
105
+ begin
106
+ data = JSON.parse(File.read(metadata_json_path))
107
+ rescue JSON::JSONError => e
108
+ raise ArgumentError, _('Invalid JSON in metadata.json: %{msg}') % { msg: e.message }
109
+ end
110
+
111
+ new(data)
112
+ end
113
+
114
+ def update!(data)
115
+ # TODO: validate all data
116
+ process_name(data) if data['name']
117
+ @data.merge!(data)
118
+ self
119
+ end
120
+
121
+ def to_json
122
+ JSON.pretty_generate(@data.dup.delete_if { |_key, value| value.nil? })
123
+ end
124
+
125
+ def write!(path)
126
+ write_file(path, to_json)
127
+ end
128
+
129
+ def forge_ready?
130
+ missing_fields.empty?
131
+ end
132
+
133
+ def interview_for_forge!
134
+ PDK::Generate::Module.module_interview(self, only_ask: missing_fields)
135
+ end
136
+
137
+ def validate_puppet_version_requirement!
138
+ msgs = {
139
+ no_reqs: _('Module metadata does not contain any requirements.'),
140
+ no_puppet_req: _('Module metadata does not contain a "puppet" requirement.'),
141
+ no_puppet_ver: _('The "puppet" requirement in module metadata does not specify a "version_requirement".'),
142
+ }
143
+
144
+ raise ArgumentError, msgs[:no_reqs] unless @data.key?('requirements')
145
+ raise ArgumentError, msgs[:no_puppet_req] if puppet_requirement.nil?
146
+ raise ArgumentError, msgs[:no_puppet_ver] unless puppet_requirement.key?('version_requirement')
147
+ raise ArgumentError, msgs[:no_puppet_ver] if puppet_requirement['version_requirement'].empty?
148
+ end
149
+
150
+ def puppet_requirement
151
+ @data['requirements'].find do |r|
152
+ r.key?('name') && r['name'] == 'puppet'
153
+ end
154
+ end
155
+
156
+ private
157
+
158
+ def missing_fields
159
+ fields = DEFAULTS.keys - %w[data_provider requirements dependencies]
160
+ fields.select { |key| @data[key].nil? || @data[key].empty? }
161
+ end
162
+
163
+ # Do basic validation and parsing of the name parameter.
164
+ def process_name(data)
165
+ validate_name(data['name'])
166
+ author, _modname = data['name'].split(%r{[-/]}, 2)
167
+
168
+ data['author'] ||= author if @data['author'] == DEFAULTS['author']
169
+ end
170
+
171
+ # Validates that the given module name is both namespaced and well-formed.
172
+ def validate_name(name)
173
+ return if name =~ %r{\A[a-z0-9]+[-\/][a-z][a-z0-9_]*\Z}i
174
+
175
+ namespace, modname = name.split(%r{[-/]}, 2)
176
+ modname = :namespace_missing if namespace == ''
177
+
178
+ err = case modname
179
+ when nil, '', :namespace_missing
180
+ _('Field must be a dash-separated user name and module name.')
181
+ when %r{[^a-z0-9_]}i
182
+ _('Module name must contain only alphanumeric or underscore characters.')
183
+ when %r{^[^a-z]}i
184
+ _('Module name must begin with a letter.')
185
+ else
186
+ _('Namespace must contain only alphanumeric characters.')
187
+ end
188
+
189
+ raise ArgumentError, _("Invalid 'name' field in metadata.json: %{err}") % { err: err }
190
+ end
191
+ end
192
+ end
193
+ end