summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorThom May <thom@chef.io>2016-04-01 09:50:32 +0100
committerThom May <thom@chef.io>2016-04-08 13:00:49 -0700
commit79548accdf02e199d48a09b6eb0cc8d8613b0916 (patch)
tree4cc9150c10a0712e3025b137e4a859731b410ad6
parent1eb4511c15f5042ecb0432170c6532fb065a842c (diff)
downloadchef-tm/apt_repository.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.rb255
-rw-r--r--lib/chef/providers.rb1
-rw-r--r--lib/chef/resource/apt_repository.rb47
-rw-r--r--lib/chef/resources.rb1
-rw-r--r--spec/unit/provider/apt_repository_spec.rb178
-rw-r--r--spec/unit/resource/apt_repository_spec.rb34
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