From 959d578910292c9ce402fb47e013cffba08ecc99 Mon Sep 17 00:00:00 2001 From: Slawomir Koszewski Date: Sat, 7 Nov 2026 02:03:17 +0100 Subject: [PATCH] fix: a module of the tfvars store says whether it discovers its configurator, as configuratorDiscovery. --- CHANGELOG.md | 10 ++++++++ README.md | 2 +- package-lock.json | 4 ++-- package.json | 2 +- server/sql/tfvars_store.sql | 47 ++++++++++++++++++++++++++++--------- server/tfvarsStore.test.ts | 34 ++++++++++++++++++++++----- server/tfvarsStore.ts | 6 ++--- shared/tfvars.ts | 5 ++++ 8 files changed, 86 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c2c7d2..694483b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## 0.7.2 + +fix: a module of the tfvars store says whether it discovers its configurator, as +configuratorDiscovery. + +- `TfModule` has `configuratorDiscovery`, and `tf_modules` the column `configurator_discovery`: + whether a module without `configurator_url` finds its configurator by the `config.json` of its + folder or one above it. A module registered before keeps it on; `modules.put` sets it. +- `tf_module_put` takes the new argument; its earlier signature is dropped. + ## 0.7.1 fix: a page in the dark color scheme, also one in an iframe, is no longer white while it loads, diff --git a/README.md b/README.md index caa5475..65bc8ae 100644 --- a/README.md +++ b/README.md @@ -178,7 +178,7 @@ which edit their variables. `createTfvarsStore(pool)` takes a `pg` pool and appl | Table | Rows | | --- | --- | -| `tf_modules` | `name`, `root`, the folder of the Terraform code, `description` and `configurator_url` | +| `tf_modules` | `name`, `root`, the folder of the Terraform code, `description`, `configurator_url` and `configurator_discovery`, whether a module without `configurator_url` finds its configurator by the `config.json` of its folder or one above it; a module registered before the column keeps it on, a new one has it off unless given | | `tf_instances` | `module`, `name` and `state_schema`, the schema of the instance's state in the pg backend, unique, and `created` | | `tf_variable_files` | `module`, `instance`, `name` (`*.tfvars.json`), `content` and `previous`, the object it replaced, as `jsonb`, and `updated` | | `tf_variable_revisions` | `module`, `instance`, `revision`, numbered from 1 for each instance, `files`, all its files after a write as a `jsonb` object by name, `schema`, the schema version of their layout or null, `producer` and `saved` | diff --git a/package-lock.json b/package-lock.json index 6581969..8441fdc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@koszewscy/sk-react-toolkit", - "version": "0.7.1", + "version": "0.7.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@koszewscy/sk-react-toolkit", - "version": "0.7.1", + "version": "0.7.2", "devDependencies": { "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.1", diff --git a/package.json b/package.json index 3cdc2f4..e987812 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@koszewscy/sk-react-toolkit", - "version": "0.7.1", + "version": "0.7.2", "description": "Shared code of the React and Fastify web applications: web components, server helpers and the Vite font and color scheme plugins.", "private": true, "type": "module", diff --git a/server/sql/tfvars_store.sql b/server/sql/tfvars_store.sql index e9ac616..3a41f29 100644 --- a/server/sql/tfvars_store.sql +++ b/server/sql/tfvars_store.sql @@ -4,12 +4,24 @@ -- again. CREATE TABLE IF NOT EXISTS tf_modules ( - name text PRIMARY KEY CHECK (name ~ '^[a-z][a-z0-9-]*$'), - root text NOT NULL CHECK (root <> ''), - description text NOT NULL DEFAULT '', - configurator_url text + name text PRIMARY KEY CHECK (name ~ '^[a-z][a-z0-9-]*$'), + root text NOT NULL CHECK (root <> ''), + description text NOT NULL DEFAULT '', + configurator_url text, + configurator_discovery boolean NOT NULL DEFAULT false ); +-- A module registered before configurator_discovery keeps finding its configurator. +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM information_schema.columns + WHERE table_schema = current_schema() AND table_name = 'tf_modules' AND column_name = 'configurator_discovery') THEN + ALTER TABLE tf_modules ADD COLUMN configurator_discovery boolean NOT NULL DEFAULT true; + ALTER TABLE tf_modules ALTER COLUMN configurator_discovery SET DEFAULT false; + END IF; +END +$$; + CREATE TABLE IF NOT EXISTS tf_instances ( module text NOT NULL REFERENCES tf_modules (name), name text NOT NULL CHECK (name ~ '^[a-z][a-z0-9-]*$'), @@ -51,23 +63,36 @@ WHERE NOT EXISTS (SELECT 1 FROM tf_variable_revisions r WHERE r.module = f.modul GROUP BY f.module, f.instance ON CONFLICT DO NOTHING; --- The modules, by name. +-- The modules, by name; its columns are those of the table, so a table of other columns replaces it. +DO $$ +BEGIN + IF EXISTS (SELECT 1 FROM pg_proc p JOIN pg_namespace n ON n.oid = p.pronamespace + WHERE n.nspname = current_schema() AND p.proname = 'tf_modules_list' + AND NOT 'configurator_discovery' = ANY (coalesce(p.proargnames, '{}'))) THEN + DROP FUNCTION tf_modules_list(); + END IF; +END +$$; + CREATE OR REPLACE FUNCTION tf_modules_list() -RETURNS TABLE (name text, root text, description text, configurator_url text) +RETURNS TABLE (name text, root text, description text, configurator_url text, configurator_discovery boolean) LANGUAGE sql STABLE SET search_path FROM CURRENT AS $$ - SELECT m.name, m.root, m.description, m.configurator_url FROM tf_modules m ORDER BY m.name + SELECT m.name, m.root, m.description, m.configurator_url, m.configurator_discovery FROM tf_modules m ORDER BY m.name $$; +DROP PROCEDURE IF EXISTS tf_module_put(text, text, text, text); + -- Creates a module, or changes the one of that name. -CREATE OR REPLACE PROCEDURE tf_module_put(module_name text, module_root text, module_description text, module_configurator_url text) +CREATE OR REPLACE PROCEDURE tf_module_put(module_name text, module_root text, module_description text, module_configurator_url text, module_configurator_discovery boolean) LANGUAGE sql SET search_path FROM CURRENT AS $$ - INSERT INTO tf_modules (name, root, description, configurator_url) - VALUES (module_name, module_root, module_description, module_configurator_url) - ON CONFLICT (name) DO UPDATE SET root = excluded.root, description = excluded.description, configurator_url = excluded.configurator_url + INSERT INTO tf_modules (name, root, description, configurator_url, configurator_discovery) + VALUES (module_name, module_root, module_description, module_configurator_url, module_configurator_discovery) + ON CONFLICT (name) DO UPDATE SET root = excluded.root, description = excluded.description, + configurator_url = excluded.configurator_url, configurator_discovery = excluded.configurator_discovery $$; -- Deletes a module; the foreign key of tf_instances refuses it while the module has instances. diff --git a/server/tfvarsStore.test.ts b/server/tfvarsStore.test.ts index 2b636c8..18bac69 100644 --- a/server/tfvarsStore.test.ts +++ b/server/tfvarsStore.test.ts @@ -35,16 +35,38 @@ const status = async (promise: Promise) => { } test('modules are created, changed, listed and deleted', async () => { - await store.modules.put({ name: 'router', root: 'router/terraform', description: 'VPN router', configuratorUrl: null }) - await store.modules.put({ name: 'router', root: 'router/terraform', description: 'VPN router', configuratorUrl: 'http://localhost:7083/' }) - await store.modules.put({ name: 'app', root: 'app', description: '', configuratorUrl: null }) + await store.modules.put({ name: 'router', root: 'router/terraform', description: 'VPN router', configuratorUrl: null, configuratorDiscovery: true }) + await store.modules.put({ name: 'router', root: 'router/terraform', description: 'VPN router', configuratorUrl: 'http://localhost:7083/', configuratorDiscovery: false }) + await store.modules.put({ name: 'app', root: 'app', description: '', configuratorUrl: null, configuratorDiscovery: true }) assert.deepEqual(await store.modules.list(), [ - { name: 'app', root: 'app', description: '', configuratorUrl: null }, - { name: 'router', root: 'router/terraform', description: 'VPN router', configuratorUrl: 'http://localhost:7083/' }, + { name: 'app', root: 'app', description: '', configuratorUrl: null, configuratorDiscovery: true }, + { name: 'router', root: 'router/terraform', description: 'VPN router', configuratorUrl: 'http://localhost:7083/', configuratorDiscovery: false }, ]) await store.modules.delete('app') assert.deepEqual((await store.modules.list()).map((module) => module.name), ['router']) - assert.equal(await status(store.modules.put({ name: 'Bad', root: 'x', description: '', configuratorUrl: null })), 400) + assert.equal(await status(store.modules.put({ name: 'Bad', root: 'x', description: '', configuratorUrl: null, configuratorDiscovery: false })), 400) +}) + +test('a module registered before configurator_discovery keeps discovering its configurator; a new one does not', async () => { + // The table, the function and the procedure as they were before the column, in a schema of their own. + await pool.query(`CREATE SCHEMA earlier; + CREATE TABLE earlier.tf_modules (name text PRIMARY KEY, root text NOT NULL, description text NOT NULL DEFAULT '', configurator_url text); + INSERT INTO earlier.tf_modules VALUES ('router', 'router/terraform', '', NULL); + CREATE FUNCTION earlier.tf_modules_list() RETURNS TABLE (name text, root text, description text, configurator_url text) + LANGUAGE sql AS $$ SELECT m.name, m.root, m.description, m.configurator_url FROM earlier.tf_modules m $$; + CREATE PROCEDURE earlier.tf_module_put(a text, b text, c text, d text) LANGUAGE sql AS $$ SELECT 1 $$;`) + const earlierPool = new pg.Pool({ database, options: '-c search_path=earlier' }) + try { + const earlier = createTfvarsStore(earlierPool) + await earlier.modules.put({ name: 'app', root: 'app', description: '', configuratorUrl: null, configuratorDiscovery: false }) + assert.deepEqual((await earlier.modules.list()).map((module) => [module.name, module.configuratorDiscovery]), [['app', false], ['router', true]]) + const { rows } = await earlierPool.query("SELECT count(*)::int AS count FROM pg_proc p JOIN pg_namespace n ON n.oid = p.pronamespace WHERE n.nspname = 'earlier' AND p.proname = 'tf_module_put'") + assert.equal(rows[0].count, 1) + // Another store applies the file again over the migrated schema, which keeps the modules as they are. + assert.deepEqual((await createTfvarsStore(earlierPool).modules.list()).map((module) => [module.name, module.configuratorDiscovery]), [['app', false], ['router', true]]) + } finally { + await earlierPool.end() + } }) test('instances are created and refused for a missing module or a name in use', async () => { diff --git a/server/tfvarsStore.ts b/server/tfvarsStore.ts index 35ced86..7d54ad1 100644 --- a/server/tfvarsStore.ts +++ b/server/tfvarsStore.ts @@ -152,10 +152,10 @@ export function createTfvarsStore(pool: Pool): TfvarsStore { } return { modules: { - list: async () => (await query<{ name: string; root: string; description: string; configurator_url: string | null }>('SELECT * FROM tf_modules_list()', [])) - .map((row) => ({ name: row.name, root: row.root, description: row.description, configuratorUrl: row.configurator_url })), + list: async () => (await query<{ name: string; root: string; description: string; configurator_url: string | null; configurator_discovery: boolean }>('SELECT * FROM tf_modules_list()', [])) + .map((row) => ({ name: row.name, root: row.root, description: row.description, configuratorUrl: row.configurator_url, configuratorDiscovery: row.configurator_discovery })), async put(module) { - await query('CALL tf_module_put($1, $2, $3, $4)', [module.name, module.root, module.description, module.configuratorUrl]) + await query('CALL tf_module_put($1, $2, $3, $4, $5)', [module.name, module.root, module.description, module.configuratorUrl, module.configuratorDiscovery]) }, async delete(name) { await query('CALL tf_module_delete($1)', [name]) diff --git a/shared/tfvars.ts b/shared/tfvars.ts index 9f13da8..6fc5281 100644 --- a/shared/tfvars.ts +++ b/shared/tfvars.ts @@ -5,6 +5,11 @@ export type TfModule = { description: string /** The address of the module's configurator, the application that edits the variables of its instances. */ configuratorUrl: string | null + /** + * Whether a module without configuratorUrl finds its configurator by the config.json of its folder or one above it; + * without either, the module has no configurator. + */ + configuratorDiscovery: boolean } /** A deployment of a module, whose state is kept in the schema stateSchema of the pg backend. */