fix(kubernetes): support localized template schema
Update migration, runtime grants, sandbox seed, and live upgrade guidance for the normalized email template schema. Ref: IT-1601
This commit is contained in:
@@ -167,7 +167,8 @@ existing Dockerfile. Record the checked commit and archive SHA256, including any
|
|||||||
explicitly recorded uncommitted delta. Mirror PostgreSQL, RabbitMQ, Mailpit and a
|
explicitly recorded uncommitted delta. Mirror PostgreSQL, RabbitMQ, Mailpit and a
|
||||||
Python 3 Linux image to `registry.hrynco.com`. Copy `artifacts.example.json` to
|
Python 3 Linux image to `registry.hrynco.com`. Copy `artifacts.example.json` to
|
||||||
`artifacts.local.json` and record all seven immutable image digests, source hashes
|
`artifacts.local.json` and record all seven immutable image digests, source hashes
|
||||||
and the two reviewed migration IDs. The script rejects placeholder/tag references,
|
and the three reviewed migration IDs, ending with `20261017072410_EmailTemplateLocalizations`.
|
||||||
|
The script rejects placeholder/tag references,
|
||||||
other registries, mismatched application/migrator archives and unsupported DB/broker
|
other registries, mismatched application/migrator archives and unsupported DB/broker
|
||||||
major versions. The archive hash fields are an operator provenance record; independently
|
major versions. The archive hash fields are an operator provenance record; independently
|
||||||
verify the build inputs and registry digests before live application.
|
verify the build inputs and registry digests before live application.
|
||||||
@@ -255,13 +256,15 @@ python3 notification.py render --config config.local.json --artifacts artifacts.
|
|||||||
python3 notification.py check --config config.local.json --artifacts artifacts.local.json
|
python3 notification.py check --config config.local.json --artifacts artifacts.local.json
|
||||||
```
|
```
|
||||||
|
|
||||||
5. First install applies storage/isolated foundation and runs migrations with Web and
|
5. First install or a schema upgrade applies storage/isolated foundation and runs
|
||||||
Worker stopped. The migration target is exactly `InitialCreate` + `PendingChanges`.
|
migrations with Web and Worker stopped. The migration target is exactly
|
||||||
|
`InitialCreate` + `PendingChanges` + `EmailTemplateLocalizations`.
|
||||||
After migration success, the script verifies history, grants a separate
|
After migration success, the script verifies history, grants a separate
|
||||||
`notification_runtime` role only DML on the three application tables, and idempotently
|
`notification_runtime` role only DML on the application tables, including the
|
||||||
seeds the Mailpit channel and neutral English `TestEmail`. Existing matching-service
|
service/language/definition catalog, and idempotently seeds the Mailpit channel,
|
||||||
channels/templates are left unchanged. It resumes Web after these gates; Worker
|
sandbox service/catalog and neutral English `TestEmail`. Existing matching-service
|
||||||
remains stopped for inspection:
|
channels/templates and shared variable contracts are left unchanged. It resumes
|
||||||
|
Web after these gates; Worker remains stopped for inspection:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
python3 notification.py deploy --config config.local.json --artifacts artifacts.local.json --migrate
|
python3 notification.py deploy --config config.local.json --artifacts artifacts.local.json --migrate
|
||||||
@@ -320,6 +323,18 @@ a snapshot of RabbitMQ's queue files. Migration does not upgrade/recreate the br
|
|||||||
volume; durable queues remain on that retained volume. Broker disk backup/recovery
|
volume; durable queues remain on that retained volume. Broker disk backup/recovery
|
||||||
must be verified before a broker upgrade or storage migration.
|
must be verified before a broker upgrade or storage migration.
|
||||||
|
|
||||||
|
Before upgrading a live flat database to `EmailTemplateLocalizations`, inspect the
|
||||||
|
exact source columns/indexes/history and audit every template's identities and JSON
|
||||||
|
variable shape. Versions of one service/key must have the same variable contract
|
||||||
|
after sorting by variable name. Rehearse the current Migrator and sandbox/grants SQL
|
||||||
|
on a disposable restored PostgreSQL 17 copy, then verify preserved content and a
|
||||||
|
repeat no-op migration. The live `deploy --migrate` creates its own final backup after
|
||||||
|
stopping Web and Worker. Do not use the old Web/Worker images with the new schema.
|
||||||
|
Rollback requires restoring a verified pre-upgrade database backup with the matching
|
||||||
|
old application images; migration `Down` is not a general rollback path. The
|
||||||
|
production network add-on and production broker access remain separate, preserved
|
||||||
|
resources; verify them and the existing production queues before re-enabling Worker.
|
||||||
|
|
||||||
## Access and networking
|
## Access and networking
|
||||||
|
|
||||||
Use SSH to the deployment host, then forward one local port when needed:
|
Use SSH to the deployment host, then forward one local port when needed:
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"sourceCommit": "REPLACE_WITH_CHECKED_SOURCE_COMMIT",
|
"sourceCommit": "REPLACE_WITH_CHECKED_SOURCE_COMMIT",
|
||||||
"sourceArchiveSha256": "REPLACE_WITH_VERIFIED_SOURCE_ARCHIVE_SHA256",
|
"sourceArchiveSha256": "REPLACE_WITH_VERIFIED_SOURCE_ARCHIVE_SHA256",
|
||||||
"migrations": ["20260501214629_InitialCreate", "20260502154249_PendingChanges"],
|
"migrations": ["20260501214629_InitialCreate", "20260502154249_PendingChanges", "20261017072410_EmailTemplateLocalizations"],
|
||||||
"images": {
|
"images": {
|
||||||
"web": {"image": "registry.hrynco.com/hrynco.notificationservice.web@sha256:REPLACE", "sourceArchiveSha256": "REPLACE_WITH_VERIFIED_SOURCE_ARCHIVE_SHA256"},
|
"web": {"image": "registry.hrynco.com/hrynco.notificationservice.web@sha256:REPLACE", "sourceArchiveSha256": "REPLACE_WITH_VERIFIED_SOURCE_ARCHIVE_SHA256"},
|
||||||
"worker": {"image": "registry.hrynco.com/hrynco.notificationservice.worker@sha256:REPLACE", "sourceArchiveSha256": "REPLACE_WITH_VERIFIED_SOURCE_ARCHIVE_SHA256"},
|
"worker": {"image": "registry.hrynco.com/hrynco.notificationservice.worker@sha256:REPLACE", "sourceArchiveSha256": "REPLACE_WITH_VERIFIED_SOURCE_ARCHIVE_SHA256"},
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ import time
|
|||||||
|
|
||||||
NAMESPACE = "shared-notification"
|
NAMESPACE = "shared-notification"
|
||||||
MANAGED = "hrynco-shared-notification"
|
MANAGED = "hrynco-shared-notification"
|
||||||
MIGRATIONS = ["20260501214629_InitialCreate", "20260502154249_PendingChanges"]
|
MIGRATIONS = ["20260501214629_InitialCreate", "20260502154249_PendingChanges",
|
||||||
|
"20261017072410_EmailTemplateLocalizations"]
|
||||||
IMAGE = re.compile(r"registry\.hrynco\.com/[a-z0-9._/-]+@sha256:[a-f0-9]{64}$")
|
IMAGE = re.compile(r"registry\.hrynco\.com/[a-z0-9._/-]+@sha256:[a-f0-9]{64}$")
|
||||||
SHA = re.compile(r"[a-f0-9]{64}$")
|
SHA = re.compile(r"[a-f0-9]{64}$")
|
||||||
PRODUCTION_RESULT_QUEUE = "invemory.production.notification-results"
|
PRODUCTION_RESULT_QUEUE = "invemory.production.notification-results"
|
||||||
@@ -53,7 +54,8 @@ REVOKE ALL ON DATABASE notification_service FROM PUBLIC;
|
|||||||
GRANT CONNECT ON DATABASE notification_service TO notification_runtime;
|
GRANT CONNECT ON DATABASE notification_service TO notification_runtime;
|
||||||
REVOKE CREATE ON SCHEMA public FROM PUBLIC;
|
REVOKE CREATE ON SCHEMA public FROM PUBLIC;
|
||||||
GRANT USAGE ON SCHEMA public TO notification_runtime;
|
GRANT USAGE ON SCHEMA public TO notification_runtime;
|
||||||
GRANT SELECT, INSERT, UPDATE, DELETE ON email_channels, email_templates, email_channel_usage TO notification_runtime;
|
GRANT SELECT, INSERT, UPDATE, DELETE ON email_channels, email_templates, email_channel_usage,
|
||||||
|
services, service_languages, email_template_definitions TO notification_runtime;
|
||||||
'''
|
'''
|
||||||
|
|
||||||
SANDBOX = r'''
|
SANDBOX = r'''
|
||||||
@@ -62,11 +64,24 @@ INSERT INTO email_channels (id, service_name, priority, provider_type, settings,
|
|||||||
SELECT gen_random_uuid(), 'Invemory.Staging.Sandbox', 1, 1,
|
SELECT gen_random_uuid(), 'Invemory.Staging.Sandbox', 1, 1,
|
||||||
'{"EmailChannelType":1,"Host":"mailpit","Port":1025,"Username":"","Password":"","UseSsl":false,"FromEmail":"no-reply@local.hrynco.test","FromName":"Invemory Sandbox"}'::jsonb, 90, true, now()
|
'{"EmailChannelType":1,"Host":"mailpit","Port":1025,"Username":"","Password":"","UseSsl":false,"FromEmail":"no-reply@local.hrynco.test","FromName":"Invemory Sandbox"}'::jsonb, 90, true, now()
|
||||||
WHERE NOT EXISTS (SELECT 1 FROM email_channels WHERE service_name = 'Invemory.Staging.Sandbox');
|
WHERE NOT EXISTS (SELECT 1 FROM email_channels WHERE service_name = 'Invemory.Staging.Sandbox');
|
||||||
INSERT INTO email_templates (id, service_name, key, language_code, subject, html_body, text_body, variables, created)
|
INSERT INTO services (id, name, revision, created)
|
||||||
VALUES (gen_random_uuid(), 'Invemory.Staging.Sandbox', 'TestEmail', 'en', 'Test notification',
|
VALUES (gen_random_uuid(), 'Invemory.Staging.Sandbox', gen_random_uuid(), now())
|
||||||
'<p>Hello {{RecipientName}},</p><p>{{Message}}</p>', 'Hello {{RecipientName}}, {{Message}}',
|
ON CONFLICT (name) DO NOTHING;
|
||||||
'[{"name":"RecipientName","required":true},{"name":"Message","required":true}]'::jsonb, now())
|
INSERT INTO service_languages (id, service_id, code, created)
|
||||||
ON CONFLICT (service_name, key, language_code) DO NOTHING;
|
SELECT gen_random_uuid(), s.id, 'en', now() FROM services s WHERE s.name = 'Invemory.Staging.Sandbox'
|
||||||
|
ON CONFLICT (service_id, code) DO NOTHING;
|
||||||
|
INSERT INTO email_template_definitions (id, service_id, key, revision, variables, created)
|
||||||
|
SELECT gen_random_uuid(), s.id, 'TestEmail', gen_random_uuid(),
|
||||||
|
'[{"name":"RecipientName","required":true},{"name":"Message","required":true}]'::jsonb, now()
|
||||||
|
FROM services s WHERE s.name = 'Invemory.Staging.Sandbox'
|
||||||
|
ON CONFLICT (service_id, key) DO NOTHING;
|
||||||
|
INSERT INTO email_templates (id, definition_id, service_id, service_language_id, subject, html_body, text_body, created)
|
||||||
|
SELECT gen_random_uuid(), d.id, s.id, l.id, 'Test notification',
|
||||||
|
'<p>Hello {{RecipientName}},</p><p>{{Message}}</p>', 'Hello {{RecipientName}}, {{Message}}', now()
|
||||||
|
FROM services s JOIN service_languages l ON l.service_id = s.id AND l.code = 'en'
|
||||||
|
JOIN email_template_definitions d ON d.service_id = s.id AND d.key = 'TestEmail'
|
||||||
|
WHERE s.name = 'Invemory.Staging.Sandbox'
|
||||||
|
ON CONFLICT (definition_id, service_language_id) DO NOTHING;
|
||||||
COMMIT;
|
COMMIT;
|
||||||
'''
|
'''
|
||||||
|
|
||||||
@@ -75,9 +90,12 @@ SANDBOX_CHECK = """SELECT
|
|||||||
settings->>'Host' = 'mailpit' AND settings->>'Port' = '1025' AND
|
settings->>'Host' = 'mailpit' AND settings->>'Port' = '1025' AND
|
||||||
settings->>'UseSsl' = 'false' AND settings->>'Username' = '' AND settings->>'Password' = '')
|
settings->>'UseSsl' = 'false' AND settings->>'Username' = '' AND settings->>'Password' = '')
|
||||||
FROM email_channels WHERE service_name = 'Invemory.Staging.Sandbox') AND
|
FROM email_channels WHERE service_name = 'Invemory.Staging.Sandbox') AND
|
||||||
EXISTS (SELECT 1 FROM email_templates WHERE service_name = 'Invemory.Staging.Sandbox'
|
EXISTS (SELECT 1 FROM email_templates t
|
||||||
AND key = 'TestEmail' AND language_code = 'en'
|
JOIN email_template_definitions d ON d.id = t.definition_id AND d.service_id = t.service_id
|
||||||
AND variables @> '[{"name":"RecipientName","required":true},{"name":"Message","required":true}]'::jsonb)"""
|
JOIN service_languages l ON l.id = t.service_language_id AND l.service_id = t.service_id
|
||||||
|
JOIN services s ON s.id = t.service_id
|
||||||
|
WHERE s.name = 'Invemory.Staging.Sandbox' AND d.key = 'TestEmail' AND l.code = 'en'
|
||||||
|
AND d.variables @> '[{"name":"RecipientName","required":true},{"name":"Message","required":true}]'::jsonb)"""
|
||||||
|
|
||||||
|
|
||||||
def read_json(path):
|
def read_json(path):
|
||||||
@@ -640,7 +658,7 @@ def deploy(cluster, config, artifacts, args):
|
|||||||
except Exception:
|
except Exception:
|
||||||
cluster.scale("worker", 0)
|
cluster.scale("worker", 0)
|
||||||
raise
|
raise
|
||||||
print("deployment complete; SMTP is sandbox-only, Invemory publisher cutover is separate")
|
print("deployment complete; verify configured SMTP channels and publisher routes separately")
|
||||||
|
|
||||||
|
|
||||||
def main(argv=None):
|
def main(argv=None):
|
||||||
|
|||||||
Reference in New Issue
Block a user