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
18 changes: 13 additions & 5 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
FROM ubuntu:26.04

ARG NODE_VERSION=26.7.0
ARG TARGETARCH=amd64
ARG TARGETARCH

ENV NODE_ENV=production \
HOST=0.0.0.0 \
Expand All @@ -25,25 +25,33 @@ SHELL ["/bin/bash", "-o", "pipefail", "-c"]
# Ubuntu package manager. TARGETARCH keeps the image usable on the OCI
# architectures supported by the Node.js release.
RUN set -eux; \
case "${TARGETARCH}" in \
debian_arch="$(dpkg --print-architecture)"; \
target_arch="${TARGETARCH:-${debian_arch}}"; \
if [ "${target_arch}" != "${debian_arch}" ]; then \
echo "TARGETARCH ${target_arch} does not match the Ubuntu target architecture ${debian_arch}" >&2; \
exit 1; \
fi; \
case "${target_arch}" in \
amd64) node_arch='x64' ;; \
arm64) node_arch='arm64' ;; \
arm) node_arch='armv7l' ;; \
ppc64le) node_arch='ppc64le' ;; \
s390x) node_arch='s390x' ;; \
*) echo "Unsupported TARGETARCH: ${TARGETARCH}" >&2; exit 1 ;; \
*) echo "Unsupported TARGETARCH: ${target_arch}" >&2; exit 1 ;; \
esac; \
export DEBIAN_FRONTEND=noninteractive; \
apt-get update; \
apt-get upgrade -y; \
apt-get install -y --no-install-recommends ca-certificates curl xz-utils; \
apt-get install -y --no-install-recommends ca-certificates curl libatomic1 xz-utils; \
curl --fail --silent --show-error --location \
"https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-${node_arch}.tar.xz" \
--output /tmp/node.tar.xz; \
curl --fail --silent --show-error --location \
"https://nodejs.org/dist/v${NODE_VERSION}/SHASUMS256.txt" \
--output /tmp/SHASUMS256.txt; \
grep -F " node-v${NODE_VERSION}-linux-${node_arch}.tar.xz" /tmp/SHASUMS256.txt | sha256sum --check --status -; \
grep -F " node-v${NODE_VERSION}-linux-${node_arch}.tar.xz" /tmp/SHASUMS256.txt | \
sed "s# node-v${NODE_VERSION}-linux-${node_arch}.tar.xz# /tmp/node.tar.xz#" | \
sha256sum --check --status -; \
tar --extract --file /tmp/node.tar.xz --directory /usr/local --strip-components=1; \
rm -f /tmp/node.tar.xz /tmp/SHASUMS256.txt; \
apt-get purge -y --auto-remove curl xz-utils; \
Expand Down
11 changes: 9 additions & 2 deletions scripts/m0-smoke.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ function Invoke-Compose {
$exitCode = $LASTEXITCODE
if ($exitCode -ne 0) {
$renderedArguments = $commandArguments -join ' '
throw "Docker Compose command failed with exit code \${exitCode}: docker $renderedArguments"
throw "Docker Compose command failed with exit code ${exitCode}: docker $renderedArguments"
}
}

Expand All @@ -63,7 +63,7 @@ function Get-ComposeOutput {
$exitCode = $LASTEXITCODE
if ($exitCode -ne 0) {
$renderedArguments = $commandArguments -join ' '
throw "Docker Compose command failed with exit code \${exitCode}: docker $renderedArguments"
throw "Docker Compose command failed with exit code ${exitCode}: docker $renderedArguments"
}
return @($output)
}
Expand Down Expand Up @@ -104,6 +104,13 @@ function Write-ComposeFailureContext {
}
$statusArguments += @('ps', '-a')
& docker @statusArguments
Write-Host 'Docker Compose logs:'
$logArguments = @('compose', '--project-name', $ProjectName, '--file', $resolvedComposeFile)
if (-not [string]::IsNullOrWhiteSpace($resolvedEnvFile)) {
$logArguments += @('--env-file', $resolvedEnvFile)
}
$logArguments += @('logs', '--no-color', '--tail', '100', 'gulogulo')
& docker @logArguments
}
catch {
Write-Warning "Unable to collect Docker Compose status: $($_.Exception.Message)"
Expand Down
20 changes: 20 additions & 0 deletions src/foundation/config.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,26 @@ test('configuration defaults are versioned, deterministic, and secret-free', ()
assert.equal(JSON.stringify(first).includes('secret'), false);
});

