From 82558b62018bf602cc43818c8a7257f17cf6443d Mon Sep 17 00:00:00 2001 From: Michal Cichra Date: Wed, 25 Oct 2017 16:53:34 +0200 Subject: [PATCH 1/2] template nginx configuration with liquid * we need to use some templating to customize nginx directives not accessible from Lua code (like buffer sizes, etc.) * using liquid templating language * introduce `bin/cli` that is a main entrypoint to: - configure paths to load local code and dependencies - execute lua cli code * `bin/apicast` is now slim perl wrapper to setup paths, because it has access to current working folder and ritch stdlib for working with filesystem and env variables * introduce concept of an environment, so each can have own config, like rails * use latest rover and lock scm dependencies https://github.com/3scale/lua-rover/pull/2 update changelog --- .circleci/config.yml | 4 +- CHANGELOG.md | 2 + Makefile | 4 +- apicast/Roverfile.lock | 43 ++++--- apicast/apicast-0.1-0.rockspec | 4 + apicast/bin/apicast | 137 ++------------------ apicast/bin/cli | 11 ++ apicast/conf/nginx.conf | 78 ----------- apicast/conf/nginx.conf.liquid | 93 ++++++++++++++ apicast/config/development.lua | 12 ++ apicast/config/production.lua | 5 + apicast/http.d/ssl.conf | 4 +- apicast/src/apicast/cli.lua | 54 ++++++++ apicast/src/apicast/cli/start.lua | 178 ++++++++++++++++++++++++++ apicast/src/apicast/configuration.lua | 55 ++++++++ apicast/src/apicast/template.lua | 99 ++++++++++++++ bin/cli | 1 + 17 files changed, 555 insertions(+), 229 deletions(-) create mode 100755 apicast/bin/cli delete mode 100644 apicast/conf/nginx.conf create mode 100644 apicast/conf/nginx.conf.liquid create mode 100644 apicast/config/development.lua create mode 100644 apicast/config/production.lua create mode 100644 apicast/src/apicast/cli.lua create mode 100644 apicast/src/apicast/cli/start.lua create mode 100644 apicast/src/apicast/configuration.lua create mode 100644 apicast/src/apicast/template.lua create mode 120000 bin/cli diff --git a/.circleci/config.yml b/.circleci/config.yml index f85ca8801..eaf46dc9b 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -7,7 +7,7 @@ jobs: environment: S2I_VERSION: "1.1.7-226afa1" DOCKER_COMPOSE_VERSION: "1.16.1" - OPENRESTY_VERSION: "1.11.2.5-1-rover" + OPENRESTY_VERSION: "1.11.2.5-1-rover2" steps: - run: apk update && apk add wget make bash curl py-pip git openssh-client - run: | @@ -37,7 +37,7 @@ jobs: - run: make prove-docker build: docker: - - image: quay.io/3scale/s2i-openresty-centos7:1.11.2.5-1-rover + - image: quay.io/3scale/s2i-openresty-centos7:1.11.2.5-1-rover2 environment: TEST_NGINX_BINARY: openresty LUA_BIN_PATH: /opt/app-root/bin diff --git a/CHANGELOG.md b/CHANGELOG.md index 636c60027..4d2d4321d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - `/status/info` endpoint to the Management API [PR #290](https://github.com/3scale/apicast/pull/290) - `/_threescale/healthz` endpoint returns a success status code, this is used for health checking in kubernetes environments [PR #285](https://github.com/3scale/apicast/pull/285) - Usage limit errors are now configurable to distinguish them from other authorization errors [PR #453](https://github.com/3scale/apicast/pull/453). +- Templating nginx configuration with liquid. [PR #449](https://github.com/3scale/apicast/pull/449) ## Changed @@ -22,6 +23,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Make OAuth tokens TTL configurable [PR #448](https://github.com/3scale/apicast/pull/448) - Detect when being executed in Test::Nginx and use default backend accordingly [PR #458](https://github.com/3scale/apicast/pull/458) - Update the s2i-openresty image to have the same path (`/opt/app-root/src`) in all images [PR #460](https://github.com/3scale/apicast/pull/460) +- Launcher scripts are now Perl + Lua instead of Shell [PR #449](https://github.com/3scale/apicast/pull/449) ### Fixed diff --git a/Makefile b/Makefile index c5d929771..3e5e95038 100644 --- a/Makefile +++ b/Makefile @@ -83,9 +83,9 @@ test-builder-image: export IMAGE_NAME = apicast-test test-builder-image: builder-image clean-containers ## Smoke test the builder image. Pass any docker image in IMAGE_NAME parameter. $(DOCKER_COMPOSE) --version @echo -e $(SEPARATOR) - $(DOCKER_COMPOSE) run --rm --user 100001 gateway openresty -c /opt/app-root/src/conf/nginx.conf -g 'error_log stderr info; pid /tmp/nginx.pid;' -t + $(DOCKER_COMPOSE) run --rm --user 100001 gateway bin/apicast --test @echo -e $(SEPARATOR) - $(DOCKER_COMPOSE) run --rm --user 100001 gateway openresty -c /opt/app-root/src/conf/nginx.conf -g 'error_log stderr info; pid /tmp/nginx.pid;' + $(DOCKER_COMPOSE) run --rm --user 100001 gateway bin/apicast --daemon @echo -e $(SEPARATOR) $(DOCKER_COMPOSE) run --rm test bash -c 'for i in {1..5}; do curl --fail http://gateway:8090/status/live && break || sleep 1; done' $(DOCKER_COMPOSE) logs gateway diff --git a/apicast/Roverfile.lock b/apicast/Roverfile.lock index e85290c4b..75c2d66d8 100644 --- a/apicast/Roverfile.lock +++ b/apicast/Roverfile.lock @@ -1,20 +1,23 @@ -busted 2.0.rc12-1 -dkjson 2.5-2 -inspect 3.1.0-1 -ldoc 1.4.6-2 -lua-resty-env 0.4.0-1 -lua-resty-http 0.10-0 -lua-resty-iputils 0.3.0-1 -lua-resty-jwt 0.1.10-1 -lua-resty-repl 0.0.6-0 -lua-resty-url 0.2.0-1 -lua-term 0.7-1 -lua_cliargs 3.0-1 -luafilesystem 1.7.0-2 -luassert 1.7.10-0 -luasystem 0.2.1-0 -markdown 0.33-1 -mediator_lua 1.1.2-0 -penlight 1.5.4-1 -router 2.1-0 -say 1.3-1 +argparse 0.5.0-1| +busted 2.0.rc12-1| +dkjson 2.5-2| +inspect 3.1.0-1| +ldoc 1.4.6-2| +liquid scm-1|811a73e38fdd9fdea116be4baf310ca326b96c77 +lua-resty-env 0.4.0-1| +lua-resty-execvp 0.1.0-1| +lua-resty-http 0.11-0| +lua-resty-iputils 0.3.0-1| +lua-resty-jwt 0.1.11-0| +lua-resty-repl 0.0.6-0|3878f41b7e8f97b1c96919db19dbee9496569dda +lua-resty-url 0.2.0-1| +lua-term 0.7-1| +lua_cliargs 3.0-1| +luafilesystem 1.7.0-2| +luassert 1.7.10-0| +luasystem 0.2.1-0| +markdown 0.33-1| +mediator_lua 1.1.2-0| +penlight 1.5.4-1| +router 2.1-0| +say 1.3-1| diff --git a/apicast/apicast-0.1-0.rockspec b/apicast/apicast-0.1-0.rockspec index 75cc81a9d..082e8b846 100644 --- a/apicast/apicast-0.1-0.rockspec +++ b/apicast/apicast-0.1-0.rockspec @@ -8,6 +8,10 @@ dependencies = { 'lua-resty-jwt', 'lua-resty-url', 'lua-resty-env', + 'lua-resty-execvp', + 'liquid', + 'argparse', + 'penlight', } build = { type = "builtin", diff --git a/apicast/bin/apicast b/apicast/bin/apicast index f454e49bd..c087189ba 100755 --- a/apicast/bin/apicast +++ b/apicast/bin/apicast @@ -1,130 +1,17 @@ -#!/bin/bash +#!/usr/bin/env perl +use strict; +use warnings FATAL => 'all'; -set -euo pipefail -IFS=$'\n\t' +use File::Basename; +use Cwd qw(abs_path); -script=${BASH_SOURCE[0]} -if (readlink -f "${script}" > /dev/null 2>&1); then - path=$(readlink -f "${script}") -elif (readlink "${script}" > /dev/null 2>&1); then - path="$(dirname "${script}")/$(readlink "${script}")" -else - path="${script}" -fi +my $apicast = $ENV{APICAST_DIR} || abs_path(dirname(abs_path(__FILE__)) . '/..'); +my $bindir = $apicast . '/bin'; +my $lua_path = $ENV{LUA_PATH} || ';'; -bin_dir=$(dirname "${path}") -apicast_dir=${APICAST_DIR:-"$( cd "${bin_dir}/.." && pwd )"} +chdir $apicast; -pick_openresty() { - for cmd in "$@" - do - if (${cmd} -V > /dev/null 2>&1); then - echo "${cmd}" - exit 0 - fi - done +$ENV{LUA_PATH} = "$apicast/src/?.lua;${lua_path}"; - (>&2 echo "ERROR: Could not find openresty executable in your PATH.") - (>&2 echo "Make sure you have one of: $(printf "%s " "$@")") - exit 1 -} - -openresty_binary=${APICAST_OPENRESTY_BINARY:-$(pick_openresty openresty-debug openresty nginx)} -log_level=${APICAST_LOG_LEVEL:-warn} -log_file=${APICAST_LOG_FILE:-stderr} -log_levels=(emerg alert crit error warn notice info debug) -((max_log_level=${#log_levels[@]}-1)) - -for ((i=0; i < ${#log_levels[@]}; i++)); do - ll=${log_levels[i]} - declare -r "log_level_${ll}=$i" -done - -log="log_level_${log_level}" -log_level="${!log}" - -daemon=off -worker_processes=${APICAST_WORKERS:-1} - -usage () { - cat <<-USAGE - Usage $0 - -h Show this help - -c Path to custom config file (JSON). - -d Daemonize - -v Increase verbosity (can be repeated) - -i Cache configuration for N seconds. Using 0 will reload on every request (not for production). - -w Number of worker processes to start. - -m Whether to start worker processes. Only for development. - -s Send signal to a master process: stop, quit, reopen, reload - -p Path to the PID file. - -b Load configuration on boot. - -e Deployment environment. Can be staging or production. -USAGE -} - -main=("") -args=("") - -while getopts ":dc:hvbqi:rw:m:s:p:e:" opt; do - case "${opt}" in - d) - daemon="on" - ;; - c) - export APICAST_CONFIGURATION="$OPTARG" - ;; - b) - export APICAST_CONFIGURATION_LOADER="boot" - ;; - v) - log_level=$((log_level == max_log_level ? max_log_level : log_level+1)) - ;; - q) - log_level=$((log_level == 0 ? 0 : log_level-1)) - ;; - i) - export APICAST_CONFIGURATION_CACHE="${OPTARG}" - ;; - e) - export THREESCALE_DEPLOYMENT_ENV="${OPTARG}" - ;; - w) - worker_processes=${OPTARG} - ;; - m) - main+=("master_process ${OPTARG};") - ;; - p) - main+=("pid ${PWD}/${OPTARG};") - ;; - s) - args+=("-s" "${OPTARG}") - ;; - h) - usage - exit 0 - ;; - \?) - echo "Invalid option: -${OPTARG}" >&2 - echo - usage - exit 1 - ;; - esac -done - -export THREESCALE_DEPLOYMENT_ENV=${THREESCALE_DEPLOYMENT_ENV-production} - -main+=("daemon ${daemon};") -main+=("worker_processes ${worker_processes};") -main+=$(printenv | awk '$1 ~ /^(APICAST|THREESCALE)_/ {split($0,env,"="); print "env", env[1] ";"}') - -function join_by { local IFS="$1"; shift; echo "$*"; } -args=$(join_by '' "${args[@]}") -main=$(join_by '' "${main[@]}") - -cd "${apicast_dir}" - -# shellcheck disable=SC2086 -exec "${openresty_binary}" -c "${apicast_dir}/conf/nginx.conf" ${args} -g "${main} error_log ${log_file} ${log_levels[log_level]};" +exec '/usr/bin/env', 'resty', + "$bindir/cli", @ARGV; diff --git a/apicast/bin/cli b/apicast/bin/cli new file mode 100755 index 000000000..330e08a59 --- /dev/null +++ b/apicast/bin/cli @@ -0,0 +1,11 @@ +#!/usr/bin/env resty + +local ok, setup = pcall(require, 'rover.setup') + +if ok then + setup() +else + package.path = './src/?.lua;' .. package.path +end + +require('apicast.cli')(arg) diff --git a/apicast/conf/nginx.conf b/apicast/conf/nginx.conf deleted file mode 100644 index 59174bcef..000000000 --- a/apicast/conf/nginx.conf +++ /dev/null @@ -1,78 +0,0 @@ -env REDIS_HOST; -env REDIS_PORT; -env REDIS_URL; -env RESOLVER; -env BACKEND_ENDPOINT_OVERRIDE; -env OPENSSL_VERIFY; - -include ../main.d/*.conf; - -env APICAST_REPORTING_THREADS; - -error_log /dev/null emerg; - -events { - worker_connections 16192; - multi_accept on; -} - -http { - sendfile on; - tcp_nopush on; - tcp_nodelay on; - - server_names_hash_bucket_size 128; - - log_format time '[$time_local] $host:$server_port $remote_addr:$remote_port "$request" $status $body_bytes_sent ($request_time) $post_action_impact'; - access_log off; - - lua_package_path ";;./?.lua;./src/?.lua"; - - # Enabling the Lua code cache is strongly encouraged for production use - # Disabling it should only be done for testing and development purposes - lua_code_cache on; - lua_max_running_timers 2048; - lua_socket_pool_size 512; - - include ../http.d/*.conf; - - server { - listen 8090; - - server_name _; - - include ../conf.d/management.conf; - } - - server { - listen 8081; - - server_name backend; - - include ../conf.d/backend.conf; - } - - server { - listen 8081 default_server; - - server_name echo _; - - include ../conf.d/echo.conf; - } - - server { - access_log /dev/stdout time; - - listen 8080; - - server_name _; - underscores_in_headers on; - - include ../http.d/ssl.conf; - - include ../apicast.d/*.conf; - include ../conf.d/apicast.conf; - } - - include ../sites.d/*.conf; -} diff --git a/apicast/conf/nginx.conf.liquid b/apicast/conf/nginx.conf.liquid new file mode 100644 index 000000000..79a13c34b --- /dev/null +++ b/apicast/conf/nginx.conf.liquid @@ -0,0 +1,93 @@ +env REDIS_HOST; +env REDIS_PORT; +env REDIS_URL; +env RESOLVER; +env BACKEND_ENDPOINT_OVERRIDE; +env OPENSSL_VERIFY; + +{% for env in env -%} + {%- if env.name | starts_with: 'APICAST_', 'THREESCALE_' %} + env {{ env.name }}; + {%- endif -%} +{%- endfor %} + +daemon {{ daemon | default: 'off' }}; +master_process {{ master_process | default: 'on' }}; +worker_processes {{ worker_processes | default: 'auto' }}; + +{% for file in "main.d/*.conf" | filesystem %} + {% include file %} +{% endfor %} + +error_log /dev/null emerg; + +events { + worker_connections 16192; + multi_accept on; +} + +http { + sendfile on; + tcp_nopush on; + tcp_nodelay on; + + # Enabling the Lua code cache is strongly encouraged for production use + # Disabling it should only be done for testing and development purposes + lua_code_cache {{ lua_code_cache | default: 'on' }}; + lua_max_running_timers {{ lua_running_timers | default: 2048 }}; + lua_socket_pool_size {{ lua_socket_pool_size | default: 512 }}; + server_names_hash_bucket_size 128; + + log_format time '[$time_local] $host:$server_port $remote_addr:$remote_port "$request" $status $body_bytes_sent ($request_time) $post_action_impact'; + access_log off; + + lua_package_path ";;{{prefix}}/?.lua;{{prefix}}/src/?.lua"; + + {% for file in "http.d/*.conf" | filesystem %} + {% include file %} + {% endfor %} + + server { + listen 8090; + + server_name _; + + {% include "conf.d/management.conf" %} + } + + server { + listen 8081; + + server_name backend; + + {% include "conf.d/backend.conf" %} + } + + server { + listen 8081 default_server; + + server_name echo _; + + {% include "conf.d/echo.conf" %} + } + + server { + access_log /dev/stdout time; + + listen 8080; + + server_name _; + underscores_in_headers on; + + {% include "http.d/ssl.conf" %} + + {% for file in "apicast.d/*.conf" | filesystem %} + {% include file %} + {% endfor %} + {% include "conf.d/apicast.conf" %} + } + + {% for file in "sites.d/*.conf" | filesystem %} + {% include file %} + {% endfor %} +} diff --git a/apicast/config/development.lua b/apicast/config/development.lua new file mode 100644 index 000000000..ee8a7af1a --- /dev/null +++ b/apicast/config/development.lua @@ -0,0 +1,12 @@ +local lr_path, lr_cpath, lr_bin = require('luarocks.cfg').package_paths() + +return { + worker_processes = '1', + master_process = 'off', + lua_code_cache = 'off', + lua_path = "./src/?.lua;./src/?/init.lua;"..lr_path, + lua_cpath = lr_cpath, + env = { + PATH = lr_bin -- this probably also needs to use the previous value + } +} diff --git a/apicast/config/production.lua b/apicast/config/production.lua new file mode 100644 index 000000000..297f7c6bf --- /dev/null +++ b/apicast/config/production.lua @@ -0,0 +1,5 @@ +return { + worker_processes = 'auto', + master_process = 'on', + lua_code_cache = 'on', +} diff --git a/apicast/http.d/ssl.conf b/apicast/http.d/ssl.conf index 5e97f5f6f..77e6d2abc 100644 --- a/apicast/http.d/ssl.conf +++ b/apicast/http.d/ssl.conf @@ -5,10 +5,10 @@ ## it really hard to have working cross platform configuration. # lua_ssl_verify_depth 5; -lua_ssl_trusted_certificate ca-bundle.crt; +lua_ssl_trusted_certificate "{{ ca_bundle | default: 'ca-bundle.crt' }}"; proxy_ssl_server_name on; proxy_ssl_name $http_host; proxy_ssl_verify_depth 5; -proxy_ssl_trusted_certificate ca-bundle.crt; +proxy_ssl_trusted_certificate "{{ ca_bundle | default: 'ca-bundle.crt' }}"; diff --git a/apicast/src/apicast/cli.lua b/apicast/src/apicast/cli.lua new file mode 100644 index 000000000..204efcffb --- /dev/null +++ b/apicast/src/apicast/cli.lua @@ -0,0 +1,54 @@ +local command_target = '_cmd' +local parser = require('argparse')() { + name = "APIcast", + description = "APIcast - 3scale API Management Platform Gateway." +} +:command_target(command_target) +:require_command(false) +:handle_options(false) + +local _M = { } + +local mt = {} + +local function load_commands(commands, argparse) + for i=1, #commands do + commands[commands[i]] = require('apicast.cli.' .. commands[i]).new(argparse) + end + return commands +end + +_M.commands = load_commands({ 'start' }, parser) + +function mt.__call(self, arg) + -- now we parse the options like usual: + local ok, ret = self.parse(arg) + + if not ok and ret then + local err = ret + table.insert(arg, 1, 'start') + ok, ret = self.parse(arg) + if not ok then + ret = err + table.remove(arg, 1) + end + end + + local cmd = ok and ret[command_target] + + if ok and cmd then + self.commands[cmd](ret) + elseif ret and not next(ret) then + local start = self.commands.start + start(start:parse(arg)) + else + print(ret) + os.exit(1) + end +end + +function _M.parse(arg) + return parser:pparse(arg) +end + +return setmetatable(_M, mt) diff --git a/apicast/src/apicast/cli/start.lua b/apicast/src/apicast/cli/start.lua new file mode 100644 index 000000000..fdc0e1d70 --- /dev/null +++ b/apicast/src/apicast/cli/start.lua @@ -0,0 +1,178 @@ +local setmetatable = setmetatable +local pairs = pairs +local min = math.min +local max = math.max +local insert = table.insert +local concat = table.concat + +local exec = require('resty.execvp') +local resty_env = require('resty.env') + +local Template = require('apicast.template') +local configuration = require('apicast.configuration') + +local pl = { + path = require('pl.path'), + file = require('pl.file'), + dir = require('pl.dir'), +} + +local _M = { + openresty = { 'openresty-debug', 'openresty', 'nginx' }, + log_levels = { 'emerg', 'alert', 'crit', 'error', 'warn', 'notice', 'info', 'debug' }, + log_level = 5, -- warn + log_file = 'stderr', +} + +local mt = { __index = _M } + +local function pick_openesty(candidates) + for i=1, #candidates do + local ok = os.execute(('%s -V 2>/dev/null'):format(candidates[i])) + + if ok then + return candidates[i] + end + end + + error("could not find openresty executable") +end + +local function update_env(env) + for name, value in pairs(env) do + resty_env.set(name, value) + end +end + +local function nginx_config(context, dir, path, env) + update_env(env) + + local template = Template:new(context, dir, true) + local tmp = pl.path.tmpname() + pl.file.write(tmp, template:render(path)) + return tmp +end + +local function create_prefix() + local tmp = os.tmpname() + + assert(pl.file.delete(tmp)) + assert(pl.dir.makepath(tmp .. '/logs')) + + return tmp +end + +local function get_log_level(self, options) + local log_level = options.log_level + local n = #(self.log_levels) + for i=1, n do + if self.log_levels[i] == log_level then + i = i + options.verbose - options.quiet + log_level = self.log_levels[max(min(i, n), 1)] + break + end + end + return log_level +end + +function mt:__call(options) + local openresty = resty_env.get('APICAST_OPENRESTY_BINARY') or pick_openesty(self.openresty) + local dir = resty_env.get('APICAST_DIR') or pl.path.abspath('.') + local config = configuration.new(dir) + local path = options.template + local environment = options.dev and 'development' or options.environment + local context = config:load(environment) + local env = { + APICAST_CONFIGURATION = options.configuration, + APICAST_CONFIGURATION_LOADER = options.boot and 'boot' or 'lazy', + APICAST_CONFIGURATION_CACHE = options.cache, + THREESCALE_DEPLOYMENT_ENV = environment, + } + + context.worker_processes = options.workers or context.worker_processes + + if options.daemon then + context.daemon = 'on' + end + + context.prefix = dir + context.ca_bundle = pl.path.abspath(context.ca_bundle or pl.path.join(dir, 'conf', 'ca-bundle.crt')) + + local nginx = nginx_config(context, dir, path, env) + + local log_level = get_log_level(self, options) + local log_file = options.log_file or self.log_file + local global = { + ('error_log %s %s'):format(log_file, log_level) + } + local prefix = create_prefix() + + local cmd = { '-c', nginx, '-g', concat(global, '; ') .. ';', '-p', prefix } + + if options.test then + insert(cmd, options.debug and '-T' or '-t') + end + + return exec(openresty, cmd, env) +end + +local function configure(cmd) + cmd:usage("Usage: apicast-cli start [OPTIONS]") + cmd:option("--template", "Nginx config template.", 'conf/nginx.conf.liquid') + + cmd:mutex( + cmd:option('-e --environment', "Deployment to start.", resty_env.get('THREESCALE_DEPLOYMENT_ENV')), + cmd:flag('--dev', 'Start in development environment') + ) + + cmd:flag("-t --test", "Test the nginx config") + cmd:flag("--debug", "Debug mode. Prints more information.") + cmd:option("-c --configuration", + "Path to custom config file (JSON)", + resty_env.get('APICAST_CONFIGURATION')) + cmd:flag("-d --daemon", "Daemonize.") + cmd:option("-w --workers", + "Number of worker processes to start.", + resty_env.get('APICAST_WORKERS') or 1) + cmd:option("-p --pid", "Path to the PID file.") + cmd:mutex( + cmd:flag('-b --boot', + "Load configuration on boot.", + resty_env.get('APICAST_CONFIGURATION_LOADER') == 'boot'), + cmd:flag('-l --lazy', + "Load configuration on demand.", + resty_env.get('APICAST_CONFIGURATION_LOADER') == 'lazy') + ) + cmd:option("-i --refresh-interval", + "Cache configuration for N seconds. Using 0 will reload on every request (not for production).", + resty_env.get('APICAST_CONFIGURATION_CACHE')) + + cmd:mutex( + cmd:flag('-v --verbose', + "Increase logging verbosity (can be repeated).") + :count(("0-%s"):format(#(_M.log_levels) - _M.log_level)), + cmd:flag('-q --quiet', "Decrease logging verbosity.") + :count(("0-%s"):format(_M.log_level - 1)) + ) + cmd:option('--log-level', 'Set log level', resty_env.get('APICAST_LOG_LEVEL') or 'warn') + cmd:option('--log-file', 'Set log file', resty_env.get('APICAST_LOG_FILE') or 'stderr') + + cmd:epilog([[ + Example: apicast start --dev + This will start APIcast in development mode.]]) + + return cmd +end + +function _M.new(parser) + local cmd = configure(parser:command('start', 'Start APIcast')) + + return setmetatable({ parser = parser, cmd = cmd }, mt) +end + +function _M:parse(arg) + return self.cmd:parse(arg) +end + + +return setmetatable(_M, mt) diff --git a/apicast/src/apicast/configuration.lua b/apicast/src/apicast/configuration.lua new file mode 100644 index 000000000..d1ce92b81 --- /dev/null +++ b/apicast/src/apicast/configuration.lua @@ -0,0 +1,55 @@ +local pl_path = require('pl.path') +local resty_env = require('resty.env') +local setmetatable = setmetatable +local loadfile = loadfile +local pcall = pcall +local require = require +local assert = assert +local error = error +local print = print +local pairs = pairs + +local _M = { + default_environment = 'production', + default_config = { + ca_bundle = resty_env.get('SSL_CERT_FILE') + } +} + +local mt = { __index = _M } + +function _M.new(root) + return setmetatable({ root = root }, mt) +end + +function _M:load(env) + local environment = env or self.default_environment + local root = self.root + local name = ("%s.lua"):format(environment) + local path = pl_path.join(root, 'config', name) + + print('loading config for: ', environment, ' environment from ', path) + + local config = loadfile(path, 't', { + print = print, inspect = require('inspect'), + pcall = pcall, require = require, assert = assert, error = error, + }) + + local default_config = {} + + if not config then + return default_config, 'invalid config' + end + + local table = config() + + for k,v in pairs(self.default_config) do + if table[k] == nil then + table[k] = v + end + end + + return table +end + +return _M diff --git a/apicast/src/apicast/template.lua b/apicast/src/apicast/template.lua new file mode 100644 index 000000000..d43acf258 --- /dev/null +++ b/apicast/src/apicast/template.lua @@ -0,0 +1,99 @@ +local _M = {} + +local setmetatable = setmetatable +local insert = table.insert +local assert = assert +local pairs = pairs +local sub = string.sub +local len = string.len +local pack = table.pack +local pl = { dir = require('pl.dir'), path = require('pl.path'), file = require('pl.file') } +local Liquid = require 'liquid' +local resty_env = require('resty.env') +local Lexer = Liquid.Lexer +local Parser = Liquid.Parser +local Interpreter = Liquid.Interpreter +local FilterSet = Liquid.FilterSet +local InterpreterContext = Liquid.InterpreterContext +local FileSystem = Liquid.FileSystem +local ResourceLimit = Liquid.ResourceLimit + +local function noop(...) return ... end + + +function _M:new(config, dir, strict) + local instance = setmetatable({}, { __index = self }) + local env = {} + + for name,value in pairs(resty_env.list()) do + insert(env, { name = name, value = value }) + end + + local context = setmetatable({ + env = env, + }, { __index = config }) + + instance.root = pl.path.abspath(dir or pl.path.currentdir()) + instance.context = InterpreterContext:new(context) + instance.strict = strict + instance.filesystem = FileSystem:new(function(path) + return instance:read(path) + end) + + return instance +end + +function _M:read(template_name) + local root = self.root + local check = self.strict and assert or noop + + assert(template_name, 'missing template name') + return check(pl.file.read(pl.path.join(root, template_name))) +end + +function _M:render(template_name) + local template = self:read(template_name) + return self:interpret(template) +end + +local function starts_with(string, match) + return sub(string,1,len(match)) == match +end + +function _M:interpret(str) + local lexer = Lexer:new(str) + local parser = Parser:new(lexer) + local interpreter = Interpreter:new(parser) + local context = self.context + local filesystem = self.filesystem + local filter_set = FilterSet:new() + local resource_limit = ResourceLimit:new(nil, 1000, nil) + + filter_set:add_filter('filesystem', function(pattern) + local files = {} + + for filename, dir in pl.dir.dirtree(self.root) do + local file = pl.path.relpath(filename, self.root) + if pl.dir.fnmatch(file, pattern) and not dir then + insert(files, file) + end + end + + return files + end) + + filter_set:add_filter('default', function(value, default) + return value or default + end) + + filter_set:add_filter('starts_with', function(string, ...) + local matches = pack(...) + for i=1, matches.n do + if starts_with(string, matches[i]) then return true end + end + end) + + return interpreter:interpret(context, filter_set, resource_limit, filesystem) +end + +return _M diff --git a/bin/cli b/bin/cli new file mode 120000 index 000000000..a1aec1df0 --- /dev/null +++ b/bin/cli @@ -0,0 +1 @@ +../apicast/bin/cli \ No newline at end of file From 1e10f562b92a31c32666d603bc0cf1123d6d4ed6 Mon Sep 17 00:00:00 2001 From: Michal Cichra Date: Thu, 2 Nov 2017 10:14:46 +0100 Subject: [PATCH 2/2] [bin] automatically execute `rover exec` when needed --- apicast/bin/apicast | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/apicast/bin/apicast b/apicast/bin/apicast index c087189ba..5755243f6 100755 --- a/apicast/bin/apicast +++ b/apicast/bin/apicast @@ -7,11 +7,21 @@ use Cwd qw(abs_path); my $apicast = $ENV{APICAST_DIR} || abs_path(dirname(abs_path(__FILE__)) . '/..'); my $bindir = $apicast . '/bin'; -my $lua_path = $ENV{LUA_PATH} || ';'; +my $lua_path = $ENV{LUA_PATH}; + +chomp(my $rover = `which rover`); +if ($rover) { $rover = abs_path($rover) } + +if ($rover && !$lua_path) { + exec '/usr/bin/env', $rover, 'exec', $0, @ARGV +} else { + $lua_path ||= ';'; +} chdir $apicast; $ENV{LUA_PATH} = "$apicast/src/?.lua;${lua_path}"; -exec '/usr/bin/env', 'resty', - "$bindir/cli", @ARGV; +my @args = ('resty', "$bindir/cli", @ARGV); + +exec '/usr/bin/env', @args;