diff --git a/Makefile b/Makefile index 7a86c0e..b25f105 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ # =====CONFIG===== V_PATH ?= v -V := $(V_PATH) -showcc -d use_openssl +V := $(V_PATH) -showcc all: vdocker @@ -10,10 +10,6 @@ all: vdocker vdocker: $(V) -g -shared . -.PHONY: c -c: - $(V) -o docker.c . - # =====DOCS===== .PHONY: api-docs diff --git a/containers.v b/containers.v index 14d295f..809ef38 100644 --- a/containers.v +++ b/containers.v @@ -1,7 +1,8 @@ module docker +import json import time -import net.http +import net.http { Method } import types { ContainerListItem } [params] @@ -13,7 +14,7 @@ pub struct ContainerListConfig { } pub fn (mut d DockerConn) container_list(c ContainerListConfig) ![]ContainerListItem { - d.request(.get, '/containers/json', {}) + d.get('/containers/json') d.params(c) d.send()! @@ -36,18 +37,30 @@ pub: } pub fn (mut d DockerConn) container_create(c NewContainer) !CreatedContainer { - d.request(.post, '/containers/create', {}) - d.body_json(c) - d.send()! + d.send_request_with_json(Method.post, '/containers/create', c)! + head, res := d.read_response()! - return d.read_json_response() + if head.status_code != 201 { + data := json.decode(DockerError, res)! + + return error(data.message) + } + + data := json.decode(CreatedContainer, res)! + + return data } // start_container starts the container with the given id. pub fn (mut d DockerConn) container_start(id string) ! { - d.request(.post, '/containers/$id/start', {}) - d.send()! - d.read_response()! + d.send_request(Method.post, '/containers/$id/start')! + head, body := d.read_response()! + + if head.status_code != 204 { + data := json.decode(DockerError, body)! + + return error(data.message) + } } struct ContainerInspect { @@ -69,10 +82,16 @@ pub mut: } pub fn (mut d DockerConn) container_inspect(id string) !ContainerInspect { - d.request(.get, '/containers/$id/json', {}) - d.send()! + d.send_request(Method.get, '/containers/$id/json')! + head, body := d.read_response()! - mut data := d.read_json_response()! + if head.status_code != 200 { + data := json.decode(DockerError, body)! + + return error(data.message) + } + + mut data := json.decode(ContainerInspect, body)! // The Docker engine API *should* always return UTC time. data.state.start_time = time.parse_rfc3339(data.state.start_time_str)! @@ -85,19 +104,27 @@ pub fn (mut d DockerConn) container_inspect(id string) !ContainerInspect { } pub fn (mut d DockerConn) container_remove(id string) ! { - d.request(.delete, '/containers/$id', {}) - d.send()! - d.read_response()! + d.send_request(Method.delete, '/containers/$id')! + head, body := d.read_response()! + + if head.status_code != 204 { + data := json.decode(DockerError, body)! + + return error(data.message) + } } pub fn (mut d DockerConn) container_get_logs(id string) !&StreamFormatReader { - d.request(.get, '/containers/$id/logs', { - 'stdout': 'true' - 'stderr': 'true' - }) - d.send()! - d.read_response_head()! - d.check_error()! + d.send_request(Method.get, '/containers/$id/logs?stdout=true&stderr=true')! + head := d.read_response_head()! + + if head.status_code != 200 { + content_length := head.header.get(http.CommonHeader.content_length)!.int() + body := d.read_response_body(content_length)! + data := json.decode(DockerError, body)! + + return error(data.message) + } return d.get_stream_format_reader() } diff --git a/docker.v b/docker.v index 093e657..7eb5ea2 100644 --- a/docker.v +++ b/docker.v @@ -4,6 +4,7 @@ import net.unix import io import net.http import strings +import net.urllib import json import util @@ -26,11 +27,7 @@ mut: url string params map[string]string content_type string - // Before send: body of the request - // After send: body of response - body string - // HTTP head of the response - head http.Response + body string } // new_conn creates a new connection to the Docker daemon. @@ -50,65 +47,113 @@ pub fn (mut d DockerConn) close() ! { d.socket.close()! } +// send_request sends an HTTP request without body. +fn (mut d DockerConn) send_request(method http.Method, url_str string) ! { + url := urllib.parse('/$docker.api_version$url_str')! + req := '$method $url.request_uri() HTTP/1.1\nHost: localhost\n\n' + + d.socket.write_string(req)! + + // When starting a new request, the reader needs to be reset. + d.reader = io.new_buffered_reader(reader: d.socket) +} + +// send_request_with_body sends an HTTP request with the given body. +fn (mut d DockerConn) send_request_with_body(method http.Method, url_str string, content_type string, body string) ! { + url := urllib.parse('/$docker.api_version$url_str')! + 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)! + + // When starting a new request, the reader needs to be reset. + d.reader = io.new_buffered_reader(reader: d.socket) +} + +// send_request_with_json is a convenience wrapper around +// send_request_with_body that encodes the input as JSON. +fn (mut d DockerConn) send_request_with_json(method http.Method, url_str string, data &T) ! { + body := json.encode(data) + + return d.send_request_with_body(method, url_str, 'application/json', body) +} + // 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 the reader past the HTTP // separator, so the body can be read fully later on. -fn (mut d DockerConn) read_response_head() ! { +fn (mut d DockerConn) read_response_head() !http.Response { mut res := []u8{} util.read_until_separator(mut d.reader, mut res, docker.http_separator)! - d.head = http.parse_response(res.bytestr())! + return http.parse_response(res.bytestr()) } -fn (mut d DockerConn) read_response_body() ! { - if d.head.status() == .no_content { - return +// read_response_body reads `length` bytes from the stream. It can be used when +// the response encoding isn't chunked to fully read it. +fn (mut d DockerConn) read_response_body(length int) !string { + if length == 0 { + return '' } + mut buf := []u8{len: docker.buf_len} + mut c := 0 mut builder := strings.new_builder(docker.buf_len) - if d.head.header.get(.transfer_encoding) or { '' } == 'chunked' { - mut body_stream := d.get_chunked_response_reader() + for builder.len < length { + c = d.reader.read(mut buf) or { break } - util.reader_to_writer(mut body_stream, mut builder)! - } else { - content_length := d.head.header.get(.content_length)!.int() - - if content_length == 0 { - return - } - - mut buf := []u8{len: docker.buf_len} - mut c := 0 - - for builder.len < content_length { - c = d.reader.read(mut buf)! - - builder.write(buf[..c])! - } + builder.write(buf[..c])! } - d.body = builder.str() + return builder.str() } // read_response is a convenience function which always consumes the entire -// response and loads it into memory. It should only be used when we're certain -// that the result isn't too large, as even chunked responses will get fully -// loaded into memory. -fn (mut d DockerConn) read_response() ! { - d.read_response_head()! - d.check_error()! - d.read_response_body()! +// response & returns it. It should only be used when we're certain that the +// result isn't too large. +fn (mut d DockerConn) read_response() !(http.Response, string) { + head := d.read_response_head()! + + if head.status().is_error() { + content_length := head.header.get(.content_length)!.int() + body := d.read_response_body(content_length)! + mut err := json.decode(DockerError, body)! + err.status = head.status_code + + return err + } + + // 204 means "No Content", so we can assume nothing follows after this + if head.status() == .no_content { + return head, '' + } + + if head.header.get(http.CommonHeader.transfer_encoding) or { '' } == 'chunked' { + mut builder := strings.new_builder(1024) + mut body := d.get_chunked_response_reader() + + util.reader_to_writer(mut body, mut builder)! + + return head, builder.str() + } + + content_length := head.header.get(http.CommonHeader.content_length)!.int() + body := d.read_response_body(content_length)! + + return head, body } -// read_json_response is a convenience function that runs read_response -// before parsing its contents, which is assumed to be JSON, into a struct. fn (mut d DockerConn) read_json_response() !T { - d.read_response()! + head, body := d.read_response()! - data := json.decode(T, d.body)! + if head.status_code < 200 || head.status_code > 300 { + data := json.decode(DockerError, body)! + + return docker_error(head.status_code, data.message) + } + + mut data := json.decode(T, body)! //$for field in T.fields { //$if field.typ is time.Time { @@ -135,21 +180,3 @@ fn (mut d DockerConn) get_stream_format_reader() &StreamFormatReader { return r2 } - -struct DockerError { -pub: - message string -} - -// check_error should be called after read_response_head. If the status code of -// the response is an error, the body is consumed and the Docker HTTP error is -// returned as a V error. If the status isn't the error, this function is a -// no-op. -fn (mut d DockerConn) check_error() ! { - if d.head.status().is_error() { - d.read_response_body()! - d_err := json.decode(DockerError, d.body)! - - return error_with_code('$d.head.status(): $d_err.message', d.head.status_code) - } -} diff --git a/errors.v b/errors.v new file mode 100644 index 0000000..31ed81a --- /dev/null +++ b/errors.v @@ -0,0 +1,22 @@ +module docker + +struct DockerError { +pub mut: + status int [skip] + message string +} + +fn (err DockerError) code() int { + return err.status +} + +fn (err DockerError) msg() string { + return err.message +} + +fn docker_error(status int, message string) DockerError { + return DockerError{ + status: status + message: message + } +} diff --git a/images.v b/images.v index d879cd9..a6f2a23 100644 --- a/images.v +++ b/images.v @@ -1,26 +1,31 @@ module docker -import net.http +import net.http { Method } import types { Image } +import json pub fn (mut d DockerConn) image_inspect(image string) !Image { - d.request(.get, '/images/$image/json', {}) - d.send()! + d.send_request(.get, '/images/$image/json')! + _, body := d.read_response()! - data := d.read_json_response()! + data := json.decode(Image, body)! return data } // pull_image pulls the given image:tag. pub fn (mut d DockerConn) pull_image(image string, tag string) ! { - d.request(.post, '/images/create', { - 'fromImage': image - 'tag': tag - }) - d.send()! - d.read_response_head()! - d.check_error()! + d.send_request(Method.post, '/images/create?fromImage=$image&tag=$tag')! + head := d.read_response_head()! + + if head.status().is_error() { + content_length := head.header.get(.content_length)!.int() + body := d.read_response_body(content_length)! + mut err := json.decode(DockerError, body)! + err.status = head.status_code + + return err + } // Keep reading the body until the pull has completed mut body := d.get_chunked_response_reader() @@ -34,19 +39,16 @@ pub fn (mut d DockerConn) pull_image(image string, tag string) ! { // create_image_from_container creates a new image from a container. pub fn (mut d DockerConn) create_image_from_container(id string, repo string, tag string) !Image { - d.request(.post, '/commit', { - 'container': id - 'repo': repo - 'tag': tag - }) - d.send()! + d.send_request(.post, '/commit?container=$id&repo=$repo&tag=$tag')! + _, body := d.read_response()! - return d.read_json_response()! + data := json.decode(Image, body)! + + return data } // remove_image removes the image with the given id. pub fn (mut d DockerConn) remove_image(id string) ! { - d.request(.delete, '/images/$id', {}) - d.send()! + d.send_request(.delete, '/images/$id')! d.read_response()! } diff --git a/request.v b/request.v index f844717..fdee0d4 100644 --- a/request.v +++ b/request.v @@ -3,29 +3,17 @@ module docker import net.http import net.urllib import io -import json -fn (mut d DockerConn) request(method http.Method, url string, params map[string]string) { +fn (mut d DockerConn) request(method http.Method, url_str string) { d.method = method - d.url = url + d.url = url_str + d.params.clear() d.content_type = '' d.body = '' - - d.params.clear() - - for key, value in params { - d.params[key] = urllib.query_escape(value.replace("'", '"')) - } } -fn (mut d DockerConn) body(content_type string, body string) { - d.content_type = content_type - d.body = body -} - -fn (mut d DockerConn) body_json(data T) { - d.content_type = 'application/json' - d.body = json.encode(data) +fn (mut d DockerConn) get(url_str string) { + d.request(http.Method.get, url_str) } fn (mut d DockerConn) params(o T) { diff --git a/volumes.v b/volumes.v index 6ce9905..5bd9936 100644 --- a/volumes.v +++ b/volumes.v @@ -1,6 +1,6 @@ module docker -import net.http +import net.http { Method } import time struct UsageData { @@ -36,8 +36,7 @@ struct VolumeListResponse { } pub fn (mut d DockerConn) volume_list() !VolumeListResponse { - d.request(.get, '/volumes', {}) - d.send()! + d.send_request(Method.get, '/volumes')! mut data := d.read_json_response()!