diff options
-rw-r--r-- | lib/chef/mixin/uris.rb | 33 | ||||
-rw-r--r-- | lib/chef/provider/package/windows.rb | 96 | ||||
-rw-r--r-- | lib/chef/resource/windows_package.rb | 26 | ||||
-rw-r--r-- | spec/unit/mixin/uris_spec.rb | 45 | ||||
-rw-r--r-- | spec/unit/provider/package/windows_spec.rb | 129 | ||||
-rw-r--r-- | spec/unit/resource/windows_package_spec.rb | 18 |
6 files changed, 312 insertions, 35 deletions
diff --git a/lib/chef/mixin/uris.rb b/lib/chef/mixin/uris.rb new file mode 100644 index 0000000000..6c1ae793e2 --- /dev/null +++ b/lib/chef/mixin/uris.rb @@ -0,0 +1,33 @@ +# +# Author:: Jay Mundrawala (<jdm@chef.io>) +# Copyright:: Copyright (c) 2015 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 'uri' + +class Chef + module Mixin + module Uris + # uri_scheme? returns true if the string starts with + # scheme:// + # For example, it will match http://foo.bar.com + def uri_scheme?(source) + # From open-uri + !!(%r{\A[A-Za-z][A-Za-z0-9+\-\.]*://} =~ source) + end + end + end +end diff --git a/lib/chef/provider/package/windows.rb b/lib/chef/provider/package/windows.rb index 143d82f111..7ff0b71807 100644 --- a/lib/chef/provider/package/windows.rb +++ b/lib/chef/provider/package/windows.rb @@ -16,14 +16,18 @@ # limitations under the License. # +require 'chef/mixin/uris' require 'chef/resource/windows_package' require 'chef/provider/package' require 'chef/util/path_helper' +require 'chef/mixin/checksum' class Chef class Provider class Package class Windows < Chef::Provider::Package + include Chef::Mixin::Uris + include Chef::Mixin::Checksum provides :package, os: "windows" provides :windows_package, os: "windows" @@ -36,19 +40,23 @@ class Chef # load_current_resource is run in Chef::Provider#run_action when not in whyrun_mode? def load_current_resource - @new_resource.source(Chef::Util::PathHelper.validate_path(@new_resource.source)) - @current_resource = Chef::Resource::WindowsPackage.new(@new_resource.name) - @current_resource.version(package_provider.installed_version) - @new_resource.version(package_provider.package_version) - @current_resource + if downloadable_file_missing? + Chef::Log.debug("We do not know the version of #{new_resource.source} because the file is not downloaded") + current_resource.version(:unknown.to_s) + else + current_resource.version(package_provider.installed_version) + new_resource.version(package_provider.package_version) + end + + current_resource end def package_provider @package_provider ||= begin case installer_type when :msi - Chef::Provider::Package::Windows::MSI.new(@new_resource) + Chef::Provider::Package::Windows::MSI.new(resource_for_provider) else raise "Unable to find a Chef::Provider::Package::Windows provider for installer_type '#{installer_type}'" end @@ -71,6 +79,17 @@ class Chef end end + def action_install + if uri_scheme?(new_resource.source) + download_source_file + load_current_resource + else + validate_content! + end + + super + end + # Chef::Provider::Package action_install + action_remove call install_package + remove_package # Pass those calls to the correct sub-provider def install_package(name, version) @@ -80,6 +99,71 @@ class Chef def remove_package(name, version) package_provider.remove_package(name, version) end + + # @return [Array] new_version(s) as an array + def new_version_array + # Because the one in the parent caches things + [new_resource.version] + end + + private + + def downloadable_file_missing? + uri_scheme?(new_resource.source) && !::File.exists?(source_location) + end + + def resource_for_provider + @resource_for_provider = Chef::Resource::WindowsPackage.new(new_resource.name).tap do |r| + r.source(Chef::Util::PathHelper.validate_path(source_location)) + r.timeout(new_resource.timeout) + r.returns(new_resource.returns) + r.options(new_resource.options) + end + end + + def download_source_file + source_resource.run_action(:create) + Chef::Log.debug("#{@new_resource} fetched source file to #{source_resource.path}") + end + + def source_resource + @source_resource ||= Chef::Resource::RemoteFile.new(default_download_cache_path, run_context).tap do |r| + r.source(new_resource.source) + r.checksum(new_resource.checksum) + r.backup(false) + + if new_resource.remote_file_attributes + new_resource.remote_file_attributes.each do |(k,v)| + r.send(k.to_sym, v) + end + end + end + end + + def default_download_cache_path + uri = ::URI.parse(new_resource.source) + filename = ::File.basename(::URI.unescape(uri.path)) + file_cache_dir = Chef::FileCache.create_cache_path("package/") + Chef::Util::PathHelper.cleanpath("#{file_cache_dir}/#{filename}") + end + + def source_location + if uri_scheme?(new_resource.source) + source_resource.path + else + Chef::Util::PathHelper.cleanpath(new_resource.source) + end + end + + def validate_content! + if new_resource.checksum + source_checksum = checksum(source_location) + if new_resource.checksum != source_checksum + raise Chef::Exceptions::ChecksumMismatch.new(short_cksum(new_resource.checksum), short_cksum(source_checksum)) + end + end + end + end end end diff --git a/lib/chef/resource/windows_package.rb b/lib/chef/resource/windows_package.rb index 16cfcf865e..d4f8ae0603 100644 --- a/lib/chef/resource/windows_package.rb +++ b/lib/chef/resource/windows_package.rb @@ -16,6 +16,7 @@ # limitations under the License. # +require 'chef/mixin/uris' require 'chef/resource/package' require 'chef/provider/package/windows' require 'chef/win32/error' if RUBY_PLATFORM =~ /mswin|mingw|windows/ @@ -23,6 +24,7 @@ require 'chef/win32/error' if RUBY_PLATFORM =~ /mswin|mingw|windows/ class Chef class Resource class WindowsPackage < Chef::Resource::Package + include Chef::Mixin::Uris provides :package, os: "windows" provides :windows_package, os: "windows" @@ -69,10 +71,30 @@ class Chef @source else raise ArgumentError, "Bad type for WindowsPackage resource, use a String" unless arg.is_a?(String) - Chef::Log.debug("#{package_name}: sanitizing source path '#{arg}'") - @source = ::File.absolute_path(arg).gsub(::File::SEPARATOR, ::File::ALT_SEPARATOR) + if uri_scheme?(arg) + @source = arg + else + @source = Chef::Util::PathHelper.canonical_path(arg, false) + end end end + + def checksum(arg=nil) + set_or_return( + :checksum, + arg, + :kind_of => [ String ] + ) + end + + def remote_file_attributes(arg=nil) + set_or_return( + :remote_file_attributes, + arg, + :kind_of => [ Hash ] + ) + end + end end end diff --git a/spec/unit/mixin/uris_spec.rb b/spec/unit/mixin/uris_spec.rb new file mode 100644 index 0000000000..07e4f34508 --- /dev/null +++ b/spec/unit/mixin/uris_spec.rb @@ -0,0 +1,45 @@ +# +# Author:: Jay Mundrawala (<jdm@chef.io>) +# Copyright:: Copyright (c) 2015 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/mixin/uris' + +class Chef::UrisTest + include Chef::Mixin::Uris +end + +describe Chef::Mixin::Uris do + let (:uris) { Chef::UrisTest.new } + + it "matches 'scheme://foo.com'" do + expect(uris.uri_scheme?('scheme://foo.com')).to eq(true) + end + + it "does not match 'c:/foo.com'" do + expect(uris.uri_scheme?('c:/foo.com')).to eq(false) + end + + it "does not match '/usr/bin/foo.com'" do + expect(uris.uri_scheme?('/usr/bin/foo.com')).to eq(false) + end + + it "does not match 'c:/foo.com://bar.com'" do + expect(uris.uri_scheme?('c:/foo.com://bar.com')).to eq(false) + end + +end diff --git a/spec/unit/provider/package/windows_spec.rb b/spec/unit/provider/package/windows_spec.rb index d402113d72..e5acc87694 100644 --- a/spec/unit/provider/package/windows_spec.rb +++ b/spec/unit/provider/package/windows_spec.rb @@ -19,50 +19,129 @@ require 'spec_helper' describe Chef::Provider::Package::Windows, :windows_only do + before(:each) do + allow(Chef::Util::PathHelper).to receive(:windows?).and_return(true) + allow(Chef::FileCache).to receive(:create_cache_path).with("package/").and_return(cache_path) + end + let(:node) { double('Chef::Node') } let(:events) { double('Chef::Events').as_null_object } # mock all the methods let(:run_context) { double('Chef::RunContext', :node => node, :events => events) } - let(:new_resource) { Chef::Resource::WindowsPackage.new("calculator.msi") } + let(:resource_source) { 'calculator.msi' } + let(:new_resource) { Chef::Resource::WindowsPackage.new(resource_source) } let(:provider) { Chef::Provider::Package::Windows.new(new_resource, run_context) } + let(:cache_path) { 'c:\\cache\\' } describe "load_current_resource" do - before(:each) do - allow(Chef::Util::PathHelper).to receive(:validate_path) - allow(provider).to receive(:package_provider).and_return(double('package_provider', + shared_examples "a local file" do + before(:each) do + allow(Chef::Util::PathHelper).to receive(:validate_path) + allow(provider).to receive(:package_provider).and_return(double('package_provider', :installed_version => "1.0", :package_version => "2.0")) - end + end - it "creates a current resource with the name of the new resource" do - provider.load_current_resource - expect(provider.current_resource).to be_a(Chef::Resource::WindowsPackage) - expect(provider.current_resource.name).to eql("calculator.msi") - end + it "creates a current resource with the name of the new resource" do + provider.load_current_resource + expect(provider.current_resource).to be_a(Chef::Resource::WindowsPackage) + expect(provider.current_resource.name).to eql(resource_source) + end + + it "sets the current version if the package is installed" do + provider.load_current_resource + expect(provider.current_resource.version).to eql("1.0") + end - it "sets the current version if the package is installed" do - provider.load_current_resource - expect(provider.current_resource.version).to eql("1.0") + it "sets the version to be installed" do + provider.load_current_resource + expect(provider.new_resource.version).to eql("2.0") + end end - it "sets the version to be installed" do - provider.load_current_resource - expect(provider.new_resource.version).to eql("2.0") + context "when the source is a uri" do + let(:resource_source) { 'https://foo.bar/calculator.msi' } + + context "when the source has not been downloaded" do + before(:each) do + allow(provider).to receive(:downloadable_file_missing?).and_return(true) + end + it "sets the current version to unknown" do + provider.load_current_resource + expect(provider.current_resource.version).to eql("unknown") + end + end + + context "when the source has been downloaded" do + before(:each) do + allow(provider).to receive(:downloadable_file_missing?).and_return(false) + end + it_behaves_like "a local file" + end + + context "when remote_file_attributes are provided" do + let (:remote_file_attributes) { {:path => 'C:\\foobar.msi'} } + before(:each) do + new_resource.remote_file_attributes(remote_file_attributes) + end + + it 'should override the attributes of the remote file resource used' do + expect(::File).to receive(:exists?).with(remote_file_attributes[:path]) + provider.load_current_resource + end + + end end - it "checks that the source path is valid" do - expect(Chef::Util::PathHelper).to receive(:validate_path) - provider.load_current_resource + context "when source is a local file" do + it_behaves_like "a local file" end end describe "package_provider" do - it "sets the package provider to MSI if the the installer type is :msi" do - allow(provider).to receive(:installer_type).and_return(:msi) - expect(provider.package_provider).to be_a(Chef::Provider::Package::Windows::MSI) + shared_examples "a local file" do + it "checks that the source path is valid" do + expect(Chef::Util::PathHelper).to receive(:validate_path) + provider.package_provider + end + + it "sets the package provider to MSI if the the installer type is :msi" do + allow(provider).to receive(:installer_type).and_return(:msi) + expect(provider.package_provider).to be_a(Chef::Provider::Package::Windows::MSI) + end + + it "raises an error if the installer_type is unknown" do + allow(provider).to receive(:installer_type).and_return(:apt_for_windows) + expect { provider.package_provider }.to raise_error + end + end + + context "when the source is a uri" do + let(:resource_source) { 'https://foo.bar/calculator.msi' } + + context "when the source has not been downloaded" do + before(:each) do + allow(provider).to receive(:should_download?).and_return(true) + end + + it "should create a package provider with source pointing at the local file" do + expect(Chef::Provider::Package::Windows::MSI).to receive(:new) do |r| + expect(r.source).to eq("#{cache_path}#{::File.basename(resource_source)}") + end + provider.package_provider + end + + it_behaves_like "a local file" + end + + context "when the source has been downloaded" do + before(:each) do + allow(provider).to receive(:should_download?).and_return(false) + end + it_behaves_like "a local file" + end end - it "raises an error if the installer_type is unknown" do - allow(provider).to receive(:installer_type).and_return(:apt_for_windows) - expect { provider.package_provider }.to raise_error + context "when source is a local file" do + it_behaves_like "a local file" end end diff --git a/spec/unit/resource/windows_package_spec.rb b/spec/unit/resource/windows_package_spec.rb index 1e02f2449b..6aa5d357ea 100644 --- a/spec/unit/resource/windows_package_spec.rb +++ b/spec/unit/resource/windows_package_spec.rb @@ -63,9 +63,9 @@ describe Chef::Resource::WindowsPackage, "initialize" do end it "coverts a source to an absolute path" do - allow(::File).to receive(:absolute_path).and_return("c:\\Files\\frost.msi") + allow(::File).to receive(:absolute_path).and_return("c:\\files\\frost.msi") resource.source("frost.msi") - expect(resource.source).to eql "c:\\Files\\frost.msi" + expect(resource.source).to eql "c:\\files\\frost.msi" end it "converts slashes to backslashes in the source path" do @@ -78,4 +78,18 @@ describe Chef::Resource::WindowsPackage, "initialize" do # it's a little late to stub out File.absolute_path expect(resource.source).to include("solitaire.msi") end + + it "supports the checksum attribute" do + resource.checksum('somechecksum') + expect(resource.checksum).to eq('somechecksum') + end + + context 'when a URL is used' do + let(:resource_source) { 'https://foo.bar/solitare.msi' } + let(:resource) { Chef::Resource::WindowsPackage.new(resource_source) } + + it "should return the source unmodified" do + expect(resource.source).to eq(resource_source) + end + end end |