~mna/tulip-cli

385dcbdee666575509356342d3ab6987fb713569 — Martin Angers 1 year, 10 months ago 43ae55d
Implement the init command
4 files changed, 352 insertions(+), 11 deletions(-)

A README.md
M src/cmds/initcmd.lua
M src/main.lua
M tulip-cli.lua
A README.md => README.md +19 -0
@@ 0,0 1,19 @@
# tulip-cli

Command-line assistant of the [Tulip][tulip] Lua web framework.

* Canonical repository: https://git.sr.ht/~mna/tulip-cli
* Issue tracker: https://todo.sr.ht/~mna/tulip
* Documentation: https://man.sr.ht/~mna/tulip

## Usage

See the [tulip documentation][gs] for usage information.

## License

The [BSD 3-clause][bsd] license.

[bsd]: http://opensource.org/licenses/BSD-3-Clause
[tulip]: https://git.sr.ht/~mna/tulip
[gs]: https://man.sr.ht/~mna/tulip/#getting-started

M src/cmds/initcmd.lua => src/cmds/initcmd.lua +324 -5
@@ 1,7 1,326 @@
local inspect = require 'inspect'
local base64 = require 'base64'
local dirent = require 'posix.dirent'
local errno = require 'posix.errno'
local libgen = require 'posix.libgen'
local posix = require 'posix'
local sh = require 'shell'
local stdlib = require 'posix.stdlib'
local unistd = require 'posix.unistd'

return function(args, opts)
  print(inspect(args))
  print(inspect(opts))
  return true
local rand = assert(io.open('/dev/urandom', 'r'))
local enc = base64.makeencoder('.', '_', '-')

local function random_string(len)
  local raw = rand:read(len)
  return base64.encode(raw, enc)
end

local function log(s, ...)
  local msg = string.format(s, ...)
  io.write(msg)
  if not string.match(msg, '\n$') then
    io.flush()
  end
end

local function write_file(path, content)
  local fd = assert(io.open(path, 'w+'))
  assert(fd:write(content))
  assert(fd:close())
end

local function create_luarocks(root_dir, luaver)
  local dir = root_dir .. '/.luarocks'
  assert(posix.mkdir(dir))
  write_file(dir .. string.format('/config-%s.lua', luaver), '')
  write_file(dir .. '/default-lua-version.lua', string.format('return "%s"', luaver))
  assert(posix.mkdir(root_dir .. '/lua_modules'))
end

