@homeflare/alchemy 0.45.0 → 0.46.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/postgres/connection.d.ts.map +1 -1
- package/dist/postgres/fake-schema-drop.d.ts +13 -0
- package/dist/postgres/fake-schema-drop.d.ts.map +1 -0
- package/dist/postgres/fake-schema-sql.d.ts +5 -0
- package/dist/postgres/fake-schema-sql.d.ts.map +1 -1
- package/dist/postgres/fake-sql.d.ts +4 -0
- package/dist/postgres/fake-sql.d.ts.map +1 -1
- package/dist/postgres/index.d.ts +2 -1
- package/dist/postgres/index.d.ts.map +1 -1
- package/dist/postgres/index.js +461 -278
- package/dist/postgres/index.js.map +19 -15
- package/dist/postgres/psql-executor.d.ts.map +1 -1
- package/dist/postgres/schema-closure-sql.d.ts +45 -0
- package/dist/postgres/schema-closure-sql.d.ts.map +1 -0
- package/dist/postgres/schema-delete.d.ts +17 -8
- package/dist/postgres/schema-delete.d.ts.map +1 -1
- package/dist/postgres/schema-drop-sql.d.ts +66 -0
- package/dist/postgres/schema-drop-sql.d.ts.map +1 -0
- package/dist/postgres/schema-errors.d.ts +19 -1
- package/dist/postgres/schema-errors.d.ts.map +1 -1
- package/dist/postgres/schema-sequence-error.d.ts +29 -0
- package/dist/postgres/schema-sequence-error.d.ts.map +1 -0
- package/dist/postgres/schema-sql.d.ts +16 -0
- package/dist/postgres/schema-sql.d.ts.map +1 -1
- package/dist/postgres/schema-test-kit.d.ts +3 -1
- package/dist/postgres/schema-test-kit.d.ts.map +1 -1
- package/dist/postgres/search-path.d.ts +38 -0
- package/dist/postgres/search-path.d.ts.map +1 -0
- package/dist/proxmox/generated/constraints/index.d.ts +1 -1
- package/dist/proxmox/generated/constraints/pve-access.d.ts +1 -1
- package/dist/proxmox/generated/constraints/pve-cluster-backup.d.ts +1 -1
- package/dist/proxmox/generated/constraints/pve-cluster-ceph.d.ts +1 -1
- package/dist/proxmox/generated/constraints/pve-cluster-firewall.d.ts +1 -1
- package/dist/proxmox/generated/constraints/pve-cluster-ha.d.ts +1 -1
- package/dist/proxmox/generated/constraints/pve-cluster-metrics.d.ts +1 -1
- package/dist/proxmox/generated/constraints/pve-cluster-notifications.d.ts +1 -1
- package/dist/proxmox/generated/constraints/pve-cluster-replication.d.ts +1 -1
- package/dist/proxmox/generated/constraints/pve-cluster-sdn.d.ts +1 -1
- package/dist/proxmox/generated/constraints/pve-nodes-ceph.d.ts +1 -1
- package/dist/proxmox/generated/constraints/pve-nodes-disks.d.ts +1 -1
- package/dist/proxmox/generated/constraints/pve-nodes-lxc.d.ts +1 -1
- package/dist/proxmox/generated/constraints/pve-nodes-network.d.ts +1 -1
- package/dist/proxmox/generated/constraints/pve-nodes-qemu.d.ts +1 -1
- package/dist/proxmox/generated/constraints/pve-nodes-qemu.d.ts.map +1 -1
- package/dist/proxmox/generated/constraints/pve-nodes-storage.d.ts +1 -1
- package/dist/proxmox/generated/constraints/pve-pools.d.ts +1 -1
- package/dist/proxmox/generated/constraints/pve-storage.d.ts +1 -1
- package/dist/proxmox/index.d.ts +1 -0
- package/dist/proxmox/index.d.ts.map +1 -1
- package/dist/proxmox/index.js +283 -130
- package/dist/proxmox/index.js.map +31 -27
- package/dist/proxmox/qemu-dns.d.ts +2 -0
- package/dist/proxmox/qemu-dns.d.ts.map +1 -0
- package/dist/proxmox/qemu-errors.d.ts +1 -0
- package/dist/proxmox/qemu-errors.d.ts.map +1 -1
- package/dist/proxmox/qemu-form.d.ts.map +1 -1
- package/dist/proxmox/qemu-judge.d.ts.map +1 -1
- package/dist/proxmox/qemu-lifecycle.d.ts +15 -10
- package/dist/proxmox/qemu-lifecycle.d.ts.map +1 -1
- package/dist/proxmox/qemu-props.d.ts +10 -0
- package/dist/proxmox/qemu-props.d.ts.map +1 -1
- package/dist/proxmox/qemu-resize.d.ts +10 -0
- package/dist/proxmox/qemu-resize.d.ts.map +1 -0
- package/dist/proxmox/qemu-size.d.ts +20 -0
- package/dist/proxmox/qemu-size.d.ts.map +1 -0
- package/dist/proxmox/qemu-spec.d.ts +25 -0
- package/dist/proxmox/qemu-spec.d.ts.map +1 -0
- package/dist/verify/fake-engine.d.ts +4 -2
- package/dist/verify/fake-engine.d.ts.map +1 -1
- package/docs/postgres-schema.md +22 -1
- package/docs/proxmox-qemu-resize.md +62 -0
- package/package.json +1 -1
- package/src/postgres/connection.ts +10 -14
- package/src/postgres/database-bigint-serialization.test.ts +2 -2
- package/src/postgres/database-sql.ts +6 -6
- package/src/postgres/fake-grants-read.ts +3 -3
- package/src/postgres/fake-role-sql.ts +2 -2
- package/src/postgres/fake-schema-drop.ts +75 -0
- package/src/postgres/fake-schema-sql.ts +10 -1
- package/src/postgres/fake-sql.ts +10 -3
- package/src/postgres/grants-database.test.ts +5 -5
- package/src/postgres/grants-existence.ts +7 -7
- package/src/postgres/grants-ownership.ts +5 -5
- package/src/postgres/grants-psql-lifecycle.test.ts +7 -3
- package/src/postgres/grants-read-defaults.ts +5 -5
- package/src/postgres/grants-read.ts +16 -16
- package/src/postgres/index.ts +2 -0
- package/src/postgres/psql-executor-escaping.test.ts +3 -1
- package/src/postgres/psql-executor.test.ts +11 -5
- package/src/postgres/psql-executor.ts +4 -1
- package/src/postgres/role-adopt.test.ts +3 -1
- package/src/postgres/role-membership-options.test.ts +3 -1
- package/src/postgres/role-membership-repair.test.ts +3 -3
- package/src/postgres/role-membership-sql.ts +4 -4
- package/src/postgres/role-password-transports.test.ts +4 -2
- package/src/postgres/role-sql.ts +1 -1
- package/src/postgres/schema-closure-sql.ts +108 -0
- package/src/postgres/schema-delete-proofs.test.ts +7 -7
- package/src/postgres/schema-delete.ts +88 -46
- package/src/postgres/schema-drop-2bp01.test.ts +29 -17
- package/src/postgres/schema-drop-real.test.ts +190 -0
- package/src/postgres/schema-drop-sql.ts +135 -0
- package/src/postgres/schema-drop.test.ts +241 -0
- package/src/postgres/schema-errors.ts +26 -0
- package/src/postgres/schema-handlers.test.ts +21 -14
- package/src/postgres/schema-lifecycle.test.ts +6 -60
- package/src/postgres/schema-sequence-error.ts +38 -0
- package/src/postgres/schema-socket.test.ts +20 -6
- package/src/postgres/schema-sql.ts +16 -11
- package/src/postgres/schema-takeover.test.ts +7 -4
- package/src/postgres/schema-test-kit.ts +18 -4
- package/src/postgres/search-path.test.ts +100 -0
- package/src/postgres/search-path.ts +62 -0
- package/src/proxmox/generated/constraints/index.ts +1 -1
- package/src/proxmox/generated/constraints/pve-access.ts +1 -1
- package/src/proxmox/generated/constraints/pve-cluster-backup.ts +1 -1
- package/src/proxmox/generated/constraints/pve-cluster-ceph.ts +1 -1
- package/src/proxmox/generated/constraints/pve-cluster-firewall.ts +1 -1
- package/src/proxmox/generated/constraints/pve-cluster-ha.ts +1 -1
- package/src/proxmox/generated/constraints/pve-cluster-metrics.ts +1 -1
- package/src/proxmox/generated/constraints/pve-cluster-notifications.ts +1 -1
- package/src/proxmox/generated/constraints/pve-cluster-replication.ts +1 -1
- package/src/proxmox/generated/constraints/pve-cluster-sdn.ts +1 -1
- package/src/proxmox/generated/constraints/pve-nodes-ceph.ts +1 -1
- package/src/proxmox/generated/constraints/pve-nodes-disks.ts +1 -1
- package/src/proxmox/generated/constraints/pve-nodes-lxc.ts +1 -1
- package/src/proxmox/generated/constraints/pve-nodes-network.ts +1 -1
- package/src/proxmox/generated/constraints/pve-nodes-qemu.ts +6 -1
- package/src/proxmox/generated/constraints/pve-nodes-storage.ts +1 -1
- package/src/proxmox/generated/constraints/pve-pools.ts +1 -1
- package/src/proxmox/generated/constraints/pve-storage.ts +1 -1
- package/src/proxmox/index.ts +1 -0
- package/src/proxmox/qemu-dns.test.ts +73 -0
- package/src/proxmox/qemu-dns.ts +32 -0
- package/src/proxmox/qemu-errors.ts +1 -0
- package/src/proxmox/qemu-form.ts +6 -3
- package/src/proxmox/qemu-judge.ts +2 -1
- package/src/proxmox/qemu-lifecycle.ts +28 -47
- package/src/proxmox/qemu-props.ts +14 -2
- package/src/proxmox/qemu-resize-lock.test.ts +121 -0
- package/src/proxmox/qemu-resize-recovery.test.ts +81 -0
- package/src/proxmox/qemu-resize.test.ts +199 -0
- package/src/proxmox/qemu-resize.ts +86 -0
- package/src/proxmox/qemu-size.test.ts +64 -0
- package/src/proxmox/qemu-size.ts +95 -0
- package/src/proxmox/qemu-spec.ts +44 -0
- package/src/proxmox/qemu-volume.ts +5 -5
- package/src/verify/fake-engine.ts +6 -3
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* branch's marker and matches its own.
|
|
12
12
|
*/
|
|
13
13
|
import type { PsqlRunner } from './psql-executor.ts';
|
|
14
|
+
import { stripPin } from './search-path.ts';
|
|
14
15
|
import type { PostgresSchemaAttributes, PostgresSchemaProps } from './schema-attrs.ts';
|
|
15
16
|
|
|
16
17
|
export const ok = (stdout: string) => Promise.resolve({ code: 0, stdout, stderr: '' });
|
|
@@ -29,9 +30,12 @@ export interface RouteAnswers {
|
|
|
29
30
|
|
|
30
31
|
/** Route one stdin to its answer by the markers above, `undefined` when no branch matches. */
|
|
31
32
|
export const route = (stdin: string, answers: RouteAnswers): string | undefined => {
|
|
33
|
+
// The atomic drop (`schema-drop-sql.ts`) quotes every marker below inside its body: it answers
|
|
34
|
+
// nothing, and a refusal is injected by `runnerWith`'s `failOn`, never routed.
|
|
35
|
+
if (stdin.startsWith('DO ')) return undefined;
|
|
32
36
|
if (stdin.includes('AS empty')) return answers.empty;
|
|
33
|
-
if (stdin.includes('FROM pg_namespace')) return answers.schema;
|
|
34
|
-
if (stdin.includes('FROM pg_database')) return answers.probe;
|
|
37
|
+
if (stdin.includes('FROM pg_catalog.pg_namespace')) return answers.schema;
|
|
38
|
+
if (stdin.includes('FROM pg_catalog.pg_database')) return answers.probe;
|
|
35
39
|
if (stdin.includes('current_database()')) return answers.proof;
|
|
36
40
|
return undefined;
|
|
37
41
|
};
|
|
@@ -46,14 +50,24 @@ export const router = (answers: RouteAnswers) => (stdin: string) => route(stdin,
|
|
|
46
50
|
*/
|
|
47
51
|
export const runnerWith = (
|
|
48
52
|
answers: (stdin: string) => string,
|
|
53
|
+
/** The `psql` stderr a statement fails with (exit 3), or `undefined` to answer normally. */
|
|
54
|
+
failOn: (stdin: string) => string | undefined = () => undefined,
|
|
49
55
|
): { run: PsqlRunner; stdins: string[]; argvs: Array<readonly string[]> } => {
|
|
50
56
|
const stdins: string[] = [];
|
|
51
57
|
const argvs: Array<readonly string[]> = [];
|
|
52
58
|
return {
|
|
53
|
-
|
|
59
|
+
// The pin prefix (`search-path.ts`) is stripped: these tests assert the STATEMENT. The pin
|
|
60
|
+
// itself is asserted once, on the raw stdin, in `search-path.test.ts`.
|
|
61
|
+
run: ({ stdin: raw, argv }) => {
|
|
62
|
+
const stdin = stripPin(raw);
|
|
54
63
|
stdins.push(stdin);
|
|
55
64
|
argvs.push(argv);
|
|
56
|
-
|
|
65
|
+
const stderr = failOn(stdin);
|
|
66
|
+
return Promise.resolve(
|
|
67
|
+
stderr === undefined
|
|
68
|
+
? { code: 0, stdout: answers(stdin), stderr: '' }
|
|
69
|
+
: { code: 3, stdout: '', stderr },
|
|
70
|
+
);
|
|
57
71
|
},
|
|
58
72
|
stdins,
|
|
59
73
|
argvs,
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `search_path` pin (`search-path.ts`): every session this family opens starts with
|
|
3
|
+
* `SET search_path = pg_catalog, pg_temp`, on both transports, and the SQL builders only name
|
|
4
|
+
* catalogs as `pg_catalog.*`. The Opus read of PR 334 (2026-10-02) found that a role allowed to
|
|
5
|
+
* create objects in a schema on the path could forge `pg_namespace` and fake the delete proof.
|
|
6
|
+
*/
|
|
7
|
+
import { readFileSync, readdirSync } from 'node:fs';
|
|
8
|
+
import { describe, expect, test } from 'bun:test';
|
|
9
|
+
import * as Effect from 'effect/Effect';
|
|
10
|
+
import { currentDatabase } from './schema-sql.ts';
|
|
11
|
+
import { makePsqlExecutor } from './psql-executor.ts';
|
|
12
|
+
import { PIN_SCRIPT_PREFIX, PIN_SEARCH_PATH_SQL } from './search-path.ts';
|
|
13
|
+
|
|
14
|
+
describe('runner transport', () => {
|
|
15
|
+
const target = { database: 'postgres', username: 'postgres' };
|
|
16
|
+
const record = () => {
|
|
17
|
+
const stdins: string[] = [];
|
|
18
|
+
const run = ({ stdin }: { stdin: string }) => {
|
|
19
|
+
stdins.push(stdin);
|
|
20
|
+
return Promise.resolve({ code: 0, stdout: '[]', stderr: '' });
|
|
21
|
+
};
|
|
22
|
+
return { stdins, run };
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
test('a read starts with the pin, in the same psql session as the statement', async () => {
|
|
26
|
+
const { stdins, run } = record();
|
|
27
|
+
await Effect.runPromise(currentDatabase(makePsqlExecutor(run, target)));
|
|
28
|
+
expect(stdins).toHaveLength(1);
|
|
29
|
+
expect(stdins[0]?.startsWith('SET search_path = pg_catalog, pg_temp;\n')).toBe(true);
|
|
30
|
+
expect(stdins[0]?.startsWith(PIN_SCRIPT_PREFIX)).toBe(true);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('a write and a transaction start with the pin too', async () => {
|
|
34
|
+
const { stdins, run } = record();
|
|
35
|
+
const pg = makePsqlExecutor(run, target);
|
|
36
|
+
await Effect.runPromise(pg.unsafe('COMMENT ON SCHEMA "a" IS E\'b\''));
|
|
37
|
+
await Effect.runPromise(pg.transaction(['CREATE ROLE "r"']));
|
|
38
|
+
expect(stdins).toHaveLength(2);
|
|
39
|
+
for (const stdin of stdins) expect(stdin.startsWith(PIN_SCRIPT_PREFIX)).toBe(true);
|
|
40
|
+
// The pin comes BEFORE `BEGIN`: it is a session setting, not part of the transaction.
|
|
41
|
+
expect(stdins[1]).toBe(`${PIN_SCRIPT_PREFIX}BEGIN;\nCREATE ROLE "r";\nCOMMIT;`);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('the pin is the one statement text both transports share', () => {
|
|
45
|
+
expect(PIN_SEARCH_PATH_SQL).toBe('SET search_path = pg_catalog, pg_temp');
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
/** Every hand-written SQL builder in this family. The `*errors.ts` files are excluded: they name
|
|
50
|
+
* catalogs in error-message PROSE, which no server ever parses. The fakes match on the qualified
|
|
51
|
+
* text the builders emit, so their own tests pin them. */
|
|
52
|
+
const sources = readdirSync(import.meta.dir).filter(
|
|
53
|
+
(f) =>
|
|
54
|
+
f.endsWith('.ts') &&
|
|
55
|
+
!f.endsWith('.test.ts') &&
|
|
56
|
+
!f.startsWith('fake-') &&
|
|
57
|
+
!/errors?\.ts$/.test(f),
|
|
58
|
+
);
|
|
59
|
+
const CATALOGS =
|
|
60
|
+
/(?<![\w.])(pg_(?:namespace|class|proc|type|operator|roles|authid|auth_members|database|tablespace|attribute|default_acl|depend|constraint|trigger|rewrite|attrdef|policy))\b/g;
|
|
61
|
+
|
|
62
|
+
describe('catalog references', () => {
|
|
63
|
+
test('no SQL builder names a catalog without the pg_catalog. qualifier', () => {
|
|
64
|
+
const offenders: string[] = [];
|
|
65
|
+
for (const file of sources) {
|
|
66
|
+
const text = readFileSync(`${import.meta.dir}/${file}`, 'utf8');
|
|
67
|
+
// Only template/quoted SQL is judged: skip comment lines and `'pg_namespace'` literals
|
|
68
|
+
// (an `obj_description` class argument is a string, not a relation reference).
|
|
69
|
+
for (const [index, line] of text.split('\n').entries()) {
|
|
70
|
+
const trimmed = line.trim();
|
|
71
|
+
if (/^(\/\/|\*|\/\*)/.test(trimmed)) continue;
|
|
72
|
+
for (const match of line.matchAll(CATALOGS)) {
|
|
73
|
+
const before = line[(match.index ?? 0) - 1];
|
|
74
|
+
if (before === "'" || before === '`') continue;
|
|
75
|
+
offenders.push(`${file}:${String(index + 1)} ${match[1] ?? ''}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
expect(offenders).toEqual([]);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('the scan sees the catalogs it is meant to police', () => {
|
|
83
|
+
const joined = sources.map((f) => readFileSync(`${import.meta.dir}/${f}`, 'utf8')).join('\n');
|
|
84
|
+
for (const needle of [
|
|
85
|
+
'pg_catalog.pg_namespace',
|
|
86
|
+
'pg_catalog.pg_class',
|
|
87
|
+
'pg_catalog.pg_depend',
|
|
88
|
+
'pg_catalog.pg_roles',
|
|
89
|
+
'pg_catalog.pg_database',
|
|
90
|
+
]) {
|
|
91
|
+
expect(joined).toContain(needle);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('the scan itself fails on an unqualified reference', () => {
|
|
96
|
+
const bad = ' FROM pg_namespace n\n JOIN pg_catalog.pg_class c ON true';
|
|
97
|
+
const hits = [...bad.matchAll(CATALOGS)].map((m) => m[1]);
|
|
98
|
+
expect(hits).toEqual(['pg_namespace']);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The session setup every `Postgres.*` connection runs first: `search_path = pg_catalog, pg_temp`.
|
|
3
|
+
*
|
|
4
|
+
* ⛔ AN UNPINNED SESSION READS ITS OWN CATALOGS THROUGH `search_path`. Postgres searches
|
|
5
|
+
* `pg_catalog` first only while the path does not NAME it (ddl.sgml 5.10.3); a role- or
|
|
6
|
+
* database-level `ALTER … SET search_path = public, pg_catalog` (or a path naming any schema
|
|
7
|
+
* another role may create in) puts a writable schema ahead of it, and a `public.pg_namespace`
|
|
8
|
+
* TABLE then answers the delete proof's `oid`/`owner` re-read. Found by the Opus read of PR 334
|
|
9
|
+
* (2026-10-02). The pin makes the order ours whatever the role, database or server says; the SQL
|
|
10
|
+
* builders ALSO write `pg_catalog.<name>` (`catalog-qualified.test.ts` greps for it), so no
|
|
11
|
+
* single layer is the whole defence.
|
|
12
|
+
* ★ `pg_temp` goes LAST on purpose: unnamed, it is searched FIRST for relations.
|
|
13
|
+
* ★ TWO TRANSPORTS, TWO MECHANISMS. The runner path prefixes the stdin script: `psql` argv is
|
|
14
|
+
* the consumer's to join into a remote command, and a value with a space would split. The
|
|
15
|
+
* socket path pins the ONE connection an operation reserves, because the driver's startup
|
|
16
|
+
* message carries user, database and application_name only (`PgConnection.ts`, 4.0.0-rc.115)
|
|
17
|
+
* and a pool hands each statement a different, unpinned connection.
|
|
18
|
+
*/
|
|
19
|
+
import type { PgClient } from '@effect/sql-pg/PgClient';
|
|
20
|
+
import * as Effect from 'effect/Effect';
|
|
21
|
+
import type * as Scope from 'effect/Scope';
|
|
22
|
+
import type { SqlError } from 'effect/unstable/sql/SqlError';
|
|
23
|
+
import type { PgExecutor } from './database-sql.ts';
|
|
24
|
+
|
|
25
|
+
export const PIN_SEARCH_PATH_SQL = 'SET search_path = pg_catalog, pg_temp';
|
|
26
|
+
|
|
27
|
+
/** The runner path's script prefix. */
|
|
28
|
+
export const PIN_SCRIPT_PREFIX = `${PIN_SEARCH_PATH_SQL};\n`;
|
|
29
|
+
|
|
30
|
+
/** A runner stdin without its pin — for tests that assert the statement that follows it. */
|
|
31
|
+
export const stripPin = (stdin: string): string =>
|
|
32
|
+
stdin.startsWith(PIN_SCRIPT_PREFIX) ? stdin.slice(PIN_SCRIPT_PREFIX.length) : stdin;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* A `PgExecutor` over ONE reserved, pinned connection, for the life of the surrounding scope.
|
|
36
|
+
* `unsafe` runs on it (the client routes any statement under `transactionService` to that
|
|
37
|
+
* connection, so spans and error classification stay the driver's); `transaction` is a plain
|
|
38
|
+
* `BEGIN` … `COMMIT` on it — `withTransaction` would open a SAVEPOINT, because the service is
|
|
39
|
+
* already provided. A failure rolls back, and the pin is re-asserted by the next operation's own
|
|
40
|
+
* reservation, never assumed to survive one.
|
|
41
|
+
*/
|
|
42
|
+
export const pinnedSocketExecutor = (
|
|
43
|
+
pg: PgClient,
|
|
44
|
+
): Effect.Effect<PgExecutor, SqlError, Scope.Scope> =>
|
|
45
|
+
Effect.gen(function* () {
|
|
46
|
+
const connection = yield* pg.reserve;
|
|
47
|
+
const run = <A extends object>(sql: string, params?: ReadonlyArray<unknown>) =>
|
|
48
|
+
Effect.provideService(pg.unsafe<A>(sql, params), pg.transactionService, [connection, 0]);
|
|
49
|
+
yield* run(PIN_SEARCH_PATH_SQL);
|
|
50
|
+
return {
|
|
51
|
+
unsafe: run,
|
|
52
|
+
transaction: (statements) =>
|
|
53
|
+
Effect.gen(function* () {
|
|
54
|
+
yield* run('BEGIN');
|
|
55
|
+
for (const sql of statements) yield* run(sql);
|
|
56
|
+
yield* run('COMMIT');
|
|
57
|
+
}).pipe(
|
|
58
|
+
Effect.onError(() => run('ROLLBACK').pipe(Effect.ignore)),
|
|
59
|
+
Effect.asVoid,
|
|
60
|
+
),
|
|
61
|
+
};
|
|
62
|
+
});
|
|
@@ -54,4 +54,4 @@ export const PROXMOX_CONSTRAINTS: Readonly<Record<string, EndpointConstraints>>
|
|
|
54
54
|
* ★ A DIGEST OF THE DATA, NOT OF THE FILE TEXT, so reformatting is not a stale generation while
|
|
55
55
|
* changing a 128 to a 129 by hand is. `tests/schema-manifest.test.ts` recomputes it.
|
|
56
56
|
*/
|
|
57
|
-
export const PROXMOX_CONSTRAINTS_DIGEST = '
|
|
57
|
+
export const PROXMOX_CONSTRAINTS_DIGEST = 'cbf63b7bac0500ef';
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* sha256 9def8f13611184ee, read on a PVE cluster node from
|
|
7
7
|
* /usr/share/pve-docs/api-viewer/apidoc.js
|
|
8
8
|
*
|
|
9
|
-
*
|
|
9
|
+
* 60 of this product's 258 POST/PUT endpoints are tabled across all areas: the ones
|
|
10
10
|
* this package writes to, named in its own source. Every other vendor write endpoint is UNTABLED
|
|
11
11
|
* and therefore unchecked at plan time.
|
|
12
12
|
*
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* sha256 9def8f13611184ee, read on a PVE cluster node from
|
|
7
7
|
* /usr/share/pve-docs/api-viewer/apidoc.js
|
|
8
8
|
*
|
|
9
|
-
*
|
|
9
|
+
* 60 of this product's 258 POST/PUT endpoints are tabled across all areas: the ones
|
|
10
10
|
* this package writes to, named in its own source. Every other vendor write endpoint is UNTABLED
|
|
11
11
|
* and therefore unchecked at plan time.
|
|
12
12
|
*
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* sha256 9def8f13611184ee, read on a PVE cluster node from
|
|
7
7
|
* /usr/share/pve-docs/api-viewer/apidoc.js
|
|
8
8
|
*
|
|
9
|
-
*
|
|
9
|
+
* 60 of this product's 258 POST/PUT endpoints are tabled across all areas: the ones
|
|
10
10
|
* this package writes to, named in its own source. Every other vendor write endpoint is UNTABLED
|
|
11
11
|
* and therefore unchecked at plan time.
|
|
12
12
|
*
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* sha256 9def8f13611184ee, read on a PVE cluster node from
|
|
7
7
|
* /usr/share/pve-docs/api-viewer/apidoc.js
|
|
8
8
|
*
|
|
9
|
-
*
|
|
9
|
+
* 60 of this product's 258 POST/PUT endpoints are tabled across all areas: the ones
|
|
10
10
|
* this package writes to, named in its own source. Every other vendor write endpoint is UNTABLED
|
|
11
11
|
* and therefore unchecked at plan time.
|
|
12
12
|
*
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* sha256 9def8f13611184ee, read on a PVE cluster node from
|
|
7
7
|
* /usr/share/pve-docs/api-viewer/apidoc.js
|
|
8
8
|
*
|
|
9
|
-
*
|
|
9
|
+
* 60 of this product's 258 POST/PUT endpoints are tabled across all areas: the ones
|
|
10
10
|
* this package writes to, named in its own source. Every other vendor write endpoint is UNTABLED
|
|
11
11
|
* and therefore unchecked at plan time.
|
|
12
12
|
*
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* sha256 9def8f13611184ee, read on a PVE cluster node from
|
|
7
7
|
* /usr/share/pve-docs/api-viewer/apidoc.js
|
|
8
8
|
*
|
|
9
|
-
*
|
|
9
|
+
* 60 of this product's 258 POST/PUT endpoints are tabled across all areas: the ones
|
|
10
10
|
* this package writes to, named in its own source. Every other vendor write endpoint is UNTABLED
|
|
11
11
|
* and therefore unchecked at plan time.
|
|
12
12
|
*
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* sha256 9def8f13611184ee, read on a PVE cluster node from
|
|
7
7
|
* /usr/share/pve-docs/api-viewer/apidoc.js
|
|
8
8
|
*
|
|
9
|
-
*
|
|
9
|
+
* 60 of this product's 258 POST/PUT endpoints are tabled across all areas: the ones
|
|
10
10
|
* this package writes to, named in its own source. Every other vendor write endpoint is UNTABLED
|
|
11
11
|
* and therefore unchecked at plan time.
|
|
12
12
|
*
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* sha256 9def8f13611184ee, read on a PVE cluster node from
|
|
7
7
|
* /usr/share/pve-docs/api-viewer/apidoc.js
|
|
8
8
|
*
|
|
9
|
-
*
|
|
9
|
+
* 60 of this product's 258 POST/PUT endpoints are tabled across all areas: the ones
|
|
10
10
|
* this package writes to, named in its own source. Every other vendor write endpoint is UNTABLED
|
|
11
11
|
* and therefore unchecked at plan time.
|
|
12
12
|
*
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* sha256 9def8f13611184ee, read on a PVE cluster node from
|
|
7
7
|
* /usr/share/pve-docs/api-viewer/apidoc.js
|
|
8
8
|
*
|
|
9
|
-
*
|
|
9
|
+
* 60 of this product's 258 POST/PUT endpoints are tabled across all areas: the ones
|
|
10
10
|
* this package writes to, named in its own source. Every other vendor write endpoint is UNTABLED
|
|
11
11
|
* and therefore unchecked at plan time.
|
|
12
12
|
*
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* sha256 9def8f13611184ee, read on a PVE cluster node from
|
|
7
7
|
* /usr/share/pve-docs/api-viewer/apidoc.js
|
|
8
8
|
*
|
|
9
|
-
*
|
|
9
|
+
* 60 of this product's 258 POST/PUT endpoints are tabled across all areas: the ones
|
|
10
10
|
* this package writes to, named in its own source. Every other vendor write endpoint is UNTABLED
|
|
11
11
|
* and therefore unchecked at plan time.
|
|
12
12
|
*
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* sha256 9def8f13611184ee, read on a PVE cluster node from
|
|
7
7
|
* /usr/share/pve-docs/api-viewer/apidoc.js
|
|
8
8
|
*
|
|
9
|
-
*
|
|
9
|
+
* 60 of this product's 258 POST/PUT endpoints are tabled across all areas: the ones
|
|
10
10
|
* this package writes to, named in its own source. Every other vendor write endpoint is UNTABLED
|
|
11
11
|
* and therefore unchecked at plan time.
|
|
12
12
|
*
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* sha256 9def8f13611184ee, read on a PVE cluster node from
|
|
7
7
|
* /usr/share/pve-docs/api-viewer/apidoc.js
|
|
8
8
|
*
|
|
9
|
-
*
|
|
9
|
+
* 60 of this product's 258 POST/PUT endpoints are tabled across all areas: the ones
|
|
10
10
|
* this package writes to, named in its own source. Every other vendor write endpoint is UNTABLED
|
|
11
11
|
* and therefore unchecked at plan time.
|
|
12
12
|
*
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* sha256 9def8f13611184ee, read on a PVE cluster node from
|
|
7
7
|
* /usr/share/pve-docs/api-viewer/apidoc.js
|
|
8
8
|
*
|
|
9
|
-
*
|
|
9
|
+
* 60 of this product's 258 POST/PUT endpoints are tabled across all areas: the ones
|
|
10
10
|
* this package writes to, named in its own source. Every other vendor write endpoint is UNTABLED
|
|
11
11
|
* and therefore unchecked at plan time.
|
|
12
12
|
*
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* sha256 9def8f13611184ee, read on a PVE cluster node from
|
|
7
7
|
* /usr/share/pve-docs/api-viewer/apidoc.js
|
|
8
8
|
*
|
|
9
|
-
*
|
|
9
|
+
* 60 of this product's 258 POST/PUT endpoints are tabled across all areas: the ones
|
|
10
10
|
* this package writes to, named in its own source. Every other vendor write endpoint is UNTABLED
|
|
11
11
|
* and therefore unchecked at plan time.
|
|
12
12
|
*
|
|
@@ -122,4 +122,9 @@ export const PVE_NODES_QEMU_CONSTRAINTS: Readonly<Record<string, EndpointConstra
|
|
|
122
122
|
"vmstatestorage": {"format":"pve-storage-id","type":"string"},
|
|
123
123
|
"watchdog": {"format":"pve-qm-watchdog","type":"string"},
|
|
124
124
|
},
|
|
125
|
+
"pve:PUT /nodes/{node}/qemu/{vmid}/resize": {
|
|
126
|
+
"digest": {"maxLength":40,"type":"string"},
|
|
127
|
+
"disk": {"enum":["ide0","ide1","ide2","ide3","scsi0","scsi1","scsi2","scsi3","scsi4","scsi5","scsi6","scsi7","scsi8","scsi9","scsi10","scsi11","scsi12","scsi13","scsi14","scsi15","scsi16","scsi17","scsi18","scsi19","scsi20","scsi21","scsi22","scsi23","scsi24","scsi25","scsi26","scsi27","scsi28","scsi29","scsi30","virtio0","virtio1","virtio2","virtio3","virtio4","virtio5","virtio6","virtio7","virtio8","virtio9","virtio10","virtio11","virtio12","virtio13","virtio14","virtio15","sata0","sata1","sata2","sata3","sata4","sata5","efidisk0","tpmstate0"],"required":true,"type":"string"},
|
|
128
|
+
"size": {"pattern":"^\\+?\\d+(\\.\\d+)?[KMGT]?\\n?$","patternSource":"\\+?\\d+(\\.\\d+)?[KMGT]?","required":true,"type":"string"},
|
|
129
|
+
},
|
|
125
130
|
};
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* sha256 9def8f13611184ee, read on a PVE cluster node from
|
|
7
7
|
* /usr/share/pve-docs/api-viewer/apidoc.js
|
|
8
8
|
*
|
|
9
|
-
*
|
|
9
|
+
* 60 of this product's 258 POST/PUT endpoints are tabled across all areas: the ones
|
|
10
10
|
* this package writes to, named in its own source. Every other vendor write endpoint is UNTABLED
|
|
11
11
|
* and therefore unchecked at plan time.
|
|
12
12
|
*
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* sha256 9def8f13611184ee, read on a PVE cluster node from
|
|
7
7
|
* /usr/share/pve-docs/api-viewer/apidoc.js
|
|
8
8
|
*
|
|
9
|
-
*
|
|
9
|
+
* 60 of this product's 258 POST/PUT endpoints are tabled across all areas: the ones
|
|
10
10
|
* this package writes to, named in its own source. Every other vendor write endpoint is UNTABLED
|
|
11
11
|
* and therefore unchecked at plan time.
|
|
12
12
|
*
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* sha256 9def8f13611184ee, read on a PVE cluster node from
|
|
7
7
|
* /usr/share/pve-docs/api-viewer/apidoc.js
|
|
8
8
|
*
|
|
9
|
-
*
|
|
9
|
+
* 60 of this product's 258 POST/PUT endpoints are tabled across all areas: the ones
|
|
10
10
|
* this package writes to, named in its own source. Every other vendor write endpoint is UNTABLED
|
|
11
11
|
* and therefore unchecked at plan time.
|
|
12
12
|
*
|
package/src/proxmox/index.ts
CHANGED
|
@@ -113,6 +113,7 @@ export {
|
|
|
113
113
|
export { provisionBootstrap } from './provision-bootstrap.ts';
|
|
114
114
|
export { type DeclareProvisionOptions, declareProvisionBaseline } from './provision-declare.ts';
|
|
115
115
|
export { ProxmoxVm, ProxmoxVmProvider, type VmAttributes, type VmProps } from './qemu.ts';
|
|
116
|
+
export { QemuDiskResizeRefused } from './qemu-size.ts';
|
|
116
117
|
export {
|
|
117
118
|
ProxmoxStorageDownload,
|
|
118
119
|
ProxmoxStorageDownloadProvider,
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { expect, test } from 'bun:test';
|
|
2
|
+
import * as Effect from 'effect/Effect';
|
|
3
|
+
import { FAKE_TARGET, fakePve, withoutBao } from './fake-pve.ts';
|
|
4
|
+
import { createForm, formRefusals, updateForm } from './qemu-form.ts';
|
|
5
|
+
import { judge } from './qemu-judge.ts';
|
|
6
|
+
import { qemuHandlers } from './qemu-lifecycle.ts';
|
|
7
|
+
import { storedConfig } from './qemu-props.ts';
|
|
8
|
+
|
|
9
|
+
const base = { target: FAKE_TARGET, node: 'n2', vmid: 10000 };
|
|
10
|
+
const props = { ...base, nameserver: '10.20.16.254', searchdomain: 'example.internal' };
|
|
11
|
+
|
|
12
|
+
test('cloud-init DNS survives form rendering, stored attributes and per-key diff', () => {
|
|
13
|
+
const config = { nameserver: props.nameserver, searchdomain: props.searchdomain };
|
|
14
|
+
expect(createForm(props)).toEqual({ ...config, vmid: '10000' });
|
|
15
|
+
expect(updateForm(props)).toEqual(config);
|
|
16
|
+
expect(storedConfig(config)).toEqual(config);
|
|
17
|
+
expect(judge(props, config)).toEqual({ drift: [], put: {}, refuse: [] });
|
|
18
|
+
expect(judge({ ...props, nameserver: '10.20.16.1' }, config)).toEqual({
|
|
19
|
+
drift: ['nameserver'],
|
|
20
|
+
put: { nameserver: '10.20.16.1' },
|
|
21
|
+
refuse: [],
|
|
22
|
+
});
|
|
23
|
+
expect(judge({ ...props, searchdomain: 'new.internal' }, config).put).toEqual({
|
|
24
|
+
searchdomain: 'new.internal',
|
|
25
|
+
});
|
|
26
|
+
expect(judge(base, config).drift).toEqual([]);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('address-list follows PVE: IPs, DNS names, separators and empty lists', () => {
|
|
30
|
+
for (const nameserver of [
|
|
31
|
+
'',
|
|
32
|
+
'10.20.16.254',
|
|
33
|
+
'2001:db8::53',
|
|
34
|
+
'dns.example.internal',
|
|
35
|
+
'10.20.16.254, 2001:db8::53;dns.example.internal',
|
|
36
|
+
'10.20.16.254\0dns.example.internal',
|
|
37
|
+
]) {
|
|
38
|
+
expect(formRefusals({ ...base, nameserver }), nameserver).toEqual([]);
|
|
39
|
+
}
|
|
40
|
+
for (const nameserver of [
|
|
41
|
+
'https://dns.example',
|
|
42
|
+
'10.20.16.254/24',
|
|
43
|
+
'[2001:db8::53]',
|
|
44
|
+
'-bad.example',
|
|
45
|
+
'bad_.example',
|
|
46
|
+
'2001:db8::broken',
|
|
47
|
+
'dns.example.',
|
|
48
|
+
]) {
|
|
49
|
+
expect(formRefusals({ ...base, nameserver })[0], nameserver).toContain('address-list');
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('invalid DNS fails with a typed refusal before any live read or write', async () => {
|
|
54
|
+
const fake = fakePve(() => {
|
|
55
|
+
throw new Error('unexpected API call');
|
|
56
|
+
});
|
|
57
|
+
await withoutBao(async () => {
|
|
58
|
+
const refusal = await Effect.runPromise(
|
|
59
|
+
qemuHandlers
|
|
60
|
+
.diff({
|
|
61
|
+
news: { ...props, nameserver: 'dns://invalid' },
|
|
62
|
+
olds: base,
|
|
63
|
+
output: undefined,
|
|
64
|
+
})
|
|
65
|
+
.pipe(
|
|
66
|
+
Effect.catchTag('QemuRefusedError', (error) => Effect.succeed(error.message)),
|
|
67
|
+
Effect.provide(fake.layer),
|
|
68
|
+
),
|
|
69
|
+
);
|
|
70
|
+
expect(refusal).toBe('nameserver: must satisfy the vendor address-list format');
|
|
71
|
+
});
|
|
72
|
+
expect(fake.calls).toEqual([]);
|
|
73
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** Cloud-init's one named format, checked against the vendor table and validator source. */
|
|
2
|
+
import { isIP } from 'node:net';
|
|
3
|
+
import { constraintsFor } from './constraint-guard.ts';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* PVE::JSONSchema pve_verify_address/check_format and PVE::ParseUtils split_list,
|
|
7
|
+
* proxmox/pve-common@defd246f31f327463f901a2daaf8dc52efcc5a97 (read 2026-10-05).
|
|
8
|
+
* https://github.com/proxmox/pve-common/blob/defd246f31f327463f901a2daaf8dc52efcc5a97/src/PVE/JSONSchema.pm
|
|
9
|
+
* ★ `address` permits DNS names too; an IP-only check would reject vendor-valid declarations.
|
|
10
|
+
* Empty lists are allowed. NUL-separated lists take precedence over whitespace/comma/semicolon.
|
|
11
|
+
* This supplements the PVE 9.2.11 schema's named format; it does not invent a DNS-server limit.
|
|
12
|
+
*/
|
|
13
|
+
const addressList = (value: string): boolean => {
|
|
14
|
+
const values = value.includes('\0')
|
|
15
|
+
? value.split('\0')
|
|
16
|
+
: value.replace(/[,;]/g, ' ').trimStart().split(/\s+/);
|
|
17
|
+
// Perl split drops trailing empty elements, but retains interior empty elements.
|
|
18
|
+
while (values.at(-1) === '') values.pop();
|
|
19
|
+
const label = '[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?';
|
|
20
|
+
const dns = new RegExp(`^(?:${label}\\.)*${label}(?![\\s\\S])`);
|
|
21
|
+
return values.every((value) => (isIP(value) !== 0 && !value.includes('%')) || dns.test(value));
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const dnsRefusals = (nameserver: unknown): string[] => {
|
|
25
|
+
if (nameserver === undefined) return [];
|
|
26
|
+
const rule = constraintsFor('pve:PUT /nodes/{node}/qemu/{vmid}/config')['nameserver'];
|
|
27
|
+
return rule?.format === 'address-list' &&
|
|
28
|
+
typeof nameserver === 'string' &&
|
|
29
|
+
addressList(nameserver)
|
|
30
|
+
? []
|
|
31
|
+
: ['nameserver: must satisfy the vendor address-list format'];
|
|
32
|
+
};
|
package/src/proxmox/qemu-form.ts
CHANGED
|
@@ -13,16 +13,19 @@ import {
|
|
|
13
13
|
isManagedKey,
|
|
14
14
|
wireValue,
|
|
15
15
|
} from './qemu-props.ts';
|
|
16
|
+
import { dnsRefusals } from './qemu-dns.ts';
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
19
|
* Why a declared key cannot go on the wire, checked before every create and every update.
|
|
19
20
|
* ★ THE SAME RULE `lxc-create-form.ts` ENFORCES FOR `Proxmox.Lxc`: a key smuggled past the types
|
|
20
21
|
* (a cast, a JS caller) is refused here rather than silently dropped or silently sent.
|
|
21
22
|
*/
|
|
22
|
-
export const formRefusals = (props: VmProps): string[] =>
|
|
23
|
-
declaredKeys(props)
|
|
23
|
+
export const formRefusals = (props: VmProps): string[] => [
|
|
24
|
+
...declaredKeys(props)
|
|
24
25
|
.filter((key) => !isManagedKey(key))
|
|
25
|
-
.map((key) => `${key}: not a config key this resource manages (vmid ${String(props.vmid)}).`)
|
|
26
|
+
.map((key) => `${key}: not a config key this resource manages (vmid ${String(props.vmid)}).`),
|
|
27
|
+
...dnsRefusals(props.nameserver),
|
|
28
|
+
];
|
|
26
29
|
|
|
27
30
|
/** The declared config, wire-spelled — every key `formRefusals` did not already refuse. */
|
|
28
31
|
export const form = (props: VmProps): Record<string, string> => {
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
import { type VmProps, declaredKeys, declaredValue, indexedKey, wireValue } from './qemu-props.ts';
|
|
14
14
|
import { judgeDisk } from './qemu-volume.ts';
|
|
15
15
|
import { netWrite, sameNet } from './qemu-net.ts';
|
|
16
|
+
import { diskSizeDrift } from './qemu-size.ts';
|
|
16
17
|
|
|
17
18
|
export type VmChange = {
|
|
18
19
|
/** Keys to `PUT …/config`, wire-spelled and built on live ids where one exists. */
|
|
@@ -26,7 +27,7 @@ export type VmChange = {
|
|
|
26
27
|
export const judge = (props: VmProps, config: Record<string, string>): VmChange => {
|
|
27
28
|
const put: Record<string, string> = {};
|
|
28
29
|
const refuse: string[] = [];
|
|
29
|
-
const drift = new Set<string>();
|
|
30
|
+
const drift = new Set<string>(diskSizeDrift(props, config));
|
|
30
31
|
|
|
31
32
|
for (const key of declaredKeys(props)) {
|
|
32
33
|
const want = wireValue(declaredValue(props, key));
|