feature: every write of an instance's variable files is kept as a revision with the schema version of their layout and the application that wrote them.
Release / release (push) Successful in 1m8s

This commit is contained in:
slawek committed 2026-11-06 22:22:35 +01:00
1 parent 7c335c7ed2
commit 5e62e143b6
11 files changed
+286 -37

No files matched your search

+22
View File
@@ -135,3 +135,25 @@ exporting them too.
Consequences: An application's shared API types import from `/shared` without the code of either
side. A type added to `shared/` is exported from `/shared` as well as from the side that uses it.
## ADR-7: Every write of an instance's variable files is a revision with its schema and producer
Status: Accepted
Context: A configurator changes the layout of the variable files it writes from one version to
the next, and Cloud Platform has to tell whether an instance's files are of the layout its
module's configurator writes before it runs Terraform with them. The files written earlier have
to stay available, to restore or compare them.
Decision: `tf_variable_revisions` holds every write of an instance's files: all files after the
write, the schema version of their layout as the writer gives it, null when it does not know it,
the producer, the writing application with its version, and the time. `write` and `remove` take
the schema and the producer and record the revision in the transaction of the change, under the
lock of the files; a write of no file records none. The current files stay in `tf_variable_files`,
so reading them and their version is unchanged. Revisions are kept until removed by hand with
SQL. Files without revisions are recorded as revision 1 of an unknown producer and schema when the
store is first used.
Consequences: The schema of an instance's files is the schema of its newest revision; files
imported without a schema, or written before revisions, have none until a configurator writes
them. Revisions grow with every write; the README describes the SQL that removes them.
+17
View File
@@ -1,5 +1,22 @@
# Changelog
## 0.7.0
feature: every write of an instance's variable files is kept as a revision with the schema version
of their layout and the application that wrote them.
- `tf_variable_revisions` keeps all files of an instance after each write, with `schema`, null when
not known, `producer` and `saved`; revisions are numbered from 1 for each instance and kept until
removed by hand.
- `write(version, contents, meta)` and `remove(version, name, meta)` take `meta`, `{ schema,
producer }`, and record the revision in the same transaction; a write of no file records none.
- `read()` answers the newest revision as `revision`; `revisions()` lists them, newest first, and
`revision(n)` answers one with its files. `TfvarsRevision` is exported from `/server` and
`/shared`.
- The files of an instance without revisions become its revision 1, of the producer `unknown` and
no schema.
- The README describes the SQL that purges some or all revisions of a module's instance.
## 0.6.0-2
fix: release.mjs reads a summary wrapped over several lines.
+37 -5
View File
@@ -180,6 +180,7 @@ which edit their variables. `createTfvarsStore(pool)` takes a `pg` pool and appl
| `tf_modules` | `name`, `root`, the folder of the Terraform code, `description` and `configurator_url` |
| `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` |
Names of modules and instances are `TF_NAME_PATTERN`, of variable files `TFVARS_FILE_PATTERN`,
of state schemas `STATE_SCHEMA_PATTERN`.
@@ -187,11 +188,42 @@ of state schemas `STATE_SCHEMA_PATTERN`.
- `modules.list()`, `modules.put(module)`, which creates or changes, and `modules.delete(name)`.
- `instances.list(module)`, `instances.create(module, name, stateSchema)` and
`instances.delete(module, name)`, which deletes its variable files too.
- `variables(module, instance)`: `read()` answers the files, their sizes and times and the
version, `versionOf` the files, the name and the SHA-256 of each in name order. `write(version,
contents)` writes files given by name as JSON text and `remove(version, name)` deletes one; both
lock the table, compare the version and change in one transaction, and answer `false`, changing
nothing, when the files changed since the version.
- `variables(module, instance)`: `read()` answers the files, their sizes and times, the version,
`versionOf` the files, the name and the SHA-256 of each in name order, and the newest revision.
`write(version, contents, meta)` writes files given by name as JSON text and `remove(version,
name, meta)` deletes one; both lock the table, compare the version, change and record all files
as the next revision with `meta`, `{ schema, producer }`, in one transaction, and answer
`false`, changing nothing, when the files changed since the version. A write of no file records
no revision. `revisions()` lists the revisions, newest first, without their files, and
`revision(n)` answers one with its files, or `null`.
Every revision is kept. The files of an instance without any revision, as those written before
revisions existed, are recorded as its revision 1, of the producer `unknown` and no schema, when
the store is first used.
### Purging revisions
Revisions are removed by hand, with `psql` connected to the database of the store as the owner of
its tables, in the schema the applications use (`SET search_path TO <schema>;` when it is not the
default). Each statement removes the revisions of one instance, here `router`/`platform`:
```sql
-- All revisions but the newest.
DELETE FROM tf_variable_revisions
WHERE module = 'router' AND instance = 'platform'
AND revision < (SELECT max(revision) FROM tf_variable_revisions WHERE module = 'router' AND instance = 'platform');
-- The revisions before revision 10.
DELETE FROM tf_variable_revisions WHERE module = 'router' AND instance = 'platform' AND revision < 10;
-- All revisions of the instance.
DELETE FROM tf_variable_revisions WHERE module = 'router' AND instance = 'platform';
```
Leaving out `AND instance = ...` applies a statement to every instance of the module. The next
write numbers its revision after the newest one left. An instance whose revisions are all removed
gets its current files back as revision 1, of the producer `unknown` and no schema, when the store
is next first used, so its schema is unknown until the next write by its configurator.
A change the tables refuse throws `HttpError`: 409 for a name or a state schema in use and for a
module that has instances, 404 for a module or an instance that does not exist, 400 for a name or
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "@koszewscy/sk-react-toolkit",
"version": "0.6.0",
"version": "0.7.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@koszewscy/sk-react-toolkit",
"version": "0.6.0",
"version": "0.7.0",
"devDependencies": {
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@koszewscy/sk-react-toolkit",
"version": "0.6.0",
"version": "0.7.0",
"description": "Shared code of the React and Fastify web applications: web components, server helpers and the Vite font plugin.",
"private": true,
"type": "module",
+12 -2
View File
@@ -4,6 +4,16 @@ export { HttpError, errorHandler, notFoundHandler, strict } from './http.ts'
export { QUIET, createLog, fail, timestampsOn, type Log } from './log.ts'
export { exitOnSignals, gitVersion, readJsonObject } from './process.ts'
export { decrypt, encrypt, encryptionKey } from './secrets.ts'
export type { TfInstance, TfModule, TfvarsFileStatus } from '../shared/tfvars.ts'
export type { TfInstance, TfModule, TfvarsFileStatus, TfvarsRevision } from '../shared/tfvars.ts'
export { STATE_SCHEMA_PATTERN, TFVARS_FILE_PATTERN, TF_NAME_PATTERN } from '../shared/tfvars.ts'
export { createTfvarsStore, tfvarsText, versionOf, type InstanceVariables, type TfvarsDocument, type TfvarsFile, type TfvarsStore } from './tfvarsStore.ts'
export {
createTfvarsStore,
tfvarsText,
versionOf,
type InstanceVariables,
type RevisionMeta,
type TfvarsDocument,
type TfvarsFile,
type TfvarsRevisionFiles,
type TfvarsStore,
} from './tfvarsStore.ts'
+58 -2
View File
@@ -1,6 +1,7 @@
-- The store of Terraform modules, their instances and the instances' variable files: the tables tf_modules,
-- tf_instances and tf_variable_files, and the functions and procedures the store calls. The store applies the file on
-- its first use in the schema its connection's search path names; every statement can run again.
-- tf_instances, tf_variable_files and tf_variable_revisions, and the functions and procedures the store calls. The
-- store applies the file on its first use in the schema its connection's search path names; every statement can run
-- again.
CREATE TABLE IF NOT EXISTS tf_modules (
name text PRIMARY KEY CHECK (name ~ '^[a-z][a-z0-9-]*$'),
@@ -28,6 +29,28 @@ CREATE TABLE IF NOT EXISTS tf_variable_files (
FOREIGN KEY (module, instance) REFERENCES tf_instances (module, name) ON DELETE CASCADE
);
-- Every write of an instance's variable files: all its files after the write, by name, the schema version of their
-- layout, null when not known, and the application that wrote them.
CREATE TABLE IF NOT EXISTS tf_variable_revisions (
module text NOT NULL,
instance text NOT NULL,
revision integer NOT NULL CHECK (revision > 0),
files jsonb NOT NULL CHECK (jsonb_typeof(files) = 'object'),
schema integer CHECK (schema > 0),
producer text NOT NULL CHECK (producer <> ''),
saved timestamptz NOT NULL DEFAULT now(),
PRIMARY KEY (module, instance, revision),
FOREIGN KEY (module, instance) REFERENCES tf_instances (module, name) ON DELETE CASCADE
);
-- The files of an instance that has no revision are its revision 1, of an unknown schema and producer.
INSERT INTO tf_variable_revisions (module, instance, revision, files, schema, producer, saved)
SELECT f.module, f.instance, 1, jsonb_object_agg(f.name, f.content), NULL, 'unknown', max(f.updated)
FROM tf_variable_files f
WHERE NOT EXISTS (SELECT 1 FROM tf_variable_revisions r WHERE r.module = f.module AND r.instance = f.instance)
GROUP BY f.module, f.instance
ON CONFLICT DO NOTHING;
-- The modules, by name.
CREATE OR REPLACE FUNCTION tf_modules_list()
RETURNS TABLE (name text, root text, description text, configurator_url text)
@@ -115,3 +138,36 @@ SET search_path FROM CURRENT
AS $$
DELETE FROM tf_variable_files WHERE module = module_name AND instance = instance_name AND name = file_name
$$;
-- Records the variable files of an instance as its next revision, with the schema of their layout and their producer;
-- called with the lock of tf_variable_files_lock held.
CREATE OR REPLACE PROCEDURE tf_variable_revision_add(module_name text, instance_name text, revision_schema integer, revision_producer text)
LANGUAGE sql
SET search_path FROM CURRENT
AS $$
INSERT INTO tf_variable_revisions (module, instance, revision, files, schema, producer)
SELECT module_name, instance_name,
(SELECT coalesce(max(r.revision), 0) + 1 FROM tf_variable_revisions r WHERE r.module = module_name AND r.instance = instance_name),
(SELECT coalesce(jsonb_object_agg(f.name, f.content), '{}'::jsonb) FROM tf_variable_files f WHERE f.module = module_name AND f.instance = instance_name),
revision_schema, revision_producer
$$;
-- The revisions of an instance, newest first, with the names of their files.
CREATE OR REPLACE FUNCTION tf_variable_revisions_list(module_name text, instance_name text)
RETURNS TABLE (revision integer, schema integer, producer text, saved timestamptz, names text[])
LANGUAGE sql STABLE
SET search_path FROM CURRENT
AS $$
SELECT r.revision, r.schema, r.producer, r.saved, ARRAY(SELECT jsonb_object_keys(r.files) ORDER BY 1)
FROM tf_variable_revisions r WHERE r.module = module_name AND r.instance = instance_name ORDER BY r.revision DESC
$$;
-- A revision of an instance with its files.
CREATE OR REPLACE FUNCTION tf_variable_revision_get(module_name text, instance_name text, revision_number integer)
RETURNS TABLE (revision integer, schema integer, producer text, saved timestamptz, files jsonb)
LANGUAGE sql STABLE
SET search_path FROM CURRENT
AS $$
SELECT r.revision, r.schema, r.producer, r.saved, r.files
FROM tf_variable_revisions r WHERE r.module = module_name AND r.instance = instance_name AND r.revision = revision_number
$$;
+41 -7
View File
@@ -60,29 +60,62 @@ test('instances are created and refused for a missing module or a name in use',
assert.equal(await status(store.modules.delete('router')), 409)
})
const META = { schema: 1, producer: 'Test Configurator 1.0.0' }
test('variable files are written with their version and refused against another version', async () => {
const variables = store.variables('router', 'test')
const empty = await variables.read()
assert.deepEqual(empty.files, [])
assert.equal(empty.version, '')
assert.equal(await variables.write(empty.version, { 'platform.tfvars.json': tfvarsText({ a: 1 }), 'secrets.tfvars.json': tfvarsText({ s: 'x' }) }), true)
assert.equal(empty.revision, null)
assert.equal(await variables.write(empty.version, { 'platform.tfvars.json': tfvarsText({ a: 1 }), 'secrets.tfvars.json': tfvarsText({ s: 'x' }) }, META), true)
const written = await variables.read()
assert.deepEqual(written.files.map((file) => [file.name, file.content.toString('utf8')]), [
['platform.tfvars.json', tfvarsText({ a: 1 })],
['secrets.tfvars.json', tfvarsText({ s: 'x' })],
])
assert.deepEqual(written.status.map((file) => file.previous), [false, false])
assert.equal(await variables.write(empty.version, { 'platform.tfvars.json': tfvarsText({ a: 2 }) }), false)
assert.equal(await variables.write(written.version, { 'platform.tfvars.json': tfvarsText({ a: 2 }) }), true)
assert.equal(await variables.write(empty.version, { 'platform.tfvars.json': tfvarsText({ a: 2 }) }, META), false)
assert.equal(await variables.write(written.version, { 'platform.tfvars.json': tfvarsText({ a: 2 }) }, META), true)
const changed = await variables.read()
assert.deepEqual(changed.status.map((file) => file.previous), [true, false])
assert.notEqual(changed.version, written.version)
assert.equal(await variables.remove(written.version, 'secrets.tfvars.json'), false)
assert.equal(await variables.remove(changed.version, 'secrets.tfvars.json'), true)
assert.equal(await variables.remove(written.version, 'secrets.tfvars.json', META), false)
assert.equal(await variables.remove(changed.version, 'secrets.tfvars.json', META), true)
assert.deepEqual((await variables.read()).files.map((file) => file.name), ['platform.tfvars.json'])
assert.deepEqual((await store.variables('router', 'platform').read()).files, [])
assert.equal(await status(variables.write((await variables.read()).version, { 'notes.json': tfvarsText({}) })), 400)
assert.equal(await status(store.variables('router', 'missing').write('', { 'a.tfvars.json': tfvarsText({}) })), 404)
assert.equal(await status(variables.write((await variables.read()).version, { 'notes.json': tfvarsText({}) }, META)), 400)
assert.equal(await status(store.variables('router', 'missing').write('', { 'a.tfvars.json': tfvarsText({}) }, META)), 404)
})
test('every write is a revision with its files, schema and producer', async () => {
const variables = store.variables('router', 'test')
assert.deepEqual((await variables.revisions()).map((revision) => [revision.revision, revision.schema, revision.producer, revision.names]), [
[3, 1, META.producer, ['platform.tfvars.json']],
[2, 1, META.producer, ['platform.tfvars.json', 'secrets.tfvars.json']],
[1, 1, META.producer, ['platform.tfvars.json', 'secrets.tfvars.json']],
])
const current = await variables.read()
assert.equal(await variables.write(current.version, {}, META), true)
assert.equal(await variables.write(current.version, { 'platform.tfvars.json': tfvarsText({ a: 3 }) }, { schema: null, producer: 'manage.mjs import' }), true)
const newest = (await variables.read()).revision
assert.deepEqual(newest && [newest.revision, newest.schema, newest.producer], [4, null, 'manage.mjs import'])
const first = await variables.revision(1)
assert.deepEqual(first?.files.map((file) => [file.name, file.content.toString('utf8')]), [
['platform.tfvars.json', tfvarsText({ a: 1 })],
['secrets.tfvars.json', tfvarsText({ s: 'x' })],
])
assert.equal(await variables.revision(9), null)
assert.deepEqual(await store.variables('router', 'platform').revisions(), [])
})
test('the files of an instance without revisions become its revision 1 of an unknown producer', async () => {
await pool.query(`INSERT INTO tf_variable_files (module, instance, name, content) VALUES ('router', 'platform', 'a.tfvars.json', '{"b": 1}')`)
await pool.query('DELETE FROM tf_variable_revisions')
const again = createTfvarsStore(pool)
const revisions = await again.variables('router', 'platform').revisions()
assert.deepEqual(revisions.map((revision) => [revision.revision, revision.schema, revision.producer, revision.names]), [[1, null, 'unknown', ['a.tfvars.json']]])
assert.deepEqual((await again.variables('router', 'test').revisions()).map((revision) => revision.revision), [1])
})
test('an instance is deleted with its variable files, then its module', async () => {
@@ -90,6 +123,7 @@ test('an instance is deleted with its variable files, then its module', async ()
await store.instances.delete('router', 'platform')
assert.deepEqual(await store.instances.list('router'), [])
assert.equal((await pool.query('SELECT count(*)::int AS n FROM tf_variable_files')).rows[0].n, 0)
assert.equal((await pool.query('SELECT count(*)::int AS n FROM tf_variable_revisions')).rows[0].n, 0)
await store.modules.delete('router')
assert.deepEqual(await store.modules.list(), [])
})
+81 -17
View File
@@ -1,25 +1,42 @@
import { createHash } from 'node:crypto'
import { readFileSync } from 'node:fs'
import type { Pool, PoolClient } from 'pg'
import type { TfInstance, TfModule, TfvarsFileStatus } from '../shared/tfvars.ts'
import type { TfInstance, TfModule, TfvarsFileStatus, TfvarsRevision } from '../shared/tfvars.ts'
import { HttpError } from './http.ts'
/** A variable file of an instance: its name and its content, the object jsonb keeps written as JSON text. */
export type TfvarsFile = { name: string; content: Buffer }
/** The variable files of an instance, by name, with their version, versionOf the files. */
export type TfvarsDocument = { files: TfvarsFile[]; status: TfvarsFileStatus[]; version: string }
/**
* The variable files of an instance, by name, with their version, versionOf the files, and their newest revision, null
* for an instance never written.
*/
export type TfvarsDocument = { files: TfvarsFile[]; status: TfvarsFileStatus[]; version: string; revision: TfvarsRevision | null }
/** A revision with its files, by name. */
export type TfvarsRevisionFiles = TfvarsRevision & { files: TfvarsFile[] }
/** What a write records with the files: the schema version of their layout, null when not known, and the producer. */
export type RevisionMeta = { schema: number | null; producer: string }
/** The variable files of one instance. */
export type InstanceVariables = {
read(): Promise<TfvarsDocument>
/**
* Writes the given files, name and JSON text of an object, when the files are still those of version, keeping each
* replaced content as its previous one; answers false, writing nothing, when they changed since.
* replaced content as its previous one, and records all files as the next revision with meta; answers false, writing
* nothing, when they changed since. A write of no file records no revision.
*/
write(version: string, contents: Record<string, string>): Promise<boolean>
/** Deletes the file name when the files are still those of version; answers false, deleting nothing, otherwise. */
remove(version: string, name: string): Promise<boolean>
write(version: string, contents: Record<string, string>, meta: RevisionMeta): Promise<boolean>
/**
* Deletes the file name when the files are still those of version, and records the remaining files as the next
* revision with meta; answers false, deleting nothing, otherwise.
*/
remove(version: string, name: string, meta: RevisionMeta): Promise<boolean>
/** The revisions, newest first. */
revisions(): Promise<TfvarsRevision[]>
/** The revision of the number with its files; null when there is none. */
revision(revision: number): Promise<TfvarsRevisionFiles | null>
}
/** The modules, the instances and their variable files in PostgreSQL. */
@@ -56,6 +73,15 @@ const SQL = new URL('./sql/tfvars_store.sql', import.meta.url)
type FileRow = { name: string; content: object; previous: boolean; updated: Date }
type RevisionRow = { revision: number; schema: number | null; producer: string; saved: Date; names: string[] }
const revisionOf = (row: Omit<RevisionRow, 'names'>, names: string[]): TfvarsRevision =>
({ revision: row.revision, schema: row.schema, producer: row.producer, saved: row.saved.toISOString(), names })
/** The files of an object of revision files, by name, written as JSON text. */
const filesOf = (files: Record<string, object>): TfvarsFile[] =>
Object.keys(files).sort().map((name) => ({ name, content: Buffer.from(tfvarsText(files[name]), 'utf8') }))
/**
* The store of sql/tfvars_store.sql in the database of pool, applied at the first use in the schema of the
* connection's search path. A refused change is an HttpError: 409 for a name in use or a module that has instances,
@@ -72,27 +98,49 @@ export function createTfvarsStore(pool: Pool): TfvarsStore {
throw refused(error)
}
}
const document = (rows: FileRow[]): TfvarsDocument => {
const document = (rows: FileRow[], newest: RevisionRow | undefined): TfvarsDocument => {
const files = rows.map((row) => ({ name: row.name, content: Buffer.from(tfvarsText(row.content), 'utf8') }))
return {
files,
status: rows.map((row, index) => ({ name: row.name, bytes: files[index].content.length, modified: row.updated.toISOString(), previous: row.previous })),
version: versionOf(files),
revision: newest === undefined ? null : revisionOf(newest, newest.names),
}
}
/** Runs change in a transaction that holds the lock of the variable files, when they are still those of version. */
const locked = async (module: string, instance: string, version: string, change: (client: PoolClient) => Promise<void>) => {
/** The files and the newest revision of an instance, read in one snapshot. */
const read = async (module: string, instance: string): Promise<TfvarsDocument> => {
await ready()
const client = await pool.connect()
try {
await client.query('BEGIN ISOLATION LEVEL REPEATABLE READ READ ONLY')
const files = await client.query<FileRow>('SELECT * FROM tf_variable_files_read($1, $2)', [module, instance])
const revisions = await client.query<RevisionRow>('SELECT * FROM tf_variable_revisions_list($1, $2) LIMIT 1', [module, instance])
await client.query('COMMIT')
return document(files.rows, revisions.rows[0])
} catch (error) {
await client.query('ROLLBACK')
throw refused(error)
} finally {
client.release()
}
}
/**
* Runs change in a transaction that holds the lock of the variable files, when they are still those of version, and
* records the files after it as the next revision with meta.
*/
const locked = async (module: string, instance: string, version: string, meta: RevisionMeta, change: (client: PoolClient) => Promise<void>) => {
await ready()
const client = await pool.connect()
try {
await client.query('BEGIN')
await client.query('CALL tf_variable_files_lock()')
const { rows } = await client.query<FileRow>('SELECT * FROM tf_variable_files_read($1, $2)', [module, instance])
if (document(rows).version !== version) {
if (document(rows, undefined).version !== version) {
await client.query('ROLLBACK')
return false
}
await change(client)
await client.query('CALL tf_variable_revision_add($1, $2, $3, $4)', [module, instance, meta.schema, meta.producer])
await client.query('COMMIT')
return true
} catch (error) {
@@ -124,15 +172,31 @@ export function createTfvarsStore(pool: Pool): TfvarsStore {
},
},
variables: (module, instance) => ({
read: async () => document(await query<FileRow>('SELECT * FROM tf_variable_files_read($1, $2)', [module, instance])),
write: (version, contents) => locked(module, instance, version, async (client) => {
for (const [name, text] of Object.entries(contents)) {
await client.query('CALL tf_variable_file_put($1, $2, $3, $4::jsonb)', [module, instance, name, text])
read: () => read(module, instance),
write: async (version, contents, meta) => {
if (Object.keys(contents).length === 0) {
return (await read(module, instance)).version === version
}
}),
remove: (version, name) => locked(module, instance, version, async (client) => {
return locked(module, instance, version, meta, async (client) => {
for (const [name, text] of Object.entries(contents)) {
await client.query('CALL tf_variable_file_put($1, $2, $3, $4::jsonb)', [module, instance, name, text])
}
})
},
remove: (version, name, meta) => locked(module, instance, version, meta, async (client) => {
await client.query('CALL tf_variable_file_delete($1, $2, $3)', [module, instance, name])
}),
revisions: async () => (await query<RevisionRow>('SELECT * FROM tf_variable_revisions_list($1, $2)', [module, instance]))
.map((row) => revisionOf(row, row.names)),
revision: async (revision) => {
const [row] = await query<Omit<RevisionRow, 'names'> & { files: Record<string, object> }>(
'SELECT * FROM tf_variable_revision_get($1, $2, $3)', [module, instance, revision])
if (row === undefined) {
return null
}
const files = filesOf(row.files)
return { ...revisionOf(row, files.map((file) => file.name)), files }
},
}),
}
}
+1 -1
View File
@@ -1,3 +1,3 @@
export type { CredentialField, CredentialKind, CredentialProfile, CredentialsDocument } from './credentials.ts'
export type { TfInstance, TfModule, TfvarsFileStatus } from './tfvars.ts'
export type { TfInstance, TfModule, TfvarsFileStatus, TfvarsRevision } from './tfvars.ts'
export { STATE_SCHEMA_PATTERN, TFVARS_FILE_PATTERN, TF_NAME_PATTERN } from './tfvars.ts'
+14
View File
@@ -26,6 +26,20 @@ export type TfvarsFileStatus = {
previous: boolean
}
/** A write of an instance's variable files, without their content. */
export type TfvarsRevision = {
/** The number of the write, from 1 for each instance. */
revision: number
/** The schema version of the files' layout, as their producer gave it; null when not known. */
schema: number | null
/** The application that wrote the files, with its version. */
producer: string
/** ISO 8601. */
saved: string
/** The names of the files of the revision. */
names: string[]
}
/** The names of modules and instances: a lower-case letter, then lower-case letters, digits and hyphens. */
export const TF_NAME_PATTERN = /^[a-z][a-z0-9-]*$/