diff options
author | Phil Dibowitz <phil@ipom.com> | 2020-06-01 21:15:32 -0700 |
---|---|---|
committer | Tim Smith <tsmith84@gmail.com> | 2020-06-23 10:53:35 -0700 |
commit | 4dbc101801bc6f12c4d6c71a0f607c672c1516fb (patch) | |
tree | 68305e182e67a89f266cfbf3ce99d08a4ae7650c | |
parent | 26e067d4bff78ed24cba7937dfb819b890ab85fb (diff) | |
download | chef-4dbc101801bc6f12c4d6c71a0f607c672c1516fb.tar.gz |
Fix snap_package bugs
First and foremost this fixes the bug in which #8827 where
`snap_package` hangs forever.
See the comment in the code for the nitty-gritty details, but snap never
returns an EOF so you have to jump through GREAT hoops not to hang
forever.
UNLESS you want to use nonblock+read, but it takes MUCH longer:
```
[phil@ldt-hardwired ~]$ time /tmp/test.rb char
real 0m0.252s
user 0m0.207s
sys 0m0.044s
[phil@ldt-hardwired ~]$ time /tmp/test.rb select
real 0m5.244s
user 0m0.183s
sys 0m0.056s
```
That's just for doing `GET /v2/snaps/black`
Second, if you try to install a snap that doesn't exist on the `stable`
channel then Chef crashes with a `undefined method [] on nil`, so let's
check for that explicitly.
And a bunch of other fixes that were required to even install anything:
* prevent get_current_versions from returning `[nil]`
* don't try to 'update' when something is not yet installed
* don't try to install versions, install packages
* don't try to push ruby hashes into HTTP without JSONing them first
* when installing, install one at a time, it's the only API that accepts a channel
* options is an array, not a hash
NOTE... there's a lot of janky here. A lot. It's the minimalist stuff
from #9106 I could pull in, while also making the socket stuff not
painfully slow. That PR aims to support versions and lots of other and
should be fixed up and [eventually] merged... but in the meantime this
provider doesn't even come close to working and this at least gets
packages installing.
After this, I never want to touch snap again.
Signed-off-by: Phil Dibowitz <phil@ipom.com>
-rw-r--r-- | lib/chef/provider/package/snap.rb | 124 |
1 files changed, 96 insertions, 28 deletions
diff --git a/lib/chef/provider/package/snap.rb b/lib/chef/provider/package/snap.rb index 1e4d6102b9..7fdedd0078 100644 --- a/lib/chef/provider/package/snap.rb +++ b/lib/chef/provider/package/snap.rb @@ -59,15 +59,14 @@ class Chef def get_current_versions package_name_array.each_with_index.map do |pkg, i| installed_version(i) - end + end.compact 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) + install_snaps(names) end end @@ -75,14 +74,16 @@ class Chef 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) + if get_current_versions.empty? + install_snaps(names, versions) + else + update_snaps(names) + end 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) + uninstall_snaps(names) end alias purge_package remove_package @@ -129,19 +130,72 @@ class Chef "Accept: application/json\r\n" + "Content-Type: application/json\r\n" if method == "POST" - request.concat("Content-Length: #{post_data.bytesize}\r\n\r\n#{post_data}") + pdata = post_data.to_json.to_s + request.concat("Content-Length: #{pdata.bytesize}\r\n\r\n#{pdata}") 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 + + # 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) + UNIXSocket.open('/run/snapd.socket') do |socket| + # send request, read the response, split the response and parse + # the body + socket.write(request) + + # WARNING!!! HERE BE DRAGONs + # + # So snapd doesn't return an EOF at the end of its body, so + # doing a normal read will just hang forever. + # + # Well, sort of. if, after it writes everything, you then send + # yet-another newline, it'll then send its EOF and promptly + # disconnect closing the pipe and preventing reading. so, you + # have to read first, and therein lies the EOF problem. + # + # So you can do non-blocking reads with selects, but it + # makes every read take about 5 seconds. If, instead, we + # read the last line char-by-char, it's about half a second. + # + # Reading a character at a time isn't efficient, and since we + # know that http headers always have a blank line after them, + # we can read lines until we find a blank line and *then* read + # a character at a time. snap returns all the json on a single + # line, so once you pass headers you must read a character a + # time. + # + # - jaymzh + + Chef::Log.trace( + "snap_package[#{new_resource.package_name}]: reading headers", + ) + loop do + response = socket.readline + break if response.strip.empty? # finished headers + end + Chef::Log.trace( + "snap_package[#{new_resource.package_name}]: past headers, " + + 'onto the body...', + ) + result = nil + body = '' + socket.each_char do |c| + body << c + # we know we're not done if we don't have a char that + # can end JSON + next unless ['}', ']'].include?(c) + begin + result = JSON.parse(body) + # if we get here, we were able to parse the json so we + # are done reading + break + rescue JSON::ParserError + next + end + end + result end end end @@ -211,20 +265,22 @@ class Chef 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) + def install_snaps(snap_names, versions) + snap_names.each do |snap| + response = post_snap(snap, "install", new_resource.channel, new_resource.options) + id = get_id_from_async_response(response) + wait_for_completion(id) + end end def update_snaps(snap_names) - response = post_snaps(snap_names, "refresh", new_resource.channel, new_resource.options) + response = post_snaps(snap_names, "refresh", nil, 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) + response = post_snaps(snap_names, "remove", nil, new_resource.options) id = get_id_from_async_response(response) wait_for_completion(id) end @@ -278,18 +334,20 @@ class Chef "action" => action, "snaps" => snap_names, } - if %w{install refresh switch}.include?(action) + if %w{install refresh switch}.include?(action) && channel 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"] + if options + request["classic"] = true if options.include?("classic") + request["devmode"] = true if options.include?("devmode") + request["jailmode"] = true if options.include?("jailmode") + request["ignore_validation"] = true if options.include?("ignore-validation") + end request["revision"] = revision unless revision.nil? - request["ignore_validation"] = true if options["ignore-validation"] request end @@ -305,12 +363,22 @@ class Chef call_snap_api("POST", "/v2/snaps", json) end + def post_snap(snap_name, action, channel, options, revision = nil) + json = generate_snap_json(snap_name, action, channel, options, revision = nil) + json.delete("snaps") + call_snap_api("POST", "/v2/snaps/#{snap_name}", 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 + unless json["result"][0]["channels"]["latest/#{channel}"] + raise Chef::Exceptions::Package, "No version of #{name} in channel #{channel}", caller + end + # Return the version matching the channel json["result"][0]["channels"]["latest/#{channel}"]["version"] end |