require "berkshelf" require_relative "config" require_relative "commands/shelf" module Berkshelf class Cli < Thor # This is the main entry point for the CLI. It exposes the method {#execute!} to # start the CLI. # # @note the arity of {#initialize} and {#execute!} are extremely important for testing purposes. It # is a requirement to perform in-process testing with Aruba. In process testing is much faster # than spawning a new Ruby process for each test. class Runner def initialize(argv, stdin = STDIN, stdout = STDOUT, stderr = STDERR, kernel = Kernel) @argv, @stdin, @stdout, @stderr, @kernel = argv, stdin, stdout, stderr, kernel end def execute! $stdin = @stdin $stdout = @stdout $stderr = @stderr Berkshelf::Cli.start(@argv) @kernel.exit(0) rescue Berkshelf::BerkshelfError => e Berkshelf.ui.error e Berkshelf.ui.error "\t" + e.backtrace.join("\n\t") if ENV["BERKSHELF_DEBUG"] @kernel.exit(e.status_code) rescue => e Berkshelf.ui.error "#{e.class} #{e}" Berkshelf.ui.error "\t" + e.backtrace.join("\n\t") if ENV["BERKSHELF_DEBUG"] @kernel.exit(47) end end class << self def dispatch(meth, given_args, given_opts, config) if given_args.length > 1 && !(given_args & Thor::HELP_MAPPINGS).empty? command = given_args.first if subcommands.include?(command) super(meth, [command, "help"].compact, nil, config) else super(meth, ["help", command].compact, nil, config) end else super Berkshelf.formatter.cleanup_hook unless config[:current_command].name == "help" end end end def initialize(*args) super(*args) if @options[:config] unless File.exist?(@options[:config]) raise ConfigNotFound.new(:berkshelf, @options[:config]) end Berkshelf.config = Berkshelf::Config.from_file(@options[:config]) end if @options[:debug] ENV["BERKSHELF_DEBUG"] = "true" Berkshelf.logger.level = ::Logger::DEBUG end if @options[:quiet] Berkshelf.ui.mute! end Berkshelf.set_format @options[:format] @options = options.dup # unfreeze frozen options Hash from Thor end namespace "berkshelf" map "in" => :install map "up" => :upload map "ud" => :update map "ls" => :list map "book" => :cookbook map ["ver", "-v", "--version"] => :version default_task :install class_option :config, type: :string, desc: "Path to Berkshelf configuration to use.", aliases: "-c", banner: "PATH" class_option :format, type: :string, default: "human", desc: "Output format to use.", aliases: "-F", banner: "FORMAT" class_option :quiet, type: :boolean, desc: "Silence all informational output.", aliases: "-q", default: false class_option :debug, type: :boolean, desc: "Output debug information", aliases: "-d", default: false method_option :except, type: :array, desc: "Exclude cookbooks that are in these groups.", aliases: "-e" method_option :only, type: :array, desc: "Only cookbooks that are in these groups.", aliases: "-o" method_option :berksfile, type: :string, default: nil, desc: "Path to a Berksfile to operate off of.", aliases: "-b", banner: "PATH" method_option :path, type: :string, aliases: "-p", hide: true desc "install", "Install the cookbooks specified in the Berksfile" def install berksfile = Berksfile.from_options(options) berksfile.install end method_option :berksfile, type: :string, default: nil, desc: "Path to a Berksfile to operate off of.", aliases: "-b", banner: "PATH" method_option :except, type: :array, desc: "Exclude cookbooks that are in these groups.", aliases: "-e" method_option :only, type: :array, desc: "Only cookbooks that are in these groups.", aliases: "-o" desc "update [COOKBOOKS]", "Update the cookbooks (and dependencies) specified in the Berksfile" def update(*cookbook_names) berksfile = Berksfile.from_options(options) berksfile.update(*cookbook_names) end method_option :berksfile, type: :string, default: nil, desc: "Path to a Berksfile to operate off of.", aliases: "-b", banner: "PATH" method_option :except, type: :array, desc: "Exclude cookbooks that are in these groups.", aliases: "-e" method_option :only, type: :array, desc: "Only cookbooks that are in these groups.", aliases: "-o" method_option :no_freeze, type: :boolean, default: false, desc: "Do not freeze uploaded cookbook(s)." method_option :force, type: :boolean, default: false, desc: "Upload all cookbooks even if a frozen one exists on the Chef Server." method_option :ssl_verify, type: :boolean, default: nil, desc: "Disable/Enable SSL verification when uploading cookbooks." method_option :skip_syntax_check, type: :boolean, default: false, desc: "Skip Ruby syntax check when uploading cookbooks.", aliases: "-s" method_option :halt_on_frozen, type: :boolean, default: false, desc: "Exit with a non zero exit code if the Chef Server already has the version of the cookbook(s)." desc "upload [COOKBOOKS]", "Upload the cookbook specified in the Berksfile to the Chef Server" def upload(*names) berksfile = Berksfile.from_options(options) options[:freeze] = !options[:no_freeze] options[:validate] = false if options[:skip_syntax_check] berksfile.upload(names, options.each_with_object({}) { |(k, v), m| m[k.to_sym] = v }) end method_option :envfile, type: :string, desc: "Path to a JSON environment file to update.", aliases: "-f" method_option :lockfile, type: :string, default: Berkshelf::Lockfile::DEFAULT_FILENAME, desc: "Path to a Berksfile.lock to operate off of.", aliases: "-b", banner: "PATH" method_option :ssl_verify, type: :boolean, default: nil, desc: "Disable/Enable SSL verification when locking cookbooks." desc "apply ENVIRONMENT", "Apply version locks from Berksfile.lock to a Chef environment" def apply(environment_name) unless File.exist?(options[:lockfile]) raise LockfileNotFound, "No lockfile found at #{options[:lockfile]}" end lockfile = Lockfile.from_file(options[:lockfile]) lock_options = Hash[options].each_with_object({}) { |(k, v), m| m[k.to_sym] = v } lockfile.apply(environment_name, lock_options) end method_option :berksfile, type: :string, default: nil, desc: "Path to a Berksfile to operate off of.", aliases: "-b", banner: "PATH" method_option :except, type: :array, desc: "Exclude cookbooks that are in these groups.", aliases: "-e" method_option :only, type: :array, desc: "Only cookbooks that are in these groups.", aliases: "-o" method_option :all, type: :boolean, desc: "Include cookbooks that don't satisfy the version constraints.", aliases: "-a", default: false desc "outdated [COOKBOOKS]", "List dependencies that have new versions available that satisfy their constraints" def outdated(*names) berksfile = Berksfile.from_options(options) outdated = berksfile.outdated(*names, include_non_satisfying: options[:all]) Berkshelf.formatter.outdated(outdated) end method_option :source, type: :string, default: Berksfile::DEFAULT_API_URL, desc: "URL to search for sources", banner: "URL" desc "search NAME", "Search the remote source for cookbooks matching the partial name" def search(name) source = Source.new(nil, options[:source]) cookbooks = source.search(name) Berkshelf.formatter.search(cookbooks) end method_option :berksfile, type: :string, default: nil, desc: "Path to a Berksfile to operate off of.", aliases: "-b", banner: "PATH" method_option :except, type: :array, desc: "Exclude cookbooks that are in these groups.", aliases: "-e" method_option :only, type: :array, desc: "Only cookbooks that are in these groups.", aliases: "-o" desc "list", "List cookbooks and their dependencies specified by your Berksfile" def list berksfile = Berksfile.from_options(options) Berkshelf.formatter.list(berksfile.list) end method_option :berksfile, type: :string, default: nil, desc: "Path to a Berksfile to operate off of.", aliases: "-b", banner: "PATH" desc "info [COOKBOOK]", "Display name, author, copyright, and dependency information about a cookbook" def info(name) berksfile = Berksfile.from_options(options) cookbook = berksfile.retrieve_locked(name) Berkshelf.formatter.info(cookbook) end method_option :berksfile, type: :string, default: nil, desc: "Path to a Berksfile to operate off of.", aliases: "-b", banner: "PATH" desc "show [COOKBOOK]", "Display the path to a cookbook on disk" def show(name) berksfile = Berksfile.from_options(options) cookbook = berksfile.retrieve_locked(name) Berkshelf.formatter.show(cookbook) end method_option :berksfile, type: :string, default: nil, desc: "Path to a Berksfile to operate off of.", aliases: "-b", banner: "PATH" desc "contingent COOKBOOK", "List all cookbooks that depend on the given cookbook in your Berksfile" def contingent(name) berksfile = Berksfile.from_options(options) dependencies = berksfile.cookbooks.select do |cookbook| cookbook.dependencies.include?(name) end if dependencies.empty? Berkshelf.formatter.msg "There are no cookbooks in this Berksfile contingent upon '#{name}'." else Berkshelf.formatter.msg "Cookbooks in this Berksfile contingent upon '#{name}':" print_list(dependencies) end end method_option :berksfile, type: :string, default: nil, desc: "Path to a Berksfile to operate off of.", aliases: "-b", banner: "PATH" method_option :except, type: :array, desc: "Exclude cookbooks that are in these groups.", aliases: "-e" method_option :only, type: :array, desc: "Only cookbooks that are in these groups.", aliases: "-o" desc "package [PATH]", "Vendor and archive the dependencies of a Berksfile" def package(path = nil) if path.nil? path ||= File.join(Dir.pwd, "cookbooks-#{Time.now.to_i}.tar.gz") else path = File.expand_path(path) end berksfile = Berksfile.from_options(options) berksfile.package(path) end method_option :except, type: :array, desc: "Exclude cookbooks that are in these groups.", aliases: "-e" method_option :delete, type: :boolean, desc: "Clean the target directory before vendoring", default: false method_option :only, type: :array, desc: "Only cookbooks that are in these groups.", aliases: "-o" method_option :berksfile, type: :string, default: nil, desc: "Path to a Berksfile to operate off of.", aliases: "-b", banner: "PATH" desc "vendor [PATH]", "Vendor the cookbooks specified by the Berksfile into a directory" def vendor(path = File.join(Dir.pwd, "berks-cookbooks")) berksfile = Berkshelf::Berksfile.from_options(options) berksfile.vendor(path) end method_option :berksfile, type: :string, default: nil desc "verify", "Perform a quick validation on the contents of your resolved cookbooks" def verify berksfile = Berksfile.from_options(options) berksfile.verify Berkshelf.formatter.msg "Verified." end method_option :berksfile, type: :string, default: nil, desc: "Path to a Berksfile to operate off of.", aliases: "-b", banner: "PATH" method_option :outfile, type: :string, default: "graph.png", desc: "The name of the output file", aliases: "-o", banner: "NAME" method_option :outfile_format, type: :string, default: "png", desc: "The format of the output file, either png or dot.", aliases: "-f", banner: "FORMAT" desc "viz", "Visualize the dependency graph" def viz berksfile = Berksfile.from_options(options) path = berksfile.viz(options[:outfile], options[:outfile_format]) Berkshelf.ui.info(path) end desc "version", "Display version" def version Berkshelf.formatter.version end private # Print a list of the given cookbooks. This is used by various # methods like {list} and {contingent}. # # @param [Array] cookbooks # def print_list(cookbooks) Array(cookbooks).sort.each do |cookbook| Berkshelf.formatter.msg " * #{cookbook.cookbook_name} (#{cookbook.version})" end end end end