Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
node_modules/
.git/
.github/
dist/
*.md
.env
.env.example
.eslintrc*
eslint.config*
17 changes: 17 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,23 @@ jobs:
- run: npm install
- run: npm test

test-docker:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Generate .env
run: ./docker/generate-env.sh
- name: Build and start services
run: docker compose -f docker/docker-compose.yml up --build -d --wait
- name: Run tests
run: docker compose -f docker/docker-compose.yml exec api npm test
- name: Logs on failure
if: failure()
run: docker compose -f docker/docker-compose.yml logs
- name: Tear down
if: always()
run: docker compose -f docker/docker-compose.yml down -v

test-win:
needs: [ get-lts ]
runs-on: windows-latest
Expand Down
18 changes: 18 additions & 0 deletions docker/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# NicTool API environment configuration
# Copy to .env, or run: ./docker/generate-env.sh

# --- Database (MariaDB/MySQL) ---
DB_ROOT_PASSWORD=changeme
NICTOOL_DB_NAME=nictool
NICTOOL_DB_USER=nictool
NICTOOL_DB_USER_PASSWORD=changeme

# --- API config overrides (optional, override conf.d/*.toml defaults) ---
# NICTOOL_DB_HOST=127.0.0.1
# NICTOOL_DB_PORT=3306
# NICTOOL_HTTP_HOST=localhost
# NICTOOL_HTTP_PORT=3000

# --- Docker Compose port mapping ---
# DB_PORT=3307
# API_PORT=3000
8 changes: 8 additions & 0 deletions docker/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
FROM node:22-trixie-slim
WORKDIR /app
COPY package*.json .
# --omit=dev is safe: tests use node:test (stdlib), devDeps are only eslint/prettier
RUN npm install --omit=dev
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]
72 changes: 72 additions & 0 deletions docker/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
services:
db:
image: mariadb:11
environment:
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
MYSQL_ROOT_HOST: '%'
MYSQL_DATABASE: ${NICTOOL_DB_NAME:-nictool}
MYSQL_USER: ${NICTOOL_DB_USER:-nictool}
MYSQL_PASSWORD: ${NICTOOL_DB_USER_PASSWORD}
SQL_DIR: /sql
volumes:
- db-data:/var/lib/mysql
- ../sql:/sql:ro
- ../sql/init-mysql.sh:/docker-entrypoint-initdb.d/init-mysql.sh:ro
ports:
- "${DB_PORT:-3307}:3306"
healthcheck:
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
interval: 5s
timeout: 5s
retries: 10

api:
build:
context: ..
dockerfile: docker/Dockerfile
ports:
- "${API_PORT:-3000}:3000"
depends_on:
db:
condition: service_healthy
environment:
NODE_ENV: development
NICTOOL_DB_HOST: db
NICTOOL_DB_USER: ${NICTOOL_DB_USER:-nictool}
NICTOOL_DB_USER_PASSWORD: ${NICTOOL_DB_USER_PASSWORD}
NICTOOL_DB_NAME: ${NICTOOL_DB_NAME:-nictool}
NICTOOL_HTTP_HOST: "0.0.0.0"
healthcheck:
test: ["CMD", "node", "-e", "fetch('http://localhost:3000/documentation').then(r=>{if(!r.ok)process.exit(1)}).catch(()=>process.exit(1))"]
interval: 10s
timeout: 5s
retries: 10
start_period: 15s

server:
profiles: [ui, e2e]
build:
context: ${NICTOOL_SERVER_PATH:-../../server}
dockerfile: docker/Dockerfile
hostname: localhost
ports:
- "${SERVER_PORT:-8080}:${SERVER_CONTAINER_PORT:-8080}"
depends_on:
api:
condition: service_healthy
volumes:
- ${NICTOOL_SERVER_PATH:-../../server}/index.js:/app/index.js:ro
- ${NICTOOL_SERVER_PATH:-../../server}/bin:/app/bin:ro
- ${NICTOOL_SERVER_PATH:-../../server}/html:/app/html:ro
environment:
NICTOOL_TLS: ${NICTOOL_TLS:-auto}
NICTOOL_DB_HOST: db
NICTOOL_DB_USER: ${NICTOOL_DB_USER:-nictool}
NICTOOL_DB_USER_PASSWORD: ${NICTOOL_DB_USER_PASSWORD}
NICTOOL_DB_NAME: ${NICTOOL_DB_NAME:-nictool}
NICTOOL_API_HOST: api
NICTOOL_API_PORT: 3000
NICTOOL_BIND_HOST: "0.0.0.0"

