lib/pdk/generate/module.rb in pdk-2.4.0 vs lib/pdk/generate/module.rb in pdk-2.5.0

- old
+ new

@@ -1,352 +1,352 @@ -require 'pdk' - -module PDK - module Generate - class Module - def self.validate_options(opts) - require 'pdk/cli/util/option_validator' - - unless PDK::CLI::Util::OptionValidator.valid_module_name?(opts[:module_name]) - error_msg = _( - "'%{module_name}' is not a valid module name.\n" \ - 'Module names must begin with a lowercase letter and can only include lowercase letters, digits, and underscores.', - ) % { module_name: opts[:module_name] } - raise PDK::CLI::ExitWithError, error_msg - end - - target_dir = PDK::Util::Filesystem.expand_path(opts[:target_dir]) - raise PDK::CLI::ExitWithError, _("The destination directory '%{dir}' already exists") % { dir: target_dir } if PDK::Util::Filesystem.exist?(target_dir) - end - - def self.invoke(opts = {}) - require 'pdk/util' - require 'pdk/util/template_uri' - require 'pathname' - - validate_options(opts) unless opts[:module_name].nil? - - metadata = prepare_metadata(opts) - - target_dir = PDK::Util::Filesystem.expand_path(opts[:target_dir] || opts[:module_name]) - parent_dir = File.dirname(target_dir) - - begin - test_file = File.join(parent_dir, '.pdk-test-writable') - PDK::Util::Filesystem.write_file(test_file, 'This file was created by the Puppet Development Kit to test if this folder was writable, you can safely remove this file.') - PDK::Util::Filesystem.rm_f(test_file) - rescue Errno::EACCES - raise PDK::CLI::FatalError, _("You do not have permission to write to '%{parent_dir}'") % { - parent_dir: parent_dir, - } - end - - temp_target_dir = PDK::Util.make_tmpdir_name('pdk-module-target') - - prepare_module_directory(temp_target_dir) - - template_uri = PDK::Util::TemplateURI.new(opts) - - if template_uri.default? && template_uri.default_ref? - PDK.logger.info _('Using the default template-url and template-ref.') - else - PDK.logger.info _( - "Using the %{method} template-url and template-ref '%{template_uri}'." % { - method: opts.key?(:'template-url') ? _('specified') : _('saved'), - template_uri: template_uri.metadata_format, - }, - ) - end - - begin - context = PDK::Context::None.new(temp_target_dir) - PDK::Template.with(template_uri, context) do |template_dir| - template_dir.render_new_module(metadata.data['name'], metadata.data) do |relative_file_path, file_content, file_status| - next if [:delete, :unmanage].include?(file_status) - file = Pathname.new(temp_target_dir) + relative_file_path - file.dirname.mkpath - PDK::Util::Filesystem.write_file(file, file_content) - end - - # Add information about the template used to generate the module to the - # metadata (for a future update command). - metadata.update!(template_dir.metadata) - - metadata.write!(File.join(temp_target_dir, 'metadata.json')) - end - rescue ArgumentError => e - raise PDK::CLI::ExitWithError, e - end - - # Only update the answers files after metadata has been written. - require 'pdk/answer_file' - if template_uri.default? && template_uri.default_ref? - # If the user specifies our default template url via the command - # line, remove the saved template-url answer so that the template_uri - # resolution can find new default URLs in the future. - PDK.config.set(%w[user module_defaults template-url], nil) if opts.key?(:'template-url') - else - # Save the template-url answers if the module was generated using a - # template/reference other than ours. - PDK.config.set(%w[user module_defaults template-url], template_uri.metadata_format) - end - - begin - if PDK::Util::Filesystem.mv(temp_target_dir, target_dir) - unless opts[:'skip-bundle-install'] - Dir.chdir(target_dir) do - require 'pdk/util/bundler' - PDK::Util::Bundler.ensure_bundle! - end - end - - PDK.logger.info _("Module '%{name}' generated at path '%{path}'.") % { - name: opts[:module_name], - path: target_dir, - } - PDK.logger.info _( - "In your module directory, add classes with the 'pdk new class' command.", - ) - end - rescue Errno::EACCES => e - raise PDK::CLI::FatalError, _("Failed to move '%{source}' to '%{target}': %{message}") % { - source: temp_target_dir, - target: target_dir, - message: e.message, - } - end - end - - def self.username_from_login - require 'etc' - - login = Etc.getlogin || '' - login_clean = login.downcase.gsub(%r{[^0-9a-z]}i, '') - login_clean = 'username' if login_clean.empty? - - if login_clean != login - PDK.logger.debug _('Your username is not a valid Forge username. Proceeding with the username %{username}. You can fix this later in metadata.json.') % { - username: login_clean, - } - end - - login_clean - end - - def self.prepare_metadata(opts = {}) - require 'pdk/answer_file' - require 'pdk/module/metadata' - - opts[:username] = (opts[:username] || PDK.config.get_within_scopes('module_defaults.forge_username') || username_from_login).downcase - - defaults = PDK::Module::Metadata::DEFAULTS.dup - - defaults['name'] = "#{opts[:username]}-#{opts[:module_name]}" unless opts[:module_name].nil? - PDK.config.with_scoped_value('module_defaults.author') { |val| defaults['author'] = val } - PDK.config.with_scoped_value('module_defaults.license') { |val| defaults['license'] = val } - defaults['license'] = opts[:license] if opts.key?(:license) - - metadata = PDK::Module::Metadata.new(defaults) - module_interview(metadata, opts) unless opts[:'skip-interview'] - - metadata - end - - def self.prepare_module_directory(target_dir) - [ - File.join(target_dir, 'examples'), - File.join(target_dir, 'files'), - File.join(target_dir, 'manifests'), - File.join(target_dir, 'templates'), - File.join(target_dir, 'tasks'), - ].each do |dir| - begin - PDK::Util::Filesystem.mkdir_p(dir) - rescue SystemCallError => e - raise PDK::CLI::FatalError, _("Unable to create directory '%{dir}': %{message}") % { - dir: dir, - message: e.message, - } - end - end - end - - def self.module_interview(metadata, opts = {}) - require 'pdk/module/metadata' - require 'pdk/cli/util/interview' - - questions = [ - { - name: 'module_name', - question: _('If you have a name for your module, add it here.'), - help: _('This is the name that will be associated with your module, it should be relevant to the modules content.'), - required: true, - validate_pattern: %r{\A[a-z][a-z0-9_]*\Z}i, - validate_message: _('Module names must begin with a lowercase letter and can only include lowercase letters, numbers, and underscores.'), - }, - { - name: 'forge_username', - question: _('If you have a Puppet Forge username, add it here.'), - help: _('We can use this to upload your module to the Forge when it\'s complete.'), - required: true, - validate_pattern: %r{\A[a-z0-9]+\Z}i, - validate_message: _('Forge usernames can only contain lowercase letters and numbers'), - default: opts[:username], - }, - { - name: 'version', - question: _('What version is this module?'), - help: _('Puppet uses Semantic Versioning (semver.org) to version modules.'), - required: true, - validate_pattern: %r{\A[0-9]+\.[0-9]+\.[0-9]+}, - validate_message: _('Semantic Version numbers must be in the form MAJOR.MINOR.PATCH'), - default: metadata.data['version'], - forge_only: true, - }, - { - name: 'author', - question: _('Who wrote this module?'), - help: _('This is used to credit the module\'s author.'), - required: true, - default: metadata.data['author'], - }, - { - name: 'license', - question: _('What license does this module code fall under?'), - help: _('This should be an identifier from https://spdx.org/licenses/. Common values are "Apache-2.0", "MIT", or "proprietary".'), - required: true, - default: metadata.data['license'], - }, - { - name: 'operatingsystem_support', - question: _('What operating systems does this module support?'), - help: _('Use the up and down keys to move between the choices, space to select and enter to continue.'), - required: true, - type: :multi_select, - choices: PDK::Module::Metadata::OPERATING_SYSTEMS, - default: PDK::Module::Metadata::DEFAULT_OPERATING_SYSTEMS.map do |os_name| - # tty-prompt uses a 1-index - PDK::Module::Metadata::OPERATING_SYSTEMS.keys.index(os_name) + 1 - end, - }, - { - name: 'summary', - question: _('Summarize the purpose of this module in a single sentence.'), - help: _('This helps other Puppet users understand what the module does.'), - required: true, - default: metadata.data['summary'], - forge_only: true, - }, - { - name: 'source', - question: _('If there is a source code repository for this module, enter the URL here.'), - help: _('Skip this if no repository exists yet. You can update this later in the metadata.json.'), - required: true, - default: metadata.data['source'], - forge_only: true, - }, - { - name: 'project_page', - question: _('If there is a URL where others can learn more about this module, enter it here.'), - help: _('Optional. You can update this later in the metadata.json.'), - default: metadata.data['project_page'], - forge_only: true, - }, - { - name: 'issues_url', - question: _('If there is a public issue tracker for this module, enter its URL here.'), - help: _('Optional. You can update this later in the metadata.json.'), - default: metadata.data['issues_url'], - forge_only: true, - }, - ] - - prompt = TTY::Prompt.new(help_color: :cyan) - - interview = PDK::CLI::Util::Interview.new(prompt) - - if opts[:only_ask] - questions.reject! do |question| - if %w[module_name forge_username].include?(question[:name]) - metadata.data['name'] && metadata.data['name'] =~ %r{\A[a-z0-9]+-[a-z][a-z0-9_]*\Z}i - else - !opts[:only_ask].include?(question[:name]) - end - end - else - questions.reject! { |q| q[:name] == 'module_name' } if opts.key?(:module_name) - questions.reject! { |q| q[:name] == 'license' } if opts.key?(:license) - questions.reject! { |q| q[:forge_only] } unless opts[:'full-interview'] - end - - interview.add_questions(questions) - - if PDK::Util::Filesystem.file?('metadata.json') - puts _( - "\nWe need to update the metadata.json file for this module, so we\'re going to ask you %{count} " \ - "questions.\n", - ) % { - count: interview.num_questions, - } - else - puts _( - "\nWe need to create the metadata.json file for this module, so we\'re going to ask you %{count} " \ - "questions.\n", - ) % { - count: interview.num_questions, - } - end - - puts _( - 'If the question is not applicable to this module, accept the default option ' \ - 'shown after each question. You can modify any answers at any time by manually updating ' \ - "the metadata.json file.\n\n", - ) - - answers = interview.run - - if answers.nil? - PDK.logger.info _('No answers given, interview cancelled.') - exit 0 - end - - unless answers['forge_username'].nil? - opts[:username] = answers['forge_username'] - - unless answers['module_name'].nil? - opts[:module_name] = answers['module_name'] - - answers.delete('module_name') - end - - answers['name'] = "#{opts[:username]}-" + (opts[:module_name]) - answers.delete('forge_username') - end - - answers['license'] = opts[:license] if opts.key?(:license) - answers['operatingsystem_support'].flatten! if answers.key?('operatingsystem_support') - - metadata.update!(answers) - - if opts[:prompt].nil? || opts[:prompt] - require 'pdk/cli/util' - - continue = PDK::CLI::Util.prompt_for_yes( - _('Metadata will be generated based on this information, continue?'), - prompt: prompt, - cancel_message: _('Interview cancelled; exiting.'), - ) - - unless continue - PDK.logger.info _('Process cancelled; exiting.') - exit 0 - end - end - - require 'pdk/answer_file' - PDK.config.set(%w[user module_defaults forge_username], opts[:username]) unless opts[:username].nil? - PDK.config.set(%w[user module_defaults author], answers['author']) unless answers['author'].nil? - PDK.config.set(%w[user module_defaults license], answers['license']) unless answers['license'].nil? - end - end - end -end +require 'pdk' + +module PDK + module Generate + class Module + def self.validate_options(opts) + require 'pdk/cli/util/option_validator' + + unless PDK::CLI::Util::OptionValidator.valid_module_name?(opts[:module_name]) + error_msg = _( + "'%{module_name}' is not a valid module name.\n" \ + 'Module names must begin with a lowercase letter and can only include lowercase letters, digits, and underscores.', + ) % { module_name: opts[:module_name] } + raise PDK::CLI::ExitWithError, error_msg + end + + target_dir = PDK::Util::Filesystem.expand_path(opts[:target_dir]) + raise PDK::CLI::ExitWithError, _("The destination directory '%{dir}' already exists") % { dir: target_dir } if PDK::Util::Filesystem.exist?(target_dir) + end + + def self.invoke(opts = {}) + require 'pdk/util' + require 'pdk/util/template_uri' + require 'pathname' + + validate_options(opts) unless opts[:module_name].nil? + + metadata = prepare_metadata(opts) + + target_dir = PDK::Util::Filesystem.expand_path(opts[:target_dir] || opts[:module_name]) + parent_dir = File.dirname(target_dir) + + begin + test_file = File.join(parent_dir, '.pdk-test-writable') + PDK::Util::Filesystem.write_file(test_file, 'This file was created by the Puppet Development Kit to test if this folder was writable, you can safely remove this file.') + PDK::Util::Filesystem.rm_f(test_file) + rescue Errno::EACCES + raise PDK::CLI::FatalError, _("You do not have permission to write to '%{parent_dir}'") % { + parent_dir: parent_dir, + } + end + + temp_target_dir = PDK::Util.make_tmpdir_name('pdk-module-target') + + prepare_module_directory(temp_target_dir) + + template_uri = PDK::Util::TemplateURI.new(opts) + + if template_uri.default? && template_uri.default_ref? + PDK.logger.info _('Using the default template-url and template-ref.') + else + PDK.logger.info _( + "Using the %{method} template-url and template-ref '%{template_uri}'." % { + method: opts.key?(:'template-url') ? _('specified') : _('saved'), + template_uri: template_uri.metadata_format, + }, + ) + end + + begin + context = PDK::Context::None.new(temp_target_dir) + PDK::Template.with(template_uri, context) do |template_dir| + template_dir.render_new_module(metadata.data['name'], metadata.data) do |relative_file_path, file_content, file_status| + next if [:delete, :unmanage].include?(file_status) + file = Pathname.new(temp_target_dir) + relative_file_path + file.dirname.mkpath + PDK::Util::Filesystem.write_file(file, file_content) + end + + # Add information about the template used to generate the module to the + # metadata (for a future update command). + metadata.update!(template_dir.metadata) + + metadata.write!(File.join(temp_target_dir, 'metadata.json')) + end + rescue ArgumentError => e + raise PDK::CLI::ExitWithError, e + end + + # Only update the answers files after metadata has been written. + require 'pdk/answer_file' + if template_uri.default? && template_uri.default_ref? + # If the user specifies our default template url via the command + # line, remove the saved template-url answer so that the template_uri + # resolution can find new default URLs in the future. + PDK.config.set(%w[user module_defaults template-url], nil) if opts.key?(:'template-url') + else + # Save the template-url answers if the module was generated using a + # template/reference other than ours. + PDK.config.set(%w[user module_defaults template-url], template_uri.metadata_format) + end + + begin + if PDK::Util::Filesystem.mv(temp_target_dir, target_dir) + unless opts[:'skip-bundle-install'] + Dir.chdir(target_dir) do + require 'pdk/util/bundler' + PDK::Util::Bundler.ensure_bundle! + end + end + + PDK.logger.info _("Module '%{name}' generated at path '%{path}'.") % { + name: opts[:module_name], + path: target_dir, + } + PDK.logger.info _( + "In your module directory, add classes with the 'pdk new class' command.", + ) + end + rescue Errno::EACCES => e + raise PDK::CLI::FatalError, _("Failed to move '%{source}' to '%{target}': %{message}") % { + source: temp_target_dir, + target: target_dir, + message: e.message, + } + end + end + + def self.username_from_login + require 'etc' + + login = Etc.getlogin || '' + login_clean = login.downcase.gsub(%r{[^0-9a-z]}i, '') + login_clean = 'username' if login_clean.empty? + + if login_clean != login + PDK.logger.debug _('Your username is not a valid Forge username. Proceeding with the username %{username}. You can fix this later in metadata.json.') % { + username: login_clean, + } + end + + login_clean + end + + def self.prepare_metadata(opts = {}) + require 'pdk/answer_file' + require 'pdk/module/metadata' + + opts[:username] = (opts[:username] || PDK.config.get_within_scopes('module_defaults.forge_username') || username_from_login).downcase + + defaults = PDK::Module::Metadata::DEFAULTS.dup + + defaults['name'] = "#{opts[:username]}-#{opts[:module_name]}" unless opts[:module_name].nil? + PDK.config.with_scoped_value('module_defaults.author') { |val| defaults['author'] = val } + PDK.config.with_scoped_value('module_defaults.license') { |val| defaults['license'] = val } + defaults['license'] = opts[:license] if opts.key?(:license) + + metadata = PDK::Module::Metadata.new(defaults) + module_interview(metadata, opts) unless opts[:'skip-interview'] + + metadata + end + + def self.prepare_module_directory(target_dir) + [ + File.join(target_dir, 'examples'), + File.join(target_dir, 'files'), + File.join(target_dir, 'manifests'), + File.join(target_dir, 'templates'), + File.join(target_dir, 'tasks'), + ].each do |dir| + begin + PDK::Util::Filesystem.mkdir_p(dir) + rescue SystemCallError => e + raise PDK::CLI::FatalError, _("Unable to create directory '%{dir}': %{message}") % { + dir: dir, + message: e.message, + } + end + end + end + + def self.module_interview(metadata, opts = {}) + require 'pdk/module/metadata' + require 'pdk/cli/util/interview' + + questions = [ + { + name: 'module_name', + question: _('If you have a name for your module, add it here.'), + help: _('This is the name that will be associated with your module, it should be relevant to the modules content.'), + required: true, + validate_pattern: %r{\A[a-z][a-z0-9_]*\Z}i, + validate_message: _('Module names must begin with a lowercase letter and can only include lowercase letters, numbers, and underscores.'), + }, + { + name: 'forge_username', + question: _('If you have a Puppet Forge username, add it here.'), + help: _('We can use this to upload your module to the Forge when it\'s complete.'), + required: true, + validate_pattern: %r{\A[a-z0-9]+\Z}i, + validate_message: _('Forge usernames can only contain lowercase letters and numbers'), + default: opts[:username], + }, + { + name: 'version', + question: _('What version is this module?'), + help: _('Puppet uses Semantic Versioning (semver.org) to version modules.'), + required: true, + validate_pattern: %r{\A[0-9]+\.[0-9]+\.[0-9]+}, + validate_message: _('Semantic Version numbers must be in the form MAJOR.MINOR.PATCH'), + default: metadata.data['version'], + forge_only: true, + }, + { + name: 'author', + question: _('Who wrote this module?'), + help: _('This is used to credit the module\'s author.'), + required: true, + default: metadata.data['author'], + }, + { + name: 'license', + question: _('What license does this module code fall under?'), + help: _('This should be an identifier from https://spdx.org/licenses/. Common values are "Apache-2.0", "MIT", or "proprietary".'), + required: true, + default: metadata.data['license'], + }, + { + name: 'operatingsystem_support', + question: _('What operating systems does this module support?'), + help: _('Use the up and down keys to move between the choices, space to select and enter to continue.'), + required: true, + type: :multi_select, + choices: PDK::Module::Metadata::OPERATING_SYSTEMS, + default: PDK::Module::Metadata::DEFAULT_OPERATING_SYSTEMS.map do |os_name| + # tty-prompt uses a 1-index + PDK::Module::Metadata::OPERATING_SYSTEMS.keys.index(os_name) + 1 + end, + }, + { + name: 'summary', + question: _('Summarize the purpose of this module in a single sentence.'), + help: _('This helps other Puppet users understand what the module does.'), + required: true, + default: metadata.data['summary'], + forge_only: true, + }, + { + name: 'source', + question: _('If there is a source code repository for this module, enter the URL here.'), + help: _('Skip this if no repository exists yet. You can update this later in the metadata.json.'), + required: true, + default: metadata.data['source'], + forge_only: true, + }, + { + name: 'project_page', + question: _('If there is a URL where others can learn more about this module, enter it here.'), + help: _('Optional. You can update this later in the metadata.json.'), + default: metadata.data['project_page'], + forge_only: true, + }, + { + name: 'issues_url', + question: _('If there is a public issue tracker for this module, enter its URL here.'), + help: _('Optional. You can update this later in the metadata.json.'), + default: metadata.data['issues_url'], + forge_only: true, + }, + ] + + prompt = TTY::Prompt.new(help_color: :cyan) + + interview = PDK::CLI::Util::Interview.new(prompt) + + if opts[:only_ask] + questions.reject! do |question| + if %w[module_name forge_username].include?(question[:name]) + metadata.data['name'] && metadata.data['name'] =~ %r{\A[a-z0-9]+-[a-z][a-z0-9_]*\Z}i + else + !opts[:only_ask].include?(question[:name]) + end + end + else + questions.reject! { |q| q[:name] == 'module_name' } if opts.key?(:module_name) + questions.reject! { |q| q[:name] == 'license' } if opts.key?(:license) + questions.reject! { |q| q[:forge_only] } unless opts[:'full-interview'] + end + + interview.add_questions(questions) + + if PDK::Util::Filesystem.file?('metadata.json') + puts _( + "\nWe need to update the metadata.json file for this module, so we\'re going to ask you %{count} " \ + "questions.\n", + ) % { + count: interview.num_questions, + } + else + puts _( + "\nWe need to create the metadata.json file for this module, so we\'re going to ask you %{count} " \ + "questions.\n", + ) % { + count: interview.num_questions, + } + end + + puts _( + 'If the question is not applicable to this module, accept the default option ' \ + 'shown after each question. You can modify any answers at any time by manually updating ' \ + "the metadata.json file.\n\n", + ) + + answers = interview.run + + if answers.nil? + PDK.logger.info _('No answers given, interview cancelled.') + exit 0 + end + + unless answers['forge_username'].nil? + opts[:username] = answers['forge_username'] + + unless answers['module_name'].nil? + opts[:module_name] = answers['module_name'] + + answers.delete('module_name') + end + + answers['name'] = "#{opts[:username]}-" + (opts[:module_name]) + answers.delete('forge_username') + end + + answers['license'] = opts[:license] if opts.key?(:license) + answers['operatingsystem_support'].flatten! if answers.key?('operatingsystem_support') + + metadata.update!(answers) + + if opts[:prompt].nil? || opts[:prompt] + require 'pdk/cli/util' + + continue = PDK::CLI::Util.prompt_for_yes( + _('Metadata will be generated based on this information, continue?'), + prompt: prompt, + cancel_message: _('Interview cancelled; exiting.'), + ) + + unless continue + PDK.logger.info _('Process cancelled; exiting.') + exit 0 + end + end + + require 'pdk/answer_file' + PDK.config.set(%w[user module_defaults forge_username], opts[:username]) unless opts[:username].nil? + PDK.config.set(%w[user module_defaults author], answers['author']) unless answers['author'].nil? + PDK.config.set(%w[user module_defaults license], answers['license']) unless answers['license'].nil? + end + end + end +end