summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPhil Dibowitz <phil@ipom.com>2020-06-01 21:15:32 -0700
committerTim Smith <tsmith84@gmail.com>2020-06-23 10:53:35 -0700
commit4dbc101801bc6f12c4d6c71a0f607c672c1516fb (patch)
tree68305e182e67a89f266cfbf3ce99d08a4ae7650c
parent26e067d4bff78ed24cba7937dfb819b890ab85fb (diff)
downloadchef-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.rb124
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