lib/berkshelf/cli.rb
require_relative "../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<CachedCookbook>] cookbooks
#
def print_list(cookbooks)
Array(cookbooks).sort.each do |cookbook|
Berkshelf.formatter.msg " * #{cookbook.cookbook_name} (#{cookbook.version})"
end
end
end
end