From 8904afb917f85fa5fa257c225fcc8da9a69c5ce5 Mon Sep 17 00:00:00 2001 From: Thom May Date: Thu, 1 Sep 2016 14:36:12 +0100 Subject: [PATCH] Implement a Chef Server fetcher Signed-off-by: Thom May --- lib/cookbook-omnifetch/chef_server.rb | 108 ++++++++++++++++++++++++++ spec/unit/chef_server_spec.rb | 75 ++++++++++++++++++ 2 files changed, 183 insertions(+) create mode 100644 lib/cookbook-omnifetch/chef_server.rb create mode 100644 spec/unit/chef_server_spec.rb diff --git a/lib/cookbook-omnifetch/chef_server.rb b/lib/cookbook-omnifetch/chef_server.rb new file mode 100644 index 0000000..c8f0716 --- /dev/null +++ b/lib/cookbook-omnifetch/chef_server.rb @@ -0,0 +1,108 @@ +require 'cookbook-omnifetch/base' + +module CookbookOmnifetch + class CookbookMetadata + + FILE_TYPES = [ + :resources, + :providers, + :recipes, + :definitions, + :libraries, + :attributes, + :files, + :templates, + :root_files + ].freeze + + def initialize(metadata) + @metadata = metadata + end + + def files(&block) + FILE_TYPES.each do |type| + next unless @metadata.has_key?(type.to_s) + + @metadata[type.to_s].each do |file| + yield file["url"], file["path"] + end + end + end + end + + class ChefserverLocation < BaseLocation + + attr_reader :cookbook_version + + def initialize(dependency, options = {}) + super + @cookbook_version = options[:version] + @http_client = options[:http_client] + @uri ||= options[:artifactserver] + end + + def repo_host + @host ||= URI.parse(uri).host + end + + def cookbook_name + dependency.name + end + + def install + FileUtils.mkdir_p(staging_root) unless staging_root.exist? + md = http_client.get("/cookbooks/#{cookbook_name}/#{cookbook_version}") + CookbookMetadata.new(md).files do |url, path| + stage = staging_path.join(path) + FileUtils.mkdir_p(File.dirname(stage)) + + http_client.streaming_request(url) do |tempfile| + tempfile.close + FileUtils.mv(tempfile.path, stage) + end + end + FileUtils.mv(staging_path, install_path) + end + + # Determine if this revision is installed. + # + # @return [Boolean] + def installed? + install_path.exist? + end + + def http_client + @http_client + end + + # The path where this cookbook would live in the store, if it were + # installed. + # + # @return [Pathname, nil] + def install_path + @install_path ||= CookbookOmnifetch.storage_path.join(cache_key) + end + + def cache_key + "#{dependency.name}-#{cookbook_version}" + end + + # The path where tarballs are downloaded to and unzipped. On certain platforms + # you have a better chance of getting an atomic move if your temporary working + # directory is on the same device/volume as the destination. To support this, + # we use a staging directory located under the cache path under the rather mild + # assumption that everything under the cache path is going to be on one device. + # + # Do not create anything under this directory that isn't randomly named and + # remember to release your files once you are done. + # + # @return [Pathname] + def staging_root + Pathname.new(CookbookOmnifetch.cache_path).join('.cache_tmp', 'artifactserver') + end + + def staging_path + staging_root.join(cache_key) + end + end +end diff --git a/spec/unit/chef_server_spec.rb b/spec/unit/chef_server_spec.rb new file mode 100644 index 0000000..b8e3a25 --- /dev/null +++ b/spec/unit/chef_server_spec.rb @@ -0,0 +1,75 @@ +require "spec_helper" +require "cookbook-omnifetch/chef_server.rb" + +module CookbookOmnifetch + METADATA = { + "recipes" => [ + {"name"=>"default.rb", "path"=>"recipes/default.rb", "checksum"=>"a6be794cdd2eb44d38fdf17f792a0d0d", "specificity"=>"default", "url"=>"https://example.com/recipes/default.rb"}, + ], + "root_files"=>[ + {"name"=>"metadata.rb", "path"=>"metadata.rb", "checksum"=>"5b346119e5e41ab99500608decac8dca", "specificity"=>"default", "url"=>"https://example.com/metadata.rb"}, + ], + } + + describe CookbookMetadata do + let(:cb_metadata) { CookbookMetadata.new(METADATA) } + + it "yields a set of paths and urls" do + expect { |b| cb_metadata.files(&b) }.to yield_successive_args(["https://example.com/recipes/default.rb", "recipes/default.rb"], ["https://example.com/metadata.rb", "metadata.rb"]) + end + end + + describe ChefserverLocation do + + let(:http_client) { double("Http Client") } + + let(:cb_metadata) { CookbookMetadata.new(METADATA) } + + let(:test_root) { Dir.mktmpdir(nil) } + + let(:storage_path) { File.join(test_root, "storage") } + + let(:cache_path) { File.join(test_root, "cache") } + + let(:constraint) { double("Constraint") } + + let(:dependency) { double("Dependency", name: cookbook_name, constraint: constraint) } + + let(:cookbook_name) { "example" } + let(:cookbook_version) { "0.5.0" } + + let(:cookbook_fixture_path) { fixtures_path.join("cookbooks/example_cookbook") } + + let(:remote_path) { File.join(test_root, "remote") } + let(:options) { {version: cookbook_version, http_client: http_client } } + + let(:cookbook_files) { %w". .. metadata.rb recipes" } + subject(:chef_server_location) { described_class.new(dependency, options) } + + before do + allow(CookbookOmnifetch).to receive(:storage_path).and_return(Pathname.new(storage_path)) + allow(CookbookOmnifetch).to receive(:cache_path).and_return(cache_path) + allow_any_instance_of(File).to receive(:close).and_return(true) + FileUtils.cp_r(cookbook_fixture_path, remote_path) + FileUtils.mkdir_p(storage_path) + end + + after do + FileUtils.rm_r(test_root) + end + + it "installs the cookbook to the desired install path" do + expect(http_client).to receive(:get).with("/cookbooks/example/0.5.0").and_return(METADATA) + expect(http_client).to receive(:streaming_request).twice do |url, &block| + path = url.split("/", 4)[3] + path = File.join(remote_path, path) + block.call(File.open(path)) + end + + chef_server_location.install + + expect(Dir).to exist(chef_server_location.install_path) + expect(Dir.entries(chef_server_location.install_path)).to match_array(cookbook_files) + end + end +end