# frozen_string_literal: true require 'bolt/project_manager/migrator' module Bolt class ProjectManager class ModuleMigrator < Migrator def migrate(project, configured_modulepath) return true if project.managed_moduledir.exist? @outputter.print_message "Migrating project modules\n\n" config = project.project_file puppetfile = project.puppetfile managed_moduledir = project.managed_moduledir new_modulepath = [(project.path + 'modules').to_s] old_modulepath = [(project.path + 'modules').to_s, (project.path + 'site-modules').to_s, (project.path + 'site').to_s] # Notify user to manually migrate modules if using non-default modulepath if configured_modulepath != new_modulepath && configured_modulepath != old_modulepath @outputter.print_action_step( "Project has a non-default configured modulepath, unable to automatically "\ "migrate project modules. To migrate project modules manually, see "\ "http://pup.pt/bolt-modules" ) true # Migrate modules from Puppetfile elsif File.exist?(puppetfile) migrate_modules_from_puppetfile(config, puppetfile, managed_moduledir, old_modulepath) # Migrate modules to updated modulepath else consolidate_modules(old_modulepath) update_project_config([], config) end end # Migrates modules by reading a Puppetfile and prompting the user for # which ones are direct dependencies for the project. Once the user has # selected the direct dependencies, this will resolve the modules, write a # new Puppetfile, install the modules, and then move any remaining modules # to the new moduledir. # private def migrate_modules_from_puppetfile(config, puppetfile_path, managed_moduledir, modulepath) require 'bolt/module_installer/installer' require 'bolt/module_installer/puppetfile' require 'bolt/module_installer/resolver' require 'bolt/module_installer/specs' begin @outputter.print_action_step("Parsing Puppetfile at #{puppetfile_path}") puppetfile = Bolt::ModuleInstaller::Puppetfile.parse(puppetfile_path, skip_unsupported_modules: true) rescue Bolt::Error => e @outputter.print_action_error("#{e.message}\nSkipping module migration.") return false end # Prompt for direct dependencies modules = select_modules(puppetfile.modules) # Create specs to resolve from specs = Bolt::ModuleInstaller::Specs.new(modules.map(&:to_hash)) @outputter.start_spin # Attempt to resolve dependencies begin @outputter.print_message('') @outputter.print_action_step("Resolving module dependencies, this might take a moment") puppetfile = Bolt::ModuleInstaller::Resolver.new.resolve(specs) rescue Bolt::Error => e @outputter.print_action_error("#{e.message}\nSkipping module migration.") return false end migrate_managed_modules(puppetfile, puppetfile_path, managed_moduledir) @outputter.stop_spin # Move remaining modules to 'modules' consolidate_modules(modulepath) # Delete old modules that are now managed delete_modules(modulepath.first, puppetfile.modules) # Add modules to project update_project_config(modules.map(&:to_hash), config) end # Migrates the managed modules. If modules were selected to be managed, # the Puppetfile is rewritten and modules are installed. If no modules # were selected, the Puppetfile is deleted. # private def migrate_managed_modules(puppetfile, puppetfile_path, managed_moduledir) if puppetfile.modules.any? # Show the new Puppetfile content message = "Generated new Puppetfile content:\n\n" message += puppetfile.modules.map(&:to_spec).join("\n").to_s @outputter.print_action_step(message) # Write Puppetfile @outputter.print_action_step("Updating Puppetfile at #{puppetfile_path}") puppetfile.write(puppetfile_path, managed_moduledir) # Install Puppetfile @outputter.print_action_step("Syncing modules from #{puppetfile_path} to #{managed_moduledir}") Bolt::ModuleInstaller::Installer.new.install(puppetfile_path, managed_moduledir) else @outputter.print_action_step( "Project does not include any managed modules, deleting Puppetfile "\ "at #{puppetfile_path}" ) FileUtils.rm(puppetfile_path) end end # Prompts the user to select modules, returning a list of # the selected modules. # private def select_modules(modules) @outputter.print_action_step( "Select modules that are direct dependencies of your project. Bolt will "\ "automatically manage dependencies for each module selected, so do not "\ "select a module's dependencies unless you use content from it directly "\ "in your project." ) all = Bolt::Util.prompt_yes_no("Select all modules?", @outputter) return modules if all modules.select do |mod| Bolt::Util.prompt_yes_no("Select #{mod.full_name}?", @outputter) end end # Consolidates all modules on the modulepath to 'modules'. # private def consolidate_modules(modulepath) moduledir, *sources = modulepath sources.select! { |source| Dir.exist?(source) } if sources.any? @outputter.print_action_step( "Moving modules from #{sources.join(', ')} to #{moduledir}" ) FileUtils.mkdir_p(moduledir) move_modules(moduledir, sources) end end # Moves modules from a list of source directories to the specified # moduledir, deleting the source directory after it's done. # private def move_modules(moduledir, sources) moduledir = Pathname.new(moduledir) sources.each do |source| source = Pathname.new(source) source.each_child do |mod| next unless mod.directory? next if (moduledir + mod.basename).directory? FileUtils.mv(mod, moduledir) end FileUtils.rm_r(source) end end # Deletes modules from a specified directory. # private def delete_modules(moduledir, modules) @outputter.print_action_step("Cleaning up #{moduledir}") moduledir = Pathname.new(moduledir) modules.each do |mod| path = moduledir + mod.name FileUtils.rm_r(path) if path.directory? end end # Adds a list of modules to the project configuration file. # private def update_project_config(modules, config_file) @outputter.print_action_step("Updating project configuration at #{config_file}") data = Bolt::Util.read_optional_yaml_hash(config_file, 'project') data.merge!('modules' => modules) data.delete('modulepath') begin File.write(config_file, data.to_yaml) true rescue StandardError => e raise Bolt::FileError.new( "Unable to write to #{config_file}: #{e.message}", config_file ) end end end end end