Skip to content
Open
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
21 changes: 5 additions & 16 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ on:
pull_request:
workflow_dispatch:
inputs:
publish_image:
description: 'Publish image to registry?'
publish_images:
description: 'Publish images to registry?'
required: true
type: boolean
default: false
Expand Down Expand Up @@ -96,13 +96,7 @@ jobs:

- run: ./test/check-docker-context.sh --min-size 5000 --max-size 10000 --min-count 600 --max-count 700

- name: Extract FRONTEND_VERSION
run: |
touch .env
echo "FRONTEND_VERSION=$(
docker compose config --format json | jq -r .services.nginx.build.args.FRONTEND_VERSION
)" >> "$GITHUB_ENV"

- run: ./files/ci/extract-frontend-version.sh
- run: FRONTEND_BUILD_MODE=fetch ./test/test-images.sh

# Check out the current frontend version referenced by docker-compose, as it should build OK.
Expand All @@ -118,7 +112,7 @@ jobs:
run: docker compose logs
build-push-image:
if: |
(github.event_name == 'workflow_dispatch' && inputs.publish_image == true) ||
(github.event_name == 'workflow_dispatch' && inputs.publish_images == true) ||
(github.event_name != 'workflow_dispatch' && (
github.ref == 'refs/heads/master' ||
startsWith(github.ref, 'refs/tags/v')
Expand Down Expand Up @@ -162,12 +156,7 @@ jobs:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4

- name: Extract FRONTEND_VERSION
run: |
touch .env
echo "FRONTEND_VERSION=$(
docker compose config --format json | jq -r .services.nginx.build.args.FRONTEND_VERSION
)" >> "$GITHUB_ENV"
- run: ./files/ci/extract-frontend-version.sh

- name: Build and push ${{ matrix.image }} Docker image
uses: docker/build-push-action@v7
Expand Down
2 changes: 1 addition & 1 deletion enketo.dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM ghcr.io/enketo/enketo:7.6.2
FROM ghcr.io/enketo/enketo:7.6.1

ENV ENKETO_SRC_DIR=/srv/src/enketo/packages/enketo-express
WORKDIR ${ENKETO_SRC_DIR}
Expand Down
21 changes: 21 additions & 0 deletions files/ci/extract-frontend-version.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#!/bin/bash -eu
set -o pipefail
shopt -s inherit_errexit

log() { echo >&2 "[$(basename "$0")] $*"; }

log "Ensuring .env exists for docker compose..."
touch .env

log "Reading FRONTEND_VERSION from docker-compose.yml..."
if ! frontendVersion="$(docker compose config --format json | jq -er .services.nginx.build.args.FRONTEND_VERSION)"; then
log "!!!"
log "!!! Failed to read FRONTEND_VERSION from docker-compose.yml (got: '$frontendVersion')."
log "!!!"
exit 1
fi

log "Writing FRONTEND_VERSION to GITHUB_ENV file ($GITHUB_ENV)..."
echo "FRONTEND_VERSION=$frontendVersion" >> "$GITHUB_ENV"

log "Completed OK."
2 changes: 1 addition & 1 deletion files/nginx/odk.conf.template
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ map $spa_name $spa_csp {
"default-src 'report-sample' 'none'; connect-src 'self' https:; font-src 'self' data:; form-action 'self'; frame-ancestors 'self'; frame-src 'self' https://getodk.github.io/central/; img-src blob: data: https:; manifest-src 'self'; media-src blob:; object-src 'none'; script-src 'report-sample' 'self' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; worker-src 'report-sample' blob: data:; report-uri /csp-report";

default
"default-src 'report-sample' 'none'; connect-src 'self' https://translate.google.com https://translate.googleapis.com; font-src 'self'; form-action 'self'; frame-ancestors 'none'; frame-src 'self' https://getodk.github.io/central/; img-src data: https:; manifest-src 'self'; media-src 'none'; object-src 'none'; script-src 'report-sample' 'self'; style-src 'report-sample' 'self'; style-src-attr 'unsafe-inline'; worker-src 'report-sample' blob:; report-uri /csp-report";
"default-src 'report-sample' 'none'; connect-src 'self' ${SENTRY_DSN_FRONTEND_ROOT} https://translate.google.com https://translate.googleapis.com; font-src 'self'; form-action 'self'; frame-ancestors 'none'; frame-src 'self' https://getodk.github.io/central/; img-src data: https:; manifest-src 'self'; media-src 'none'; object-src 'none'; script-src 'report-sample' 'self'; style-src 'report-sample' 'self'; style-src-attr 'unsafe-inline'; worker-src 'report-sample' blob:; report-uri /csp-report";
}

map $upstream_http_content_security_policy $central_backend_csp {
Expand Down
3 changes: 2 additions & 1 deletion files/nginx/setup-odk.sh
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ fi
SELFSIGN_PATH="/etc/selfsign/live/$DOMAIN"
if [ "$SSL_TYPE" = "selfsign" ] && [ ! -s "$SELFSIGN_PATH/privkey.pem" ]; then
mkdir -p "$SELFSIGN_PATH"
openssl req -x509 -newkey rsa:4086 \
openssl req -x509 -newkey rsa:4096 \
-subj "/C=XX/ST=XXXX/L=XXXX/O=XXXX/CN=localhost" \
-keyout "$SELFSIGN_PATH/privkey.pem" \
-out "$SELFSIGN_PATH/fullchain.pem" \
Expand All @@ -45,6 +45,7 @@ echo "writing fresh nginx templates..."
> /etc/nginx/conf.d/redirector.conf

CERT_DOMAIN=$( [ "$SSL_TYPE" = "customssl" ] && echo "local" || echo "$DOMAIN") \
SENTRY_DSN_FRONTEND_ROOT="$(awk 'sub(/:\/\/[a-z0-9]*@/, "://") && sub(/\/[0-9]+$/, "")' <<<"$SENTRY_DSN_FRONTEND")" \
/scripts/envsub.awk \
< /usr/share/odk/nginx/odk.conf.template \
> /etc/nginx/conf.d/odk.conf
Expand Down
14 changes: 12 additions & 2 deletions files/service/scripts/start-odk.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ set -o pipefail
shopt -s inherit_errexit

# Check for illegal DB_SSL environment variable.
if ! [[ "${DB_SSL-}" = null ]]; then
if [[ -v DB_SSL ]] && ! [[ "$DB_SSL" = "null" ]]; then
echo "!!!"
echo "!!! You have the DB_SSL variable defined (in your .env file, probably)."
echo "!!! This variable is no longer supported from Central 2026.1 onwards."
Expand Down Expand Up @@ -43,7 +43,17 @@ SENTRY_TAGS="{ \"version.central\": \"$(cat sentry-versions/central)\", \"versio
export SENTRY_TAGS

echo "waiting for PostgreSQL to become connectable to..."
while ! (psql --no-password --quiet --command "" > /dev/null 2>&1 || (echo "sleeping 1 second waiting for a database connection"; false)); do sleep 1; done
maxTries=15
retries=$((maxTries-1))
while ! pg_isready; do
if [[ "$retries" = 0 ]]; then
echo "PostgreSQL not available after $maxTries attempts."
exit 1
fi
echo "PostgreSQL not yet available; sleeping 1 second..."
sleep 1
retries=$((retries-1))
done

echo "running migrations.."
node ./lib/bin/run-migrations
Expand Down
6 changes: 5 additions & 1 deletion test/nginx/lib.docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,17 @@ services:
- SENTRY_ORG_SUBDOMAIN=o-fake-dsn
- SENTRY_PROJECT=example-sentry-project
- OIDC_ENABLED=false
- SENTRY_DSN_FRONTEND=https://fake-dsn.fake-sentry
- SENTRY_DSN_FRONTEND=${SENTRY_DSN_FRONTEND-https://abcdef0123456789abcdef0123456789@o-fake-dsn.ingest.sentry.io/1234567890123456}
volumes:
- ../../files/nginx/odk.conf.template:/usr/share/odk/nginx/odk.conf.template:ro
- ../../files/nginx/client-config.json.template:/usr/share/odk/nginx/client-config.json.template:ro
- ./files/nginx/dh.pem:/etc/dh/nginx.pem:ro
healthcheck:
test: [ "CMD-SHELL", "nc -z localhost 80 || exit 1" ]
interval: 1s
timeout: 1s
retries: 10
start_period: 0s
restart: always
logging:
driver: local
Expand Down
1 change: 1 addition & 0 deletions test/nginx/lint-config.sh
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ EOF
log "$service: config looks OK."
}

docker_compose up --detach --wait
lint_service nginx-ssl-selfsign
lint_service nginx-ssl-upstream

Expand Down
6 changes: 3 additions & 3 deletions test/nginx/mock-http-server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@ app.use('/-/', (req, res, next) => {
next();
});

app.get('/health', (req, res) => res.send('OK'));
app.get('/request-log', (req, res) => res.json(requests));
app.get('/reset', (req, res) => {
app.get('/__mock_http_server/health', (req, res) => res.send('OK'));
app.get('/__mock_http_server/request-log', (req, res) => res.json(requests));
app.get('/__mock_http_server/reset', (req, res) => {
requests.length = 0;
res.json('OK');
});
Expand Down
19 changes: 17 additions & 2 deletions test/nginx/mock-sentry/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,12 @@ app.use(express.json({
'application/reports+json', // https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/report-to#violation_report_syntax
],
}));
app.get('/event-log', (req, res) => res.json(events));
app.get('/reset', (req, res) => {
app.get('/__mock_sentry/event-log', (req, res) => res.json(events));
app.get('/__mock_sentry/reset', (req, res) => {
events.length = 0;
res.json('OK');
});

app.use('/api', (req, res, next) => {
log(new Date(), req.method, req.originalUrl);

Expand All @@ -49,6 +50,20 @@ app.use('/api', (req, res, next) => {

next();
});

app.use('/api/:projectId/envelope/', (req, res, next) => {
res.setHeader('Access-Control-Allow-Origin', 'https://odk-nginx.example.test:9001');
res.setHeader('Access-Control-Allow-Methods', 'OPTIONS, POST');

if(req.method === 'OPTIONS') return res.sendStatus(204);

next();
});
app.post('/api/:projectId/envelope/', (req, res) => {
if(req.params.projectId !== '1234567890123456') return res.status(400).send('Unexpected Sentry projectId.');
res.send('envelope:OK');
});

app.get('/api/check-cert', (req, res) => res.send('OK'));
app.post('/api/example-sentry-project/security/', (req, res) => {
const { sentry_key } = req.query;
Expand Down
13 changes: 13 additions & 0 deletions test/nginx/nginx.test.docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,16 @@ services:
- "[::1]:10000:80"
- "127.0.0.1:10001:443"
- "[::1]:10001:443"
nginx-test-setup-odk:
extends:
file: lib.docker-compose.yml
service: nginx
environment:
SSL_TYPE: selfsign
ports:
- "10002:80"
- "10003:443"
volumes:
- ./src/mocha/setup-odk/certs/nginx.pem:/etc/dh/nginx.pem:ro
- ./src/mocha/setup-odk/certs/privkey.pem:/etc/selfsign/live/odk-nginx.example.test/privkey.pem:ro
- ./src/mocha/setup-odk/certs/fullchain.pem:/etc/selfsign/live/odk-nginx.example.test/fullchain.pem:ro
4 changes: 2 additions & 2 deletions test/nginx/setup-tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@ log "Starting test services..."
docker_compose up --build --detach

log "Waiting for mock backend..."
wait_for_http_response 5 localhost:8383/health 200
wait_for_http_response 5 localhost:8383/__mock_http_server/health 200
log "Waiting for mock enketo..."
wait_for_http_response 5 localhost:8005/health 200
wait_for_http_response 5 localhost:8005/__mock_http_server/health 200
log "Waiting for nginx..."
wait_for_http_response 5 localhost:9000 421

Expand Down
4 changes: 2 additions & 2 deletions test/nginx/src/lib.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ module.exports = {
};

async function assertSentryReceived(...expectedRequests) {
const { status, body } = await requestSentryMock({ path:'/event-log' });
const { status, body } = await requestSentryMock({ path:'/__mock_sentry/event-log' });
assert.equal(status, 200);

const actual = JSON.parse(body);
Expand All @@ -27,7 +27,7 @@ async function assertSentryReceived(...expectedRequests) {
}

async function resetSentryMock() {
const res = await requestSentryMock({ path:'/reset' });
const res = await requestSentryMock({ path:'/__mock_sentry/reset' });
assert.equal(res.status, 200);
}

Expand Down
16 changes: 16 additions & 0 deletions test/nginx/src/mocha/mocha.setup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
const http = require('node:http');
const https = require('node:https');

const log = (...args) => console.log('[mocha-setup]', ...args);

module.exports = {
mochaHooks: {
afterAll() {
log('Cleaning up HTTP(S) Response objects whose bodies have not been read...');
http.globalAgent.destroy();
https.globalAgent.destroy();

log('Cleanup complete.');
},
},
};
9 changes: 5 additions & 4 deletions test/nginx/src/mocha/nginx.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ const contentSecurityPolicies = {
],
'connect-src': [
self,
'https://o-fake-dsn.ingest.sentry.io',
],
'font-src': self,
'form-action': self,
Expand Down Expand Up @@ -442,7 +443,7 @@ function standardTestSuite({ fetchHttp, fetchHttp6, apiFetch, apiFetch6, forward

// then
assert.equal(res.status, 200);
assert.deepEqual(await res.json(), { oidcEnabled: false, sentryDsn: 'https://fake-dsn.fake-sentry' });
assert.deepEqual(await res.json(), { oidcEnabled:false, sentryDsn:'https://abcdef0123456789abcdef0123456789@o-fake-dsn.ingest.sentry.io/1234567890123456' });
assertSecurityHeaders(res, { csp:'central-frontend' });
});

Expand All @@ -452,7 +453,7 @@ function standardTestSuite({ fetchHttp, fetchHttp6, apiFetch, apiFetch6, forward

// then
assert.equal(res.status, 200);
assert.deepEqual(await res.json(), { oidcEnabled: false, sentryDsn: 'https://fake-dsn.fake-sentry' });
assert.deepEqual(await res.json(), { oidcEnabled:false, sentryDsn:'https://abcdef0123456789abcdef0123456789@o-fake-dsn.ingest.sentry.io/1234567890123456' });
assertSecurityHeaders(res, { csp:'central-frontend' });
});

Expand Down Expand Up @@ -1162,7 +1163,7 @@ function assertBackendReceived(...expectedRequests) {
}

async function assertMockHttpReceived(port, expectedRequests) {
const res = await request(`http://localhost:${port}/request-log`);
const res = await request(`http://localhost:${port}/__mock_http_server/request-log`);
assert.isTrue(res.ok);
assert.deepEqual(expectedRequests, await res.json());
}
Expand All @@ -1176,7 +1177,7 @@ function resetBackendMock() {
}

async function resetMock(port) {
const res = await request(`http://localhost:${port}/reset`);
const res = await request(`http://localhost:${port}/__mock_http_server/reset`);
assert.isTrue(res.ok);
}

Expand Down
27 changes: 4 additions & 23 deletions test/nginx/src/mocha/request.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
const { isIPv6 } = require('node:net');
const { Readable } = require('node:stream');

module.exports = request;

Expand All @@ -16,29 +15,11 @@ function request(url, { body, ...options }={}) {
const req = getProtocolImplFrom(url).request({ ...options, ...preserve(url) }, res => {
res.on('error', reject);

const body = new Readable({ read:() => {} });
res.on('error', err => body.destroy(err));
res.on('data', data => body.push(data));
res.on('end', () => body.push(null));

const text = () => new Promise((resolve, reject) => {
const chunks = [];
body.on('error', reject);
body.on('data', data => chunks.push(data));
body.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
});

const status = res.statusCode;

resolve({
status,
ok: status >= 200 && status < 300,
statusText: res.statusText,
body,
text,
json: async () => JSON.parse(await text()),
resolve(new Response(res, {
status: res.statusCode,
statusText: res.statusMessage,
headers: new Headers(res.headers),
});
}));
});
req.on('error', reject);
if(body !== undefined) req.write(body);
Expand Down
Loading
Loading