diff options
author | S.Cavallo <smcavallo@hotmail.com> | 2018-10-09 13:15:04 -0400 |
---|---|---|
committer | Lamont Granquist <lamont@scriptkiddie.org> | 2018-11-28 22:30:43 -0800 |
commit | 3aba92ade1504aa49028f769855b4d650de87b5b (patch) | |
tree | 5943ae11a5302b4f2aee86feec372b8f15154667 | |
parent | b8433f37539f83c37ddfe4301df87f6d1cebb12f (diff) | |
download | chef-3aba92ade1504aa49028f769855b4d650de87b5b.tar.gz |
initial suppport for snap packages
Signed-off-by: S.Cavallo <smcavallo@hotmail.com>
-rw-r--r-- | lib/chef/provider/package/snap.rb | 318 | ||||
-rw-r--r-- | lib/chef/resource/snap_package.rb | 36 | ||||
-rw-r--r-- | lib/chef/resources.rb | 1 | ||||
-rw-r--r-- | spec/unit/provider/package/snap_spec.rb | 190 | ||||
-rw-r--r-- | spec/unit/provider_resolver_spec.rb | 1 | ||||
-rw-r--r-- | spec/unit/resource/snap_package_spec.rb | 61 |
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 |