test('the production process environment is accepted by the loader', () => {
const configuration = loadConfiguration(process.env, { configFilePath: null });

assert.equal(configuration.schemaVersion, CONFIG_SCHEMA_VERSION);
assert.equal(configuration.project.machineName, 'gulogulo');
});

test('empty Compose secret-reference placeholders remain unset', () => {
const configuration = loadConfiguration(
{
LDAP_BIND_SECRET_REF: '',
POSTGRES_DSN_SECRET_REF: '',
},
{ configFilePath: null },
);

assert.equal(configuration.ldap.bindSecretRef, null);
assert.equal(configuration.postgres.dsnSecretRef, null);
});

test('environment variables override mounted file values with canonical names first', () => {
withConfigFile(
{
Expand Down
26 changes: 21 additions & 5 deletions src/runtime/config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,14 @@ function readSecretReference(value, name) {
return readString(value, name, SECRET_REFERENCE_PATTERN);
}

function readEnvironmentOptionalSecretReference(value, name) {
if (value === '') {
return null;
}

return readSecretReference(value, name);
}

function readDistinguishedName(value, name) {
return readString(value, name, DISTINGUISHED_NAME_PATTERN);
}
Expand Down Expand Up @@ -478,7 +486,7 @@ function buildConfiguration(fileConfiguration, environment) {
applyEnvironmentValue(target, 'enabled', environment, variables.enabled, readEnvironmentBoolean);
applyEnvironmentValue(target, 'url', environment, variables.url, readUrl);
applyEnvironmentValue(target, 'startTls', environment, variables.startTls, readEnvironmentBoolean);
applyEnvironmentValue(target, 'bindSecretRef', environment, variables.bindSecretRef, readSecretReference);
applyEnvironmentValue(target, 'bindSecretRef', environment, variables.bindSecretRef, readEnvironmentOptionalSecretReference);
applyEnvironmentValue(target, 'userBaseDn', environment, variables.userBaseDn, readDistinguishedName);
applyEnvironmentValue(target, 'connectTimeoutMs', environment, variables.connectTimeoutMs, (value, name) => readEnvironmentInteger(value, name, 100, 120_000));
continue;
Expand All @@ -491,7 +499,7 @@ function buildConfiguration(fileConfiguration, environment) {
applyEnvironmentValue(target, 'database', environment, variables.database, (value, name) => readEnvironmentString(value, name, SAFE_NAME_PATTERN));
applyEnvironmentValue(target, 'user', environment, variables.user, (value, name) => readEnvironmentString(value, name, SAFE_NAME_PATTERN));
applyEnvironmentValue(target, 'sslMode', environment, variables.sslMode, (value, name) => readEnvironmentEnum(value, name, ['disable', 'allow', 'prefer', 'require', 'verify-ca', 'verify-full']));
applyEnvironmentValue(target, 'dsnSecretRef', environment, variables.dsnSecretRef, readSecretReference);
applyEnvironmentValue(target, 'dsnSecretRef', environment, variables.dsnSecretRef, readEnvironmentOptionalSecretReference);
applyEnvironmentValue(target, 'connectTimeoutMs', environment, variables.connectTimeoutMs, (value, name) => readEnvironmentInteger(value, name, 100, 120_000));
continue;
}
Expand Down Expand Up @@ -629,15 +637,23 @@ function attachContractMetadata(legacyConfig, fullConfig) {
* is mandatory and fails closed when it cannot be read or validated.
*/
export function loadConfiguration(environment = process.env, options = {}) {
assertObject(environment, 'environment');
// Node exposes process.env as an environment-backed object rather than a
// normal plain object. Copy the default process environment before schema
// validation so the production entry point and direct test callers use the
// same contract without weakening the configuration-file checks.
const effectiveEnvironment = environment === process.env
? Object.fromEntries(Object.entries(environment))
: environment;

assertObject(effectiveEnvironment, 'environment');
assertObject(options, 'options');

const resolvedFile = resolveConfigurationFile(environment, options);
const resolvedFile = resolveConfigurationFile(effectiveEnvironment, options);
const fileConfiguration = readConfigurationFile(resolvedFile.path, {
optional: resolvedFile.optional,
readFile: options.readFile,
});
const configuration = buildConfiguration(fileConfiguration, environment);
const configuration = buildConfiguration(fileConfiguration, effectiveEnvironment);
return deepFreeze(configuration);
}

Expand Down