diff --git a/src/docker/containers.v b/src/docker/containers.v index ff48c698..05c9cc7f 100644 --- a/src/docker/containers.v +++ b/src/docker/containers.v @@ -14,6 +14,7 @@ struct Container { names []string [json: Names] } +// containers returns a list of all containers. pub fn (mut d DockerDaemon) containers() ?[]Container { d.send_request('GET', urllib.parse('/v1.41/containers/json')?)? head, res := d.read_response()? @@ -45,6 +46,7 @@ pub: warnings []string [json: Warnings] } +// create_container creates a new container with the given config. pub fn (mut d DockerDaemon) create_container(c NewContainer) ?CreatedContainer { d.send_request_with_json('POST', urllib.parse('/v1.41/containers/create')?, c)? head, res := d.read_response()? @@ -60,6 +62,7 @@ pub fn (mut d DockerDaemon) create_container(c NewContainer) ?CreatedContainer { return data } +// start_container starts the container with the given id. pub fn (mut d DockerDaemon) start_container(id string) ? { d.send_request('POST', urllib.parse('/v1.41/containers/$id/start')?)? head, body := d.read_response()? @@ -109,6 +112,7 @@ pub mut: end_time time.Time [skip] } +// inspect_container returns detailed information for a given container. pub fn (mut d DockerDaemon) inspect_container(id string) ?ContainerInspect { d.send_request('GET', urllib.parse('/v1.41/containers/$id/json')?)? head, body := d.read_response()? @@ -150,6 +154,7 @@ pub fn inspect_container(id string) ?ContainerInspect { return data } +// remove_container removes the container with the given id. pub fn (mut d DockerDaemon) remove_container(id string) ? { d.send_request('DELETE', urllib.parse('/v1.41/containers/$id')?)? head, body := d.read_response()? @@ -168,6 +173,8 @@ pub fn remove_container(id string) ?bool { return res.status_code == 204 } +// get_container_logs returns a reader object allowing access to the +// container's logs. pub fn (mut d DockerDaemon) get_container_logs(id string) ?&StreamFormatReader { d.send_request('GET', urllib.parse('/v1.41/containers/$id/logs?stdout=true&stderr=true')?)? head := d.read_response_head()? diff --git a/src/docker/images.v b/src/docker/images.v index 22491139..974b4083 100644 --- a/src/docker/images.v +++ b/src/docker/images.v @@ -9,6 +9,7 @@ pub: id string [json: Id] } +// pull_image pulls the given image:tag. pub fn (mut d DockerDaemon) pull_image(image string, tag string) ? { d.send_request('POST', urllib.parse('/v1.41/images/create?fromImage=$image&tag=$tag')?)? head := d.read_response_head()? diff --git a/src/docker/socket.v b/src/docker/socket.v index f6dfeb1c..b1e30800 100644 --- a/src/docker/socket.v +++ b/src/docker/socket.v @@ -21,6 +21,7 @@ mut: reader &io.BufferedReader } +// new_conn creates a new connection to the Docker daemon. pub fn new_conn() ?&DockerDaemon { s := unix.connect_stream(docker.socket)? @@ -32,18 +33,22 @@ pub fn new_conn() ?&DockerDaemon { return d } +// send_request sends an HTTP request without body. pub fn (mut d DockerDaemon) send_request(method string, url urllib.URL) ? { req := '$method $url.request_uri() HTTP/1.1\nHost: localhost\n\n' d.socket.write_string(req)? } +// send_request_with_body sends an HTTP request with the given body. pub fn (mut d DockerDaemon) send_request_with_body(method string, url urllib.URL, content_type string, body string) ? { req := '$method $url.request_uri() HTTP/1.1\nHost: localhost\nContent-Type: $content_type\nContent-Length: $body.len\n\n$body\n\n' d.socket.write_string(req)? } +// send_request_with_json is a convenience wrapper around +// send_request_with_body that encodes the input as JSON. pub fn (mut d DockerDaemon) send_request_with_json(method string, url urllib.URL, data &T) ? { body := json.encode(data) @@ -52,8 +57,8 @@ pub fn (mut d DockerDaemon) send_request_with_json(method string, url urllib. // read_response_head consumes the socket's contents until it encounters // '\r\n\r\n', after which it parses the response as an HTTP response. -// Importantly, this function never consumes past the HTTP separator, so the -// body can be read fully later on. +// Importantly, this function never consumes the reader past the HTTP +// separator, so the body can be read fully later on. pub fn (mut d DockerDaemon) read_response_head() ?http.Response { mut c := 0 mut buf := []u8{len: 4} @@ -64,21 +69,12 @@ pub fn (mut d DockerDaemon) read_response_head() ?http.Response { res << buf[..c] match_len := util.match_array_in_array(buf[..c], docker.http_separator) - // mut i := 0 - // mut match_len := 0 - - // for i + match_len < c { - // if buf[i + match_len] == docker.http_separator[match_len] { - // match_len += 1 - // } else { - // i += match_len + 1 - // match_len = 0 - // } - //} if match_len == 4 { break - } else if match_len > 0 { + } + + if match_len > 0 { mut buf2 := []u8{len: 4 - match_len} c2 := d.reader.read(mut buf2)? res << buf2[..c2] @@ -92,6 +88,8 @@ pub fn (mut d DockerDaemon) read_response_head() ?http.Response { return http.parse_response(res.bytestr()) } +// read_response_body reads `length` bytes from the stream. It can be used when +// the response encoding isn't chunked to fully read it. pub fn (mut d DockerDaemon) read_response_body(length int) ?string { if length == 0 { return '' @@ -110,6 +108,9 @@ pub fn (mut d DockerDaemon) read_response_body(length int) ?string { return builder.str() } +// read_response is a convenience function combining read_response_head & +// read_response_body. It can be used when you know for certain the response +// won't be chunked. pub fn (mut d DockerDaemon) read_response() ?(http.Response, string) { head := d.read_response_head()? content_length := head.header.get(http.CommonHeader.content_length)?.int() @@ -118,12 +119,16 @@ pub fn (mut d DockerDaemon) read_response() ?(http.Response, string) { return head, res } +// get_chunked_response_reader returns a ChunkedResponseReader using the socket +// as reader. pub fn (mut d DockerDaemon) get_chunked_response_reader() &ChunkedResponseReader { r := new_chunked_response_reader(d.reader) return r } +// get_stream_format_reader returns a StreamFormatReader using the socket as +// reader. pub fn (mut d DockerDaemon) get_stream_format_reader() &StreamFormatReader { r := new_chunked_response_reader(d.reader) r2 := new_stream_format_reader(r) diff --git a/src/docker/stream.v b/src/docker/stream.v index 8822fd3a..f20fe185 100644 --- a/src/docker/stream.v +++ b/src/docker/stream.v @@ -5,6 +5,8 @@ import util import encoding.binary import encoding.hex +// ChunkedResponseReader parses an underlying HTTP chunked response, exposing +// it as if it was a continuous stream of data. struct ChunkedResponseReader { mut: reader io.Reader @@ -13,6 +15,8 @@ mut: started bool } +// new_chunked_response_reader creates a new ChunkedResponseReader on the heap +// with the provided reader. pub fn new_chunked_response_reader(reader io.Reader) &ChunkedResponseReader { r := &ChunkedResponseReader{ reader: reader @@ -21,7 +25,7 @@ pub fn new_chunked_response_reader(reader io.Reader) &ChunkedResponseReader { return r } -// We satisfy the io.Reader interface +// read satisfies the io.Reader interface. pub fn (mut r ChunkedResponseReader) read(mut buf []u8) ?int { if r.end_of_stream { return none @@ -37,6 +41,8 @@ pub fn (mut r ChunkedResponseReader) read(mut buf []u8) ?int { mut c := 0 + // Make sure we don't read more than we can safely read. This is to avoid + // the underlying reader from becoming out of sync with our parsing: if buf.len > r.bytes_left_in_chunk { c = r.reader.read(mut buf[..r.bytes_left_in_chunk])? } else { @@ -48,6 +54,9 @@ pub fn (mut r ChunkedResponseReader) read(mut buf []u8) ?int { return c } +// read_chunk_size advances the reader & reads the size of the next HTTP chunk. +// This function should only be called if the previous chunk has been +// completely consumed. fn (mut r ChunkedResponseReader) read_chunk_size() ?u64 { mut buf := []u8{len: 2} mut res := []u8{} @@ -80,6 +89,7 @@ fn (mut r ChunkedResponseReader) read_chunk_size() ?u64 { } } + // The length of the next chunk is provided as a hexadecimal mut num_data := hex.decode(res#[..-2].bytestr())? for num_data.len < 8 { @@ -88,6 +98,8 @@ fn (mut r ChunkedResponseReader) read_chunk_size() ?u64 { num := binary.big_endian_u64(num_data) + // This only occurs for the very last chunk, which always reports a size of + // 0. if num == 0 { r.end_of_stream = true } @@ -95,16 +107,17 @@ fn (mut r ChunkedResponseReader) read_chunk_size() ?u64 { return num } +// StreamFormatReader parses an underlying stream of Docker logs, removing the +// header bytes. struct StreamFormatReader { - stdout bool - stderr bool - stdin bool mut: reader io.Reader bytes_left_in_chunk u32 end_of_stream bool } +// new_stream_format_reader creates a new StreamFormatReader using the given +// reader. pub fn new_stream_format_reader(reader io.Reader) &StreamFormatReader { r := &StreamFormatReader{ reader: reader @@ -113,6 +126,7 @@ pub fn new_stream_format_reader(reader io.Reader) &StreamFormatReader { return r } +// read satisfies the io.Reader interface. pub fn (mut r StreamFormatReader) read(mut buf []u8) ?int { if r.end_of_stream { return none @@ -139,6 +153,8 @@ pub fn (mut r StreamFormatReader) read(mut buf []u8) ?int { return c } +// read_chunk_size advances the reader & reads the header bytes for the length +// of the next chunk. fn (mut r StreamFormatReader) read_chunk_size() ?u32 { mut buf := []u8{len: 8} diff --git a/src/util/util.v b/src/util/util.v index f9f58edd..f805e6d8 100644 --- a/src/util/util.v +++ b/src/util/util.v @@ -93,6 +93,9 @@ pub fn pretty_bytes(bytes int) string { return '${n:.2}${util.prefixes[i]}' } +// match_array_in_array returns how many elements of a2 overlap with a1. For +// example, if a1 = "abcd" & a2 = "cd", the result will be 2. If the match is +// not at the end of a1, the result is 0. pub fn match_array_in_array(a1 []T, a2 []T) int { mut i := 0 mut match_len := 0