summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorS.Cavallo <smcavallo@hotmail.com>2018-10-09 13:15:04 -0400
committerLamont Granquist <lamont@scriptkiddie.org>2018-11-28 22:30:43 -0800
commit3aba92ade1504aa49028f769855b4d650de87b5b (patch)
tree5943ae11a5302b4f2aee86feec372b8f15154667
parentb8433f37539f83c37ddfe4301df87f6d1cebb12f (diff)
downloadchef-3aba92ade1504aa49028f769855b4d650de87b5b.tar.gz
initial suppport for snap packages
Signed-off-by: S.Cavallo <smcavallo@hotmail.com>
-rw-r--r--lib/chef/provider/package/snap.rb318
-rw-r--r--lib/chef/resource/snap_package.rb36
-rw-r--r--lib/chef/resources.rb1
-rw-r--r--spec/unit/provider/package/snap_spec.rb190
-rw-r--r--spec/unit/provider_resolver_spec.rb1
-rw-r--r--spec/unit/resource/snap_package_spec.rb61
6 files changed, 607 insertions, 0 deletions
diff --git a/lib/chef/provider/package/snap.rb b/lib/chef/provider/package/snap.rb
new file mode 100644
index 0000000000..556cd788ce
--- /dev/null
+++ b/lib/chef/provider/package/snap.rb
@@ -0,0 +1,318 @@
+#
+# Author:: S.Cavallo (<smcavallo@hotmail.com>)
+# Copyright:: Copyright 2016-2018, 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/provider/package"
+require "chef/resource/snap_package"
+require "chef/mixin/shell_out"
+require 'socket'
+require 'json'
+
+class Chef
+ class Provider
+ class Package
+ class Snap < Chef::Provider::Package
+ extend Chef::Mixin::Which
+ extend Chef::Mixin::ShellOut
+
+ allow_nils
+ use_multipackage_api
+
+ # Todo: support non-debian platforms
+ provides :package, platform_family: "debian"
+
+ provides :snap_package
+
+ def load_current_resource
+ @current_resource = Chef::Resource::SnapPackage.new(new_resource.name)
+ current_resource.package_name(new_resource.package_name)
+ current_resource.version(get_current_versions)
+
+ current_resource
+ end
+
+ def define_resource_requirements
+ requirements.assert(:install, :upgrade, :remove, :purge) do |a|
+ a.assertion {!new_resource.source || ::File.exist?(new_resource.source)}
+ a.failure_message Chef::Exceptions::Package, "Package #{new_resource.package_name} not found: #{new_resource.source}"
+ a.whyrun "assuming #{new_resource.source} would have previously been created"
+ end
+
+ super
+ end
+
+ def candidate_version
+ package_name_array.each_with_index.map do |pkg, i|
+ available_version(i)
+ end
+ end
+
+ def get_current_versions
+ package_name_array.each_with_index.map do |pkg, i|
+ installed_version(i)
+ end
+ end
+
+ def install_package(names, versions)
+ if new_resource.source
+ install_snap_from_source(names, new_resource.source)
+ else
+ resolved_names = names.each_with_index.map {|name, i| available_version(i).to_s unless name.nil?}
+ install_snaps(resolved_names)
+ end
+ end
+
+ def upgrade_package(names, versions)
+ if new_resource.source
+ install_snap_from_source(names, new_resource.source)
+ else
+ resolved_names = names.each_with_index.map {|name, i| available_version(i).to_s unless name.nil?}
+ update_snaps(resolved_names)
+ end
+ end
+
+ def remove_package(names, versions)
+ resolved_names = names.each_with_index.map {|name, i| installed_version(i).to_s unless name.nil?}
+ uninstall_snaps(resolved_names)
+ end
+
+ alias purge_package remove_package
+
+ private
+
+ # @return Array<Version>
+ def available_version(index)
+ @available_version ||= []
+
+ @available_version[index] ||= if new_resource.source
+ get_snap_version_from_source(new_resource.source)
+ else
+ get_latest_package_version(package_name_array[index], new_resource.channel)
+ end
+
+ @available_version[index]
+ end
+
+ # @return [Array<Version>]
+ def installed_version(index)
+ @installed_version ||= []
+ @installed_version[index] ||= get_installed_package_version_by_name(package_name_array[index])
+ @installed_version[index]
+ end
+
+ def safe_version_array
+ if new_resource.version.is_a?(Array)
+ new_resource.version
+ elsif new_resource.version.nil?
+ package_name_array.map {nil}
+ else
+ [new_resource.version]
+ end
+ end
+
+ # ToDo: Support authentication
+ # ToDo: Support private snap repos
+ # https://github.com/snapcore/snapd/wiki/REST-API
+
+ # ToDo: Would prefer to use net/http over socket
+ def call_snap_api(method, uri, post_data = nil?)
+ request = "#{method} #{uri} HTTP/1.0\r\n" +
+ "Accept: application/json\r\n" +
+ "Content-Type: application/json\r\n"
+ if method == "POST"
+ request.concat("Content-Length: #{post_data.bytesize.to_s}\r\n\r\n#{post_data}")
+ end
+ request.concat("\r\n")
+ # While it is expected to allow clients to connect using HTTPS over a TCP socket,
+ # at this point only a UNIX socket is supported. The socket is /run/snapd.socket
+ # Note - UNIXSocket is not defined on windows systems
+ if defined?(::UNIXSocket)
+ UNIXSocket.open('/run/snapd.socket') do |socket|
+ # Send request, read the response, split the response and parse the body
+ socket.print(request)
+ response = socket.read
+ headers, body = response.split("\r\n\r\n", 2)
+ JSON.parse(body)
+ end
+ end
+ end
+
+ def get_change_id(id)
+ call_snap_api('GET', "/v2/changes/#{id}")
+ end
+
+ def get_id_from_async_response(response)
+ if response['type'] == 'error'
+ raise "status: #{response['status']}, kind: #{response['result']['kind']}, message: #{response['result']['message']}"
+ end
+ response['change']
+ end
+
+ def wait_for_completion(id)
+ n = 0
+ waiting = true
+ while waiting do
+ result = get_change_id(id)
+ puts "STATUS: #{result['result']['status']}"
+ case result['result']['status']
+ when "Do", "Doing", "Undoing", "Undo"
+ # Continue
+ when "Abort"
+ raise result
+ when "Hold", "Error"
+ raise result
+ when "Done"
+ waiting = false
+ else
+ # How to handle unknown status
+ end
+ n += 1
+ raise "Snap operating timed out after #{n} seconds." if n == 300
+ sleep(1)
+ end
+ end
+
+ def snapctl(*args)
+ shell_out!("snap", *args)
+ end
+
+ def get_snap_version_from_source(path)
+ body = {
+ "context-id" => "get_snap_version_from_source_#{path}",
+ "args" => ["info", path]
+ }.to_json
+
+ #json = call_snap_api('POST', '/v2/snapctl', body)
+ response = snapctl(["info", path])
+ Chef::Log.trace(response)
+ response.error!
+ get_version_from_stdout(response.stdout)
+ end
+
+ def get_version_from_stdout(stdout)
+ stdout.match(/version: (\S+)/)[1]
+ end
+
+ def install_snap_from_source(name, path)
+ #json = call_snap_api('POST', '/v2/snapctl', body)
+ response = snapctl(["install", path])
+ Chef::Log.trace(response)
+ response.error!
+ end
+
+ def install_snaps(snap_names)
+ response = post_snaps(snap_names, 'install', new_resource.channel, new_resource.options)
+ id = get_id_from_async_response(response)
+ wait_for_completion(id)
+ end
+
+ def update_snaps(snap_names)
+ response = post_snaps(snap_names, 'refresh', new_resource.channel, new_resource.options)
+ id = get_id_from_async_response(response)
+ wait_for_completion(id)
+ end
+
+ def uninstall_snaps(snap_names)
+ response = post_snaps(snap_names, 'remove', new_resource.channel, new_resource.options)
+ id = get_id_from_async_response(response)
+ wait_for_completion(id)
+ end
+
+ # Constructs json to post for snap changes
+ #
+ # @param snap_names [Array] An array of snap package names to install
+ # @param action [String] The action. install, refresh, remove, revert, enable, disable or switch
+ # @param channel [String] The release channel. Ex. stable
+ # @param options [Hash] Misc configuration Options
+ # @param revision [String] A revision/version
+ def generate_snap_json(snap_names, action, channel, options, revision = nil)
+ request = {
+ "action" => action,
+ "snaps" => snap_names
+ }
+ if ['install', 'refresh', 'switch'].include?(action)
+ request['channel'] = channel
+ end
+
+ # No defensive handling of params
+ # Snap will throw the proper exception if called improperly
+ # And we can provide that exception to the end user
+ request['classic'] = true if options['classic']
+ request['devmode'] = true if options['devmode']
+ request['jailmode'] = true if options['jailmode']
+ request['revision'] = revision unless revision.nil?
+ request['ignore_validation'] = true if options['ignore-validation']
+ request
+ end
+
+ # Post to the snap api to update snaps
+ #
+ # @param snap_names [Array] An array of snap package names to install
+ # @param action [String] The action. install, refresh, remove, revert, enable, disable or switch
+ # @param channel [String] The release channel. Ex. stable
+ # @param options [Hash] Misc configuration Options
+ # @param revision [String] A revision/version
+ def post_snaps(snap_names, action, channel, options, revision = nil)
+ json = generate_snap_json(snap_names, action, channel, options, revision = nil)
+ call_snap_api('POST', '/v2/snaps', json)
+ end
+
+ def get_latest_package_version(name, channel)
+ json = call_snap_api('GET', "/v2/find?name=#{name}")
+ if json["status-code"] != 200
+ raise Chef::Exceptions::Package, json["result"], caller
+ end
+
+ # Return the version matching the channel
+ json['result'][0]['channels']["latest/#{channel}"]['version']
+ end
+
+ def get_installed_packages
+ json = call_snap_api('GET', '/v2/snaps')
+ json['result']
+ end
+
+ def get_installed_package_version_by_name(name)
+ result = get_installed_package_by_name(name)
+ # Return nil if not installed
+ if result["status-code"] == 404
+ nil
+ else
+ result["version"]
+ end
+ end
+
+ def get_installed_package_by_name(name)
+ json = call_snap_api('GET', "/v2/snaps/#{name}")
+ json['result']
+ end
+
+ def get_installed_package_conf(name)
+ json = call_snap_api('GET', "/v2/snaps/#{name}/conf")
+ json['result']
+ end
+
+ def set_installed_package_conf(name, value)
+ response = call_snap_api('PUT', "/v2/snaps/#{name}/conf", value)
+ id = get_id_from_async_response(response)
+ wait_for_completion(id)
+ end
+
+ end
+ end
+ end
+end
diff --git a/lib/chef/resource/snap_package.rb b/lib/chef/resource/snap_package.rb
new file mode 100644
index 0000000000..f114234e65
--- /dev/null
+++ b/lib/chef/resource/snap_package.rb
@@ -0,0 +1,36 @@
+#
+# Author:: S.Cavallo (<smcavallo@hotmail.com>)
+# Copyright:: Copyright 2008-2018, 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/package"
+
+class Chef
+ class Resource
+ class SnapPackage < Chef::Resource::Package
+ resource_name :snap_package
+ provides :package, platform_family: "debian"
+
+ description "Use the snap_package resource to manage snap packages on Debian and Ubuntu platforms."
+
+ property :channel, String,
+ description: "The default channel. For example: stable.",
+ default: "stable",
+ equal_to: ["edge", "beta", "candidate", "stable"],
+ desired_state: false
+ end
+ end
+end
diff --git a/lib/chef/resources.rb b/lib/chef/resources.rb
index 32739087d5..9a418229c8 100644
--- a/lib/chef/resources.rb
+++ b/lib/chef/resources.rb
@@ -93,6 +93,7 @@ require "chef/resource/rhsm_register"
require "chef/resource/rhsm_repo"
require "chef/resource/rhsm_subscription"
require "chef/resource/rpm_package"
+require "chef/resource/snap_package"
require "chef/resource/solaris_package"
require "chef/resource/route"
require "chef/resource/ruby"
diff --git a/spec/unit/provider/package/snap_spec.rb b/spec/unit/provider/package/snap_spec.rb
new file mode 100644
index 0000000000..9a1d1401f5
--- /dev/null
+++ b/spec/unit/provider/package/snap_spec.rb
@@ -0,0 +1,190 @@
+# Author:: S.Cavallo (smcavallo@hotmail.com)
+# Copyright 2014-2018, Chef Software, Inc. <legal@chef.io>
+# 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/provider/package'
+require 'chef/provider/package/snap'
+require 'json'
+
+describe Chef::Provider::Package::Snap do
+ let(:node) { Chef::Node.new }
+ let(:events) { Chef::EventDispatch::Dispatcher.new }
+ let(:run_context) { Chef::RunContext.new(node, {}, events) }
+ let(:package) { "hello" }
+ let(:source) { "/tmp/hello_20.snap" }
+ let(:new_resource) do
+ new_resource = Chef::Resource::SnapPackage.new(package)
+ new_resource.source source
+ new_resource
+ end
+ let(:provider) { Chef::Provider::Package::Snap.new(new_resource, run_context) }
+ let(:snap_status) do
+ stdout = <<~SNAP_S
+ path: "/tmp/hello_20.snap"
+ name: hello
+ summary: GNU Hello, the "hello world" snap
+ version: 2.10 -
+ SNAP_S
+ status = double(stdout: stdout, stderr: "", exitstatus: 0)
+ allow(status).to receive(:error!).with(no_args).and_return(false)
+ status
+ end
+
+ before(:each) do
+ allow(provider).to receive(:shell_out_compacted!).with("snap", "info", source, timeout: 900).and_return(snap_status)
+ end
+
+ # Example output from https://github.com/snapcore/snapd/wiki/REST-API
+ find_result_success = JSON.parse({"type" => "sync", "status-code" => 200, "status" => "OK", "result" => [{"id" => "mVyGrEwiqSi5PugCwyH7WgpoQLemtTd6", "title" => "hello", "summary" => "GNU Hello, the \"hello world\" snap", "description" => "GNU hello prints a friendly greeting. This is part of the snapcraft tour at https://snapcraft.io/", "download-size" => 65536, "name" => "hello", "publisher" => {"id" => "canonical", "username" => "canonical", "display-name" => "Canonical", "validation" => "verified"}, "developer" => "canonical", "status" => "available", "type" => "app", "version" => "2.10", "channel" => "stable", "ignore-validation" => false, "revision" => "20", "confinement" => "strict", "private" => false, "devmode" => false, "jailmode" => false, "contact" => "mailto:snaps@canonical.com", "license" => "GPL-3.0", "channels" => {"latest/beta" => {"revision" => "29", "confinement" => "strict", "version" => "2.10.1", "channel" => "beta", "epoch" => "0", "size" => 65536}, "latest/candidate" => {"revision" => "20", "confinement" => "strict", "version" => "2.10", "channel" => "candidate", "epoch" => "0", "size" => 65536}, "latest/edge" => {"revision" => "34", "confinement" => "strict", "version" => "2.10.42", "channel" => "edge", "epoch" => "0", "size" => 65536}, "latest/stable" => {"revision" => "20", "confinement" => "strict", "version" => "2.10", "channel" => "stable", "epoch" => "0", "size" => 65536}}, "tracks" => ["latest"]}], "sources" => ["store"], "suggested-currency" => "USD"}.to_json)
+ find_result_fail = JSON.parse({"type" => "error", "status-code" => 404, "status" => "Not Found", "result" => {"message" => "snap not found", "kind" => "snap-not-found", "value" => "hello2"}}.to_json)
+ get_by_name_result_success = JSON.parse({"type" => "sync", "status-code" => 200, "status" => "OK", "result" => {"id" => "CRrJViJiSuDcCkU31G0xpNRVNaj4P960", "summary" => "Universal Command Line Interface for Amazon Web Services", "description" => "This package provides a unified command line interface to Amazon Web\nServices.\n", "installed-size" => 15851520, "name" => "aws-cli", "publisher" => {"id" => "S7iQ7mKDXBDliQqRcgefvc2TKXIH9pYk", "username" => "aws", "display-name" => "Amazon Web Services", "validation" => "verified"}, "developer" => "aws", "status" => "active", "type" => "app", "version" => "1.15.71", "channel" => "", "tracking-channel" => "stable", "ignore-validation" => false, "revision" => "135", "confinement" => "classic", "private" => false, "devmode" => false, "jailmode" => false, "apps" => [{"snap" => "aws-cli", "name" => "aws"}], "contact" => "", "mounted-from" => "/var/lib/snapd/snaps/aws-cli_135.snap", "install-date" => "2018-09-17T20:39:38.516Z"}}.to_json)
+ get_by_name_result_fail = JSON.parse({"type" => "error", "status-code" => 404, "status" => "Not Found", "result" => {"message" => "snap not installed", "kind" => "snap-not-found", "value" => "aws-cliasdfasdf"}}.to_json)
+ async_result_success = JSON.parse({
+ "type" => "async",
+ "status-code" => 202,
+ "status" => "Accepted",
+ "change" => "401"
+ }.to_json)
+ result_fail = JSON.parse({
+ "type" => "error",
+ "status-code" => 401,
+ "status" => "Unauthorized",
+ "result" => {
+ "message" => "access denied",
+ "kind" => "login-required",
+ }
+ }.to_json)
+
+ change_id_result = JSON.parse({"type" => "sync", "status-code" => 200, "status" => "OK", "result" => {"id" => "15", "kind" => "install-snap", "summary" => "Install snap \"hello\"", "status" => "Done", "tasks" => [{"id" => "165", "kind" => "prerequisites", "summary" => "Ensure prerequisites for \"hello\" are available", "status" => "Done", "progress" => {"label" => "", "done" => 1, "total" => 1}, "spawn-time" => "2018-09-22T20:25:25.22104314Z", "ready-time" => "2018-09-22T20:25:25.231090966Z"}, {"id" => "166", "kind" => "download-snap", "summary" => "Download snap \"hello\" (20) from channel \"stable\"", "status" => "Done", "progress" => {"label" => "", "done" => 1, "total" => 1}, "spawn-time" => "2018-09-22T20:25:25.221070859Z", "ready-time" => "2018-09-22T20:25:25.24321909Z"}, {"id" => "167", "kind" => "validate-snap", "summary" => "Fetch and check assertions for snap \"hello\" (20)", "status" => "Done", "progress" => {"label" => "", "done" => 1, "total" => 1}, "spawn-time" => "2018-09-22T20:25:25.221080163Z", "ready-time" => "2018-09-22T20:25:25.55308904Z"}, {"id" => "168", "kind" => "mount-snap", "summary" => "Mount snap \"hello\" (20)", "status" => "Done", "progress" => {"label" => "", "done" => 1, "total" => 1}, "spawn-time" => "2018-09-22T20:25:25.221082984Z", "ready-time" => "2018-09-22T20:25:25.782452658Z"}, {"id" => "169", "kind" => "copy-snap-data", "summary" => "Copy snap \"hello\" data", "status" => "Done", "progress" => {"label" => "", "done" => 1, "total" => 1}, "spawn-time" => "2018-09-22T20:25:25.221085677Z", "ready-time" => "2018-09-22T20:25:25.790911883Z"}, {"id" => "170", "kind" => "setup-profiles", "summary" => "Setup snap \"hello\" (20) security profiles", "status" => "Done", "progress" => {"label" => "", "done" => 1, "total" => 1}, "spawn-time" => "2018-09-22T20:25:25.221088261Z", "ready-time" => "2018-09-22T20:25:25.972796111Z"}, {"id" => "171", "kind" => "link-snap", "summary" => "Make snap \"hello\" (20) available to the system", "status" => "Done", "progress" => {"label" => "", "done" => 1, "total" => 1}, "spawn-time" => "2018-09-22T20:25:25.221090669Z", "ready-time" => "2018-09-22T20:25:25.986931331Z"}, {"id" => "172", "kind" => "auto-connect", "summary" => "Automatically connect eligible plugs and slots of snap \"hello\"", "status" => "Done", "progress" => {"label" => "", "done" => 1, "total" => 1}, "spawn-time" => "2018-09-22T20:25:25.221093357Z", "ready-time" => "2018-09-22T20:25:25.996914144Z"}, {"id" => "173", "kind" => "set-auto-aliases", "summary" => "Set automatic aliases for snap \"hello\"", "status" => "Done", "progress" => {"label" => "", "done" => 1, "total" => 1}, "spawn-time" => "2018-09-22T20:25:25.221097651Z", "ready-time" => "2018-09-22T20:25:26.009155888Z"}, {"id" => "174", "kind" => "setup-aliases", "summary" => "Setup snap \"hello\" aliases", "status" => "Done", "progress" => {"label" => "", "done" => 1, "total" => 1}, "spawn-time" => "2018-09-22T20:25:25.221100379Z", "ready-time" => "2018-09-22T20:25:26.021062388Z"}, {"id" => "175", "kind" => "run-hook", "summary" => "Run install hook of \"hello\" snap if present", "status" => "Done", "progress" => {"label" => "", "done" => 1, "total" => 1}, "spawn-time" => "2018-09-22T20:25:25.221103116Z", "ready-time" => "2018-09-22T20:25:26.031383884Z"}, {"id" => "176", "kind" => "start-snap-services", "summary" => "Start snap \"hello\" (20) services", "status" => "Done", "progress" => {"label" => "", "done" => 1, "total" => 1}, "spawn-time" => "2018-09-22T20:25:25.221110251Z", "ready-time" => "2018-09-22T20:25:26.039564637Z"}, {"id" => "177", "kind" => "run-hook", "summary" => "Run configure hook of \"hello\" snap if present", "status" => "Done", "progress" => {"label" => "", "done" => 1, "total" => 1}, "spawn-time" => "2018-09-22T20:25:25.221115952Z", "ready-time" => "2018-09-22T20:25:26.05069451Z"}], "ready" => true, "spawn-time" => "2018-09-22T20:25:25.221130149Z", "ready-time" => "2018-09-22T20:25:26.050696298Z", "data" => {"snap-names" => ["hello"]}}}.to_json)
+
+ get_conf_success = JSON.parse({"type" => "sync", "status-code" => 200, "status" => "OK", "result" =>{"address" =>"0.0.0.0","allow-privileged" =>true,"anonymous-auth" => false}}.to_json)
+
+ describe "#define_resource_requirements" do
+
+ before do
+ allow_any_instance_of(Chef::Provider::Package::Snap).to receive(:call_snap_api).with('GET', "/v2/snaps/#{package}").and_return(get_by_name_result_success)
+ end
+
+ it "should raise an exception if a source is supplied but not found when :install" do
+ allow(::File).to receive(:exist?).with(source).and_return(false)
+ expect { provider.run_action(:install) }.to raise_error(Chef::Exceptions::Package)
+ end
+
+ it "should raise an exception if a source is supplied but not found when :upgrade" do
+ allow(::File).to receive(:exist?).with(source).and_return(false)
+ expect { provider.run_action(:upgrade) }.to raise_error(Chef::Exceptions::Package)
+ end
+ end
+
+ describe "when using a local file source" do
+ let(:source) { '/tmp/hello_20.snap' }
+
+ before do
+ allow_any_instance_of(Chef::Provider::Package::Snap).to receive(:call_snap_api).with('GET', "/v2/snaps/#{package}").and_return(get_by_name_result_success)
+ end
+
+ it "should create a current resource with the name of the new_resource" do
+ provider.load_current_resource
+ expect(provider.current_resource.package_name).to eq("hello")
+ end
+
+ describe "gets the candidate version from the source package" do
+
+ def check_version(version)
+ provider.load_current_resource
+ expect(provider.current_resource.package_name).to eq("hello")
+ expect(provider.get_current_versions).to eq(["1.15.71"])
+ expect(provider.candidate_version).to eq([version])
+ end
+
+ it "checks the installed and local candidate versions" do
+ check_version("2.10")
+ end
+ end
+ end
+
+ describe "when using the snap store" do
+ let(:source) { nil }
+ describe "gets the candidate version from the snap store" do
+ before do
+ allow_any_instance_of(Chef::Provider::Package::Snap).to receive(:call_snap_api).with('GET', "/v2/find?name=#{package}").and_return(find_result_success)
+ allow_any_instance_of(Chef::Provider::Package::Snap).to receive(:call_snap_api).with('GET', "/v2/snaps/#{package}").and_return(get_by_name_result_success)
+ end
+
+ def check_version(version)
+ provider.load_current_resource
+ expect(provider.current_resource.package_name).to eq("hello")
+ expect(provider.get_current_versions).to eq(["1.15.71"])
+ expect(provider.candidate_version).to eq([version])
+ end
+
+ it "checks the installed and store candidate versions" do
+ check_version("2.10")
+ end
+
+ end
+
+ describe "fails to get the candidate version from the snap store" do
+ before do
+ allow_any_instance_of(Chef::Provider::Package::Snap).to receive(:call_snap_api).with('GET', "/v2/find?name=#{package}").and_return(find_result_fail)
+ allow_any_instance_of(Chef::Provider::Package::Snap).to receive(:call_snap_api).with('GET', "/v2/snaps/#{package}").and_return(get_by_name_result_fail)
+ end
+
+ it "throws an error if candidate version not found" do
+ provider.load_current_resource
+ expect{provider.candidate_version}.to raise_error(Chef::Exceptions::Package)
+ end
+
+ it "does not throw an error if installed version not found" do
+ provider.load_current_resource
+ expect(provider.get_current_versions).to eq([nil])
+ end
+ end
+ end
+
+ describe "when calling async operations" do
+
+ it "should should throw if the async response is an error" do
+ expect { provider.send(:get_id_from_async_response, result_fail) }.to raise_error
+ end
+
+ it "should get the id from an async response" do
+ result = provider.send(:get_id_from_async_response, async_result_success)
+ expect(result).to eq("401")
+ end
+
+ it "should wait for change completion" do
+ result = provider.send(:get_id_from_async_response, async_result_success)
+ expect(result).to eq("401")
+ end
+ end
+
+ describe Chef::Provider::Package::Snap do
+
+ it "should post the correct json" do
+ snap_names = ['hello']
+ action = 'install'
+ channel = 'stable'
+ options = {}
+ revision = nil
+ actual = provider.send(:generate_snap_json, snap_names, action, channel, options, revision)
+
+ expect(actual).to eq({"action" => "install", "snaps" => ["hello"], "channel" => "stable"})
+ end
+
+ end
+end
diff --git a/spec/unit/provider_resolver_spec.rb b/spec/unit/provider_resolver_spec.rb
index a3f2801adf..5066135b90 100644
--- a/spec/unit/provider_resolver_spec.rb
+++ b/spec/unit/provider_resolver_spec.rb
@@ -593,6 +593,7 @@ describe Chef::ProviderResolver do
ruby: [ Chef::Resource::Ruby, Chef::Provider::Script ],
script: [ Chef::Resource::Script, Chef::Provider::Script ],
smartos_package: [ Chef::Resource::SmartosPackage, Chef::Provider::Package::SmartOS ],
+ snap_package: [ Chef::Resource::SnapPackage, Chef::Provider::Package::Snap ],
solaris_package: [ Chef::Resource::SolarisPackage, Chef::Provider::Package::Solaris ],
solaris_user: [ Chef::Resource::User::SolarisUser, Chef::Provider::User::Solaris ],
subversion: [ Chef::Resource::Subversion, Chef::Provider::Subversion ],
diff --git a/spec/unit/resource/snap_package_spec.rb b/spec/unit/resource/snap_package_spec.rb
new file mode 100644
index 0000000000..5d30c2c0bf
--- /dev/null
+++ b/spec/unit/resource/snap_package_spec.rb
@@ -0,0 +1,61 @@
+#
+# Author:: S.Cavallo (<smcavallo@hotmail.com>)
+# Copyright:: Copyright 2008-2018, 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/resource/snap_package"
+require 'chef/provider/package/snap'
+require "support/shared/unit/resource/static_provider_resolution"
+
+describe Chef::Resource::SnapPackage, "initialize" do
+
+ static_provider_resolution(
+ resource: Chef::Resource::SnapPackage,
+ provider: Chef::Provider::Package::Snap,
+ name: :snap_package,
+ action: :install,
+ os: "linux"
+ )
+
+ let(:resource) { Chef::Resource::SnapPackage.new("foo") }
+
+ it "sets the default action as :install" do
+ expect(resource.action).to eql([:install])
+ end
+
+ it "supports :install, :lock, :purge, :reconfig, :remove, :unlock, :upgrade actions" do
+ expect { resource.action :install }.not_to raise_error
+ expect { resource.action :lock }.not_to raise_error
+ expect { resource.action :purge }.not_to raise_error
+ expect { resource.action :reconfig }.not_to raise_error
+ expect { resource.action :remove }.not_to raise_error
+ expect { resource.action :unlock }.not_to raise_error
+ expect { resource.action :upgrade }.not_to raise_error
+ end
+
+ it "channel defaults to stable" do
+ "#{resource.channel("stable")}" # the default
+ expect(resource.channel).to eql("stable")
+ end
+
+ it "supports all channel values" do
+ expect { resource.channel "stable" }.not_to raise_error
+ expect { resource.channel "edge" }.not_to raise_error
+ expect { resource.channel "beta" }.not_to raise_error
+ expect { resource.channel "candidate" }.not_to raise_error
+ end
+end