From 1d434db166918a19fee18c7d5e66c9c87e2ea9dd Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Mon, 21 Feb 2022 20:40:13 +0100 Subject: [PATCH 1/7] Wrote a proper env file system --- CHANGELOG.md | 3 ++ Makefile | 19 +++++++----- src/auth.v | 2 +- src/build.v | 13 ++++----- src/env.v | 81 ++++++++++++++++++++++++++++++++++++++++++++++++++++ src/main.v | 24 ++-------------- src/routes.v | 4 +-- src/server.v | 38 +++++++++++++----------- 8 files changed, 128 insertions(+), 56 deletions(-) create mode 100644 src/env.v diff --git a/CHANGELOG.md b/CHANGELOG.md index 1166377..238fd00 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Packages are always rebuilt, even if they haven't changed * Hardcoded planning of builds * Builds are sequential +* Better environment variable support + * Each env var can now be provided from a file by appending it with `_FILE` + & passing the path to the file as value ## Fixed diff --git a/Makefile b/Makefile index d69e48e..94b079b 100644 --- a/Makefile +++ b/Makefile @@ -34,16 +34,21 @@ c: # Run the server in the default 'data' directory .PHONY: run run: vieter - API_KEY=test DOWNLOAD_DIR=data/downloads REPO_DIR=data/repo PKG_DIR=data/pkgs LOG_LEVEL=DEBUG ./vieter server + VIETER_API_KEY=test \ + VIETER_DOWNLOAD_DIR=data/downloads \ + VIETER_REPO_DIR=data/repo \ + VIETER_PKG_DIR=data/pkgs \ + VIETER_LOG_LEVEL=DEBUG \ + ./vieter server .PHONY: run-prod run-prod: prod - API_KEY=test DOWNLOAD_DIR=data/downloads REPO_DIR=data/repo PKG_DIR=data/pkgs LOG_LEVEL=DEBUG ./pvieter - -# Same as run, but restart when the source code changes -.PHONY: watch -watch: - API_KEY=test DOWNLOAD_DIR=data/downloads REPO_DIR=data/repo PKG_DIR=data/pkgs LOG_LEVEL=DEBUG $(V) watch run vieter + VIETER_API_KEY=test \ + VIETER_DOWNLOAD_DIR=data/downloads \ + VIETER_REPO_DIR=data/repo \ + VIETER_PKG_DIR=data/pkgs \ + VIETER_LOG_LEVEL=DEBUG \ + ./pvieter server # =====OTHER===== .PHONY: lint diff --git a/src/auth.v b/src/auth.v index eab63c8..942bdda 100644 --- a/src/auth.v +++ b/src/auth.v @@ -7,5 +7,5 @@ fn (mut app App) is_authorized() bool { return false } - return x_header.trim_space() == app.api_key + return x_header.trim_space() == app.conf.api_key } diff --git a/src/build.v b/src/build.v index 00cf9ed..0acca8f 100644 --- a/src/build.v +++ b/src/build.v @@ -7,16 +7,15 @@ import time import os import json import git +import env const container_build_dir = '/build' -fn build(key string, repo_dir string) ? { - server_url := os.getenv_opt('VIETER_ADDRESS') or { - exit_with_message(1, 'No Vieter server address was provided.') - } +fn build() ? { + conf := env.load() ? // Read in the repos from a json file - filename := os.join_path_single(repo_dir, 'repos.json') + filename := os.join_path_single(conf.repo_dir, 'repos.json') txt := os.read_file(filename) ? repos := json.decode([]git.GitRepo, txt) ? @@ -48,7 +47,7 @@ fn build(key string, repo_dir string) ? { uuids << uuid commands << "su builder -c 'git clone --single-branch --depth 1 --branch $repo.branch $repo.url /build/$uuid'" - commands << 'su builder -c \'cd /build/$uuid && makepkg -s --noconfirm --needed && for pkg in \$(ls -1 *.pkg*); do curl -XPOST -T "\${pkg}" -H "X-API-KEY: \$API_KEY" $server_url/publish; done\'' + commands << 'su builder -c \'cd /build/$uuid && makepkg -s --noconfirm --needed && for pkg in \$(ls -1 *.pkg*); do curl -XPOST -T "\${pkg}" -H "X-API-KEY: \$API_KEY" $conf.address/publish; done\'' } // We convert the list of commands into a base64 string, which then gets @@ -57,7 +56,7 @@ fn build(key string, repo_dir string) ? { c := docker.NewContainer{ image: 'archlinux:latest' - env: ['BUILD_SCRIPT=$cmds_str', 'API_KEY=$key'] + env: ['BUILD_SCRIPT=$cmds_str', 'API_KEY=$conf.api_key'] entrypoint: ['/bin/sh', '-c'] cmd: ['echo \$BUILD_SCRIPT | base64 -d | /bin/sh -e'] } diff --git a/src/env.v b/src/env.v new file mode 100644 index 0000000..804d37c --- /dev/null +++ b/src/env.v @@ -0,0 +1,81 @@ +module env + +import os + +// 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' + +pub struct ServerConfig { +pub: + log_level string [default: WARN] + log_file string [default: 'vieter.log'] + pkg_dir string + download_dir string + api_key string + repo_dir string +} + +pub struct BuildConfig { +pub: + api_key string + repo_dir string + address string +} + +fn get_env_var(field_name string) ?string { + env_var_name := '${prefix}${field_name.to_upper()}' + env_file_name := '${prefix}${field_name.to_upper()}${file_suffix}' + env_var := os.getenv(env_var_name) + env_file := os.getenv(env_file_name) + + // If both aren't set, we report them missing + if env_var == '' && env_file == '' { + return error('Either $env_var_name or $env_file_name is required.') + } + + // If they're both set, we report a conflict + if env_var != '' && env_file != '' { + return error('Only one of $env_var_name or $env_file_name can be defined.') + } + + // If it's the env var itself, we return it + if env_var != '' { + return env_var + } + + // Otherwise, we process the file + return os.read_file(env_file) or { + error('Failed to read file defined in $env_file_name: ${err.msg}.') + } +} + +// load attempts to create the given type from environment variables. +pub fn load() ?T { + res := T{} + + $for field in T.fields { + res.$(field.name) = get_env_var(field.name) or { + // We use the default instead, if it's present + mut default := '' + + for attr in field.attrs { + if attr.starts_with('default: ') { + default = attr[9..] + break + } + } + + if default == '' { + return err + } + + default + } + } + + return res +} diff --git a/src/main.v b/src/main.v index cc66b59..2472ac0 100644 --- a/src/main.v +++ b/src/main.v @@ -1,22 +1,7 @@ module main -import web import os import io -import repo - -const port = 8000 - -const buf_size = 1_000_000 - -struct App { - web.Context -pub: - api_key string [required; web_global] - dl_dir string [required; web_global] -pub mut: - repo repo.Repo [required; web_global] -} [noreturn] fn exit_with_message(code int, msg string) { @@ -51,18 +36,13 @@ fn reader_to_file(mut reader io.BufferedReader, length int, path string) ? { } fn main() { - key := os.getenv_opt('API_KEY') or { exit_with_message(1, 'No API key was provided.') } - repo_dir := os.getenv_opt('REPO_DIR') or { - exit_with_message(1, 'No repo directory was configured.') - } - if os.args.len == 1 { exit_with_message(1, 'No action provided.') } match os.args[1] { - 'server' { server(key, repo_dir) } - 'build' { build(key, repo_dir) ? } + 'server' { server() ? } + 'build' { build() ? } else { exit_with_message(1, 'Unknown action: ${os.args[1]}') } } } diff --git a/src/routes.v b/src/routes.v index 8b7ddeb..b152a9f 100644 --- a/src/routes.v +++ b/src/routes.v @@ -58,10 +58,10 @@ fn (mut app App) put_package() web.Result { if length := app.req.header.get(.content_length) { // Generate a random filename for the temp file - pkg_path = os.join_path_single(app.dl_dir, rand.uuid_v4()) + pkg_path = os.join_path_single(app.conf.download_dir, rand.uuid_v4()) for os.exists(pkg_path) { - pkg_path = os.join_path_single(app.dl_dir, rand.uuid_v4()) + pkg_path = os.join_path_single(app.conf.download_dir, rand.uuid_v4()) } app.ldebug("Uploading $length bytes (${pretty_bytes(length.int())}) to '$pkg_path'.") diff --git a/src/server.v b/src/server.v index e0f22ec..d5c5ab2 100644 --- a/src/server.v +++ b/src/server.v @@ -4,20 +4,33 @@ import web import os import log import repo +import env + +const port = 8000 + +const buf_size = 1_000_000 + +struct App { + web.Context +pub: + conf env.ServerConfig [required: web_global] +pub mut: + repo repo.Repo [required; web_global] +} + +fn server() ? { + conf := env.load() ? -fn server(key string, repo_dir string) { // Configure logger - log_level_str := os.getenv_opt('LOG_LEVEL') or { 'WARN' } - log_level := log.level_from_tag(log_level_str) or { + log_level := log.level_from_tag(conf.log_level) or { exit_with_message(1, 'Invalid log level. The allowed values are FATAL, ERROR, WARN, INFO & DEBUG.') } - log_file := os.getenv_opt('LOG_FILE') or { 'vieter.log' } mut logger := log.Log{ level: log_level } - logger.set_full_logpath(log_file) + logger.set_full_logpath(conf.log_file) logger.log_to_console_too() defer { @@ -26,26 +39,17 @@ fn server(key string, repo_dir string) { logger.close() } - // Configure web server - pkg_dir := os.getenv_opt('PKG_DIR') or { - exit_with_message(1, 'No package directory was configured.') - } - dl_dir := os.getenv_opt('DOWNLOAD_DIR') or { - exit_with_message(1, 'No download directory was configured.') - } - // This also creates the directories if needed - repo := repo.new(repo_dir, pkg_dir) or { + repo := repo.new(conf.repo_dir, conf.pkg_dir) or { logger.error(err.msg) exit(1) } - os.mkdir_all(dl_dir) or { exit_with_message(1, 'Failed to create download directory.') } + os.mkdir_all(conf.download_dir) or { exit_with_message(1, 'Failed to create download directory.') } web.run(&App{ logger: logger - api_key: key - dl_dir: dl_dir + conf: conf repo: repo }, port) } From 92ad0c51eb5ea0ef75a90bcf46d6d00b94e44007 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Mon, 21 Feb 2022 20:51:41 +0100 Subject: [PATCH 2/7] Split off server into own module --- src/env.v | 19 +++++++++--------- src/main.v | 41 +++++---------------------------------- src/{ => server}/auth.v | 2 +- src/{ => server}/routes.v | 7 ++++--- src/{ => server}/server.v | 15 +++++++------- src/util.v | 35 +++++++++++++++++++++++++++++++++ 6 files changed, 62 insertions(+), 57 deletions(-) rename src/{ => server}/auth.v (94%) rename src/{ => server}/routes.v (95%) rename src/{ => server}/server.v (72%) diff --git a/src/env.v b/src/env.v index 804d37c..2cbcf63 100644 --- a/src/env.v +++ b/src/env.v @@ -11,24 +11,24 @@ const file_suffix = '_FILE' pub struct ServerConfig { pub: - log_level string [default: WARN] - log_file string [default: 'vieter.log'] - pkg_dir string + log_level string [default: WARN] + log_file string [default: 'vieter.log'] + pkg_dir string download_dir string - api_key string - repo_dir string + api_key string + repo_dir string } pub struct BuildConfig { pub: - api_key string + api_key string repo_dir string - address string + address string } fn get_env_var(field_name string) ?string { - env_var_name := '${prefix}${field_name.to_upper()}' - env_file_name := '${prefix}${field_name.to_upper()}${file_suffix}' + env_var_name := '$env.prefix$field_name.to_upper()' + env_file_name := '$env.prefix$field_name.to_upper()$env.file_suffix' env_var := os.getenv(env_var_name) env_file := os.getenv(env_file_name) @@ -76,6 +76,5 @@ pub fn load() ?T { default } } - return res } diff --git a/src/main.v b/src/main.v index 2472ac0..156f0a3 100644 --- a/src/main.v +++ b/src/main.v @@ -1,48 +1,17 @@ module main import os -import io - -[noreturn] -fn exit_with_message(code int, msg string) { - eprintln(msg) - exit(code) -} - -fn reader_to_file(mut reader io.BufferedReader, length int, path string) ? { - mut file := os.create(path) ? - defer { - file.close() - } - - mut buf := []byte{len: 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 } - - to_write = to_write - bytes_written - } - } -} +import server +import util fn main() { if os.args.len == 1 { - exit_with_message(1, 'No action provided.') + util.exit_with_message(1, 'No action provided.') } match os.args[1] { - 'server' { server() ? } + 'server' { server.server() ? } 'build' { build() ? } - else { exit_with_message(1, 'Unknown action: ${os.args[1]}') } + else { util.exit_with_message(1, 'Unknown action: ${os.args[1]}') } } } diff --git a/src/auth.v b/src/server/auth.v similarity index 94% rename from src/auth.v rename to src/server/auth.v index 942bdda..8bc9d55 100644 --- a/src/auth.v +++ b/src/server/auth.v @@ -1,4 +1,4 @@ -module main +module server import net.http diff --git a/src/routes.v b/src/server/routes.v similarity index 95% rename from src/routes.v rename to src/server/routes.v index b152a9f..872894e 100644 --- a/src/routes.v +++ b/src/server/routes.v @@ -1,10 +1,11 @@ -module main +module server import web import os import repo import time import rand +import util const prefixes = ['B', 'KB', 'MB', 'GB'] @@ -18,7 +19,7 @@ fn pretty_bytes(bytes int) string { n /= 1024 } - return '${n:.2}${prefixes[i]}' + return '${n:.2}${server.prefixes[i]}' } fn is_pkg_name(s string) bool { @@ -69,7 +70,7 @@ fn (mut app App) put_package() web.Result { // This is used to time how long it takes to upload a file mut sw := time.new_stopwatch(time.StopWatchOptions{ auto_start: true }) - reader_to_file(mut app.reader, length.int(), pkg_path) or { + util.reader_to_file(mut app.reader, length.int(), pkg_path) or { app.lwarn("Failed to upload '$pkg_path'") return app.text('Failed to upload file.') diff --git a/src/server.v b/src/server/server.v similarity index 72% rename from src/server.v rename to src/server/server.v index d5c5ab2..3b16c63 100644 --- a/src/server.v +++ b/src/server/server.v @@ -1,15 +1,14 @@ -module main +module server import web import os import log import repo import env +import util const port = 8000 -const buf_size = 1_000_000 - struct App { web.Context pub: @@ -18,12 +17,12 @@ pub mut: repo repo.Repo [required; web_global] } -fn server() ? { +pub fn server() ? { conf := env.load() ? // Configure logger log_level := log.level_from_tag(conf.log_level) or { - exit_with_message(1, 'Invalid log level. The allowed values are FATAL, ERROR, WARN, INFO & DEBUG.') + util.exit_with_message(1, 'Invalid log level. The allowed values are FATAL, ERROR, WARN, INFO & DEBUG.') } mut logger := log.Log{ @@ -45,11 +44,13 @@ fn server() ? { exit(1) } - os.mkdir_all(conf.download_dir) or { exit_with_message(1, 'Failed to create download directory.') } + os.mkdir_all(conf.download_dir) or { + util.exit_with_message(1, 'Failed to create download directory.') + } web.run(&App{ logger: logger conf: conf repo: repo - }, port) + }, server.port) } diff --git a/src/util.v b/src/util.v index f81a256..d0fa984 100644 --- a/src/util.v +++ b/src/util.v @@ -1,9 +1,44 @@ module util import os +import io import crypto.md5 import crypto.sha256 +const reader_buf_size = 1_000_000 + +[noreturn] +pub fn exit_with_message(code int, msg string) { + eprintln(msg) + exit(code) +} + +pub fn reader_to_file(mut reader io.BufferedReader, length int, path string) ? { + mut file := os.create(path) ? + defer { + file.close() + } + + mut buf := []byte{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 } + + 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) { From e13252d36826004f298a96c2ed4bb8e9df4ea61c Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Mon, 21 Feb 2022 22:22:36 +0100 Subject: [PATCH 3/7] Initial part of repos API (SEGFAULTS) [CI SKIP] --- .gitignore | 3 ++ Makefile | 13 +++++- src/build.v | 4 +- src/env.v | 6 ++- src/git.v | 7 ---- src/repo/repo.v | 9 +--- src/server/git.v | 100 ++++++++++++++++++++++++++++++++++++++++++++ src/server/routes.v | 21 +--------- src/server/server.v | 4 +- src/util.v | 22 ++++++++++ 10 files changed, 150 insertions(+), 39 deletions(-) delete mode 100644 src/git.v create mode 100644 src/server/git.v diff --git a/.gitignore b/.gitignore index ca2e2f8..3a6b11b 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,6 @@ test/ # V compiler directory v/ + +# gdb log file +gdb.txt diff --git a/Makefile b/Makefile index 94b079b..b1b91f2 100644 --- a/Makefile +++ b/Makefile @@ -16,7 +16,17 @@ vieter: $(SOURCES) .PHONY: debug debug: dvieter dvieter: $(SOURCES) - $(V) -keepc -cg -cc gcc -o dvieter $(SRC_DIR) + $(V_PATH) -showcc -keepc -cg -o dvieter $(SRC_DIR) + +.PHONY: gdb +gdb: dvieter + VIETER_API_KEY=test \ + VIETER_DOWNLOAD_DIR=data/downloads \ + VIETER_REPO_DIR=data/repo \ + VIETER_PKG_DIR=data/pkgs \ + VIETER_LOG_LEVEL=DEBUG \ + VIETER_REPOS_FILE=data/repos.json \ + gdb --args ./dvieter # Optimised production build .PHONY: prod @@ -39,6 +49,7 @@ run: vieter VIETER_REPO_DIR=data/repo \ VIETER_PKG_DIR=data/pkgs \ VIETER_LOG_LEVEL=DEBUG \ + VIETER_REPOS_FILE=data/repos.json \ ./vieter server .PHONY: run-prod diff --git a/src/build.v b/src/build.v index 0acca8f..8d6af14 100644 --- a/src/build.v +++ b/src/build.v @@ -6,7 +6,7 @@ import rand import time import os import json -import git +import server import env const container_build_dir = '/build' @@ -17,7 +17,7 @@ fn build() ? { // Read in the repos from a json file filename := os.join_path_single(conf.repo_dir, 'repos.json') txt := os.read_file(filename) ? - repos := json.decode([]git.GitRepo, txt) ? + repos := json.decode([]server.GitRepo, txt) ? mut commands := [ // Update repos & install required packages diff --git a/src/env.v b/src/env.v index 2cbcf63..1e601bd 100644 --- a/src/env.v +++ b/src/env.v @@ -17,6 +17,7 @@ pub: download_dir string api_key string repo_dir string + repos_file string } pub struct BuildConfig { @@ -42,7 +43,10 @@ fn get_env_var(field_name string) ?string { return error('Only one of $env_var_name or $env_file_name can be defined.') } - // If it's the env var itself, we return it + // If it's the env var itself, we return it. + // I'm pretty sure this also prevents variable ending in _FILE (e.g. + // VIETER_LOG_FILE) from being mistakingely read as an _FILE suffixed env + // var. if env_var != '' { return env_var } diff --git a/src/git.v b/src/git.v deleted file mode 100644 index 76d80d7..0000000 --- a/src/git.v +++ /dev/null @@ -1,7 +0,0 @@ -module git - -pub struct GitRepo { -pub: - url string [required] - branch string [required] -} diff --git a/src/repo/repo.v b/src/repo/repo.v index 1bf2d0c..f1419ac 100644 --- a/src/repo/repo.v +++ b/src/repo/repo.v @@ -2,17 +2,12 @@ module repo import os import package - -// Dummy struct to work around the fact that you can only share structs, maps & -// arrays -pub struct Dummy { - x int -} +import util // This struct manages a single repository. pub struct Repo { mut: - mutex shared Dummy + mutex shared util.Dummy pub: // Where to store repository files repo_dir string [required] diff --git a/src/server/git.v b/src/server/git.v new file mode 100644 index 0000000..6a1d929 --- /dev/null +++ b/src/server/git.v @@ -0,0 +1,100 @@ +module server + +import web +import os +import json + +const repos_file = 'repos.json' + +pub struct GitRepo { +pub: + url string [required] + branch string [required] +} + +fn read_repos(path string) ?[]GitRepo { + if !os.exists(path) { + mut f := os.create(path) ? + + defer { + f.close() + } + + f.write_string('{}') ? + + return [] + } + + content := os.read_file(path) ? + return json.decode([]GitRepo, content) +} + +fn write_repos(path string, repos []GitRepo) ? { + mut f := os.create(path) ? + + defer { + f.close() + } + + dump(repos) + value := json.encode(repos) + f.write_string(value) ? +} + +['/api/repos'; get] +pub fn (mut app App) get_repos() web.Result { + if !app.is_authorized() { + return app.text('Unauthorized.') + } + + repos := rlock app.git_mutex { + read_repos(app.conf.repos_file) or { + app.lerror('Failed to read repos file.') + + return app.server_error(500) + } + } + + return app.json(repos) +} + +['/api/repos'; post] +pub fn (mut app App) post_repo() web.Result { + if !app.is_authorized() { + return app.text('Unauthorized.') + } + + if !('url' in app.query && 'branch' in app.query) { + return app.server_error(400) + } + + new_repo := GitRepo{ + url: app.query['url'] + branch: app.query['branch'] + } + + mut repos := rlock app.git_mutex { + read_repos(app.conf.repos_file) or { + app.lerror('Failed to read repos file.') + + return app.server_error(500) + } + } + + // We need to check for duplicates + for r in repos { + if r == new_repo { + return app.text('Duplicate repository.') + } + } + + repos << new_repo + + lock app.git_mutex { + write_repos(app.conf.repos_file, repos) or { + return app.server_error(500) + } + } + + return app.ok('Repo added successfully.') +} diff --git a/src/server/routes.v b/src/server/routes.v index 872894e..7a0ee38 100644 --- a/src/server/routes.v +++ b/src/server/routes.v @@ -7,25 +7,6 @@ import time import rand import util -const prefixes = ['B', 'KB', 'MB', 'GB'] - -// pretty_bytes converts a byte count to human-readable version -fn pretty_bytes(bytes int) string { - mut i := 0 - mut n := f32(bytes) - - for n >= 1024 { - i++ - n /= 1024 - } - - return '${n:.2}${server.prefixes[i]}' -} - -fn is_pkg_name(s string) bool { - return s.contains('.pkg') -} - // healthcheck just returns a string, but can be used to quickly check if the // server is still responsive. ['/health'; get] @@ -65,7 +46,7 @@ fn (mut app App) put_package() web.Result { pkg_path = os.join_path_single(app.conf.download_dir, rand.uuid_v4()) } - app.ldebug("Uploading $length bytes (${pretty_bytes(length.int())}) to '$pkg_path'.") + app.ldebug("Uploading $length bytes (${util.pretty_bytes(length.int())}) to '$pkg_path'.") // This is used to time how long it takes to upload a file mut sw := time.new_stopwatch(time.StopWatchOptions{ auto_start: true }) diff --git a/src/server/server.v b/src/server/server.v index 3b16c63..ebb9f3b 100644 --- a/src/server/server.v +++ b/src/server/server.v @@ -12,9 +12,11 @@ const port = 8000 struct App { web.Context pub: - conf env.ServerConfig [required: web_global] + conf env.ServerConfig [required; web_global] pub mut: repo repo.Repo [required; web_global] + // This is used to claim the file lock on the repos file + git_mutex shared util.Dummy } pub fn server() ? { diff --git a/src/util.v b/src/util.v index d0fa984..65d5294 100644 --- a/src/util.v +++ b/src/util.v @@ -7,6 +7,14 @@ import crypto.sha256 const reader_buf_size = 1_000_000 +const prefixes = ['B', 'KB', 'MB', 'GB'] + +// Dummy struct to work around the fact that you can only share structs, maps & +// arrays +pub struct Dummy { + x int +} + [noreturn] pub fn exit_with_message(code int, msg string) { eprintln(msg) @@ -67,3 +75,17 @@ pub fn hash_file(path &string) ?(string, string) { return md5sum.checksum().hex(), sha256sum.checksum().hex() } + +// pretty_bytes converts a byte count to human-readable version +pub fn pretty_bytes(bytes int) string { + mut i := 0 + mut n := f32(bytes) + + for n >= 1024 { + i++ + n /= 1024 + } + + return '${n:.2}${util.prefixes[i]}' +} + From 398758a72790f8ebdf9e1b5276e8a0ed8d5b57a7 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Mon, 21 Feb 2022 22:32:54 +0100 Subject: [PATCH 4/7] Fixed segfault All together now: thank you spytheman --- src/env.v | 2 +- src/server/git.v | 10 ++++------ src/util.v | 1 - 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/env.v b/src/env.v index 1e601bd..26919f8 100644 --- a/src/env.v +++ b/src/env.v @@ -17,7 +17,7 @@ pub: download_dir string api_key string repo_dir string - repos_file string + repos_file string } pub struct BuildConfig { diff --git a/src/server/git.v b/src/server/git.v index 6a1d929..8c569e6 100644 --- a/src/server/git.v +++ b/src/server/git.v @@ -20,13 +20,14 @@ fn read_repos(path string) ?[]GitRepo { f.close() } - f.write_string('{}') ? + f.write_string('[]') ? return [] } content := os.read_file(path) ? - return json.decode([]GitRepo, content) + res := json.decode([]GitRepo, content) ? + return res } fn write_repos(path string, repos []GitRepo) ? { @@ -36,7 +37,6 @@ fn write_repos(path string, repos []GitRepo) ? { f.close() } - dump(repos) value := json.encode(repos) f.write_string(value) ? } @@ -91,9 +91,7 @@ pub fn (mut app App) post_repo() web.Result { repos << new_repo lock app.git_mutex { - write_repos(app.conf.repos_file, repos) or { - return app.server_error(500) - } + write_repos(app.conf.repos_file, repos) or { return app.server_error(500) } } return app.ok('Repo added successfully.') diff --git a/src/util.v b/src/util.v index 65d5294..24d33fc 100644 --- a/src/util.v +++ b/src/util.v @@ -88,4 +88,3 @@ pub fn pretty_bytes(bytes int) string { return '${n:.2}${util.prefixes[i]}' } - From fe98112f796d39bb14175386f6e0a0b29e94500b Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Mon, 21 Feb 2022 22:40:59 +0100 Subject: [PATCH 5/7] Added repos delete route --- src/server/git.v | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/src/server/git.v b/src/server/git.v index 8c569e6..d506c7e 100644 --- a/src/server/git.v +++ b/src/server/git.v @@ -96,3 +96,34 @@ pub fn (mut app App) post_repo() web.Result { return app.ok('Repo added successfully.') } + +['/api/repos'; delete] +pub fn (mut app App) delete_repo() web.Result { + if !app.is_authorized() { + return app.text('Unauthorized.') + } + + if !('url' in app.query && 'branch' in app.query) { + return app.server_error(400) + } + + repo_to_remove := GitRepo{ + url: app.query['url'] + branch: app.query['branch'] + } + + mut repos := rlock app.git_mutex { + read_repos(app.conf.repos_file) or { + app.lerror('Failed to read repos file.') + + return app.server_error(500) + } + } + filtered := repos.filter(it != repo_to_remove) + + lock app.git_mutex { + write_repos(app.conf.repos_file, filtered) or { return app.server_error(500) } + } + + return app.ok('Repo removed successfully.') +} From 6194a3f4086bf97009d6c83c5eb687032bb9e924 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Mon, 21 Feb 2022 22:58:05 +0100 Subject: [PATCH 6/7] Builder now gets list of repos from server --- CHANGELOG.md | 1 + src/build.v | 11 +++++++---- src/env.v | 1 - 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 238fd00..edae68b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Better environment variable support * Each env var can now be provided from a file by appending it with `_FILE` & passing the path to the file as value +* API for managing Git repositories to build ## Fixed diff --git a/src/build.v b/src/build.v index 8d6af14..ab72a59 100644 --- a/src/build.v +++ b/src/build.v @@ -8,16 +8,19 @@ import os import json import server import env +import net.http const container_build_dir = '/build' fn build() ? { conf := env.load() ? - // Read in the repos from a json file - filename := os.join_path_single(conf.repo_dir, 'repos.json') - txt := os.read_file(filename) ? - repos := json.decode([]server.GitRepo, txt) ? + // We get the repos list from the Vieter instance + mut req := http.new_request(http.Method.get, '${conf.address}/api/repos', '') ? + req.add_custom_header('X-Api-Key', conf.api_key) ? + + res := req.do() ? + repos := json.decode([]server.GitRepo, res.text) ? mut commands := [ // Update repos & install required packages diff --git a/src/env.v b/src/env.v index 26919f8..8b92d8d 100644 --- a/src/env.v +++ b/src/env.v @@ -23,7 +23,6 @@ pub: pub struct BuildConfig { pub: api_key string - repo_dir string address string } From 27f59c6664e7b4fbb15e3b49735c55417f9f7605 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Tue, 22 Feb 2022 08:14:20 +0100 Subject: [PATCH 7/7] Updated CI Dockerfile; fixed formatting & vet --- Dockerfile.ci | 7 ++++--- Makefile | 3 +++ src/build.v | 3 +-- src/env.v | 9 ++++++--- src/server/git.v | 6 +++--- src/server/server.v | 1 + src/util.v | 3 +++ 7 files changed, 21 insertions(+), 11 deletions(-) diff --git a/Dockerfile.ci b/Dockerfile.ci index e0dd5da..a062d95 100644 --- a/Dockerfile.ci +++ b/Dockerfile.ci @@ -24,9 +24,10 @@ RUN curl --fail \ FROM busybox:1.35.0 ENV PATH=/bin \ - REPO_DIR=/data/repo \ - PKG_DIR=/data/pkgs \ - DOWNLOAD_DIR=/data/downloads + VIETER_REPO_DIR=/data/repo \ + VIETER_PKG_DIR=/data/pkgs \ + VIETER_DOWNLOAD_DIR=/data/downloads \ + VIETER_REPOS_FILE=/data/repos.json COPY --from=builder /app/dumb-init /app/vieter /bin/ diff --git a/Makefile b/Makefile index b1b91f2..062a47f 100644 --- a/Makefile +++ b/Makefile @@ -13,11 +13,14 @@ vieter: $(SOURCES) $(V) -g -o vieter $(SRC_DIR) # Debug build using gcc +# The debug build can't use the boehm garbage collector, as that is +# multi-threaded and causes issues when running vieter inside gdb. .PHONY: debug debug: dvieter dvieter: $(SOURCES) $(V_PATH) -showcc -keepc -cg -o dvieter $(SRC_DIR) +# Run the debug build inside gdb .PHONY: gdb gdb: dvieter VIETER_API_KEY=test \ diff --git a/src/build.v b/src/build.v index ab72a59..fc1fe6f 100644 --- a/src/build.v +++ b/src/build.v @@ -4,7 +4,6 @@ import docker import encoding.base64 import rand import time -import os import json import server import env @@ -16,7 +15,7 @@ fn build() ? { conf := env.load() ? // We get the repos list from the Vieter instance - mut req := http.new_request(http.Method.get, '${conf.address}/api/repos', '') ? + mut req := http.new_request(http.Method.get, '$conf.address/api/repos', '') ? req.add_custom_header('X-Api-Key', conf.api_key) ? res := req.do() ? diff --git a/src/env.v b/src/env.v index 8b92d8d..bd544cf 100644 --- a/src/env.v +++ b/src/env.v @@ -22,8 +22,8 @@ pub: pub struct BuildConfig { pub: - api_key string - address string + api_key string + address string } fn get_env_var(field_name string) ?string { @@ -56,7 +56,10 @@ fn get_env_var(field_name string) ?string { } } -// load attempts to create the given type from environment variables. +// load attempts to create the given type from environment variables. For +// each field, the corresponding env var is its name in uppercase prepended +// with the hardcoded prefix. If this one isn't present, it looks for the env +// var with the file_suffix suffix. pub fn load() ?T { res := T{} diff --git a/src/server/git.v b/src/server/git.v index d506c7e..0147d87 100644 --- a/src/server/git.v +++ b/src/server/git.v @@ -42,7 +42,7 @@ fn write_repos(path string, repos []GitRepo) ? { } ['/api/repos'; get] -pub fn (mut app App) get_repos() web.Result { +fn (mut app App) get_repos() web.Result { if !app.is_authorized() { return app.text('Unauthorized.') } @@ -59,7 +59,7 @@ pub fn (mut app App) get_repos() web.Result { } ['/api/repos'; post] -pub fn (mut app App) post_repo() web.Result { +fn (mut app App) post_repo() web.Result { if !app.is_authorized() { return app.text('Unauthorized.') } @@ -98,7 +98,7 @@ pub fn (mut app App) post_repo() web.Result { } ['/api/repos'; delete] -pub fn (mut app App) delete_repo() web.Result { +fn (mut app App) delete_repo() web.Result { if !app.is_authorized() { return app.text('Unauthorized.') } diff --git a/src/server/server.v b/src/server/server.v index ebb9f3b..4b31b99 100644 --- a/src/server/server.v +++ b/src/server/server.v @@ -19,6 +19,7 @@ pub mut: git_mutex shared util.Dummy } +// server starts the web server & starts listening for requests pub fn server() ? { conf := env.load() ? diff --git a/src/util.v b/src/util.v index 24d33fc..49c9d22 100644 --- a/src/util.v +++ b/src/util.v @@ -15,12 +15,15 @@ pub struct Dummy { x int } +// exit_with_message exits the program with a given status code after having +// first printed a specific message to STDERR [noreturn] pub fn exit_with_message(code int, msg string) { eprintln(msg) exit(code) } +// 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 {