postgresai 0.16.0-rc.4 → 0.16.0
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/README.md +154 -0
- package/dist/bin/postgres-ai.js +2911 -255
- package/package.json +12 -3
- package/schemas/A002.schema.json +63 -0
- package/schemas/A003.schema.json +73 -0
- package/schemas/A004.schema.json +81 -0
- package/schemas/A007.schema.json +71 -0
- package/schemas/A013.schema.json +61 -0
- package/schemas/D001.schema.json +71 -0
- package/schemas/D004.schema.json +136 -0
- package/schemas/F001.schema.json +73 -0
- package/schemas/F002.schema.json +108 -0
- package/schemas/F003.schema.json +138 -0
- package/schemas/F004.schema.json +125 -0
- package/schemas/F005.schema.json +131 -0
- package/schemas/F009.schema.json +155 -0
- package/schemas/G001.schema.json +135 -0
- package/schemas/G003.schema.json +90 -0
- package/schemas/H001.schema.json +141 -0
- package/schemas/H002.schema.json +129 -0
- package/schemas/H004.schema.json +128 -0
- package/schemas/I001.schema.json +149 -0
- package/schemas/K001.schema.json +161 -0
- package/schemas/K003.schema.json +163 -0
- package/schemas/K004.schema.json +110 -0
- package/schemas/K005.schema.json +110 -0
- package/schemas/K006.schema.json +110 -0
- package/schemas/K007.schema.json +110 -0
- package/schemas/K008.schema.json +110 -0
- package/schemas/M001.schema.json +119 -0
- package/schemas/M002.schema.json +110 -0
- package/schemas/M003.schema.json +128 -0
- package/schemas/N001.schema.json +161 -0
- package/schemas/query.schema.json +62 -0
- package/CHANGELOG.md +0 -11
- package/bin/postgres-ai.ts +0 -5578
- package/bun.lock +0 -258
- package/bunfig.toml +0 -20
- package/lib/aas-onboard.ts +0 -251
- package/lib/auth-server.ts +0 -285
- package/lib/checkup-api.ts +0 -526
- package/lib/checkup-dictionary.ts +0 -103
- package/lib/checkup-summary.ts +0 -338
- package/lib/checkup.ts +0 -2261
- package/lib/config.ts +0 -171
- package/lib/init.ts +0 -1152
- package/lib/instances.ts +0 -245
- package/lib/issues.ts +0 -1060
- package/lib/mcp-server.ts +0 -667
- package/lib/metrics-loader.ts +0 -134
- package/lib/pkce.ts +0 -79
- package/lib/reports.ts +0 -373
- package/lib/storage.ts +0 -367
- package/lib/supabase.ts +0 -826
- package/lib/util.ts +0 -134
- package/packages/postgres-ai/README.md +0 -26
- package/packages/postgres-ai/bin/postgres-ai.js +0 -27
- package/packages/postgres-ai/package.json +0 -27
- package/scripts/embed-checkup-dictionary.ts +0 -115
- package/scripts/embed-metrics.ts +0 -160
- package/scripts/generate-release-notes.ts +0 -668
- package/test/PERMISSION_CHECK_TEST_SUMMARY.md +0 -139
- package/test/aas-onboard.test.ts +0 -301
- package/test/auth.test.ts +0 -287
- package/test/checkup.integration.test.ts +0 -413
- package/test/checkup.test.ts +0 -3626
- package/test/compose-cmd.test.ts +0 -120
- package/test/config-consistency.test.ts +0 -352
- package/test/init.integration.test.ts +0 -438
- package/test/init.test.ts +0 -1816
- package/test/issues.cli.test.ts +0 -1162
- package/test/issues.test.ts +0 -456
- package/test/mcp-server.test.ts +0 -2530
- package/test/monitoring.test.ts +0 -746
- package/test/permission-check-sql.test.ts +0 -116
- package/test/reports.cli.test.ts +0 -793
- package/test/reports.test.ts +0 -977
- package/test/schema-validation.test.ts +0 -231
- package/test/storage.test.ts +0 -935
- package/test/supabase.test.ts +0 -709
- package/test/targets-add-config.test.ts +0 -28
- package/test/test-utils.ts +0 -190
- package/test/upgrade.test.ts +0 -1056
- package/test/util.test.ts +0 -44
- package/tsconfig.json +0 -20
|
@@ -1,139 +0,0 @@
|
|
|
1
|
-
# Permission Check Test Summary
|
|
2
|
-
|
|
3
|
-
## Changes Made
|
|
4
|
-
|
|
5
|
-
Changed all references from `public.pg_statistic` to `postgres_ai.pg_statistic` in:
|
|
6
|
-
- `cli/lib/init.ts` - Permission check SQL query
|
|
7
|
-
- `cli/test/init.test.ts` - All test expectations (28 occurrences)
|
|
8
|
-
|
|
9
|
-
## Key Fix: Safe Schema Checking
|
|
10
|
-
|
|
11
|
-
**Before (883fa95):**
|
|
12
|
-
```sql
|
|
13
|
-
exists (
|
|
14
|
-
select from pg_views
|
|
15
|
-
where schemaname = 'public' and viewname = 'pg_statistic'
|
|
16
|
-
) as granted
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
**After (6db79f6) - INCORRECT, caused crashes:**
|
|
20
|
-
```sql
|
|
21
|
-
to_regclass('postgres_ai.pg_statistic') is not null as granted
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
**Current (this fix):**
|
|
25
|
-
```sql
|
|
26
|
-
case
|
|
27
|
-
when not has_schema_privilege(current_user, 'postgres_ai', 'USAGE') then null
|
|
28
|
-
else to_regclass('postgres_ai.pg_statistic') is not null
|
|
29
|
-
end as granted
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
### Why this fix matters
|
|
33
|
-
|
|
34
|
-
**Issue with bare `to_regclass()`:**
|
|
35
|
-
- Returns NULL when the schema doesn't exist ✓
|
|
36
|
-
- Returns NULL when the view doesn't exist ✓
|
|
37
|
-
- **Throws error** when the schema exists but user lacks USAGE privilege ✗
|
|
38
|
-
|
|
39
|
-
**Fix:**
|
|
40
|
-
- Check `has_schema_privilege()` first to avoid the permission error
|
|
41
|
-
- Returns NULL safely in all cases where we can't check the view
|
|
42
|
-
- Prevents crashes when postgres_ai schema exists but user lacks USAGE
|
|
43
|
-
|
|
44
|
-
## Test Results
|
|
45
|
-
|
|
46
|
-
### Unit Tests ✅
|
|
47
|
-
```
|
|
48
|
-
✓ 95 tests passed across 3 files
|
|
49
|
-
- 84 tests in init.test.ts (including 9 checkCurrentUserPermissions tests)
|
|
50
|
-
- 2 tests in config-consistency.test.ts
|
|
51
|
-
- 9 tests in permission-check-sql.test.ts
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
### Expected Behavior by Scenario
|
|
55
|
-
|
|
56
|
-
| Scenario | User Permissions | postgres_ai Schema | Expected Result |
|
|
57
|
-
|----------|-----------------|-------------------|-----------------|
|
|
58
|
-
| 1. Superuser | superuser + postgres_ai.pg_statistic | ✓ Exists | ✅ PASS (clean) |
|
|
59
|
-
| 2. pg_monitor, no schema access | pg_monitor only | ✗ No USAGE | ✅ PASS (warning) |
|
|
60
|
-
| 3. No pg_monitor | minimal permissions | ✗ Doesn't exist | ✅ PASS (error + fix SQL) |
|
|
61
|
-
| 8. After prepare-db | pg_monitor + postgres_ai grants | ✓ Exists + SELECT | ✅ PASS (clean) |
|
|
62
|
-
|
|
63
|
-
### SQL Behavior Verification
|
|
64
|
-
|
|
65
|
-
**Scenario 2 & 3: Schema doesn't exist or no USAGE**
|
|
66
|
-
```sql
|
|
67
|
-
-- Check privilege first, then to_regclass (no crash)
|
|
68
|
-
case
|
|
69
|
-
when not has_schema_privilege(current_user, 'postgres_ai', 'USAGE') then null
|
|
70
|
-
else to_regclass('postgres_ai.pg_statistic') is not null
|
|
71
|
-
end → NULL
|
|
72
|
-
|
|
73
|
-
-- SELECT check is skipped (returns NULL, not treated as missing optional)
|
|
74
|
-
case
|
|
75
|
-
when not has_schema_privilege(current_user, 'postgres_ai', 'USAGE') then null
|
|
76
|
-
when to_regclass('postgres_ai.pg_statistic') is null then null
|
|
77
|
-
else has_table_privilege(current_user, 'postgres_ai.pg_statistic', 'select')
|
|
78
|
-
end → NULL
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
**Scenario 1 & 8: Schema exists with proper grants**
|
|
82
|
-
```sql
|
|
83
|
-
-- User has USAGE, to_regclass returns OID (view is visible)
|
|
84
|
-
case
|
|
85
|
-
when not has_schema_privilege(current_user, 'postgres_ai', 'USAGE') then null
|
|
86
|
-
else to_regclass('postgres_ai.pg_statistic') is not null
|
|
87
|
-
end → TRUE
|
|
88
|
-
|
|
89
|
-
-- SELECT check is performed
|
|
90
|
-
has_table_privilege(current_user, 'postgres_ai.pg_statistic', 'select') → TRUE/FALSE
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
## Integration Test Limitations
|
|
94
|
-
|
|
95
|
-
Integration tests cannot run due to locale configuration issues with `initdb`:
|
|
96
|
-
```
|
|
97
|
-
error: initdb: error: invalid locale settings; check LANG and LC_* environment variables
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
However, unit tests provide comprehensive coverage of the permission check logic, including:
|
|
101
|
-
- All permission scenarios (granted, denied, skipped)
|
|
102
|
-
- Multiple missing permissions
|
|
103
|
-
- Error propagation
|
|
104
|
-
- Fix command generation
|
|
105
|
-
- Message formatting
|
|
106
|
-
|
|
107
|
-
## Schema Consistency
|
|
108
|
-
|
|
109
|
-
The change ensures consistency across the codebase:
|
|
110
|
-
- ✅ `cli/lib/init.ts` - now checks postgres_ai.pg_statistic
|
|
111
|
-
- ✅ `cli/lib/supabase.ts` - already checks postgres_ai.pg_statistic
|
|
112
|
-
- ✅ `cli/sql/03.permissions.sql` - creates postgres_ai.pg_statistic
|
|
113
|
-
- ✅ `config/target-db/init.sql` - creates postgres_ai.pg_statistic
|
|
114
|
-
- ✅ `config/pgwatch-prometheus/metrics.yml` - references postgres_ai.pg_statistic
|
|
115
|
-
|
|
116
|
-
## Commits
|
|
117
|
-
|
|
118
|
-
1. **955cff2** - `fix: change public.pg_statistic to postgres_ai.pg_statistic`
|
|
119
|
-
- Updated permission check queries
|
|
120
|
-
- Updated all test expectations
|
|
121
|
-
|
|
122
|
-
2. **6db79f6** - `fix: use to_regclass() for safe postgres_ai.pg_statistic check`
|
|
123
|
-
- Replaced pg_views query with to_regclass()
|
|
124
|
-
- ⚠️ This introduced a bug: crashes when schema exists but user lacks USAGE
|
|
125
|
-
|
|
126
|
-
3. **[current]** - `fix: wrap to_regclass() with has_schema_privilege() check`
|
|
127
|
-
- Fixed crash when postgres_ai schema exists but user lacks USAGE privilege
|
|
128
|
-
- Added privilege check before calling to_regclass() in all locations
|
|
129
|
-
- Updated in: init.ts (3 places) and supabase.ts (1 place)
|
|
130
|
-
|
|
131
|
-
## Verification Command
|
|
132
|
-
|
|
133
|
-
```bash
|
|
134
|
-
# Run all permission-related tests
|
|
135
|
-
bun test test/init.test.ts test/config-consistency.test.ts test/permission-check-sql.test.ts
|
|
136
|
-
|
|
137
|
-
# Verify no public.pg_statistic references remain (except in comments)
|
|
138
|
-
git grep -n 'public\.pg_statistic' cli/
|
|
139
|
-
```
|
package/test/aas-onboard.test.ts
DELETED
|
@@ -1,301 +0,0 @@
|
|
|
1
|
-
import { describe, test, expect, beforeEach, afterEach, spyOn } from "bun:test";
|
|
2
|
-
import * as fs from "fs";
|
|
3
|
-
import * as os from "os";
|
|
4
|
-
import * as path from "path";
|
|
5
|
-
import { addInstanceToFile, buildInstance } from "../lib/instances";
|
|
6
|
-
import {
|
|
7
|
-
parseVcpus,
|
|
8
|
-
resolveAasLabels,
|
|
9
|
-
registerAasCollection,
|
|
10
|
-
} from "../lib/aas-onboard";
|
|
11
|
-
|
|
12
|
-
/** Minimal Response-like stub for mocking fetch. */
|
|
13
|
-
function res(ok: boolean, status: number, jsonBody: unknown, textBody = ""): Response {
|
|
14
|
-
return {
|
|
15
|
-
ok,
|
|
16
|
-
status,
|
|
17
|
-
json: async () => jsonBody,
|
|
18
|
-
text: async () => textBody,
|
|
19
|
-
} as unknown as Response;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
describe("parseVcpus", () => {
|
|
23
|
-
test("non-positive / junk → 0 (the 'unknown' fallback)", () => {
|
|
24
|
-
expect(parseVcpus(undefined)).toBe(0);
|
|
25
|
-
expect(parseVcpus(null)).toBe(0);
|
|
26
|
-
expect(parseVcpus("")).toBe(0);
|
|
27
|
-
expect(parseVcpus("0")).toBe(0);
|
|
28
|
-
expect(parseVcpus("-4")).toBe(0);
|
|
29
|
-
expect(parseVcpus("abc")).toBe(0);
|
|
30
|
-
});
|
|
31
|
-
test("positive values → integer", () => {
|
|
32
|
-
expect(parseVcpus("16")).toBe(16);
|
|
33
|
-
expect(parseVcpus(8)).toBe(8);
|
|
34
|
-
expect(parseVcpus("12.9")).toBe(12);
|
|
35
|
-
expect(parseVcpus(" 4 ")).toBe(4);
|
|
36
|
-
});
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
describe("resolveAasLabels", () => {
|
|
40
|
-
let dir: string;
|
|
41
|
-
let file: string;
|
|
42
|
-
beforeEach(() => {
|
|
43
|
-
dir = fs.mkdtempSync(path.join(os.tmpdir(), "aas-labels-"));
|
|
44
|
-
file = path.join(dir, "instances.yml");
|
|
45
|
-
});
|
|
46
|
-
afterEach(() => fs.rmSync(dir, { recursive: true, force: true }));
|
|
47
|
-
|
|
48
|
-
test("single enabled target → its (cluster, node_name) from custom_tags", () => {
|
|
49
|
-
addInstanceToFile(file, buildInstance("appdb", "postgresql://u@h:5432/db"));
|
|
50
|
-
expect(resolveAasLabels(file)).toEqual({ cluster: "default", node: "appdb" });
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
test("no targets → null", () => {
|
|
54
|
-
fs.writeFileSync(file, "# empty\n");
|
|
55
|
-
expect(resolveAasLabels(file)).toBeNull();
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
test("more than one enabled target → null (cannot disambiguate)", () => {
|
|
59
|
-
addInstanceToFile(file, buildInstance("a", "postgresql://u@h:5432/a"));
|
|
60
|
-
addInstanceToFile(file, buildInstance("b", "postgresql://u@h:5432/b"));
|
|
61
|
-
expect(resolveAasLabels(file)).toBeNull();
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
test("missing file → null (no throw)", () => {
|
|
65
|
-
expect(resolveAasLabels(path.join(dir, "nope.yml"))).toBeNull();
|
|
66
|
-
});
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
describe("registerAasCollection", () => {
|
|
70
|
-
let dir: string;
|
|
71
|
-
let instancesPath: string;
|
|
72
|
-
let fetchSpy: ReturnType<typeof spyOn>;
|
|
73
|
-
let calls: Array<{ url: string; method: string; body?: string }>;
|
|
74
|
-
|
|
75
|
-
// Route a fetch by URL+method to canned Grafana/RPC responses. Options let a
|
|
76
|
-
// test exercise the existing-SA branch, datasource ambiguity, a keyless mint,
|
|
77
|
-
// and RPC success/failure.
|
|
78
|
-
function installFetch(opts: {
|
|
79
|
-
rpc?: { ok: boolean; status: number; text?: string };
|
|
80
|
-
existingSa?: boolean; // search finds an existing pgai-aas-collect SA
|
|
81
|
-
prometheusCount?: number; // # of prometheus-typed datasources (default 1)
|
|
82
|
-
mintKey?: string | null; // token .key; null => mint returns no key
|
|
83
|
-
} = {}) {
|
|
84
|
-
const rpc = opts.rpc ?? { ok: true, status: 200 };
|
|
85
|
-
const existingSa = opts.existingSa ?? false;
|
|
86
|
-
const promCount = opts.prometheusCount ?? 1;
|
|
87
|
-
const mintKey = opts.mintKey === undefined ? "glsa_mock_token_xyz" : opts.mintKey;
|
|
88
|
-
calls = [];
|
|
89
|
-
fetchSpy = spyOn(globalThis, "fetch").mockImplementation((async (input: unknown, init?: { method?: string; body?: string }) => {
|
|
90
|
-
const url = String(input);
|
|
91
|
-
const method = (init?.method || "GET").toUpperCase();
|
|
92
|
-
calls.push({ url, method, body: init?.body });
|
|
93
|
-
if (url.includes("/api/serviceaccounts/search"))
|
|
94
|
-
return res(true, 200, existingSa ? { serviceAccounts: [{ id: 99, name: "pgai-aas-collect" }] } : { serviceAccounts: [] });
|
|
95
|
-
if (url.match(/\/tokens$/) && method === "POST") return res(true, 200, mintKey === null ? {} : { key: mintKey });
|
|
96
|
-
if (url.endsWith("/api/serviceaccounts") && method === "POST") return res(true, 201, { id: 42, name: "pgai-aas-collect" });
|
|
97
|
-
if (url.includes("/api/datasources")) {
|
|
98
|
-
const dss: Array<Record<string, unknown>> = [];
|
|
99
|
-
for (let i = 0; i < promCount; i++) dss.push({ id: 8 + i, uid: `prom${i}`, type: "prometheus" });
|
|
100
|
-
dss.push({ id: 3, uid: "loki1", type: "loki" });
|
|
101
|
-
return res(true, 200, dss);
|
|
102
|
-
}
|
|
103
|
-
if (url.includes("/rpc/monitoring_instance_aas_register")) return res(rpc.ok, rpc.status, {}, rpc.text || "");
|
|
104
|
-
return res(false, 404, {});
|
|
105
|
-
}) as unknown as typeof fetch);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
beforeEach(() => {
|
|
109
|
-
dir = fs.mkdtempSync(path.join(os.tmpdir(), "aas-reg-"));
|
|
110
|
-
instancesPath = path.join(dir, "instances.yml");
|
|
111
|
-
addInstanceToFile(instancesPath, buildInstance("appdb", "postgresql://u@h:5432/db"));
|
|
112
|
-
});
|
|
113
|
-
afterEach(() => {
|
|
114
|
-
fetchSpy?.mockRestore();
|
|
115
|
-
fs.rmSync(dir, { recursive: true, force: true });
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
test("happy path: mints SA, resolves datasource, POSTs the RPC with the right body", async () => {
|
|
119
|
-
installFetch();
|
|
120
|
-
const r = await registerAasCollection("apikey-1", "inst-123", {
|
|
121
|
-
grafanaPassword: "pw",
|
|
122
|
-
instancesPath,
|
|
123
|
-
vcpus: 16,
|
|
124
|
-
apiBaseUrl: "https://api.test",
|
|
125
|
-
});
|
|
126
|
-
expect(r.ok).toBe(true);
|
|
127
|
-
|
|
128
|
-
const rpc = calls.find((c) => c.url.includes("/rpc/monitoring_instance_aas_register"));
|
|
129
|
-
expect(rpc).toBeDefined();
|
|
130
|
-
expect(rpc!.url).toBe("https://api.test/rpc/monitoring_instance_aas_register");
|
|
131
|
-
const body = JSON.parse(rpc!.body!);
|
|
132
|
-
expect(body).toMatchObject({
|
|
133
|
-
api_token: "apikey-1",
|
|
134
|
-
instance_id: "inst-123",
|
|
135
|
-
sa_token: "glsa_mock_token_xyz",
|
|
136
|
-
cluster_name: "default",
|
|
137
|
-
node_name: "appdb",
|
|
138
|
-
vcpus: 16,
|
|
139
|
-
datasource_id: 8, // the prometheus one, not loki
|
|
140
|
-
});
|
|
141
|
-
// a fresh SA was created (search found none) and a token minted on its id.
|
|
142
|
-
expect(calls.some((c) => c.url.endsWith("/api/serviceaccounts") && c.method === "POST")).toBe(true);
|
|
143
|
-
expect(calls.some((c) => c.url.match(/\/serviceaccounts\/42\/tokens$/) && c.method === "POST")).toBe(true);
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
test("platform error → ok:false, reason carries the status (best-effort, no throw)", async () => {
|
|
147
|
-
installFetch({ rpc: { ok: false, status: 403, text: "forbidden" } });
|
|
148
|
-
const r = await registerAasCollection("apikey-1", "inst-123", {
|
|
149
|
-
grafanaPassword: "pw",
|
|
150
|
-
instancesPath,
|
|
151
|
-
vcpus: 16,
|
|
152
|
-
apiBaseUrl: "https://api.test",
|
|
153
|
-
});
|
|
154
|
-
expect(r.ok).toBe(false);
|
|
155
|
-
expect(r.reason).toContain("403");
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
test("no resolvable target → ok:false and NO outbound calls (labels checked first)", async () => {
|
|
159
|
-
installFetch();
|
|
160
|
-
const empty = path.join(dir, "empty.yml");
|
|
161
|
-
fs.writeFileSync(empty, "# none\n");
|
|
162
|
-
const r = await registerAasCollection("apikey-1", "inst-123", {
|
|
163
|
-
grafanaPassword: "pw",
|
|
164
|
-
instancesPath: empty,
|
|
165
|
-
vcpus: 16,
|
|
166
|
-
apiBaseUrl: "https://api.test",
|
|
167
|
-
});
|
|
168
|
-
expect(r.ok).toBe(false);
|
|
169
|
-
expect(r.reason).toContain("cluster");
|
|
170
|
-
expect(calls.length).toBe(0); // bailed before any HTTP
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
test("missing api key / instance id → ok:false, no calls", async () => {
|
|
174
|
-
installFetch();
|
|
175
|
-
const r = await registerAasCollection("", "inst-123", {
|
|
176
|
-
grafanaPassword: "pw",
|
|
177
|
-
instancesPath,
|
|
178
|
-
vcpus: 0,
|
|
179
|
-
apiBaseUrl: "https://api.test",
|
|
180
|
-
});
|
|
181
|
-
expect(r.ok).toBe(false);
|
|
182
|
-
expect(calls.length).toBe(0);
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
test("existing service account is reused (no create), token minted on its id", async () => {
|
|
186
|
-
installFetch({ existingSa: true });
|
|
187
|
-
const r = await registerAasCollection("apikey-1", "inst-123", {
|
|
188
|
-
grafanaPassword: "pw", instancesPath, vcpus: 8, apiBaseUrl: "https://api.test",
|
|
189
|
-
});
|
|
190
|
-
expect(r.ok).toBe(true);
|
|
191
|
-
expect(calls.some((c) => c.url.endsWith("/api/serviceaccounts") && c.method === "POST")).toBe(false);
|
|
192
|
-
expect(calls.some((c) => c.url.match(/\/serviceaccounts\/99\/tokens$/) && c.method === "POST")).toBe(true);
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
test("absent or ambiguous (>1) prometheus datasource → ok:false, no RPC call", async () => {
|
|
196
|
-
for (const n of [0, 2]) {
|
|
197
|
-
fetchSpy?.mockRestore();
|
|
198
|
-
installFetch({ prometheusCount: n });
|
|
199
|
-
const r = await registerAasCollection("apikey-1", "inst-123", {
|
|
200
|
-
grafanaPassword: "pw", instancesPath, vcpus: 8, apiBaseUrl: "https://api.test",
|
|
201
|
-
// 0/>1 is a definitive skip; cap the readiness retry so the test stays fast.
|
|
202
|
-
datasourceMaxAttempts: 2, datasourceRetryDelayMs: 0,
|
|
203
|
-
});
|
|
204
|
-
expect(r.ok).toBe(false);
|
|
205
|
-
expect(r.reason).toContain("datasource");
|
|
206
|
-
expect(calls.some((c) => c.url.includes("/rpc/monitoring_instance_aas_register"))).toBe(false);
|
|
207
|
-
}
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
test("polls the datasource until Grafana is ready, then registers", async () => {
|
|
211
|
-
// Grafana isn't ready on the first probes (no prometheus datasource yet),
|
|
212
|
-
// then it provisions — the readiness retry must keep going and then succeed.
|
|
213
|
-
let dsProbes = 0;
|
|
214
|
-
calls = [];
|
|
215
|
-
fetchSpy = spyOn(globalThis, "fetch").mockImplementation((async (input: unknown, init?: { method?: string; body?: string }) => {
|
|
216
|
-
const url = String(input);
|
|
217
|
-
const method = (init?.method || "GET").toUpperCase();
|
|
218
|
-
calls.push({ url, method, body: init?.body });
|
|
219
|
-
if (url.includes("/api/serviceaccounts/search")) return res(true, 200, { serviceAccounts: [] });
|
|
220
|
-
if (url.match(/\/tokens$/) && method === "POST") return res(true, 200, { key: "glsa_mock" });
|
|
221
|
-
if (url.endsWith("/api/serviceaccounts") && method === "POST") return res(true, 201, { id: 42 });
|
|
222
|
-
if (url.includes("/api/datasources")) {
|
|
223
|
-
dsProbes++;
|
|
224
|
-
return dsProbes < 3
|
|
225
|
-
? res(true, 200, [{ id: 3, type: "loki" }]) // not ready yet
|
|
226
|
-
: res(true, 200, [{ id: 8, type: "prometheus" }, { id: 3, type: "loki" }]);
|
|
227
|
-
}
|
|
228
|
-
if (url.includes("/rpc/monitoring_instance_aas_register")) return res(true, 200, {});
|
|
229
|
-
return res(false, 404, {});
|
|
230
|
-
}) as unknown as typeof fetch);
|
|
231
|
-
|
|
232
|
-
const r = await registerAasCollection("apikey-1", "inst-123", {
|
|
233
|
-
grafanaPassword: "pw", instancesPath, vcpus: 8, apiBaseUrl: "https://api.test",
|
|
234
|
-
datasourceMaxAttempts: 6, datasourceRetryDelayMs: 0,
|
|
235
|
-
});
|
|
236
|
-
expect(r.ok).toBe(true);
|
|
237
|
-
expect(dsProbes).toBeGreaterThanOrEqual(3); // kept polling past the not-ready probes
|
|
238
|
-
const rpc = calls.find((c) => c.url.includes("/rpc/monitoring_instance_aas_register"));
|
|
239
|
-
expect(rpc).toBeDefined();
|
|
240
|
-
expect(JSON.parse(rpc!.body!).datasource_id).toBe(8);
|
|
241
|
-
});
|
|
242
|
-
|
|
243
|
-
test(">1 prometheus datasource is a definitive skip: one probe, no retry", async () => {
|
|
244
|
-
// The >1 case is permanent (the datasource count only grows), so the
|
|
245
|
-
// readiness loop must bail after a single probe, not burn its whole budget.
|
|
246
|
-
let dsProbes = 0;
|
|
247
|
-
calls = [];
|
|
248
|
-
fetchSpy = spyOn(globalThis, "fetch").mockImplementation((async (input: unknown, init?: { method?: string; body?: string }) => {
|
|
249
|
-
const url = String(input);
|
|
250
|
-
const method = (init?.method || "GET").toUpperCase();
|
|
251
|
-
calls.push({ url, method, body: init?.body });
|
|
252
|
-
if (url.includes("/api/datasources")) {
|
|
253
|
-
dsProbes++;
|
|
254
|
-
return res(true, 200, [{ id: 8, type: "prometheus" }, { id: 9, type: "prometheus" }, { id: 3, type: "loki" }]);
|
|
255
|
-
}
|
|
256
|
-
return res(false, 404, {});
|
|
257
|
-
}) as unknown as typeof fetch);
|
|
258
|
-
|
|
259
|
-
const r = await registerAasCollection("apikey-1", "inst-123", {
|
|
260
|
-
grafanaPassword: "pw", instancesPath, vcpus: 8, apiBaseUrl: "https://api.test",
|
|
261
|
-
datasourceMaxAttempts: 5, datasourceRetryDelayMs: 0,
|
|
262
|
-
});
|
|
263
|
-
expect(r.ok).toBe(false);
|
|
264
|
-
expect(r.reason).toContain("datasource");
|
|
265
|
-
expect(dsProbes).toBe(1); // bailed after one probe; did NOT retry 5x
|
|
266
|
-
expect(calls.some((c) => c.url.includes("/rpc/monitoring_instance_aas_register"))).toBe(false);
|
|
267
|
-
});
|
|
268
|
-
|
|
269
|
-
test("never-ready datasource: polls exactly maxAttempts times, then ok:false", async () => {
|
|
270
|
-
// Bounds the readiness loop: a never-appearing datasource must probe exactly
|
|
271
|
-
// maxAttempts times (N probes, N-1 sleeps) and then give up — not loop forever.
|
|
272
|
-
let dsProbes = 0;
|
|
273
|
-
calls = [];
|
|
274
|
-
fetchSpy = spyOn(globalThis, "fetch").mockImplementation((async (input: unknown, init?: { method?: string; body?: string }) => {
|
|
275
|
-
const url = String(input);
|
|
276
|
-
const method = (init?.method || "GET").toUpperCase();
|
|
277
|
-
calls.push({ url, method, body: init?.body });
|
|
278
|
-
if (url.includes("/api/datasources")) { dsProbes++; return res(true, 200, [{ id: 3, type: "loki" }]); } // never a prometheus
|
|
279
|
-
return res(false, 404, {});
|
|
280
|
-
}) as unknown as typeof fetch);
|
|
281
|
-
|
|
282
|
-
const r = await registerAasCollection("apikey-1", "inst-123", {
|
|
283
|
-
grafanaPassword: "pw", instancesPath, vcpus: 8, apiBaseUrl: "https://api.test",
|
|
284
|
-
datasourceMaxAttempts: 3, datasourceRetryDelayMs: 0,
|
|
285
|
-
});
|
|
286
|
-
expect(r.ok).toBe(false);
|
|
287
|
-
expect(r.reason).toContain("datasource");
|
|
288
|
-
expect(dsProbes).toBe(3); // bounded: exactly maxAttempts probes
|
|
289
|
-
expect(calls.some((c) => c.url.includes("/rpc/monitoring_instance_aas_register"))).toBe(false);
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
test("mint returning no key → ok:false, no RPC call", async () => {
|
|
293
|
-
installFetch({ mintKey: null });
|
|
294
|
-
const r = await registerAasCollection("apikey-1", "inst-123", {
|
|
295
|
-
grafanaPassword: "pw", instancesPath, vcpus: 8, apiBaseUrl: "https://api.test",
|
|
296
|
-
});
|
|
297
|
-
expect(r.ok).toBe(false);
|
|
298
|
-
expect(r.reason).toContain("service-account token");
|
|
299
|
-
expect(calls.some((c) => c.url.includes("/rpc/monitoring_instance_aas_register"))).toBe(false);
|
|
300
|
-
});
|
|
301
|
-
});
|