diff --git a/lib/docker_engine_ruby.rb b/lib/docker_engine_ruby.rb index f7f1910..f43def6 100644 --- a/lib/docker_engine_ruby.rb +++ b/lib/docker_engine_ruby.rb @@ -44,6 +44,7 @@ require_relative "docker_engine_ruby/internal/type/hash_of" require_relative "docker_engine_ruby/internal/type/base_model" require_relative "docker_engine_ruby/internal/type/base_page" +require_relative "docker_engine_ruby/internal/jsonl_stream" require_relative "docker_engine_ruby/internal/type/request_parameters" require_relative "docker_engine_ruby/internal" require_relative "docker_engine_ruby/request_options" @@ -227,6 +228,13 @@ require_relative "docker_engine_ruby/models/volume_prune_response" require_relative "docker_engine_ruby/models/volume_update_params" require_relative "docker_engine_ruby/models/wait_response" +require_relative "docker_engine_ruby/models/error_detail" +require_relative "docker_engine_ruby/models/progress_detail" +require_relative "docker_engine_ruby/models/image_id" +require_relative "docker_engine_ruby/models/build_info" +require_relative "docker_engine_ruby/models/create_image_info" +require_relative "docker_engine_ruby/models/push_image_info" +require_relative "docker_engine_ruby/helpers/buildkit_status" require_relative "docker_engine_ruby/models" require_relative "docker_engine_ruby/resources/auth" require_relative "docker_engine_ruby/resources/configs" diff --git a/lib/docker_engine_ruby/errors.rb b/lib/docker_engine_ruby/errors.rb index 06af5da..93da3f0 100644 --- a/lib/docker_engine_ruby/errors.rb +++ b/lib/docker_engine_ruby/errors.rb @@ -33,6 +33,20 @@ def initialize(on:, method:, target:, value:, cause: nil) end end + # Docker reports a failed build, pull or push inside a 200 response, as the last + # message of the progress stream. + class StreamError < DockerEngineRuby::Errors::Error + # @return [DockerEngineRuby::Models::ErrorDetail, nil] + attr_reader :detail + + # @param message [String] + # @param detail [DockerEngineRuby::Models::ErrorDetail, nil] + def initialize(message, detail: nil) + @detail = detail + super(message) + end + end + class APIError < DockerEngineRuby::Errors::Error # @return [URI::Generic] attr_accessor :url diff --git a/lib/docker_engine_ruby/helpers/buildkit_status.rb b/lib/docker_engine_ruby/helpers/buildkit_status.rb new file mode 100644 index 0000000..987ad32 --- /dev/null +++ b/lib/docker_engine_ruby/helpers/buildkit_status.rb @@ -0,0 +1,136 @@ +# frozen_string_literal: true + +require "base64" + +module DockerEngineRuby + module Helpers + # Progress of a BuildKit build (`version: :"2"`). The daemon sends it as `BuildInfo` messages + # with `id: "moby.buildkit.trace"` and a base64 protobuf `StatusResponse` in `aux` + # (moby/buildkit `api/services/control/control.proto`). Only the fields a progress log + # needs are decoded; the rest are skipped. + module BuildkitStatus + TRACE_ID = "moby.buildkit.trace" + + Vertex = Struct.new(:digest, :name, :cached, :started, :completed, :error, keyword_init: true) + Log = Struct.new(:vertex, :stream, :msg, keyword_init: true) + Status = Struct.new(:vertexes, :logs, keyword_init: true) + + class << self + # @param info [DockerEngineRuby::Models::BuildInfo] + # @return [Boolean] + def trace?(info) = info.id == TRACE_ID && info.aux.is_a?(String) + + # @param aux [String] base64-encoded `StatusResponse` + # @return [DockerEngineRuby::Helpers::BuildkitStatus::Status] + def decode(aux) + status = Status.new(vertexes: [], logs: []) + each_field(Base64.decode64(aux)) do |number, value| + case number + in 1 then status.vertexes << decode_vertex(value) + in 3 then status.logs << decode_log(value) + else nil + end + end + status + end + + private + + def decode_vertex(bytes) + vertex = Vertex.new(cached: false, started: false, completed: false) + each_field(bytes) do |number, value| + case number + in 1 then vertex.digest = utf8(value) + in 3 then vertex.name = utf8(value) + in 4 then vertex.cached = value != 0 + in 5 then vertex.started = true + in 6 then vertex.completed = true + in 7 then vertex.error = utf8(value) + else nil + end + end + vertex + end + + def decode_log(bytes) + log = Log.new(stream: 0, msg: +"") + each_field(bytes) do |number, value| + case number + in 1 then log.vertex = utf8(value) + in 3 then log.stream = value + in 4 then log.msg = utf8(value) + else nil + end + end + log + end + + def utf8(bytes) = bytes.dup.force_encoding(Encoding::UTF_8).scrub + + # Protobuf wire format: yields [field number, varint Integer | length-delimited String]. + def each_field(bytes) + reader = StringIO.new(bytes.b) + until reader.eof? + key = read_varint(reader) + number = key >> 3 + case key & 7 + in 0 then yield(number, read_varint(reader)) + in 1 then reader.read(8) + in 2 then yield(number, reader.read(read_varint(reader)).to_s) + in 5 then reader.read(4) + end + end + end + + def read_varint(reader) + result = 0 + shift = 0 + loop do + byte = reader.readbyte + result |= (byte & 0x7f) << shift + return result if byte < 0x80 + + shift += 7 + end + end + end + + # Renders statuses as `docker build --progress=plain` does: vertexes are numbered in the + # order they appear, and each state change or log chunk becomes a line. + class PlainPrinter + # @param out [#call] receives each rendered line + def initialize(&out) + @out = out + @numbers = {} + @printed = {} + end + + # @param status [DockerEngineRuby::Helpers::BuildkitStatus::Status] + def print(status) + status.vertexes.each { print_vertex(_1) } + status.logs.each do |log| + prefix = "##{number(log.vertex)} " + log.msg.each_line { @out.call(prefix + _1.chomp) } + end + end + + private + + def number(digest) = @numbers[digest] ||= @numbers.size + 1 + + def print_vertex(vertex) + n = number(vertex.digest) + state = if vertex.error then "ERROR: #{vertex.error}" + elsif vertex.cached then "CACHED" + elsif vertex.completed then "DONE" + elsif vertex.started then vertex.name + end + return if state.nil? || @printed[[n, state]] + + @printed[[n, state]] = true + @out.call(state == vertex.name ? "##{n} #{vertex.name}" : "##{n} #{state}") + end + end + end + end +end diff --git a/lib/docker_engine_ruby/internal/jsonl_stream.rb b/lib/docker_engine_ruby/internal/jsonl_stream.rb new file mode 100644 index 0000000..d5c4126 --- /dev/null +++ b/lib/docker_engine_ruby/internal/jsonl_stream.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +module DockerEngineRuby + module Internal + # Progress streams of Docker (`/build`, `/images/create`, `/images/{name}/push`) are one JSON + # object per line, sent as `application/json`. Each line is coerced to `model` as it arrives. + # + # @generic Elem + class JSONLStream + include Enumerable + + # @param model [DockerEngineRuby::Internal::Type::Converter, Class] + # @param stream [Enumerable] raw response lines + def initialize(model:, stream:, **) + @model = model + @lines = stream + end + + # @yieldparam message [generic] + def each + return enum_for(__method__) unless block_given? + + @lines.each do |line| + next if line.strip.empty? + + yield(DockerEngineRuby::Internal::Type::Converter.coerce( + @model, + JSON.parse(line, symbolize_names: true) + )) + end + end + end + end +end diff --git a/lib/docker_engine_ruby/internal/transport/base_client.rb b/lib/docker_engine_ruby/internal/transport/base_client.rb index d683727..500b7a7 100644 --- a/lib/docker_engine_ruby/internal/transport/base_client.rb +++ b/lib/docker_engine_ruby/internal/transport/base_client.rb @@ -537,7 +537,14 @@ def request(req) return nil end - decoded = DockerEngineRuby::Internal::Util.decode_content(headers, stream: stream) + # A stream class takes the raw lines: Docker sends its progress streams as `application/json`, + # which `decode_content` would join and parse as a single document. + decoded = + if req.key?(:stream) + DockerEngineRuby::Internal::Util.decode_lines(stream) + else + DockerEngineRuby::Internal::Util.decode_content(headers, stream: stream) + end case req in {stream: Class => st} st.new( diff --git a/lib/docker_engine_ruby/internal/type/union.rb b/lib/docker_engine_ruby/internal/type/union.rb index db499f0..a3b528d 100644 --- a/lib/docker_engine_ruby/internal/type/union.rb +++ b/lib/docker_engine_ruby/internal/type/union.rb @@ -150,6 +150,8 @@ def coerce(value, state:) target = variant_fn.call exact = state[:exactness] = {yes: 0, no: 0, maybe: 0} state[:branched] += 1 + # A failed attempt must not leave its error behind for the variant that fits. + state[:error] = nil coerced = DockerEngineRuby::Internal::Type::Converter.coerce(target, value, state: state) yes, no, maybe = exact.values @@ -158,7 +160,7 @@ def coerce(value, state:) state[:exactness] = exactness return coerced elsif maybe.positive? - alternatives << [[-yes, -maybe, no], exact, coerced] + alternatives << [[-yes, -maybe, no], exact, coerced, state[:error]] end end @@ -167,8 +169,9 @@ def coerce(value, state:) exactness[:no] += 1 state[:error] = ArgumentError.new("no matching variant for #{value.inspect}") value - in [[_, exact, coerced], *] + in [[_, exact, coerced, error], *] exact.each { exactness[_1] += _2 } + state[:error] = error coerced end .tap { state[:exactness] = exactness } diff --git a/lib/docker_engine_ruby/models.rb b/lib/docker_engine_ruby/models.rb index 1562fae..bd37ca1 100644 --- a/lib/docker_engine_ruby/models.rb +++ b/lib/docker_engine_ruby/models.rb @@ -331,4 +331,16 @@ module DockerEngineRuby VolumeUpdateParams = DockerEngineRuby::Models::VolumeUpdateParams WaitResponse = DockerEngineRuby::Models::WaitResponse + + BuildInfo = DockerEngineRuby::Models::BuildInfo + + CreateImageInfo = DockerEngineRuby::Models::CreateImageInfo + + ErrorDetail = DockerEngineRuby::Models::ErrorDetail + + ImageID = DockerEngineRuby::Models::ImageID + + ProgressDetail = DockerEngineRuby::Models::ProgressDetail + + PushImageInfo = DockerEngineRuby::Models::PushImageInfo end diff --git a/lib/docker_engine_ruby/models/build_info.rb b/lib/docker_engine_ruby/models/build_info.rb new file mode 100644 index 0000000..8b96042 --- /dev/null +++ b/lib/docker_engine_ruby/models/build_info.rb @@ -0,0 +1,81 @@ +# frozen_string_literal: true + +module DockerEngineRuby + module Models + # Сообщение из потока `/build`. + class BuildInfo < DockerEngineRuby::Internal::Type::BaseModel + # @!attribute id + # + # @return [String, nil] + optional :id, String + + # @!attribute stream + # Строка вывода сборки. + # + # @return [String, nil] + optional :stream, String + + # @!attribute error + # Устаревшее поле: то же, что `error_detail.message`. + # + # @return [String, nil] + optional :error, String + + # @!attribute error_detail + # + # @return [DockerEngineRuby::Models::ErrorDetail, nil] + optional :error_detail, -> { DockerEngineRuby::ErrorDetail }, api_name: :errorDetail + + # @!attribute status + # + # @return [String, nil] + optional :status, String + + # @!attribute progress + # Устаревшее поле: прогресс строкой. + # + # @return [String, nil] + optional :progress, String + + # @!attribute progress_detail + # + # @return [DockerEngineRuby::Models::ProgressDetail, nil] + optional :progress_detail, -> { DockerEngineRuby::ProgressDetail }, api_name: :progressDetail + + # @!attribute aux + # Классический сборщик кладёт сюда ID образа, BuildKit — base64 protobuf `StatusResponse` при `id` = `moby.buildkit.trace`. + # + # @return [DockerEngineRuby::Models::ImageID, String, nil] + optional :aux, union: -> { DockerEngineRuby::BuildInfo::Aux } + + # @!method initialize(id: nil, stream: nil, error: nil, error_detail: nil, status: nil, progress: nil, progress_detail: nil, aux: nil) + # @param id [String] + # + # @param stream [String] Строка вывода сборки. + # + # @param error [String] Устаревшее поле: то же, что `error_detail.message`. + # + # @param error_detail [DockerEngineRuby::Models::ErrorDetail] + # + # @param status [String] + # + # @param progress [String] Устаревшее поле: прогресс строкой. + # + # @param progress_detail [DockerEngineRuby::Models::ProgressDetail] + # + # @param aux [DockerEngineRuby::Models::ImageID, String] Классический сборщик кладёт сюда ID образа, BuildKit — base64 protobuf `StatusResponse` при `id` = `moby.buildkit.trace`. + + # @see DockerEngineRuby::Models::BuildInfo#aux + module Aux + extend DockerEngineRuby::Internal::Type::Union + + variant -> { DockerEngineRuby::ImageID } + + variant String + + # @!method self.variants + # @return [Array(DockerEngineRuby::Models::ImageID, String)] + end + end + end +end diff --git a/lib/docker_engine_ruby/models/create_image_info.rb b/lib/docker_engine_ruby/models/create_image_info.rb new file mode 100644 index 0000000..d12297e --- /dev/null +++ b/lib/docker_engine_ruby/models/create_image_info.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +module DockerEngineRuby + module Models + # Сообщение из потока `/images/create` (pull). + class CreateImageInfo < DockerEngineRuby::Internal::Type::BaseModel + # @!attribute id + # + # @return [String, nil] + optional :id, String + + # @!attribute error + # Устаревшее поле: то же, что `error_detail.message`. + # + # @return [String, nil] + optional :error, String + + # @!attribute error_detail + # + # @return [DockerEngineRuby::Models::ErrorDetail, nil] + optional :error_detail, -> { DockerEngineRuby::ErrorDetail }, api_name: :errorDetail + + # @!attribute status + # + # @return [String, nil] + optional :status, String + + # @!attribute progress + # Устаревшее поле: прогресс строкой. + # + # @return [String, nil] + optional :progress, String + + # @!attribute progress_detail + # + # @return [DockerEngineRuby::Models::ProgressDetail, nil] + optional :progress_detail, -> { DockerEngineRuby::ProgressDetail }, api_name: :progressDetail + + # @!method initialize(id: nil, error: nil, error_detail: nil, status: nil, progress: nil, progress_detail: nil) + # @param id [String] + # + # @param error [String] Устаревшее поле: то же, что `error_detail.message`. + # + # @param error_detail [DockerEngineRuby::Models::ErrorDetail] + # + # @param status [String] + # + # @param progress [String] Устаревшее поле: прогресс строкой. + # + # @param progress_detail [DockerEngineRuby::Models::ProgressDetail] + end + end +end diff --git a/lib/docker_engine_ruby/models/error_detail.rb b/lib/docker_engine_ruby/models/error_detail.rb new file mode 100644 index 0000000..685050d --- /dev/null +++ b/lib/docker_engine_ruby/models/error_detail.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module DockerEngineRuby + module Models + # Ошибка из потока прогресса Docker. + class ErrorDetail < DockerEngineRuby::Internal::Type::BaseModel + # @!attribute code + # Код ошибки. + # + # @return [Integer, nil] + optional :code, Integer + + # @!attribute message + # Сообщение об ошибке. + # + # @return [String, nil] + optional :message, String + + # @!method initialize(code: nil, message: nil) + # @param code [Integer] Код ошибки. + # + # @param message [String] Сообщение об ошибке. + end + end +end diff --git a/lib/docker_engine_ruby/models/image_id.rb b/lib/docker_engine_ruby/models/image_id.rb new file mode 100644 index 0000000..0835312 --- /dev/null +++ b/lib/docker_engine_ruby/models/image_id.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module DockerEngineRuby + module Models + # Идентификатор или дайджест образа. + class ImageID < DockerEngineRuby::Internal::Type::BaseModel + # @!attribute id + # + # @return [String, nil] + optional :id, String, api_name: :ID + + # @!method initialize(id: nil) + # @param id [String] + end + end +end diff --git a/lib/docker_engine_ruby/models/progress_detail.rb b/lib/docker_engine_ruby/models/progress_detail.rb new file mode 100644 index 0000000..576a41f --- /dev/null +++ b/lib/docker_engine_ruby/models/progress_detail.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module DockerEngineRuby + module Models + # Прогресс шага: сколько сделано из скольки. + class ProgressDetail < DockerEngineRuby::Internal::Type::BaseModel + # @!attribute current + # + # @return [Integer, nil] + optional :current, Integer + + # @!attribute total + # + # @return [Integer, nil] + optional :total, Integer + + # @!method initialize(current: nil, total: nil) + # @param current [Integer] + # + # @param total [Integer] + end + end +end diff --git a/lib/docker_engine_ruby/models/push_image_info.rb b/lib/docker_engine_ruby/models/push_image_info.rb new file mode 100644 index 0000000..1fb710f --- /dev/null +++ b/lib/docker_engine_ruby/models/push_image_info.rb @@ -0,0 +1,86 @@ +# frozen_string_literal: true + +module DockerEngineRuby + module Models + # Сообщение из потока `/images/{name}/push`. + class PushImageInfo < DockerEngineRuby::Internal::Type::BaseModel + # @!attribute id + # + # @return [String, nil] + optional :id, String + + # @!attribute error + # Устаревшее поле: то же, что `error_detail.message`. + # + # @return [String, nil] + optional :error, String + + # @!attribute error_detail + # + # @return [DockerEngineRuby::Models::ErrorDetail, nil] + optional :error_detail, -> { DockerEngineRuby::ErrorDetail }, api_name: :errorDetail + + # @!attribute status + # + # @return [String, nil] + optional :status, String + + # @!attribute progress + # Устаревшее поле: прогресс строкой. + # + # @return [String, nil] + optional :progress, String + + # @!attribute progress_detail + # + # @return [DockerEngineRuby::Models::ProgressDetail, nil] + optional :progress_detail, -> { DockerEngineRuby::ProgressDetail }, api_name: :progressDetail + + # @!attribute aux + # Итог публикации: тег, дайджест и размер. + # + # @return [DockerEngineRuby::Models::PushImageInfo::Aux, nil] + optional :aux, -> { DockerEngineRuby::PushImageInfo::Aux } + + # @!method initialize(id: nil, error: nil, error_detail: nil, status: nil, progress: nil, progress_detail: nil, aux: nil) + # @param id [String] + # + # @param error [String] Устаревшее поле: то же, что `error_detail.message`. + # + # @param error_detail [DockerEngineRuby::Models::ErrorDetail] + # + # @param status [String] + # + # @param progress [String] Устаревшее поле: прогресс строкой. + # + # @param progress_detail [DockerEngineRuby::Models::ProgressDetail] + # + # @param aux [DockerEngineRuby::Models::PushImageInfo::Aux] Итог публикации: тег, дайджест и размер. + + # @see DockerEngineRuby::Models::PushImageInfo#aux + class Aux < DockerEngineRuby::Internal::Type::BaseModel + # @!attribute tag + # + # @return [String, nil] + optional :tag, String, api_name: :Tag + + # @!attribute digest + # + # @return [String, nil] + optional :digest, String, api_name: :Digest + + # @!attribute size + # + # @return [Integer, nil] + optional :size, Integer, api_name: :Size + + # @!method initialize(tag: nil, digest: nil, size: nil) + # @param tag [String] + # + # @param digest [String] + # + # @param size [Integer] + end + end + end +end diff --git a/lib/docker_engine_ruby/resources/images.rb b/lib/docker_engine_ruby/resources/images.rb index 2581ce9..a577e55 100644 --- a/lib/docker_engine_ruby/resources/images.rb +++ b/lib/docker_engine_ruby/resources/images.rb @@ -114,10 +114,14 @@ def delete(name, params = {}) # # @param request_options [DockerEngineRuby::RequestOptions, Hash{Symbol=>Object}, nil] # + # @yieldparam message [DockerEngineRuby::Models::BuildInfo] each progress message, as it arrives + # + # @raise [DockerEngineRuby::Errors::StreamError] when the daemon reports a failure inside the stream + # # @return [nil] # # @see DockerEngineRuby::Models::ImageBuildParams - def build(params) + def build(params, &block) query_params = [ :buildargs, @@ -147,7 +151,7 @@ def build(params) :version ] parsed, options = DockerEngineRuby::ImageBuildParams.dump_request(params) - @client.request( + messages = @client.request( method: :post, path: "build", query: parsed.slice(*query_params), @@ -158,9 +162,11 @@ def build(params) x_registry_config: "x-registry-config" ), body: parsed[:body], - model: NilClass, + stream: DockerEngineRuby::Internal::JSONLStream, + model: DockerEngineRuby::BuildInfo, options: options ) + each_progress(messages, &block) end # Delete builder cache @@ -450,13 +456,17 @@ def prune(params = {}) # # @param request_options [DockerEngineRuby::RequestOptions, Hash{Symbol=>Object}, nil] # + # @yieldparam message [DockerEngineRuby::Models::CreateImageInfo] each progress message, as it arrives + # + # @raise [DockerEngineRuby::Errors::StreamError] when the daemon reports a failure inside the stream + # # @return [nil] # # @see DockerEngineRuby::Models::ImagePullParams - def pull(params) + def pull(params, &block) query_params = [:changes, :from_image, :from_src, :message, :platform, :repo, :tag] parsed, options = DockerEngineRuby::ImagePullParams.dump_request(params) - @client.request( + messages = @client.request( method: :post, path: "images/create", query: parsed.slice(*query_params).transform_keys(from_image: "fromImage", from_src: "fromSrc"), @@ -464,9 +474,11 @@ def pull(params) x_registry_auth: "x-registry-auth" ), body: parsed[:body], - model: NilClass, + stream: DockerEngineRuby::Internal::JSONLStream, + model: DockerEngineRuby::CreateImageInfo, options: options ) + each_progress(messages, &block) end # Push an image @@ -483,21 +495,27 @@ def pull(params) # # @param request_options [DockerEngineRuby::RequestOptions, Hash{Symbol=>Object}, nil] # + # @yieldparam message [DockerEngineRuby::Models::PushImageInfo] each progress message, as it arrives + # + # @raise [DockerEngineRuby::Errors::StreamError] when the daemon reports a failure inside the stream + # # @return [nil] # # @see DockerEngineRuby::Models::ImagePushParams - def push(name, params) + def push(name, params, &block) query_params = [:platform, :tag] parsed, options = DockerEngineRuby::ImagePushParams.dump_request(params) query = DockerEngineRuby::Internal::Util.encode_query_params(parsed.slice(*query_params)) - @client.request( + messages = @client.request( method: :post, path: ["images/%1$s/push", name], query: query, headers: parsed.except(*query_params).transform_keys(x_registry_auth: "x-registry-auth"), - model: NilClass, + stream: DockerEngineRuby::Internal::JSONLStream, + model: DockerEngineRuby::PushImageInfo, options: options ) + each_progress(messages, &block) end # Search images @@ -548,6 +566,23 @@ def tag(name, params = {}) ) end + private + + # Docker answers a failed build, pull or push with 200 and an error in the last message, + # so the stream is read to the end and that message turned into an exception. + def each_progress(messages) + messages.each do |message| + detail = message.error_detail + error = detail&.message || message.error + raise DockerEngineRuby::Errors::StreamError.new(error, detail: detail) if error + + yield(message) if block_given? + end + nil + end + + public + # @api private # # @param client [DockerEngineRuby::Client] diff --git a/rbi/docker_engine_ruby/errors.rbi b/rbi/docker_engine_ruby/errors.rbi index 9eb326c..f507c90 100644 --- a/rbi/docker_engine_ruby/errors.rbi +++ b/rbi/docker_engine_ruby/errors.rbi @@ -26,6 +26,20 @@ module DockerEngineRuby end end + class StreamError < DockerEngineRuby::Errors::Error + sig { returns(T.nilable(DockerEngineRuby::Models::ErrorDetail)) } + attr_reader :detail + + sig do + params( + message: String, + detail: T.nilable(DockerEngineRuby::Models::ErrorDetail) + ).returns(T.attached_class) + end + def self.new(message, detail: nil) + end + end + class APIError < DockerEngineRuby::Errors::Error sig { returns(URI::Generic) } attr_accessor :url diff --git a/rbi/docker_engine_ruby/internal/jsonl_stream.rbi b/rbi/docker_engine_ruby/internal/jsonl_stream.rbi new file mode 100644 index 0000000..472cc1d --- /dev/null +++ b/rbi/docker_engine_ruby/internal/jsonl_stream.rbi @@ -0,0 +1,39 @@ +# typed: strong + +module DockerEngineRuby + module Internal + class JSONLStream + include Enumerable + + extend T::Generic + + Elem = type_member(:out) + + sig do + params( + model: T.anything, + url: URI::Generic, + status: Integer, + headers: T::Hash[String, String], + response: Net::HTTPResponse, + unwrap: T.anything, + stream: T::Enumerable[String] + ).void + end + def initialize( + model:, + url:, + status:, + headers:, + response:, + unwrap:, + stream: + ) + end + + sig { override.params(blk: T.proc.params(message: Elem).void).void } + def each(&blk) + end + end + end +end diff --git a/rbi/docker_engine_ruby/models.rbi b/rbi/docker_engine_ruby/models.rbi index 0bbb9a8..7baa95a 100644 --- a/rbi/docker_engine_ruby/models.rbi +++ b/rbi/docker_engine_ruby/models.rbi @@ -291,4 +291,16 @@ module DockerEngineRuby VolumeUpdateParams = DockerEngineRuby::Models::VolumeUpdateParams WaitResponse = DockerEngineRuby::Models::WaitResponse + + BuildInfo = DockerEngineRuby::Models::BuildInfo + + CreateImageInfo = DockerEngineRuby::Models::CreateImageInfo + + ErrorDetail = DockerEngineRuby::Models::ErrorDetail + + ImageID = DockerEngineRuby::Models::ImageID + + ProgressDetail = DockerEngineRuby::Models::ProgressDetail + + PushImageInfo = DockerEngineRuby::Models::PushImageInfo end diff --git a/rbi/docker_engine_ruby/models/build_info.rbi b/rbi/docker_engine_ruby/models/build_info.rbi new file mode 100644 index 0000000..5ab0f07 --- /dev/null +++ b/rbi/docker_engine_ruby/models/build_info.rbi @@ -0,0 +1,122 @@ +# typed: strong + +module DockerEngineRuby + module Models + class BuildInfo < DockerEngineRuby::Internal::Type::BaseModel + OrHash = + T.type_alias do + T.any( + DockerEngineRuby::BuildInfo, + DockerEngineRuby::Internal::AnyHash + ) + end + + sig { returns(T.nilable(String)) } + attr_reader :id + + sig { params(id: String).void } + attr_writer :id + + # Строка вывода сборки. + sig { returns(T.nilable(String)) } + attr_reader :stream + + sig { params(stream: String).void } + attr_writer :stream + + # Устаревшее поле: то же, что `error_detail.message`. + sig { returns(T.nilable(String)) } + attr_reader :error + + sig { params(error: String).void } + attr_writer :error + + sig { returns(T.nilable(DockerEngineRuby::ErrorDetail)) } + attr_reader :error_detail + + sig { params(error_detail: DockerEngineRuby::ErrorDetail::OrHash).void } + attr_writer :error_detail + + sig { returns(T.nilable(String)) } + attr_reader :status + + sig { params(status: String).void } + attr_writer :status + + # Устаревшее поле: прогресс строкой. + sig { returns(T.nilable(String)) } + attr_reader :progress + + sig { params(progress: String).void } + attr_writer :progress + + sig { returns(T.nilable(DockerEngineRuby::ProgressDetail)) } + attr_reader :progress_detail + + sig do + params(progress_detail: DockerEngineRuby::ProgressDetail::OrHash).void + end + attr_writer :progress_detail + + # Классический сборщик кладёт сюда ID образа, BuildKit — base64 protobuf `StatusResponse` при `id` = `moby.buildkit.trace`. + sig { returns(T.nilable(DockerEngineRuby::BuildInfo::Aux::Variants)) } + attr_reader :aux + + sig { params(aux: DockerEngineRuby::BuildInfo::Aux::Variants).void } + attr_writer :aux + + sig do + params( + id: String, + stream: String, + error: String, + error_detail: DockerEngineRuby::ErrorDetail::OrHash, + status: String, + progress: String, + progress_detail: DockerEngineRuby::ProgressDetail::OrHash, + aux: DockerEngineRuby::BuildInfo::Aux::Variants + ).returns(T.attached_class) + end + def self.new( + id: nil, + stream: nil, + error: nil, + error_detail: nil, + status: nil, + progress: nil, + progress_detail: nil, + aux: nil + ) + end + + sig do + override.returns( + { + id: String, + stream: String, + error: String, + error_detail: DockerEngineRuby::ErrorDetail, + status: String, + progress: String, + progress_detail: DockerEngineRuby::ProgressDetail, + aux: DockerEngineRuby::BuildInfo::Aux::Variants + } + ) + end + def to_hash + end + + module Aux + extend DockerEngineRuby::Internal::Type::Union + + Variants = T.type_alias { T.any(DockerEngineRuby::ImageID, String) } + + sig do + override.returns(T::Array[DockerEngineRuby::BuildInfo::Aux::Variants]) + end + def self.variants + end + end + end + end +end diff --git a/rbi/docker_engine_ruby/models/create_image_info.rbi b/rbi/docker_engine_ruby/models/create_image_info.rbi new file mode 100644 index 0000000..54c25b3 --- /dev/null +++ b/rbi/docker_engine_ruby/models/create_image_info.rbi @@ -0,0 +1,90 @@ +# typed: strong + +module DockerEngineRuby + module Models + class CreateImageInfo < DockerEngineRuby::Internal::Type::BaseModel + OrHash = + T.type_alias do + T.any( + DockerEngineRuby::CreateImageInfo, + DockerEngineRuby::Internal::AnyHash + ) + end + + sig { returns(T.nilable(String)) } + attr_reader :id + + sig { params(id: String).void } + attr_writer :id + + # Устаревшее поле: то же, что `error_detail.message`. + sig { returns(T.nilable(String)) } + attr_reader :error + + sig { params(error: String).void } + attr_writer :error + + sig { returns(T.nilable(DockerEngineRuby::ErrorDetail)) } + attr_reader :error_detail + + sig { params(error_detail: DockerEngineRuby::ErrorDetail::OrHash).void } + attr_writer :error_detail + + sig { returns(T.nilable(String)) } + attr_reader :status + + sig { params(status: String).void } + attr_writer :status + + # Устаревшее поле: прогресс строкой. + sig { returns(T.nilable(String)) } + attr_reader :progress + + sig { params(progress: String).void } + attr_writer :progress + + sig { returns(T.nilable(DockerEngineRuby::ProgressDetail)) } + attr_reader :progress_detail + + sig do + params(progress_detail: DockerEngineRuby::ProgressDetail::OrHash).void + end + attr_writer :progress_detail + + sig do + params( + id: String, + error: String, + error_detail: DockerEngineRuby::ErrorDetail::OrHash, + status: String, + progress: String, + progress_detail: DockerEngineRuby::ProgressDetail::OrHash + ).returns(T.attached_class) + end + def self.new( + id: nil, + error: nil, + error_detail: nil, + status: nil, + progress: nil, + progress_detail: nil + ) + end + + sig do + override.returns( + { + id: String, + error: String, + error_detail: DockerEngineRuby::ErrorDetail, + status: String, + progress: String, + progress_detail: DockerEngineRuby::ProgressDetail + } + ) + end + def to_hash + end + end + end +end diff --git a/rbi/docker_engine_ruby/models/error_detail.rbi b/rbi/docker_engine_ruby/models/error_detail.rbi new file mode 100644 index 0000000..9b846dc --- /dev/null +++ b/rbi/docker_engine_ruby/models/error_detail.rbi @@ -0,0 +1,37 @@ +# typed: strong + +module DockerEngineRuby + module Models + class ErrorDetail < DockerEngineRuby::Internal::Type::BaseModel + OrHash = + T.type_alias do + T.any( + DockerEngineRuby::ErrorDetail, + DockerEngineRuby::Internal::AnyHash + ) + end + + # Код ошибки. + sig { returns(T.nilable(Integer)) } + attr_reader :code + + sig { params(code: Integer).void } + attr_writer :code + + # Сообщение об ошибке. + sig { returns(T.nilable(String)) } + attr_reader :message + + sig { params(message: String).void } + attr_writer :message + + sig { params(code: Integer, message: String).returns(T.attached_class) } + def self.new(code: nil, message: nil) + end + + sig { override.returns({ code: Integer, message: String }) } + def to_hash + end + end + end +end diff --git a/rbi/docker_engine_ruby/models/image_id.rbi b/rbi/docker_engine_ruby/models/image_id.rbi new file mode 100644 index 0000000..86d0e48 --- /dev/null +++ b/rbi/docker_engine_ruby/models/image_id.rbi @@ -0,0 +1,26 @@ +# typed: strong + +module DockerEngineRuby + module Models + class ImageID < DockerEngineRuby::Internal::Type::BaseModel + OrHash = + T.type_alias do + T.any(DockerEngineRuby::ImageID, DockerEngineRuby::Internal::AnyHash) + end + + sig { returns(T.nilable(String)) } + attr_reader :id + + sig { params(id: String).void } + attr_writer :id + + sig { params(id: String).returns(T.attached_class) } + def self.new(id: nil) + end + + sig { override.returns({ id: String }) } + def to_hash + end + end + end +end diff --git a/rbi/docker_engine_ruby/models/progress_detail.rbi b/rbi/docker_engine_ruby/models/progress_detail.rbi new file mode 100644 index 0000000..8c47c2d --- /dev/null +++ b/rbi/docker_engine_ruby/models/progress_detail.rbi @@ -0,0 +1,35 @@ +# typed: strong + +module DockerEngineRuby + module Models + class ProgressDetail < DockerEngineRuby::Internal::Type::BaseModel + OrHash = + T.type_alias do + T.any( + DockerEngineRuby::ProgressDetail, + DockerEngineRuby::Internal::AnyHash + ) + end + + sig { returns(T.nilable(Integer)) } + attr_reader :current + + sig { params(current: Integer).void } + attr_writer :current + + sig { returns(T.nilable(Integer)) } + attr_reader :total + + sig { params(total: Integer).void } + attr_writer :total + + sig { params(current: Integer, total: Integer).returns(T.attached_class) } + def self.new(current: nil, total: nil) + end + + sig { override.returns({ current: Integer, total: Integer }) } + def to_hash + end + end + end +end diff --git a/rbi/docker_engine_ruby/models/push_image_info.rbi b/rbi/docker_engine_ruby/models/push_image_info.rbi new file mode 100644 index 0000000..c67cd9d --- /dev/null +++ b/rbi/docker_engine_ruby/models/push_image_info.rbi @@ -0,0 +1,140 @@ +# typed: strong + +module DockerEngineRuby + module Models + class PushImageInfo < DockerEngineRuby::Internal::Type::BaseModel + OrHash = + T.type_alias do + T.any( + DockerEngineRuby::PushImageInfo, + DockerEngineRuby::Internal::AnyHash + ) + end + + sig { returns(T.nilable(String)) } + attr_reader :id + + sig { params(id: String).void } + attr_writer :id + + # Устаревшее поле: то же, что `error_detail.message`. + sig { returns(T.nilable(String)) } + attr_reader :error + + sig { params(error: String).void } + attr_writer :error + + sig { returns(T.nilable(DockerEngineRuby::ErrorDetail)) } + attr_reader :error_detail + + sig { params(error_detail: DockerEngineRuby::ErrorDetail::OrHash).void } + attr_writer :error_detail + + sig { returns(T.nilable(String)) } + attr_reader :status + + sig { params(status: String).void } + attr_writer :status + + # Устаревшее поле: прогресс строкой. + sig { returns(T.nilable(String)) } + attr_reader :progress + + sig { params(progress: String).void } + attr_writer :progress + + sig { returns(T.nilable(DockerEngineRuby::ProgressDetail)) } + attr_reader :progress_detail + + sig do + params(progress_detail: DockerEngineRuby::ProgressDetail::OrHash).void + end + attr_writer :progress_detail + + # Итог публикации: тег, дайджест и размер. + sig { returns(T.nilable(DockerEngineRuby::PushImageInfo::Aux)) } + attr_reader :aux + + sig { params(aux: DockerEngineRuby::PushImageInfo::Aux::OrHash).void } + attr_writer :aux + + sig do + params( + id: String, + error: String, + error_detail: DockerEngineRuby::ErrorDetail::OrHash, + status: String, + progress: String, + progress_detail: DockerEngineRuby::ProgressDetail::OrHash, + aux: DockerEngineRuby::PushImageInfo::Aux::OrHash + ).returns(T.attached_class) + end + def self.new( + id: nil, + error: nil, + error_detail: nil, + status: nil, + progress: nil, + progress_detail: nil, + aux: nil + ) + end + + sig do + override.returns( + { + id: String, + error: String, + error_detail: DockerEngineRuby::ErrorDetail, + status: String, + progress: String, + progress_detail: DockerEngineRuby::ProgressDetail, + aux: DockerEngineRuby::PushImageInfo::Aux + } + ) + end + def to_hash + end + + class Aux < DockerEngineRuby::Internal::Type::BaseModel + OrHash = + T.type_alias do + T.any( + DockerEngineRuby::PushImageInfo::Aux, + DockerEngineRuby::Internal::AnyHash + ) + end + + sig { returns(T.nilable(String)) } + attr_reader :tag + + sig { params(tag: String).void } + attr_writer :tag + + sig { returns(T.nilable(String)) } + attr_reader :digest + + sig { params(digest: String).void } + attr_writer :digest + + sig { returns(T.nilable(Integer)) } + attr_reader :size + + sig { params(size: Integer).void } + attr_writer :size + + sig do + params(tag: String, digest: String, size: Integer).returns( + T.attached_class + ) + end + def self.new(tag: nil, digest: nil, size: nil) + end + + sig { override.returns({ tag: String, digest: String, size: Integer }) } + def to_hash + end + end + end + end +end diff --git a/rbi/docker_engine_ruby/resources/images.rbi b/rbi/docker_engine_ruby/resources/images.rbi index 0e5c806..04ca88e 100644 --- a/rbi/docker_engine_ruby/resources/images.rbi +++ b/rbi/docker_engine_ruby/resources/images.rbi @@ -73,7 +73,9 @@ module DockerEngineRuby target: String, version: DockerEngineRuby::ImageBuildParams::Version::OrSymbol, x_registry_config: String, - request_options: DockerEngineRuby::RequestOptions::OrHash + request_options: DockerEngineRuby::RequestOptions::OrHash, + blk: + T.nilable(T.proc.params(message: DockerEngineRuby::BuildInfo).void) ).void end def build( @@ -131,7 +133,8 @@ module DockerEngineRuby version: nil, # Header param x_registry_config: nil, - request_options: {} + request_options: {}, + &blk ) end @@ -364,7 +367,11 @@ module DockerEngineRuby repo: String, tag: String, x_registry_auth: String, - request_options: DockerEngineRuby::RequestOptions::OrHash + request_options: DockerEngineRuby::RequestOptions::OrHash, + blk: + T.nilable( + T.proc.params(message: DockerEngineRuby::CreateImageInfo).void + ) ).void end def pull( @@ -386,7 +393,8 @@ module DockerEngineRuby tag: nil, # Header param x_registry_auth: nil, - request_options: {} + request_options: {}, + &blk ) end @@ -397,7 +405,11 @@ module DockerEngineRuby x_registry_auth: String, platform: String, tag: String, - request_options: DockerEngineRuby::RequestOptions::OrHash + request_options: DockerEngineRuby::RequestOptions::OrHash, + blk: + T.nilable( + T.proc.params(message: DockerEngineRuby::PushImageInfo).void + ) ).void end def push( @@ -409,7 +421,8 @@ module DockerEngineRuby platform: nil, # Query param tag: nil, - request_options: {} + request_options: {}, + &blk ) end diff --git a/sig/docker_engine_ruby/errors.rbs b/sig/docker_engine_ruby/errors.rbs index 8f536d1..1bde32c 100644 --- a/sig/docker_engine_ruby/errors.rbs +++ b/sig/docker_engine_ruby/errors.rbs @@ -16,6 +16,15 @@ module DockerEngineRuby ) -> void end + class StreamError < DockerEngineRuby::Errors::Error + attr_reader detail: DockerEngineRuby::Models::ErrorDetail? + + def initialize: ( + String message, + ?detail: DockerEngineRuby::Models::ErrorDetail? + ) -> void + end + class APIError < DockerEngineRuby::Errors::Error attr_accessor url: URI::Generic diff --git a/sig/docker_engine_ruby/internal/jsonl_stream.rbs b/sig/docker_engine_ruby/internal/jsonl_stream.rbs new file mode 100644 index 0000000..e8b4efd --- /dev/null +++ b/sig/docker_engine_ruby/internal/jsonl_stream.rbs @@ -0,0 +1,19 @@ +module DockerEngineRuby + module Internal + class JSONLStream[Elem] + include Enumerable[Elem] + + def initialize: ( + model: top, + url: URI::Generic, + status: Integer, + headers: ::Hash[String, String], + response: top, + unwrap: top, + stream: Enumerable[String] + ) -> void + + def each: { (Elem message) -> void } -> void + end + end +end diff --git a/sig/docker_engine_ruby/models.rbs b/sig/docker_engine_ruby/models.rbs index 9236511..af179cc 100644 --- a/sig/docker_engine_ruby/models.rbs +++ b/sig/docker_engine_ruby/models.rbs @@ -288,4 +288,16 @@ module DockerEngineRuby class VolumeUpdateParams = DockerEngineRuby::Models::VolumeUpdateParams class WaitResponse = DockerEngineRuby::Models::WaitResponse + + class BuildInfo = DockerEngineRuby::Models::BuildInfo + + class CreateImageInfo = DockerEngineRuby::Models::CreateImageInfo + + class ErrorDetail = DockerEngineRuby::Models::ErrorDetail + + class ImageID = DockerEngineRuby::Models::ImageID + + class ProgressDetail = DockerEngineRuby::Models::ProgressDetail + + class PushImageInfo = DockerEngineRuby::Models::PushImageInfo end diff --git a/sig/docker_engine_ruby/models/build_info.rbs b/sig/docker_engine_ruby/models/build_info.rbs new file mode 100644 index 0000000..44315bc --- /dev/null +++ b/sig/docker_engine_ruby/models/build_info.rbs @@ -0,0 +1,85 @@ +module DockerEngineRuby + module Models + type build_info = + { + id: String, + stream: String, + error: String, + error_detail: DockerEngineRuby::ErrorDetail, + status: String, + progress: String, + progress_detail: DockerEngineRuby::ProgressDetail, + aux: DockerEngineRuby::Models::BuildInfo::aux + } + + class BuildInfo < DockerEngineRuby::Internal::Type::BaseModel + attr_reader id: String? + + def id=: (String) -> String + + attr_reader stream: String? + + def stream=: (String) -> String + + attr_reader error: String? + + def error=: (String) -> String + + attr_reader error_detail: DockerEngineRuby::ErrorDetail? + + def error_detail=: ( + DockerEngineRuby::ErrorDetail + ) -> DockerEngineRuby::ErrorDetail + + attr_reader status: String? + + def status=: (String) -> String + + attr_reader progress: String? + + def progress=: (String) -> String + + attr_reader progress_detail: DockerEngineRuby::ProgressDetail? + + def progress_detail=: ( + DockerEngineRuby::ProgressDetail + ) -> DockerEngineRuby::ProgressDetail + + attr_reader aux: DockerEngineRuby::Models::BuildInfo::aux? + + def aux=: ( + DockerEngineRuby::Models::BuildInfo::aux + ) -> DockerEngineRuby::Models::BuildInfo::aux + + def initialize: ( + ?id: String, + ?stream: String, + ?error: String, + ?error_detail: DockerEngineRuby::ErrorDetail, + ?status: String, + ?progress: String, + ?progress_detail: DockerEngineRuby::ProgressDetail, + ?aux: DockerEngineRuby::Models::BuildInfo::aux + ) -> void + + def to_hash: -> { + id: String, + stream: String, + error: String, + error_detail: DockerEngineRuby::ErrorDetail, + status: String, + progress: String, + progress_detail: DockerEngineRuby::ProgressDetail, + aux: DockerEngineRuby::Models::BuildInfo::aux + } + + type aux = DockerEngineRuby::ImageID | String + + module Aux + extend DockerEngineRuby::Internal::Type::Union + + def self?.variants: -> ::Array[DockerEngineRuby::Models::BuildInfo::aux] + end + end + end +end diff --git a/sig/docker_engine_ruby/models/create_image_info.rbs b/sig/docker_engine_ruby/models/create_image_info.rbs new file mode 100644 index 0000000..8c27452 --- /dev/null +++ b/sig/docker_engine_ruby/models/create_image_info.rbs @@ -0,0 +1,61 @@ +module DockerEngineRuby + module Models + type create_image_info = + { + id: String, + error: String, + error_detail: DockerEngineRuby::ErrorDetail, + status: String, + progress: String, + progress_detail: DockerEngineRuby::ProgressDetail + } + + class CreateImageInfo < DockerEngineRuby::Internal::Type::BaseModel + attr_reader id: String? + + def id=: (String) -> String + + attr_reader error: String? + + def error=: (String) -> String + + attr_reader error_detail: DockerEngineRuby::ErrorDetail? + + def error_detail=: ( + DockerEngineRuby::ErrorDetail + ) -> DockerEngineRuby::ErrorDetail + + attr_reader status: String? + + def status=: (String) -> String + + attr_reader progress: String? + + def progress=: (String) -> String + + attr_reader progress_detail: DockerEngineRuby::ProgressDetail? + + def progress_detail=: ( + DockerEngineRuby::ProgressDetail + ) -> DockerEngineRuby::ProgressDetail + + def initialize: ( + ?id: String, + ?error: String, + ?error_detail: DockerEngineRuby::ErrorDetail, + ?status: String, + ?progress: String, + ?progress_detail: DockerEngineRuby::ProgressDetail + ) -> void + + def to_hash: -> { + id: String, + error: String, + error_detail: DockerEngineRuby::ErrorDetail, + status: String, + progress: String, + progress_detail: DockerEngineRuby::ProgressDetail + } + end + end +end diff --git a/sig/docker_engine_ruby/models/error_detail.rbs b/sig/docker_engine_ruby/models/error_detail.rbs new file mode 100644 index 0000000..95acc50 --- /dev/null +++ b/sig/docker_engine_ruby/models/error_detail.rbs @@ -0,0 +1,19 @@ +module DockerEngineRuby + module Models + type error_detail = { code: Integer, message: String } + + class ErrorDetail < DockerEngineRuby::Internal::Type::BaseModel + attr_reader code: Integer? + + def code=: (Integer) -> Integer + + attr_reader message: String? + + def message=: (String) -> String + + def initialize: (?code: Integer, ?message: String) -> void + + def to_hash: -> { code: Integer, message: String } + end + end +end diff --git a/sig/docker_engine_ruby/models/image_id.rbs b/sig/docker_engine_ruby/models/image_id.rbs new file mode 100644 index 0000000..65e3c65 --- /dev/null +++ b/sig/docker_engine_ruby/models/image_id.rbs @@ -0,0 +1,15 @@ +module DockerEngineRuby + module Models + type image_id = { id: String } + + class ImageID < DockerEngineRuby::Internal::Type::BaseModel + attr_reader id: String? + + def id=: (String) -> String + + def initialize: (?id: String) -> void + + def to_hash: -> { id: String } + end + end +end diff --git a/sig/docker_engine_ruby/models/progress_detail.rbs b/sig/docker_engine_ruby/models/progress_detail.rbs new file mode 100644 index 0000000..464b159 --- /dev/null +++ b/sig/docker_engine_ruby/models/progress_detail.rbs @@ -0,0 +1,19 @@ +module DockerEngineRuby + module Models + type progress_detail = { current: Integer, total: Integer } + + class ProgressDetail < DockerEngineRuby::Internal::Type::BaseModel + attr_reader current: Integer? + + def current=: (Integer) -> Integer + + attr_reader total: Integer? + + def total=: (Integer) -> Integer + + def initialize: (?current: Integer, ?total: Integer) -> void + + def to_hash: -> { current: Integer, total: Integer } + end + end +end diff --git a/sig/docker_engine_ruby/models/push_image_info.rbs b/sig/docker_engine_ruby/models/push_image_info.rbs new file mode 100644 index 0000000..c101630 --- /dev/null +++ b/sig/docker_engine_ruby/models/push_image_info.rbs @@ -0,0 +1,90 @@ +module DockerEngineRuby + module Models + type push_image_info = + { + id: String, + error: String, + error_detail: DockerEngineRuby::ErrorDetail, + status: String, + progress: String, + progress_detail: DockerEngineRuby::ProgressDetail, + aux: DockerEngineRuby::PushImageInfo::Aux + } + + class PushImageInfo < DockerEngineRuby::Internal::Type::BaseModel + attr_reader id: String? + + def id=: (String) -> String + + attr_reader error: String? + + def error=: (String) -> String + + attr_reader error_detail: DockerEngineRuby::ErrorDetail? + + def error_detail=: ( + DockerEngineRuby::ErrorDetail + ) -> DockerEngineRuby::ErrorDetail + + attr_reader status: String? + + def status=: (String) -> String + + attr_reader progress: String? + + def progress=: (String) -> String + + attr_reader progress_detail: DockerEngineRuby::ProgressDetail? + + def progress_detail=: ( + DockerEngineRuby::ProgressDetail + ) -> DockerEngineRuby::ProgressDetail + + attr_reader aux: DockerEngineRuby::PushImageInfo::Aux? + + def aux=: ( + DockerEngineRuby::PushImageInfo::Aux + ) -> DockerEngineRuby::PushImageInfo::Aux + + def initialize: ( + ?id: String, + ?error: String, + ?error_detail: DockerEngineRuby::ErrorDetail, + ?status: String, + ?progress: String, + ?progress_detail: DockerEngineRuby::ProgressDetail, + ?aux: DockerEngineRuby::PushImageInfo::Aux + ) -> void + + def to_hash: -> { + id: String, + error: String, + error_detail: DockerEngineRuby::ErrorDetail, + status: String, + progress: String, + progress_detail: DockerEngineRuby::ProgressDetail, + aux: DockerEngineRuby::PushImageInfo::Aux + } + + type aux = { tag: String, digest: String, size: Integer } + + class Aux < DockerEngineRuby::Internal::Type::BaseModel + attr_reader tag: String? + + def tag=: (String) -> String + + attr_reader digest: String? + + def digest=: (String) -> String + + attr_reader size: Integer? + + def size=: (Integer) -> Integer + + def initialize: (?tag: String, ?digest: String, ?size: Integer) -> void + + def to_hash: -> { tag: String, digest: String, size: Integer } + end + end + end +end diff --git a/sig/docker_engine_ruby/resources/images.rbs b/sig/docker_engine_ruby/resources/images.rbs index 81c0eef..492010c 100644 --- a/sig/docker_engine_ruby/resources/images.rbs +++ b/sig/docker_engine_ruby/resources/images.rbs @@ -47,7 +47,9 @@ module DockerEngineRuby ?version: DockerEngineRuby::Models::ImageBuildParams::version, ?x_registry_config: String, ?request_options: DockerEngineRuby::request_opts - ) -> nil + ) ?{ + (DockerEngineRuby::BuildInfo message) -> void + } -> nil def build_prune: ( ?all: bool, @@ -140,7 +142,9 @@ module DockerEngineRuby ?tag: String, ?x_registry_auth: String, ?request_options: DockerEngineRuby::request_opts - ) -> nil + ) ?{ + (DockerEngineRuby::CreateImageInfo message) -> void + } -> nil def push: ( String name, @@ -148,7 +152,9 @@ module DockerEngineRuby ?platform: String, ?tag: String, ?request_options: DockerEngineRuby::request_opts - ) -> nil + ) ?{ + (DockerEngineRuby::PushImageInfo message) -> void + } -> nil def search: ( term: String, diff --git a/test/docker_engine_ruby/helpers/buildkit_status_test.rb b/test/docker_engine_ruby/helpers/buildkit_status_test.rb new file mode 100644 index 0000000..2bebf1c --- /dev/null +++ b/test/docker_engine_ruby/helpers/buildkit_status_test.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require_relative "../test_helper" +require_relative "buildkit_trace_fixture" + +class DockerEngineRuby::Test::BuildkitStatusTest < Minitest::Test + def test_plain_printer_renders_steps_logs_and_errors + lines = [] + printer = DockerEngineRuby::Helpers::BuildkitStatus::PlainPrinter.new { lines << _1 } + BUILDKIT_TRACE.each { printer.print(DockerEngineRuby::Helpers::BuildkitStatus.decode(_1)) } + + assert_includes(lines.join("\n"), "[2/3] RUN echo line-one && echo line-two") + assert(lines.any? { _1.end_with?(" line-one") }) + assert(lines.any? { _1.end_with?(" line-two") }) + assert( + lines.any? do + _1.include?(%(ERROR: process "/bin/sh -c exit 3" did not complete successfully: exit code: 3)) + end + ) + end +end diff --git a/test/docker_engine_ruby/helpers/buildkit_trace_fixture.rb b/test/docker_engine_ruby/helpers/buildkit_trace_fixture.rb new file mode 100644 index 0000000..72e29c7 --- /dev/null +++ b/test/docker_engine_ruby/helpers/buildkit_trace_fixture.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +# Captured from Docker 29.8 building `RUN echo line-one && echo line-two` then `RUN exit 3` with BuildKit. +BUILDKIT_TRACE = %w[ + Cm8KR3NoYTI1NjowNjI2YTc2MmNhZDA0YTY2YTA0NTA5NTIyMjU3MzI5YTkyMTYzMTk2MGM1ZTA0YmE3MzIzMDM5ZTZiN2Q2YWUzGiRbaW50ZXJuYWxdIGxvYWQgcmVtb3RlIGJ1aWxkIGNvbnRleHQ= + Cn0KR3NoYTI1NjowNjI2YTc2MmNhZDA0YTY2YTA0NTA5NTIyMjU3MzI5YTkyMTYzMTk2MGM1ZTA0YmE3MzIzMDM5ZTZiN2Q2YWUzGiRbaW50ZXJuYWxdIGxvYWQgcmVtb3RlIGJ1aWxkIGNvbnRleHQqDAiM6eXVBhCuzJPoAQ== + CosBCkdzaGEyNTY6MDYyNmE3NjJjYWQwNGE2NmEwNDUwOTUyMjI1NzMyOWE5MjE2MzE5NjBjNWUwNGJhNzMyMzAzOWU2YjdkNmFlMxokW2ludGVybmFsXSBsb2FkIHJlbW90ZSBidWlsZCBjb250ZXh0KgwIjOnl1QYQrsyT6AEyDAiM6eXVBhCe/7OFAg== + CosBCkdzaGEyNTY6MDYyNmE3NjJjYWQwNGE2NmEwNDUwOTUyMjI1NzMyOWE5MjE2MzE5NjBjNWUwNGJhNzMyMzAzOWU2YjdkNmFlMxokW2ludGVybmFsXSBsb2FkIHJlbW90ZSBidWlsZCBjb250ZXh0KgwIjOnl1QYQxZXShQIyDAiM6eXVBhCAj9eFAg== + CqMBCkdzaGEyNTY6Zjg3Mjg1NDMzZmIyY2I0NGZkMjc4MDVlOGI4MjBhMDIxN2ZjNTI2ZjQ0YzAyMjYyOWJkMWE2NDY1ZGQ1MzM4MxJHc2hhMjU2OjA2MjZhNzYyY2FkMDRhNjZhMDQ1MDk1MjIyNTczMjlhOTIxNjMxOTYwYzVlMDRiYTczMjMwMzllNmI3ZDZhZTMaD2NvcHkgL2NvbnRleHQgLw== + CrEBCkdzaGEyNTY6Zjg3Mjg1NDMzZmIyY2I0NGZkMjc4MDVlOGI4MjBhMDIxN2ZjNTI2ZjQ0YzAyMjYyOWJkMWE2NDY1ZGQ1MzM4MxJHc2hhMjU2OjA2MjZhNzYyY2FkMDRhNjZhMDQ1MDk1MjIyNTczMjlhOTIxNjMxOTYwYzVlMDRiYTczMjMwMzllNmI3ZDZhZTMaD2NvcHkgL2NvbnRleHQgLyoMCIzp5dUGEMvmkZMC + Cr8BCkdzaGEyNTY6Zjg3Mjg1NDMzZmIyY2I0NGZkMjc4MDVlOGI4MjBhMDIxN2ZjNTI2ZjQ0YzAyMjYyOWJkMWE2NDY1ZGQ1MzM4MxJHc2hhMjU2OjA2MjZhNzYyY2FkMDRhNjZhMDQ1MDk1MjIyNTczMjlhOTIxNjMxOTYwYzVlMDRiYTczMjMwMzllNmI3ZDZhZTMaD2NvcHkgL2NvbnRleHQgLyoMCIzp5dUGEMvmkZMCMgwIjOnl1QYQyOGAsgI= + CpMBCkdzaGEyNTY6ZmRlMGU0ZjRhMTA3NzQwNjUxMjNkYzhhYzE2ZDBjZjllMmYwMmRlMmRiZWNiOGY4ZTllOTBhY2QyMjFkZjliNBo6W2ludGVybmFsXSBsb2FkIG1ldGFkYXRhIGZvciBkb2NrZXIuaW8vbGlicmFyeS9hbHBpbmU6My4yMCoMCIzp5dUGEN3sw7cC + CqEBCkdzaGEyNTY6ZmRlMGU0ZjRhMTA3NzQwNjUxMjNkYzhhYzE2ZDBjZjllMmYwMmRlMmRiZWNiOGY4ZTllOTBhY2QyMjFkZjliNBo6W2ludGVybmFsXSBsb2FkIG1ldGFkYXRhIGZvciBkb2NrZXIuaW8vbGlicmFyeS9hbHBpbmU6My4yMCoMCIzp5dUGEN3sw7cCMgwIjOnl1QYQquW02QM= + CqQBCkdzaGEyNTY6ZmQxMGI0OTYzMjQ2NTY0NWExN2M2MDNjMDhhZGFhZWJjYTUwMjY0ZTZjNzNhYWM4ODhmMGEyMTcwYmY5ZmFjYxJHc2hhMjU2OjI0YTA1ZDgzZmZjYzVjMTg4YTdhOTBlYzg1ZjU0OWM4YzMzZDgyNGI5OGIxOTQwYjZhMzM0NjcyOTk0NDBhNWMaEFszLzNdIFJVTiBleGl0IDMKvAEKR3NoYTI1NjoyNGEwNWQ4M2ZmY2M1YzE4OGE3YTkwZWM4NWY1NDljOGMzM2Q4MjRiOThiMTk0MGI2YTMzNDY3Mjk5NDQwYTVjEkdzaGEyNTY6NjBmODA1YmU4MTRkOTUwZGJlNDZkNDYwZjJjOTZmYjlkOTc2OThiZGU2YTdlYmJkZDQ5OWViMTJiN2UwZDYxORooWzIvM10gUlVOIGVjaG8gbGluZS1vbmUgJiYgZWNobyBsaW5lLXR3bwq7AQpHc2hhMjU2OjYwZjgwNWJlODE0ZDk1MGRiZTQ2ZDQ2MGYyYzk2ZmI5ZDk3Njk4YmRlNmE3ZWJiZGQ0OTllYjEyYjdlMGQ2MTkacFsxLzNdIEZST00gZG9ja2VyLmlvL2xpYnJhcnkvYWxwaW5lOjMuMjBAc2hhMjU2OmQ5ZTg1M2U4N2U1NTUyNmY2YjI5MTdkZjkxYTIxMTVjMzZkZDdjNjk2YTM1YmUxMjE2M2Q0NGU2ZTJhNGI2YmM= + CskBCkdzaGEyNTY6NjBmODA1YmU4MTRkOTUwZGJlNDZkNDYwZjJjOTZmYjlkOTc2OThiZGU2YTdlYmJkZDQ5OWViMTJiN2UwZDYxORpwWzEvM10gRlJPTSBkb2NrZXIuaW8vbGlicmFyeS9hbHBpbmU6My4yMEBzaGEyNTY6ZDllODUzZTg3ZTU1NTI2ZjZiMjkxN2RmOTFhMjExNWMzNmRkN2M2OTZhMzViZTEyMTYzZDQ0ZTZlMmE0YjZiYyoMCIzp5dUGEJXPjtsD + EtQBCm1yZXNvbHZlIGRvY2tlci5pby9saWJyYXJ5L2FscGluZTozLjIwQHNoYTI1NjpkOWU4NTNlODdlNTU1MjZmNmIyOTE3ZGY5MWEyMTE1YzM2ZGQ3YzY5NmEzNWJlMTIxNjNkNDRlNmUyYTRiNmJjEkdzaGEyNTY6NjBmODA1YmU4MTRkOTUwZGJlNDZkNDYwZjJjOTZmYjlkOTc2OThiZGU2YTdlYmJkZDQ5OWViMTJiN2UwZDYxOTIMCIzp5dUGELuFstwDOgwIjOnl1QYQyoCy3AM= + EuABCm1yZXNvbHZlIGRvY2tlci5pby9saWJyYXJ5L2FscGluZTozLjIwQHNoYTI1NjpkOWU4NTNlODdlNTU1MjZmNmIyOTE3ZGY5MWEyMTE1YzM2ZGQ3YzY5NmEzNWJlMTIxNjNkNDRlNmUyYTRiNmJjEkdzaGEyNTY6NjBmODA1YmU4MTRkOTUwZGJlNDZkNDYwZjJjOTZmYjlkOTc2OThiZGU2YTdlYmJkZDQ5OWViMTJiN2UwZDYxOTILCI3p5dUGEI3yswo6DAiM6eXVBhDKgLLcA0ILCI3p5dUGEPXrswo= + CtYBCkdzaGEyNTY6NjBmODA1YmU4MTRkOTUwZGJlNDZkNDYwZjJjOTZmYjlkOTc2OThiZGU2YTdlYmJkZDQ5OWViMTJiN2UwZDYxORpwWzEvM10gRlJPTSBkb2NrZXIuaW8vbGlicmFyeS9hbHBpbmU6My4yMEBzaGEyNTY6ZDllODUzZTg3ZTU1NTI2ZjZiMjkxN2RmOTFhMjExNWMzNmRkN2M2OTZhMzViZTEyMTYzZDQ0ZTZlMmE0YjZiYyoMCIzp5dUGEJXPjtsDMgsIjenl1QYQprm6Cg== + CsoBCkdzaGEyNTY6NjBmODA1YmU4MTRkOTUwZGJlNDZkNDYwZjJjOTZmYjlkOTc2OThiZGU2YTdlYmJkZDQ5OWViMTJiN2UwZDYxORpwWzEvM10gRlJPTSBkb2NrZXIuaW8vbGlicmFyeS9hbHBpbmU6My4yMEBzaGEyNTY6ZDllODUzZTg3ZTU1NTI2ZjZiMjkxN2RmOTFhMjExNWMzNmRkN2M2OTZhMzViZTEyMTYzZDQ0ZTZlMmE0YjZiYyABKgsIjenl1QYQiaCLCw== + CtcBCkdzaGEyNTY6NjBmODA1YmU4MTRkOTUwZGJlNDZkNDYwZjJjOTZmYjlkOTc2OThiZGU2YTdlYmJkZDQ5OWViMTJiN2UwZDYxORpwWzEvM10gRlJPTSBkb2NrZXIuaW8vbGlicmFyeS9hbHBpbmU6My4yMEBzaGEyNTY6ZDllODUzZTg3ZTU1NTI2ZjZiMjkxN2RmOTFhMjExNWMzNmRkN2M2OTZhMzViZTEyMTYzZDQ0ZTZlMmE0YjZiYyABKgsIjenl1QYQiaCLCzILCI3p5dUGENGY2Qs= + CskBCkdzaGEyNTY6MjRhMDVkODNmZmNjNWMxODhhN2E5MGVjODVmNTQ5YzhjMzNkODI0Yjk4YjE5NDBiNmEzMzQ2NzI5OTQ0MGE1YxJHc2hhMjU2OjYwZjgwNWJlODE0ZDk1MGRiZTQ2ZDQ2MGYyYzk2ZmI5ZDk3Njk4YmRlNmE3ZWJiZGQ0OTllYjEyYjdlMGQ2MTkaKFsyLzNdIFJVTiBlY2hvIGxpbmUtb25lICYmIGVjaG8gbGluZS10d28qCwiN6eXVBhD9rvsL + Gm0KR3NoYTI1NjoyNGEwNWQ4M2ZmY2M1YzE4OGE3YTkwZWM4NWY1NDljOGMzM2Q4MjRiOThiMTk0MGI2YTMzNDY3Mjk5NDQwYTVjEgwIjenl1QYQ3qnLyAEYASISbGluZS1vbmUKbGluZS10d28K + CtcBCkdzaGEyNTY6MjRhMDVkODNmZmNjNWMxODhhN2E5MGVjODVmNTQ5YzhjMzNkODI0Yjk4YjE5NDBiNmEzMzQ2NzI5OTQ0MGE1YxJHc2hhMjU2OjYwZjgwNWJlODE0ZDk1MGRiZTQ2ZDQ2MGYyYzk2ZmI5ZDk3Njk4YmRlNmE3ZWJiZGQ0OTllYjEyYjdlMGQ2MTkaKFsyLzNdIFJVTiBlY2hvIGxpbmUtb25lICYmIGVjaG8gbGluZS10d28qCwiN6eXVBhD9rvsLMgwIjenl1QYQ/L3a3QE= + CrIBCkdzaGEyNTY6ZmQxMGI0OTYzMjQ2NTY0NWExN2M2MDNjMDhhZGFhZWJjYTUwMjY0ZTZjNzNhYWM4ODhmMGEyMTcwYmY5ZmFjYxJHc2hhMjU2OjI0YTA1ZDgzZmZjYzVjMTg4YTdhOTBlYzg1ZjU0OWM4YzMzZDgyNGI5OGIxOTQwYjZhMzM0NjcyOTk0NDBhNWMaEFszLzNdIFJVTiBleGl0IDMqDAiN6eXVBhDh+uvhAQ== + CokCCkdzaGEyNTY6ZmQxMGI0OTYzMjQ2NTY0NWExN2M2MDNjMDhhZGFhZWJjYTUwMjY0ZTZjNzNhYWM4ODhmMGEyMTcwYmY5ZmFjYxJHc2hhMjU2OjI0YTA1ZDgzZmZjYzVjMTg4YTdhOTBlYzg1ZjU0OWM4YzMzZDgyNGI5OGIxOTQwYjZhMzM0NjcyOTk0NDBhNWMaEFszLzNdIFJVTiBleGl0IDMqDAiN6eXVBhDh+uvhATIMCI3p5dUGEN2czPYCOkdwcm9jZXNzICIvYmluL3NoIC1jIGV4aXQgMyIgZGlkIG5vdCBjb21wbGV0ZSBzdWNjZXNzZnVsbHk6IGV4aXQgY29kZTogMw== +].freeze diff --git a/test/docker_engine_ruby/models/progress_stream_test.rb b/test/docker_engine_ruby/models/progress_stream_test.rb new file mode 100644 index 0000000..7f4d138 --- /dev/null +++ b/test/docker_engine_ruby/models/progress_stream_test.rb @@ -0,0 +1,91 @@ +# frozen_string_literal: true + +require_relative "../test_helper" + +class DockerEngineRuby::Test::ProgressStreamModelTest < Minitest::Test + extend Minitest::Serial + include WebMock::API + + def before_all + super + WebMock.enable! + end + + def teardown + WebMock.reset! + super + end + + def after_all + WebMock.disable! + super + end + + def test_build_yields_each_progress_message + stub_stream( + :post, + "http://localhost/build", + [ + {stream: "Step 1/1 : FROM alpine"}, + {aux: {ID: "sha256:abc"}}, + {stream: "Successfully built abc\n"} + ] + ) + messages = [] + + client.images.build(body: "tar") { messages << _1 } + + assert_equal(["Step 1/1 : FROM alpine", nil, "Successfully built abc\n"], messages.map(&:stream)) + assert_equal("sha256:abc", messages[1].aux.id) + end + + def test_build_raises_on_error_inside_the_stream + stub_stream( + :post, + "http://localhost/build", + [ + {stream: "Step 2/2 : RUN exit 3"}, + {error: "exit code: 3", errorDetail: {code: 3, message: "exit code: 3"}} + ] + ) + + error = assert_raises(DockerEngineRuby::Errors::StreamError) { client.images.build(body: "tar") } + + assert_equal("exit code: 3", error.message) + assert_equal(3, error.detail&.code) + end + + def test_pull_without_block_reads_the_stream_to_the_end + stub_stream( + :post, + "http://localhost/images/create?fromImage=alpine", + [ + {status: "Pulling from library/alpine", id: "3.20"}, + {status: "Status: Image is up to date for alpine:3.20"} + ] + ) + + assert_nil(client.images.pull(body: "", from_image: "alpine")) + end + + def test_aux_is_read_as_the_variant_that_fits + trace = DockerEngineRuby::Internal::Type::Converter.coerce(DockerEngineRuby::BuildInfo, {aux: "c3RhdHVz"}) + image = DockerEngineRuby::Internal::Type::Converter.coerce(DockerEngineRuby::BuildInfo, {aux: {ID: "sha256:abc"}}) + + assert_equal("c3RhdHVz", trace.aux) + assert_equal("sha256:abc", image.aux.id) + end + + private + + def client = DockerEngineRuby::Client.new(base_url: "http://localhost", max_retries: 0) + + def stub_stream(method, url, messages) + body = messages.map { "#{JSON.generate(_1)}\r\n" }.join + stub_request(method, url).to_return( + status: 200, + headers: {"Content-Type" => "application/json"}, + body: body + ) + end +end