volumes:
db-data:
35 changes: 35 additions & 0 deletions docker/generate-env.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
#!/bin/bash
set -e

ENV_FILE="$(cd "$(dirname "$0")" && pwd)/.env"

if [ -f "$ENV_FILE" ]; then
echo ".env already exists, not overwriting." >&2
exit 0
fi

DB_ROOT_PW=$(openssl rand -base64 24)
NT_DB_PW=$(openssl rand -base64 24)

cat > "$ENV_FILE" <<EOF
# NicTool API environment configuration
# Generated by docker/generate-env.sh

# --- Database (MariaDB/MySQL) ---
DB_ROOT_PASSWORD=$DB_ROOT_PW
NICTOOL_DB_NAME=nictool
NICTOOL_DB_USER=nictool
NICTOOL_DB_USER_PASSWORD=$NT_DB_PW

# --- API config overrides (optional, override conf.d/*.toml defaults) ---
# NICTOOL_DB_HOST=127.0.0.1
# NICTOOL_DB_PORT=3306
# NICTOOL_HTTP_HOST=localhost
# NICTOOL_HTTP_PORT=3000

# --- Docker Compose port mapping ---
# DB_PORT=3307
# API_PORT=3000
EOF

echo "Generated $ENV_FILE with random passwords."
26 changes: 26 additions & 0 deletions lib/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ class Config {

const str = await fs.readFile(`./conf.d/${name}.toml`, 'utf8')
const cfg = parse(str)
applyEnvOverrides(name, cfg)
if (this.debug) console.debug(cfg)

if (name === 'http') {
Expand All @@ -35,6 +36,7 @@ class Config {

const str = fsSync.readFileSync(`./conf.d/${name}.toml`, 'utf8')
const cfg = parse(str)
applyEnvOverrides(name, cfg)
if (this.debug) console.debug(cfg)

if (name === 'http') {
Expand All @@ -47,6 +49,30 @@ class Config {
}
}

function parsePort(envVar) {
const raw = process.env[envVar]
if (!raw) return undefined
const port = parseInt(raw, 10)
if (Number.isNaN(port) || port < 1 || port > 65535) {
throw new Error(`${envVar}="${raw}" is not a valid port (1-65535)`)
}
return port
}

function applyEnvOverrides(name, cfg) {
if (name === 'mysql') {
if (process.env.NICTOOL_DB_HOST) cfg.host = process.env.NICTOOL_DB_HOST
if (process.env.NICTOOL_DB_PORT) cfg.port = parsePort('NICTOOL_DB_PORT')
if (process.env.NICTOOL_DB_USER) cfg.user = process.env.NICTOOL_DB_USER
if (process.env.NICTOOL_DB_USER_PASSWORD) cfg.password = process.env.NICTOOL_DB_USER_PASSWORD
if (process.env.NICTOOL_DB_NAME) cfg.database = process.env.NICTOOL_DB_NAME
}
if (name === 'http') {
if (process.env.NICTOOL_HTTP_HOST) cfg.host = process.env.NICTOOL_HTTP_HOST
if (process.env.NICTOOL_HTTP_PORT) cfg.port = parsePort('NICTOOL_HTTP_PORT')
}
}

async function loadPEM(dir) {
let entries
try {
Expand Down
22 changes: 21 additions & 1 deletion lib/config.test.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,29 @@
import assert from 'node:assert/strict'
import { describe, it } from 'node:test'
import { describe, it, before, after } from 'node:test'

import Config from './config.js'

const envOverrideKeys = ['NICTOOL_DB_HOST', 'NICTOOL_DB_PORT', 'NICTOOL_DB_USER', 'NICTOOL_DB_USER_PASSWORD', 'NICTOOL_DB_NAME', 'NICTOOL_HTTP_HOST', 'NICTOOL_HTTP_PORT']

describe('config', () => {
const savedEnv = {}

before(() => {
for (const key of envOverrideKeys) {
savedEnv[key] = process.env[key]
delete process.env[key]
}
Config.cfg = {}
})

after(() => {
for (const key of envOverrideKeys) {
if (savedEnv[key] !== undefined) process.env[key] = savedEnv[key]
else delete process.env[key]
}
Config.cfg = {}
})

describe('get', () => {
it(`loads mysql config`, async () => {
const cfg = await Config.get('mysql')
Expand All @@ -26,14 +46,14 @@

it(`loads http config`, async () => {
const cfg = await Config.get('http')
const { tls, ...rest } = cfg

Check warning on line 49 in lib/config.test.js

View workflow job for this annotation

GitHub Actions / lint / lint

'tls' is assigned a value but never used
delete rest.password
assert.deepEqual(rest, httpCfg)
})

it(`loads http config synchronously`, () => {
const cfg = Config.getSync('http')
const { tls, ...rest } = cfg

Check warning on line 56 in lib/config.test.js

View workflow job for this annotation

GitHub Actions / lint / lint

'tls' is assigned a value but never used
delete rest.password
assert.deepEqual(rest, httpCfg)
})
Expand Down
68 changes: 44 additions & 24 deletions sql/init-mysql.sh
Original file line number Diff line number Diff line change
@@ -1,37 +1,57 @@
#!/bin/sh

DB_USER="${DB_USER:-root}"
DB_NAME="${DB_NAME:-nictool}"
MYSQL_BIN=""

if [ "$MYSQL_PWD" = "" ];
then
export MYSQL_PWD=root

# configure MySQL in the GitHub workflow runners
case "$(uname -s)" in
Linux*)
;;
Darwin*)
MYSQL_BIN=/opt/homebrew/opt/mysql@8.4/bin/
${MYSQL_BIN}mysqladmin --user=root --password='' --protocol=tcp password 'root'
;;
CYGWIN*|MINGW*|MINGW32*|MSYS*)
mysqladmin --user=root --password='' --protocol=tcp password 'root'
# export MYSQL_PWD=""
;;
esac
if [ "$MYSQL_PWD" = "" ]; then
if [ -n "$MYSQL_ROOT_PASSWORD" ]; then
# Docker: MYSQL_ROOT_PASSWORD is set by the MariaDB container
export MYSQL_PWD="$MYSQL_ROOT_PASSWORD"
else
export MYSQL_PWD=root

# configure MySQL in the GitHub workflow runners
case "$(uname -s)" in
Linux*)
;;
Darwin*)
MYSQL_BIN=/opt/homebrew/opt/mysql@8.4/bin/
${MYSQL_BIN}mysqladmin --user=root --password='' --protocol=tcp password 'root'
;;
CYGWIN*|MINGW*|MINGW32*|MSYS*)
mysqladmin --user=root --password='' --protocol=tcp password 'root'
# export MYSQL_PWD=""
;;
esac
fi
fi

# AUTH="--defaults-extra-file=./sql/my-gha.cnf"
if [ -z "$MYSQL_CMD" ]; then
# prefer mariadb client if available (MariaDB 11+ dropped the mysql symlink)
if [ -z "$MYSQL_BIN" ] && command -v mariadb >/dev/null 2>&1; then
MYSQL_CMD="mariadb --user=$DB_USER"
elif [ -n "$MYSQL_BIN" ]; then
MYSQL_CMD="${MYSQL_BIN}mysql --user=$DB_USER"
else
MYSQL_CMD="mysql --user=$DB_USER"
fi
fi

if [ "$1" = "drop" ]; then
${MYSQL_BIN}mysql --user=root -e 'DROP DATABASE IF EXISTS nictool;' || exit 1
$MYSQL_CMD -e "DROP DATABASE IF EXISTS $DB_NAME;" || exit 1
fi
${MYSQL_BIN}mysql --user=root -e 'CREATE DATABASE nictool;' || exit 1

for f in ./sql/*.sql;
$MYSQL_CMD -e "CREATE DATABASE IF NOT EXISTS $DB_NAME;" || exit 1

# In Docker, SQL_DIR is set via compose env (e.g. /sql).
# Outside Docker, defaults to ./sql relative to CWD (typically the repo root).
SQL_DIR="${SQL_DIR:-./sql}"

for f in "$SQL_DIR"/*.sql;
do
echo "cat $f | ${MYSQL_BIN}mysql nictool"
cat $f | ${MYSQL_BIN}mysql --user=root nictool || exit 1
echo "$f"
$MYSQL_CMD "$DB_NAME" < "$f" || exit 1
done

exit 0
exit 0
Loading