local function create_postgres(root_dir)
  assert(posix.mkdir(root_dir .. '/db'))
  assert(posix.mkdir(root_dir .. '/db/postgres'))
  assert(posix.mkdir(root_dir .. '/db/postgres/config'))
  assert(posix.mkdir(root_dir .. '/db/postgres/data'))
  assert(posix.mkdir(root_dir .. '/db/postgres/image'))

  write_file(root_dir .. '/db/postgres/.gitignore', [[
# ignore the data directory
/data/
]])

  write_file(root_dir .. '/db/postgres/config/postgres.conf', [[
# See https://github.com/postgres/postgres/blob/master/src/backend/utils/misc/postgresql.conf.sample
#
# -----------------------------
# PostgreSQL configuration file
# -----------------------------
#
# This file consists of lines of the form:
#
#   name = value
#
# (The "=" is optional.)  Whitespace may be used.  Comments are introduced with
# "#" anywhere on a line.  The complete list of parameter names and allowed
# values can be found in the PostgreSQL documentation.
#
# The commented-out settings shown in this file represent the default values.
# Re-commenting a setting is NOT sufficient to revert it to the default value;
# you need to reload the server.
#
# This file is read on server startup and when the server receives a SIGHUP
# signal.  If you edit the file on a running system, you have to SIGHUP the
# server for the changes to take effect, run "pg_ctl reload", or execute
# "SELECT pg_reload_conf()".  Some parameters, which are marked below,
# require a server shutdown and restart to take effect.
#
# Any parameter can also be given as a command-line option to the server, e.g.,
# "postgres -c log_connections=on".  Some parameters can be changed at run time
# with the "SET" SQL command.
#
# Memory units:  kB = kilobytes        Time units:  ms  = milliseconds
#                MB = megabytes                     s   = seconds
#                GB = gigabytes                     min = minutes
#                TB = terabytes                     h   = hours
#                                                   d   = days

# - Connection Settings -

listen_addresses = '*'
					# comma-separated list of addresses;
					# defaults to 'localhost'; use '*' for all
					# (change requires restart)

timezone = 'UTC'

shared_preload_libraries = 'pg_cron, pllua'
cron.database_name = 'postgres'
]])

  write_file(root_dir .. '/db/postgres/image/Dockerfile', [[
FROM postgres:13

RUN set -ex                      \
      && apt-get --yes update    \
      && apt-get --yes install   \
        build-essential          \
        git                      \
        liblua5.3-dev            \
        lua5.3                   \
        postgresql-server-dev-13 \
      && cd /tmp                 \
      && git clone https://github.com/citusdata/pg_cron.git \
      && cd pg_cron              \
      && make                    \
      && make install            \
      && cd /tmp                 \
      && git clone https://github.com/pllua/pllua-ng.git    \
      && cd pllua-ng             \
      && make                    \
        PG_CONFIG=pg_config      \
        LUA_INCDIR=/usr/include/lua5.3 \
        LUALIB=-llua5.3          \
        LUAC=luac5.3             \
        LUA=lua5.3               \
        install

COPY ./install_pg_cron.sql /docker-entrypoint-initdb.d/install_pg_cron.sql
COPY ./install_pllua.sql   /docker-entrypoint-initdb.d/install_pllua.sql
]])

  write_file(root_dir .. '/db/postgres/image/install_pg_cron.sql', [[
CREATE EXTENSION pg_cron;
]])

  write_file(root_dir .. '/db/postgres/image/install_pllua.sql', [[
CREATE EXTENSION pllua;
]])

  write_file(root_dir .. '/docker-compose.yml', [[
version: '3.6'
services:
  pg:
    build: ./db/postgres/image
    command: -c 'config_file=/etc/postgresql/postgresql.conf'
    ports:
      - "127.0.0.1:5432:5432"
    environment:
      - POSTGRES_PASSWORD_FILE=/run/secrets/root_pwd
    volumes:
      - ./db/postgres/config/postgres.conf:/etc/postgresql/postgresql.conf:ro
      - ./db/postgres/data:/var/lib/postgresql/data
    secrets:
      - root_pwd

secrets:
  root_pwd:
    file: ./run/secrets/pgroot_pwd
]])

  if sh.cmd('command', '-v', 'chcon'):output() then
    -- change the SELinux label for the volume to be mounted on the container
    assert(sh('chcon', '-Rt', 'svirt_sandbox_file_t', root_dir .. '/db/postgres/data'))
    assert(sh('chcon', '-Rt', 'svirt_sandbox_file_t', root_dir .. '/db/postgres/config'))
  end
end

local function create_secrets(root_dir)
  local dir = root_dir .. '/run/secrets'
  assert(posix.mkdir(root_dir .. '/run'))
  assert(posix.mkdir(dir))

  -- postgresql root password
  local pgpwd = random_string(32)
  write_file(dir .. '/pgroot_pwd', pgpwd)
  assert(sh('chmod', '0600', dir .. '/pgroot_pwd'))
  if sh.cmd('command', '-v', 'chcon'):output() then
    assert(sh('chcon', '-Rt', 'svirt_sandbox_file_t', dir .. '/pgroot_pwd'))
  end

  -- csrf secret key
  local csrf = random_string(32)
  write_file(dir .. '/csrf_key', csrf)
  assert(sh('chmod', '0600', dir .. '/csrf_key'))

  -- account secret key
  local acct = random_string(32)
  write_file(dir .. '/account_key', acct)
  assert(sh('chmod', '0600', dir .. '/account_key'))

  -- pgpass file
  write_file(dir .. '/pgpass', string.format([[
localhost:5432:*:postgres:%s
]], pgpwd))
  assert(sh('chmod', '0600', dir .. '/pgpass'))
end

local function create_certs(root_dir)
  local dir = root_dir .. '/run/certs'
  assert(posix.mkdir(dir))

  assert(sh('mkcert', '-install'))
  assert(sh('mkcert',
    '-cert-file', dir .. '/fullchain.pem',
    '-key-file',  dir .. '/privkey.pem',
    'localhost', '127.0.0.1', '::1'))
