diff options
author | Tim Smith <tsmith@chef.io> | 2017-10-03 11:37:06 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2017-10-03 11:37:06 -0700 |
commit | 92212e72d70e5be7c46fd4e0e763a0b4720e94c1 (patch) | |
tree | 82887f67ebfb3530a91a84d942f0ec141509ef97 | |
parent | 447dca82a3b2575e99ca776ad9a9fc1efea34767 (diff) | |
parent | 7ec360197a63585cb6c713f05bca402fdd142349 (diff) | |
download | chef-92212e72d70e5be7c46fd4e0e763a0b4720e94c1.tar.gz |
Merge pull request #6410 from chef/zypper_gpg_key
Import the zypper GPG key before templating the repo
-rw-r--r-- | lib/chef/provider/zypper_repository.rb | 112 | ||||
-rw-r--r-- | lib/chef/resource/zypper_repository.rb | 1 | ||||
-rw-r--r-- | spec/unit/provider/zypper_repository_spec.rb | 124 | ||||
-rw-r--r-- | spec/unit/resource/zypper_repository_spec.rb | 20 |
4 files changed, 236 insertions, 21 deletions
diff --git a/lib/chef/provider/zypper_repository.rb b/lib/chef/provider/zypper_repository.rb index e6fd917d77..31cf8839b2 100644 --- a/lib/chef/provider/zypper_repository.rb +++ b/lib/chef/provider/zypper_repository.rb @@ -18,24 +18,25 @@ require "chef/resource" require "chef/dsl/declare_resource" -require "chef/mixin/which" require "chef/provider/noop" +require "chef/mixin/shell_out" require "shellwords" class Chef class Provider class ZypperRepository < Chef::Provider - - extend Chef::Mixin::Which - - provides :zypper_repository do - which "zypper" - end + provides :zypper_repository, platform_family: "suse" def load_current_resource end action :create do + if new_resource.gpgautoimportkeys + install_gpg_key(new_resource.gpgkey) + else + Chef::Log.debug("'gpgautoimportkeys' property is set to false. Skipping key import.") + end + declare_resource(:template, "/etc/zypp/repos.d/#{escaped_repo_name}.repo") do if template_available?(new_resource.source) source new_resource.source @@ -51,14 +52,14 @@ class Chef end action :delete do - declare_resource(:execute, "zypper removerepo #{escaped_repo_name}") do - only_if "zypper lr #{escaped_repo_name}" + declare_resource(:execute, "zypper --quiet --non-interactive removerepo #{escaped_repo_name}") do + only_if "zypper --quiet lr #{escaped_repo_name}" end end action :refresh do - declare_resource(:execute, "zypper#{' --gpg-auto-import-keys' if new_resource.gpgautoimportkeys} --quiet --no-confirm refresh --force #{escaped_repo_name}") do - only_if "zypper lr #{escaped_repo_name}" + declare_resource(:execute, "zypper --quiet --non-interactive refresh --force #{escaped_repo_name}") do + only_if "zypper --quiet lr #{escaped_repo_name}" end end @@ -66,14 +67,101 @@ class Chef alias_method :action_remove, :action_delete # zypper repos are allowed to have spaces in the names + # @return [String] escaped repo string def escaped_repo_name Shellwords.escape(new_resource.repo_name) end + # return the specified cookbook name or the cookbook containing the + # resource. + # + # @return [String] name of the cookbook + def cookbook_name + new_resource.cookbook || new_resource.cookbook_name + end + + # determine if a template file is available in the current run + # @param [String] path the path to the template file + # + # @return [Boolean] template file exists or doesn't def template_available?(path) - !path.nil? && run_context.has_template_in_cookbook?(new_resource.cookbook_name, path) + !path.nil? && run_context.has_template_in_cookbook?(cookbook_name, path) + end + + # determine if a cookbook file is available in the run + # @param [String] path the path to the template file + # + # @return [Boolean] cookbook file exists or doesn't + def has_cookbook_file?(fn) + run_context.has_cookbook_file_in_cookbook?(cookbook_name, fn) + end + + # Given the provided key URI determine what kind of chef resource we need + # to fetch the key + # @param [String] uri the uri of the gpg key (local path or http URL) + # + # @raise [Chef::Exceptions::FileNotFound] Key isn't remote or found in the current run + # + # @return [Symbol] :remote_file or :cookbook_file + def key_type(uri) + if uri.start_with?("http") + Chef::Log.debug("Will use :remote_file resource to cache the gpg key locally") + :remote_file + elsif has_cookbook_file?(uri) + Chef::Log.debug("Will use :cookbook_file resource to cache the gpg key locally") + :cookbook_file + else + raise Chef::Exceptions::FileNotFound, "Cannot determine location of gpgkey. Must start with 'http' or be a file managed by Chef." + end end + # is the provided key already installed + # @param [String] key_path the path to the key on the local filesystem + # + # @return [boolean] is the key already known by rpm + def key_installed?(key_path) + so = shell_out("rpm -qa gpg-pubkey*") + # expected output & match: http://rubular.com/r/RdF7EcXEtb + status = /gpg-pubkey-#{key_fingerprint(key_path)}/.match(so.stdout) + Chef::Log.debug("GPG key at #{key_path} is known by rpm? #{status ? "true" : "false"}") + status + end + + # extract the gpg key fingerprint from a local file + # @param [String] key_path the path to the key on the local filesystem + # + # @return [String] the fingerprint of the key + def key_fingerprint(key_path) + so = shell_out!("gpg --with-fingerprint #{key_path}") + # expected output and match: http://rubular.com/r/BpfMjxySQM + fingerprint = /pub\s*\S*\/(\S*)/.match(so.stdout)[1].downcase + Chef::Log.debug("GPG fingerprint of key at #{key_path} is #{fingerprint}") + fingerprint + end + + # install the provided gpg key + # @param [String] uri the uri of the local or remote gpg key + def install_gpg_key(uri) + unless uri + Chef::Log.debug("'gpgkey' property not provided or set to nil. Skipping key import.") + return + end + + cached_keyfile = ::File.join(Chef::Config[:file_cache_path], uri.split("/")[-1]) + + declare_resource(key_type(new_resource.gpgkey), cached_keyfile) do + source uri + mode "0644" + sensitive new_resource.sensitive + action :create + end + + declare_resource(:execute, "import gpg key from #{new_resource.gpgkey}") do + command "/bin/rpm --import #{cached_keyfile}" + not_if { key_installed?(cached_keyfile) } + action :run + end + end end end end diff --git a/lib/chef/resource/zypper_repository.rb b/lib/chef/resource/zypper_repository.rb index 69a96b42cf..88b6fd9336 100644 --- a/lib/chef/resource/zypper_repository.rb +++ b/lib/chef/resource/zypper_repository.rb @@ -39,6 +39,7 @@ class Chef property :mode, default: "0644" property :refresh_cache, [true, false], default: true property :source, String, regex: /.*/ + property :cookbook, String property :gpgautoimportkeys, [true, false], default: true default_action :create diff --git a/spec/unit/provider/zypper_repository_spec.rb b/spec/unit/provider/zypper_repository_spec.rb new file mode 100644 index 0000000000..a366a33e86 --- /dev/null +++ b/spec/unit/provider/zypper_repository_spec.rb @@ -0,0 +1,124 @@ +# +# Author:: Tim Smith (<tsmith@chef.io>) +# Copyright:: 2017, 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" + +# Output of the command: +# => rpm -qa gpg-pubkey* +RPM_KEYS = <<-EOF +gpg-pubkey-307e3d54-4be01a65 +gpg-pubkey-3dbdc284-53674dd4 +EOF + +# Output of the command: +# => gpg --with-fingerprint [FILE] +GPG_FINGER = <<-EOF +pub 2048R/3DBDC284 2011-08-19 [expires: 2024-06-14] + Key fingerprint = 573B FD6B 3D8F BC64 1079 A6AB ABF5 BD82 7BD9 BF62 +uid nginx signing key <signing-key@nginx.com> +EOF + +describe Chef::Provider::ZypperRepository do + let(:new_resource) { Chef::Resource::ZypperRepository.new("Nginx Repository") } + let(:provider) do + node = Chef::Node.new + events = Chef::EventDispatch::Dispatcher.new + run_context = Chef::RunContext.new(node, {}, events) + Chef::Provider::ZypperRepository.new(new_resource, run_context) + end + + let(:rpm_key_finger) do + double("shell_out_with_systems_locale", stdout: RPM_KEYS, exitstatus: 0, error?: false) + end + + let(:gpg_finger) do + double("shell_out_with_systems_locale", stdout: GPG_FINGER, exitstatus: 0, error?: false) + end + + it "responds to load_current_resource" do + expect(provider).to respond_to(:load_current_resource) + end + + describe "#action_create" do + it "skips key import if gpgautoimportkeys is false" do + new_resource.gpgautoimportkeys(false) + expect(provider).to receive(:declare_resource) + expect(Chef::Log).to receive(:debug) + provider.run_action(:create) + end + end + + describe "#escaped_repo_name" do + it "returns an escaped repo name" do + expect(provider.escaped_repo_name).to eq('Nginx\\ Repository') + end + end + + describe "#cookbook_name" do + it "returns 'test' when the cookbook property is set" do + new_resource.cookbook("test") + expect(provider.cookbook_name).to eq("test") + end + end + + describe "#key_type" do + it "returns :remote_file with an http URL" do + expect(provider.key_type("https://www.chef.io/key")).to eq(:remote_file) + end + + it "returns :cookbook_file with a chef managed file" do + expect(provider).to receive(:has_cookbook_file?).and_return(true) + expect(provider.key_type("/foo/nginx.key")).to eq(:cookbook_file) + end + + it "throws exception if an unknown file specified" do + expect(provider).to receive(:has_cookbook_file?).and_return(false) + expect { provider.key_type("/foo/nginx.key") }.to raise_error(Chef::Exceptions::FileNotFound) + end + end + + describe "#key_installed?" do + before do + expect(provider).to receive(:shell_out).with("rpm -qa gpg-pubkey*").and_return(rpm_key_finger) + end + + it "returns true if the key is installed" do + expect(provider).to receive(:key_fingerprint).and_return("3dbdc284") + expect(provider.key_installed?("/foo/nginx.key")).to be_truthy + end + + it "returns false if the key is not installed" do + expect(provider).to receive(:key_fingerprint).and_return("BOGUS") + expect(provider.key_installed?("/foo/nginx.key")).to be_falsey + end + end + + describe "#key_fingerprint" do + it "returns the key's fingerprint" do + expect(provider).to receive(:shell_out!).with("gpg --with-fingerprint /foo/nginx.key").and_return(gpg_finger) + expect(provider.key_fingerprint("/foo/nginx.key")).to eq("3dbdc284") + end + end + + describe "#install_gpg_key" do + it "skips installing the key if a nil value for key is passed" do + expect(Chef::Log).to receive(:debug) + provider.install_gpg_key(nil) + end + end +end diff --git a/spec/unit/resource/zypper_repository_spec.rb b/spec/unit/resource/zypper_repository_spec.rb index 16951d071c..de08b183a5 100644 --- a/spec/unit/resource/zypper_repository_spec.rb +++ b/spec/unit/resource/zypper_repository_spec.rb @@ -46,20 +46,22 @@ describe Chef::Resource::ZypperRepository do expect { resource.action :delete }.to raise_error(ArgumentError) end - it "should resolve to a Noop class when zypper is not found" do - expect(Chef::Provider::ZypperRepository).to receive(:which).with("zypper").and_return(false) + it "resolves to a Noop class when on non-linux OS" do + node.automatic[:os] = "windows" + node.automatic[:platform_family] = "windows" expect(resource.provider_for_action(:add)).to be_a(Chef::Provider::Noop) end - it "should resolve to a ZypperRepository class when zypper is found" do - expect(Chef::Provider::ZypperRepository).to receive(:which).with("zypper").and_return(true) - expect(resource.provider_for_action(:add)).to be_a(Chef::Provider::ZypperRepository) + it "resolves to a Noop class when on non-suse linux" do + node.automatic[:os] = "linux" + node.automatic[:platform_family] = "debian" + expect(resource.provider_for_action(:add)).to be_a(Chef::Provider::Noop) end - end - context "on windows", :windows_only do - it "should resolve to a NoOp provider" do - expect(resource.provider_for_action(:add)).to be_a(Chef::Provider::Noop) + it "resolves to a ZypperRepository class when on a suse platform_family" do + node.automatic[:os] = "linux" + node.automatic[:platform_family] = "suse" + expect(resource.provider_for_action(:add)).to be_a(Chef::Provider::ZypperRepository) end end end |