diff --git a/CHANGELOG.md b/CHANGELOG.md index 11663779..238fd004 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 d69e48e8..94b079bc 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 eab63c85..942bddad 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 00cf9ed7..0acca8fc 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 00000000..804d37c8 --- /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 cc66b59a..2472ac0b 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 8b7ddebe..b152a9f3 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 e0f22ecf..d5c5ab2c 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) }