diff options
author | Thom May <thom@chef.io> | 2016-04-01 09:50:32 +0100 |
---|---|---|
committer | Thom May <thom@chef.io> | 2016-04-08 13:00:49 -0700 |
commit | 79548accdf02e199d48a09b6eb0cc8d8613b0916 (patch) | |
tree | 4cc9150c10a0712e3025b137e4a859731b410ad6 | |
parent | 1eb4511c15f5042ecb0432170c6532fb065a842c (diff) | |
download | chef-79548accdf02e199d48a09b6eb0cc8d8613b0916.tar.gz |
Add an apt_repository resourcetm/apt_repository
More or less stolen from the apt cookbook, but tweaked and fixes a heap
of bugs.
-rw-r--r-- | lib/chef/provider/apt_repository.rb | 255 | ||||
-rw-r--r-- | lib/chef/providers.rb | 1 | ||||
-rw-r--r-- | lib/chef/resource/apt_repository.rb | 47 | ||||
-rw-r--r-- | lib/chef/resources.rb | 1 | ||||
-rw-r--r-- | spec/unit/provider/apt_repository_spec.rb | 178 | ||||
-rw-r--r-- | spec/unit/resource/apt_repository_spec.rb | 34 |
6 files changed, 516 insertions, 0 deletions
diff --git a/lib/chef/provider/apt_repository.rb b/lib/chef/provider/apt_repository.rb new file mode 100644 index 0000000000..8880a059ac --- /dev/null +++ b/lib/chef/provider/apt_repository.rb @@ -0,0 +1,255 @@ +# +# Author:: Thom May (<thom@chef.io>) +# Copyright:: Copyright (c) 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 "chef/resource" +require "chef/dsl/declare_resource" +require "chef/mixin/shell_out" +require "chef/http/simple" +require "chef/provider/noop" + +class Chef + class Provider + class AptRepository < Chef::Provider + use_inline_resources + + include Chef::Mixin::ShellOut + + provides :apt_repository do + uses_apt? + end + + def whyrun_supported? + true + end + + def load_current_resource + end + + action :add do + unless new_resource.key.nil? + if is_key_id?(new_resource.key) && !has_cookbook_file?(new_resource.key) + install_key_from_keyserver + else + install_key_from_uri + end + end + + declare_resource(:execute, "apt-cache gencaches") do + ignore_failure true + action :nothing + end + + declare_resource(:apt_update, new_resource.name) do + ignore_failure true + action :nothing + end + + components = if is_ppa_url?(new_resource.uri) && new_resource.components.empty? + "main" + else + new_resource.components + end + + repo = build_repo( + new_resource.uri, + new_resource.distribution, + components, + new_resource.trusted, + new_resource.arch, + new_resource.deb_src + ) + + declare_resource(:file, "/etc/apt/sources.list.d/#{new_resource.name}.list") do + owner "root" + group "root" + mode "0644" + content repo + sensitive new_resource.sensitive + action :create + notifies :run, "execute[apt-cache gencaches]", :immediately + notifies :update, "apt_update[#{new_resource.name}]", :immediately if new_resource.cache_rebuild + end + end + + action :remove do + if ::File.exist?("/etc/apt/sources.list.d/#{new_resource.name}.list") + converge_by "Removing #{new_resource.name} repository from /etc/apt/sources.list.d/" do + declare_resource(:file, "/etc/apt/sources.list.d/#{new_resource.name}.list") do + sensitive new_resource.sensitive + action :delete + notifies :update, "apt_update[#{new_resource.name}]", :immediately if new_resource.cache_rebuild + end + + declare_resource(:apt_update, new_resource.name) do + ignore_failure true + action :nothing + end + + end + end + end + + def self.uses_apt? + ENV["PATH"] ||= "" + paths = %w{ /bin /usr/bin /sbin /usr/sbin } + ENV["PATH"].split(::File::PATH_SEPARATOR) + paths.any? { |path| ::File.executable?(::File.join(path, "apt-get")) } + end + + def is_key_id?(id) + id = id[2..-1] if id.start_with?("0x") + id =~ /^\h+$/ && [8, 16, 40].include?(id.length) + end + + def extract_fingerprints_from_cmd(cmd) + so = shell_out(cmd) + so.run_command + so.stdout.split(/\n/).map do |t| + if z = t.match(/^ +Key fingerprint = ([0-9A-F ]+)/) + z[1].split.join + end + end.compact + end + + def key_is_valid?(cmd, key) + valid = true + + so = shell_out(cmd) + so.run_command + so.stdout.split(/\n/).map do |t| + if t =~ %r{^\/#{key}.*\[expired: .*\]$} + Chef::Log.debug "Found expired key: #{t}" + valid = false + break + end + end + + Chef::Log.debug "key #{key} #{valid ? "is valid" : "is not valid"}" + valid + end + + def cookbook_name + new_resource.cookbook || new_resource.cookbook_name + end + + def has_cookbook_file?(fn) + run_context.has_cookbook_file_in_cookbook?(cookbook_name, fn) + end + + def no_new_keys?(file) + installed_keys = extract_fingerprints_from_cmd("apt-key finger") + proposed_keys = extract_fingerprints_from_cmd("gpg --with-fingerprint #{file}") + (installed_keys & proposed_keys).sort == proposed_keys.sort + end + + def install_key_from_uri + key_name = new_resource.key.split(%r{\/}).last + cached_keyfile = ::File.join(Chef::Config[:file_cache_path], key_name) + type = if new_resource.key.start_with?("http") + :remote_file + elsif has_cookbook_file?(new_resource.key) + :cookbook_file + else + raise Chef::Exceptions::FileNotFound, "Cannot locate key file" + end + + declare_resource(type, cached_keyfile) do + source new_resource.key + mode "0644" + sensitive new_resource.sensitive + action :create + end + + raise "The key #{cached_keyfile} is invalid and cannot be used to verify an apt repository." unless key_is_valid?("gpg #{cached_keyfile}", "") + + declare_resource(:execute, "apt-key add #{cached_keyfile}") do + sensitive new_resource.sensitive + action :run + not_if do + no_new_keys?(cached_keyfile) + end + notifies :run, "execute[apt-cache gencaches]", :immediately + end + end + + def install_key_from_keyserver(key = new_resource.key, keyserver = new_resource.keyserver) + cmd = "apt-key adv --recv" + cmd << " --keyserver-options http-proxy=#{new_resource.key_proxy}" if new_resource.key_proxy + cmd << " --keyserver " + cmd << if keyserver.start_with?("hkp://") + keyserver + else + "hkp://#{keyserver}:80" + end + + cmd << " #{key}" + + declare_resource(:execute, "install-key #{key}") do + command cmd + sensitive new_resource.sensitive + not_if do + present = extract_fingerprints_from_cmd("apt-key finger").any? do |fp| + fp.end_with? key.upcase + end + present && key_is_valid?("apt-key list", key.upcase) + end + notifies :run, "execute[apt-cache gencaches]", :immediately + end + + raise "The key #{key} is invalid and cannot be used to verify an apt repository." unless key_is_valid?("apt-key list", key.upcase) + end + + def install_ppa_key(owner, repo) + url = "https://launchpad.net/api/1.0/~#{owner}/+archive/#{repo}" + key_id = Chef::HTTP::Simple.new(url).get("signing_key_fingerprint").delete('"') + install_key_from_keyserver(key_id, "keyserver.ubuntu.com") + rescue Net::HTTPServerException => e + raise "Could not access Launchpad ppa API: #{e.message}" + end + + def is_ppa_url?(url) + url.start_with?("ppa:") + end + + def make_ppa_url(ppa) + return unless is_ppa_url?(ppa) + owner, repo = ppa[4..-1].split("/") + repo ||= "ppa" + + install_ppa_key(owner, repo) + "http://ppa.launchpad.net/#{owner}/#{repo}/ubuntu" + end + + def build_repo(uri, distribution, components, trusted, arch, add_src = false) + uri = make_ppa_url(uri) if is_ppa_url?(uri) + + uri = '"' + uri + '"' unless uri.start_with?("'", '"') + components = Array(components).join(" ") + options = "" + options << "arch=#{arch} " if arch + options << "trusted=yes" if trusted + options = "[#{options}]" unless options.empty? + info = "#{options} #{uri} #{distribution} #{components}\n".lstrip + repo = "deb #{info}" + repo << "deb-src #{info}" if add_src + repo + end + end + end +end + +Chef::Provider::Noop.provides :apt_resource diff --git a/lib/chef/providers.rb b/lib/chef/providers.rb index 80fadfc8c2..c723da030b 100644 --- a/lib/chef/providers.rb +++ b/lib/chef/providers.rb @@ -17,6 +17,7 @@ # require "chef/provider/apt_update" +require "chef/provider/apt_repository" require "chef/provider/batch" require "chef/provider/breakpoint" require "chef/provider/cookbook_file" diff --git a/lib/chef/resource/apt_repository.rb b/lib/chef/resource/apt_repository.rb new file mode 100644 index 0000000000..e1ea665858 --- /dev/null +++ b/lib/chef/resource/apt_repository.rb @@ -0,0 +1,47 @@ +# +# Author:: Thom May (<thom@chef.io>) +# Copyright:: Copyright (c) 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 "chef/resource" + +class Chef + class Resource + class AptRepository < Chef::Resource + resource_name :apt_repository + provides :apt_repository + + property :repo_name, String, name_property: true + property :uri, String + property :distribution, String, default: lazy { node["lsb"]["codename"] } + property :components, Array, default: [] + property :arch, [String, nil], default: nil + property :trusted, [TrueClass, FalseClass], default: false + # whether or not to add the repository as a source repo, too + property :deb_src, [TrueClass, FalseClass], default: false + property :keyserver, [String, nil], default: "keyserver.ubuntu.com" + property :key, [String, nil], default: nil + property :key_proxy, [String, nil], default: nil + + property :cookbook, [String, nil], default: nil, desired_state: false + property :cache_rebuild, [TrueClass, FalseClass], default: true, desired_state: false + property :sensitive, [TrueClass, FalseClass], default: false, desired_state: false + + default_action :add + allowed_actions :add, :remove + end + end +end diff --git a/lib/chef/resources.rb b/lib/chef/resources.rb index 797f7b8b7e..d8cec8c51d 100644 --- a/lib/chef/resources.rb +++ b/lib/chef/resources.rb @@ -17,6 +17,7 @@ # require "chef/resource/apt_package" +require "chef/resource/apt_repository" require "chef/resource/apt_update" require "chef/resource/bash" require "chef/resource/batch" diff --git a/spec/unit/provider/apt_repository_spec.rb b/spec/unit/provider/apt_repository_spec.rb new file mode 100644 index 0000000000..543b65cc90 --- /dev/null +++ b/spec/unit/provider/apt_repository_spec.rb @@ -0,0 +1,178 @@ +# +# Author:: Thom May (<thom@chef.io>) +# Copyright:: Copyright (c) 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" + +APT_KEY_FINGER = <<-EOF +/etc/apt/trusted.gpg +-------------------- +pub 1024D/437D05B5 2004-09-12 + Key fingerprint = 6302 39CC 130E 1A7F D81A 27B1 4097 6EAF 437D 05B5 +uid Ubuntu Archive Automatic Signing Key <ftpmaster@ubuntu.com> +sub 2048g/79164387 2004-09-12 + +pub 1024D/FBB75451 2004-12-30 + Key fingerprint = C598 6B4F 1257 FFA8 6632 CBA7 4618 1433 FBB7 5451 +uid Ubuntu CD Image Automatic Signing Key <cdimage@ubuntu.com> + +pub 4096R/C0B21F32 2012-05-11 + Key fingerprint = 790B C727 7767 219C 42C8 6F93 3B4F E6AC C0B2 1F32 +uid Ubuntu Archive Automatic Signing Key (2012) <ftpmaster@ubuntu.com> + +pub 4096R/EFE21092 2012-05-11 + Key fingerprint = 8439 38DF 228D 22F7 B374 2BC0 D94A A3F0 EFE2 1092 +uid Ubuntu CD Image Automatic Signing Key (2012) <cdimage@ubuntu.com> + +EOF + +GPG_FINGER = <<-EOF +pub 1024D/02A818DD 2009-04-22 Cloudera Apt Repository + Key fingerprint = F36A 89E3 3CC1 BD0F 7107 9007 3275 74EE 02A8 18DD +sub 2048g/D1CA74A1 2009-04-22 +EOF + +describe Chef::Provider::AptRepository do + let(:new_resource) { Chef::Resource::AptRepository.new("multiverse") } + + let(:shellout_env) { { env: { "LANG" => "en_US", "LANGUAGE" => "en_US" } } } + let(:provider) do + node = Chef::Node.new + events = Chef::EventDispatch::Dispatcher.new + run_context = Chef::RunContext.new(node, {}, events) + Chef::Provider::AptRepository.new(new_resource, run_context) + end + + let(:apt_key_finger) do + r = double("Mixlib::ShellOut", stdout: APT_KEY_FINGER, exitstatus: 0, live_stream: true) + allow(r).to receive(:run_command) + r + end + + let(:gpg_finger) do + r = double("Mixlib::ShellOut", stdout: GPG_FINGER, exitstatus: 0, live_stream: true) + allow(r).to receive(:run_command) + r + end + + let(:apt_fingerprints) do + %w{630239CC130E1A7FD81A27B140976EAF437D05B5 +C5986B4F1257FFA86632CBA746181433FBB75451 +790BC7277767219C42C86F933B4FE6ACC0B21F32 +843938DF228D22F7B3742BC0D94AA3F0EFE21092} + end + + it "responds to load_current_resource" do + expect(provider).to respond_to(:load_current_resource) + end + + describe "#is_key_id?" do + it "should detect a key" do + expect(provider.is_key_id?("A4FF2279")).to be_truthy + end + it "should detect a key with a hex signifier" do + expect(provider.is_key_id?("0xA4FF2279")).to be_truthy + end + it "should reject a key with the wrong length" do + expect(provider.is_key_id?("4FF2279")).to be_falsey + end + it "should reject a key with non-hex characters" do + expect(provider.is_key_id?("A4KF2279")).to be_falsey + end + end + + describe "#extract_fingerprints_from_cmd" do + before do + expect(Mixlib::ShellOut).to receive(:new).and_return(apt_key_finger) + end + + it "should run the desired command" do + expect(apt_key_finger).to receive(:run_command) + provider.extract_fingerprints_from_cmd("apt-key finger") + end + + it "should return a list of key fingerprints" do + expect(provider.extract_fingerprints_from_cmd("apt-key finger")).to eql(apt_fingerprints) + end + end + + describe "#no_new_keys?" do + before do + allow(provider).to receive(:extract_fingerprints_from_cmd).with("apt-key finger").and_return(apt_fingerprints) + end + + let(:file) { "/tmp/remote-gpg-keyfile" } + + it "should match a set of keys" do + allow(provider).to receive(:extract_fingerprints_from_cmd).with("gpg --with-fingerprint #{file}").and_return(Array(apt_fingerprints.first)) + expect(provider.no_new_keys?(file)).to be_truthy + end + + it "should notice missing keys" do + allow(provider).to receive(:extract_fingerprints_from_cmd).with("gpg --with-fingerprint #{file}").and_return(%w{ F36A89E33CC1BD0F71079007327574EE02A818DD }) + expect(provider.no_new_keys?(file)).to be_falsey + end + end + + describe "#install_ppa_key" do + let(:url) { "https://launchpad.net/api/1.0/~chef/+archive/main" } + let(:key) { "C5986B4F1257FFA86632CBA746181433FBB75451" } + + it "should get a key" do + simples = double("HTTP") + allow(simples).to receive(:get).and_return("\"#{key}\"") + expect(Chef::HTTP::Simple).to receive(:new).with(url).and_return(simples) + expect(provider).to receive(:install_key_from_keyserver).with(key, "keyserver.ubuntu.com") + provider.install_ppa_key("chef", "main") + end + end + + describe "#make_ppa_url" do + it "should ignore non-ppa repositories" do + expect(provider.make_ppa_url("some_string")).to be_nil + end + + it "should create a URL" do + expect(provider).to receive(:install_ppa_key).with("chef", "main").and_return(true) + expect(provider.make_ppa_url("ppa:chef/main")).to eql("http://ppa.launchpad.net/chef/main/ubuntu") + end + end + + describe "#build_repo" do + it "should create a repository string" do + target = %Q{deb "http://test/uri" unstable main\n} + expect(provider.build_repo("http://test/uri", "unstable", "main", false, nil)).to eql(target) + end + + it "should create a repository string with source" do + target = %Q{deb "http://test/uri" unstable main\ndeb-src "http://test/uri" unstable main\n} + expect(provider.build_repo("http://test/uri", "unstable", "main", false, nil, true)).to eql(target) + end + + it "should create a repository string with options" do + target = %Q{deb [trusted=yes] "http://test/uri" unstable main\n} + expect(provider.build_repo("http://test/uri", "unstable", "main", true, nil)).to eql(target) + end + + it "should handle a ppa repo" do + target = %Q{deb "http://ppa.launchpad.net/chef/main/ubuntu" unstable main\n} + expect(provider).to receive(:make_ppa_url).with("ppa:chef/main").and_return("http://ppa.launchpad.net/chef/main/ubuntu") + expect(provider.build_repo("ppa:chef/main", "unstable", "main", false, nil)).to eql(target) + end + end + +end diff --git a/spec/unit/resource/apt_repository_spec.rb b/spec/unit/resource/apt_repository_spec.rb new file mode 100644 index 0000000000..88d9ca2508 --- /dev/null +++ b/spec/unit/resource/apt_repository_spec.rb @@ -0,0 +1,34 @@ +# +# Author:: Thom May (<thom@chef.io>) +# Copyright:: Copyright (c) 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::Resource::AptRepository do + + let(:resource) { Chef::Resource::AptRepository.new("multiverse") } + + it "should create a new Chef::Resource::AptUpdate" do + expect(resource).to be_a_kind_of(Chef::Resource) + expect(resource).to be_a_kind_of(Chef::Resource::AptRepository) + end + + it "the default keyserver should be keyserver.ubuntu.com" do + expect(resource.keyserver).to eql("keyserver.ubuntu.com") + end + +end |