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/ROADMAP.md b/ROADMAP.md index 648d4c5..52e22a2 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -42,7 +42,7 @@ reference](https://docs.docker.com/engine/api/v1.41/). - [ ] Inspect an image - [ ] Get the history of an image - [ ] Push an image - - [x] Tag an image + - [ ] Tag an image - [ ] Remove an image - [ ] Search images - [ ] Delete unused images diff --git a/containers.v b/containers.v index 6981a2c..809ef38 100644 --- a/containers.v +++ b/containers.v @@ -1,6 +1,8 @@ module docker +import json import time +import net.http { Method } import types { ContainerListItem } [params] @@ -12,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()! @@ -35,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 { @@ -68,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)! @@ -84,20 +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') - d.params({ - '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 cd30b60..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,71 +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()) } -// read_response_body consumes the rest of the HTTP response and stores it as -// the response body. This function should only be called after -// read_response_head. This function always reads the entire response into -// memory, even if it's chunked. -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 { - d.body = '' - - 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 that 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()! +// read_response is a convenience function which always consumes the entire +// 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 { @@ -126,7 +165,7 @@ fn (mut d DockerConn) read_json_response() !T { } // get_chunked_response_reader returns a ChunkedResponseReader using the socket -// as reader. This function should only be called after check_error. +// as reader. fn (mut d DockerConn) get_chunked_response_reader() &ChunkedResponseReader { r := new_chunked_response_reader(d.reader) @@ -134,28 +173,10 @@ fn (mut d DockerConn) get_chunked_response_reader() &ChunkedResponseReader { } // get_stream_format_reader returns a StreamFormatReader using the socket as -// reader. This function should only be called after check_error. +// reader. fn (mut d DockerConn) get_stream_format_reader() &StreamFormatReader { r := new_chunked_response_reader(d.reader) r2 := new_stream_format_reader(r) 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, and the body can be read. -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 76df20d..a6f2a23 100644 --- a/images.v +++ b/images.v @@ -1,26 +1,31 @@ module docker +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 } -// image_pull pulls the given image:tag. -pub fn (mut d DockerConn) image_pull(image string, tag string) ! { - d.request(.post, '/images/create') - d.params({ - 'fromImage': image - 'tag': tag - }) - d.send()! - d.read_response_head()! - d.check_error()! +// pull_image pulls the given image:tag. +pub fn (mut d DockerConn) pull_image(image string, tag string) ! { + 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() @@ -33,31 +38,17 @@ pub fn (mut d DockerConn) image_pull(image string, tag string) ! { } // create_image_from_container creates a new image from a container. -pub fn (mut d DockerConn) image_from_container(id string, repo string, tag string) !Image { - d.request(.post, '/commit') - d.params({ - 'container': id - 'repo': repo - 'tag': tag - }) - d.send()! +pub fn (mut d DockerConn) create_image_from_container(id string, repo string, tag string) !Image { + 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) image_remove(id string) ! { - d.request(.delete, '/images/$id') - d.send()! - d.read_response()! -} - -pub fn (mut d DockerConn) image_tag(name string, repo string, tag string) ! { - d.request(.post, '/images/$name/tag') - d.params({ - 'repo': repo - 'tag': tag - }) - d.send()! +pub fn (mut d DockerConn) remove_image(id string) ! { + d.send_request(.delete, '/images/$id')! d.read_response()! } diff --git a/request.v b/request.v index 92d7073..fdee0d4 100644 --- a/request.v +++ b/request.v @@ -3,39 +3,25 @@ module docker import net.http import net.urllib import io -import json -fn (mut d DockerConn) request(method http.Method, url 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() } -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) { - $if T is map[string]string { - for key, value in o { - d.params[key] = urllib.query_escape(value.replace("'", '"')) - } - } $else { - $for field in T.fields { - v := o.$(field.name) + $for field in T.fields { + v := o.$(field.name) - if !isnil(v) { - d.params[field.name] = urllib.query_escape(v.str().replace("'", '"')) - } + if !isnil(v) { + d.params[field.name] = urllib.query_escape(v.str().replace("'", '"')) } } } diff --git a/types/volume.v b/types/volume.v deleted file mode 100644 index 14bc012..0000000 --- a/types/volume.v +++ /dev/null @@ -1,22 +0,0 @@ -module types - -import time - -pub struct UsageData { - size int [json: Size] - ref_count int [json: RefCount] -} - -pub struct Volume { -pub mut: - created_at_str string [json: CreatedAt] - created_at time.Time [skip] - name string [json: Name] - driver string [json: Driver] - mountpoint string [json: Mountpoint] - status map[string]string [json: Status] - labels map[string]string [json: Labels] - scope string [json: Scope] - options map[string]string [json: Options] - usage_data UsageData [json: UsageData] -} diff --git a/volumes.v b/volumes.v index be648c8..5bd9936 100644 --- a/volumes.v +++ b/volumes.v @@ -1,7 +1,26 @@ module docker +import net.http { Method } import time -import types { Volume } + +struct UsageData { + size int [json: Size] + ref_count int [json: RefCount] +} + +struct Volume { + created_at_str string [json: CreatedAt] +pub mut: + created_at time.Time [skip] + name string [json: Name] + driver string [json: Driver] + mountpoint string [json: Mountpoint] + status map[string]string [json: Status] + labels map[string]string [json: Labels] + scope string [json: Scope] + options map[string]string [json: Options] + usage_data UsageData [json: UsageData] +} [params] pub struct VolumeListFilter { @@ -17,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()!