diff --git a/Makefile b/Makefile index b25f105..7a86c0e 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ # =====CONFIG===== V_PATH ?= v -V := $(V_PATH) -showcc +V := $(V_PATH) -showcc -d use_openssl all: vdocker @@ -10,6 +10,10 @@ 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 52e22a2..648d4c5 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 - - [ ] Tag an image + - [x] Tag an image - [ ] Remove an image - [ ] Search images - [ ] Delete unused images diff --git a/containers.v b/containers.v index 809ef38..6981a2c 100644 --- a/containers.v +++ b/containers.v @@ -1,8 +1,6 @@ module docker -import json import time -import net.http { Method } import types { ContainerListItem } [params] @@ -14,7 +12,7 @@ pub struct ContainerListConfig { } pub fn (mut d DockerConn) container_list(c ContainerListConfig) ![]ContainerListItem { - d.get('/containers/json') + d.request(.get, '/containers/json') d.params(c) d.send()! @@ -37,30 +35,18 @@ pub: } pub fn (mut d DockerConn) container_create(c NewContainer) !CreatedContainer { - d.send_request_with_json(Method.post, '/containers/create', c)! - head, res := d.read_response()! + d.request(.post, '/containers/create') + d.body_json(c) + d.send()! - if head.status_code != 201 { - data := json.decode(DockerError, res)! - - return error(data.message) - } - - data := json.decode(CreatedContainer, res)! - - return data + return d.read_json_response() } // start_container starts the container with the given id. pub fn (mut d DockerConn) container_start(id string) ! { - 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) - } + d.request(.post, '/containers/$id/start') + d.send()! + d.read_response()! } struct ContainerInspect { @@ -82,16 +68,10 @@ pub mut: } pub fn (mut d DockerConn) container_inspect(id string) !ContainerInspect { - d.send_request(Method.get, '/containers/$id/json')! - head, body := d.read_response()! + d.request(.get, '/containers/$id/json') + d.send()! - if head.status_code != 200 { - data := json.decode(DockerError, body)! - - return error(data.message) - } - - mut data := json.decode(ContainerInspect, body)! + mut data := d.read_json_response()! // The Docker engine API *should* always return UTC time. data.state.start_time = time.parse_rfc3339(data.state.start_time_str)! @@ -104,27 +84,20 @@ pub fn (mut d DockerConn) container_inspect(id string) !ContainerInspect { } pub fn (mut d DockerConn) container_remove(id string) ! { - 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) - } + d.request(.delete, '/containers/$id') + d.send()! + d.read_response()! } pub fn (mut d DockerConn) container_get_logs(id string) !&StreamFormatReader { - 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) - } + d.request(.get, '/containers/$id/logs') + d.params({ + 'stdout': 'true' + 'stderr': 'true' + }) + d.send()! + d.read_response_head()! + d.check_error()! return d.get_stream_format_reader() } diff --git a/docker.v b/docker.v index 7eb5ea2..cd30b60 100644 --- a/docker.v +++ b/docker.v @@ -4,7 +4,6 @@ import net.unix import io import net.http import strings -import net.urllib import json import util @@ -27,7 +26,11 @@ mut: url string params map[string]string content_type string - body string + // Before send: body of the request + // After send: body of response + body string + // HTTP head of the response + head http.Response } // new_conn creates a new connection to the Docker daemon. @@ -47,113 +50,71 @@ 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() !http.Response { +fn (mut d DockerConn) read_response_head() ! { mut res := []u8{} util.read_until_separator(mut d.reader, mut res, docker.http_separator)! - return http.parse_response(res.bytestr()) + d.head = 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. -fn (mut d DockerConn) read_response_body(length int) !string { - if length == 0 { - return '' +// 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 } - mut buf := []u8{len: docker.buf_len} - mut c := 0 mut builder := strings.new_builder(docker.buf_len) - for builder.len < length { - c = d.reader.read(mut buf) or { break } + if d.head.header.get(.transfer_encoding) or { '' } == 'chunked' { + mut body_stream := d.get_chunked_response_reader() - builder.write(buf[..c])! + 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])! + } } - return builder.str() + d.body = builder.str() } -// 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_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_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 { - head, body := d.read_response()! + d.read_response()! - 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)! + data := json.decode(T, d.body)! //$for field in T.fields { //$if field.typ is time.Time { @@ -165,7 +126,7 @@ fn (mut d DockerConn) read_json_response() !T { } // get_chunked_response_reader returns a ChunkedResponseReader using the socket -// as reader. +// as reader. This function should only be called after check_error. fn (mut d DockerConn) get_chunked_response_reader() &ChunkedResponseReader { r := new_chunked_response_reader(d.reader) @@ -173,10 +134,28 @@ fn (mut d DockerConn) get_chunked_response_reader() &ChunkedResponseReader { } // get_stream_format_reader returns a StreamFormatReader using the socket as -// reader. +// reader. This function should only be called after check_error. 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 deleted file mode 100644 index 31ed81a..0000000 --- a/errors.v +++ /dev/null @@ -1,22 +0,0 @@ -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 a6f2a23..76df20d 100644 --- a/images.v +++ b/images.v @@ -1,31 +1,26 @@ module docker -import net.http { Method } import types { Image } -import json pub fn (mut d DockerConn) image_inspect(image string) !Image { - d.send_request(.get, '/images/$image/json')! - _, body := d.read_response()! + d.request(.get, '/images/$image/json') + d.send()! - data := json.decode(Image, body)! + data := d.read_json_response()! return data } -// 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 - } +// 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()! // Keep reading the body until the pull has completed mut body := d.get_chunked_response_reader() @@ -38,17 +33,31 @@ 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.send_request(.post, '/commit?container=$id&repo=$repo&tag=$tag')! - _, body := d.read_response()! +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()! - data := json.decode(Image, body)! - - return data + return d.read_json_response()! } // remove_image removes the image with the given id. -pub fn (mut d DockerConn) remove_image(id string) ! { - d.send_request(.delete, '/images/$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()! d.read_response()! } diff --git a/request.v b/request.v index fdee0d4..92d7073 100644 --- a/request.v +++ b/request.v @@ -3,25 +3,39 @@ module docker import net.http import net.urllib import io +import json -fn (mut d DockerConn) request(method http.Method, url_str string) { +fn (mut d DockerConn) request(method http.Method, url string) { d.method = method - d.url = url_str - d.params.clear() + d.url = url d.content_type = '' d.body = '' + + d.params.clear() } -fn (mut d DockerConn) get(url_str string) { - d.request(http.Method.get, url_str) +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) params(o T) { - $for field in T.fields { - v := o.$(field.name) + $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) - 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 new file mode 100644 index 0000000..14bc012 --- /dev/null +++ b/types/volume.v @@ -0,0 +1,22 @@ +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 5bd9936..be648c8 100644 --- a/volumes.v +++ b/volumes.v @@ -1,26 +1,7 @@ module docker -import net.http { Method } import time - -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] -} +import types { Volume } [params] pub struct VolumeListFilter { @@ -36,7 +17,8 @@ struct VolumeListResponse { } pub fn (mut d DockerConn) volume_list() !VolumeListResponse { - d.send_request(Method.get, '/volumes')! + d.request(.get, '/volumes') + d.send()! mut data := d.read_json_response()!