From 5b90d09398021b0c6f6dac2081c1d884c8270f51 Mon Sep 17 00:00:00 2001 From: Tim Smith Date: Fri, 13 Jul 2018 13:12:25 -0700 Subject: Move all plugin logic into knife supermarket We want to deprecate knife cookbook site. This moves all the logic into the knife supermarket plugins and has the legacy knife cookbook site plugins inherit from that. Signed-off-by: Tim Smith --- lib/chef/knife/cookbook_site_download.rb | 98 +----------- lib/chef/knife/cookbook_site_install.rb | 171 +------------------- lib/chef/knife/cookbook_site_list.rb | 42 +---- lib/chef/knife/cookbook_site_search.rb | 31 +--- lib/chef/knife/cookbook_site_share.rb | 145 +---------------- lib/chef/knife/cookbook_site_show.rb | 44 +----- lib/chef/knife/cookbook_site_unshare.rb | 39 +---- lib/chef/knife/supermarket_download.rb | 101 +++++++++++- lib/chef/knife/supermarket_install.rb | 175 ++++++++++++++++++++- lib/chef/knife/supermarket_list.rb | 44 +++++- lib/chef/knife/supermarket_search.rb | 34 +++- lib/chef/knife/supermarket_share.rb | 148 ++++++++++++++++- lib/chef/knife/supermarket_show.rb | 46 +++++- lib/chef/knife/supermarket_unshare.rb | 42 ++++- spec/unit/knife/cookbook_site_download_spec.rb | 150 ------------------ spec/unit/knife/cookbook_site_install_spec.rb | 200 ----------------------- spec/unit/knife/cookbook_site_share_spec.rb | 209 ------------------------- spec/unit/knife/cookbook_site_unshare_spec.rb | 77 --------- spec/unit/knife/supermarket_download_spec.rb | 152 ++++++++++++++++++ spec/unit/knife/supermarket_install_spec.rb | 201 ++++++++++++++++++++++++ spec/unit/knife/supermarket_share_spec.rb | 209 +++++++++++++++++++++++++ spec/unit/knife/supermarket_unshare_spec.rb | 78 +++++++++ 22 files changed, 1225 insertions(+), 1211 deletions(-) delete mode 100644 spec/unit/knife/cookbook_site_download_spec.rb delete mode 100644 spec/unit/knife/cookbook_site_install_spec.rb delete mode 100644 spec/unit/knife/cookbook_site_share_spec.rb delete mode 100644 spec/unit/knife/cookbook_site_unshare_spec.rb create mode 100644 spec/unit/knife/supermarket_download_spec.rb create mode 100644 spec/unit/knife/supermarket_install_spec.rb create mode 100644 spec/unit/knife/supermarket_share_spec.rb create mode 100644 spec/unit/knife/supermarket_unshare_spec.rb diff --git a/lib/chef/knife/cookbook_site_download.rb b/lib/chef/knife/cookbook_site_download.rb index ad4a2a83a2..8e9022ce53 100644 --- a/lib/chef/knife/cookbook_site_download.rb +++ b/lib/chef/knife/cookbook_site_download.rb @@ -17,106 +17,18 @@ # require "chef/knife" +require "chef/knife/supermarket_download" class Chef class Knife - class CookbookSiteDownload < Knife + class CookbookSiteDownload < Knife::SupermarketDownload - deps do - require "fileutils" - end + # Handle the subclassing (knife doesn't do this :() + dependency_loaders.concat(superclass.dependency_loaders) + options.merge!(superclass.options) banner "knife cookbook site download COOKBOOK [VERSION] (options)" category "cookbook site" - - option :file, - short: "-f FILE", - long: "--file FILE", - description: "The filename to write to" - - option :force, - long: "--force", - description: "Force download deprecated version" - - option :supermarket_site, - short: "-m SUPERMARKET_SITE", - long: "--supermarket-site SUPERMARKET_SITE", - description: "Supermarket Site", - default: "https://supermarket.chef.io", - proc: Proc.new { |supermarket| Chef::Config[:knife][:supermarket_site] = supermarket } - - def run - if current_cookbook_deprecated? - message = "DEPRECATION: This cookbook has been deprecated. " - replacement = replacement_cookbook - if !replacement.to_s.strip.empty? - message << "It has been replaced by #{replacement}." - else - message << "No replacement has been defined." - end - ui.warn message - - unless config[:force] - ui.warn "Use --force to force download deprecated cookbook." - return - end - end - - download_cookbook - end - - def version - @version = desired_cookbook_data["version"] - end - - private - - def cookbooks_api_url - "#{config[:supermarket_site]}/api/v1/cookbooks" - end - - def current_cookbook_data - @current_cookbook_data ||= begin - noauth_rest.get "#{cookbooks_api_url}/#{@name_args[0]}" - end - end - - def current_cookbook_deprecated? - current_cookbook_data["deprecated"] == true - end - - def desired_cookbook_data - @desired_cookbook_data ||= begin - uri = if @name_args.length == 1 - current_cookbook_data["latest_version"] - else - specific_cookbook_version_url - end - - noauth_rest.get uri - end - end - - def download_cookbook - ui.info "Downloading #{@name_args[0]} from Supermarket at version #{version} to #{download_location}" - tf = noauth_rest.streaming_request(desired_cookbook_data["file"]) - - ::FileUtils.cp tf.path, download_location - ui.info "Cookbook saved: #{download_location}" - end - - def download_location - config[:file] ||= File.join Dir.pwd, "#{@name_args[0]}-#{version}.tar.gz" - config[:file] - end - - def replacement_cookbook - File.basename(current_cookbook_data["replacement"] || "") - end - - def specific_cookbook_version_url - "#{cookbooks_api_url}/#{@name_args[0]}/versions/#{@name_args[1].tr('.', '_')}" - end end end end diff --git a/lib/chef/knife/cookbook_site_install.rb b/lib/chef/knife/cookbook_site_install.rb index 18cddb19f4..18e75442ca 100644 --- a/lib/chef/knife/cookbook_site_install.rb +++ b/lib/chef/knife/cookbook_site_install.rb @@ -17,180 +17,19 @@ # require "chef/knife" -require "chef/exceptions" -require "shellwords" -require "mixlib/archive" +require "chef/knife/supermarket_install" class Chef class Knife - class CookbookSiteInstall < Knife + class CookbookSiteInstall < Knife::SupermarketInstall - deps do - require "chef/mixin/shell_out" - require "chef/knife/core/cookbook_scm_repo" - require "chef/cookbook/metadata" - end + # Handle the subclassing (knife doesn't do this :() + dependency_loaders.concat(superclass.dependency_loaders) + options.merge!(superclass.options) banner "knife cookbook site install COOKBOOK [VERSION] (options)" category "cookbook site" - option :no_deps, - short: "-D", - long: "--skip-dependencies", - boolean: true, - default: false, - description: "Skips automatic dependency installation." - - option :cookbook_path, - short: "-o PATH:PATH", - long: "--cookbook-path PATH:PATH", - description: "A colon-separated path to look for cookbooks in", - proc: lambda { |o| o.split(":") } - - option :default_branch, - short: "-B BRANCH", - long: "--branch BRANCH", - description: "Default branch to work with", - default: "master" - - option :use_current_branch, - short: "-b", - long: "--use-current-branch", - description: "Use the current branch", - boolean: true, - default: false - - option :supermarket_site, - short: "-m SUPERMARKET_SITE", - long: "--supermarket-site SUPERMARKET_SITE", - description: "Supermarket Site", - default: "https://supermarket.chef.io", - proc: Proc.new { |supermarket| Chef::Config[:knife][:supermarket_site] = supermarket } - - attr_reader :cookbook_name - attr_reader :vendor_path - - def run - extend Chef::Mixin::ShellOut - - if config[:cookbook_path] - Chef::Config[:cookbook_path] = config[:cookbook_path] - else - config[:cookbook_path] = Chef::Config[:cookbook_path] - end - - @cookbook_name = parse_name_args! - # Check to ensure we have a valid source of cookbooks before continuing - # - @install_path = File.expand_path(Array(config[:cookbook_path]).first) - ui.info "Installing #{@cookbook_name} to #{@install_path}" - - @repo = CookbookSCMRepo.new(@install_path, ui, config) - # cookbook_path = File.join(vendor_path, name_args[0]) - upstream_file = File.join(@install_path, "#{@cookbook_name}.tar.gz") - - @repo.sanity_check - unless config[:use_current_branch] - @repo.reset_to_default_state - @repo.prepare_to_import(@cookbook_name) - end - - downloader = download_cookbook_to(upstream_file) - clear_existing_files(File.join(@install_path, @cookbook_name)) - extract_cookbook(upstream_file, downloader.version) - - # TODO: it'd be better to store these outside the cookbook repo and - # keep them around, e.g., in ~/Library/Caches on OS X. - ui.info("Removing downloaded tarball") - File.unlink(upstream_file) - - if @repo.finalize_updates_to(@cookbook_name, downloader.version) - unless config[:use_current_branch] - @repo.reset_to_default_state - end - @repo.merge_updates_from(@cookbook_name, downloader.version) - else - unless config[:use_current_branch] - @repo.reset_to_default_state - end - end - - unless config[:no_deps] - preferred_metadata.dependencies.each_key do |cookbook| - # Doesn't do versions.. yet - nv = self.class.new - nv.config = config - nv.name_args = [ cookbook ] - nv.run - end - end - end - - def parse_name_args! - if name_args.empty? - ui.error("Please specify a cookbook to download and install.") - exit 1 - elsif name_args.size >= 2 - unless name_args.last.match(/^(\d+)(\.\d+){1,2}$/) && name_args.size == 2 - ui.error("Installing multiple cookbooks at once is not supported.") - exit 1 - end - end - name_args.first - end - - def download_cookbook_to(download_path) - downloader = Chef::Knife::CookbookSiteDownload.new - downloader.config[:file] = download_path - downloader.config[:supermarket_site] = config[:supermarket_site] - downloader.name_args = name_args - downloader.run - downloader - end - - def extract_cookbook(upstream_file, version) - ui.info("Uncompressing #{@cookbook_name} version #{version}.") - Mixlib::Archive.new(convert_path(upstream_file)).extract(@install_path, perms: false) - end - - def clear_existing_files(cookbook_path) - ui.info("Removing pre-existing version.") - FileUtils.rmtree(cookbook_path) if File.directory?(cookbook_path) - end - - def convert_path(upstream_file) - # converts a Windows path (C:\foo) to a mingw path (/c/foo) - if ENV["MSYSTEM"] == "MINGW32" - upstream_file.sub(/^([[:alpha:]]):/, '/\1') - else - Shellwords.escape upstream_file - end - end - - # Get the preferred metadata path on disk. Chef prefers the metadata.rb - # over the metadata.json. - # - # @raise if there is no metadata in the cookbook - # - # @return [Chef::Cookbook::Metadata] - def preferred_metadata - md = Chef::Cookbook::Metadata.new - - rb = File.join(@install_path, @cookbook_name, "metadata.rb") - if File.exist?(rb) - md.from_file(rb) - return md - end - - json = File.join(@install_path, @cookbook_name, "metadata.json") - if File.exist?(json) - json = IO.read(json) - md.from_json(json) - return md - end - - raise Chef::Exceptions::MetadataNotFound.new(@install_path, @cookbook_name) - end end end end diff --git a/lib/chef/knife/cookbook_site_list.rb b/lib/chef/knife/cookbook_site_list.rb index 3b0b20a4df..b0ec22ac1e 100644 --- a/lib/chef/knife/cookbook_site_list.rb +++ b/lib/chef/knife/cookbook_site_list.rb @@ -17,49 +17,19 @@ # require "chef/knife" +require "chef/knife/supermarket_list" class Chef class Knife - class CookbookSiteList < Knife + class CookbookSiteList < Knife::SupermarketList + + # Handle the subclassing (knife doesn't do this :() + dependency_loaders.concat(superclass.dependency_loaders) + options.merge!(superclass.options) banner "knife cookbook site list (options)" category "cookbook site" - option :with_uri, - short: "-w", - long: "--with-uri", - description: "Show corresponding URIs" - - option :supermarket_site, - short: "-m SUPERMARKET_SITE", - long: "--supermarket-site SUPERMARKET_SITE", - description: "Supermarket Site", - default: "https://supermarket.chef.io", - proc: Proc.new { |supermarket| Chef::Config[:knife][:supermarket_site] = supermarket } - - def run - if config[:with_uri] - cookbooks = Hash.new - get_cookbook_list.each { |k, v| cookbooks[k] = v["cookbook"] } - ui.output(format_for_display(cookbooks)) - else - ui.msg(ui.list(get_cookbook_list.keys.sort, :columns_down)) - end - end - - def get_cookbook_list(items = 10, start = 0, cookbook_collection = {}) - cookbooks_url = "#{config[:supermarket_site]}/api/v1/cookbooks?items=#{items}&start=#{start}" - cr = noauth_rest.get(cookbooks_url) - cr["items"].each do |cookbook| - cookbook_collection[cookbook["cookbook_name"]] = cookbook - end - new_start = start + cr["items"].length - if new_start < cr["total"] - get_cookbook_list(items, new_start, cookbook_collection) - else - cookbook_collection - end - end end end end diff --git a/lib/chef/knife/cookbook_site_search.rb b/lib/chef/knife/cookbook_site_search.rb index 6a598bd22b..b5e375913f 100644 --- a/lib/chef/knife/cookbook_site_search.rb +++ b/lib/chef/knife/cookbook_site_search.rb @@ -17,38 +17,19 @@ # require "chef/knife" +require "chef/knife/supermarket_search" class Chef class Knife - class CookbookSiteSearch < Knife + class CookbookSiteSearch < Knife::SupermarketSearch + + # Handle the subclassing (knife doesn't do this :() + dependency_loaders.concat(superclass.dependency_loaders) + options.merge!(superclass.options) banner "knife cookbook site search QUERY (options)" category "cookbook site" - option :supermarket_site, - short: "-m SUPERMARKET_SITE", - long: "--supermarket-site SUPERMARKET_SITE", - description: "Supermarket Site", - default: "https://supermarket.chef.io", - proc: Proc.new { |supermarket| Chef::Config[:knife][:supermarket_site] = supermarket } - - def run - output(search_cookbook(name_args[0])) - end - - def search_cookbook(query, items = 10, start = 0, cookbook_collection = {}) - cookbooks_url = "#{config[:supermarket_site]}/api/v1/search?q=#{query}&items=#{items}&start=#{start}" - cr = noauth_rest.get(cookbooks_url) - cr["items"].each do |cookbook| - cookbook_collection[cookbook["cookbook_name"]] = cookbook - end - new_start = start + cr["items"].length - if new_start < cr["total"] - search_cookbook(query, items, new_start, cookbook_collection) - else - cookbook_collection - end - end end end end diff --git a/lib/chef/knife/cookbook_site_share.rb b/lib/chef/knife/cookbook_site_share.rb index dcfd224295..6e14337ff5 100644 --- a/lib/chef/knife/cookbook_site_share.rb +++ b/lib/chef/knife/cookbook_site_share.rb @@ -18,154 +18,19 @@ # require "chef/knife" -require "chef/mixin/shell_out" +require "chef/knife/supermarket_share" class Chef class Knife - class CookbookSiteShare < Knife + class CookbookSiteShare < Knife::SupermarketShare - include Chef::Mixin::ShellOut - - deps do - require "chef/cookbook_loader" - require "chef/cookbook_uploader" - require "chef/cookbook_site_streaming_uploader" - require "mixlib/shellout" - end - - include Chef::Mixin::ShellOut + # Handle the subclassing (knife doesn't do this :() + dependency_loaders.concat(superclass.dependency_loaders) + options.merge!(superclass.options) banner "knife cookbook site share COOKBOOK [CATEGORY] (options)" category "cookbook site" - option :cookbook_path, - short: "-o PATH:PATH", - long: "--cookbook-path PATH:PATH", - description: "A colon-separated path to look for cookbooks in", - proc: lambda { |o| Chef::Config.cookbook_path = o.split(":") } - - option :dry_run, - long: "--dry-run", - short: "-n", - boolean: true, - default: false, - description: "Don't take action, only print what files will be uploaded to Supermarket." - - option :supermarket_site, - short: "-m SUPERMARKET_SITE", - long: "--supermarket-site SUPERMARKET_SITE", - description: "Supermarket Site", - default: "https://supermarket.chef.io", - proc: Proc.new { |supermarket| Chef::Config[:knife][:supermarket_site] = supermarket } - - def run - config[:cookbook_path] ||= Chef::Config[:cookbook_path] - - if @name_args.length < 1 - show_usage - ui.fatal("You must specify the cookbook name.") - exit(1) - elsif @name_args.length < 2 - cookbook_name = @name_args[0] - category = get_category(cookbook_name) - else - cookbook_name = @name_args[0] - category = @name_args[1] - end - - cl = Chef::CookbookLoader.new(config[:cookbook_path]) - if cl.cookbook_exists?(cookbook_name) - cookbook = cl[cookbook_name] - Chef::CookbookUploader.new(cookbook).validate_cookbooks - tmp_cookbook_dir = Chef::CookbookSiteStreamingUploader.create_build_dir(cookbook) - begin - Chef::Log.trace("Temp cookbook directory is #{tmp_cookbook_dir.inspect}") - ui.info("Making tarball #{cookbook_name}.tgz") - shell_out!("#{tar_cmd} -czf #{cookbook_name}.tgz #{cookbook_name}", cwd: tmp_cookbook_dir) - rescue => e - ui.error("Error making tarball #{cookbook_name}.tgz: #{e.message}. Increase log verbosity (-VV) for more information.") - Chef::Log.trace("\n#{e.backtrace.join("\n")}") - exit(1) - end - - if config[:dry_run] - ui.info("Not uploading #{cookbook_name}.tgz due to --dry-run flag.") - result = shell_out!("#{tar_cmd} -tzf #{cookbook_name}.tgz", cwd: tmp_cookbook_dir) - ui.info(result.stdout) - FileUtils.rm_rf tmp_cookbook_dir - return - end - - begin - do_upload("#{tmp_cookbook_dir}/#{cookbook_name}.tgz", category, Chef::Config[:node_name], Chef::Config[:client_key]) - ui.info("Upload complete") - Chef::Log.trace("Removing local staging directory at #{tmp_cookbook_dir}") - FileUtils.rm_rf tmp_cookbook_dir - rescue => e - ui.error("Error uploading cookbook #{cookbook_name} to Supermarket: #{e.message}. Increase log verbosity (-VV) for more information.") - Chef::Log.trace("\n#{e.backtrace.join("\n")}") - exit(1) - end - - else - ui.error("Could not find cookbook #{cookbook_name} in your cookbook path.") - exit(1) - end - end - - def get_category(cookbook_name) - data = noauth_rest.get("#{config[:supermarket_site]}/api/v1/cookbooks/#{@name_args[0]}") - data["category"] - rescue => e - return "Other" if e.kind_of?(Net::HTTPServerException) && e.response.code == "404" - ui.fatal("Unable to reach Supermarket: #{e.message}. Increase log verbosity (-VV) for more information.") - Chef::Log.trace("\n#{e.backtrace.join("\n")}") - exit(1) - end - - def do_upload(cookbook_filename, cookbook_category, user_id, user_secret_filename) - uri = "#{config[:supermarket_site]}/api/v1/cookbooks" - - category_string = Chef::JSONCompat.to_json({ "category" => cookbook_category }) - - http_resp = Chef::CookbookSiteStreamingUploader.post(uri, user_id, user_secret_filename, { - tarball: File.open(cookbook_filename), - cookbook: category_string, - }) - - res = Chef::JSONCompat.from_json(http_resp.body) - if http_resp.code.to_i != 201 - if res["error_messages"] - if res["error_messages"][0] =~ /Version already exists/ - ui.error "The same version of this cookbook already exists on Supermarket." - exit(1) - else - ui.error (res["error_messages"][0]).to_s - exit(1) - end - else - ui.error "Unknown error while sharing cookbook" - ui.error "Server response: #{http_resp.body}" - exit(1) - end - end - res - end - - def tar_cmd - if !@tar_cmd - @tar_cmd = "tar" - begin - # Unix and Mac only - prefer gnutar - if shell_out("which gnutar").exitstatus.equal?(0) - @tar_cmd = "gnutar" - end - rescue Errno::ENOENT - end - end - @tar_cmd - end end - end end diff --git a/lib/chef/knife/cookbook_site_show.rb b/lib/chef/knife/cookbook_site_show.rb index 1ae242d051..f365ed1fe0 100644 --- a/lib/chef/knife/cookbook_site_show.rb +++ b/lib/chef/knife/cookbook_site_show.rb @@ -17,51 +17,19 @@ # require "chef/knife" +require "chef/knife/supermarket_show" class Chef class Knife - class CookbookSiteShow < Knife + class CookbookSiteShow < Knife::SupermarketShow + + # Handle the subclassing (knife doesn't do this :() + dependency_loaders.concat(superclass.dependency_loaders) + options.merge!(superclass.options) banner "knife cookbook site show COOKBOOK [VERSION] (options)" category "cookbook site" - option :supermarket_site, - short: "-m SUPERMARKET_SITE", - long: "--supermarket-site SUPERMARKET_SITE", - description: "Supermarket Site", - default: "https://supermarket.chef.io", - proc: Proc.new { |supermarket| Chef::Config[:knife][:supermarket_site] = supermarket } - - def run - output(format_for_display(get_cookbook_data)) - end - - def supermarket_uri - "#{config[:supermarket_site]}/api/v1" - end - - def get_cookbook_data - case @name_args.length - when 1 - noauth_rest.get("#{supermarket_uri}/cookbooks/#{@name_args[0]}") - when 2 - noauth_rest.get("#{supermarket_uri}/cookbooks/#{@name_args[0]}/versions/#{name_args[1].tr('.', '_')}") - end - end - - def get_cookbook_list(items = 10, start = 0, cookbook_collection = {}) - cookbooks_url = "#{supermarket_uri}/cookbooks?items=#{items}&start=#{start}" - cr = noauth_rest.get(cookbooks_url) - cr["items"].each do |cookbook| - cookbook_collection[cookbook["cookbook_name"]] = cookbook - end - new_start = start + cr["items"].length - if new_start < cr["total"] - get_cookbook_list(items, new_start, cookbook_collection) - else - cookbook_collection - end - end end end end diff --git a/lib/chef/knife/cookbook_site_unshare.rb b/lib/chef/knife/cookbook_site_unshare.rb index 5f24781f38..8f24df8b0f 100644 --- a/lib/chef/knife/cookbook_site_unshare.rb +++ b/lib/chef/knife/cookbook_site_unshare.rb @@ -18,46 +18,19 @@ # require "chef/knife" +require "chef/knife/supermarket_unshare" class Chef class Knife - class CookbookSiteUnshare < Knife + class CookbookSiteUnshare < Knife::SupermarketUnshare - deps do - require "chef/json_compat" - end + # Handle the subclassing (knife doesn't do this :() + dependency_loaders.concat(superclass.dependency_loaders) + options.merge!(superclass.options) - banner "knife cookbook site unshare COOKBOOK" + banner "knife cookbook site unshare COOKBOOK (options)" category "cookbook site" - option :supermarket_site, - short: "-m SUPERMARKET_SITE", - long: "--supermarket-site SUPERMARKET_SITE", - description: "Supermarket Site", - default: "https://supermarket.chef.io", - proc: Proc.new { |supermarket| Chef::Config[:knife][:supermarket_site] = supermarket } - - def run - @cookbook_name = @name_args[0] - if @cookbook_name.nil? - show_usage - ui.fatal "You must provide the name of the cookbook to unshare" - exit 1 - end - - confirm "Do you really want to unshare all versions of the cookbook #{@cookbook_name}" - - begin - rest.delete "#{config[:supermarket_site]}/api/v1/cookbooks/#{@name_args[0]}" - rescue Net::HTTPServerException => e - raise e unless e.message =~ /Forbidden/ - ui.error "Forbidden: You must be the maintainer of #{@cookbook_name} to unshare it." - exit 1 - end - - ui.info "Unshared all versions of the cookbook #{@cookbook_name}" - end - end end end diff --git a/lib/chef/knife/supermarket_download.rb b/lib/chef/knife/supermarket_download.rb index 5657558591..a912f18d85 100644 --- a/lib/chef/knife/supermarket_download.rb +++ b/lib/chef/knife/supermarket_download.rb @@ -1,6 +1,6 @@ # # Author:: Christopher Webber () -# Copyright:: Copyright (c) 2014 Chef Software, Inc. +# Copyright:: Copyright (c) 2014-2018 Chef Software, Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,17 +17,106 @@ # require "chef/knife" -require "chef/knife/cookbook_site_download" class Chef class Knife - class SupermarketDownload < Knife::CookbookSiteDownload - # Handle the subclassing (knife doesn't do this :() - dependency_loaders.concat(superclass.dependency_loaders) - options.merge!(superclass.options) + class SupermarketDownload < Knife banner "knife supermarket download COOKBOOK [VERSION] (options)" category "supermarket" + + deps do + require "fileutils" + end + + option :file, + short: "-f FILE", + long: "--file FILE", + description: "The filename to write to" + + option :force, + long: "--force", + description: "Force download deprecated version" + + option :supermarket_site, + short: "-m SUPERMARKET_SITE", + long: "--supermarket-site SUPERMARKET_SITE", + description: "Supermarket Site", + default: "https://supermarket.chef.io", + proc: Proc.new { |supermarket| Chef::Config[:knife][:supermarket_site] = supermarket } + + def run + if current_cookbook_deprecated? + message = "DEPRECATION: This cookbook has been deprecated. " + replacement = replacement_cookbook + if !replacement.to_s.strip.empty? + message << "It has been replaced by #{replacement}." + else + message << "No replacement has been defined." + end + ui.warn message + + unless config[:force] + ui.warn "Use --force to force download deprecated cookbook." + return + end + end + + download_cookbook + end + + def version + @version = desired_cookbook_data["version"] + end + + private + + def cookbooks_api_url + "#{config[:supermarket_site]}/api/v1/cookbooks" + end + + def current_cookbook_data + @current_cookbook_data ||= begin + noauth_rest.get "#{cookbooks_api_url}/#{@name_args[0]}" + end + end + + def current_cookbook_deprecated? + current_cookbook_data["deprecated"] == true + end + + def desired_cookbook_data + @desired_cookbook_data ||= begin + uri = if @name_args.length == 1 + current_cookbook_data["latest_version"] + else + specific_cookbook_version_url + end + + noauth_rest.get uri + end + end + + def download_cookbook + ui.info "Downloading #{@name_args[0]} from Supermarket at version #{version} to #{download_location}" + tf = noauth_rest.streaming_request(desired_cookbook_data["file"]) + + ::FileUtils.cp tf.path, download_location + ui.info "Cookbook saved: #{download_location}" + end + + def download_location + config[:file] ||= File.join Dir.pwd, "#{@name_args[0]}-#{version}.tar.gz" + config[:file] + end + + def replacement_cookbook + File.basename(current_cookbook_data["replacement"] || "") + end + + def specific_cookbook_version_url + "#{cookbooks_api_url}/#{@name_args[0]}/versions/#{@name_args[1].tr('.', '_')}" + end end end end diff --git a/lib/chef/knife/supermarket_install.rb b/lib/chef/knife/supermarket_install.rb index 7642e68181..97a761e69b 100644 --- a/lib/chef/knife/supermarket_install.rb +++ b/lib/chef/knife/supermarket_install.rb @@ -1,6 +1,6 @@ # # Author:: Christopher Webber () -# Copyright:: Copyright (c) 2014 Chef Software, Inc. +# Copyright:: Copyright (c) 2014-2018 Chef Software, Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,17 +17,180 @@ # require "chef/knife" -require "chef/knife/cookbook_site_install" +require "chef/exceptions" +require "shellwords" +require "mixlib/archive" class Chef class Knife - class SupermarketInstall < Knife::CookbookSiteInstall - # Handle the subclassing (knife doesn't do this :() - dependency_loaders.concat(superclass.dependency_loaders) - options.merge!(superclass.options) + class SupermarketInstall < Knife + + deps do + require "chef/mixin/shell_out" + require "chef/knife/core/cookbook_scm_repo" + require "chef/cookbook/metadata" + end banner "knife supermarket install COOKBOOK [VERSION] (options)" category "supermarket" + + option :no_deps, + short: "-D", + long: "--skip-dependencies", + boolean: true, + default: false, + description: "Skips automatic dependency installation." + + option :cookbook_path, + short: "-o PATH:PATH", + long: "--cookbook-path PATH:PATH", + description: "A colon-separated path to look for cookbooks in", + proc: lambda { |o| o.split(":") } + + option :default_branch, + short: "-B BRANCH", + long: "--branch BRANCH", + description: "Default branch to work with", + default: "master" + + option :use_current_branch, + short: "-b", + long: "--use-current-branch", + description: "Use the current branch", + boolean: true, + default: false + + option :supermarket_site, + short: "-m SUPERMARKET_SITE", + long: "--supermarket-site SUPERMARKET_SITE", + description: "Supermarket Site", + default: "https://supermarket.chef.io", + proc: Proc.new { |supermarket| Chef::Config[:knife][:supermarket_site] = supermarket } + + attr_reader :cookbook_name + attr_reader :vendor_path + + def run + extend Chef::Mixin::ShellOut + + if config[:cookbook_path] + Chef::Config[:cookbook_path] = config[:cookbook_path] + else + config[:cookbook_path] = Chef::Config[:cookbook_path] + end + + @cookbook_name = parse_name_args! + # Check to ensure we have a valid source of cookbooks before continuing + # + @install_path = File.expand_path(Array(config[:cookbook_path]).first) + ui.info "Installing #{@cookbook_name} to #{@install_path}" + + @repo = CookbookSCMRepo.new(@install_path, ui, config) + # cookbook_path = File.join(vendor_path, name_args[0]) + upstream_file = File.join(@install_path, "#{@cookbook_name}.tar.gz") + + @repo.sanity_check + unless config[:use_current_branch] + @repo.reset_to_default_state + @repo.prepare_to_import(@cookbook_name) + end + + downloader = download_cookbook_to(upstream_file) + clear_existing_files(File.join(@install_path, @cookbook_name)) + extract_cookbook(upstream_file, downloader.version) + + # TODO: it'd be better to store these outside the cookbook repo and + # keep them around, e.g., in ~/Library/Caches on OS X. + ui.info("Removing downloaded tarball") + File.unlink(upstream_file) + + if @repo.finalize_updates_to(@cookbook_name, downloader.version) + unless config[:use_current_branch] + @repo.reset_to_default_state + end + @repo.merge_updates_from(@cookbook_name, downloader.version) + else + unless config[:use_current_branch] + @repo.reset_to_default_state + end + end + + unless config[:no_deps] + preferred_metadata.dependencies.each_key do |cookbook| + # Doesn't do versions.. yet + nv = self.class.new + nv.config = config + nv.name_args = [ cookbook ] + nv.run + end + end + end + + def parse_name_args! + if name_args.empty? + ui.error("Please specify a cookbook to download and install.") + exit 1 + elsif name_args.size >= 2 + unless name_args.last.match(/^(\d+)(\.\d+){1,2}$/) && name_args.size == 2 + ui.error("Installing multiple cookbooks at once is not supported.") + exit 1 + end + end + name_args.first + end + + def download_cookbook_to(download_path) + downloader = Chef::Knife::CookbookSiteDownload.new + downloader.config[:file] = download_path + downloader.config[:supermarket_site] = config[:supermarket_site] + downloader.name_args = name_args + downloader.run + downloader + end + + def extract_cookbook(upstream_file, version) + ui.info("Uncompressing #{@cookbook_name} version #{version}.") + Mixlib::Archive.new(convert_path(upstream_file)).extract(@install_path, perms: false) + end + + def clear_existing_files(cookbook_path) + ui.info("Removing pre-existing version.") + FileUtils.rmtree(cookbook_path) if File.directory?(cookbook_path) + end + + def convert_path(upstream_file) + # converts a Windows path (C:\foo) to a mingw path (/c/foo) + if ENV["MSYSTEM"] == "MINGW32" + upstream_file.sub(/^([[:alpha:]]):/, '/\1') + else + Shellwords.escape upstream_file + end + end + + # Get the preferred metadata path on disk. Chef prefers the metadata.rb + # over the metadata.json. + # + # @raise if there is no metadata in the cookbook + # + # @return [Chef::Cookbook::Metadata] + def preferred_metadata + md = Chef::Cookbook::Metadata.new + + rb = File.join(@install_path, @cookbook_name, "metadata.rb") + if File.exist?(rb) + md.from_file(rb) + return md + end + + json = File.join(@install_path, @cookbook_name, "metadata.json") + if File.exist?(json) + json = IO.read(json) + md.from_json(json) + return md + end + + raise Chef::Exceptions::MetadataNotFound.new(@install_path, @cookbook_name) + end end end end diff --git a/lib/chef/knife/supermarket_list.rb b/lib/chef/knife/supermarket_list.rb index f2bc98bd0e..a7274714c6 100644 --- a/lib/chef/knife/supermarket_list.rb +++ b/lib/chef/knife/supermarket_list.rb @@ -1,6 +1,6 @@ # # Author:: Christopher Webber () -# Copyright:: Copyright (c) 2014 Chef Software, Inc. +# Copyright:: Copyright (c) 2014-2018 Chef Software, Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,17 +17,49 @@ # require "chef/knife" -require "chef/knife/cookbook_site_list" class Chef class Knife - class SupermarketList < Knife::CookbookSiteList - # Handle the subclassing (knife doesn't do this :() - dependency_loaders.concat(superclass.dependency_loaders) - options.merge!(superclass.options) + class SupermarketList < Knife banner "knife supermarket list (options)" category "supermarket" + + option :with_uri, + short: "-w", + long: "--with-uri", + description: "Show corresponding URIs" + + option :supermarket_site, + short: "-m SUPERMARKET_SITE", + long: "--supermarket-site SUPERMARKET_SITE", + description: "Supermarket Site", + default: "https://supermarket.chef.io", + proc: Proc.new { |supermarket| Chef::Config[:knife][:supermarket_site] = supermarket } + + def run + if config[:with_uri] + cookbooks = Hash.new + get_cookbook_list.each { |k, v| cookbooks[k] = v["cookbook"] } + ui.output(format_for_display(cookbooks)) + else + ui.msg(ui.list(get_cookbook_list.keys.sort, :columns_down)) + end + end + + def get_cookbook_list(items = 10, start = 0, cookbook_collection = {}) + cookbooks_url = "#{config[:supermarket_site]}/api/v1/cookbooks?items=#{items}&start=#{start}" + cr = noauth_rest.get(cookbooks_url) + cr["items"].each do |cookbook| + cookbook_collection[cookbook["cookbook_name"]] = cookbook + end + new_start = start + cr["items"].length + if new_start < cr["total"] + get_cookbook_list(items, new_start, cookbook_collection) + else + cookbook_collection + end + end end end end diff --git a/lib/chef/knife/supermarket_search.rb b/lib/chef/knife/supermarket_search.rb index 3206b0cb80..6613b1244b 100644 --- a/lib/chef/knife/supermarket_search.rb +++ b/lib/chef/knife/supermarket_search.rb @@ -1,6 +1,6 @@ # # Author:: Christopher Webber () -# Copyright:: Copyright (c) 2014 Chef Software, Inc. +# Copyright:: Copyright (c) 2014-2018 Chef Software, Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,17 +17,37 @@ # require "chef/knife" -require "chef/knife/cookbook_site_search" class Chef class Knife - class SupermarketSearch < Knife::CookbookSiteSearch - # Handle the subclassing (knife doesn't do this :() - dependency_loaders.concat(superclass.dependency_loaders) - options.merge!(superclass.options) - + class SupermarketSearch < Knife banner "knife supermarket search QUERY (options)" category "supermarket" + + option :supermarket_site, + short: "-m SUPERMARKET_SITE", + long: "--supermarket-site SUPERMARKET_SITE", + description: "Supermarket Site", + default: "https://supermarket.chef.io", + proc: Proc.new { |supermarket| Chef::Config[:knife][:supermarket_site] = supermarket } + + def run + output(search_cookbook(name_args[0])) + end + + def search_cookbook(query, items = 10, start = 0, cookbook_collection = {}) + cookbooks_url = "#{config[:supermarket_site]}/api/v1/search?q=#{query}&items=#{items}&start=#{start}" + cr = noauth_rest.get(cookbooks_url) + cr["items"].each do |cookbook| + cookbook_collection[cookbook["cookbook_name"]] = cookbook + end + new_start = start + cr["items"].length + if new_start < cr["total"] + search_cookbook(query, items, new_start, cookbook_collection) + else + cookbook_collection + end + end end end end diff --git a/lib/chef/knife/supermarket_share.rb b/lib/chef/knife/supermarket_share.rb index 3109b9e794..27d2293679 100644 --- a/lib/chef/knife/supermarket_share.rb +++ b/lib/chef/knife/supermarket_share.rb @@ -1,6 +1,6 @@ # # Author:: Christopher Webber () -# Copyright:: Copyright (c) 2014 Chef Software, Inc. +# Copyright:: Copyright (c) 2014-2018 Chef Software, Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,17 +17,153 @@ # require "chef/knife" -require "chef/knife/cookbook_site_share" +require "chef/mixin/shell_out" class Chef class Knife - class SupermarketShare < Knife::CookbookSiteShare - # Handle the subclassing (knife doesn't do this :() - dependency_loaders.concat(superclass.dependency_loaders) - options.merge!(superclass.options) + class SupermarketShare < Knife + + include Chef::Mixin::ShellOut + + deps do + require "chef/cookbook_loader" + require "chef/cookbook_uploader" + require "chef/cookbook_site_streaming_uploader" + require "mixlib/shellout" + end + + include Chef::Mixin::ShellOut banner "knife supermarket share COOKBOOK [CATEGORY] (options)" category "supermarket" + + option :cookbook_path, + short: "-o PATH:PATH", + long: "--cookbook-path PATH:PATH", + description: "A colon-separated path to look for cookbooks in", + proc: lambda { |o| Chef::Config.cookbook_path = o.split(":") } + + option :dry_run, + long: "--dry-run", + short: "-n", + boolean: true, + default: false, + description: "Don't take action, only print what files will be uploaded to Supermarket." + + option :supermarket_site, + short: "-m SUPERMARKET_SITE", + long: "--supermarket-site SUPERMARKET_SITE", + description: "Supermarket Site", + default: "https://supermarket.chef.io", + proc: Proc.new { |supermarket| Chef::Config[:knife][:supermarket_site] = supermarket } + + def run + config[:cookbook_path] ||= Chef::Config[:cookbook_path] + + if @name_args.length < 1 + show_usage + ui.fatal("You must specify the cookbook name.") + exit(1) + elsif @name_args.length < 2 + cookbook_name = @name_args[0] + category = get_category(cookbook_name) + else + cookbook_name = @name_args[0] + category = @name_args[1] + end + + cl = Chef::CookbookLoader.new(config[:cookbook_path]) + if cl.cookbook_exists?(cookbook_name) + cookbook = cl[cookbook_name] + Chef::CookbookUploader.new(cookbook).validate_cookbooks + tmp_cookbook_dir = Chef::CookbookSiteStreamingUploader.create_build_dir(cookbook) + begin + Chef::Log.trace("Temp cookbook directory is #{tmp_cookbook_dir.inspect}") + ui.info("Making tarball #{cookbook_name}.tgz") + shell_out!("#{tar_cmd} -czf #{cookbook_name}.tgz #{cookbook_name}", cwd: tmp_cookbook_dir) + rescue => e + ui.error("Error making tarball #{cookbook_name}.tgz: #{e.message}. Increase log verbosity (-VV) for more information.") + Chef::Log.trace("\n#{e.backtrace.join("\n")}") + exit(1) + end + + if config[:dry_run] + ui.info("Not uploading #{cookbook_name}.tgz due to --dry-run flag.") + result = shell_out!("#{tar_cmd} -tzf #{cookbook_name}.tgz", cwd: tmp_cookbook_dir) + ui.info(result.stdout) + FileUtils.rm_rf tmp_cookbook_dir + return + end + + begin + do_upload("#{tmp_cookbook_dir}/#{cookbook_name}.tgz", category, Chef::Config[:node_name], Chef::Config[:client_key]) + ui.info("Upload complete") + Chef::Log.trace("Removing local staging directory at #{tmp_cookbook_dir}") + FileUtils.rm_rf tmp_cookbook_dir + rescue => e + ui.error("Error uploading cookbook #{cookbook_name} to Supermarket: #{e.message}. Increase log verbosity (-VV) for more information.") + Chef::Log.trace("\n#{e.backtrace.join("\n")}") + exit(1) + end + + else + ui.error("Could not find cookbook #{cookbook_name} in your cookbook path.") + exit(1) + end + end + + def get_category(cookbook_name) + data = noauth_rest.get("#{config[:supermarket_site]}/api/v1/cookbooks/#{@name_args[0]}") + data["category"] + rescue => e + return "Other" if e.kind_of?(Net::HTTPServerException) && e.response.code == "404" + ui.fatal("Unable to reach Supermarket: #{e.message}. Increase log verbosity (-VV) for more information.") + Chef::Log.trace("\n#{e.backtrace.join("\n")}") + exit(1) + end + + def do_upload(cookbook_filename, cookbook_category, user_id, user_secret_filename) + uri = "#{config[:supermarket_site]}/api/v1/cookbooks" + + category_string = Chef::JSONCompat.to_json({ "category" => cookbook_category }) + + http_resp = Chef::CookbookSiteStreamingUploader.post(uri, user_id, user_secret_filename, { + tarball: File.open(cookbook_filename), + cookbook: category_string, + }) + + res = Chef::JSONCompat.from_json(http_resp.body) + if http_resp.code.to_i != 201 + if res["error_messages"] + if res["error_messages"][0] =~ /Version already exists/ + ui.error "The same version of this cookbook already exists on Supermarket." + exit(1) + else + ui.error (res["error_messages"][0]).to_s + exit(1) + end + else + ui.error "Unknown error while sharing cookbook" + ui.error "Server response: #{http_resp.body}" + exit(1) + end + end + res + end + + def tar_cmd + if !@tar_cmd + @tar_cmd = "tar" + begin + # Unix and Mac only - prefer gnutar + if shell_out("which gnutar").exitstatus.equal?(0) + @tar_cmd = "gnutar" + end + rescue Errno::ENOENT + end + end + @tar_cmd + end end end end diff --git a/lib/chef/knife/supermarket_show.rb b/lib/chef/knife/supermarket_show.rb index 2ad122143f..f32639dfc3 100644 --- a/lib/chef/knife/supermarket_show.rb +++ b/lib/chef/knife/supermarket_show.rb @@ -1,6 +1,6 @@ # # Author:: Christopher Webber () -# Copyright:: Copyright (c) 2014 Chef Software, Inc. +# Copyright:: Copyright (c) 2014-2018 Chef Software, Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,17 +17,51 @@ # require "chef/knife" -require "chef/knife/cookbook_site_show" class Chef class Knife - class SupermarketShow < Knife::CookbookSiteShow - # Handle the subclassing (knife doesn't do this :() - dependency_loaders.concat(superclass.dependency_loaders) - options.merge!(superclass.options) + class SupermarketShow < Knife banner "knife supermarket show COOKBOOK [VERSION] (options)" category "supermarket" + + option :supermarket_site, + short: "-m SUPERMARKET_SITE", + long: "--supermarket-site SUPERMARKET_SITE", + description: "Supermarket Site", + default: "https://supermarket.chef.io", + proc: Proc.new { |supermarket| Chef::Config[:knife][:supermarket_site] = supermarket } + + def run + output(format_for_display(get_cookbook_data)) + end + + def supermarket_uri + "#{config[:supermarket_site]}/api/v1" + end + + def get_cookbook_data + case @name_args.length + when 1 + noauth_rest.get("#{supermarket_uri}/cookbooks/#{@name_args[0]}") + when 2 + noauth_rest.get("#{supermarket_uri}/cookbooks/#{@name_args[0]}/versions/#{name_args[1].tr('.', '_')}") + end + end + + def get_cookbook_list(items = 10, start = 0, cookbook_collection = {}) + cookbooks_url = "#{supermarket_uri}/cookbooks?items=#{items}&start=#{start}" + cr = noauth_rest.get(cookbooks_url) + cr["items"].each do |cookbook| + cookbook_collection[cookbook["cookbook_name"]] = cookbook + end + new_start = start + cr["items"].length + if new_start < cr["total"] + get_cookbook_list(items, new_start, cookbook_collection) + else + cookbook_collection + end + end end end end diff --git a/lib/chef/knife/supermarket_unshare.rb b/lib/chef/knife/supermarket_unshare.rb index fd48e172ce..eb17fc6bd4 100644 --- a/lib/chef/knife/supermarket_unshare.rb +++ b/lib/chef/knife/supermarket_unshare.rb @@ -1,6 +1,6 @@ # # Author:: Christopher Webber () -# Copyright:: Copyright (c) 2014 Chef Software, Inc. +# Copyright:: Copyright (c) 2014-2018 Chef Software, Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,17 +17,45 @@ # require "chef/knife" -require "chef/knife/cookbook_site_unshare" class Chef class Knife - class SupermarketUnshare < Knife::CookbookSiteUnshare - # Handle the subclassing (knife doesn't do this :() - dependency_loaders.concat(superclass.dependency_loaders) - options.merge!(superclass.options) + class SupermarketUnshare < Knife - banner "knife supermarket unshare COOKBOOK (options)" + deps do + require "chef/json_compat" + end + + banner "knife supermarket unshare COOKBOOK" category "supermarket" + + option :supermarket_site, + short: "-m SUPERMARKET_SITE", + long: "--supermarket-site SUPERMARKET_SITE", + description: "Supermarket Site", + default: "https://supermarket.chef.io", + proc: Proc.new { |supermarket| Chef::Config[:knife][:supermarket_site] = supermarket } + + def run + @cookbook_name = @name_args[0] + if @cookbook_name.nil? + show_usage + ui.fatal "You must provide the name of the cookbook to unshare" + exit 1 + end + + confirm "Do you really want to unshare all versions of the cookbook #{@cookbook_name}" + + begin + rest.delete "#{config[:supermarket_site]}/api/v1/cookbooks/#{@name_args[0]}" + rescue Net::HTTPServerException => e + raise e unless e.message =~ /Forbidden/ + ui.error "Forbidden: You must be the maintainer of #{@cookbook_name} to unshare it." + exit 1 + end + + ui.info "Unshared all versions of the cookbook #{@cookbook_name}" + end end end end diff --git a/spec/unit/knife/cookbook_site_download_spec.rb b/spec/unit/knife/cookbook_site_download_spec.rb deleted file mode 100644 index 9bf10859c0..0000000000 --- a/spec/unit/knife/cookbook_site_download_spec.rb +++ /dev/null @@ -1,150 +0,0 @@ -# -# Author:: Thomas Bishop () -# Copyright:: Copyright 2012-2016, Thomas Bishop -# License:: Apache License, Version 2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -require File.expand_path(File.dirname(__FILE__) + "/../../spec_helper") - -describe Chef::Knife::CookbookSiteDownload do - - describe "run" do - before do - @knife = Chef::Knife::CookbookSiteDownload.new - @knife.name_args = ["apache2"] - @noauth_rest = double("no auth rest") - @stderr = StringIO.new - @cookbook_api_url = "https://supermarket.chef.io/api/v1/cookbooks" - @version = "1.0.2" - @version_us = @version.tr ".", "_" - @current_data = { "deprecated" => false, - "latest_version" => "#{@cookbook_api_url}/apache2/versions/#{@version_us}", - "replacement" => "other_apache2" } - - allow(@knife.ui).to receive(:stderr).and_return(@stderr) - allow(@knife).to receive(:noauth_rest).and_return(@noauth_rest) - expect(@noauth_rest).to receive(:get) - .with("#{@cookbook_api_url}/apache2") - .and_return(@current_data) - @knife.configure_chef - end - - context "when the cookbook is deprecated and not forced" do - before do - @current_data["deprecated"] = true - end - - it "should warn with info about the replacement" do - expect(@knife.ui).to receive(:warn) - .with(/.+deprecated.+replaced by other_apache2.+/i) - expect(@knife.ui).to receive(:warn) - .with(/use --force.+download.+/i) - @knife.run - end - end - - context "when" do - before do - @cookbook_data = { "version" => @version, - "file" => "http://example.com/apache2_#{@version_us}.tgz" } - @temp_file = double( path: "/tmp/apache2_#{@version_us}.tgz" ) - @file = File.join(Dir.pwd, "apache2-#{@version}.tar.gz") - end - - context "downloading the latest version" do - before do - expect(@noauth_rest).to receive(:get) - .with(@current_data["latest_version"]) - .and_return(@cookbook_data) - expect(@noauth_rest).to receive(:streaming_request) - .with(@cookbook_data["file"]) - .and_return(@temp_file) - end - - context "and it is deprecated and with --force" do - before do - @current_data["deprecated"] = true - @knife.config[:force] = true - end - - it "should download the latest version" do - expect(@knife.ui).to receive(:warn) - .with(/.+deprecated.+replaced by other_apache2.+/i) - expect(FileUtils).to receive(:cp).with(@temp_file.path, @file) - @knife.run - expect(@stderr.string).to match /downloading apache2.+version.+#{Regexp.escape(@version)}/i - expect(@stderr.string).to match /cookbook save.+#{Regexp.escape(@file)}/i - end - - end - - it "should download the latest version" do - expect(FileUtils).to receive(:cp).with(@temp_file.path, @file) - @knife.run - expect(@stderr.string).to match /downloading apache2.+version.+#{Regexp.escape(@version)}/i - expect(@stderr.string).to match /cookbook save.+#{Regexp.escape(@file)}/i - end - - context "with -f or --file" do - before do - @file = "/opt/chef/cookbooks/apache2.tar.gz" - @knife.config[:file] = @file - expect(FileUtils).to receive(:cp).with(@temp_file.path, @file) - end - - it "should download the cookbook to the desired file" do - @knife.run - expect(@stderr.string).to match /downloading apache2.+version.+#{Regexp.escape(@version)}/i - expect(@stderr.string).to match /cookbook save.+#{Regexp.escape(@file)}/i - end - end - - it "should provide an accessor to the version" do - allow(FileUtils).to receive(:cp).and_return(true) - expect(@knife.version).to eq(@version) - @knife.run - end - end - - context "downloading a cookbook of a specific version" do - before do - @version = "1.0.1" - @version_us = @version.tr ".", "_" - @cookbook_data = { "version" => @version, - "file" => "http://example.com/apache2_#{@version_us}.tgz" } - @temp_file = double(path: "/tmp/apache2_#{@version_us}.tgz") - @file = File.join(Dir.pwd, "apache2-#{@version}.tar.gz") - @knife.name_args << @version - end - - it "should download the desired version" do - expect(@noauth_rest).to receive(:get) - .with("#{@cookbook_api_url}/apache2/versions/#{@version_us}") - .and_return(@cookbook_data) - expect(@noauth_rest).to receive(:streaming_request) - .with(@cookbook_data["file"]) - .and_return(@temp_file) - expect(FileUtils).to receive(:cp).with(@temp_file.path, @file) - @knife.run - expect(@stderr.string).to match /downloading apache2.+version.+#{Regexp.escape(@version)}/i - expect(@stderr.string).to match /cookbook save.+#{Regexp.escape(@file)}/i - end - end - - end - - end - -end diff --git a/spec/unit/knife/cookbook_site_install_spec.rb b/spec/unit/knife/cookbook_site_install_spec.rb deleted file mode 100644 index 4e9c7b6265..0000000000 --- a/spec/unit/knife/cookbook_site_install_spec.rb +++ /dev/null @@ -1,200 +0,0 @@ -# -# Author:: Steven Danna () -# Copyright:: Copyright 2011-2016, Chef Software, Inc. -# License:: Apache License, Version 2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -require File.expand_path(File.join(File.dirname(__FILE__), "..", "..", "spec_helper")) - -describe Chef::Knife::CookbookSiteInstall do - let(:knife) { Chef::Knife::CookbookSiteInstall.new } - let(:stdout) { StringIO.new } - let(:stderr) { StringIO.new } - let(:downloader) { Hash.new } - let(:archive) { double(Mixlib::Archive, extract: true) } - let(:repo) do - double(sanity_check: true, reset_to_default_state: true, - prepare_to_import: true, finalize_updates_to: true, - merge_updates_from: true) end - let(:install_path) do - if Chef::Platform.windows? - "C:/tmp/chef" - else - "/var/tmp/chef" - end - end - - before(:each) do - require "chef/knife/core/cookbook_scm_repo" - - allow(knife.ui).to receive(:stdout).and_return(stdout) - knife.config = {} - knife.config[:cookbook_path] = [ install_path ] - - allow(knife).to receive(:stderr).and_return(stderr) - allow(knife).to receive(:stdout).and_return(stdout) - - # Assume all external commands would have succeed. :( - allow(File).to receive(:unlink) - allow(File).to receive(:rmtree) - allow(knife).to receive(:shell_out!).and_return(true) - allow(Mixlib::Archive).to receive(:new).and_return(archive) - - # CookbookSiteDownload Stup - allow(knife).to receive(:download_cookbook_to).and_return(downloader) - allow(downloader).to receive(:version) do - if knife.name_args.size == 2 - knife.name_args[1] - else - "0.3.0" - end - end - - # Stubs for CookbookSCMRepo - allow(Chef::Knife::CookbookSCMRepo).to receive(:new).and_return(repo) - end - - describe "run" do - it "raises an error if a cookbook name is not provided" do - knife.name_args = [] - expect(knife.ui).to receive(:error).with("Please specify a cookbook to download and install.") - expect { knife.run }.to raise_error(SystemExit) - end - - it "raises an error if more than two arguments are given" do - knife.name_args = %w{foo bar baz} - expect(knife.ui).to receive(:error).with("Installing multiple cookbooks at once is not supported.") - expect { knife.run }.to raise_error(SystemExit) - end - - it "raises an error if the second argument is not a version" do - knife.name_args = ["getting-started", "1pass"] - expect(knife.ui).to receive(:error).with("Installing multiple cookbooks at once is not supported.") - expect { knife.run }.to raise_error(SystemExit) - end - - it "raises an error if the second argument is a four-digit version" do - knife.name_args = ["getting-started", "0.0.0.1"] - expect(knife.ui).to receive(:error).with("Installing multiple cookbooks at once is not supported.") - expect { knife.run }.to raise_error(SystemExit) - end - - it "raises an error if the second argument is a one-digit version" do - knife.name_args = ["getting-started", "1"] - expect(knife.ui).to receive(:error).with("Installing multiple cookbooks at once is not supported.") - expect { knife.run }.to raise_error(SystemExit) - end - - it "installs the specified version if second argument is a three-digit version" do - knife.name_args = ["getting-started", "0.1.0"] - knife.config[:no_deps] = true - upstream_file = File.join(install_path, "getting-started.tar.gz") - expect(knife).to receive(:download_cookbook_to).with(upstream_file) - expect(knife).to receive(:extract_cookbook).with(upstream_file, "0.1.0") - expect(knife).to receive(:clear_existing_files).with(File.join(install_path, "getting-started")) - expect(repo).to receive(:merge_updates_from).with("getting-started", "0.1.0") - knife.run - end - - it "installs the specified version if second argument is a two-digit version" do - knife.name_args = ["getting-started", "0.1"] - knife.config[:no_deps] = true - upstream_file = File.join(install_path, "getting-started.tar.gz") - expect(knife).to receive(:download_cookbook_to).with(upstream_file) - expect(knife).to receive(:extract_cookbook).with(upstream_file, "0.1") - expect(knife).to receive(:clear_existing_files).with(File.join(install_path, "getting-started")) - expect(repo).to receive(:merge_updates_from).with("getting-started", "0.1") - knife.run - end - - it "installs the latest version if only a cookbook name is given" do - knife.name_args = ["getting-started"] - knife.config[:no_deps] = true - upstream_file = File.join(install_path, "getting-started.tar.gz") - expect(knife).to receive(:download_cookbook_to).with(upstream_file) - expect(knife).to receive(:extract_cookbook).with(upstream_file, "0.3.0") - expect(knife).to receive(:clear_existing_files).with(File.join(install_path, "getting-started")) - expect(repo).to receive(:merge_updates_from).with("getting-started", "0.3.0") - knife.run - end - - it "does not create/reset git branches if use_current_branch is set" do - knife.name_args = ["getting-started"] - knife.config[:use_current_branch] = true - knife.config[:no_deps] = true - upstream_file = File.join(install_path, "getting-started.tar.gz") - expect(repo).not_to receive(:prepare_to_import) - expect(repo).not_to receive(:reset_to_default_state) - knife.run - end - - it "does not raise an error if cookbook_path is a string" do - knife.config[:cookbook_path] = install_path - knife.config[:no_deps] = true - knife.name_args = ["getting-started"] - upstream_file = File.join(install_path, "getting-started.tar.gz") - expect(knife).to receive(:download_cookbook_to).with(upstream_file) - expect(knife).to receive(:extract_cookbook).with(upstream_file, "0.3.0") - expect(knife).to receive(:clear_existing_files).with(File.join(install_path, "getting-started")) - expect(repo).to receive(:merge_updates_from).with("getting-started", "0.3.0") - expect { knife.run }.not_to raise_error - end - end # end of run - - let(:metadata) { Chef::Cookbook::Metadata.new } - let(:rb_metadata_path) { File.join(install_path, "post-punk-kitchen", "metadata.rb") } - let(:json_metadata_path) { File.join(install_path, "post-punk-kitchen", "metadata.json") } - - describe "preferred_metadata" do - before do - allow(Chef::Cookbook::Metadata).to receive(:new).and_return(metadata) - allow(File).to receive(:exist?).and_return(false) - knife.instance_variable_set(:@cookbook_name, "post-punk-kitchen") - knife.instance_variable_set(:@install_path, install_path) - end - - it "returns a populated Metadata object if metadata.rb exists" do - allow(File).to receive(:exist?).with(rb_metadata_path).and_return(true) - expect(metadata).to receive(:from_file).with(rb_metadata_path) - knife.preferred_metadata - end - - it "returns a populated Metadata object if metadata.json exists" do - allow(File).to receive(:exist?).with(json_metadata_path).and_return(true) - # expect(IO).to receive(:read).with(json_metadata_path) - allow(IO).to receive(:read) - expect(metadata).to receive(:from_json) - knife.preferred_metadata - end - - it "prefers metadata.rb over metadata.json" do - allow(File).to receive(:exist?).with(rb_metadata_path).and_return(true) - allow(File).to receive(:exist?).with(json_metadata_path).and_return(true) - allow(IO).to receive(:read) - expect(metadata).to receive(:from_file).with(rb_metadata_path) - expect(metadata).not_to receive(:from_json) - knife.preferred_metadata - end - - it "rasies an error if it finds no metadata file" do - expect { knife.preferred_metadata }.to raise_error { |error| - expect(error).to be_a(Chef::Exceptions::MetadataNotFound) - expect(error.cookbook_name).to eq("post-punk-kitchen") - expect(error.install_path).to eq(install_path) - } - end - - end -end diff --git a/spec/unit/knife/cookbook_site_share_spec.rb b/spec/unit/knife/cookbook_site_share_spec.rb deleted file mode 100644 index 773c1a78c3..0000000000 --- a/spec/unit/knife/cookbook_site_share_spec.rb +++ /dev/null @@ -1,209 +0,0 @@ -# -# Author:: Stephen Delano () -# Copyright:: Copyright 2010-2016, Chef Software Inc. -# License:: Apache License, Version 2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -require "spec_helper" - -require "chef/cookbook_uploader" -require "chef/cookbook_site_streaming_uploader" - -describe Chef::Knife::CookbookSiteShare do - - before(:each) do - @knife = Chef::Knife::CookbookSiteShare.new - # Merge default settings in. - @knife.merge_configs - @knife.name_args = %w{cookbook_name AwesomeSausage} - - @cookbook = Chef::CookbookVersion.new("cookbook_name") - - @cookbook_loader = double("Chef::CookbookLoader") - allow(@cookbook_loader).to receive(:cookbook_exists?).and_return(true) - allow(@cookbook_loader).to receive(:[]).and_return(@cookbook) - allow(Chef::CookbookLoader).to receive(:new).and_return(@cookbook_loader) - - @noauth_rest = double(Chef::ServerAPI) - allow(@knife).to receive(:noauth_rest).and_return(@noauth_rest) - - @cookbook_uploader = Chef::CookbookUploader.new("herpderp", rest: "norest") - allow(Chef::CookbookUploader).to receive(:new).and_return(@cookbook_uploader) - allow(@cookbook_uploader).to receive(:validate_cookbooks).and_return(true) - allow(Chef::CookbookSiteStreamingUploader).to receive(:create_build_dir).and_return(Dir.mktmpdir) - - allow(@knife).to receive(:shell_out!).and_return(true) - @stdout = StringIO.new - allow(@knife.ui).to receive(:stdout).and_return(@stdout) - end - - describe "run" do - - before(:each) do - allow(@knife).to receive(:do_upload).and_return(true) - @category_response = { - "name" => "cookbook_name", - "category" => "Testing Category", - } - @bad_category_response = { - "error_code" => "NOT_FOUND", - "error_messages" => [ - "Resource does not exist.", - ], - } - end - - it "should set true to config[:dry_run] as default" do - expect(@knife.config[:dry_run]).to be_falsey - end - - it "should should print usage and exit when given no arguments" do - @knife.name_args = [] - expect(@knife).to receive(:show_usage) - expect(@knife.ui).to receive(:fatal) - expect { @knife.run }.to raise_error(SystemExit) - end - - it "should not fail when given only 1 argument and can determine category" do - @knife.name_args = ["cookbook_name"] - expect(@noauth_rest).to receive(:get).with("https://supermarket.chef.io/api/v1/cookbooks/cookbook_name").and_return(@category_response) - expect(@knife).to receive(:do_upload) - @knife.run - end - - it "should use a default category when given only 1 argument and cannot determine category" do - @knife.name_args = ["cookbook_name"] - expect(@noauth_rest).to receive(:get).with("https://supermarket.chef.io/api/v1/cookbooks/cookbook_name") { raise Net::HTTPServerException.new("404 Not Found", OpenStruct.new(code: "404")) } - expect(@knife).to receive(:do_upload) - expect { @knife.run }.to_not raise_error - end - - it "should print error and exit when given only 1 argument and Chef::ServerAPI throws an exception" do - @knife.name_args = ["cookbook_name"] - expect(@noauth_rest).to receive(:get).with("https://supermarket.chef.io/api/v1/cookbooks/cookbook_name") { raise Errno::ECONNREFUSED, "Connection refused" } - expect(@knife.ui).to receive(:fatal) - expect { @knife.run }.to raise_error(SystemExit) - end - - it "should check if the cookbook exists" do - expect(@cookbook_loader).to receive(:cookbook_exists?) - @knife.run - end - - it "should exit and log to error if the cookbook doesn't exist" do - allow(@cookbook_loader).to receive(:cookbook_exists?).and_return(false) - expect(@knife.ui).to receive(:error) - expect { @knife.run }.to raise_error(SystemExit) - end - - if File.exists?("/usr/bin/gnutar") || File.exists?("/bin/gnutar") - it "should use gnutar to make a tarball of the cookbook" do - expect(@knife).to receive(:shell_out!) do |args| - expect(args.to_s).to match(/gnutar -czf/) - end - @knife.run - end - else - it "should make a tarball of the cookbook" do - expect(@knife).to receive(:shell_out!) do |args| - expect(args.to_s).to match(/tar -czf/) - end - @knife.run - end - end - - it "should exit and log to error when the tarball creation fails" do - allow(@knife).to receive(:shell_out!).and_raise(Chef::Exceptions::Exec) - expect(@knife.ui).to receive(:error) - expect { @knife.run }.to raise_error(SystemExit) - end - - it "should upload the cookbook and clean up the tarball" do - expect(@knife).to receive(:do_upload) - expect(FileUtils).to receive(:rm_rf) - @knife.run - end - - context "when the --dry-run flag is specified" do - before do - allow(Chef::CookbookSiteStreamingUploader).to receive(:create_build_dir).and_return("/var/tmp/dummy") - @knife.config = { dry_run: true } - allow(@knife).to receive_message_chain(:shell_out!, :stdout).and_return("file") - end - - it "should list files in the tarball" do - allow(@knife).to receive(:tar_cmd).and_return("footar") - expect(@knife).to receive(:shell_out!).with("footar -czf #{@cookbook.name}.tgz #{@cookbook.name}", { cwd: "/var/tmp/dummy" }) - expect(@knife).to receive(:shell_out!).with("footar -tzf #{@cookbook.name}.tgz", { cwd: "/var/tmp/dummy" }) - @knife.run - end - - it "does not upload the cookbook" do - allow(@knife).to receive(:shell_out!).and_return(true) - expect(@knife).not_to receive(:do_upload) - @knife.run - end - end - end - - describe "do_upload" do - - before(:each) do - @upload_response = double("Net::HTTPResponse") - allow(Chef::CookbookSiteStreamingUploader).to receive(:post).and_return(@upload_response) - - @stdout = StringIO.new - @stderr = StringIO.new - allow(@knife.ui).to receive(:stdout).and_return(@stdout) - allow(@knife.ui).to receive(:stderr).and_return(@stderr) - allow(File).to receive(:open).and_return(true) - end - - it 'should post the cookbook to "https://supermarket.chef.io"' do - response_text = Chef::JSONCompat.to_json({ uri: "https://supermarket.chef.io/cookbooks/cookbook_name" }) - allow(@upload_response).to receive(:body).and_return(response_text) - allow(@upload_response).to receive(:code).and_return(201) - expect(Chef::CookbookSiteStreamingUploader).to receive(:post).with(/supermarket\.chef\.io/, anything(), anything(), anything()) - @knife.run - end - - it "should alert the user when a version already exists" do - response_text = Chef::JSONCompat.to_json({ error_messages: ["Version already exists"] }) - allow(@upload_response).to receive(:body).and_return(response_text) - allow(@upload_response).to receive(:code).and_return(409) - expect { @knife.run }.to raise_error(SystemExit) - expect(@stderr.string).to match(/ERROR(.+)cookbook already exists/) - end - - it "should pass any errors on to the user" do - response_text = Chef::JSONCompat.to_json({ error_messages: ["You're holding it wrong"] }) - allow(@upload_response).to receive(:body).and_return(response_text) - allow(@upload_response).to receive(:code).and_return(403) - expect { @knife.run }.to raise_error(SystemExit) - expect(@stderr.string).to match("ERROR(.*)You're holding it wrong") - end - - it "should print the body if no errors are exposed on failure" do - response_text = Chef::JSONCompat.to_json({ system_error: "Your call was dropped", reason: "There's a map for that" }) - allow(@upload_response).to receive(:body).and_return(response_text) - allow(@upload_response).to receive(:code).and_return(500) - expect(@knife.ui).to receive(:error).with(/#{Regexp.escape(response_text)}/) # .ordered - expect(@knife.ui).to receive(:error).with(/Unknown error/) # .ordered - expect { @knife.run }.to raise_error(SystemExit) - end - - end - -end diff --git a/spec/unit/knife/cookbook_site_unshare_spec.rb b/spec/unit/knife/cookbook_site_unshare_spec.rb deleted file mode 100644 index 7797fdb3f8..0000000000 --- a/spec/unit/knife/cookbook_site_unshare_spec.rb +++ /dev/null @@ -1,77 +0,0 @@ -# -# Author:: Stephen Delano () -# Author:: Tim Hinderliter () -# Copyright:: Copyright 2010-2016, Chef Software Inc. -# License:: Apache License, Version 2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -require "spec_helper" - -describe Chef::Knife::CookbookSiteUnshare do - - before(:each) do - @knife = Chef::Knife::CookbookSiteUnshare.new - @knife.name_args = ["cookbook_name"] - allow(@knife).to receive(:confirm).and_return(true) - - @rest = double("Chef::ServerAPI") - allow(@rest).to receive(:delete).and_return(true) - allow(@knife).to receive(:rest).and_return(@rest) - @stdout = StringIO.new - allow(@knife.ui).to receive(:stdout).and_return(@stdout) - end - - describe "run" do - - describe "with no cookbook argument" do - it "should print the usage and exit" do - @knife.name_args = [] - expect(@knife.ui).to receive(:fatal) - expect(@knife).to receive(:show_usage) - expect { @knife.run }.to raise_error(SystemExit) - end - end - - it "should confirm you want to unshare the cookbook" do - expect(@knife).to receive(:confirm) - @knife.run - end - - it "should send a delete request to the cookbook site" do - expect(@rest).to receive(:delete) - @knife.run - end - - it "should log an error and exit when forbidden" do - exception = double('403 "Forbidden"', code: "403") - allow(@rest).to receive(:delete).and_raise(Net::HTTPServerException.new('403 "Forbidden"', exception)) - expect(@knife.ui).to receive(:error) - expect { @knife.run }.to raise_error(SystemExit) - end - - it "should re-raise any non-forbidden errors on delete" do - exception = double('500 "Application Error"', code: "500") - allow(@rest).to receive(:delete).and_raise(Net::HTTPServerException.new('500 "Application Error"', exception)) - expect { @knife.run }.to raise_error(Net::HTTPServerException) - end - - it "should log a success message" do - expect(@knife.ui).to receive(:info) - @knife.run - end - - end - -end diff --git a/spec/unit/knife/supermarket_download_spec.rb b/spec/unit/knife/supermarket_download_spec.rb new file mode 100644 index 0000000000..0332bd4390 --- /dev/null +++ b/spec/unit/knife/supermarket_download_spec.rb @@ -0,0 +1,152 @@ +# +# Author:: Thomas Bishop () +# Copyright:: Copyright 2012-2016, Thomas Bishop +# Copyright:: Copyright 2018, Chef Software, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require "chef/knife/supermarket_download" +require "spec_helper" + +describe Chef::Knife::SupermarketDownload do + + describe "run" do + before do + @knife = Chef::Knife::SupermarketDownload.new + @knife.name_args = ["apache2"] + @noauth_rest = double("no auth rest") + @stderr = StringIO.new + @cookbook_api_url = "https://supermarket.chef.io/api/v1/cookbooks" + @version = "1.0.2" + @version_us = @version.tr ".", "_" + @current_data = { "deprecated" => false, + "latest_version" => "#{@cookbook_api_url}/apache2/versions/#{@version_us}", + "replacement" => "other_apache2" } + + allow(@knife.ui).to receive(:stderr).and_return(@stderr) + allow(@knife).to receive(:noauth_rest).and_return(@noauth_rest) + expect(@noauth_rest).to receive(:get) + .with("#{@cookbook_api_url}/apache2") + .and_return(@current_data) + @knife.configure_chef + end + + context "when the cookbook is deprecated and not forced" do + before do + @current_data["deprecated"] = true + end + + it "should warn with info about the replacement" do + expect(@knife.ui).to receive(:warn) + .with(/.+deprecated.+replaced by other_apache2.+/i) + expect(@knife.ui).to receive(:warn) + .with(/use --force.+download.+/i) + @knife.run + end + end + + context "when" do + before do + @cookbook_data = { "version" => @version, + "file" => "http://example.com/apache2_#{@version_us}.tgz" } + @temp_file = double( path: "/tmp/apache2_#{@version_us}.tgz" ) + @file = File.join(Dir.pwd, "apache2-#{@version}.tar.gz") + end + + context "downloading the latest version" do + before do + expect(@noauth_rest).to receive(:get) + .with(@current_data["latest_version"]) + .and_return(@cookbook_data) + expect(@noauth_rest).to receive(:streaming_request) + .with(@cookbook_data["file"]) + .and_return(@temp_file) + end + + context "and it is deprecated and with --force" do + before do + @current_data["deprecated"] = true + @knife.config[:force] = true + end + + it "should download the latest version" do + expect(@knife.ui).to receive(:warn) + .with(/.+deprecated.+replaced by other_apache2.+/i) + expect(FileUtils).to receive(:cp).with(@temp_file.path, @file) + @knife.run + expect(@stderr.string).to match /downloading apache2.+version.+#{Regexp.escape(@version)}/i + expect(@stderr.string).to match /cookbook save.+#{Regexp.escape(@file)}/i + end + + end + + it "should download the latest version" do + expect(FileUtils).to receive(:cp).with(@temp_file.path, @file) + @knife.run + expect(@stderr.string).to match /downloading apache2.+version.+#{Regexp.escape(@version)}/i + expect(@stderr.string).to match /cookbook save.+#{Regexp.escape(@file)}/i + end + + context "with -f or --file" do + before do + @file = "/opt/chef/cookbooks/apache2.tar.gz" + @knife.config[:file] = @file + expect(FileUtils).to receive(:cp).with(@temp_file.path, @file) + end + + it "should download the cookbook to the desired file" do + @knife.run + expect(@stderr.string).to match /downloading apache2.+version.+#{Regexp.escape(@version)}/i + expect(@stderr.string).to match /cookbook save.+#{Regexp.escape(@file)}/i + end + end + + it "should provide an accessor to the version" do + allow(FileUtils).to receive(:cp).and_return(true) + expect(@knife.version).to eq(@version) + @knife.run + end + end + + context "downloading a cookbook of a specific version" do + before do + @version = "1.0.1" + @version_us = @version.tr ".", "_" + @cookbook_data = { "version" => @version, + "file" => "http://example.com/apache2_#{@version_us}.tgz" } + @temp_file = double(path: "/tmp/apache2_#{@version_us}.tgz") + @file = File.join(Dir.pwd, "apache2-#{@version}.tar.gz") + @knife.name_args << @version + end + + it "should download the desired version" do + expect(@noauth_rest).to receive(:get) + .with("#{@cookbook_api_url}/apache2/versions/#{@version_us}") + .and_return(@cookbook_data) + expect(@noauth_rest).to receive(:streaming_request) + .with(@cookbook_data["file"]) + .and_return(@temp_file) + expect(FileUtils).to receive(:cp).with(@temp_file.path, @file) + @knife.run + expect(@stderr.string).to match /downloading apache2.+version.+#{Regexp.escape(@version)}/i + expect(@stderr.string).to match /cookbook save.+#{Regexp.escape(@file)}/i + end + end + + end + + end + +end diff --git a/spec/unit/knife/supermarket_install_spec.rb b/spec/unit/knife/supermarket_install_spec.rb new file mode 100644 index 0000000000..68c8af1028 --- /dev/null +++ b/spec/unit/knife/supermarket_install_spec.rb @@ -0,0 +1,201 @@ +# +# Author:: Steven Danna () +# Copyright:: Copyright 2011-2018, Chef Software, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require "spec_helper" +require "chef/knife/supermarket_install" + +describe Chef::Knife::SupermarketInstall do + let(:knife) { Chef::Knife::SupermarketInstall.new } + let(:stdout) { StringIO.new } + let(:stderr) { StringIO.new } + let(:downloader) { Hash.new } + let(:archive) { double(Mixlib::Archive, extract: true) } + let(:repo) do + double(sanity_check: true, reset_to_default_state: true, + prepare_to_import: true, finalize_updates_to: true, + merge_updates_from: true) end + let(:install_path) do + if Chef::Platform.windows? + "C:/tmp/chef" + else + "/var/tmp/chef" + end + end + + before(:each) do + require "chef/knife/core/cookbook_scm_repo" + + allow(knife.ui).to receive(:stdout).and_return(stdout) + knife.config = {} + knife.config[:cookbook_path] = [ install_path ] + + allow(knife).to receive(:stderr).and_return(stderr) + allow(knife).to receive(:stdout).and_return(stdout) + + # Assume all external commands would have succeed. :( + allow(File).to receive(:unlink) + allow(File).to receive(:rmtree) + allow(knife).to receive(:shell_out!).and_return(true) + allow(Mixlib::Archive).to receive(:new).and_return(archive) + + # SupermarketDownload Setup + allow(knife).to receive(:download_cookbook_to).and_return(downloader) + allow(downloader).to receive(:version) do + if knife.name_args.size == 2 + knife.name_args[1] + else + "0.3.0" + end + end + + # Stubs for CookbookSCMRepo + allow(Chef::Knife::CookbookSCMRepo).to receive(:new).and_return(repo) + end + + describe "run" do + it "raises an error if a cookbook name is not provided" do + knife.name_args = [] + expect(knife.ui).to receive(:error).with("Please specify a cookbook to download and install.") + expect { knife.run }.to raise_error(SystemExit) + end + + it "raises an error if more than two arguments are given" do + knife.name_args = %w{foo bar baz} + expect(knife.ui).to receive(:error).with("Installing multiple cookbooks at once is not supported.") + expect { knife.run }.to raise_error(SystemExit) + end + + it "raises an error if the second argument is not a version" do + knife.name_args = ["getting-started", "1pass"] + expect(knife.ui).to receive(:error).with("Installing multiple cookbooks at once is not supported.") + expect { knife.run }.to raise_error(SystemExit) + end + + it "raises an error if the second argument is a four-digit version" do + knife.name_args = ["getting-started", "0.0.0.1"] + expect(knife.ui).to receive(:error).with("Installing multiple cookbooks at once is not supported.") + expect { knife.run }.to raise_error(SystemExit) + end + + it "raises an error if the second argument is a one-digit version" do + knife.name_args = ["getting-started", "1"] + expect(knife.ui).to receive(:error).with("Installing multiple cookbooks at once is not supported.") + expect { knife.run }.to raise_error(SystemExit) + end + + it "installs the specified version if second argument is a three-digit version" do + knife.name_args = ["getting-started", "0.1.0"] + knife.config[:no_deps] = true + upstream_file = File.join(install_path, "getting-started.tar.gz") + expect(knife).to receive(:download_cookbook_to).with(upstream_file) + expect(knife).to receive(:extract_cookbook).with(upstream_file, "0.1.0") + expect(knife).to receive(:clear_existing_files).with(File.join(install_path, "getting-started")) + expect(repo).to receive(:merge_updates_from).with("getting-started", "0.1.0") + knife.run + end + + it "installs the specified version if second argument is a two-digit version" do + knife.name_args = ["getting-started", "0.1"] + knife.config[:no_deps] = true + upstream_file = File.join(install_path, "getting-started.tar.gz") + expect(knife).to receive(:download_cookbook_to).with(upstream_file) + expect(knife).to receive(:extract_cookbook).with(upstream_file, "0.1") + expect(knife).to receive(:clear_existing_files).with(File.join(install_path, "getting-started")) + expect(repo).to receive(:merge_updates_from).with("getting-started", "0.1") + knife.run + end + + it "installs the latest version if only a cookbook name is given" do + knife.name_args = ["getting-started"] + knife.config[:no_deps] = true + upstream_file = File.join(install_path, "getting-started.tar.gz") + expect(knife).to receive(:download_cookbook_to).with(upstream_file) + expect(knife).to receive(:extract_cookbook).with(upstream_file, "0.3.0") + expect(knife).to receive(:clear_existing_files).with(File.join(install_path, "getting-started")) + expect(repo).to receive(:merge_updates_from).with("getting-started", "0.3.0") + knife.run + end + + it "does not create/reset git branches if use_current_branch is set" do + knife.name_args = ["getting-started"] + knife.config[:use_current_branch] = true + knife.config[:no_deps] = true + upstream_file = File.join(install_path, "getting-started.tar.gz") + expect(repo).not_to receive(:prepare_to_import) + expect(repo).not_to receive(:reset_to_default_state) + knife.run + end + + it "does not raise an error if cookbook_path is a string" do + knife.config[:cookbook_path] = install_path + knife.config[:no_deps] = true + knife.name_args = ["getting-started"] + upstream_file = File.join(install_path, "getting-started.tar.gz") + expect(knife).to receive(:download_cookbook_to).with(upstream_file) + expect(knife).to receive(:extract_cookbook).with(upstream_file, "0.3.0") + expect(knife).to receive(:clear_existing_files).with(File.join(install_path, "getting-started")) + expect(repo).to receive(:merge_updates_from).with("getting-started", "0.3.0") + expect { knife.run }.not_to raise_error + end + end # end of run + + let(:metadata) { Chef::Cookbook::Metadata.new } + let(:rb_metadata_path) { File.join(install_path, "post-punk-kitchen", "metadata.rb") } + let(:json_metadata_path) { File.join(install_path, "post-punk-kitchen", "metadata.json") } + + describe "preferred_metadata" do + before do + allow(Chef::Cookbook::Metadata).to receive(:new).and_return(metadata) + allow(File).to receive(:exist?).and_return(false) + knife.instance_variable_set(:@cookbook_name, "post-punk-kitchen") + knife.instance_variable_set(:@install_path, install_path) + end + + it "returns a populated Metadata object if metadata.rb exists" do + allow(File).to receive(:exist?).with(rb_metadata_path).and_return(true) + expect(metadata).to receive(:from_file).with(rb_metadata_path) + knife.preferred_metadata + end + + it "returns a populated Metadata object if metadata.json exists" do + allow(File).to receive(:exist?).with(json_metadata_path).and_return(true) + # expect(IO).to receive(:read).with(json_metadata_path) + allow(IO).to receive(:read) + expect(metadata).to receive(:from_json) + knife.preferred_metadata + end + + it "prefers metadata.rb over metadata.json" do + allow(File).to receive(:exist?).with(rb_metadata_path).and_return(true) + allow(File).to receive(:exist?).with(json_metadata_path).and_return(true) + allow(IO).to receive(:read) + expect(metadata).to receive(:from_file).with(rb_metadata_path) + expect(metadata).not_to receive(:from_json) + knife.preferred_metadata + end + + it "rasies an error if it finds no metadata file" do + expect { knife.preferred_metadata }.to raise_error { |error| + expect(error).to be_a(Chef::Exceptions::MetadataNotFound) + expect(error.cookbook_name).to eq("post-punk-kitchen") + expect(error.install_path).to eq(install_path) + } + end + + end +end diff --git a/spec/unit/knife/supermarket_share_spec.rb b/spec/unit/knife/supermarket_share_spec.rb new file mode 100644 index 0000000000..dde58775f4 --- /dev/null +++ b/spec/unit/knife/supermarket_share_spec.rb @@ -0,0 +1,209 @@ +# +# Author:: Stephen Delano () +# Copyright:: Copyright 2010-2018, Chef Software Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require "spec_helper" +require "chef/knife/supermarket_share" +require "chef/cookbook_uploader" +require "chef/cookbook_site_streaming_uploader" + +describe Chef::Knife::SupermarketShare do + + before(:each) do + @knife = Chef::Knife::SupermarketShare.new + # Merge default settings in. + @knife.merge_configs + @knife.name_args = %w{cookbook_name AwesomeSausage} + + @cookbook = Chef::CookbookVersion.new("cookbook_name") + + @cookbook_loader = double("Chef::CookbookLoader") + allow(@cookbook_loader).to receive(:cookbook_exists?).and_return(true) + allow(@cookbook_loader).to receive(:[]).and_return(@cookbook) + allow(Chef::CookbookLoader).to receive(:new).and_return(@cookbook_loader) + + @noauth_rest = double(Chef::ServerAPI) + allow(@knife).to receive(:noauth_rest).and_return(@noauth_rest) + + @cookbook_uploader = Chef::CookbookUploader.new("herpderp", rest: "norest") + allow(Chef::CookbookUploader).to receive(:new).and_return(@cookbook_uploader) + allow(@cookbook_uploader).to receive(:validate_cookbooks).and_return(true) + allow(Chef::CookbookSiteStreamingUploader).to receive(:create_build_dir).and_return(Dir.mktmpdir) + + allow(@knife).to receive(:shell_out!).and_return(true) + @stdout = StringIO.new + allow(@knife.ui).to receive(:stdout).and_return(@stdout) + end + + describe "run" do + + before(:each) do + allow(@knife).to receive(:do_upload).and_return(true) + @category_response = { + "name" => "cookbook_name", + "category" => "Testing Category", + } + @bad_category_response = { + "error_code" => "NOT_FOUND", + "error_messages" => [ + "Resource does not exist.", + ], + } + end + + it "should set true to config[:dry_run] as default" do + expect(@knife.config[:dry_run]).to be_falsey + end + + it "should should print usage and exit when given no arguments" do + @knife.name_args = [] + expect(@knife).to receive(:show_usage) + expect(@knife.ui).to receive(:fatal) + expect { @knife.run }.to raise_error(SystemExit) + end + + it "should not fail when given only 1 argument and can determine category" do + @knife.name_args = ["cookbook_name"] + expect(@noauth_rest).to receive(:get).with("https://supermarket.chef.io/api/v1/cookbooks/cookbook_name").and_return(@category_response) + expect(@knife).to receive(:do_upload) + @knife.run + end + + it "should use a default category when given only 1 argument and cannot determine category" do + @knife.name_args = ["cookbook_name"] + expect(@noauth_rest).to receive(:get).with("https://supermarket.chef.io/api/v1/cookbooks/cookbook_name") { raise Net::HTTPServerException.new("404 Not Found", OpenStruct.new(code: "404")) } + expect(@knife).to receive(:do_upload) + expect { @knife.run }.to_not raise_error + end + + it "should print error and exit when given only 1 argument and Chef::ServerAPI throws an exception" do + @knife.name_args = ["cookbook_name"] + expect(@noauth_rest).to receive(:get).with("https://supermarket.chef.io/api/v1/cookbooks/cookbook_name") { raise Errno::ECONNREFUSED, "Connection refused" } + expect(@knife.ui).to receive(:fatal) + expect { @knife.run }.to raise_error(SystemExit) + end + + it "should check if the cookbook exists" do + expect(@cookbook_loader).to receive(:cookbook_exists?) + @knife.run + end + + it "should exit and log to error if the cookbook doesn't exist" do + allow(@cookbook_loader).to receive(:cookbook_exists?).and_return(false) + expect(@knife.ui).to receive(:error) + expect { @knife.run }.to raise_error(SystemExit) + end + + if File.exists?("/usr/bin/gnutar") || File.exists?("/bin/gnutar") + it "should use gnutar to make a tarball of the cookbook" do + expect(@knife).to receive(:shell_out!) do |args| + expect(args.to_s).to match(/gnutar -czf/) + end + @knife.run + end + else + it "should make a tarball of the cookbook" do + expect(@knife).to receive(:shell_out!) do |args| + expect(args.to_s).to match(/tar -czf/) + end + @knife.run + end + end + + it "should exit and log to error when the tarball creation fails" do + allow(@knife).to receive(:shell_out!).and_raise(Chef::Exceptions::Exec) + expect(@knife.ui).to receive(:error) + expect { @knife.run }.to raise_error(SystemExit) + end + + it "should upload the cookbook and clean up the tarball" do + expect(@knife).to receive(:do_upload) + expect(FileUtils).to receive(:rm_rf) + @knife.run + end + + context "when the --dry-run flag is specified" do + before do + allow(Chef::CookbookSiteStreamingUploader).to receive(:create_build_dir).and_return("/var/tmp/dummy") + @knife.config = { dry_run: true } + allow(@knife).to receive_message_chain(:shell_out!, :stdout).and_return("file") + end + + it "should list files in the tarball" do + allow(@knife).to receive(:tar_cmd).and_return("footar") + expect(@knife).to receive(:shell_out!).with("footar -czf #{@cookbook.name}.tgz #{@cookbook.name}", { cwd: "/var/tmp/dummy" }) + expect(@knife).to receive(:shell_out!).with("footar -tzf #{@cookbook.name}.tgz", { cwd: "/var/tmp/dummy" }) + @knife.run + end + + it "does not upload the cookbook" do + allow(@knife).to receive(:shell_out!).and_return(true) + expect(@knife).not_to receive(:do_upload) + @knife.run + end + end + end + + describe "do_upload" do + + before(:each) do + @upload_response = double("Net::HTTPResponse") + allow(Chef::CookbookSiteStreamingUploader).to receive(:post).and_return(@upload_response) + + @stdout = StringIO.new + @stderr = StringIO.new + allow(@knife.ui).to receive(:stdout).and_return(@stdout) + allow(@knife.ui).to receive(:stderr).and_return(@stderr) + allow(File).to receive(:open).and_return(true) + end + + it 'should post the cookbook to "https://supermarket.chef.io"' do + response_text = Chef::JSONCompat.to_json({ uri: "https://supermarket.chef.io/cookbooks/cookbook_name" }) + allow(@upload_response).to receive(:body).and_return(response_text) + allow(@upload_response).to receive(:code).and_return(201) + expect(Chef::CookbookSiteStreamingUploader).to receive(:post).with(/supermarket\.chef\.io/, anything(), anything(), anything()) + @knife.run + end + + it "should alert the user when a version already exists" do + response_text = Chef::JSONCompat.to_json({ error_messages: ["Version already exists"] }) + allow(@upload_response).to receive(:body).and_return(response_text) + allow(@upload_response).to receive(:code).and_return(409) + expect { @knife.run }.to raise_error(SystemExit) + expect(@stderr.string).to match(/ERROR(.+)cookbook already exists/) + end + + it "should pass any errors on to the user" do + response_text = Chef::JSONCompat.to_json({ error_messages: ["You're holding it wrong"] }) + allow(@upload_response).to receive(:body).and_return(response_text) + allow(@upload_response).to receive(:code).and_return(403) + expect { @knife.run }.to raise_error(SystemExit) + expect(@stderr.string).to match("ERROR(.*)You're holding it wrong") + end + + it "should print the body if no errors are exposed on failure" do + response_text = Chef::JSONCompat.to_json({ system_error: "Your call was dropped", reason: "There's a map for that" }) + allow(@upload_response).to receive(:body).and_return(response_text) + allow(@upload_response).to receive(:code).and_return(500) + expect(@knife.ui).to receive(:error).with(/#{Regexp.escape(response_text)}/) # .ordered + expect(@knife.ui).to receive(:error).with(/Unknown error/) # .ordered + expect { @knife.run }.to raise_error(SystemExit) + end + + end + +end diff --git a/spec/unit/knife/supermarket_unshare_spec.rb b/spec/unit/knife/supermarket_unshare_spec.rb new file mode 100644 index 0000000000..206717e211 --- /dev/null +++ b/spec/unit/knife/supermarket_unshare_spec.rb @@ -0,0 +1,78 @@ +# +# Author:: Stephen Delano () +# Author:: Tim Hinderliter () +# Copyright:: Copyright 2010-2018, Chef Software Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require "spec_helper" +require "chef/knife/supermarket_unshare" + +describe Chef::Knife::SupermarketUnshare do + + before(:each) do + @knife = Chef::Knife::SupermarketUnshare.new + @knife.name_args = ["cookbook_name"] + allow(@knife).to receive(:confirm).and_return(true) + + @rest = double("Chef::ServerAPI") + allow(@rest).to receive(:delete).and_return(true) + allow(@knife).to receive(:rest).and_return(@rest) + @stdout = StringIO.new + allow(@knife.ui).to receive(:stdout).and_return(@stdout) + end + + describe "run" do + + describe "with no cookbook argument" do + it "should print the usage and exit" do + @knife.name_args = [] + expect(@knife.ui).to receive(:fatal) + expect(@knife).to receive(:show_usage) + expect { @knife.run }.to raise_error(SystemExit) + end + end + + it "should confirm you want to unshare the cookbook" do + expect(@knife).to receive(:confirm) + @knife.run + end + + it "should send a delete request to the cookbook site" do + expect(@rest).to receive(:delete) + @knife.run + end + + it "should log an error and exit when forbidden" do + exception = double('403 "Forbidden"', code: "403") + allow(@rest).to receive(:delete).and_raise(Net::HTTPServerException.new('403 "Forbidden"', exception)) + expect(@knife.ui).to receive(:error) + expect { @knife.run }.to raise_error(SystemExit) + end + + it "should re-raise any non-forbidden errors on delete" do + exception = double('500 "Application Error"', code: "500") + allow(@rest).to receive(:delete).and_raise(Net::HTTPServerException.new('500 "Application Error"', exception)) + expect { @knife.run }.to raise_error(Net::HTTPServerException) + end + + it "should log a success message" do + expect(@knife.ui).to receive(:info) + @knife.run + end + + end + +end -- cgit v1.2.1