Feature/191 production demo mode interim #192
@@ -48,6 +48,10 @@ const DEPLOYMENTS = [
|
|||||||
{
|
{
|
||||||
file: 'docker-compose.qa.yml',
|
file: 'docker-compose.qa.yml',
|
||||||
demoMode: 'true',
|
demoMode: 'true',
|
||||||
|
// Written into the file rather than supplied by the stack, unlike
|
||||||
|
// production: QA exists to run without PayPal credentials, so there is no
|
||||||
|
// circumstance in which it should be anything else.
|
||||||
|
stackVariables: {},
|
||||||
// The entire safety property of QA: delivery is restricted to named
|
// The entire safety property of QA: delivery is restricted to named
|
||||||
// recipients, so a run against a database full of test fixtures cannot
|
// recipients, so a run against a database full of test fixtures cannot
|
||||||
// email a real customer. Asserted because the compose file claims it must
|
// email a real customer. Asserted because the compose file claims it must
|
||||||
@@ -59,10 +63,20 @@ const DEPLOYMENTS = [
|
|||||||
// TEMPORARY, and #190 restores it to 'false'. Production is in demo mode
|
// TEMPORARY, and #190 restores it to 'false'. Production is in demo mode
|
||||||
// since 2026-08-25 — the stack was brought up during the cutover to this
|
// since 2026-08-25 — the stack was brought up during the cutover to this
|
||||||
// file before the live PayPal credentials were available, and demo mode is
|
// file before the live PayPal credentials were available, and demo mode is
|
||||||
// the sanctioned interim for that. This assertion is what keeps the state
|
// the sanctioned interim for that.
|
||||||
// honest: it fails the moment the compose file and this expectation
|
|
||||||
// disagree, so neither can be changed quietly, in either direction.
|
|
||||||
demoMode: 'true',
|
demoMode: 'true',
|
||||||
|
// Production reads DEMO_MODE from the stack, so the file holds `${DEMO_MODE}`
|
||||||
|
// rather than a value and this is what the stack has to be set to.
|
||||||
|
//
|
||||||
|
// Be clear about what this can and cannot prove. It cannot see Portainer,
|
||||||
|
// so it does not verify the stack actually holds `true` — nothing in this
|
||||||
|
// repository can. What it does is keep the intent written down beside the
|
||||||
|
// file, and make the pair inseparable: change the compose line to a literal
|
||||||
|
// and this disagrees, change this and the boot behaviour it claims no
|
||||||
|
// longer matches. The runtime half is `checkDemoMode`, which refuses to
|
||||||
|
// start on anything that is not exactly `true` or `false`, so a stack that
|
||||||
|
// is unset or mistyped fails loudly rather than falling back to either.
|
||||||
|
stackVariables: { DEMO_MODE: 'true' },
|
||||||
// Deliberately unrestricted. Production has to be able to reach real
|
// Deliberately unrestricted. Production has to be able to reach real
|
||||||
// customers, and it is the one environment where that is correct.
|
// customers, and it is the one environment where that is correct.
|
||||||
requiresMailAllowlist: false
|
requiresMailAllowlist: false
|
||||||
@@ -84,9 +98,29 @@ function resolveDefault(value: string): string {
|
|||||||
return withDefault?.[1] ?? value;
|
return withDefault?.[1] ?? value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* What the container receives for one entry, given what the stack is declared
|
||||||
|
* to set.
|
||||||
|
*
|
||||||
|
* A bare `${VAR}` the deployment names in `stackVariables` resolves to that
|
||||||
|
* value — which is how a file that reads DEMO_MODE from the stack can still be
|
||||||
|
* handed to `validateEnv` and checked as the container will see it. Every other
|
||||||
|
* bare `${VAR}` stays opaque, as before: those are secrets, and what is being
|
||||||
|
* checked of them is that the line exists.
|
||||||
|
*/
|
||||||
|
function resolveEntry(value: string, stackVariables: Readonly<Record<string, string>>): string {
|
||||||
|
const bare = /^\$\{([A-Z_0-9]+)\}$/.exec(value);
|
||||||
|
const name = bare?.[1];
|
||||||
|
const named = name === undefined ? undefined : stackVariables[name];
|
||||||
|
return named ?? resolveDefault(value);
|
||||||
|
}
|
||||||
|
|
||||||
// Only real environment entries — `- NAME=value` at an indented list position.
|
// Only real environment entries — `- NAME=value` at an indented list position.
|
||||||
// A mention inside a comment cannot match, because a comment line starts with #.
|
// A mention inside a comment cannot match, because a comment line starts with #.
|
||||||
function environmentEntries(source: string): Map<string, string> {
|
function environmentEntries(
|
||||||
|
source: string,
|
||||||
|
stackVariables: Readonly<Record<string, string>>
|
||||||
|
): Map<string, string> {
|
||||||
const entries = new Map<string, string>();
|
const entries = new Map<string, string>();
|
||||||
// The split pattern tolerates carriage returns. A checkout with CRLF line
|
// The split pattern tolerates carriage returns. A checkout with CRLF line
|
||||||
// endings, which is every fresh clone on Windows, otherwise leaves a stray
|
// endings, which is every fresh clone on Windows, otherwise leaves a stray
|
||||||
@@ -100,7 +134,7 @@ function environmentEntries(source: string): Map<string, string> {
|
|||||||
// but RegExpExecArray cannot say so, and #101 made the compiler insist.
|
// but RegExpExecArray cannot say so, and #101 made the compiler insist.
|
||||||
const [, name, value] = match ?? [];
|
const [, name, value] = match ?? [];
|
||||||
if (name !== undefined && value !== undefined) {
|
if (name !== undefined && value !== undefined) {
|
||||||
entries.set(name, resolveDefault(value.trim()));
|
entries.set(name, resolveEntry(value.trim(), stackVariables));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return entries;
|
return entries;
|
||||||
@@ -108,7 +142,7 @@ function environmentEntries(source: string): Map<string, string> {
|
|||||||
|
|
||||||
describe.each(DEPLOYMENTS)('$file provides everything the app requires to boot', (deployment) => {
|
describe.each(DEPLOYMENTS)('$file provides everything the app requires to boot', (deployment) => {
|
||||||
const compose = readFileSync(path.join(REPO_ROOT, deployment.file), 'utf8');
|
const compose = readFileSync(path.join(REPO_ROOT, deployment.file), 'utf8');
|
||||||
const entries = environmentEntries(compose);
|
const entries = environmentEntries(compose, deployment.stackVariables);
|
||||||
|
|
||||||
it('parsed some environment entries at all', () => {
|
it('parsed some environment entries at all', () => {
|
||||||
// Guards the guard: a regex that matched nothing would make every
|
// Guards the guard: a regex that matched nothing would make every
|
||||||
|
|||||||
+22
-12
@@ -58,6 +58,12 @@
|
|||||||
# Required stack environment variables — all secrets, all must be set in
|
# Required stack environment variables — all secrets, all must be set in
|
||||||
# Portainer for this stack:
|
# Portainer for this stack:
|
||||||
#
|
#
|
||||||
|
# DEMO_MODE `true` or `false`, exactly. Whether real payments are
|
||||||
|
# taken. Not a secret — it is here rather than written
|
||||||
|
# into the file below because it is the one value that
|
||||||
|
# gets flipped without a code change. There is no
|
||||||
|
# default: unset or mistyped refuses to boot rather than
|
||||||
|
# choosing for you.
|
||||||
# DB_PASSWORD Postgres password for the `redefined` database.
|
# DB_PASSWORD Postgres password for the `redefined` database.
|
||||||
# SMTP_USER Brevo SMTP login.
|
# SMTP_USER Brevo SMTP login.
|
||||||
# SMTP_PASSWORD
|
# SMTP_PASSWORD
|
||||||
@@ -112,24 +118,28 @@ services:
|
|||||||
# on 2026-08-25 to bring the stack up during the cutover to this file
|
# on 2026-08-25 to bring the stack up during the cutover to this file
|
||||||
# before the live PayPal credentials were to hand.
|
# before the live PayPal credentials were to hand.
|
||||||
#
|
#
|
||||||
# Restoring it is #190. Set this back to `false`, fill the three stack
|
# Restoring it is #190: fill the three PayPal stack variables and set the
|
||||||
# variables below, and redeploy. Until then every order placed is a
|
# DEMO_MODE stack variable to `false`. Both are Portainer values now, so
|
||||||
# pretend one.
|
# restoring real payments needs no commit — which is the point, and also
|
||||||
|
# why this banner and the guard test exist to keep the state visible.
|
||||||
|
# Until then every order placed is a pretend one.
|
||||||
# ===================================================================
|
# ===================================================================
|
||||||
#
|
#
|
||||||
# Real payments. This is the difference between production and QA, and it
|
# Real payments. This is the difference between production and QA, and it
|
||||||
# is why the three PayPal secrets are required rather than optional — the
|
# is why the three PayPal secrets are required rather than optional — the
|
||||||
# app refuses to start without them when this is false.
|
# app refuses to start without them when this is false.
|
||||||
#
|
#
|
||||||
# HARDCODED, AND NOT INTERPOLATED, ON PURPOSE. Portainer substitutes stack
|
# Supplied by the stack, and with NO DEFAULT, deliberately.
|
||||||
# variables into this file; it does not hand them to the container. A
|
# `${DEMO_MODE:-false}` is the obvious thing to write and the wrong thing:
|
||||||
# `DEMO_MODE` stack variable therefore does NOTHING — there is no `${...}`
|
# a default decides whether the shop takes money on the operator's behalf,
|
||||||
# here for it to substitute into, and the value below wins silently. That
|
# silently, whichever way it points.
|
||||||
# is deliberate: the one value that decides whether the shop takes money
|
#
|
||||||
# should not be flippable from a web UI without a commit anybody can read.
|
# Having no default is safe rather than fragile, because `checkDemoMode`
|
||||||
# It is also exactly what wasted an hour during the cutover, so it is
|
# is strict. An unset stack variable substitutes to an empty string, and
|
||||||
# written down rather than left to be rediscovered.
|
# anything that is not exactly `true` or `false` refuses to boot naming
|
||||||
- DEMO_MODE=true
|
# DEMO_MODE. So a missing or mistyped value fails loudly at startup
|
||||||
|
# instead of guessing — which is what makes interpolating this one safe.
|
||||||
|
- DEMO_MODE=${DEMO_MODE}
|
||||||
- PAYPAL_ENV=live
|
- PAYPAL_ENV=live
|
||||||
- PAYPAL_CLIENT_ID=${PAYPAL_CLIENT_ID}
|
- PAYPAL_CLIENT_ID=${PAYPAL_CLIENT_ID}
|
||||||
- PAYPAL_CLIENT_SECRET=${PAYPAL_CLIENT_SECRET}
|
- PAYPAL_CLIENT_SECRET=${PAYPAL_CLIENT_SECRET}
|
||||||
|
|||||||
Reference in New Issue
Block a user