end

local function create_envrc(root_dir)
  write_file(root_dir .. '/.envrc', [[
export LUA_VERSION=`lua -e 'print(dofile(".luarocks/default-lua-version.lua"))'`
export LUA_MODULES=`pwd`/lua_modules
export LUA_PATH="${LUA_MODULES}/share/lua/${LUA_VERSION}/?.lua;${LUA_MODULES}/share/lua/${LUA_VERSION}/?/init.lua;;"
export LUA_CPATH="${LUA_MODULES}/lib/lua/${LUA_VERSION}/?.so;;"

export PGPASSFILE=`pwd`/run/secrets/pgpass
export PGHOST=localhost
export PGPORT=5432
export PGCONNECT_TIMEOUT=10
export PGUSER=postgres
export PGDATABASE=postgres
]])
end

local function create_gitignore(root_dir)
  write_file(root_dir .. '/.gitignore', [[
# environment files
.env*

# locally-installed lua modules
/lua_modules/

# local luarocks configuration
/.luarocks/

# local runtime directory
/run/

# output files generated by tools
*.out

# rocks packages
*.src.rock
]])
end

local function install_tulip(root_dir)
  assert(unistd.chdir(root_dir))
  assert(sh('luarocks', 'install', 'tulip'))
end

return function(args, opts, errf)
  local _ = opts -- for now, no options available

  if #args == 0 then
    errf('missing directory argument')
    return
  elseif #args > 1 then
    errf('unexpected argument starting at %q', args[2])
    return
  end

  local dir = args[1]
  local ok, err, code = posix.mkdir(dir)
  if not ok then
    if code == errno.EEXIST then
      local entries = dirent.dir(dir)
      if #entries > 2 then
        errf('directory exists and is not empty')
        return
      end
    else
      errf('%d: %s', code, err)
      return
    end
  end
  local base = libgen.basename(assert(stdlib.realpath(dir)))
  local luaver = string.match(_VERSION, '(%d%.%d)')

  log([[
> initializing a tulip setup for project %s and Lua %s

Press ENTER to start.
]], base, luaver)
  io.read('l')

  log('> creating LuaRocks project configuration...')
  create_luarocks(dir, luaver)
  log(' ok\n')

  log('> creating postgresql database and docker files...')
  create_postgres(dir)
  log(' ok\n')

  log('> creating passwords and secrets...')
  create_secrets(dir)
  log(' ok\n')

  if sh.cmd('command', '-v', 'mkcert'):output() then
    log('> creating localhost certificates...\n')
    create_certs(dir)
  else
    log('> skipping localhost certificates, mkcert not installed\n')
  end

  log('> creating direnv envrc file...')
  create_envrc(dir)
  log(' ok\n')

  log('> creating gitignore file...')
  create_gitignore(dir)
  log(' ok\n')

  log('> installing the Tulip LuaRock package and its dependencies...\n')
  install_tulip(dir)

  log([[
> initialization of the tulip setup for project %s and Lua %s completed:

Execute the following
  cd %s
  direnv allow .
to enable the environment configuration, and
  docker-compose up
to build and start the postgresql database.
]], base, luaver, dir)
end

M src/main.lua => src/main.lua +8 -5
@@ 23,14 23,17 @@ return function(args)
  local arg, opts = parser:parse(args)
  if #arg == 0 then
    parser:opterr('the command is required')
    return true
    return
  end

  local f = cmds[arg[1]]
  local cmd = table.remove(arg, 1)
  local f = cmds[cmd]
  if not f then
    parser:opterr(string.format('unknown command %q', arg[1]))
    return true
    parser:opterr(string.format('unknown command %q', cmd))
    return
  end

  return f(arg, opts)
  return f(arg, opts, function(msg, ...)
    parser:opterr(string.format(msg, ...))
  end)
end

M tulip-cli.lua => tulip-cli.lua +1 -1
@@ 1,4 1,4 @@
#!/usr/bin/env lua

local main = require 'src.main'
assert(main(_G.arg))
main(_G.arg)