From 58c1ecd25e82f4466093151525454cf420ecfc6e Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Sat, 7 May 2022 14:16:30 +0200 Subject: [PATCH 01/43] db: added BuildLog & required methods --- src/db/db.v | 1 + src/db/git.v | 2 +- src/db/logs.v | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 src/db/logs.v diff --git a/src/db/db.v b/src/db/db.v index a75c34c4..5ec240de 100644 --- a/src/db/db.v +++ b/src/db/db.v @@ -12,6 +12,7 @@ pub fn init(db_path string) ?VieterDb { sql conn { create table GitRepo + create table BuildLog } return VieterDb{ diff --git a/src/db/git.v b/src/db/git.v index c40086b2..b7791401 100644 --- a/src/db/git.v +++ b/src/db/git.v @@ -94,7 +94,7 @@ pub fn (db &VieterDb) get_git_repo(repo_id int) ?GitRepo { // If a select statement fails, it returns a zeroed object. By // checking one of the required fields, we can see whether the query // returned a result or not. - if res.url == '' { + if res.id == 0 { return none } diff --git a/src/db/logs.v b/src/db/logs.v new file mode 100644 index 00000000..3e5b6008 --- /dev/null +++ b/src/db/logs.v @@ -0,0 +1,47 @@ +module db + +import time + +pub struct BuildLog { + id int [primary; sql: serial] + repo GitRepo [nonull] + start_time time.Time [nonull] + end_time time.Time [nonull] + exit_code int [nonull] +} + +// get_build_logs returns all BuildLog's in the database. +pub fn (db &VieterDb) get_build_logs() []BuildLog { + res := sql db.conn { + select from BuildLog order by id + } + + return res +} + +// get_build_log tries to return a specific BuildLog. +pub fn (db &VieterDb) get_build_log(id int) ?BuildLog { + res := sql db.conn { + select from BuildLog where id == id + } + + if res.id == 0 { + return none + } + + return res +} + +// add_build_log inserts the given BuildLog into the database. +pub fn (db &VieterDb) add_build_log(log BuildLog) { + sql db.conn { + insert log into BuildLog + } +} + +// delete_build_log delete the BuildLog with the given ID from the database. +pub fn (db &VieterDb) delete_build_log(id int) { + sql db.conn { + delete from BuildLog where id == id + } +} From 7e01dbafec2ecb6dd92debc79a2b9f0f107d7198 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Sat, 7 May 2022 15:10:07 +0200 Subject: [PATCH 02/43] feat(server): added endpoints for listing & uploading build logs --- src/server/logs.v | 99 +++++++++++++++++++++++++++++++++++++++++++++ src/server/server.v | 9 +++++ 2 files changed, 108 insertions(+) create mode 100644 src/server/logs.v diff --git a/src/server/logs.v b/src/server/logs.v new file mode 100644 index 00000000..01116b4b --- /dev/null +++ b/src/server/logs.v @@ -0,0 +1,99 @@ +module server + +import web +import net.http +import net.urllib +import response { new_data_response, new_response } +import db +import time +import os +import util + +['/api/logs'; get] +fn (mut app App) get_logs() web.Result { + if !app.is_authorized() { + return app.json(http.Status.unauthorized, new_response('Unauthorized.')) + } + + logs := app.db.get_build_logs() + + return app.json(http.Status.ok, new_data_response(logs)) +} + +// parse_query_time unescapes an HTTP query parameter & tries to parse it as a +// time.Time struct. +fn parse_query_time(query string) ?time.Time { + unescaped := urllib.query_unescape(query) ? + t := time.parse(unescaped) ? + + return t +} + +['/api/logs'; post] +fn (mut app App) post_log() web.Result { + if !app.is_authorized() { + return app.json(http.Status.unauthorized, new_response('Unauthorized.')) + } + + // Parse query params + start_time := parse_query_time(app.query['startTime']) or { + return app.json(http.Status.bad_request, new_response('Invalid or missing start time.')) + } + + end_time := time.parse(app.query['endTime'].replace('_', ' ')) or { + return app.json(http.Status.bad_request, new_response('Invalid or missing end time.')) + } + + if 'exitCode' !in app.query { + return app.json(http.Status.bad_request, new_response('Missing exit code.')) + } + + exit_code := app.query['exitCode'].int() + + if 'arch' !in app.query { + return app.json(http.Status.bad_request, new_response("Missing parameter 'arch'.")) + } + + arch := app.query['arch'] + + repo := app.db.get_git_repo(app.query['repo'].int()) or { + return app.json(http.Status.bad_request, new_response('Unknown repo.')) + } + + // Store log in db + log := db.BuildLog{ + repo: repo + start_time: start_time + end_time: end_time + exit_code: exit_code + } + + app.db.add_build_log(log) + + repo_logs_dir := os.join_path(app.conf.data_dir, logs_dir_name, repo.id.str(), arch) + + // Create the logs directory of it doesn't exist + if !os.exists(repo_logs_dir) { + os.mkdir_all(repo_logs_dir) or { + app.lerror("Couldn't create dir '$repo_logs_dir'.") + + return app.json(http.Status.internal_server_error, new_response('An error occured while processing the request.')) + } + } + + // Stream log contents to correct file + file_name := start_time.custom_format('YYYY-MM-DD_HH-mm-ss') + full_path := os.join_path_single(repo_logs_dir, file_name) + + if length := app.req.header.get(.content_length) { + util.reader_to_file(mut app.reader, length.int(), full_path) or { + app.lerror('An error occured while receiving logs: $err.msg()') + + return app.json(http.Status.internal_server_error, new_response('Failed to upload logs.')) + } + } else { + return app.status(http.Status.length_required) + } + + return app.json(http.Status.ok, new_response('Logs added successfully.')) +} diff --git a/src/server/server.v b/src/server/server.v index b2a2ad29..090aa76e 100644 --- a/src/server/server.v +++ b/src/server/server.v @@ -12,6 +12,7 @@ const ( log_file_name = 'vieter.log' repo_dir_name = 'repos' db_file_name = 'vieter.sqlite' + logs_dir_name = 'logs' ) struct App { @@ -37,6 +38,14 @@ pub fn server(conf Config) ? { os.mkdir_all(conf.data_dir) or { util.exit_with_message(1, 'Failed to create data directory.') } + logs_dir := os.join_path_single(conf.data_dir, server.logs_dir_name) + + if !os.exists(logs_dir) { + os.mkdir(os.join_path_single(conf.data_dir, server.logs_dir_name)) or { + util.exit_with_message(1, 'Failed to create logs directory.') + } + } + mut logger := log.Log{ level: log_level } From 393e641a7666682c1e4ff38b83affbf596c461d6 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Sat, 7 May 2022 15:31:01 +0200 Subject: [PATCH 03/43] feat(server): allow filtering of builds per repo --- src/db/logs.v | 12 +++++++++++- src/server/logs.v | 16 +++++++++++----- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/src/db/logs.v b/src/db/logs.v index 3e5b6008..9a2405b4 100644 --- a/src/db/logs.v +++ b/src/db/logs.v @@ -4,7 +4,7 @@ import time pub struct BuildLog { id int [primary; sql: serial] - repo GitRepo [nonull] + repo_id int [nonull] start_time time.Time [nonull] end_time time.Time [nonull] exit_code int [nonull] @@ -19,6 +19,16 @@ pub fn (db &VieterDb) get_build_logs() []BuildLog { return res } +// get_build_logs_for_repo returns all BuildLog's in the database for a given +// repo. +pub fn (db &VieterDb) get_build_logs_for_repo(repo_id int) []BuildLog { + res := sql db.conn { + select from BuildLog where repo_id == repo_id order by id + } + + return res +} + // get_build_log tries to return a specific BuildLog. pub fn (db &VieterDb) get_build_log(id int) ?BuildLog { res := sql db.conn { diff --git a/src/server/logs.v b/src/server/logs.v index 01116b4b..8b0f297f 100644 --- a/src/server/logs.v +++ b/src/server/logs.v @@ -15,7 +15,11 @@ fn (mut app App) get_logs() web.Result { return app.json(http.Status.unauthorized, new_response('Unauthorized.')) } - logs := app.db.get_build_logs() + logs := if 'repo' in app.query { + app.db.get_build_logs_for_repo(app.query['repo'].int()) + } else { + app.db.get_build_logs() + } return app.json(http.Status.ok, new_data_response(logs)) } @@ -56,13 +60,15 @@ fn (mut app App) post_log() web.Result { arch := app.query['arch'] - repo := app.db.get_git_repo(app.query['repo'].int()) or { - return app.json(http.Status.bad_request, new_response('Unknown repo.')) + repo := app.query['repo'].int() + + if repo == 0 { + return app.json(http.Status.bad_request, new_response('Invalid Git repo.')) } // Store log in db log := db.BuildLog{ - repo: repo + repo_id: repo start_time: start_time end_time: end_time exit_code: exit_code @@ -70,7 +76,7 @@ fn (mut app App) post_log() web.Result { app.db.add_build_log(log) - repo_logs_dir := os.join_path(app.conf.data_dir, logs_dir_name, repo.id.str(), arch) + repo_logs_dir := os.join_path(app.conf.data_dir, logs_dir_name, repo.str(), arch) // Create the logs directory of it doesn't exist if !os.exists(repo_logs_dir) { From 139142fcec4f66ad08e95ed4dcf6c3c30acc423e Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Sat, 7 May 2022 15:41:49 +0200 Subject: [PATCH 04/43] feat(server): added endpoint for content of build log --- src/db/logs.v | 2 ++ src/server/logs.v | 27 ++++++++++++++++++++++++++- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/src/db/logs.v b/src/db/logs.v index 9a2405b4..589304e7 100644 --- a/src/db/logs.v +++ b/src/db/logs.v @@ -3,10 +3,12 @@ module db import time pub struct BuildLog { +pub: id int [primary; sql: serial] repo_id int [nonull] start_time time.Time [nonull] end_time time.Time [nonull] + arch string [nonull] exit_code int [nonull] } diff --git a/src/server/logs.v b/src/server/logs.v index 8b0f297f..9bddc210 100644 --- a/src/server/logs.v +++ b/src/server/logs.v @@ -24,6 +24,30 @@ fn (mut app App) get_logs() web.Result { return app.json(http.Status.ok, new_data_response(logs)) } +['/api/logs/:id'; get] +fn (mut app App) get_single_log(id int) web.Result { + if !app.is_authorized() { + return app.json(http.Status.unauthorized, new_response('Unauthorized.')) + } + + log := app.db.get_build_log(id) or { return app.not_found() } + + return app.json(http.Status.ok, new_data_response(log)) +} + +['/api/logs/:id/content'; get] +fn (mut app App) get_log_contents(id int) web.Result { + if !app.is_authorized() { + return app.json(http.Status.unauthorized, new_response('Unauthorized.')) + } + + log := app.db.get_build_log(id) or { return app.not_found() } + file_name := log.start_time.custom_format('YYYY-MM-DD_HH-mm-ss') + full_path := os.join_path(app.conf.data_dir, server.logs_dir_name, log.repo_id.str(), log.arch, file_name) + + return app.file(full_path) +} + // parse_query_time unescapes an HTTP query parameter & tries to parse it as a // time.Time struct. fn parse_query_time(query string) ?time.Time { @@ -44,7 +68,7 @@ fn (mut app App) post_log() web.Result { return app.json(http.Status.bad_request, new_response('Invalid or missing start time.')) } - end_time := time.parse(app.query['endTime'].replace('_', ' ')) or { + end_time := parse_query_time(app.query['endTime']) or { return app.json(http.Status.bad_request, new_response('Invalid or missing end time.')) } @@ -71,6 +95,7 @@ fn (mut app App) post_log() web.Result { repo_id: repo start_time: start_time end_time: end_time + arch: arch exit_code: exit_code } From f42d3fd8b0f2005f96b94e39c66e94f300b2649e Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Sat, 7 May 2022 15:44:59 +0200 Subject: [PATCH 05/43] fix(server): prevent adding logs to non-existent repo --- src/db/git.v | 8 ++++++++ src/server/logs.v | 10 +++++----- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/db/git.v b/src/db/git.v index b7791401..6eeecc8c 100644 --- a/src/db/git.v +++ b/src/db/git.v @@ -152,3 +152,11 @@ pub fn (db &VieterDb) update_git_repo_archs(repo_id int, archs []GitRepoArch) { } } } + +pub fn (db &VieterDb) git_repo_exists(repo_id int) bool { + db.get_git_repo(repo_id) or { + return false + } + + return true +} diff --git a/src/server/logs.v b/src/server/logs.v index 9bddc210..10b734da 100644 --- a/src/server/logs.v +++ b/src/server/logs.v @@ -84,15 +84,15 @@ fn (mut app App) post_log() web.Result { arch := app.query['arch'] - repo := app.query['repo'].int() + repo_id := app.query['repo'].int() - if repo == 0 { - return app.json(http.Status.bad_request, new_response('Invalid Git repo.')) + if !app.db.git_repo_exists(repo_id) { + return app.json(http.Status.bad_request, new_response('Unknown Git repo.')) } // Store log in db log := db.BuildLog{ - repo_id: repo + repo_id: repo_id start_time: start_time end_time: end_time arch: arch @@ -101,7 +101,7 @@ fn (mut app App) post_log() web.Result { app.db.add_build_log(log) - repo_logs_dir := os.join_path(app.conf.data_dir, logs_dir_name, repo.str(), arch) + repo_logs_dir := os.join_path(app.conf.data_dir, logs_dir_name, repo_id.str(), arch) // Create the logs directory of it doesn't exist if !os.exists(repo_logs_dir) { From 407b2269556e23d561ecd878c82c898502d28fc5 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Sat, 7 May 2022 16:10:27 +0200 Subject: [PATCH 06/43] refactor: moved client code into own module --- src/build/build.v | 4 +-- src/client/client.v | 40 +++++++++++++++++++++ src/client/git.v | 51 ++++++++++++++++++++++++++ src/cron/daemon/build.v | 2 +- src/cron/daemon/daemon.v | 10 +++--- src/git/cli.v | 17 ++++++--- src/git/client.v | 77 ---------------------------------------- 7 files changed, 110 insertions(+), 91 deletions(-) create mode 100644 src/client/client.v create mode 100644 src/client/git.v delete mode 100644 src/git/client.v diff --git a/src/build/build.v b/src/build/build.v index 15a5eb81..6f033e67 100644 --- a/src/build/build.v +++ b/src/build/build.v @@ -3,9 +3,9 @@ module build import docker import encoding.base64 import time -import git import os import db +import client const container_build_dir = '/build' @@ -126,7 +126,7 @@ fn build(conf Config) ? { build_arch := os.uname().machine // We get the repos map from the Vieter instance - repos := git.get_repos(conf.address, conf.api_key) ? + repos := client.new(conf.address, conf.api_key).get_git_repos() ? // We filter out any repos that aren't allowed to be built on this // architecture diff --git a/src/client/client.v b/src/client/client.v new file mode 100644 index 00000000..614b3dba --- /dev/null +++ b/src/client/client.v @@ -0,0 +1,40 @@ +module client + +import net.http +import response { Response } +import json + +pub struct Client { +pub: + address string + api_key string +} + +pub fn new(address string, api_key string) Client { + return Client{ + address: address + api_key: api_key + } +} + +// send_request is a convenience method for sending requests to the repos +// API. It mostly does string manipulation to create a query string containing +// the provided params. +fn (c &Client) send_request(method http.Method, url string, params map[string]string) ?Response { + mut full_url := '${c.address}$url' + + if params.len > 0 { + params_str := params.keys().map('$it=${params[it]}').join('&') + + full_url = '$full_url?$params_str' + } + + mut req := http.new_request(method, full_url, '') ? + req.add_custom_header('X-API-Key', c.api_key) ? + + res := req.do() ? + data := json.decode(Response, res.text) ? + + return data +} + diff --git a/src/client/git.v b/src/client/git.v new file mode 100644 index 00000000..5ed06200 --- /dev/null +++ b/src/client/git.v @@ -0,0 +1,51 @@ +module client + +import db +import net.http +import response { Response } + +// get_repos returns the current list of repos. +pub fn (c &Client) get_git_repos() ?[]db.GitRepo { + data := c.send_request<[]db.GitRepo>(http.Method.get, '/api/repos', {}) ? + + return data.data +} + +// get_repo returns the repo for a specific ID. +pub fn (c &Client) get_git_repo(id int) ?db.GitRepo { + data := c.send_request(http.Method.get, '/api/repos/$id', {}) ? + + return data.data +} + +// add_repo adds a new repo to the server. +pub fn (c &Client) add_git_repo(url string, branch string, repo string, arch []string) ?Response { + mut params := { + 'url': url + 'branch': branch + 'repo': repo + } + + if arch.len > 0 { + params['arch'] = arch.join(',') + } + + data := c.send_request(http.Method.post, '/api/repos', params) ? + + return data +} + +// remove_repo removes the repo with the given ID from the server. +pub fn (c &Client) remove_git_repo(id int) ?Response { + data := c.send_request(http.Method.delete, '/api/repos/$id', {}) ? + + return data +} + +// patch_repo sends a PATCH request to the given repo with the params as +// payload. +pub fn (c &Client) patch_git_repo(id int, params map[string]string) ?Response { + data := c.send_request(http.Method.patch, '/api/repos/$id', params) ? + + return data +} diff --git a/src/cron/daemon/build.v b/src/cron/daemon/build.v index e54a39e0..d107fd35 100644 --- a/src/cron/daemon/build.v +++ b/src/cron/daemon/build.v @@ -77,7 +77,7 @@ fn (mut d Daemon) run_build(build_index int, sb ScheduledBuild) { // 0 means success, 1 means failure mut status := 0 - build.build_repo(d.address, d.api_key, d.builder_images.last(), &sb.repo) or { + build.build_repo(d.client.address, d.client.api_key, d.builder_images.last(), &sb.repo) or { d.ldebug('build_repo error: $err.msg()') status = 1 } diff --git a/src/cron/daemon/daemon.v b/src/cron/daemon/daemon.v index ffa2f6e2..71fc5754 100644 --- a/src/cron/daemon/daemon.v +++ b/src/cron/daemon/daemon.v @@ -1,6 +1,5 @@ module daemon -import git import time import log import datatypes { MinHeap } @@ -10,6 +9,7 @@ import build import docker import db import os +import client const ( // How many seconds to wait before retrying to update API if failed @@ -31,8 +31,7 @@ fn (r1 ScheduledBuild) < (r2 ScheduledBuild) bool { pub struct Daemon { mut: - address string - api_key string + client client.Client base_image string builder_images []string global_schedule CronExpression @@ -56,8 +55,7 @@ mut: // populates the build queue for the first time. pub fn init_daemon(logger log.Log, address string, api_key string, base_image string, global_schedule CronExpression, max_concurrent_builds int, api_update_frequency int, image_rebuild_frequency int) ?Daemon { mut d := Daemon{ - address: address - api_key: api_key + client: client.new(address, api_key) base_image: base_image global_schedule: global_schedule api_update_frequency: api_update_frequency @@ -180,7 +178,7 @@ fn (mut d Daemon) schedule_build(repo db.GitRepo) { fn (mut d Daemon) renew_repos() { d.linfo('Renewing repos...') - mut new_repos := git.get_repos(d.address, d.api_key) or { + mut new_repos := d.client.get_git_repos() or { d.lerror('Failed to renew repos. Retrying in ${daemon.api_update_retry_timeout}s...') d.api_update_timestamp = time.now().add_seconds(daemon.api_update_retry_timeout) diff --git a/src/git/cli.v b/src/git/cli.v index 634b7782..bdc0479d 100644 --- a/src/git/cli.v +++ b/src/git/cli.v @@ -3,6 +3,7 @@ module git import cli import env import cron.expression { parse_expression } +import client struct Config { address string [required] @@ -119,7 +120,8 @@ pub fn cmd() cli.Command { // list prints out a list of all repositories. fn list(conf Config) ? { - repos := get_repos(conf.address, conf.api_key) ? + c := client.new(conf.address, conf.api_key) + repos := c.get_git_repos() ? for repo in repos { println('$repo.id\t$repo.url\t$repo.branch\t$repo.repo') @@ -128,7 +130,8 @@ fn list(conf Config) ? { // add adds a new repository to the server's list. fn add(conf Config, url string, branch string, repo string) ? { - res := add_repo(conf.address, conf.api_key, url, branch, repo, []) ? + c := client.new(conf.address, conf.api_key) + res := c.add_git_repo(url, branch, repo, []) ? println(res.message) } @@ -139,7 +142,8 @@ fn remove(conf Config, id string) ? { id_int := id.int() if id_int != 0 { - res := remove_repo(conf.address, conf.api_key, id_int) ? + c := client.new(conf.address, conf.api_key) + res := c.remove_git_repo(id_int) ? println(res.message) } } @@ -156,7 +160,9 @@ fn patch(conf Config, id string, params map[string]string) ? { id_int := id.int() if id_int != 0 { - res := patch_repo(conf.address, conf.api_key, id_int, params) ? + + c := client.new(conf.address, conf.api_key) + res := c.patch_git_repo(id_int, params) ? println(res.message) } @@ -170,6 +176,7 @@ fn info(conf Config, id string) ? { return } - repo := get_repo(conf.address, conf.api_key, id_int) ? + c := client.new(conf.address, conf.api_key) + repo := c.get_git_repo(id_int) ? println(repo) } diff --git a/src/git/client.v b/src/git/client.v deleted file mode 100644 index b5f8e9fd..00000000 --- a/src/git/client.v +++ /dev/null @@ -1,77 +0,0 @@ -module git - -import json -import response { Response } -import net.http -import db - -// send_request is a convenience method for sending requests to the repos -// API. It mostly does string manipulation to create a query string containing -// the provided params. -fn send_request(method http.Method, address string, url string, api_key string, params map[string]string) ?Response { - mut full_url := '$address$url' - - if params.len > 0 { - params_str := params.keys().map('$it=${params[it]}').join('&') - - full_url = '$full_url?$params_str' - } - - mut req := http.new_request(method, full_url, '') ? - req.add_custom_header('X-API-Key', api_key) ? - - res := req.do() ? - data := json.decode(Response, res.text) ? - - return data -} - -// get_repos returns the current list of repos. -pub fn get_repos(address string, api_key string) ?[]db.GitRepo { - data := send_request<[]db.GitRepo>(http.Method.get, address, '/api/repos', api_key, - {}) ? - - return data.data -} - -// get_repo returns the repo for a specific ID. -pub fn get_repo(address string, api_key string, id int) ?db.GitRepo { - data := send_request(http.Method.get, address, '/api/repos/$id', api_key, - {}) ? - - return data.data -} - -// add_repo adds a new repo to the server. -pub fn add_repo(address string, api_key string, url string, branch string, repo string, arch []string) ?Response { - mut params := { - 'url': url - 'branch': branch - 'repo': repo - } - - if arch.len > 0 { - params['arch'] = arch.join(',') - } - - data := send_request(http.Method.post, address, '/api/repos', api_key, params) ? - - return data -} - -// remove_repo removes the repo with the given ID from the server. -pub fn remove_repo(address string, api_key string, id int) ?Response { - data := send_request(http.Method.delete, address, '/api/repos/$id', api_key, - {}) ? - - return data -} - -// patch_repo sends a PATCH request to the given repo with the params as -// payload. -pub fn patch_repo(address string, api_key string, id int, params map[string]string) ?Response { - data := send_request(http.Method.patch, address, '/api/repos/$id', api_key, - params) ? - - return data -} From fa6603bd459c1f3c240d5f389e33441488cebcbf Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Sat, 7 May 2022 19:38:28 +0200 Subject: [PATCH 07/43] feat(client): added client code for logs API --- src/client/client.v | 32 ++++++++++++++++++++---------- src/client/git.v | 18 ++++++++--------- src/client/logs.v | 42 ++++++++++++++++++++++++++++++++++++++++ src/cron/daemon/daemon.v | 2 +- src/db/git.v | 6 ++---- src/db/logs.v | 2 +- src/git/cli.v | 3 +-- src/server/logs.v | 5 +++-- 8 files changed, 81 insertions(+), 29 deletions(-) create mode 100644 src/client/logs.v diff --git a/src/client/client.v b/src/client/client.v index 614b3dba..7446e91b 100644 --- a/src/client/client.v +++ b/src/client/client.v @@ -1,6 +1,7 @@ module client -import net.http +import net.http { Method } +import net.urllib import response { Response } import json @@ -17,24 +18,35 @@ pub fn new(address string, api_key string) Client { } } -// send_request is a convenience method for sending requests to the repos -// API. It mostly does string manipulation to create a query string containing -// the provided params. -fn (c &Client) send_request(method http.Method, url string, params map[string]string) ?Response { - mut full_url := '${c.address}$url' +// send_request just calls send_request_with_body with an empty body. +fn (c &Client) send_request(method Method, url string, params map[string]string) ?Response { + return c.send_request_with_body(method, url, params, '') +} + +// send_request_with_body is a convenience method for sending requests to +// the repos API. It mostly does string manipulation to create a query string +// containing the provided params. +fn (c &Client) send_request_with_body(method Method, url string, params map[string]string, body string) ?Response { + mut full_url := '$c.address$url' if params.len > 0 { - params_str := params.keys().map('$it=${params[it]}').join('&') + mut params_escaped := map[string]string{} + + // Escape each query param + for k, v in params { + params_escaped[k] = urllib.query_escape(v) + } + + params_str := params_escaped.keys().map('$it=${params[it]}').join('&') full_url = '$full_url?$params_str' } - mut req := http.new_request(method, full_url, '') ? - req.add_custom_header('X-API-Key', c.api_key) ? + mut req := http.new_request(method, full_url, body) ? + req.add_custom_header('X-Api-Key', c.api_key) ? res := req.do() ? data := json.decode(Response, res.text) ? return data } - diff --git a/src/client/git.v b/src/client/git.v index 5ed06200..7f4c27ad 100644 --- a/src/client/git.v +++ b/src/client/git.v @@ -1,19 +1,19 @@ module client -import db -import net.http +import db { GitRepo } +import net.http { Method } import response { Response } // get_repos returns the current list of repos. -pub fn (c &Client) get_git_repos() ?[]db.GitRepo { - data := c.send_request<[]db.GitRepo>(http.Method.get, '/api/repos', {}) ? +pub fn (c &Client) get_git_repos() ?[]GitRepo { + data := c.send_request<[]GitRepo>(Method.get, '/api/repos', {}) ? return data.data } // get_repo returns the repo for a specific ID. -pub fn (c &Client) get_git_repo(id int) ?db.GitRepo { - data := c.send_request(http.Method.get, '/api/repos/$id', {}) ? +pub fn (c &Client) get_git_repo(id int) ?GitRepo { + data := c.send_request(Method.get, '/api/repos/$id', {}) ? return data.data } @@ -30,14 +30,14 @@ pub fn (c &Client) add_git_repo(url string, branch string, repo string, arch []s params['arch'] = arch.join(',') } - data := c.send_request(http.Method.post, '/api/repos', params) ? + data := c.send_request(Method.post, '/api/repos', params) ? return data } // remove_repo removes the repo with the given ID from the server. pub fn (c &Client) remove_git_repo(id int) ?Response { - data := c.send_request(http.Method.delete, '/api/repos/$id', {}) ? + data := c.send_request(Method.delete, '/api/repos/$id', {}) ? return data } @@ -45,7 +45,7 @@ pub fn (c &Client) remove_git_repo(id int) ?Response { // patch_repo sends a PATCH request to the given repo with the params as // payload. pub fn (c &Client) patch_git_repo(id int, params map[string]string) ?Response { - data := c.send_request(http.Method.patch, '/api/repos/$id', params) ? + data := c.send_request(Method.patch, '/api/repos/$id', params) ? return data } diff --git a/src/client/logs.v b/src/client/logs.v new file mode 100644 index 00000000..9182f691 --- /dev/null +++ b/src/client/logs.v @@ -0,0 +1,42 @@ +module client + +import db { BuildLog } +import net.http { Method } +import response { Response } +import time + +pub fn (c &Client) get_build_logs() ?Response<[]BuildLog> { + data := c.send_request<[]BuildLog>(Method.get, '/api/logs', {}) ? + + return data +} + +pub fn (c &Client) get_build_logs_for_repo(repo_id int) ?Response<[]BuildLog> { + params := { + 'repo': repo_id.str() + } + + data := c.send_request<[]BuildLog>(Method.get, '/api/logs', params) ? + + return data +} + +pub fn (c &Client) get_build_log(id int) ?Response { + data := c.send_request(Method.get, '/api/logs/$id', {}) ? + + return data +} + +pub fn (c &Client) add_build_log(repo_id int, start_time time.Time, end_time time.Time, arch string, exit_code int, content string) ?Response { + params := { + 'repo': repo_id.str() + 'startTime': start_time.str() + 'endTime': end_time.str() + 'arch': arch + 'exitCode': exit_code.str() + } + + data := c.send_request_with_body(Method.post, '/api/logs', params, content) ? + + return data +} diff --git a/src/cron/daemon/daemon.v b/src/cron/daemon/daemon.v index 71fc5754..ade8fcbf 100644 --- a/src/cron/daemon/daemon.v +++ b/src/cron/daemon/daemon.v @@ -31,7 +31,7 @@ fn (r1 ScheduledBuild) < (r2 ScheduledBuild) bool { pub struct Daemon { mut: - client client.Client + client client.Client base_image string builder_images []string global_schedule CronExpression diff --git a/src/db/git.v b/src/db/git.v index 6eeecc8c..7aba2845 100644 --- a/src/db/git.v +++ b/src/db/git.v @@ -154,9 +154,7 @@ pub fn (db &VieterDb) update_git_repo_archs(repo_id int, archs []GitRepoArch) { } pub fn (db &VieterDb) git_repo_exists(repo_id int) bool { - db.get_git_repo(repo_id) or { - return false - } - + db.get_git_repo(repo_id) or { return false } + return true } diff --git a/src/db/logs.v b/src/db/logs.v index 589304e7..9ce28652 100644 --- a/src/db/logs.v +++ b/src/db/logs.v @@ -8,7 +8,7 @@ pub: repo_id int [nonull] start_time time.Time [nonull] end_time time.Time [nonull] - arch string [nonull] + arch string [nonull] exit_code int [nonull] } diff --git a/src/git/cli.v b/src/git/cli.v index bdc0479d..3bf78d10 100644 --- a/src/git/cli.v +++ b/src/git/cli.v @@ -160,7 +160,6 @@ fn patch(conf Config, id string, params map[string]string) ? { id_int := id.int() if id_int != 0 { - c := client.new(conf.address, conf.api_key) res := c.patch_git_repo(id_int, params) ? @@ -176,7 +175,7 @@ fn info(conf Config, id string) ? { return } - c := client.new(conf.address, conf.api_key) + c := client.new(conf.address, conf.api_key) repo := c.get_git_repo(id_int) ? println(repo) } diff --git a/src/server/logs.v b/src/server/logs.v index 10b734da..f53464dc 100644 --- a/src/server/logs.v +++ b/src/server/logs.v @@ -29,7 +29,7 @@ fn (mut app App) get_single_log(id int) web.Result { if !app.is_authorized() { return app.json(http.Status.unauthorized, new_response('Unauthorized.')) } - + log := app.db.get_build_log(id) or { return app.not_found() } return app.json(http.Status.ok, new_data_response(log)) @@ -43,7 +43,8 @@ fn (mut app App) get_log_contents(id int) web.Result { log := app.db.get_build_log(id) or { return app.not_found() } file_name := log.start_time.custom_format('YYYY-MM-DD_HH-mm-ss') - full_path := os.join_path(app.conf.data_dir, server.logs_dir_name, log.repo_id.str(), log.arch, file_name) + full_path := os.join_path(app.conf.data_dir, logs_dir_name, log.repo_id.str(), log.arch, + file_name) return app.file(full_path) } From 5b016df85ddcef017648039601ff08668d329ba8 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Sat, 7 May 2022 21:50:20 +0200 Subject: [PATCH 08/43] feat(cli): added commands for interacting with build logs --- src/client/client.v | 32 ++++++--- src/client/logs.v | 6 ++ src/console/console.v | 1 + src/{git/cli.v => console/git/git.v} | 0 src/console/logs/logs.v | 99 ++++++++++++++++++++++++++++ src/db/logs.v | 14 ++++ src/main.v | 4 +- 7 files changed, 145 insertions(+), 11 deletions(-) create mode 100644 src/console/console.v rename src/{git/cli.v => console/git/git.v} (100%) create mode 100644 src/console/logs/logs.v diff --git a/src/client/client.v b/src/client/client.v index 7446e91b..12c92d38 100644 --- a/src/client/client.v +++ b/src/client/client.v @@ -18,15 +18,7 @@ pub fn new(address string, api_key string) Client { } } -// send_request just calls send_request_with_body with an empty body. -fn (c &Client) send_request(method Method, url string, params map[string]string) ?Response { - return c.send_request_with_body(method, url, params, '') -} - -// send_request_with_body is a convenience method for sending requests to -// the repos API. It mostly does string manipulation to create a query string -// containing the provided params. -fn (c &Client) send_request_with_body(method Method, url string, params map[string]string, body string) ?Response { +fn (c &Client) send_request_raw(method Method, url string, params map[string]string, body string) ?http.Response { mut full_url := '$c.address$url' if params.len > 0 { @@ -46,7 +38,27 @@ fn (c &Client) send_request_with_body(method Method, url string, params map[s req.add_custom_header('X-Api-Key', c.api_key) ? res := req.do() ? - data := json.decode(Response, res.text) ? + + return res +} + +// send_request just calls send_request_with_body with an empty body. +fn (c &Client) send_request(method Method, url string, params map[string]string) ?Response { + return c.send_request_with_body(method, url, params, '') +} + +// send_request_with_body is a convenience method for sending requests to +// the repos API. It mostly does string manipulation to create a query string +// containing the provided params. +fn (c &Client) send_request_with_body(method Method, url string, params map[string]string, body string) ?Response { + res_text := c.send_request_raw_response(method, url, params, body) ? + data := json.decode(Response, res_text) ? return data } + +fn (c &Client) send_request_raw_response(method Method, url string, params map[string]string, body string) ?string { + res := c.send_request_raw(method, url, params, body) ? + + return res.text +} diff --git a/src/client/logs.v b/src/client/logs.v index 9182f691..19575a6e 100644 --- a/src/client/logs.v +++ b/src/client/logs.v @@ -27,6 +27,12 @@ pub fn (c &Client) get_build_log(id int) ?Response { return data } +pub fn (c &Client) get_build_log_content(id int) ?string { + data := c.send_request_raw_response(Method.get, '/api/logs/$id/content', {}, '') ? + + return data +} + pub fn (c &Client) add_build_log(repo_id int, start_time time.Time, end_time time.Time, arch string, exit_code int, content string) ?Response { params := { 'repo': repo_id.str() diff --git a/src/console/console.v b/src/console/console.v new file mode 100644 index 00000000..6f296bdd --- /dev/null +++ b/src/console/console.v @@ -0,0 +1 @@ +module console diff --git a/src/git/cli.v b/src/console/git/git.v similarity index 100% rename from src/git/cli.v rename to src/console/git/git.v diff --git a/src/console/logs/logs.v b/src/console/logs/logs.v new file mode 100644 index 00000000..14aeddf3 --- /dev/null +++ b/src/console/logs/logs.v @@ -0,0 +1,99 @@ +module logs + +import cli +import env +import client +import db + +struct Config { + address string [required] + api_key string [required] +} + +pub fn cmd() cli.Command { + return cli.Command{ + name: 'logs' + description: 'Interact with the build logs API.' + commands: [ + cli.Command{ + name: 'list' + description: 'List the build logs. If a repo ID is provided, only list the build logs for that repo.' + flags: [ + cli.Flag{ + name: 'repo' + description: 'ID of the Git repo to restrict list to.' + flag: cli.FlagType.int + }, + ] + execute: fn (cmd cli.Command) ? { + config_file := cmd.flags.get_string('config-file') ? + conf := env.load(config_file) ? + + repo_id := cmd.flags.get_int('repo') ? + + if repo_id == 0 { list(conf) ? } else { list_for_repo(conf, repo_id) ? } + } + }, + cli.Command{ + name: 'info' + required_args: 1 + usage: 'id' + description: 'Show all info for a specific build log.' + execute: fn (cmd cli.Command) ? { + config_file := cmd.flags.get_string('config-file') ? + conf := env.load(config_file) ? + + id := cmd.args[0].int() + info(conf, id) ? + } + }, + cli.Command{ + name: 'content' + required_args: 1 + usage: 'id' + description: 'Output the content of a build log to stdout.' + execute: fn (cmd cli.Command) ? { + config_file := cmd.flags.get_string('config-file') ? + conf := env.load(config_file) ? + + id := cmd.args[0].int() + content(conf, id) ? + } + }, + ] + } +} + +fn print_log_list(logs []db.BuildLog) { + for log in logs { + println('$log.id\t$log.start_time\t$log.exit_code') + } +} + +fn list(conf Config) ? { + c := client.new(conf.address, conf.api_key) + logs := c.get_build_logs() ?.data + + print_log_list(logs) +} + +fn list_for_repo(conf Config, repo_id int) ? { + c := client.new(conf.address, conf.api_key) + logs := c.get_build_logs_for_repo(repo_id) ?.data + + print_log_list(logs) +} + +fn info(conf Config, id int) ? { + c := client.new(conf.address, conf.api_key) + log := c.get_build_log(id) ?.data + + print(log) +} + +fn content(conf Config, id int) ? { + c := client.new(conf.address, conf.api_key) + content := c.get_build_log_content(id) ? + + println(content) +} diff --git a/src/db/logs.v b/src/db/logs.v index 9ce28652..05973af1 100644 --- a/src/db/logs.v +++ b/src/db/logs.v @@ -12,6 +12,20 @@ pub: exit_code int [nonull] } +pub fn (bl &BuildLog) str() string { + mut parts := [ + 'id: $bl.id', + 'repo id: $bl.repo_id', + 'start time: $bl.start_time', + 'end time: $bl.end_time', + 'arch: $bl.arch', + 'exit code: $bl.exit_code', + ] + str := parts.join('\n') + + return str +} + // get_build_logs returns all BuildLog's in the database. pub fn (db &VieterDb) get_build_logs() []BuildLog { res := sql db.conn { diff --git a/src/main.v b/src/main.v index 4ba6d30f..41d0d331 100644 --- a/src/main.v +++ b/src/main.v @@ -4,7 +4,8 @@ import os import server import cli import build -import git +import console.git +import console.logs import cron fn main() { @@ -27,6 +28,7 @@ fn main() { build.cmd(), git.cmd(), cron.cmd(), + logs.cmd(), ] } From 5f7d7c47801b3b6145e945c9a69c3d891085f593 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Sat, 7 May 2022 22:06:17 +0200 Subject: [PATCH 09/43] doc: added documentation to all functions --- src/client/client.v | 9 ++++++--- src/client/git.v | 10 +++++----- src/client/logs.v | 5 +++++ src/console/logs/logs.v | 7 +++++++ src/db/git.v | 2 ++ src/db/logs.v | 1 + src/server/logs.v | 7 ++++++- 7 files changed, 32 insertions(+), 9 deletions(-) diff --git a/src/client/client.v b/src/client/client.v index 12c92d38..25224a51 100644 --- a/src/client/client.v +++ b/src/client/client.v @@ -11,6 +11,7 @@ pub: api_key string } +// new creates a new Client instance. pub fn new(address string, api_key string) Client { return Client{ address: address @@ -18,6 +19,8 @@ pub fn new(address string, api_key string) Client { } } +// send_request_raw sends an HTTP request, returning the http.Response object. +// It encodes the params so that they're safe to pass as HTTP query parameters. fn (c &Client) send_request_raw(method Method, url string, params map[string]string, body string) ?http.Response { mut full_url := '$c.address$url' @@ -47,9 +50,8 @@ fn (c &Client) send_request(method Method, url string, params map[string]stri return c.send_request_with_body(method, url, params, '') } -// send_request_with_body is a convenience method for sending requests to -// the repos API. It mostly does string manipulation to create a query string -// containing the provided params. +// send_request_with_body calls send_request_raw_response & parses its +// output as a Response object. fn (c &Client) send_request_with_body(method Method, url string, params map[string]string, body string) ?Response { res_text := c.send_request_raw_response(method, url, params, body) ? data := json.decode(Response, res_text) ? @@ -57,6 +59,7 @@ fn (c &Client) send_request_with_body(method Method, url string, params map[s return data } +// send_request_raw_response returns the raw text response for an HTTP request. fn (c &Client) send_request_raw_response(method Method, url string, params map[string]string, body string) ?string { res := c.send_request_raw(method, url, params, body) ? diff --git a/src/client/git.v b/src/client/git.v index 7f4c27ad..b09d4c22 100644 --- a/src/client/git.v +++ b/src/client/git.v @@ -4,21 +4,21 @@ import db { GitRepo } import net.http { Method } import response { Response } -// get_repos returns the current list of repos. +// get_git_repos returns the current list of repos. pub fn (c &Client) get_git_repos() ?[]GitRepo { data := c.send_request<[]GitRepo>(Method.get, '/api/repos', {}) ? return data.data } -// get_repo returns the repo for a specific ID. +// get_git_repo returns the repo for a specific ID. pub fn (c &Client) get_git_repo(id int) ?GitRepo { data := c.send_request(Method.get, '/api/repos/$id', {}) ? return data.data } -// add_repo adds a new repo to the server. +// add_git_repo adds a new repo to the server. pub fn (c &Client) add_git_repo(url string, branch string, repo string, arch []string) ?Response { mut params := { 'url': url @@ -35,14 +35,14 @@ pub fn (c &Client) add_git_repo(url string, branch string, repo string, arch []s return data } -// remove_repo removes the repo with the given ID from the server. +// remove_git_repo removes the repo with the given ID from the server. pub fn (c &Client) remove_git_repo(id int) ?Response { data := c.send_request(Method.delete, '/api/repos/$id', {}) ? return data } -// patch_repo sends a PATCH request to the given repo with the params as +// patch_git_repo sends a PATCH request to the given repo with the params as // payload. pub fn (c &Client) patch_git_repo(id int, params map[string]string) ?Response { data := c.send_request(Method.patch, '/api/repos/$id', params) ? diff --git a/src/client/logs.v b/src/client/logs.v index 19575a6e..8c532138 100644 --- a/src/client/logs.v +++ b/src/client/logs.v @@ -5,12 +5,14 @@ import net.http { Method } import response { Response } import time +// get_build_logs returns all build logs. pub fn (c &Client) get_build_logs() ?Response<[]BuildLog> { data := c.send_request<[]BuildLog>(Method.get, '/api/logs', {}) ? return data } +// get_build_logs_for_repo returns all build logs for a given repo. pub fn (c &Client) get_build_logs_for_repo(repo_id int) ?Response<[]BuildLog> { params := { 'repo': repo_id.str() @@ -21,18 +23,21 @@ pub fn (c &Client) get_build_logs_for_repo(repo_id int) ?Response<[]BuildLog> { return data } +// get_build_log returns a specific build log. pub fn (c &Client) get_build_log(id int) ?Response { data := c.send_request(Method.get, '/api/logs/$id', {}) ? return data } +// get_build_log_content returns the contents of the build log file. pub fn (c &Client) get_build_log_content(id int) ?string { data := c.send_request_raw_response(Method.get, '/api/logs/$id/content', {}, '') ? return data } +// add_build_log adds a new build log to the server. pub fn (c &Client) add_build_log(repo_id int, start_time time.Time, end_time time.Time, arch string, exit_code int, content string) ?Response { params := { 'repo': repo_id.str() diff --git a/src/console/logs/logs.v b/src/console/logs/logs.v index 14aeddf3..e3c7d14a 100644 --- a/src/console/logs/logs.v +++ b/src/console/logs/logs.v @@ -10,6 +10,7 @@ struct Config { api_key string [required] } +// cmd returns the cli module that handles the build repos API. pub fn cmd() cli.Command { return cli.Command{ name: 'logs' @@ -64,12 +65,14 @@ pub fn cmd() cli.Command { } } +// print_log_list prints a list of logs. fn print_log_list(logs []db.BuildLog) { for log in logs { println('$log.id\t$log.start_time\t$log.exit_code') } } +// list prints a list of all build logs. fn list(conf Config) ? { c := client.new(conf.address, conf.api_key) logs := c.get_build_logs() ?.data @@ -77,6 +80,7 @@ fn list(conf Config) ? { print_log_list(logs) } +// list prints a list of all build logs for a given repo. fn list_for_repo(conf Config, repo_id int) ? { c := client.new(conf.address, conf.api_key) logs := c.get_build_logs_for_repo(repo_id) ?.data @@ -84,6 +88,7 @@ fn list_for_repo(conf Config, repo_id int) ? { print_log_list(logs) } +// info print the detailed info for a given build log. fn info(conf Config, id int) ? { c := client.new(conf.address, conf.api_key) log := c.get_build_log(id) ?.data @@ -91,6 +96,8 @@ fn info(conf Config, id int) ? { print(log) } +// content outputs the contents of the log file for a given build log to +// stdout. fn content(conf Config, id int) ? { c := client.new(conf.address, conf.api_key) content := c.get_build_log_content(id) ? diff --git a/src/db/git.v b/src/db/git.v index 7aba2845..9a475a54 100644 --- a/src/db/git.v +++ b/src/db/git.v @@ -153,6 +153,8 @@ pub fn (db &VieterDb) update_git_repo_archs(repo_id int, archs []GitRepoArch) { } } +// git_repo_exists is a utility function that checks whether a repo with the +// given id exists. pub fn (db &VieterDb) git_repo_exists(repo_id int) bool { db.get_git_repo(repo_id) or { return false } diff --git a/src/db/logs.v b/src/db/logs.v index 05973af1..817db78b 100644 --- a/src/db/logs.v +++ b/src/db/logs.v @@ -12,6 +12,7 @@ pub: exit_code int [nonull] } +// str returns a string representation. pub fn (bl &BuildLog) str() string { mut parts := [ 'id: $bl.id', diff --git a/src/server/logs.v b/src/server/logs.v index f53464dc..b048dc4f 100644 --- a/src/server/logs.v +++ b/src/server/logs.v @@ -9,6 +9,8 @@ import time import os import util +// get_logs returns all build logs in the database. A 'repo' query param can +// optionally be added to limit the list of build logs to that repository. ['/api/logs'; get] fn (mut app App) get_logs() web.Result { if !app.is_authorized() { @@ -24,6 +26,7 @@ fn (mut app App) get_logs() web.Result { return app.json(http.Status.ok, new_data_response(logs)) } +// get_single_log returns the build log with the given id. ['/api/logs/:id'; get] fn (mut app App) get_single_log(id int) web.Result { if !app.is_authorized() { @@ -35,8 +38,9 @@ fn (mut app App) get_single_log(id int) web.Result { return app.json(http.Status.ok, new_data_response(log)) } +// get_log_content returns the actual build log file for the given id. ['/api/logs/:id/content'; get] -fn (mut app App) get_log_contents(id int) web.Result { +fn (mut app App) get_log_content(id int) web.Result { if !app.is_authorized() { return app.json(http.Status.unauthorized, new_response('Unauthorized.')) } @@ -58,6 +62,7 @@ fn parse_query_time(query string) ?time.Time { return t } +// post_log adds a new log to the database. ['/api/logs'; post] fn (mut app App) post_log() web.Result { if !app.is_authorized() { From 30cce4fa72c4eaf632dc7a329392f1dd05edd326 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Sat, 7 May 2022 22:13:35 +0200 Subject: [PATCH 10/43] chore: updated changelog --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2bbe4f06..7d9eb4d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased](https://git.rustybever.be/vieter/vieter/src/branch/dev) + +### Added + +* Web API for adding & querying build logs +* CLI commands to access build logs API + ## [0.3.0-alpha.1](https://git.rustybever.be/vieter/vieter/src/tag/0.3.0-alpha.1) ### Changed From 27aa215effb206e2256ac67308935bc2340a0bdb Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Sun, 8 May 2022 10:29:06 +0200 Subject: [PATCH 11/43] feat(docker): added function to retrieve container logs --- src/docker/containers.v | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/docker/containers.v b/src/docker/containers.v index d0f5a4d7..81343702 100644 --- a/src/docker/containers.v +++ b/src/docker/containers.v @@ -76,3 +76,23 @@ pub fn remove_container(id string) ?bool { return res.status_code == 204 } + +pub fn get_container_logs(id string) ?string { + res := request('GET', urllib.parse('/v1.41/containers/$id/logs?stdout=true&stderr=true') ?) ? + mut res_bytes := res.text.bytes() + + // Docker uses a special "stream" format for their logs, so we have to + // clean up the data. + mut index := 0 + + for index < res_bytes.len { + // The reverse is required because V reads in the bytes differently + t := res_bytes[index + 4..index + 8].reverse() + len_length := unsafe { *(&u32(&t[0])) } + + res_bytes.delete_many(index, 8) + index += int(len_length) + } + + return res_bytes.bytestr() +} From 4b172cb5d8e9dc8d96f7c73fc84e0d7aa1b8b5ab Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Sun, 8 May 2022 13:17:54 +0200 Subject: [PATCH 12/43] feat(cli): `vieter build` now builds a single repo & uploads build logs --- src/build/build.v | 56 +++++++++++++++++++++++++---------------- src/build/cli.v | 8 ++++-- src/docker/containers.v | 29 +++++++++++++++++++-- 3 files changed, 68 insertions(+), 25 deletions(-) diff --git a/src/build/build.v b/src/build/build.v index 6f033e67..9505171c 100644 --- a/src/build/build.v +++ b/src/build/build.v @@ -73,10 +73,17 @@ pub fn create_build_image(base_image string) ?string { return image.id } +struct BuildResult { + start_time time.Time + end_time time.Time + exit_code int + logs string +} + // build_repo builds, packages & publishes a given Arch package based on the // provided GitRepo. The base image ID should be of an image previously created -// by create_build_image. -pub fn build_repo(address string, api_key string, base_image_id string, repo &db.GitRepo) ? { +// by create_build_image. It returns the logs of the container. +pub fn build_repo(address string, api_key string, base_image_id string, repo &db.GitRepo) ?BuildResult { build_arch := os.uname().machine // TODO what to do with PKGBUILDs that build multiple packages? @@ -107,43 +114,50 @@ pub fn build_repo(address string, api_key string, base_image_id string, repo &db id := docker.create_container(c) ? docker.start_container(id) ? + mut data := docker.inspect_container(id) ? + // This loop waits until the container has stopped, so we can remove it after for { - data := docker.inspect_container(id) ? - if !data.state.running { break } time.sleep(1 * time.second) + + data = docker.inspect_container(id) ? } + logs := docker.get_container_logs(id) ? + docker.remove_container(id) ? + + return BuildResult{ + start_time: data.state.start_time + end_time: data.state.end_time + exit_code: data.state.exit_code + logs: logs + } } // build builds every Git repo in the server's list. -fn build(conf Config) ? { +fn build(conf Config, repo_id int) ? { + c := client.new(conf.address, conf.api_key) + repo := c.get_git_repo(repo_id) ? + build_arch := os.uname().machine - // We get the repos map from the Vieter instance - repos := client.new(conf.address, conf.api_key).get_git_repos() ? - - // We filter out any repos that aren't allowed to be built on this - // architecture - filtered_repos := repos.filter(it.arch.map(it.value).contains(build_arch)) - - // No point in doing work if there's no repos present - if filtered_repos.len == 0 { - return - } - // First, we create a base image which has updated repos n stuff + println('Creating base image...') image_id := create_build_image(conf.base_image) ? - for repo in filtered_repos { - build_repo(conf.address, conf.api_key, image_id, repo) ? - } + println('Running build...') + res := build_repo(conf.address, conf.api_key, image_id, repo) ? - // Finally, we remove the builder image + // Remove the builder image + println('Removing build image...') docker.remove_image(image_id) ? + + // Upload the build log to the Vieter instance + println('Uploading logs to Vieter...') + c.add_build_log(repo.id, res.start_time, res.end_time, build_arch, res.exit_code, res.logs) ? } diff --git a/src/build/cli.v b/src/build/cli.v index 01313960..5247e871 100644 --- a/src/build/cli.v +++ b/src/build/cli.v @@ -14,12 +14,16 @@ pub: pub fn cmd() cli.Command { return cli.Command{ name: 'build' - description: 'Run the build process.' + required_args: 1 + usage: 'id' + description: 'Build the repository with the given ID.' execute: fn (cmd cli.Command) ? { config_file := cmd.flags.get_string('config-file') ? conf := env.load(config_file) ? - build(conf) ? + id := cmd.args[0].int() + + build(conf, id) ? } } } diff --git a/src/docker/containers.v b/src/docker/containers.v index 81343702..63095a6b 100644 --- a/src/docker/containers.v +++ b/src/docker/containers.v @@ -2,6 +2,8 @@ module docker import json import net.urllib +import regex +import time struct Container { id string [json: Id] @@ -49,13 +51,28 @@ pub fn start_container(id string) ?bool { } struct ContainerInspect { -pub: +pub mut: state ContainerState [json: State] } struct ContainerState { pub: running bool [json: Running] + status string [json: Status] + exit_code int [json: ExitCode] + // These use a rather specific format so they have to be parsed later + start_time_str string [json: StartedAt] + end_time_str string [json: FinishedAt] +pub mut: + start_time time.Time [skip] + end_time time.Time [skip] +} + +fn docker_timestamp_to_time(s string) ?time.Time { + parts := s.split('.') + clipped := parts[0] + '.' + parts[1][..3] + + return time.parse_rfc3339(clipped) } // inspect_container returns the result of inspecting a container with a given @@ -67,7 +84,15 @@ pub fn inspect_container(id string) ?ContainerInspect { return error('Failed to inspect container.') } - return json.decode(ContainerInspect, res.text) or {} + mut data := json.decode(ContainerInspect, res.text) ? + + data.state.start_time = docker_timestamp_to_time(data.state.start_time_str) ? + + if data.state.status == "exited" { + data.state.end_time = docker_timestamp_to_time(data.state.end_time_str) ? + } + + return data } // remove_container removes a container with a given ID. From e79d18100f38599429006e3c07406e0526cd0b74 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Sun, 8 May 2022 14:53:35 +0200 Subject: [PATCH 13/43] chore: ran `make fmt` --- src/build/build.v | 14 ++++++-------- src/docker/containers.v | 12 ++++++------ 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/src/build/build.v b/src/build/build.v index 9505171c..c97a5f7e 100644 --- a/src/build/build.v +++ b/src/build/build.v @@ -75,9 +75,9 @@ pub fn create_build_image(base_image string) ?string { struct BuildResult { start_time time.Time - end_time time.Time - exit_code int - logs string + end_time time.Time + exit_code int + logs string } // build_repo builds, packages & publishes a given Arch package based on the @@ -94,7 +94,7 @@ pub fn build_repo(address string, api_key string, base_image_id string, repo &db 'source PKGBUILD', // The build container checks whether the package is already // present on the server - 'curl --head --fail $address/$repo.repo/$build_arch/\$pkgname-\$pkgver-\$pkgrel && exit 0', + 'curl -s --head --fail $address/$repo.repo/$build_arch/\$pkgname-\$pkgver-\$pkgrel && exit 0', 'MAKEFLAGS="-j\$(nproc)" makepkg -s --noconfirm --needed && for pkg in \$(ls -1 *.pkg*); do curl -XPOST -T "\$pkg" -H "X-API-KEY: \$API_KEY" $address/$repo.repo/publish; done', ] @@ -146,18 +146,16 @@ fn build(conf Config, repo_id int) ? { build_arch := os.uname().machine - // First, we create a base image which has updated repos n stuff println('Creating base image...') image_id := create_build_image(conf.base_image) ? println('Running build...') res := build_repo(conf.address, conf.api_key, image_id, repo) ? - // Remove the builder image println('Removing build image...') docker.remove_image(image_id) ? - // Upload the build log to the Vieter instance println('Uploading logs to Vieter...') - c.add_build_log(repo.id, res.start_time, res.end_time, build_arch, res.exit_code, res.logs) ? + c.add_build_log(repo.id, res.start_time, res.end_time, build_arch, res.exit_code, + res.logs) ? } diff --git a/src/docker/containers.v b/src/docker/containers.v index 63095a6b..6d2eb3e5 100644 --- a/src/docker/containers.v +++ b/src/docker/containers.v @@ -57,15 +57,15 @@ pub mut: struct ContainerState { pub: - running bool [json: Running] - status string [json: Status] - exit_code int [json: ExitCode] + running bool [json: Running] + status string [json: Status] + exit_code int [json: ExitCode] // These use a rather specific format so they have to be parsed later start_time_str string [json: StartedAt] - end_time_str string [json: FinishedAt] + end_time_str string [json: FinishedAt] pub mut: start_time time.Time [skip] - end_time time.Time [skip] + end_time time.Time [skip] } fn docker_timestamp_to_time(s string) ?time.Time { @@ -88,7 +88,7 @@ pub fn inspect_container(id string) ?ContainerInspect { data.state.start_time = docker_timestamp_to_time(data.state.start_time_str) ? - if data.state.status == "exited" { + if data.state.status == 'exited' { data.state.end_time = docker_timestamp_to_time(data.state.end_time_str) ? } From ea4c4fce1650543bcbf22e20d6ce01015120500f Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Sun, 8 May 2022 15:07:54 +0200 Subject: [PATCH 14/43] feat(cron): upload logs after build --- src/build/build.v | 3 ++- src/cron/daemon/build.v | 12 ++++++++++-- src/docker/containers.v | 1 - 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/build/build.v b/src/build/build.v index c97a5f7e..774591d6 100644 --- a/src/build/build.v +++ b/src/build/build.v @@ -73,7 +73,8 @@ pub fn create_build_image(base_image string) ?string { return image.id } -struct BuildResult { +pub struct BuildResult { +pub: start_time time.Time end_time time.Time exit_code int diff --git a/src/cron/daemon/build.v b/src/cron/daemon/build.v index d107fd35..aa08f9f3 100644 --- a/src/cron/daemon/build.v +++ b/src/cron/daemon/build.v @@ -3,6 +3,7 @@ module daemon import time import sync.stdatomic import build +import os const ( build_empty = 0 @@ -77,13 +78,20 @@ fn (mut d Daemon) run_build(build_index int, sb ScheduledBuild) { // 0 means success, 1 means failure mut status := 0 - build.build_repo(d.client.address, d.client.api_key, d.builder_images.last(), &sb.repo) or { + res := build.build_repo(d.client.address, d.client.api_key, d.builder_images.last(), + &sb.repo) or { d.ldebug('build_repo error: $err.msg()') status = 1 + + build.BuildResult{} } if status == 0 { - d.linfo('finished build: $sb.repo.url $sb.repo.branch') + d.linfo('finished build: $sb.repo.url $sb.repo.branch; uploading logs...') + + build_arch := os.uname().machine + d.client.add_build_log(sb.repo.id, res.start_time, res.end_time, build_arch, res.exit_code, + res.logs) or { d.lerror('Failed to upload logs for $sb.repo.url $sb.repo.arch') } } else { d.linfo('failed build: $sb.repo.url $sb.repo.branch') } diff --git a/src/docker/containers.v b/src/docker/containers.v index 6d2eb3e5..fe0bb7b1 100644 --- a/src/docker/containers.v +++ b/src/docker/containers.v @@ -2,7 +2,6 @@ module docker import json import net.urllib -import regex import time struct Container { From 5a5f7f83461f20785e18b006e8033316a9f24d9c Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Mon, 9 May 2022 14:58:20 +0200 Subject: [PATCH 15/43] refactor(docker): use builtin parse_rfc3339 function --- src/docker/containers.v | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/src/docker/containers.v b/src/docker/containers.v index fe0bb7b1..2258f3bd 100644 --- a/src/docker/containers.v +++ b/src/docker/containers.v @@ -67,13 +67,6 @@ pub mut: end_time time.Time [skip] } -fn docker_timestamp_to_time(s string) ?time.Time { - parts := s.split('.') - clipped := parts[0] + '.' + parts[1][..3] - - return time.parse_rfc3339(clipped) -} - // inspect_container returns the result of inspecting a container with a given // ID. pub fn inspect_container(id string) ?ContainerInspect { @@ -85,10 +78,10 @@ pub fn inspect_container(id string) ?ContainerInspect { mut data := json.decode(ContainerInspect, res.text) ? - data.state.start_time = docker_timestamp_to_time(data.state.start_time_str) ? + data.state.start_time = time.parse_rfc3339(data.state.start_time_str) ? if data.state.status == 'exited' { - data.state.end_time = docker_timestamp_to_time(data.state.end_time_str) ? + data.state.end_time = time.parse_rfc3339(data.state.end_time_str) ? } return data @@ -101,6 +94,8 @@ pub fn remove_container(id string) ?bool { return res.status_code == 204 } +// get_container_logs retrieves the logs for a Docker container, both stdout & +// stderr. pub fn get_container_logs(id string) ?string { res := request('GET', urllib.parse('/v1.41/containers/$id/logs?stdout=true&stderr=true') ?) ? mut res_bytes := res.text.bytes() From 3821ed29fd4d0dd9fda4447dab444baf40d5cd6b Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Mon, 9 May 2022 15:05:53 +0200 Subject: [PATCH 16/43] refactor(docker): simplified loop expression --- src/build/build.v | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/build/build.v b/src/build/build.v index 774591d6..0a978aa0 100644 --- a/src/build/build.v +++ b/src/build/build.v @@ -118,11 +118,7 @@ pub fn build_repo(address string, api_key string, base_image_id string, repo &db mut data := docker.inspect_container(id) ? // This loop waits until the container has stopped, so we can remove it after - for { - if !data.state.running { - break - } - + for data.state.running { time.sleep(1 * time.second) data = docker.inspect_container(id) ? From 78fc3afcd3b53860c21e227fc452a876567976cd Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Mon, 9 May 2022 15:16:30 +0200 Subject: [PATCH 17/43] feat(ci): also publish dev images as specific commit hash --- .woodpecker/.docker.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.woodpecker/.docker.yml b/.woodpecker/.docker.yml index bab869b4..f31490a1 100644 --- a/.woodpecker/.docker.yml +++ b/.woodpecker/.docker.yml @@ -11,7 +11,9 @@ pipeline: - 'docker_password' settings: repo: 'chewingbever/vieter' - tag: 'dev' + tags: + - 'dev' + - ${CI_COMMIT_SHA} platforms: [ 'linux/arm64/v8', 'linux/amd64' ] build_args_from_env: - 'CI_COMMIT_SHA' From 0bac221aee85bc24514733eee3f5280a6b1bce41 Mon Sep 17 00:00:00 2001 From: LordMZTE Date: Tue, 10 May 2022 13:36:07 +0200 Subject: [PATCH 18/43] fix: don't pass --nodeps to initial build step (#173) This fixes packages that require their dependencies in `pkgver` or `prepare` failing to build. Reviewed-on: https://git.rustybever.be/vieter/vieter/pulls/173 Co-authored-by: LordMZTE Co-committed-by: LordMZTE --- docs/content/builder.md | 2 +- src/build/build.v | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/content/builder.md b/docs/content/builder.md index 6a1bc3ab..659717df 100644 --- a/docs/content/builder.md +++ b/docs/content/builder.md @@ -15,7 +15,7 @@ repositories. After the image has been created, each repository returned by previously created image as a base. Each container goes through the following steps: 1. The repository is cloned -2. `makepkg --nobuild --nodeps` is ran to update the `pkgver` variable inside +2. `makepkg --nobuild --syncdeps --needed --noconfirm` is ran to update the `pkgver` variable inside the `PKGBUILD` file 3. A HEAD request is sent to the Vieter server to check whether the specific version of the package is already present. If it is, the container exits. diff --git a/src/build/build.v b/src/build/build.v index 0a978aa0..7bd63a78 100644 --- a/src/build/build.v +++ b/src/build/build.v @@ -91,7 +91,7 @@ pub fn build_repo(address string, api_key string, base_image_id string, repo &db commands := [ 'git clone --single-branch --depth 1 --branch $repo.branch $repo.url repo', 'cd repo', - 'makepkg --nobuild --nodeps', + 'makepkg --nobuild --syncdeps --needed --noconfirm', 'source PKGBUILD', // The build container checks whether the package is already // present on the server From c018aad14382fd32746d5f245290375b25501b98 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Tue, 10 May 2022 13:43:09 +0200 Subject: [PATCH 19/43] chore: updated CHANGELOG --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d9eb4d6..da09449b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Web API for adding & querying build logs * CLI commands to access build logs API +* Cron build logs are uploaded to above API + +### Changed + +- `vieter build` command now only builds a single repository & uploads the + build logs ## [0.3.0-alpha.1](https://git.rustybever.be/vieter/vieter/src/tag/0.3.0-alpha.1) From ad207bdb7024ed37b63acce7409f7a5a5e359725 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Tue, 10 May 2022 19:30:26 +0200 Subject: [PATCH 20/43] feat(ci): split Arch releases into vieter & vieter-git --- .woodpecker/.arch-rel.yml | 39 +++++++++++++++++++++++++++++++++++++++ .woodpecker/.arch.yml | 2 +- CHANGELOG.md | 5 ++++- PKGBUILD | 17 ++++++----------- PKGBUILD.dev | 35 +++++++++++++++++++++++++++++++++++ 5 files changed, 85 insertions(+), 13 deletions(-) create mode 100644 .woodpecker/.arch-rel.yml create mode 100644 PKGBUILD.dev diff --git a/.woodpecker/.arch-rel.yml b/.woodpecker/.arch-rel.yml new file mode 100644 index 00000000..b8f4c7ae --- /dev/null +++ b/.woodpecker/.arch-rel.yml @@ -0,0 +1,39 @@ +matrix: + PLATFORM: + - linux/amd64 + - linux/arm64 + +platform: ${PLATFORM} +branches: [main] +skip_clone: true + +pipeline: + build: + image: 'menci/archlinuxarm:base-devel' + commands: + # Add the vieter repository so we can use the compiler + - echo -e '[vieter]\nServer = https://arch.r8r.be/$repo/$arch\nSigLevel = Optional' >> /etc/pacman.conf + # Update packages + - pacman -Syu --noconfirm + # Create non-root user to perform build & switch to their home + - groupadd -g 1000 builder + - useradd -mg builder builder + - chown -R builder:builder "$PWD" + - "echo 'builder ALL=(ALL) NOPASSWD: ALL' >> /etc/sudoers" + - su builder + # Due to a bug with the V compiler, we can't just use the PKGBUILD from + # inside the repo + - curl -OL "https://git.rustybever.be/vieter/vieter/raw/tag/$CI_COMMIT_TAG/PKGBUILD" + - makepkg -s --noconfirm --needed + when: + event: tag + + publish: + image: 'curlimages/curl' + commands: + # Publish the package + - 'for pkg in $(ls -1 *.pkg*); do curl -XPOST -T "$pkg" -H "X-API-KEY: $VIETER_API_KEY" https://arch.r8r.be/vieter/publish; done' + secrets: + - vieter_api_key + when: + event: tag diff --git a/.woodpecker/.arch.yml b/.woodpecker/.arch.yml index 6b8f8f2f..b2a59ba5 100644 --- a/.woodpecker/.arch.yml +++ b/.woodpecker/.arch.yml @@ -23,7 +23,7 @@ pipeline: - su builder # Due to a bug with the V compiler, we can't just use the PKGBUILD from # inside the repo - - curl -OL https://git.rustybever.be/vieter/vieter/raw/branch/dev/PKGBUILD + - curl -o PKGBUILD -L https://git.rustybever.be/vieter/vieter/raw/branch/dev/PKGBUILD.dev - makepkg -s --noconfirm --needed when: event: push diff --git a/CHANGELOG.md b/CHANGELOG.md index da09449b..2e17cd60 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,8 +15,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed -- `vieter build` command now only builds a single repository & uploads the +* `vieter build` command now only builds a single repository & uploads the build logs +* Official Arch packages are now split between `vieter` & `vieter-git` + * `vieter` is the latest release + * `vieter-git` is the latest commit on the dev branch ## [0.3.0-alpha.1](https://git.rustybever.be/vieter/vieter/src/tag/0.3.0-alpha.1) diff --git a/PKGBUILD b/PKGBUILD index 87c575ff..83ab8961 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -1,23 +1,18 @@ +# vim: ft=bash # Maintainer: Jef Roosens pkgbase='vieter' pkgname='vieter' -pkgver=0.2.0.r25.g20112b8 +pkgver='0.3.0_alpha.1' pkgrel=1 -depends=('glibc' 'openssl' 'libarchive' 'gc' 'sqlite') -makedepends=('git' 'gcc' 'vieter-v') +depends=('glibc' 'openssl' 'libarchive' 'sqlite') +makedepends=('git' 'vieter-v') arch=('x86_64' 'aarch64') url='https://git.rustybever.be/vieter/vieter' license=('AGPL3') -source=($pkgname::git+https://git.rustybever.be/vieter/vieter#branch=dev) +source=("$pkgname::git+https://git.rustybever.be/vieter/vieter#tag=${pkgver//_/-}") md5sums=('SKIP') -pkgver() { - cd "$pkgname" - - git describe --long --tags | sed 's/^v//;s/\([^-]*-g\)/r\1/;s/-/./g' -} - build() { cd "$pkgname" @@ -28,5 +23,5 @@ package() { pkgdesc="Vieter is a lightweight implementation of an Arch repository server." install -dm755 "$pkgdir/usr/bin" - install -Dm755 "$pkgbase/pvieter" "$pkgdir/usr/bin/vieter" + install -Dm755 "$pkgname/pvieter" "$pkgdir/usr/bin/vieter" } diff --git a/PKGBUILD.dev b/PKGBUILD.dev new file mode 100644 index 00000000..d0176d86 --- /dev/null +++ b/PKGBUILD.dev @@ -0,0 +1,35 @@ +# vim: ft=bash +# Maintainer: Jef Roosens + +pkgbase='vieter-git' +pkgname='vieter-git' +pkgver=0.2.0.r25.g20112b8 +pkgrel=1 +depends=('glibc' 'openssl' 'libarchive' 'sqlite') +makedepends=('git' 'vieter-v') +arch=('x86_64' 'aarch64') +url='https://git.rustybever.be/vieter/vieter' +license=('AGPL3') +source=("$pkgname::git+https://git.rustybever.be/vieter/vieter#branch=dev") +md5sums=('SKIP') +provides=('vieter') +conflicts=('vieter') + +pkgver() { + cd "$pkgname" + + git describe --long --tags | sed 's/^v//;s/\([^-]*-g\)/r\1/;s/-/./g' +} + +build() { + cd "$pkgname" + + make prod +} + +package() { + pkgdesc="Vieter is a lightweight implementation of an Arch repository server." + + install -dm755 "$pkgdir/usr/bin" + install -Dm755 "$pkgname/pvieter" "$pkgdir/usr/bin/vieter" +} From 06bab98a88ebd3e0818cae3f199f66934d8a19d6 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Tue, 10 May 2022 19:49:28 +0200 Subject: [PATCH 21/43] chore: update README --- README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.md b/README.md index 08f1e759..138282a2 100644 --- a/README.md +++ b/README.md @@ -55,3 +55,13 @@ clone my compiler in the `v` directory & build it. Afterwards, you can use this compiler with make by prepending all make commands with `V_PATH=v/v`. If you do encounter this issue, please let me know so I can update my mirror & the codebase to fix it! + +## Contributing + +If you wish to contribute to the project, please take note of the following: + +* Rebase instead of merging whenever possible, e.g. when updating your branch + with the dev branch. +* Please follow the + [Conventional Commits](https://www.conventionalcommits.org/) style for your + commit messages. From 5f21e256eefb4d9049f7ff90bb06c11f1d92e1b9 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Sat, 14 May 2022 20:06:08 +0200 Subject: [PATCH 22/43] refactor: apply new vfmt defaults --- src/build/build.v | 34 ++++++++--------- src/build/cli.v | 6 +-- src/client/client.v | 12 +++--- src/client/git.v | 10 ++--- src/client/logs.v | 10 ++--- src/console/git/git.v | 42 ++++++++++----------- src/console/logs/logs.v | 28 +++++++------- src/cron/cli.v | 6 +-- src/cron/cron.v | 2 +- src/cron/expression/expression.v | 2 +- src/cron/expression/expression_parse_test.v | 18 ++++----- src/cron/expression/expression_test.v | 20 +++++----- src/db/db.v | 2 +- src/docker/containers.v | 20 +++++----- src/docker/docker.v | 4 +- src/docker/images.v | 6 +-- src/env/env.v | 4 +- src/package/package.v | 2 +- src/repo/repo.v | 32 ++++++++-------- src/repo/sync.v | 2 +- src/server/cli.v | 6 +-- src/server/logs.v | 4 +- src/util/util.v | 2 +- src/web/web.v | 2 +- 24 files changed, 138 insertions(+), 138 deletions(-) diff --git a/src/build/build.v b/src/build/build.v index 7bd63a78..41f68e28 100644 --- a/src/build/build.v +++ b/src/build/build.v @@ -46,14 +46,14 @@ pub fn create_build_image(base_image string) ?string { image_tag := if image_parts.len > 1 { image_parts[1] } else { 'latest' } // We pull the provided image - docker.pull_image(image_name, image_tag) ? + docker.pull_image(image_name, image_tag)? - id := docker.create_container(c) ? - docker.start_container(id) ? + id := docker.create_container(c)? + docker.start_container(id)? // This loop waits until the container has stopped, so we can remove it after for { - data := docker.inspect_container(id) ? + data := docker.inspect_container(id)? if !data.state.running { break @@ -67,8 +67,8 @@ pub fn create_build_image(base_image string) ?string { // TODO also add the base image's name into the image name to prevent // conflicts. tag := time.sys_mono_now().str() - image := docker.create_image_from_container(id, 'vieter-build', tag) ? - docker.remove_container(id) ? + image := docker.create_image_from_container(id, 'vieter-build', tag)? + docker.remove_container(id)? return image.id } @@ -112,21 +112,21 @@ pub fn build_repo(address string, api_key string, base_image_id string, repo &db user: 'builder:builder' } - id := docker.create_container(c) ? - docker.start_container(id) ? + id := docker.create_container(c)? + docker.start_container(id)? - mut data := docker.inspect_container(id) ? + mut data := docker.inspect_container(id)? // This loop waits until the container has stopped, so we can remove it after for data.state.running { time.sleep(1 * time.second) - data = docker.inspect_container(id) ? + data = docker.inspect_container(id)? } - logs := docker.get_container_logs(id) ? + logs := docker.get_container_logs(id)? - docker.remove_container(id) ? + docker.remove_container(id)? return BuildResult{ start_time: data.state.start_time @@ -139,20 +139,20 @@ pub fn build_repo(address string, api_key string, base_image_id string, repo &db // build builds every Git repo in the server's list. fn build(conf Config, repo_id int) ? { c := client.new(conf.address, conf.api_key) - repo := c.get_git_repo(repo_id) ? + repo := c.get_git_repo(repo_id)? build_arch := os.uname().machine println('Creating base image...') - image_id := create_build_image(conf.base_image) ? + image_id := create_build_image(conf.base_image)? println('Running build...') - res := build_repo(conf.address, conf.api_key, image_id, repo) ? + res := build_repo(conf.address, conf.api_key, image_id, repo)? println('Removing build image...') - docker.remove_image(image_id) ? + docker.remove_image(image_id)? println('Uploading logs to Vieter...') c.add_build_log(repo.id, res.start_time, res.end_time, build_arch, res.exit_code, - res.logs) ? + res.logs)? } diff --git a/src/build/cli.v b/src/build/cli.v index 5247e871..64814cb2 100644 --- a/src/build/cli.v +++ b/src/build/cli.v @@ -18,12 +18,12 @@ pub fn cmd() cli.Command { usage: 'id' description: 'Build the repository with the given ID.' execute: fn (cmd cli.Command) ? { - config_file := cmd.flags.get_string('config-file') ? - conf := env.load(config_file) ? + config_file := cmd.flags.get_string('config-file')? + conf := env.load(config_file)? id := cmd.args[0].int() - build(conf, id) ? + build(conf, id)? } } } diff --git a/src/client/client.v b/src/client/client.v index 25224a51..3b280736 100644 --- a/src/client/client.v +++ b/src/client/client.v @@ -37,10 +37,10 @@ fn (c &Client) send_request_raw(method Method, url string, params map[string]str full_url = '$full_url?$params_str' } - mut req := http.new_request(method, full_url, body) ? - req.add_custom_header('X-Api-Key', c.api_key) ? + mut req := http.new_request(method, full_url, body)? + req.add_custom_header('X-Api-Key', c.api_key)? - res := req.do() ? + res := req.do()? return res } @@ -53,15 +53,15 @@ fn (c &Client) send_request(method Method, url string, params map[string]stri // send_request_with_body calls send_request_raw_response & parses its // output as a Response object. fn (c &Client) send_request_with_body(method Method, url string, params map[string]string, body string) ?Response { - res_text := c.send_request_raw_response(method, url, params, body) ? - data := json.decode(Response, res_text) ? + res_text := c.send_request_raw_response(method, url, params, body)? + data := json.decode(Response, res_text)? return data } // send_request_raw_response returns the raw text response for an HTTP request. fn (c &Client) send_request_raw_response(method Method, url string, params map[string]string, body string) ?string { - res := c.send_request_raw(method, url, params, body) ? + res := c.send_request_raw(method, url, params, body)? return res.text } diff --git a/src/client/git.v b/src/client/git.v index b09d4c22..280caabe 100644 --- a/src/client/git.v +++ b/src/client/git.v @@ -6,14 +6,14 @@ import response { Response } // get_git_repos returns the current list of repos. pub fn (c &Client) get_git_repos() ?[]GitRepo { - data := c.send_request<[]GitRepo>(Method.get, '/api/repos', {}) ? + data := c.send_request<[]GitRepo>(Method.get, '/api/repos', {})? return data.data } // get_git_repo returns the repo for a specific ID. pub fn (c &Client) get_git_repo(id int) ?GitRepo { - data := c.send_request(Method.get, '/api/repos/$id', {}) ? + data := c.send_request(Method.get, '/api/repos/$id', {})? return data.data } @@ -30,14 +30,14 @@ pub fn (c &Client) add_git_repo(url string, branch string, repo string, arch []s params['arch'] = arch.join(',') } - data := c.send_request(Method.post, '/api/repos', params) ? + data := c.send_request(Method.post, '/api/repos', params)? return data } // remove_git_repo removes the repo with the given ID from the server. pub fn (c &Client) remove_git_repo(id int) ?Response { - data := c.send_request(Method.delete, '/api/repos/$id', {}) ? + data := c.send_request(Method.delete, '/api/repos/$id', {})? return data } @@ -45,7 +45,7 @@ pub fn (c &Client) remove_git_repo(id int) ?Response { // patch_git_repo sends a PATCH request to the given repo with the params as // payload. pub fn (c &Client) patch_git_repo(id int, params map[string]string) ?Response { - data := c.send_request(Method.patch, '/api/repos/$id', params) ? + data := c.send_request(Method.patch, '/api/repos/$id', params)? return data } diff --git a/src/client/logs.v b/src/client/logs.v index 8c532138..cdacab9b 100644 --- a/src/client/logs.v +++ b/src/client/logs.v @@ -7,7 +7,7 @@ import time // get_build_logs returns all build logs. pub fn (c &Client) get_build_logs() ?Response<[]BuildLog> { - data := c.send_request<[]BuildLog>(Method.get, '/api/logs', {}) ? + data := c.send_request<[]BuildLog>(Method.get, '/api/logs', {})? return data } @@ -18,21 +18,21 @@ pub fn (c &Client) get_build_logs_for_repo(repo_id int) ?Response<[]BuildLog> { 'repo': repo_id.str() } - data := c.send_request<[]BuildLog>(Method.get, '/api/logs', params) ? + data := c.send_request<[]BuildLog>(Method.get, '/api/logs', params)? return data } // get_build_log returns a specific build log. pub fn (c &Client) get_build_log(id int) ?Response { - data := c.send_request(Method.get, '/api/logs/$id', {}) ? + data := c.send_request(Method.get, '/api/logs/$id', {})? return data } // get_build_log_content returns the contents of the build log file. pub fn (c &Client) get_build_log_content(id int) ?string { - data := c.send_request_raw_response(Method.get, '/api/logs/$id/content', {}, '') ? + data := c.send_request_raw_response(Method.get, '/api/logs/$id/content', {}, '')? return data } @@ -47,7 +47,7 @@ pub fn (c &Client) add_build_log(repo_id int, start_time time.Time, end_time tim 'exitCode': exit_code.str() } - data := c.send_request_with_body(Method.post, '/api/logs', params, content) ? + data := c.send_request_with_body(Method.post, '/api/logs', params, content)? return data } diff --git a/src/console/git/git.v b/src/console/git/git.v index 3bf78d10..83837446 100644 --- a/src/console/git/git.v +++ b/src/console/git/git.v @@ -20,10 +20,10 @@ pub fn cmd() cli.Command { name: 'list' description: 'List the current repos.' execute: fn (cmd cli.Command) ? { - config_file := cmd.flags.get_string('config-file') ? - conf := env.load(config_file) ? + config_file := cmd.flags.get_string('config-file')? + conf := env.load(config_file)? - list(conf) ? + list(conf)? } }, cli.Command{ @@ -32,10 +32,10 @@ pub fn cmd() cli.Command { usage: 'url branch repo' description: 'Add a new repository.' execute: fn (cmd cli.Command) ? { - config_file := cmd.flags.get_string('config-file') ? - conf := env.load(config_file) ? + config_file := cmd.flags.get_string('config-file')? + conf := env.load(config_file)? - add(conf, cmd.args[0], cmd.args[1], cmd.args[2]) ? + add(conf, cmd.args[0], cmd.args[1], cmd.args[2])? } }, cli.Command{ @@ -44,10 +44,10 @@ pub fn cmd() cli.Command { usage: 'id' description: 'Remove a repository that matches the given ID prefix.' execute: fn (cmd cli.Command) ? { - config_file := cmd.flags.get_string('config-file') ? - conf := env.load(config_file) ? + config_file := cmd.flags.get_string('config-file')? + conf := env.load(config_file)? - remove(conf, cmd.args[0]) ? + remove(conf, cmd.args[0])? } }, cli.Command{ @@ -56,10 +56,10 @@ pub fn cmd() cli.Command { usage: 'id' description: 'Show detailed information for the repo matching the ID prefix.' execute: fn (cmd cli.Command) ? { - config_file := cmd.flags.get_string('config-file') ? - conf := env.load(config_file) ? + config_file := cmd.flags.get_string('config-file')? + conf := env.load(config_file)? - info(conf, cmd.args[0]) ? + info(conf, cmd.args[0])? } }, cli.Command{ @@ -95,8 +95,8 @@ pub fn cmd() cli.Command { }, ] execute: fn (cmd cli.Command) ? { - config_file := cmd.flags.get_string('config-file') ? - conf := env.load(config_file) ? + config_file := cmd.flags.get_string('config-file')? + conf := env.load(config_file)? found := cmd.flags.get_all_found() @@ -104,11 +104,11 @@ pub fn cmd() cli.Command { for f in found { if f.name != 'config-file' { - params[f.name] = f.get_string() ? + params[f.name] = f.get_string()? } } - patch(conf, cmd.args[0], params) ? + patch(conf, cmd.args[0], params)? } }, ] @@ -121,7 +121,7 @@ pub fn cmd() cli.Command { // list prints out a list of all repositories. fn list(conf Config) ? { c := client.new(conf.address, conf.api_key) - repos := c.get_git_repos() ? + repos := c.get_git_repos()? for repo in repos { println('$repo.id\t$repo.url\t$repo.branch\t$repo.repo') @@ -131,7 +131,7 @@ fn list(conf Config) ? { // add adds a new repository to the server's list. fn add(conf Config, url string, branch string, repo string) ? { c := client.new(conf.address, conf.api_key) - res := c.add_git_repo(url, branch, repo, []) ? + res := c.add_git_repo(url, branch, repo, [])? println(res.message) } @@ -143,7 +143,7 @@ fn remove(conf Config, id string) ? { if id_int != 0 { c := client.new(conf.address, conf.api_key) - res := c.remove_git_repo(id_int) ? + res := c.remove_git_repo(id_int)? println(res.message) } } @@ -161,7 +161,7 @@ fn patch(conf Config, id string, params map[string]string) ? { id_int := id.int() if id_int != 0 { c := client.new(conf.address, conf.api_key) - res := c.patch_git_repo(id_int, params) ? + res := c.patch_git_repo(id_int, params)? println(res.message) } @@ -176,6 +176,6 @@ fn info(conf Config, id string) ? { } c := client.new(conf.address, conf.api_key) - repo := c.get_git_repo(id_int) ? + repo := c.get_git_repo(id_int)? println(repo) } diff --git a/src/console/logs/logs.v b/src/console/logs/logs.v index e3c7d14a..0e965232 100644 --- a/src/console/logs/logs.v +++ b/src/console/logs/logs.v @@ -27,12 +27,12 @@ pub fn cmd() cli.Command { }, ] execute: fn (cmd cli.Command) ? { - config_file := cmd.flags.get_string('config-file') ? - conf := env.load(config_file) ? + config_file := cmd.flags.get_string('config-file')? + conf := env.load(config_file)? - repo_id := cmd.flags.get_int('repo') ? + repo_id := cmd.flags.get_int('repo')? - if repo_id == 0 { list(conf) ? } else { list_for_repo(conf, repo_id) ? } + if repo_id == 0 { list(conf)? } else { list_for_repo(conf, repo_id)? } } }, cli.Command{ @@ -41,11 +41,11 @@ pub fn cmd() cli.Command { usage: 'id' description: 'Show all info for a specific build log.' execute: fn (cmd cli.Command) ? { - config_file := cmd.flags.get_string('config-file') ? - conf := env.load(config_file) ? + config_file := cmd.flags.get_string('config-file')? + conf := env.load(config_file)? id := cmd.args[0].int() - info(conf, id) ? + info(conf, id)? } }, cli.Command{ @@ -54,11 +54,11 @@ pub fn cmd() cli.Command { usage: 'id' description: 'Output the content of a build log to stdout.' execute: fn (cmd cli.Command) ? { - config_file := cmd.flags.get_string('config-file') ? - conf := env.load(config_file) ? + config_file := cmd.flags.get_string('config-file')? + conf := env.load(config_file)? id := cmd.args[0].int() - content(conf, id) ? + content(conf, id)? } }, ] @@ -75,7 +75,7 @@ fn print_log_list(logs []db.BuildLog) { // list prints a list of all build logs. fn list(conf Config) ? { c := client.new(conf.address, conf.api_key) - logs := c.get_build_logs() ?.data + logs := c.get_build_logs()?.data print_log_list(logs) } @@ -83,7 +83,7 @@ fn list(conf Config) ? { // list prints a list of all build logs for a given repo. fn list_for_repo(conf Config, repo_id int) ? { c := client.new(conf.address, conf.api_key) - logs := c.get_build_logs_for_repo(repo_id) ?.data + logs := c.get_build_logs_for_repo(repo_id)?.data print_log_list(logs) } @@ -91,7 +91,7 @@ fn list_for_repo(conf Config, repo_id int) ? { // info print the detailed info for a given build log. fn info(conf Config, id int) ? { c := client.new(conf.address, conf.api_key) - log := c.get_build_log(id) ?.data + log := c.get_build_log(id)?.data print(log) } @@ -100,7 +100,7 @@ fn info(conf Config, id int) ? { // stdout. fn content(conf Config, id int) ? { c := client.new(conf.address, conf.api_key) - content := c.get_build_log_content(id) ? + content := c.get_build_log_content(id)? println(content) } diff --git a/src/cron/cli.v b/src/cron/cli.v index 15bc9867..9703c663 100644 --- a/src/cron/cli.v +++ b/src/cron/cli.v @@ -23,10 +23,10 @@ pub fn cmd() cli.Command { name: 'cron' description: 'Start the cron service that periodically runs builds.' execute: fn (cmd cli.Command) ? { - config_file := cmd.flags.get_string('config-file') ? - conf := env.load(config_file) ? + config_file := cmd.flags.get_string('config-file')? + conf := env.load(config_file)? - cron(conf) ? + cron(conf)? } } } diff --git a/src/cron/cron.v b/src/cron/cron.v index e356faa1..5f128cf0 100644 --- a/src/cron/cron.v +++ b/src/cron/cron.v @@ -27,7 +27,7 @@ pub fn cron(conf Config) ? { } mut d := daemon.init_daemon(logger, conf.address, conf.api_key, conf.base_image, ce, - conf.max_concurrent_builds, conf.api_update_frequency, conf.image_rebuild_frequency) ? + conf.max_concurrent_builds, conf.api_update_frequency, conf.image_rebuild_frequency)? d.run() } diff --git a/src/cron/expression/expression.v b/src/cron/expression/expression.v index 124337f0..5eae332c 100644 --- a/src/cron/expression/expression.v +++ b/src/cron/expression/expression.v @@ -218,7 +218,7 @@ fn parse_part(s string, min int, max int) ?[]int { mut bitv := []bool{len: max - min + 1, init: false} for range in s.split(',') { - parse_range(range, min, max, mut bitv) ? + parse_range(range, min, max, mut bitv)? } return bitv_to_ints(bitv, min) diff --git a/src/cron/expression/expression_parse_test.v b/src/cron/expression/expression_parse_test.v index 18531c0c..4eebc49e 100644 --- a/src/cron/expression/expression_parse_test.v +++ b/src/cron/expression/expression_parse_test.v @@ -13,14 +13,14 @@ fn parse_range_error(s string, min int, max int) string { // =====parse_range===== fn test_range_star_range() ? { mut bitv := []bool{len: 6, init: false} - parse_range('*', 0, 5, mut bitv) ? + parse_range('*', 0, 5, mut bitv)? assert bitv == [true, true, true, true, true, true] } fn test_range_number() ? { mut bitv := []bool{len: 6, init: false} - parse_range('4', 0, 5, mut bitv) ? + parse_range('4', 0, 5, mut bitv)? assert bitv_to_ints(bitv, 0) == [4] } @@ -39,14 +39,14 @@ fn test_range_number_invalid() ? { fn test_range_step_star_1() ? { mut bitv := []bool{len: 21, init: false} - parse_range('*/4', 0, 20, mut bitv) ? + parse_range('*/4', 0, 20, mut bitv)? assert bitv_to_ints(bitv, 0) == [0, 4, 8, 12, 16, 20] } fn test_range_step_star_2() ? { mut bitv := []bool{len: 8, init: false} - parse_range('*/3', 1, 8, mut bitv) ? + parse_range('*/3', 1, 8, mut bitv)? assert bitv_to_ints(bitv, 1) == [1, 4, 7] } @@ -61,7 +61,7 @@ fn test_range_step_zero() ? { fn test_range_step_number() ? { mut bitv := []bool{len: 21, init: false} - parse_range('5/4', 2, 22, mut bitv) ? + parse_range('5/4', 2, 22, mut bitv)? assert bitv_to_ints(bitv, 2) == [5, 9, 13, 17, 21] } @@ -76,23 +76,23 @@ fn test_range_step_number_too_small() ? { fn test_range_dash() ? { mut bitv := []bool{len: 10, init: false} - parse_range('4-8', 0, 9, mut bitv) ? + parse_range('4-8', 0, 9, mut bitv)? assert bitv_to_ints(bitv, 0) == [4, 5, 6, 7, 8] } fn test_range_dash_step() ? { mut bitv := []bool{len: 10, init: false} - parse_range('4-8/2', 0, 9, mut bitv) ? + parse_range('4-8/2', 0, 9, mut bitv)? assert bitv_to_ints(bitv, 0) == [4, 6, 8] } // =====parse_part===== fn test_part_single() ? { - assert parse_part('*', 0, 5) ? == [0, 1, 2, 3, 4, 5] + assert parse_part('*', 0, 5)? == [0, 1, 2, 3, 4, 5] } fn test_part_multiple() ? { - assert parse_part('*/2,2/3', 1, 8) ? == [1, 2, 3, 5, 7, 8] + assert parse_part('*/2,2/3', 1, 8)? == [1, 2, 3, 5, 7, 8] } diff --git a/src/cron/expression/expression_test.v b/src/cron/expression/expression_test.v index ef0283a7..9e25e924 100644 --- a/src/cron/expression/expression_test.v +++ b/src/cron/expression/expression_test.v @@ -3,11 +3,11 @@ module expression import time { parse } fn util_test_time(exp string, t1_str string, t2_str string) ? { - ce := parse_expression(exp) ? - t1 := parse(t1_str) ? - t2 := parse(t2_str) ? + ce := parse_expression(exp)? + t1 := parse(t1_str)? + t2 := parse(t2_str)? - t3 := ce.next(t1) ? + t3 := ce.next(t1)? assert t2.year == t3.year assert t2.month == t3.month @@ -18,17 +18,17 @@ fn util_test_time(exp string, t1_str string, t2_str string) ? { fn test_next_simple() ? { // Very simple - util_test_time('0 3', '2002-01-01 00:00:00', '2002-01-01 03:00:00') ? + util_test_time('0 3', '2002-01-01 00:00:00', '2002-01-01 03:00:00')? // Overlap to next day - util_test_time('0 3', '2002-01-01 03:00:00', '2002-01-02 03:00:00') ? - util_test_time('0 3', '2002-01-01 04:00:00', '2002-01-02 03:00:00') ? + util_test_time('0 3', '2002-01-01 03:00:00', '2002-01-02 03:00:00')? + util_test_time('0 3', '2002-01-01 04:00:00', '2002-01-02 03:00:00')? - util_test_time('0 3/4', '2002-01-01 04:00:00', '2002-01-01 07:00:00') ? + util_test_time('0 3/4', '2002-01-01 04:00:00', '2002-01-01 07:00:00')? // Overlap to next month - util_test_time('0 3', '2002-11-31 04:00:00', '2002-12-01 03:00:00') ? + util_test_time('0 3', '2002-11-31 04:00:00', '2002-12-01 03:00:00')? // Overlap to next year - util_test_time('0 3', '2002-12-31 04:00:00', '2003-01-01 03:00:00') ? + util_test_time('0 3', '2002-12-31 04:00:00', '2003-01-01 03:00:00')? } diff --git a/src/db/db.v b/src/db/db.v index 5ec240de..7c1acf1e 100644 --- a/src/db/db.v +++ b/src/db/db.v @@ -8,7 +8,7 @@ struct VieterDb { // init initializes a database & adds the correct tables. pub fn init(db_path string) ?VieterDb { - conn := sqlite.connect(db_path) ? + conn := sqlite.connect(db_path)? sql conn { create table GitRepo diff --git a/src/docker/containers.v b/src/docker/containers.v index 2258f3bd..14ac12d9 100644 --- a/src/docker/containers.v +++ b/src/docker/containers.v @@ -11,7 +11,7 @@ struct Container { // containers returns a list of all currently running containers pub fn containers() ?[]Container { - res := request('GET', urllib.parse('/v1.41/containers/json') ?) ? + res := request('GET', urllib.parse('/v1.41/containers/json')?)? return json.decode([]Container, res.text) or {} } @@ -32,19 +32,19 @@ struct CreatedContainer { // create_container creates a container defined by the given configuration. If // successful, it returns the ID of the newly created container. pub fn create_container(c &NewContainer) ?string { - res := request_with_json('POST', urllib.parse('/v1.41/containers/create') ?, c) ? + res := request_with_json('POST', urllib.parse('/v1.41/containers/create')?, c)? if res.status_code != 201 { return error('Failed to create container.') } - return json.decode(CreatedContainer, res.text) ?.id + return json.decode(CreatedContainer, res.text)?.id } // start_container starts a container with a given ID. It returns whether the // container was started or not. pub fn start_container(id string) ?bool { - res := request('POST', urllib.parse('/v1.41/containers/$id/start') ?) ? + res := request('POST', urllib.parse('/v1.41/containers/$id/start')?)? return res.status_code == 204 } @@ -70,18 +70,18 @@ pub mut: // inspect_container returns the result of inspecting a container with a given // ID. pub fn inspect_container(id string) ?ContainerInspect { - res := request('GET', urllib.parse('/v1.41/containers/$id/json') ?) ? + res := request('GET', urllib.parse('/v1.41/containers/$id/json')?)? if res.status_code != 200 { return error('Failed to inspect container.') } - mut data := json.decode(ContainerInspect, res.text) ? + mut data := json.decode(ContainerInspect, res.text)? - data.state.start_time = time.parse_rfc3339(data.state.start_time_str) ? + data.state.start_time = time.parse_rfc3339(data.state.start_time_str)? if data.state.status == 'exited' { - data.state.end_time = time.parse_rfc3339(data.state.end_time_str) ? + data.state.end_time = time.parse_rfc3339(data.state.end_time_str)? } return data @@ -89,7 +89,7 @@ pub fn inspect_container(id string) ?ContainerInspect { // remove_container removes a container with a given ID. pub fn remove_container(id string) ?bool { - res := request('DELETE', urllib.parse('/v1.41/containers/$id') ?) ? + res := request('DELETE', urllib.parse('/v1.41/containers/$id')?)? return res.status_code == 204 } @@ -97,7 +97,7 @@ pub fn remove_container(id string) ?bool { // get_container_logs retrieves the logs for a Docker container, both stdout & // stderr. pub fn get_container_logs(id string) ?string { - res := request('GET', urllib.parse('/v1.41/containers/$id/logs?stdout=true&stderr=true') ?) ? + res := request('GET', urllib.parse('/v1.41/containers/$id/logs?stdout=true&stderr=true')?)? mut res_bytes := res.text.bytes() // Docker uses a special "stream" format for their logs, so we have to diff --git a/src/docker/docker.v b/src/docker/docker.v index 5deef830..305e8253 100644 --- a/src/docker/docker.v +++ b/src/docker/docker.v @@ -27,7 +27,7 @@ fn send(req &string) ?http.Response { // Write the request to the socket s.write_string(req) or { return error('Failed to write request to socket ${docker.socket}.') } - s.wait_for_write() ? + s.wait_for_write()? mut c := 0 mut buf := []u8{len: docker.buf_len} @@ -56,7 +56,7 @@ fn send(req &string) ?http.Response { // A chunked HTTP response always ends with '0\r\n\r\n'. for res.len < 5 || res#[-5..] != [u8(`0`), `\r`, `\n`, `\r`, `\n`] { // Wait for the server to respond - s.wait_for_write() ? + s.wait_for_write()? for { c = s.read(mut buf) or { diff --git a/src/docker/images.v b/src/docker/images.v index e94ceca2..2e873fa0 100644 --- a/src/docker/images.v +++ b/src/docker/images.v @@ -11,13 +11,13 @@ pub: // pull_image pulls tries to pull the image for the given image & tag pub fn pull_image(image string, tag string) ?http.Response { - return request('POST', urllib.parse('/v1.41/images/create?fromImage=$image&tag=$tag') ?) + return request('POST', urllib.parse('/v1.41/images/create?fromImage=$image&tag=$tag')?) } // create_image_from_container creates a new image from a container with the // given repo & tag, given the container's ID. pub fn create_image_from_container(id string, repo string, tag string) ?Image { - res := request('POST', urllib.parse('/v1.41/commit?container=$id&repo=$repo&tag=$tag') ?) ? + res := request('POST', urllib.parse('/v1.41/commit?container=$id&repo=$repo&tag=$tag')?)? if res.status_code != 201 { return error('Failed to create image from container.') @@ -28,7 +28,7 @@ pub fn create_image_from_container(id string, repo string, tag string) ?Image { // remove_image removes the image with the given ID. pub fn remove_image(id string) ?bool { - res := request('DELETE', urllib.parse('/v1.41/images/$id') ?) ? + res := request('DELETE', urllib.parse('/v1.41/images/$id')?)? return res.status_code == 200 } diff --git a/src/env/env.v b/src/env/env.v index b2b5f446..d1459311 100644 --- a/src/env/env.v +++ b/src/env/env.v @@ -50,7 +50,7 @@ pub fn load(path string) ?T { if os.exists(path) { // We don't use reflect here because reflect also sets any fields not // in the toml back to their zero value, which we don't want - doc := toml.parse_file(path) ? + doc := toml.parse_file(path)? $for field in T.fields { s := doc.value(field.name) @@ -66,7 +66,7 @@ pub fn load(path string) ?T { } $for field in T.fields { - env_value := get_env_var(field.name) ? + env_value := get_env_var(field.name)? // The value of an env var will always take precedence over the toml // file. diff --git a/src/package/package.v b/src/package/package.v index a1042b54..273322fa 100644 --- a/src/package/package.v +++ b/src/package/package.v @@ -159,7 +159,7 @@ pub fn read_pkg_archive(pkg_path string) ?Pkg { pkg_text := unsafe { buf.vstring_with_len(size).clone() } - pkg_info = parse_pkg_info_string(pkg_text) ? + pkg_info = parse_pkg_info_string(pkg_text)? } else { C.archive_read_data_skip(a) } diff --git a/src/repo/repo.v b/src/repo/repo.v index e27e232b..817ec304 100644 --- a/src/repo/repo.v +++ b/src/repo/repo.v @@ -53,22 +53,22 @@ pub fn (r &RepoGroupManager) add_pkg_from_path(repo string, pkg_path string) ?Re return error('Failed to read package file: $err.msg()') } - added := r.add_pkg_in_repo(repo, pkg) ? + added := r.add_pkg_in_repo(repo, pkg)? // If the add was successful, we move the file to the packages directory for arch in added { repo_pkg_path := os.real_path(os.join_path(r.pkg_dir, repo, arch)) dest_path := os.join_path_single(repo_pkg_path, pkg.filename()) - os.mkdir_all(repo_pkg_path) ? + os.mkdir_all(repo_pkg_path)? // We create hard links so that "any" arch packages aren't stored // multiple times - os.link(pkg_path, dest_path) ? + os.link(pkg_path, dest_path)? } // After linking, we can remove the original file - os.rm(pkg_path) ? + os.rm(pkg_path)? return RepoAddResult{ added: added.len > 0 @@ -87,7 +87,7 @@ fn (r &RepoGroupManager) add_pkg_in_repo(repo string, pkg &package.Pkg) ?[]strin // A package not of arch 'any' can be handled easily by adding it to the // respective repo if pkg.info.arch != 'any' { - if r.add_pkg_in_arch_repo(repo, pkg.info.arch, pkg) ? { + if r.add_pkg_in_arch_repo(repo, pkg.info.arch, pkg)? { return [pkg.info.arch] } else { return [] @@ -104,7 +104,7 @@ fn (r &RepoGroupManager) add_pkg_in_repo(repo string, pkg &package.Pkg) ?[]strin // If this is the first package that's added to the repo, the directory // won't exist yet if os.exists(repo_dir) { - arch_repos = os.ls(repo_dir) ? + arch_repos = os.ls(repo_dir)? } // The default_arch should always be updated when a package with arch 'any' @@ -118,7 +118,7 @@ fn (r &RepoGroupManager) add_pkg_in_repo(repo string, pkg &package.Pkg) ?[]strin // We add the package to each repository. If any of the repositories // return true, the result of the function is also true. for arch in arch_repos { - if r.add_pkg_in_arch_repo(repo, arch, pkg) ? { + if r.add_pkg_in_arch_repo(repo, arch, pkg)? { added << arch } } @@ -135,22 +135,22 @@ fn (r &RepoGroupManager) add_pkg_in_arch_repo(repo string, arch string, pkg &pac pkg_dir := os.join_path(r.repos_dir, repo, arch, '$pkg.info.name-$pkg.info.version') // Remove the previous version of the package, if present - r.remove_pkg_from_arch_repo(repo, arch, pkg.info.name, false) ? + r.remove_pkg_from_arch_repo(repo, arch, pkg.info.name, false)? os.mkdir_all(pkg_dir) or { return error('Failed to create package directory.') } os.write_file(os.join_path_single(pkg_dir, 'desc'), pkg.to_desc()) or { - os.rmdir_all(pkg_dir) ? + os.rmdir_all(pkg_dir)? return error('Failed to write desc file.') } os.write_file(os.join_path_single(pkg_dir, 'files'), pkg.to_files()) or { - os.rmdir_all(pkg_dir) ? + os.rmdir_all(pkg_dir)? return error('Failed to write files file.') } - r.sync(repo, arch) ? + r.sync(repo, arch)? return true } @@ -168,7 +168,7 @@ fn (r &RepoGroupManager) remove_pkg_from_arch_repo(repo string, arch string, pkg // We iterate over every directory in the repo dir // TODO filter so we only check directories - for d in os.ls(repo_dir) ? { + for d in os.ls(repo_dir)? { // Because a repository only allows a single version of each package, // we need only compare whether the name of the package is the same, // not the version. @@ -178,22 +178,22 @@ fn (r &RepoGroupManager) remove_pkg_from_arch_repo(repo string, arch string, pkg // We lock the mutex here to prevent other routines from creating a // new archive while we remove an entry lock r.mutex { - os.rmdir_all(os.join_path_single(repo_dir, d)) ? + os.rmdir_all(os.join_path_single(repo_dir, d))? } // Also remove the package archive repo_pkg_dir := os.join_path(r.pkg_dir, repo, arch) - archives := os.ls(repo_pkg_dir) ?.filter(it.split('-')#[..-3].join('-') == name) + archives := os.ls(repo_pkg_dir)?.filter(it.split('-')#[..-3].join('-') == name) for archive_name in archives { full_path := os.join_path_single(repo_pkg_dir, archive_name) - os.rm(full_path) ? + os.rm(full_path)? } // Sync the db archives if requested if sync { - r.sync(repo, arch) ? + r.sync(repo, arch)? } return true diff --git a/src/repo/sync.v b/src/repo/sync.v index 9c5e7ed2..73d21c88 100644 --- a/src/repo/sync.v +++ b/src/repo/sync.v @@ -54,7 +54,7 @@ fn (r &RepoGroupManager) sync(repo string, arch string) ? { C.archive_write_open_filename(a_files, &char(files_path.str)) // Iterate over each directory - for d in os.ls(subrepo_path) ?.filter(os.is_dir(os.join_path_single(subrepo_path, + for d in os.ls(subrepo_path)?.filter(os.is_dir(os.join_path_single(subrepo_path, it))) { // desc mut inner_path := os.join_path_single(d, 'desc') diff --git a/src/server/cli.v b/src/server/cli.v index 4d396661..556efcfe 100644 --- a/src/server/cli.v +++ b/src/server/cli.v @@ -18,10 +18,10 @@ pub fn cmd() cli.Command { name: 'server' description: 'Start the Vieter server.' execute: fn (cmd cli.Command) ? { - config_file := cmd.flags.get_string('config-file') ? - conf := env.load(config_file) ? + config_file := cmd.flags.get_string('config-file')? + conf := env.load(config_file)? - server(conf) ? + server(conf)? } } } diff --git a/src/server/logs.v b/src/server/logs.v index b048dc4f..21331e5b 100644 --- a/src/server/logs.v +++ b/src/server/logs.v @@ -56,8 +56,8 @@ fn (mut app App) get_log_content(id int) web.Result { // parse_query_time unescapes an HTTP query parameter & tries to parse it as a // time.Time struct. fn parse_query_time(query string) ?time.Time { - unescaped := urllib.query_unescape(query) ? - t := time.parse(unescaped) ? + unescaped := urllib.query_unescape(query)? + t := time.parse(unescaped)? return t } diff --git a/src/util/util.v b/src/util/util.v index c1af30ec..66026215 100644 --- a/src/util/util.v +++ b/src/util/util.v @@ -25,7 +25,7 @@ pub fn exit_with_message(code int, msg string) { // reader_to_file writes the contents of a BufferedReader to a file pub fn reader_to_file(mut reader io.BufferedReader, length int, path string) ? { - mut file := os.create(path) ? + mut file := os.create(path)? defer { file.close() } diff --git a/src/web/web.v b/src/web/web.v index 3e7b0478..f237e246 100644 --- a/src/web/web.v +++ b/src/web/web.v @@ -190,7 +190,7 @@ pub fn (ctx Context) before_request() {} // send_string fn send_string(mut conn net.TcpConn, s string) ? { - conn.write(s.bytes()) ? + conn.write(s.bytes())? } // send_response_to_client sends a response to the client From 5c5c2f87e0e74c940210744b525cb6e9b302c188 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Mon, 9 May 2022 16:16:25 +0200 Subject: [PATCH 23/43] feat(cli): lists are now properly formatted in an ascii table --- src/console/console.v | 55 +++++++++++++++++++++++++++++++++++++++++ src/console/git/git.v | 6 ++--- src/console/logs/logs.v | 14 ++++++----- 3 files changed, 66 insertions(+), 9 deletions(-) diff --git a/src/console/console.v b/src/console/console.v index 6f296bdd..dfb2fac8 100644 --- a/src/console/console.v +++ b/src/console/console.v @@ -1 +1,56 @@ module console + +import arrays +import strings + +// pretty_table converts a list of string data into a pretty table. Many thanks +// to @hungrybluedev in the Vlang Discord for providing this code! +// https://ptb.discord.com/channels/592103645835821068/592106336838352923/970278787143045192 +pub fn pretty_table(header []string, data [][]string) ?string { + column_count := header.len + + mut column_widths := []int{len: column_count, init: header[it].len} + + for values in data { + for col, value in values { + if column_widths[col] < value.len { + column_widths[col] = value.len + } + } + } + + single_line_length := arrays.sum(column_widths)? + (column_count + 1) * 3 - 4 + + horizontal_line := '+' + strings.repeat(`-`, single_line_length) + '+' + mut buffer := strings.new_builder(data.len * single_line_length) + + buffer.writeln(horizontal_line) + + buffer.write_string('| ') + for col, head in header { + if col != 0 { + buffer.write_string(' | ') + } + buffer.write_string(head) + buffer.write_string(strings.repeat(` `, column_widths[col] - head.len)) + } + buffer.writeln(' |') + + buffer.writeln(horizontal_line) + + for values in data { + buffer.write_string('| ') + for col, value in values { + if col != 0 { + buffer.write_string(' | ') + } + buffer.write_string(value) + buffer.write_string(strings.repeat(` `, column_widths[col] - value.len)) + } + buffer.writeln(' |') + } + + buffer.writeln(horizontal_line) + + return buffer.str() +} diff --git a/src/console/git/git.v b/src/console/git/git.v index 83837446..db9dec59 100644 --- a/src/console/git/git.v +++ b/src/console/git/git.v @@ -4,6 +4,7 @@ import cli import env import cron.expression { parse_expression } import client +import console struct Config { address string [required] @@ -122,10 +123,9 @@ pub fn cmd() cli.Command { fn list(conf Config) ? { c := client.new(conf.address, conf.api_key) repos := c.get_git_repos()? + data := repos.map([it.id.str(), it.url, it.branch, it.repo]) - for repo in repos { - println('$repo.id\t$repo.url\t$repo.branch\t$repo.repo') - } + println(console.pretty_table(['id', 'url', 'branch', 'repo'], data)?) } // add adds a new repository to the server's list. diff --git a/src/console/logs/logs.v b/src/console/logs/logs.v index 0e965232..6400e801 100644 --- a/src/console/logs/logs.v +++ b/src/console/logs/logs.v @@ -4,6 +4,7 @@ import cli import env import client import db +import console struct Config { address string [required] @@ -66,10 +67,11 @@ pub fn cmd() cli.Command { } // print_log_list prints a list of logs. -fn print_log_list(logs []db.BuildLog) { - for log in logs { - println('$log.id\t$log.start_time\t$log.exit_code') - } +fn print_log_list(logs []db.BuildLog) ? { + data := logs.map([it.id.str(), it.repo_id.str(), it.start_time.str(), + it.exit_code.str()]) + + println(console.pretty_table(['id', 'repo', 'start time', 'exit code'], data)?) } // list prints a list of all build logs. @@ -77,7 +79,7 @@ fn list(conf Config) ? { c := client.new(conf.address, conf.api_key) logs := c.get_build_logs()?.data - print_log_list(logs) + print_log_list(logs)? } // list prints a list of all build logs for a given repo. @@ -85,7 +87,7 @@ fn list_for_repo(conf Config, repo_id int) ? { c := client.new(conf.address, conf.api_key) logs := c.get_build_logs_for_repo(repo_id)?.data - print_log_list(logs) + print_log_list(logs)? } // info print the detailed info for a given build log. From 473c7ec06b10735b82959a50df7bb470d12ded7a Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Wed, 11 May 2022 10:03:43 +0200 Subject: [PATCH 24/43] feat(docker): start of new socket implementation --- src/docker/docker.v | 4 ---- src/docker/socket.v | 47 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 4 deletions(-) create mode 100644 src/docker/socket.v diff --git a/src/docker/docker.v b/src/docker/docker.v index 305e8253..ce01e7ef 100644 --- a/src/docker/docker.v +++ b/src/docker/docker.v @@ -5,10 +5,6 @@ import net.urllib import net.http import json -const socket = '/var/run/docker.sock' - -const buf_len = 1024 - // send writes a request to the Docker socket, waits for a response & returns // it. fn send(req &string) ?http.Response { diff --git a/src/docker/socket.v b/src/docker/socket.v new file mode 100644 index 00000000..3a72f144 --- /dev/null +++ b/src/docker/socket.v @@ -0,0 +1,47 @@ +module docker + +import net.unix +import io +import net.http + +const socket = '/var/run/docker.sock' + +const buf_len = 10 * 1024 + +pub struct DockerDaemon { +mut: + socket &unix.StreamConn + reader &io.BufferedReader +} + +pub fn new_conn() ?DockerDaemon { + s := unix.connect_stream(socket) ? + + d := DockerDaemon{socket: s, reader: io.new_buffered_reader(reader: s)} + + return d +} + +fn (mut d DockerDaemon) send_request(req &string) ? { + d.socket.write_string(req) ? + d.socket.wait_for_write() ? +} + +// read_response_head consumes the socket's contents until it encounters +// '\n\n', after which it parses the response as an HTTP response. +fn (mut d DockerDaemon) read_response_head() ?http.Response { + mut c := 0 + mut buf := [buf_len]u8{len: docker.buf_len} + mut res := []u8{} + + for { + c = d.socket.read(mut buf) ? + res << buf[..c] + + if res#[-2..] == [u8(`\n`), `\n`] { + break + } + } + + return http.parse_response(res.bytestr()) +} From dd9958ea28982d39c4dba26036cb9f03c304e27d Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Fri, 13 May 2022 21:28:24 +0200 Subject: [PATCH 25/43] refactor: ran vfmt with new defaults feat(docker): started work on new implementation --- src/docker/containers.v | 13 +++++-- src/docker/docker.v | 20 +++++----- src/docker/socket.v | 83 ++++++++++++++++++++++++++++++++++------- src/main.v | 2 +- 4 files changed, 88 insertions(+), 30 deletions(-) diff --git a/src/docker/containers.v b/src/docker/containers.v index 14ac12d9..3b674e69 100644 --- a/src/docker/containers.v +++ b/src/docker/containers.v @@ -3,17 +3,22 @@ module docker import json import net.urllib import time +import net.http struct Container { id string [json: Id] names []string [json: Names] } -// containers returns a list of all currently running containers -pub fn containers() ?[]Container { - res := request('GET', urllib.parse('/v1.41/containers/json')?)? +pub fn (mut d DockerDaemon) containers() ?[]Container { + d.send_request('GET', urllib.parse('/v1.41/containers/json')?)? + res_header := d.read_response_head()? + content_length := res_header.header.get(http.CommonHeader.content_length)?.int() + res := d.read_response_body(content_length)? - return json.decode([]Container, res.text) or {} + data := json.decode([]Container, res)? + + return data } pub struct NewContainer { diff --git a/src/docker/docker.v b/src/docker/docker.v index ce01e7ef..fa83d897 100644 --- a/src/docker/docker.v +++ b/src/docker/docker.v @@ -9,8 +9,8 @@ import json // it. fn send(req &string) ?http.Response { // Open a connection to the socket - mut s := unix.connect_stream(docker.socket) or { - return error('Failed to connect to socket ${docker.socket}.') + mut s := unix.connect_stream(socket) or { + return error('Failed to connect to socket ${socket}.') } defer { @@ -21,19 +21,19 @@ fn send(req &string) ?http.Response { } // Write the request to the socket - s.write_string(req) or { return error('Failed to write request to socket ${docker.socket}.') } + s.write_string(req) or { return error('Failed to write request to socket ${socket}.') } s.wait_for_write()? mut c := 0 - mut buf := []u8{len: docker.buf_len} + mut buf := []u8{len: buf_len} mut res := []u8{} for { - c = s.read(mut buf) or { return error('Failed to read data from socket ${docker.socket}.') } + c = s.read(mut buf) or { return error('Failed to read data from socket ${socket}.') } res << buf[..c] - if c < docker.buf_len { + if c < buf_len { break } } @@ -41,7 +41,7 @@ fn send(req &string) ?http.Response { // After reading the first part of the response, we parse it into an HTTP // response. If it isn't chunked, we return early with the data. parsed := http.parse_response(res.bytestr()) or { - return error('Failed to parse HTTP response from socket ${docker.socket}.') + return error('Failed to parse HTTP response from socket ${socket}.') } if parsed.header.get(http.CommonHeader.transfer_encoding) or { '' } != 'chunked' { @@ -55,12 +55,10 @@ fn send(req &string) ?http.Response { s.wait_for_write()? for { - c = s.read(mut buf) or { - return error('Failed to read data from socket ${docker.socket}.') - } + c = s.read(mut buf) or { return error('Failed to read data from socket ${socket}.') } res << buf[..c] - if c < docker.buf_len { + if c < buf_len { break } } diff --git a/src/docker/socket.v b/src/docker/socket.v index 3a72f144..78154352 100644 --- a/src/docker/socket.v +++ b/src/docker/socket.v @@ -3,10 +3,15 @@ module docker import net.unix import io import net.http +import strings +import net.urllib +import json -const socket = '/var/run/docker.sock' - -const buf_len = 10 * 1024 +const ( + socket = '/var/run/docker.sock' + buf_len = 10 * 1024 + http_separator = [u8(`\r`), `\n`, `\r`, `\n`] +) pub struct DockerDaemon { mut: @@ -14,34 +19,84 @@ mut: reader &io.BufferedReader } -pub fn new_conn() ?DockerDaemon { - s := unix.connect_stream(socket) ? +pub fn new_conn() ?&DockerDaemon { + s := unix.connect_stream(docker.socket)? - d := DockerDaemon{socket: s, reader: io.new_buffered_reader(reader: s)} + d := &DockerDaemon{ + socket: s + reader: io.new_buffered_reader(reader: s) + } return d } -fn (mut d DockerDaemon) send_request(req &string) ? { - d.socket.write_string(req) ? - d.socket.wait_for_write() ? +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)? +} + +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)? +} + +pub fn (mut d DockerDaemon) request_with_json(method string, url urllib.URL, data &T) ? { + body := json.encode(data) + + return request_with_body(method, url, 'application/json', body) } // read_response_head consumes the socket's contents until it encounters -// '\n\n', after which it parses the response as an HTTP response. -fn (mut d DockerDaemon) read_response_head() ?http.Response { +// '\r\n\r\n', after which it parses the response as an HTTP response. +pub fn (mut d DockerDaemon) read_response_head() ?http.Response { mut c := 0 - mut buf := [buf_len]u8{len: docker.buf_len} + mut buf := []u8{len: 4} mut res := []u8{} for { - c = d.socket.read(mut buf) ? + c = d.reader.read(mut buf)? res << buf[..c] - if res#[-2..] == [u8(`\n`), `\n`] { + 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 { + mut buf2 := []u8{len: 4 - match_len} + c2 := d.reader.read(mut buf2)? + res << buf2[..c2] + + if buf2 == docker.http_separator[match_len..] { + break + } } } return http.parse_response(res.bytestr()) } + +pub fn (mut d DockerDaemon) read_response_body(length int) ?string { + 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 } + + builder.write(buf[..c])? + } + + return builder.str() +} diff --git a/src/main.v b/src/main.v index 41d0d331..db6d5ef9 100644 --- a/src/main.v +++ b/src/main.v @@ -31,7 +31,7 @@ fn main() { logs.cmd(), ] } - app.setup() app.parse(os.args) + return } From 4c97489f8a7f868e2ea58afbe9fe3fd47477ed02 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Fri, 13 May 2022 22:03:06 +0200 Subject: [PATCH 26/43] feat(docker): partially migrate to new code --- src/build/build.v | 11 ++++++---- src/docker/containers.v | 47 ++++++++++++++++++++++++++++++++++++++--- src/docker/socket.v | 12 +++++++++-- src/main.v | 1 + 4 files changed, 62 insertions(+), 9 deletions(-) diff --git a/src/build/build.v b/src/build/build.v index 41f68e28..b4708538 100644 --- a/src/build/build.v +++ b/src/build/build.v @@ -17,6 +17,8 @@ const build_image_repo = 'vieter-build' // makepkg with. The base image should be some Linux distribution that uses // Pacman as its package manager. pub fn create_build_image(base_image string) ?string { + mut dd := docker.new_conn() ? + commands := [ // Update repos & install required packages 'pacman -Syu --needed --noconfirm base-devel git' @@ -48,12 +50,13 @@ pub fn create_build_image(base_image string) ?string { // We pull the provided image docker.pull_image(image_name, image_tag)? - id := docker.create_container(c)? - docker.start_container(id)? + id := dd.create_container(c)?.id + /* id := docker.create_container(c)? */ + dd.start_container(id)? // This loop waits until the container has stopped, so we can remove it after for { - data := docker.inspect_container(id)? + data := dd.inspect_container(id)? if !data.state.running { break @@ -68,7 +71,7 @@ pub fn create_build_image(base_image string) ?string { // conflicts. tag := time.sys_mono_now().str() image := docker.create_image_from_container(id, 'vieter-build', tag)? - docker.remove_container(id)? + dd.remove_container(id)? return image.id } diff --git a/src/docker/containers.v b/src/docker/containers.v index 3b674e69..98116ccc 100644 --- a/src/docker/containers.v +++ b/src/docker/containers.v @@ -12,15 +12,14 @@ struct Container { pub fn (mut d DockerDaemon) containers() ?[]Container { d.send_request('GET', urllib.parse('/v1.41/containers/json')?)? - res_header := d.read_response_head()? - content_length := res_header.header.get(http.CommonHeader.content_length)?.int() - res := d.read_response_body(content_length)? + _, res := d.read_response()? data := json.decode([]Container, res)? return data } +[params] pub struct NewContainer { image string [json: Image] entrypoint []string [json: Entrypoint] @@ -31,7 +30,25 @@ pub struct NewContainer { } struct CreatedContainer { +pub: id string [json: Id] + warnings []string [json: Warnings] +} + +pub fn (mut d DockerDaemon) create_container(c NewContainer) ?CreatedContainer { + d.send_request_with_json('POST', urllib.parse('/v1.41/containers/create')?, c)? + _, res := d.read_response()? + + data := json.decode(CreatedContainer, res)? + + return data +} + +pub fn (mut d DockerDaemon) start_container(id string) ?bool { + d.send_request('POST', urllib.parse('/v1.41/containers/$id/start')?)? + head := d.read_response_head() ? + + return head.status_code == 204 } // create_container creates a container defined by the given configuration. If @@ -72,6 +89,25 @@ pub mut: end_time time.Time [skip] } +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()? + + if head.status_code != 200 { + return error('Failed to inspect container.') + } + + mut data := json.decode(ContainerInspect, body)? + + data.state.start_time = time.parse_rfc3339(data.state.start_time_str)? + + if data.state.status == 'exited' { + data.state.end_time = time.parse_rfc3339(data.state.end_time_str)? + } + + return data +} + // inspect_container returns the result of inspecting a container with a given // ID. pub fn inspect_container(id string) ?ContainerInspect { @@ -92,6 +128,11 @@ pub fn inspect_container(id string) ?ContainerInspect { return data } +pub fn (mut d DockerDaemon) remove_container(id string) ? { + d.send_request('DELETE', urllib.parse('/v1.41/containers/$id')?)? + head := d.read_response_head() ? +} + // remove_container removes a container with a given ID. pub fn remove_container(id string) ?bool { res := request('DELETE', urllib.parse('/v1.41/containers/$id')?)? diff --git a/src/docker/socket.v b/src/docker/socket.v index 78154352..41f9f6dd 100644 --- a/src/docker/socket.v +++ b/src/docker/socket.v @@ -42,10 +42,10 @@ pub fn (mut d DockerDaemon) send_request_with_body(method string, url urllib.URL d.socket.write_string(req)? } -pub fn (mut d DockerDaemon) request_with_json(method string, url urllib.URL, data &T) ? { +pub fn (mut d DockerDaemon) send_request_with_json(method string, url urllib.URL, data &T) ? { body := json.encode(data) - return request_with_body(method, url, 'application/json', body) + return d.send_request_with_body(method, url, 'application/json', body) } // read_response_head consumes the socket's contents until it encounters @@ -100,3 +100,11 @@ pub fn (mut d DockerDaemon) read_response_body(length int) ?string { return builder.str() } + +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() + res := d.read_response_body(content_length)? + + return head, res +} diff --git a/src/main.v b/src/main.v index db6d5ef9..6b1e7bc2 100644 --- a/src/main.v +++ b/src/main.v @@ -7,6 +7,7 @@ import build import console.git import console.logs import cron +import docker fn main() { mut app := cli.Command{ From da46b8b4ae4483fed98b911e0523182ffe2156e8 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Fri, 13 May 2022 22:15:30 +0200 Subject: [PATCH 27/43] feat(docker): error when HTTP requests fail --- src/build/build.v | 1 + src/docker/containers.v | 42 ++++++++++++++++++++++++++++++++++------- src/docker/socket.v | 6 ++++++ src/docker/stream.v | 9 +++++++++ 4 files changed, 51 insertions(+), 7 deletions(-) create mode 100644 src/docker/stream.v diff --git a/src/build/build.v b/src/build/build.v index b4708538..ae103189 100644 --- a/src/build/build.v +++ b/src/build/build.v @@ -56,6 +56,7 @@ pub fn create_build_image(base_image string) ?string { // This loop waits until the container has stopped, so we can remove it after for { + println('wot') data := dd.inspect_container(id)? if !data.state.running { diff --git a/src/docker/containers.v b/src/docker/containers.v index 98116ccc..e2da938f 100644 --- a/src/docker/containers.v +++ b/src/docker/containers.v @@ -5,6 +5,10 @@ import net.urllib import time import net.http +struct DockerError { + message string +} + struct Container { id string [json: Id] names []string [json: Names] @@ -12,7 +16,13 @@ struct Container { pub fn (mut d DockerDaemon) containers() ?[]Container { d.send_request('GET', urllib.parse('/v1.41/containers/json')?)? - _, res := d.read_response()? + head, res := d.read_response()? + + if head.status_code != 200 { + data := json.decode(DockerError, res)? + + return error(data.message) + } data := json.decode([]Container, res)? @@ -37,18 +47,28 @@ pub: pub fn (mut d DockerDaemon) create_container(c NewContainer) ?CreatedContainer { d.send_request_with_json('POST', urllib.parse('/v1.41/containers/create')?, c)? - _, res := d.read_response()? + head, res := d.read_response()? + + if head.status_code != 201 { + data := json.decode(DockerError, res)? + + return error(data.message) + } data := json.decode(CreatedContainer, res)? return data } -pub fn (mut d DockerDaemon) start_container(id string) ?bool { +pub fn (mut d DockerDaemon) start_container(id string) ? { d.send_request('POST', urllib.parse('/v1.41/containers/$id/start')?)? - head := d.read_response_head() ? + head, body := d.read_response() ? - return head.status_code == 204 + if head.status_code != 204 { + data := json.decode(DockerError, body)? + + return error(data.message) + } } // create_container creates a container defined by the given configuration. If @@ -94,7 +114,9 @@ pub fn (mut d DockerDaemon) inspect_container(id string) ?ContainerInspect { head, body := d.read_response()? if head.status_code != 200 { - return error('Failed to inspect container.') + data := json.decode(DockerError, body)? + + return error(data.message) } mut data := json.decode(ContainerInspect, body)? @@ -130,7 +152,13 @@ pub fn inspect_container(id string) ?ContainerInspect { pub fn (mut d DockerDaemon) remove_container(id string) ? { d.send_request('DELETE', urllib.parse('/v1.41/containers/$id')?)? - head := d.read_response_head() ? + head, body := d.read_response() ? + + if head.status_code != 204 { + data := json.decode(DockerError, body)? + + return error(data.message) + } } // remove_container removes a container with a given ID. diff --git a/src/docker/socket.v b/src/docker/socket.v index 41f9f6dd..749f14e6 100644 --- a/src/docker/socket.v +++ b/src/docker/socket.v @@ -50,6 +50,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. pub fn (mut d DockerDaemon) read_response_head() ?http.Response { mut c := 0 mut buf := []u8{len: 4} @@ -88,6 +90,10 @@ pub fn (mut d DockerDaemon) read_response_head() ?http.Response { } pub fn (mut d DockerDaemon) 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) diff --git a/src/docker/stream.v b/src/docker/stream.v new file mode 100644 index 00000000..24c51c12 --- /dev/null +++ b/src/docker/stream.v @@ -0,0 +1,9 @@ +module docker + +import io + +struct ChunkedResponseStream { + reader io.Reader +} + + From 92cbea69d6e4f8929ff5f397b36d7852e7084172 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Sat, 14 May 2022 21:55:19 +0200 Subject: [PATCH 28/43] feat(docker): added ChunkedResponseReader implementation --- src/build/build.v | 6 +-- src/docker/containers.v | 6 +-- src/docker/images.v | 26 ++++++++++++ src/docker/socket.v | 35 ++++++++++------ src/docker/stream.v | 92 ++++++++++++++++++++++++++++++++++++++++- src/util/util.v | 16 +++++++ 6 files changed, 161 insertions(+), 20 deletions(-) diff --git a/src/build/build.v b/src/build/build.v index ae103189..dd72c174 100644 --- a/src/build/build.v +++ b/src/build/build.v @@ -17,7 +17,7 @@ const build_image_repo = 'vieter-build' // makepkg with. The base image should be some Linux distribution that uses // Pacman as its package manager. pub fn create_build_image(base_image string) ?string { - mut dd := docker.new_conn() ? + mut dd := docker.new_conn()? commands := [ // Update repos & install required packages @@ -51,12 +51,12 @@ pub fn create_build_image(base_image string) ?string { docker.pull_image(image_name, image_tag)? id := dd.create_container(c)?.id - /* id := docker.create_container(c)? */ + // id := docker.create_container(c)? dd.start_container(id)? // This loop waits until the container has stopped, so we can remove it after for { - println('wot') + println('wot') data := dd.inspect_container(id)? if !data.state.running { diff --git a/src/docker/containers.v b/src/docker/containers.v index e2da938f..0027747d 100644 --- a/src/docker/containers.v +++ b/src/docker/containers.v @@ -41,7 +41,7 @@ pub struct NewContainer { struct CreatedContainer { pub: - id string [json: Id] + id string [json: Id] warnings []string [json: Warnings] } @@ -62,7 +62,7 @@ pub fn (mut d DockerDaemon) create_container(c NewContainer) ?CreatedContainer { 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() ? + head, body := d.read_response()? if head.status_code != 204 { data := json.decode(DockerError, body)? @@ -152,7 +152,7 @@ pub fn inspect_container(id string) ?ContainerInspect { pub fn (mut d DockerDaemon) remove_container(id string) ? { d.send_request('DELETE', urllib.parse('/v1.41/containers/$id')?)? - head, body := d.read_response() ? + head, body := d.read_response()? if head.status_code != 204 { data := json.decode(DockerError, body)? diff --git a/src/docker/images.v b/src/docker/images.v index 2e873fa0..22491139 100644 --- a/src/docker/images.v +++ b/src/docker/images.v @@ -9,6 +9,32 @@ pub: id string [json: Id] } +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()? + + 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) + } + + mut body := d.get_chunked_response_reader() + + mut buf := []u8{len: 1024} + + for { + c := body.read(mut buf)? + + if c == 0 { + break + } + print(buf[..c].bytestr()) + } +} + // pull_image pulls tries to pull the image for the given image & tag pub fn pull_image(image string, tag string) ?http.Response { return request('POST', urllib.parse('/v1.41/images/create?fromImage=$image&tag=$tag')?) diff --git a/src/docker/socket.v b/src/docker/socket.v index 749f14e6..444d9a96 100644 --- a/src/docker/socket.v +++ b/src/docker/socket.v @@ -6,11 +6,13 @@ import net.http import strings import net.urllib import json +import util const ( - socket = '/var/run/docker.sock' - buf_len = 10 * 1024 - http_separator = [u8(`\r`), `\n`, `\r`, `\n`] + socket = '/var/run/docker.sock' + buf_len = 10 * 1024 + http_separator = [u8(`\r`), `\n`, `\r`, `\n`] + http_chunk_separator = [u8(`\r`), `\n`] ) pub struct DockerDaemon { @@ -61,17 +63,18 @@ pub fn (mut d DockerDaemon) read_response_head() ?http.Response { c = d.reader.read(mut buf)? res << buf[..c] - mut i := 0 - mut match_len := 0 + 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 - } - } + // 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 @@ -114,3 +117,9 @@ pub fn (mut d DockerDaemon) read_response() ?(http.Response, string) { return head, res } + +pub fn (mut d DockerDaemon) get_chunked_response_reader() &ChunkedResponseReader { + r := new_chunked_response_reader(d.reader) + + return r +} diff --git a/src/docker/stream.v b/src/docker/stream.v index 24c51c12..fbc48bad 100644 --- a/src/docker/stream.v +++ b/src/docker/stream.v @@ -1,9 +1,99 @@ module docker import io +import util +import encoding.binary +import encoding.hex -struct ChunkedResponseStream { +struct ChunkedResponseReader { +mut: reader io.Reader + // buf []u8 + // offset int + // len int + bytes_left_in_chunk u64 + end_of_stream bool + started bool } +pub fn new_chunked_response_reader(reader io.Reader) &ChunkedResponseReader { + r := &ChunkedResponseReader{ + reader: reader + } + return r +} + +// We satisfy the io.Reader interface +pub fn (mut r ChunkedResponseReader) read(mut buf []u8) ?int { + if r.end_of_stream { + return none + } + + if r.bytes_left_in_chunk == 0 { + r.bytes_left_in_chunk = r.read_chunk_size()? + + if r.end_of_stream { + return none + } + } + + mut c := 0 + + if buf.len > r.bytes_left_in_chunk { + c = r.reader.read(mut buf[..r.bytes_left_in_chunk])? + } else { + c = r.reader.read(mut buf)? + } + + r.bytes_left_in_chunk -= u64(c) + + return c +} + +fn (mut r ChunkedResponseReader) read_chunk_size() ?u64 { + mut buf := []u8{len: 2} + mut res := []u8{} + + if r.started { + // Each chunk ends with a `\r\n` which we want to skip first + r.reader.read(mut buf) ? + } + + r.started = true + + for { + c := r.reader.read(mut buf)? + res << buf[..c] + + match_len := util.match_array_in_array(buf[..c], http_chunk_separator) + + if match_len == http_chunk_separator.len { + break + } + + if match_len > 0 { + mut buf2 := []u8{len: 2 - match_len} + c2 := r.reader.read(mut buf2)? + res << buf2[..c2] + + if buf2 == http_chunk_separator[match_len..] { + break + } + } + } + + mut num_data := hex.decode(res#[..-2].bytestr())? + + for num_data.len < 8 { + num_data.insert(0, 0) + } + + num := binary.big_endian_u64(num_data) + + if num == 0 { + r.end_of_stream = true + } + + return num +} diff --git a/src/util/util.v b/src/util/util.v index 66026215..f9f58edd 100644 --- a/src/util/util.v +++ b/src/util/util.v @@ -92,3 +92,19 @@ pub fn pretty_bytes(bytes int) string { return '${n:.2}${util.prefixes[i]}' } + +pub fn match_array_in_array(a1 []T, a2 []T) int { + mut i := 0 + mut match_len := 0 + + for i + match_len < a1.len { + if a1[i + match_len] == a2[match_len] { + match_len += 1 + } else { + i += match_len + 1 + match_len = 0 + } + } + + return match_len +} From f22ed29631a3e5d09b9496bb6bba255d4b5dc400 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Sat, 14 May 2022 22:39:52 +0200 Subject: [PATCH 29/43] feat(docker): added StreamFormatReader --- src/docker/containers.v | 15 +++++++++ src/docker/socket.v | 7 ++++ src/docker/stream.v | 73 ++++++++++++++++++++++++++++++++++------- 3 files changed, 84 insertions(+), 11 deletions(-) diff --git a/src/docker/containers.v b/src/docker/containers.v index 0027747d..ff48c698 100644 --- a/src/docker/containers.v +++ b/src/docker/containers.v @@ -168,6 +168,21 @@ pub fn remove_container(id string) ?bool { return res.status_code == 204 } +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()? + + 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() +} + // get_container_logs retrieves the logs for a Docker container, both stdout & // stderr. pub fn get_container_logs(id string) ?string { diff --git a/src/docker/socket.v b/src/docker/socket.v index 444d9a96..f6dfeb1c 100644 --- a/src/docker/socket.v +++ b/src/docker/socket.v @@ -123,3 +123,10 @@ pub fn (mut d DockerDaemon) get_chunked_response_reader() &ChunkedResponseReader return r } + +pub fn (mut d DockerDaemon) get_stream_format_reader() &StreamFormatReader { + r := new_chunked_response_reader(d.reader) + r2 := new_stream_format_reader(r) + + return r2 +} diff --git a/src/docker/stream.v b/src/docker/stream.v index fbc48bad..8822fd3a 100644 --- a/src/docker/stream.v +++ b/src/docker/stream.v @@ -7,13 +7,10 @@ import encoding.hex struct ChunkedResponseReader { mut: - reader io.Reader - // buf []u8 - // offset int - // len int + reader io.Reader bytes_left_in_chunk u64 end_of_stream bool - started bool + started bool } pub fn new_chunked_response_reader(reader io.Reader) &ChunkedResponseReader { @@ -39,7 +36,7 @@ pub fn (mut r ChunkedResponseReader) read(mut buf []u8) ?int { } mut c := 0 - + if buf.len > r.bytes_left_in_chunk { c = r.reader.read(mut buf[..r.bytes_left_in_chunk])? } else { @@ -56,8 +53,8 @@ fn (mut r ChunkedResponseReader) read_chunk_size() ?u64 { mut res := []u8{} if r.started { - // Each chunk ends with a `\r\n` which we want to skip first - r.reader.read(mut buf) ? + // Each chunk ends with a `\r\n` which we want to skip first + r.reader.read(mut buf)? } r.started = true @@ -85,9 +82,9 @@ fn (mut r ChunkedResponseReader) read_chunk_size() ?u64 { mut num_data := hex.decode(res#[..-2].bytestr())? - for num_data.len < 8 { - num_data.insert(0, 0) - } + for num_data.len < 8 { + num_data.insert(0, 0) + } num := binary.big_endian_u64(num_data) @@ -97,3 +94,57 @@ fn (mut r ChunkedResponseReader) read_chunk_size() ?u64 { return num } + +struct StreamFormatReader { + stdout bool + stderr bool + stdin bool +mut: + reader io.Reader + bytes_left_in_chunk u32 + end_of_stream bool +} + +pub fn new_stream_format_reader(reader io.Reader) &StreamFormatReader { + r := &StreamFormatReader{ + reader: reader + } + + return r +} + +pub fn (mut r StreamFormatReader) read(mut buf []u8) ?int { + if r.end_of_stream { + return none + } + + if r.bytes_left_in_chunk == 0 { + r.bytes_left_in_chunk = r.read_chunk_size()? + + if r.end_of_stream { + return none + } + } + + mut c := 0 + + if buf.len > r.bytes_left_in_chunk { + c = r.reader.read(mut buf[..r.bytes_left_in_chunk])? + } else { + c = r.reader.read(mut buf)? + } + + r.bytes_left_in_chunk -= u32(c) + + return c +} + +fn (mut r StreamFormatReader) read_chunk_size() ?u32 { + mut buf := []u8{len: 8} + + r.reader.read(mut buf)? + + num := binary.big_endian_u32(buf[4..]) + + return num +} From 1811ebbe3f3549b1034274a6a74675af4ef8469f Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Sat, 14 May 2022 23:42:20 +0200 Subject: [PATCH 30/43] doc: documented new Docker code --- src/docker/containers.v | 7 +++++++ src/docker/images.v | 1 + src/docker/socket.v | 33 +++++++++++++++++++-------------- src/docker/stream.v | 24 ++++++++++++++++++++---- src/util/util.v | 3 +++ 5 files changed, 50 insertions(+), 18 deletions(-) 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 From 79fd9c1f8741f15cda9cb03200fa37576b047e91 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Sun, 15 May 2022 00:23:52 +0200 Subject: [PATCH 31/43] fix(docker): read_response now handles chunked data --- src/build/build.v | 1 - src/docker/images.v | 5 +---- src/docker/socket.v | 22 +++++++++++++++++++--- src/util/util.v | 11 +++++++++++ 4 files changed, 31 insertions(+), 8 deletions(-) diff --git a/src/build/build.v b/src/build/build.v index dd72c174..98c56f5f 100644 --- a/src/build/build.v +++ b/src/build/build.v @@ -56,7 +56,6 @@ pub fn create_build_image(base_image string) ?string { // This loop waits until the container has stopped, so we can remove it after for { - println('wot') data := dd.inspect_container(id)? if !data.state.running { diff --git a/src/docker/images.v b/src/docker/images.v index 974b4083..ab735f24 100644 --- a/src/docker/images.v +++ b/src/docker/images.v @@ -27,11 +27,8 @@ pub fn (mut d DockerDaemon) pull_image(image string, tag string) ? { mut buf := []u8{len: 1024} for { - c := body.read(mut buf)? + c := body.read(mut buf) or { break } - if c == 0 { - break - } print(buf[..c].bytestr()) } } diff --git a/src/docker/socket.v b/src/docker/socket.v index b1e30800..dfa7ea7e 100644 --- a/src/docker/socket.v +++ b/src/docker/socket.v @@ -38,6 +38,9 @@ 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)? + + // 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. @@ -45,6 +48,9 @@ pub fn (mut d DockerDaemon) send_request_with_body(method string, url urllib.URL 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 @@ -108,11 +114,21 @@ 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. +// 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. pub fn (mut d DockerDaemon) read_response() ?(http.Response, string) { head := d.read_response_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() res := d.read_response_body(content_length)? diff --git a/src/util/util.v b/src/util/util.v index f805e6d8..7aabc1b9 100644 --- a/src/util/util.v +++ b/src/util/util.v @@ -23,6 +23,17 @@ pub fn exit_with_message(code int, msg string) { exit(code) } +// reader_to_writer tries to consume the entire reader & write it to the writer. +pub fn reader_to_writer(mut reader io.Reader, mut writer io.Writer) ? { + mut buf := []u8{len: 10 * 1024} + + for { + c := reader.read(mut buf) or { break } + + writer.write(buf) or { break } + } +} + // reader_to_file writes the contents of a BufferedReader to a file pub fn reader_to_file(mut reader io.BufferedReader, length int, path string) ? { mut file := os.create(path)? From e041682feae9d058d20e44dba58e185ba0310443 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Sun, 15 May 2022 09:56:23 +0200 Subject: [PATCH 32/43] feat(docker): fully migrate build commands to new code --- src/build/build.v | 43 +++++++++++++++++++++++++++++++++---------- src/docker/images.v | 33 ++++++++++++++++++++++++++++++--- src/docker/socket.v | 7 ++++++- src/main.v | 1 - src/util/util.v | 2 +- 5 files changed, 70 insertions(+), 16 deletions(-) diff --git a/src/build/build.v b/src/build/build.v index 98c56f5f..0de91a64 100644 --- a/src/build/build.v +++ b/src/build/build.v @@ -6,6 +6,8 @@ import time import os import db import client +import strings +import util const container_build_dir = '/build' @@ -19,6 +21,10 @@ const build_image_repo = 'vieter-build' pub fn create_build_image(base_image string) ?string { mut dd := docker.new_conn()? + defer { + dd.close() or {} + } + commands := [ // Update repos & install required packages 'pacman -Syu --needed --noconfirm base-devel git' @@ -48,7 +54,7 @@ pub fn create_build_image(base_image string) ?string { image_tag := if image_parts.len > 1 { image_parts[1] } else { 'latest' } // We pull the provided image - docker.pull_image(image_name, image_tag)? + dd.pull_image(image_name, image_tag)? id := dd.create_container(c)?.id // id := docker.create_container(c)? @@ -70,7 +76,7 @@ pub fn create_build_image(base_image string) ?string { // TODO also add the base image's name into the image name to prevent // conflicts. tag := time.sys_mono_now().str() - image := docker.create_image_from_container(id, 'vieter-build', tag)? + image := dd.create_image_from_container(id, 'vieter-build', tag)? dd.remove_container(id)? return image.id @@ -88,6 +94,12 @@ pub: // provided GitRepo. The base image ID should be of an image previously created // by create_build_image. It returns the logs of the container. pub fn build_repo(address string, api_key string, base_image_id string, repo &db.GitRepo) ?BuildResult { + mut dd := docker.new_conn()? + + defer { + dd.close() or {} + } + build_arch := os.uname().machine // TODO what to do with PKGBUILDs that build multiple packages? @@ -115,27 +127,31 @@ pub fn build_repo(address string, api_key string, base_image_id string, repo &db user: 'builder:builder' } - id := docker.create_container(c)? - docker.start_container(id)? + id := dd.create_container(c)?.id + dd.start_container(id)? - mut data := docker.inspect_container(id)? + mut data := dd.inspect_container(id)? // This loop waits until the container has stopped, so we can remove it after for data.state.running { time.sleep(1 * time.second) - data = docker.inspect_container(id)? + data = dd.inspect_container(id)? } - logs := docker.get_container_logs(id)? + mut logs_stream := dd.get_container_logs(id)? - docker.remove_container(id)? + // Read in the entire stream + mut logs_builder := strings.new_builder(10 * 1024) + util.reader_to_writer(mut logs_stream, mut logs_builder)? + + dd.remove_container(id)? return BuildResult{ start_time: data.state.start_time end_time: data.state.end_time exit_code: data.state.exit_code - logs: logs + logs: logs_builder.str() } } @@ -153,7 +169,14 @@ fn build(conf Config, repo_id int) ? { res := build_repo(conf.address, conf.api_key, image_id, repo)? println('Removing build image...') - docker.remove_image(image_id)? + + mut dd := docker.new_conn()? + + defer { + dd.close() or {} + } + + dd.remove_image(image_id)? println('Uploading logs to Vieter...') c.add_build_log(repo.id, res.start_time, res.end_time, build_arch, res.exit_code, diff --git a/src/docker/images.v b/src/docker/images.v index ab735f24..51620afd 100644 --- a/src/docker/images.v +++ b/src/docker/images.v @@ -22,14 +22,13 @@ pub fn (mut d DockerDaemon) pull_image(image string, tag string) ? { return error(data.message) } + // Keep reading the body until the pull has completed mut body := d.get_chunked_response_reader() mut buf := []u8{len: 1024} for { - c := body.read(mut buf) or { break } - - print(buf[..c].bytestr()) + body.read(mut buf) or { break } } } @@ -38,6 +37,22 @@ pub fn pull_image(image string, tag string) ?http.Response { return request('POST', urllib.parse('/v1.41/images/create?fromImage=$image&tag=$tag')?) } +// create_image_from_container creates a new image from a container. +pub fn (mut d DockerDaemon) create_image_from_container(id string, repo string, tag string) ?Image { + d.send_request('POST', urllib.parse('/v1.41/commit?container=$id&repo=$repo&tag=$tag')?)? + head, body := d.read_response()? + + if head.status_code != 201 { + data := json.decode(DockerError, body)? + + return error(data.message) + } + + data := json.decode(Image, body)? + + return data +} + // create_image_from_container creates a new image from a container with the // given repo & tag, given the container's ID. pub fn create_image_from_container(id string, repo string, tag string) ?Image { @@ -56,3 +71,15 @@ pub fn remove_image(id string) ?bool { return res.status_code == 200 } + +// remove_image removes the image with the given id. +pub fn (mut d DockerDaemon) remove_image(id string) ? { + d.send_request('DELETE', urllib.parse('/v1.41/images/$id')?)? + head, body := d.read_response()? + + if head.status_code != 200 { + data := json.decode(DockerError, body)? + + return error(data.message) + } +} diff --git a/src/docker/socket.v b/src/docker/socket.v index dfa7ea7e..2ee9bff7 100644 --- a/src/docker/socket.v +++ b/src/docker/socket.v @@ -33,6 +33,11 @@ pub fn new_conn() ?&DockerDaemon { return d } +// close closes the underlying socket connection. +pub fn (mut d DockerDaemon) close() ? { + d.socket.close()? +} + // 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' @@ -124,7 +129,7 @@ pub fn (mut d DockerDaemon) read_response() ?(http.Response, string) { mut builder := strings.new_builder(1024) mut body := d.get_chunked_response_reader() - util.reader_to_writer(mut body, mut builder) ? + util.reader_to_writer(mut body, mut builder)? return head, builder.str() } diff --git a/src/main.v b/src/main.v index 6b1e7bc2..db6d5ef9 100644 --- a/src/main.v +++ b/src/main.v @@ -7,7 +7,6 @@ import build import console.git import console.logs import cron -import docker fn main() { mut app := cli.Command{ diff --git a/src/util/util.v b/src/util/util.v index 7aabc1b9..324fb3d1 100644 --- a/src/util/util.v +++ b/src/util/util.v @@ -28,7 +28,7 @@ pub fn reader_to_writer(mut reader io.Reader, mut writer io.Writer) ? { mut buf := []u8{len: 10 * 1024} for { - c := reader.read(mut buf) or { break } + reader.read(mut buf) or { break } writer.write(buf) or { break } } From ce67208fbdda00d226c26358d5c42a1833c232f6 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Sun, 15 May 2022 10:01:12 +0200 Subject: [PATCH 33/43] refactor(docker): remove old code --- src/cron/daemon/daemon.v | 13 ++- src/docker/containers.v | 70 ---------------- src/docker/docker.v | 175 +++++++++++++++++++++++++++------------ src/docker/images.v | 24 ------ src/docker/socket.v | 158 ----------------------------------- 5 files changed, 131 insertions(+), 309 deletions(-) delete mode 100644 src/docker/socket.v diff --git a/src/cron/daemon/daemon.v b/src/cron/daemon/daemon.v index ade8fcbf..da3b46e2 100644 --- a/src/cron/daemon/daemon.v +++ b/src/cron/daemon/daemon.v @@ -253,14 +253,21 @@ fn (mut d Daemon) rebuild_base_image() bool { fn (mut d Daemon) clean_old_base_images() { mut i := 0 + mut dd := docker.new_conn() or { + d.lerror('Failed to connect to Docker socket.') + return + } + + defer { + dd.close() or {} + } + for i < d.builder_images.len - 1 { // For each builder image, we try to remove it by calling the Docker // API. If the function returns an error or false, that means the image // wasn't deleted. Therefore, we move the index over. If the function // returns true, the array's length has decreased by one so we don't // move the index. - if !docker.remove_image(d.builder_images[i]) or { false } { - i += 1 - } + dd.remove_image(d.builder_images[i]) or { i += 1 } } } diff --git a/src/docker/containers.v b/src/docker/containers.v index 05c9cc7f..76054526 100644 --- a/src/docker/containers.v +++ b/src/docker/containers.v @@ -30,7 +30,6 @@ pub fn (mut d DockerDaemon) containers() ?[]Container { return data } -[params] pub struct NewContainer { image string [json: Image] entrypoint []string [json: Entrypoint] @@ -74,26 +73,6 @@ pub fn (mut d DockerDaemon) start_container(id string) ? { } } -// create_container creates a container defined by the given configuration. If -// successful, it returns the ID of the newly created container. -pub fn create_container(c &NewContainer) ?string { - res := request_with_json('POST', urllib.parse('/v1.41/containers/create')?, c)? - - if res.status_code != 201 { - return error('Failed to create container.') - } - - return json.decode(CreatedContainer, res.text)?.id -} - -// start_container starts a container with a given ID. It returns whether the -// container was started or not. -pub fn start_container(id string) ?bool { - res := request('POST', urllib.parse('/v1.41/containers/$id/start')?)? - - return res.status_code == 204 -} - struct ContainerInspect { pub mut: state ContainerState [json: State] @@ -134,26 +113,6 @@ pub fn (mut d DockerDaemon) inspect_container(id string) ?ContainerInspect { return data } -// inspect_container returns the result of inspecting a container with a given -// ID. -pub fn inspect_container(id string) ?ContainerInspect { - res := request('GET', urllib.parse('/v1.41/containers/$id/json')?)? - - if res.status_code != 200 { - return error('Failed to inspect container.') - } - - mut data := json.decode(ContainerInspect, res.text)? - - data.state.start_time = time.parse_rfc3339(data.state.start_time_str)? - - if data.state.status == 'exited' { - data.state.end_time = time.parse_rfc3339(data.state.end_time_str)? - } - - 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')?)? @@ -166,13 +125,6 @@ pub fn (mut d DockerDaemon) remove_container(id string) ? { } } -// remove_container removes a container with a given ID. -pub fn remove_container(id string) ?bool { - res := request('DELETE', urllib.parse('/v1.41/containers/$id')?)? - - 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 { @@ -189,25 +141,3 @@ pub fn (mut d DockerDaemon) get_container_logs(id string) ?&StreamFormatReader { return d.get_stream_format_reader() } - -// get_container_logs retrieves the logs for a Docker container, both stdout & -// stderr. -pub fn get_container_logs(id string) ?string { - res := request('GET', urllib.parse('/v1.41/containers/$id/logs?stdout=true&stderr=true')?)? - mut res_bytes := res.text.bytes() - - // Docker uses a special "stream" format for their logs, so we have to - // clean up the data. - mut index := 0 - - for index < res_bytes.len { - // The reverse is required because V reads in the bytes differently - t := res_bytes[index + 4..index + 8].reverse() - len_length := unsafe { *(&u32(&t[0])) } - - res_bytes.delete_many(index, 8) - index += int(len_length) - } - - return res_bytes.bytestr() -} diff --git a/src/docker/docker.v b/src/docker/docker.v index fa83d897..2ee9bff7 100644 --- a/src/docker/docker.v +++ b/src/docker/docker.v @@ -1,91 +1,158 @@ module docker import net.unix -import net.urllib +import io import net.http +import strings +import net.urllib import json +import util -// send writes a request to the Docker socket, waits for a response & returns -// it. -fn send(req &string) ?http.Response { - // Open a connection to the socket - mut s := unix.connect_stream(socket) or { - return error('Failed to connect to socket ${socket}.') +const ( + socket = '/var/run/docker.sock' + buf_len = 10 * 1024 + http_separator = [u8(`\r`), `\n`, `\r`, `\n`] + http_chunk_separator = [u8(`\r`), `\n`] +) + +pub struct DockerDaemon { +mut: + socket &unix.StreamConn + reader &io.BufferedReader +} + +// new_conn creates a new connection to the Docker daemon. +pub fn new_conn() ?&DockerDaemon { + s := unix.connect_stream(docker.socket)? + + d := &DockerDaemon{ + socket: s + reader: io.new_buffered_reader(reader: s) } - defer { - // This or is required because otherwise, the V compiler segfaults for - // some reason - // https://github.com/vlang/v/issues/13534 - s.close() or {} - } + return d +} - // Write the request to the socket - s.write_string(req) or { return error('Failed to write request to socket ${socket}.') } +// close closes the underlying socket connection. +pub fn (mut d DockerDaemon) close() ? { + d.socket.close()? +} - s.wait_for_write()? +// 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)? + + // 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. +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)? + + // 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. +pub fn (mut d DockerDaemon) send_request_with_json(method string, url urllib.URL, data &T) ? { + body := json.encode(data) + + return d.send_request_with_body(method, url, '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. +pub fn (mut d DockerDaemon) read_response_head() ?http.Response { mut c := 0 - mut buf := []u8{len: buf_len} + mut buf := []u8{len: 4} mut res := []u8{} for { - c = s.read(mut buf) or { return error('Failed to read data from socket ${socket}.') } + c = d.reader.read(mut buf)? res << buf[..c] - if c < buf_len { + match_len := util.match_array_in_array(buf[..c], docker.http_separator) + + if match_len == 4 { break } - } - // After reading the first part of the response, we parse it into an HTTP - // response. If it isn't chunked, we return early with the data. - parsed := http.parse_response(res.bytestr()) or { - return error('Failed to parse HTTP response from socket ${socket}.') - } + if match_len > 0 { + mut buf2 := []u8{len: 4 - match_len} + c2 := d.reader.read(mut buf2)? + res << buf2[..c2] - if parsed.header.get(http.CommonHeader.transfer_encoding) or { '' } != 'chunked' { - return parsed - } - - // We loop until we've encountered the end of the chunked response - // A chunked HTTP response always ends with '0\r\n\r\n'. - for res.len < 5 || res#[-5..] != [u8(`0`), `\r`, `\n`, `\r`, `\n`] { - // Wait for the server to respond - s.wait_for_write()? - - for { - c = s.read(mut buf) or { return error('Failed to read data from socket ${socket}.') } - res << buf[..c] - - if c < buf_len { + if buf2 == docker.http_separator[match_len..] { break } } } - // Decode chunked response return http.parse_response(res.bytestr()) } -// request_with_body sends a request to the Docker socket with the given body. -fn request_with_body(method string, url urllib.URL, content_type string, body string) ?http.Response { - req := '$method $url.request_uri() HTTP/1.1\nHost: localhost\nContent-Type: $content_type\nContent-Length: $body.len\n\n$body\n\n' +// 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 '' + } - return send(req) + 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 } + + builder.write(buf[..c])? + } + + return builder.str() } -// request sends a request to the Docker socket with an empty body. -fn request(method string, url urllib.URL) ?http.Response { - req := '$method $url.request_uri() HTTP/1.1\nHost: localhost\n\n' +// 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. +pub fn (mut d DockerDaemon) read_response() ?(http.Response, string) { + head := d.read_response_head()? - return send(req) + 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() + res := d.read_response_body(content_length)? + + return head, res } -// request_with_json sends a request to the Docker socket with a given JSON -// payload -pub fn request_with_json(method string, url urllib.URL, data &T) ?http.Response { - body := json.encode(data) +// 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 request_with_body(method, url, 'application/json', body) + 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) + + return r2 } diff --git a/src/docker/images.v b/src/docker/images.v index 51620afd..58669058 100644 --- a/src/docker/images.v +++ b/src/docker/images.v @@ -32,11 +32,6 @@ pub fn (mut d DockerDaemon) pull_image(image string, tag string) ? { } } -// pull_image pulls tries to pull the image for the given image & tag -pub fn pull_image(image string, tag string) ?http.Response { - return request('POST', urllib.parse('/v1.41/images/create?fromImage=$image&tag=$tag')?) -} - // create_image_from_container creates a new image from a container. pub fn (mut d DockerDaemon) create_image_from_container(id string, repo string, tag string) ?Image { d.send_request('POST', urllib.parse('/v1.41/commit?container=$id&repo=$repo&tag=$tag')?)? @@ -53,25 +48,6 @@ pub fn (mut d DockerDaemon) create_image_from_container(id string, repo string, return data } -// create_image_from_container creates a new image from a container with the -// given repo & tag, given the container's ID. -pub fn create_image_from_container(id string, repo string, tag string) ?Image { - res := request('POST', urllib.parse('/v1.41/commit?container=$id&repo=$repo&tag=$tag')?)? - - if res.status_code != 201 { - return error('Failed to create image from container.') - } - - return json.decode(Image, res.text) or {} -} - -// remove_image removes the image with the given ID. -pub fn remove_image(id string) ?bool { - res := request('DELETE', urllib.parse('/v1.41/images/$id')?)? - - return res.status_code == 200 -} - // remove_image removes the image with the given id. pub fn (mut d DockerDaemon) remove_image(id string) ? { d.send_request('DELETE', urllib.parse('/v1.41/images/$id')?)? diff --git a/src/docker/socket.v b/src/docker/socket.v deleted file mode 100644 index 2ee9bff7..00000000 --- a/src/docker/socket.v +++ /dev/null @@ -1,158 +0,0 @@ -module docker - -import net.unix -import io -import net.http -import strings -import net.urllib -import json -import util - -const ( - socket = '/var/run/docker.sock' - buf_len = 10 * 1024 - http_separator = [u8(`\r`), `\n`, `\r`, `\n`] - http_chunk_separator = [u8(`\r`), `\n`] -) - -pub struct DockerDaemon { -mut: - socket &unix.StreamConn - reader &io.BufferedReader -} - -// new_conn creates a new connection to the Docker daemon. -pub fn new_conn() ?&DockerDaemon { - s := unix.connect_stream(docker.socket)? - - d := &DockerDaemon{ - socket: s - reader: io.new_buffered_reader(reader: s) - } - - return d -} - -// close closes the underlying socket connection. -pub fn (mut d DockerDaemon) close() ? { - d.socket.close()? -} - -// 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)? - - // 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. -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)? - - // 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. -pub fn (mut d DockerDaemon) send_request_with_json(method string, url urllib.URL, data &T) ? { - body := json.encode(data) - - return d.send_request_with_body(method, url, '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. -pub fn (mut d DockerDaemon) read_response_head() ?http.Response { - mut c := 0 - mut buf := []u8{len: 4} - mut res := []u8{} - - for { - c = d.reader.read(mut buf)? - res << buf[..c] - - match_len := util.match_array_in_array(buf[..c], docker.http_separator) - - if match_len == 4 { - break - } - - if match_len > 0 { - mut buf2 := []u8{len: 4 - match_len} - c2 := d.reader.read(mut buf2)? - res << buf2[..c2] - - if buf2 == docker.http_separator[match_len..] { - break - } - } - } - - 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 '' - } - - 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 } - - builder.write(buf[..c])? - } - - return 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. -pub fn (mut d DockerDaemon) read_response() ?(http.Response, string) { - head := d.read_response_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() - res := d.read_response_body(content_length)? - - 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) - - return r2 -} From 97cdaa18e1bb435ca512e81b70cb4fa3bc13f414 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Mon, 16 May 2022 13:03:44 +0200 Subject: [PATCH 34/43] refactor(docker): split stream separator code into own function --- CHANGELOG.md | 2 ++ src/docker/docker.v | 23 +---------------------- src/docker/stream.v | 23 ++--------------------- src/util/util.v | 36 ++++++++++++++++++++++++++++++++++-- 4 files changed, 39 insertions(+), 45 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e17cd60..c86761c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Web API for adding & querying build logs * CLI commands to access build logs API * Cron build logs are uploaded to above API +* Proper ASCII table output in CLI ### Changed @@ -20,6 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Official Arch packages are now split between `vieter` & `vieter-git` * `vieter` is the latest release * `vieter-git` is the latest commit on the dev branch +* Full refactor of Docker socket code ## [0.3.0-alpha.1](https://git.rustybever.be/vieter/vieter/src/tag/0.3.0-alpha.1) diff --git a/src/docker/docker.v b/src/docker/docker.v index 2ee9bff7..f612d1f1 100644 --- a/src/docker/docker.v +++ b/src/docker/docker.v @@ -71,30 +71,9 @@ pub fn (mut d DockerDaemon) send_request_with_json(method string, url urllib. // 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} mut res := []u8{} - for { - c = d.reader.read(mut buf)? - res << buf[..c] - - match_len := util.match_array_in_array(buf[..c], docker.http_separator) - - if match_len == 4 { - break - } - - if match_len > 0 { - mut buf2 := []u8{len: 4 - match_len} - c2 := d.reader.read(mut buf2)? - res << buf2[..c2] - - if buf2 == docker.http_separator[match_len..] { - break - } - } - } + util.read_until_separator(mut d.reader, mut res, http_separator) ? return http.parse_response(res.bytestr()) } diff --git a/src/docker/stream.v b/src/docker/stream.v index f20fe185..ed73098f 100644 --- a/src/docker/stream.v +++ b/src/docker/stream.v @@ -59,7 +59,6 @@ pub fn (mut r ChunkedResponseReader) read(mut buf []u8) ?int { // completely consumed. fn (mut r ChunkedResponseReader) read_chunk_size() ?u64 { mut buf := []u8{len: 2} - mut res := []u8{} if r.started { // Each chunk ends with a `\r\n` which we want to skip first @@ -68,26 +67,8 @@ fn (mut r ChunkedResponseReader) read_chunk_size() ?u64 { r.started = true - for { - c := r.reader.read(mut buf)? - res << buf[..c] - - match_len := util.match_array_in_array(buf[..c], http_chunk_separator) - - if match_len == http_chunk_separator.len { - break - } - - if match_len > 0 { - mut buf2 := []u8{len: 2 - match_len} - c2 := r.reader.read(mut buf2)? - res << buf2[..c2] - - if buf2 == http_chunk_separator[match_len..] { - break - } - } - } + mut res := []u8{} + util.read_until_separator(mut r.reader, mut res, http_chunk_separator) ? // The length of the next chunk is provided as a hexadecimal mut num_data := hex.decode(res#[..-2].bytestr())? diff --git a/src/util/util.v b/src/util/util.v index 324fb3d1..9cf3011c 100644 --- a/src/util/util.v +++ b/src/util/util.v @@ -28,9 +28,14 @@ pub fn reader_to_writer(mut reader io.Reader, mut writer io.Writer) ? { mut buf := []u8{len: 10 * 1024} for { - reader.read(mut buf) or { break } + bytes_read := reader.read(mut buf) or { break } + mut bytes_written := 0 - writer.write(buf) or { break } + for bytes_written < bytes_read { + c := writer.write(buf[bytes_written..bytes_read]) or { break } + + bytes_written += c + } } } @@ -122,3 +127,30 @@ pub fn match_array_in_array(a1 []T, a2 []T) int { return match_len } + +// read_until_separator consumes an io.Reader until it encounters some +// separator array. The data read is stored inside the provided res array. +pub fn read_until_separator(mut reader io.Reader, mut res []u8, sep []u8) ? { + mut buf := []u8{len: sep.len} + + for { + c := reader.read(mut buf)? + res << buf[..c] + + match_len := match_array_in_array(buf[..c], sep) + + if match_len == sep.len { + break + } + + if match_len > 0 { + match_left := sep.len - match_len + c2 := reader.read(mut buf[..match_left])? + res << buf[..c2] + + if buf[..c2] == sep[match_len..] { + break + } + } + } +} From 1d3c7a1651c238c6818a3434f230a4a54ab7840c Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Mon, 16 May 2022 14:09:21 +0200 Subject: [PATCH 35/43] refactor(docker): renamed DockerDaemon to DockerConn --- src/docker/README.md | 5 +++++ src/docker/containers.v | 12 ++++++------ src/docker/docker.v | 26 +++++++++++++------------- src/docker/images.v | 6 +++--- src/docker/stream.v | 2 +- 5 files changed, 28 insertions(+), 23 deletions(-) create mode 100644 src/docker/README.md diff --git a/src/docker/README.md b/src/docker/README.md new file mode 100644 index 00000000..5236277e --- /dev/null +++ b/src/docker/README.md @@ -0,0 +1,5 @@ +# docker + +This module implements part of the Docker Engine API v1.41 +([documentation](https://docs.docker.com/engine/api/v1.41/)) using socket-based +HTTP communication. diff --git a/src/docker/containers.v b/src/docker/containers.v index 76054526..62c8031c 100644 --- a/src/docker/containers.v +++ b/src/docker/containers.v @@ -15,7 +15,7 @@ struct Container { } // containers returns a list of all containers. -pub fn (mut d DockerDaemon) containers() ?[]Container { +pub fn (mut d DockerConn) containers() ?[]Container { d.send_request('GET', urllib.parse('/v1.41/containers/json')?)? head, res := d.read_response()? @@ -46,7 +46,7 @@ pub: } // create_container creates a new container with the given config. -pub fn (mut d DockerDaemon) create_container(c NewContainer) ?CreatedContainer { +pub fn (mut d DockerConn) create_container(c NewContainer) ?CreatedContainer { d.send_request_with_json('POST', urllib.parse('/v1.41/containers/create')?, c)? head, res := d.read_response()? @@ -62,7 +62,7 @@ pub fn (mut d DockerDaemon) create_container(c NewContainer) ?CreatedContainer { } // start_container starts the container with the given id. -pub fn (mut d DockerDaemon) start_container(id string) ? { +pub fn (mut d DockerConn) start_container(id string) ? { d.send_request('POST', urllib.parse('/v1.41/containers/$id/start')?)? head, body := d.read_response()? @@ -92,7 +92,7 @@ pub mut: } // inspect_container returns detailed information for a given container. -pub fn (mut d DockerDaemon) inspect_container(id string) ?ContainerInspect { +pub fn (mut d DockerConn) inspect_container(id string) ?ContainerInspect { d.send_request('GET', urllib.parse('/v1.41/containers/$id/json')?)? head, body := d.read_response()? @@ -114,7 +114,7 @@ pub fn (mut d DockerDaemon) inspect_container(id string) ?ContainerInspect { } // remove_container removes the container with the given id. -pub fn (mut d DockerDaemon) remove_container(id string) ? { +pub fn (mut d DockerConn) remove_container(id string) ? { d.send_request('DELETE', urllib.parse('/v1.41/containers/$id')?)? head, body := d.read_response()? @@ -127,7 +127,7 @@ pub fn (mut d DockerDaemon) remove_container(id string) ? { // 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 { +pub fn (mut d DockerConn) 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/docker.v b/src/docker/docker.v index f612d1f1..4ce1ea6a 100644 --- a/src/docker/docker.v +++ b/src/docker/docker.v @@ -15,17 +15,17 @@ const ( http_chunk_separator = [u8(`\r`), `\n`] ) -pub struct DockerDaemon { +pub struct DockerConn { mut: socket &unix.StreamConn reader &io.BufferedReader } // new_conn creates a new connection to the Docker daemon. -pub fn new_conn() ?&DockerDaemon { +pub fn new_conn() ?&DockerConn { s := unix.connect_stream(docker.socket)? - d := &DockerDaemon{ + d := &DockerConn{ socket: s reader: io.new_buffered_reader(reader: s) } @@ -34,12 +34,12 @@ pub fn new_conn() ?&DockerDaemon { } // close closes the underlying socket connection. -pub fn (mut d DockerDaemon) close() ? { +pub fn (mut d DockerConn) close() ? { d.socket.close()? } // send_request sends an HTTP request without body. -pub fn (mut d DockerDaemon) send_request(method string, url urllib.URL) ? { +pub fn (mut d DockerConn) send_request(method string, url urllib.URL) ? { req := '$method $url.request_uri() HTTP/1.1\nHost: localhost\n\n' d.socket.write_string(req)? @@ -49,7 +49,7 @@ pub fn (mut d DockerDaemon) send_request(method string, url urllib.URL) ? { } // 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) ? { +pub fn (mut d DockerConn) 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)? @@ -60,7 +60,7 @@ pub fn (mut d DockerDaemon) send_request_with_body(method string, url urllib.URL // 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) ? { +pub fn (mut d DockerConn) send_request_with_json(method string, url urllib.URL, data &T) ? { body := json.encode(data) return d.send_request_with_body(method, url, 'application/json', body) @@ -70,17 +70,17 @@ pub fn (mut d DockerDaemon) send_request_with_json(method string, url urllib. // '\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. -pub fn (mut d DockerDaemon) read_response_head() ?http.Response { +pub fn (mut d DockerConn) read_response_head() ?http.Response { mut res := []u8{} - util.read_until_separator(mut d.reader, mut res, http_separator) ? + util.read_until_separator(mut d.reader, mut res, docker.http_separator)? 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 { +pub fn (mut d DockerConn) read_response_body(length int) ?string { if length == 0 { return '' } @@ -101,7 +101,7 @@ pub fn (mut d DockerDaemon) read_response_body(length int) ?string { // 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. -pub fn (mut d DockerDaemon) read_response() ?(http.Response, string) { +pub fn (mut d DockerConn) read_response() ?(http.Response, string) { head := d.read_response_head()? if head.header.get(http.CommonHeader.transfer_encoding) or { '' } == 'chunked' { @@ -121,7 +121,7 @@ pub fn (mut d DockerDaemon) read_response() ?(http.Response, string) { // get_chunked_response_reader returns a ChunkedResponseReader using the socket // as reader. -pub fn (mut d DockerDaemon) get_chunked_response_reader() &ChunkedResponseReader { +pub fn (mut d DockerConn) get_chunked_response_reader() &ChunkedResponseReader { r := new_chunked_response_reader(d.reader) return r @@ -129,7 +129,7 @@ pub fn (mut d DockerDaemon) get_chunked_response_reader() &ChunkedResponseReader // get_stream_format_reader returns a StreamFormatReader using the socket as // reader. -pub fn (mut d DockerDaemon) get_stream_format_reader() &StreamFormatReader { +pub fn (mut d DockerConn) get_stream_format_reader() &StreamFormatReader { r := new_chunked_response_reader(d.reader) r2 := new_stream_format_reader(r) diff --git a/src/docker/images.v b/src/docker/images.v index 58669058..cab7f341 100644 --- a/src/docker/images.v +++ b/src/docker/images.v @@ -10,7 +10,7 @@ pub: } // pull_image pulls the given image:tag. -pub fn (mut d DockerDaemon) pull_image(image string, tag string) ? { +pub fn (mut d DockerConn) 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()? @@ -33,7 +33,7 @@ pub fn (mut d DockerDaemon) pull_image(image string, tag string) ? { } // create_image_from_container creates a new image from a container. -pub fn (mut d DockerDaemon) create_image_from_container(id string, repo string, tag string) ?Image { +pub fn (mut d DockerConn) create_image_from_container(id string, repo string, tag string) ?Image { d.send_request('POST', urllib.parse('/v1.41/commit?container=$id&repo=$repo&tag=$tag')?)? head, body := d.read_response()? @@ -49,7 +49,7 @@ pub fn (mut d DockerDaemon) create_image_from_container(id string, repo string, } // remove_image removes the image with the given id. -pub fn (mut d DockerDaemon) remove_image(id string) ? { +pub fn (mut d DockerConn) remove_image(id string) ? { d.send_request('DELETE', urllib.parse('/v1.41/images/$id')?)? head, body := d.read_response()? diff --git a/src/docker/stream.v b/src/docker/stream.v index ed73098f..dff17846 100644 --- a/src/docker/stream.v +++ b/src/docker/stream.v @@ -68,7 +68,7 @@ fn (mut r ChunkedResponseReader) read_chunk_size() ?u64 { r.started = true mut res := []u8{} - util.read_until_separator(mut r.reader, mut res, http_chunk_separator) ? + util.read_until_separator(mut r.reader, mut res, http_chunk_separator)? // The length of the next chunk is provided as a hexadecimal mut num_data := hex.decode(res#[..-2].bytestr())? From 055b168ff12d1db28ad0dc918a3790cd523597c5 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Mon, 16 May 2022 14:22:53 +0200 Subject: [PATCH 36/43] refactor(util): split into two files --- src/build/build.v | 7 ++-- src/docker/README.md | 2 - src/docker/stream.v | 4 +- src/env/env.v | 13 +++--- src/util/README.md | 2 + src/util/stream.v | 95 ++++++++++++++++++++++++++++++++++++++++++ src/util/util.v | 98 ++------------------------------------------ 7 files changed, 114 insertions(+), 107 deletions(-) create mode 100644 src/util/README.md create mode 100644 src/util/stream.v diff --git a/src/build/build.v b/src/build/build.v index 0de91a64..2784c260 100644 --- a/src/build/build.v +++ b/src/build/build.v @@ -9,9 +9,10 @@ import client import strings import util -const container_build_dir = '/build' - -const build_image_repo = 'vieter-build' +const ( + container_build_dir = '/build' + build_image_repo = 'vieter-build' +) // create_build_image creates a builder image given some base image which can // then be used to build & package Arch images. It mostly just updates the diff --git a/src/docker/README.md b/src/docker/README.md index 5236277e..4cc8971a 100644 --- a/src/docker/README.md +++ b/src/docker/README.md @@ -1,5 +1,3 @@ -# docker - This module implements part of the Docker Engine API v1.41 ([documentation](https://docs.docker.com/engine/api/v1.41/)) using socket-based HTTP communication. diff --git a/src/docker/stream.v b/src/docker/stream.v index dff17846..02fb9720 100644 --- a/src/docker/stream.v +++ b/src/docker/stream.v @@ -58,9 +58,9 @@ pub fn (mut r ChunkedResponseReader) read(mut buf []u8) ?int { // 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} - if r.started { + mut buf := []u8{len: 2} + // Each chunk ends with a `\r\n` which we want to skip first r.reader.read(mut buf)? } diff --git a/src/env/env.v b/src/env/env.v index d1459311..3d07d235 100644 --- a/src/env/env.v +++ b/src/env/env.v @@ -3,12 +3,13 @@ module env import os import toml -// The prefix that every environment variable should have -const prefix = 'VIETER_' - -// The suffix an environment variable in order for it to be loaded from a file -// instead -const file_suffix = '_FILE' +const ( + // The prefix that every environment variable should have + prefix = 'VIETER_' + // The suffix an environment variable in order for it to be loaded from a file + // instead + file_suffix = '_FILE' +) fn get_env_var(field_name string) ?string { env_var_name := '$env.prefix$field_name.to_upper()' diff --git a/src/util/README.md b/src/util/README.md new file mode 100644 index 00000000..529e412e --- /dev/null +++ b/src/util/README.md @@ -0,0 +1,2 @@ +This module defines a few useful functions used throughout the codebase that +don't specifically fit inside a module. diff --git a/src/util/stream.v b/src/util/stream.v new file mode 100644 index 00000000..06397aa8 --- /dev/null +++ b/src/util/stream.v @@ -0,0 +1,95 @@ +// Functions for interacting with `io.Reader` & `io.Writer` objects. +module util + +import io +import os + +// reader_to_writer tries to consume the entire reader & write it to the writer. +pub fn reader_to_writer(mut reader io.Reader, mut writer io.Writer) ? { + mut buf := []u8{len: 10 * 1024} + + for { + bytes_read := reader.read(mut buf) or { break } + mut bytes_written := 0 + + for bytes_written < bytes_read { + c := writer.write(buf[bytes_written..bytes_read]) or { break } + + bytes_written += c + } + } +} + +// reader_to_file writes the contents of a BufferedReader to a file +pub fn reader_to_file(mut reader io.BufferedReader, length int, path string) ? { + mut file := os.create(path)? + defer { + file.close() + } + + mut buf := []u8{len: reader_buf_size} + mut bytes_left := length + + // Repeat as long as the stream still has data + for bytes_left > 0 { + // TODO check if just breaking here is safe + bytes_read := reader.read(mut buf) or { break } + bytes_left -= bytes_read + + mut to_write := bytes_read + + for to_write > 0 { + // TODO don't just loop infinitely here + bytes_written := file.write(buf[bytes_read - to_write..bytes_read]) or { continue } + // file.flush() + + to_write = to_write - bytes_written + } + } +} + +// 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 + + for i + match_len < a1.len { + if a1[i + match_len] == a2[match_len] { + match_len += 1 + } else { + i += match_len + 1 + match_len = 0 + } + } + + return match_len +} + +// read_until_separator consumes an io.Reader until it encounters some +// separator array. The data read is stored inside the provided res array. +pub fn read_until_separator(mut reader io.Reader, mut res []u8, sep []u8) ? { + mut buf := []u8{len: sep.len} + + for { + c := reader.read(mut buf)? + res << buf[..c] + + match_len := match_array_in_array(buf[..c], sep) + + if match_len == sep.len { + break + } + + if match_len > 0 { + match_left := sep.len - match_len + c2 := reader.read(mut buf[..match_left])? + res << buf[..c2] + + if buf[..c2] == sep[match_len..] { + break + } + } + } +} diff --git a/src/util/util.v b/src/util/util.v index 9cf3011c..266bcb5b 100644 --- a/src/util/util.v +++ b/src/util/util.v @@ -1,13 +1,13 @@ module util import os -import io import crypto.md5 import crypto.sha256 -const reader_buf_size = 1_000_000 - -const prefixes = ['B', 'KB', 'MB', 'GB'] +const ( + reader_buf_size = 1_000_000 + prefixes = ['B', 'KB', 'MB', 'GB'] +) // Dummy struct to work around the fact that you can only share structs, maps & // arrays @@ -23,50 +23,6 @@ pub fn exit_with_message(code int, msg string) { exit(code) } -// reader_to_writer tries to consume the entire reader & write it to the writer. -pub fn reader_to_writer(mut reader io.Reader, mut writer io.Writer) ? { - mut buf := []u8{len: 10 * 1024} - - for { - bytes_read := reader.read(mut buf) or { break } - mut bytes_written := 0 - - for bytes_written < bytes_read { - c := writer.write(buf[bytes_written..bytes_read]) or { break } - - bytes_written += c - } - } -} - -// reader_to_file writes the contents of a BufferedReader to a file -pub fn reader_to_file(mut reader io.BufferedReader, length int, path string) ? { - mut file := os.create(path)? - defer { - file.close() - } - - mut buf := []u8{len: util.reader_buf_size} - mut bytes_left := length - - // Repeat as long as the stream still has data - for bytes_left > 0 { - // TODO check if just breaking here is safe - bytes_read := reader.read(mut buf) or { break } - bytes_left -= bytes_read - - mut to_write := bytes_read - - for to_write > 0 { - // TODO don't just loop infinitely here - bytes_written := file.write(buf[bytes_read - to_write..bytes_read]) or { continue } - // file.flush() - - to_write = to_write - bytes_written - } - } -} - // hash_file returns the md5 & sha256 hash of a given file // TODO actually implement sha256 pub fn hash_file(path &string) ?(string, string) { @@ -108,49 +64,3 @@ 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 - - for i + match_len < a1.len { - if a1[i + match_len] == a2[match_len] { - match_len += 1 - } else { - i += match_len + 1 - match_len = 0 - } - } - - return match_len -} - -// read_until_separator consumes an io.Reader until it encounters some -// separator array. The data read is stored inside the provided res array. -pub fn read_until_separator(mut reader io.Reader, mut res []u8, sep []u8) ? { - mut buf := []u8{len: sep.len} - - for { - c := reader.read(mut buf)? - res << buf[..c] - - match_len := match_array_in_array(buf[..c], sep) - - if match_len == sep.len { - break - } - - if match_len > 0 { - match_left := sep.len - match_len - c2 := reader.read(mut buf[..match_left])? - res << buf[..c2] - - if buf[..c2] == sep[match_len..] { - break - } - } - } -} From d4c803c41c7d1322cd3a3e7975bc10c6adb69a59 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Mon, 16 May 2022 14:53:48 +0200 Subject: [PATCH 37/43] doc(env): added missing docstring & README --- src/env/README.md | 7 +++++++ src/env/env.v | 5 +++++ 2 files changed, 12 insertions(+) create mode 100644 src/env/README.md diff --git a/src/env/README.md b/src/env/README.md new file mode 100644 index 00000000..135e8fa9 --- /dev/null +++ b/src/env/README.md @@ -0,0 +1,7 @@ +This module provides a framework for parsing a configuration, defined as a +struct, from both a TOML configuration file & environment variables. Some +notable features are: + +* Overwrite values in config file using environment variables +* Allow default values in config struct +* Read environment variable value from file diff --git a/src/env/env.v b/src/env/env.v index 3d07d235..5ed19552 100644 --- a/src/env/env.v +++ b/src/env/env.v @@ -11,6 +11,11 @@ const ( file_suffix = '_FILE' ) +// get_env_var tries to read the contents of the given environment variable. It +// looks for either `${env.prefix}${field_name.to_upper()}` or +// `${env.prefix}${field_name.to_upper()}${env.file_suffix}`, returning the +// contents of the file instead if the latter. If both or neither exist, the +// function returns an error. fn get_env_var(field_name string) ?string { env_var_name := '$env.prefix$field_name.to_upper()' env_file_name := '$env.prefix$field_name.to_upper()$env.file_suffix' From 850cba6ab9b81b8bf2c28a9348c5ba7ed2f9181f Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Mon, 16 May 2022 15:02:57 +0200 Subject: [PATCH 38/43] refactor(docker): use http.Method instead of strings --- src/docker/containers.v | 14 +++++++------- src/docker/docker.v | 6 +++--- src/docker/images.v | 8 ++++---- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/docker/containers.v b/src/docker/containers.v index 62c8031c..a790243d 100644 --- a/src/docker/containers.v +++ b/src/docker/containers.v @@ -3,7 +3,7 @@ module docker import json import net.urllib import time -import net.http +import net.http { Method } struct DockerError { message string @@ -16,7 +16,7 @@ struct Container { // containers returns a list of all containers. pub fn (mut d DockerConn) containers() ?[]Container { - d.send_request('GET', urllib.parse('/v1.41/containers/json')?)? + d.send_request(Method.get, urllib.parse('/v1.41/containers/json')?)? head, res := d.read_response()? if head.status_code != 200 { @@ -47,7 +47,7 @@ pub: // create_container creates a new container with the given config. pub fn (mut d DockerConn) create_container(c NewContainer) ?CreatedContainer { - d.send_request_with_json('POST', urllib.parse('/v1.41/containers/create')?, c)? + d.send_request_with_json(Method.post, urllib.parse('/v1.41/containers/create')?, c)? head, res := d.read_response()? if head.status_code != 201 { @@ -63,7 +63,7 @@ pub fn (mut d DockerConn) create_container(c NewContainer) ?CreatedContainer { // start_container starts the container with the given id. pub fn (mut d DockerConn) start_container(id string) ? { - d.send_request('POST', urllib.parse('/v1.41/containers/$id/start')?)? + d.send_request(Method.post, urllib.parse('/v1.41/containers/$id/start')?)? head, body := d.read_response()? if head.status_code != 204 { @@ -93,7 +93,7 @@ pub mut: // inspect_container returns detailed information for a given container. pub fn (mut d DockerConn) inspect_container(id string) ?ContainerInspect { - d.send_request('GET', urllib.parse('/v1.41/containers/$id/json')?)? + d.send_request(Method.get, urllib.parse('/v1.41/containers/$id/json')?)? head, body := d.read_response()? if head.status_code != 200 { @@ -115,7 +115,7 @@ pub fn (mut d DockerConn) inspect_container(id string) ?ContainerInspect { // remove_container removes the container with the given id. pub fn (mut d DockerConn) remove_container(id string) ? { - d.send_request('DELETE', urllib.parse('/v1.41/containers/$id')?)? + d.send_request(Method.delete, urllib.parse('/v1.41/containers/$id')?)? head, body := d.read_response()? if head.status_code != 204 { @@ -128,7 +128,7 @@ pub fn (mut d DockerConn) remove_container(id string) ? { // get_container_logs returns a reader object allowing access to the // container's logs. pub fn (mut d DockerConn) get_container_logs(id string) ?&StreamFormatReader { - d.send_request('GET', urllib.parse('/v1.41/containers/$id/logs?stdout=true&stderr=true')?)? + d.send_request(Method.get, urllib.parse('/v1.41/containers/$id/logs?stdout=true&stderr=true')?)? head := d.read_response_head()? if head.status_code != 200 { diff --git a/src/docker/docker.v b/src/docker/docker.v index 4ce1ea6a..ccc6bedd 100644 --- a/src/docker/docker.v +++ b/src/docker/docker.v @@ -39,7 +39,7 @@ pub fn (mut d DockerConn) close() ? { } // send_request sends an HTTP request without body. -pub fn (mut d DockerConn) send_request(method string, url urllib.URL) ? { +pub fn (mut d DockerConn) send_request(method http.Method, url urllib.URL) ? { req := '$method $url.request_uri() HTTP/1.1\nHost: localhost\n\n' d.socket.write_string(req)? @@ -49,7 +49,7 @@ pub fn (mut d DockerConn) send_request(method string, url urllib.URL) ? { } // send_request_with_body sends an HTTP request with the given body. -pub fn (mut d DockerConn) send_request_with_body(method string, url urllib.URL, content_type string, body string) ? { +pub fn (mut d DockerConn) send_request_with_body(method http.Method, 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)? @@ -60,7 +60,7 @@ pub fn (mut d DockerConn) send_request_with_body(method string, url urllib.URL, // send_request_with_json is a convenience wrapper around // send_request_with_body that encodes the input as JSON. -pub fn (mut d DockerConn) send_request_with_json(method string, url urllib.URL, data &T) ? { +pub fn (mut d DockerConn) send_request_with_json(method http.Method, url urllib.URL, data &T) ? { body := json.encode(data) return d.send_request_with_body(method, url, 'application/json', body) diff --git a/src/docker/images.v b/src/docker/images.v index cab7f341..6161565a 100644 --- a/src/docker/images.v +++ b/src/docker/images.v @@ -1,6 +1,6 @@ module docker -import net.http +import net.http { Method } import net.urllib import json @@ -11,7 +11,7 @@ pub: // pull_image pulls the given image:tag. pub fn (mut d DockerConn) pull_image(image string, tag string) ? { - d.send_request('POST', urllib.parse('/v1.41/images/create?fromImage=$image&tag=$tag')?)? + d.send_request(Method.post, urllib.parse('/v1.41/images/create?fromImage=$image&tag=$tag')?)? head := d.read_response_head()? if head.status_code != 200 { @@ -34,7 +34,7 @@ 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', urllib.parse('/v1.41/commit?container=$id&repo=$repo&tag=$tag')?)? + d.send_request(Method.post, urllib.parse('/v1.41/commit?container=$id&repo=$repo&tag=$tag')?)? head, body := d.read_response()? if head.status_code != 201 { @@ -50,7 +50,7 @@ pub fn (mut d DockerConn) create_image_from_container(id string, repo string, ta // remove_image removes the image with the given id. pub fn (mut d DockerConn) remove_image(id string) ? { - d.send_request('DELETE', urllib.parse('/v1.41/images/$id')?)? + d.send_request(Method.delete, urllib.parse('/v1.41/images/$id')?)? head, body := d.read_response()? if head.status_code != 200 { From 3c87e60293810b37a1c574976261b1d7fc5a9df0 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Mon, 16 May 2022 15:36:21 +0200 Subject: [PATCH 39/43] refactor(docker): more tightly integrate streams --- src/docker/stream.v | 34 +++++++++++----------------------- 1 file changed, 11 insertions(+), 23 deletions(-) diff --git a/src/docker/stream.v b/src/docker/stream.v index 02fb9720..001f4b3e 100644 --- a/src/docker/stream.v +++ b/src/docker/stream.v @@ -9,15 +9,14 @@ import encoding.hex // it as if it was a continuous stream of data. struct ChunkedResponseReader { mut: - reader io.Reader + reader io.BufferedReader bytes_left_in_chunk u64 - end_of_stream bool 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 { +pub fn new_chunked_response_reader(reader io.BufferedReader) &ChunkedResponseReader { r := &ChunkedResponseReader{ reader: reader } @@ -27,16 +26,10 @@ pub fn new_chunked_response_reader(reader io.Reader) &ChunkedResponseReader { // read satisfies the io.Reader interface. pub fn (mut r ChunkedResponseReader) read(mut buf []u8) ?int { - if r.end_of_stream { - return none - } - if r.bytes_left_in_chunk == 0 { + // An io.BufferedReader always returns none if its stream has + // ended. r.bytes_left_in_chunk = r.read_chunk_size()? - - if r.end_of_stream { - return none - } } mut c := 0 @@ -82,7 +75,7 @@ fn (mut r ChunkedResponseReader) read_chunk_size() ?u64 { // This only occurs for the very last chunk, which always reports a size of // 0. if num == 0 { - r.end_of_stream = true + return none } return num @@ -92,14 +85,13 @@ fn (mut r ChunkedResponseReader) read_chunk_size() ?u64 { // header bytes. struct StreamFormatReader { mut: - reader io.Reader + reader ChunkedResponseReader 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 { +pub fn new_stream_format_reader(reader ChunkedResponseReader) &StreamFormatReader { r := &StreamFormatReader{ reader: reader } @@ -109,16 +101,8 @@ pub fn new_stream_format_reader(reader io.Reader) &StreamFormatReader { // read satisfies the io.Reader interface. pub fn (mut r StreamFormatReader) read(mut buf []u8) ?int { - if r.end_of_stream { - return none - } - if r.bytes_left_in_chunk == 0 { r.bytes_left_in_chunk = r.read_chunk_size()? - - if r.end_of_stream { - return none - } } mut c := 0 @@ -143,5 +127,9 @@ fn (mut r StreamFormatReader) read_chunk_size() ?u32 { num := binary.big_endian_u32(buf[4..]) + if num == 0 { + return none + } + return num } From 889d5a08849a6ab08b41e6ff94b78f7a9624c460 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Mon, 16 May 2022 15:39:23 +0200 Subject: [PATCH 40/43] refactor(docker): removed unused function --- src/docker/containers.v | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/src/docker/containers.v b/src/docker/containers.v index a790243d..0bc59bb3 100644 --- a/src/docker/containers.v +++ b/src/docker/containers.v @@ -9,27 +9,6 @@ struct DockerError { message string } -struct Container { - id string [json: Id] - names []string [json: Names] -} - -// containers returns a list of all containers. -pub fn (mut d DockerConn) containers() ?[]Container { - d.send_request(Method.get, urllib.parse('/v1.41/containers/json')?)? - head, res := d.read_response()? - - if head.status_code != 200 { - data := json.decode(DockerError, res)? - - return error(data.message) - } - - data := json.decode([]Container, res)? - - return data -} - pub struct NewContainer { image string [json: Image] entrypoint []string [json: Entrypoint] From 73d2d4b08f8af1b7c315520ced96256617b8852f Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Mon, 16 May 2022 17:12:37 +0200 Subject: [PATCH 41/43] feat(console): replaced `vieter build` with `vieter repos build` --- CHANGELOG.md | 7 +++++++ src/build/build.v | 29 ----------------------------- src/build/cli.v | 29 ----------------------------- src/console/git/build.v | 34 ++++++++++++++++++++++++++++++++++ src/console/git/git.v | 17 +++++++++++++++-- src/main.v | 2 -- 6 files changed, 56 insertions(+), 62 deletions(-) delete mode 100644 src/build/cli.v create mode 100644 src/console/git/build.v diff --git a/CHANGELOG.md b/CHANGELOG.md index c86761c4..7c203938 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * CLI commands to access build logs API * Cron build logs are uploaded to above API * Proper ASCII table output in CLI +* `vieter repos build id` command to run builds locally + +### Removed + +* `vieter build` command + * This command was used alongside cron for periodic builds, but this has + been replaced by `vieter cron` ### Changed diff --git a/src/build/build.v b/src/build/build.v index 2784c260..fab6c35b 100644 --- a/src/build/build.v +++ b/src/build/build.v @@ -5,7 +5,6 @@ import encoding.base64 import time import os import db -import client import strings import util @@ -155,31 +154,3 @@ pub fn build_repo(address string, api_key string, base_image_id string, repo &db logs: logs_builder.str() } } - -// build builds every Git repo in the server's list. -fn build(conf Config, repo_id int) ? { - c := client.new(conf.address, conf.api_key) - repo := c.get_git_repo(repo_id)? - - build_arch := os.uname().machine - - println('Creating base image...') - image_id := create_build_image(conf.base_image)? - - println('Running build...') - res := build_repo(conf.address, conf.api_key, image_id, repo)? - - println('Removing build image...') - - mut dd := docker.new_conn()? - - defer { - dd.close() or {} - } - - dd.remove_image(image_id)? - - println('Uploading logs to Vieter...') - c.add_build_log(repo.id, res.start_time, res.end_time, build_arch, res.exit_code, - res.logs)? -} diff --git a/src/build/cli.v b/src/build/cli.v deleted file mode 100644 index 64814cb2..00000000 --- a/src/build/cli.v +++ /dev/null @@ -1,29 +0,0 @@ -module build - -import cli -import env - -pub struct Config { -pub: - api_key string - address string - base_image string = 'archlinux:base-devel' -} - -// cmd returns the cli submodule that handles the build process -pub fn cmd() cli.Command { - return cli.Command{ - name: 'build' - required_args: 1 - usage: 'id' - description: 'Build the repository with the given ID.' - execute: fn (cmd cli.Command) ? { - config_file := cmd.flags.get_string('config-file')? - conf := env.load(config_file)? - - id := cmd.args[0].int() - - build(conf, id)? - } - } -} diff --git a/src/console/git/build.v b/src/console/git/build.v new file mode 100644 index 00000000..fac760df --- /dev/null +++ b/src/console/git/build.v @@ -0,0 +1,34 @@ +module git + +import client +import docker +import os +import build + +// build builds every Git repo in the server's list. +fn build(conf Config, repo_id int) ? { + c := client.new(conf.address, conf.api_key) + repo := c.get_git_repo(repo_id)? + + build_arch := os.uname().machine + + println('Creating base image...') + image_id := build.create_build_image(conf.base_image)? + + println('Running build...') + res := build.build_repo(conf.address, conf.api_key, image_id, repo)? + + println('Removing build image...') + + mut dd := docker.new_conn()? + + defer { + dd.close() or {} + } + + dd.remove_image(image_id)? + + println('Uploading logs to Vieter...') + c.add_build_log(repo.id, res.start_time, res.end_time, build_arch, res.exit_code, + res.logs)? +} diff --git a/src/console/git/git.v b/src/console/git/git.v index db9dec59..06d5f80c 100644 --- a/src/console/git/git.v +++ b/src/console/git/git.v @@ -7,8 +7,9 @@ import client import console struct Config { - address string [required] - api_key string [required] + address string [required] + api_key string [required] + base_image string = 'archlinux:base-devel' } // cmd returns the cli submodule that handles the repos API interaction @@ -112,6 +113,18 @@ pub fn cmd() cli.Command { patch(conf, cmd.args[0], params)? } }, + cli.Command{ + name: 'build' + required_args: 1 + usage: 'id' + description: 'Build the repo with the given id & publish it.' + execute: fn (cmd cli.Command) ? { + config_file := cmd.flags.get_string('config-file')? + conf := env.load(config_file)? + + build(conf, cmd.args[0].int())? + } + }, ] } } diff --git a/src/main.v b/src/main.v index db6d5ef9..dbfac09e 100644 --- a/src/main.v +++ b/src/main.v @@ -3,7 +3,6 @@ module main import os import server import cli -import build import console.git import console.logs import cron @@ -25,7 +24,6 @@ fn main() { ] commands: [ server.cmd(), - build.cmd(), git.cmd(), cron.cmd(), logs.cmd(), From 67c4d199218b3403825fa956b37ecaba5065e560 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Mon, 16 May 2022 17:17:42 +0200 Subject: [PATCH 42/43] chore: removed healthcheck & unused cron stuff from Dockerfile --- Dockerfile | 9 +-------- docs/content/api.md | 5 +++++ 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/Dockerfile b/Dockerfile index 2ba61817..5997adca 100644 --- a/Dockerfile +++ b/Dockerfile @@ -36,15 +36,8 @@ ENV PATH=/bin \ COPY --from=builder /app/dumb-init /app/vieter /bin/ -HEALTHCHECK --interval=30s \ - --timeout=3s \ - --start-period=5s \ - CMD /bin/wget --spider http://localhost:8000/health || exit 1 - RUN mkdir /data && \ - chown -R www-data:www-data /data && \ - mkdir -p '/var/spool/cron/crontabs' && \ - echo '0 3 * * * /bin/vieter build' | crontab - + chown -R www-data:www-data /data WORKDIR /data diff --git a/docs/content/api.md b/docs/content/api.md index 7c395eb2..0fbb694f 100644 --- a/docs/content/api.md +++ b/docs/content/api.md @@ -56,6 +56,11 @@ Vieter only supports uploading archives compressed using either gzip, zstd or xz at the moment. {{< /hint >}} +### `GET /health` + +This endpoint's only use is to be used with healthchecks. It returns a JSON +response with the message "Healthy.". + ## API All API routes require the API key to provided using the `X-Api-Key` header. From 0de5ffb45d2734b95c3c11dd25d2bfdf6dd2bf0e Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Mon, 16 May 2022 17:34:51 +0200 Subject: [PATCH 43/43] chore: bumped versions --- CHANGELOG.md | 2 ++ PKGBUILD | 2 +- src/main.v | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c203938..7dc0a009 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://git.rustybever.be/vieter/vieter/src/branch/dev) +## [0.3.0-alpha.2](https://git.rustybever.be/vieter/vieter/src/tag/0.3.0-alpha.2) + ### Added * Web API for adding & querying build logs diff --git a/PKGBUILD b/PKGBUILD index 83ab8961..49fcf548 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -3,7 +3,7 @@ pkgbase='vieter' pkgname='vieter' -pkgver='0.3.0_alpha.1' +pkgver='0.3.0_alpha.2' pkgrel=1 depends=('glibc' 'openssl' 'libarchive' 'sqlite') makedepends=('git' 'vieter-v') diff --git a/src/main.v b/src/main.v index dbfac09e..6df45dc3 100644 --- a/src/main.v +++ b/src/main.v @@ -11,7 +11,7 @@ fn main() { mut app := cli.Command{ name: 'vieter' description: 'Vieter is a lightweight implementation of an Arch repository server.' - version: '0.3.0-alpha.1' + version: '0.3.0-alpha.2' flags: [ cli.Flag{ flag: cli.FlagType.string