@stacksjs/ts-cloud 0.7.93 → 0.7.95

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.
Files changed (71) hide show
  1. package/dist/bin/cli.js +1188 -1184
  2. package/dist/bin/dashboard-server.js +413 -409
  3. package/dist/{chunk-y5wrqyyz.js → chunk-3d4204h1.js} +365 -1
  4. package/dist/{chunk-fbwcv2vf.js → chunk-xp3txtg9.js} +458 -42
  5. package/dist/deploy/dashboard-database.d.ts +91 -3
  6. package/dist/deploy/dashboard-vitess.d.ts +154 -0
  7. package/dist/deploy/dashboard-vitess.test.d.ts +1 -0
  8. package/dist/deploy/index.js +2 -2
  9. package/dist/drivers/hetzner/client.d.ts +96 -0
  10. package/dist/drivers/hetzner/migrate-server.d.ts +124 -0
  11. package/dist/drivers/hetzner/migrate-server.test.d.ts +1 -0
  12. package/dist/drivers/hetzner/role-swap.d.ts +139 -0
  13. package/dist/drivers/hetzner/role-swap.test.d.ts +1 -0
  14. package/dist/drivers/index.js +1 -1
  15. package/dist/drivers/shared/db-provision.d.ts +4 -0
  16. package/dist/drivers/shared/deploy-script.d.ts +17 -1
  17. package/dist/drivers/shared/package-manager.d.ts +2 -0
  18. package/dist/drivers/shared/releases.d.ts +41 -2
  19. package/dist/drivers/shared/vitess-provision.d.ts +133 -0
  20. package/dist/drivers/shared/vitess-provision.test.d.ts +1 -0
  21. package/dist/index.d.ts +1 -1
  22. package/dist/index.js +6 -2
  23. package/dist/ui/access-denied.html +2 -2
  24. package/dist/ui/account/automation.html +3 -3
  25. package/dist/ui/applications/compose.html +4 -4
  26. package/dist/ui/applications/new.html +2 -2
  27. package/dist/ui/data/backups.html +4 -4
  28. package/dist/ui/data/services.html +4 -4
  29. package/dist/ui/data/volumes.html +4 -4
  30. package/dist/ui/index.html +4 -4
  31. package/dist/ui/integrations.html +2 -2
  32. package/dist/ui/operations/alerts.html +4 -4
  33. package/dist/ui/operations/configuration.html +4 -4
  34. package/dist/ui/operations/jobs.html +4 -4
  35. package/dist/ui/operations/maintenance.html +4 -4
  36. package/dist/ui/operations/observability.html +4 -4
  37. package/dist/ui/operations/previews.html +4 -4
  38. package/dist/ui/operations/queue.html +4 -4
  39. package/dist/ui/operations/regions.html +4 -4
  40. package/dist/ui/operations/releases.html +4 -4
  41. package/dist/ui/operations/workloads.html +4 -4
  42. package/dist/ui/security.html +2 -2
  43. package/dist/ui/server/actions.html +4 -4
  44. package/dist/ui/server/activity.html +2 -2
  45. package/dist/ui/server/capacity.html +3 -3
  46. package/dist/ui/server/database.html +123 -7
  47. package/dist/ui/server/deployments.html +4 -4
  48. package/dist/ui/server/firewall.html +4 -4
  49. package/dist/ui/server/fleet.html +4 -4
  50. package/dist/ui/server/logs.html +4 -4
  51. package/dist/ui/server/metrics.html +4 -4
  52. package/dist/ui/server/security.html +2 -2
  53. package/dist/ui/server/services.html +2 -2
  54. package/dist/ui/server/sites.html +4 -4
  55. package/dist/ui/server/ssh-keys.html +4 -4
  56. package/dist/ui/server/team.html +4 -4
  57. package/dist/ui/server/terminal.html +2 -2
  58. package/dist/ui/serverless/alarms.html +4 -4
  59. package/dist/ui/serverless/assets.html +2 -2
  60. package/dist/ui/serverless/cost.html +2 -2
  61. package/dist/ui/serverless/data.html +4 -4
  62. package/dist/ui/serverless/firewall.html +2 -2
  63. package/dist/ui/serverless/functions.html +4 -4
  64. package/dist/ui/serverless/logs.html +4 -4
  65. package/dist/ui/serverless/metrics.html +2 -2
  66. package/dist/ui/serverless/queues.html +4 -4
  67. package/dist/ui/serverless/secrets.html +4 -4
  68. package/dist/ui/serverless/traces.html +4 -4
  69. package/dist/ui/serverless.html +4 -4
  70. package/dist/ui-src/pages/server/database.stx +257 -9
  71. package/package.json +3 -3
@@ -8,6 +8,100 @@ import { requestJson } from '../../functions/requestJson.ts'
8
8
 
9
9
  // Reactive database management - create databases/users, live lists.
10
10
  const engine = state('mysql')
11
+
12
+ // Engines that only exist as an external cluster. There is no on-box engine
13
+ // to administer, so the create/backup forms below are hidden rather than
14
+ // left to fail: they would shell into a pantry socket that cannot exist.
15
+ const EXTERNAL_ENGINES = ['singlestore', 'vitess']
16
+ const isExternal = derived(() => EXTERNAL_ENGINES.includes(engine()))
17
+ const isVitess = derived(() => engine() === 'vitess')
18
+
19
+ // Vitess topology, read from vtgate. Read-only: resharding is a vtctld
20
+ // operation with real blast radius and does not belong behind a button.
21
+ const keyspaces = state([])
22
+ const shards = state([])
23
+ const tablets = state([])
24
+ const unhealthy = state([])
25
+ const missingPrimary = state([])
26
+ const vitessError = state('')
27
+ async function loadVitess() {
28
+ if (!isVitess()) return
29
+ try {
30
+ const res = await fetch('/api/databases/vitess')
31
+ const d = await res.json()
32
+ if (d.ok) {
33
+ keyspaces.set(d.keyspaces || [])
34
+ shards.set(d.shards || [])
35
+ tablets.set(d.tablets || [])
36
+ unhealthy.set(d.unhealthy || [])
37
+ missingPrimary.set(d.missingPrimary || [])
38
+ vitessError.set('')
39
+ } else {
40
+ vitessError.set(d.error || 'Could not reach vtgate.')
41
+ }
42
+ } catch (e) { vitessError.set((e && e.message) || String(e)) }
43
+ }
44
+
45
+ // Online-DDL migrations. Read through vtgate, so this works even on a
46
+ // cluster with no vtctld address configured.
47
+ const migrations = state([])
48
+ const failedMigs = state([])
49
+ const runningMigs = state([])
50
+ async function loadMigrations() {
51
+ if (!isVitess()) return
52
+ try {
53
+ const res = await fetch('/api/databases/vitess/migrations')
54
+ const d = await res.json()
55
+ if (d.ok) { migrations.set(d.migrations || []); failedMigs.set(d.failed || []); runningMigs.set(d.running || []) }
56
+ } catch {}
57
+ }
58
+ async function migrationAction(uuid, action) {
59
+ if (busy()) return
60
+ busy.set(true)
61
+ show(action + ' ' + uuid + '...')
62
+ try {
63
+ const b = await requestJson('/api/databases/vitess/migrations/action', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ uuid, action }) })
64
+ show((b.ok ? 'OK ' + action : 'FAILED') + '\n\n' + (b.stdout || '') + (b.stderr ? '\n' + b.stderr : '') + (b.error ? '\n' + b.error : ''))
65
+ if (b.ok) loadMigrations()
66
+ } catch (e) { show('FAILED\n\n' + ((e && e.message) || e)) }
67
+ finally { busy.set(false) }
68
+ }
69
+
70
+ // Topology operations. These need vtctld, so each reports clearly when no
71
+ // control-plane address is configured rather than looking like a glitch.
72
+ const ksName = state('')
73
+ const ksSharded = state(true)
74
+ const vschemaKeyspace = state('')
75
+ const vschemaJson = state('')
76
+ const ddlSql = state('')
77
+ async function post(path, payload, label) {
78
+ if (busy()) return
79
+ busy.set(true)
80
+ show(label + '...')
81
+ try {
82
+ const b = await requestJson(path, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(payload) })
83
+ show((b.ok ? 'OK ' + label : 'FAILED') + '\n\n' + (b.stdout || '') + (b.stderr ? '\n' + b.stderr : '') + (b.error ? '\n' + b.error : ''))
84
+ return b.ok
85
+ } catch (e) { show('FAILED\n\n' + ((e && e.message) || e)); return false }
86
+ finally { busy.set(false) }
87
+ }
88
+ async function createKeyspace() {
89
+ if (await post('/api/databases/vitess/keyspaces', { name: ksName(), sharded: ksSharded() }, 'create keyspace ' + ksName())) {
90
+ ksName.set(''); load(); loadVitess()
91
+ }
92
+ }
93
+ async function applyVSchema() {
94
+ if (await post('/api/databases/vitess/vschema', { keyspace: vschemaKeyspace(), vschema: vschemaJson() }, 'apply VSchema to ' + vschemaKeyspace()))
95
+ loadVitess()
96
+ }
97
+ async function applySchema() {
98
+ if (await post('/api/databases/vitess/schema', { sql: ddlSql() }, 'apply schema change')) {
99
+ ddlSql.set(''); loadMigrations()
100
+ }
101
+ }
102
+ async function installClient() {
103
+ await post('/api/databases/vitess/install-client', {}, 'install vtctldclient')
104
+ }
11
105
  const databases = state([])
12
106
  const users = state([])
13
107
  const dbError = state('')
@@ -70,7 +164,7 @@ async function backupDb(name) {
70
164
  } catch (e) { show('FAILED\n\n' + ((e && e.message) || e)) }
71
165
  finally { busy.set(false) }
72
166
  }
73
- onMount(() => { load(); loadBackups() })
167
+ onMount(async () => { await load(); loadBackups(); loadVitess(); loadMigrations() })
74
168
  </script>
75
169
  <!DOCTYPE html>
76
170
  <html lang="en" data-theme="dark">
@@ -80,10 +174,24 @@ onMount(() => { load(); loadBackups() })
80
174
  <div class="wrap">
81
175
  <div class="crumbs"><a href="/">Server</a> <span class="sep">/</span> Databases</div>
82
176
  <header class="page">
83
- <div><h1>Databases &amp; users</h1><p>Create databases and app users on the managed engine (<span class="mono">{{ engine() }}</span>).</p></div>
177
+ <div>
178
+ <h1>Databases &amp; users</h1>
179
+ <p @show="!isExternal()">Create databases and app users on the managed engine (<span class="mono">{{ engine() }}</span>).</p>
180
+ <p @show="isExternal()"><span class="mono">{{ engine() }}</span> is an external cluster. This box does not host the engine, so databases and users are managed on the cluster itself.</p>
181
+ </div>
84
182
  </header>
85
183
 
86
- <div class="cols section">
184
+ <div class="section" @show="isVitess()">
185
+ <div class="panel">
186
+ <div class="kv"><span>Engine</span> <b class="mono">Vitess</b></div>
187
+ <div class="kv"><span>Endpoint</span> <b class="mono">vtgate</b></div>
188
+ <div class="kv"><span>Keyspaces</span> <b class="mono">{{ keyspaces().length }}</b></div>
189
+ <div class="kv"><span>Shards</span> <b class="mono">{{ shards().length }}</b></div>
190
+ <div class="kv"><span>Tablets</span> <b class="mono">{{ tablets().length }}</b></div>
191
+ </div>
192
+ </div>
193
+
194
+ <div class="cols section" @show="!isExternal()">
87
195
  <div>
88
196
  <h2>Create database</h2>
89
197
  <div class="panel">
@@ -121,16 +229,18 @@ onMount(() => { load(); loadBackups() })
121
229
 
122
230
  <div class="cols section">
123
231
  <div>
124
- <h2>Databases</h2>
232
+ <h2 @show="!isVitess()">Databases</h2>
233
+ <h2 @show="isVitess()">Keyspaces</h2>
125
234
  <div class="panel">
126
235
  <template :for="n in databases()">
127
- <div class="kv"><span class="mono">{{ n }}</span> <button class="btn ghost sm" type="button" :disabled="busy()" @click="backupDb(n)" :aria-label="'Back up ' + n">Back up</button></div>
236
+ <div class="kv"><span class="mono">{{ n }}</span> <button class="btn ghost sm" type="button" @show="!isExternal()" :disabled="busy()" @click="backupDb(n)" :aria-label="'Back up ' + n">Back up</button></div>
128
237
  </template>
129
238
  <span class="item" @show="databases().length === 0 && dbError() !== ''">{{ dbError() }}</span>
130
- <span class="item" @show="databases().length === 0 && dbError() === ''">no databases</span>
239
+ <span class="item" @show="databases().length === 0 && dbError() === '' && !isVitess()">no databases</span>
240
+ <span class="item" @show="databases().length === 0 && dbError() === '' && isVitess()">no keyspaces</span>
131
241
  </div>
132
242
  </div>
133
- <div>
243
+ <div @show="!isExternal()">
134
244
  <h2>Users</h2>
135
245
  <div class="panel">
136
246
  <div class="svc">
@@ -140,8 +250,145 @@ onMount(() => { load(); loadBackups() })
140
250
  </div>
141
251
  </div>
142
252
  </div>
253
+ <div @show="isVitess()">
254
+ <h2>Shards</h2>
255
+ <div class="panel">
256
+ <template :for="s in shards()">
257
+ <div class="kv"><span class="mono">{{ s.keyspace }}/{{ s.shard }}</span></div>
258
+ </template>
259
+ <span class="item" @show="shards().length === 0">no shards reported</span>
260
+ </div>
261
+ </div>
262
+ </div>
263
+
264
+ <div class="section" @show="isVitess()">
265
+ <h2>Tablets</h2>
266
+
267
+ <div class="panel" @show="missingPrimary().length > 0">
268
+ <div class="compact empty">
269
+ <strong>{{ missingPrimary().length }} shard(s) have no serving PRIMARY</strong>
270
+ <span>Vitess routes writes to a shard's primary. Without one the shard still answers reads and silently fails writes. Check tablet health, then reparent with <span class="mono">vtctldclient PlannedReparentShard</span>.</span>
271
+ </div>
272
+ <template :for="s in missingPrimary()">
273
+ <div class="kv"><span class="mono">{{ s.keyspace }}/{{ s.shard }}</span> <b class="mono">no primary</b></div>
274
+ </template>
275
+ </div>
276
+
277
+ <div class="panel" @show="unhealthy().length > 0">
278
+ <div class="compact empty"><strong>{{ unhealthy().length }} tablet(s) not serving</strong></div>
279
+ <template :for="t in unhealthy()">
280
+ <div class="kv"><span class="mono">{{ t.keyspace }}/{{ t.shard }} {{ t.alias }}</span> <b class="mono">{{ t.type }} {{ t.state }}</b></div>
281
+ </template>
282
+ </div>
283
+
284
+ <div class="panel">
285
+ <template :for="t in tablets()">
286
+ <div class="kv"><span class="mono">{{ t.keyspace }}/{{ t.shard }} {{ t.alias }}</span> <b class="mono" style="font-size:11px;color:var(--txt3)">{{ t.type }} {{ t.state }} {{ t.hostname }}</b></div>
287
+ </template>
288
+ <div class="compact empty" @show="tablets().length === 0 && vitessError() === ''"><strong>No tablets reported</strong><span>vtgate returned no tablets for this cluster.</span></div>
289
+ <div class="compact empty" @show="vitessError() !== ''"><strong>Could not read the Vitess topology</strong><span>{{ vitessError() }}</span></div>
290
+ </div>
291
+ </div>
292
+
293
+ <div class="section" @show="isVitess()">
294
+ <h2>Schema migrations</h2>
295
+
296
+ <div class="panel" @show="failedMigs().length > 0">
297
+ <div class="compact empty"><strong>{{ failedMigs().length }} migration(s) failed</strong><span>A failed online DDL leaves the table unchanged. Retry once the cause is fixed, or cancel to discard it.</span></div>
298
+ <template :for="m in failedMigs()">
299
+ <div class="kv">
300
+ <span class="mono">{{ m.keyspace }}/{{ m.shard }} {{ m.table }}</span>
301
+ <span>
302
+ <button class="btn ghost sm" type="button" :disabled="busy()" @click="migrationAction(m.uuid, 'retry')">Retry</button>
303
+ <button class="btn ghost sm" type="button" :disabled="busy()" @click="migrationAction(m.uuid, 'cleanup')">Clean up</button>
304
+ </span>
305
+ </div>
306
+ </template>
307
+ </div>
308
+
309
+ <div class="panel" @show="runningMigs().length > 0">
310
+ <div class="compact empty"><strong>{{ runningMigs().length }} migration(s) in flight</strong></div>
311
+ <template :for="m in runningMigs()">
312
+ <div class="kv">
313
+ <span class="mono">{{ m.keyspace }}/{{ m.shard }} {{ m.table }} {{ m.progress }}</span>
314
+ <span>
315
+ <button class="btn ghost sm" type="button" :disabled="busy()" @click="migrationAction(m.uuid, 'complete')">Complete</button>
316
+ <button class="btn ghost sm" type="button" :disabled="busy()" @click="migrationAction(m.uuid, 'cancel')">Cancel</button>
317
+ </span>
318
+ </div>
319
+ </template>
320
+ </div>
321
+
322
+ <div class="panel">
323
+ <template :for="m in migrations()">
324
+ <div class="kv"><span class="mono">{{ m.table }} <b style="color:var(--txt3)">{{ m.keyspace }}/{{ m.shard }}</b></span> <b class="mono" style="font-size:11px;color:var(--txt3)">{{ m.status }} {{ m.progress }} {{ m.added }}</b></div>
325
+ </template>
326
+ <div class="compact empty" @show="migrations().length === 0"><strong>No schema migrations</strong><span>Changes applied below appear here with live progress.</span></div>
327
+ </div>
328
+
329
+ <h2>Apply a schema change</h2>
330
+ <div class="panel">
331
+ <form class="form-grid" @submit.prevent="applySchema()">
332
+ <div class="field-block wide"><label for="ddl-sql">DDL</label><input id="ddl-sql" placeholder="ALTER TABLE users ADD COLUMN nickname VARCHAR(64)" autocomplete="off" :value="ddlSql()" @input="ddlSql.set($event.target.value)"></div>
333
+ <div class="form-actions"><button class="btn" type="submit" :disabled="busy()">Apply online</button></div>
334
+ </form>
335
+ <p class="note">Runs as Vitess online DDL: applied shard by shard without locking the keyspace, revertible, and tracked above. A direct (locking) change is a config choice, not a button, because on a sharded keyspace it is an outage rather than a migration.</p>
336
+ </div>
337
+ </div>
338
+
339
+ <div class="section" @show="isVitess()">
340
+ <h2>Topology</h2>
341
+ <div class="cols">
342
+ <div>
343
+ <h3>Create keyspace</h3>
344
+ <div class="panel">
345
+ <form class="form-grid" @submit.prevent="createKeyspace()">
346
+ <div class="field-block wide"><label for="ks-name">Name</label><input id="ks-name" placeholder="commerce" autocomplete="off" :value="ksName()" @input="ksName.set($event.target.value)"></div>
347
+ <div class="field-block"><label for="ks-sharded">Sharded</label>
348
+ <select id="ks-sharded" :value="ksSharded() ? 'yes' : 'no'" @change="ksSharded.set($event.target.value === 'yes')">
349
+ <option value="yes">sharded</option>
350
+ <option value="no">unsharded</option>
351
+ </select>
352
+ </div>
353
+ <div class="form-actions"><button class="btn" type="submit" :disabled="busy()">Create keyspace</button></div>
354
+ </form>
355
+ <p class="note">A sharded keyspace is not routable until its VSchema names a vindex per table. Apply one next.</p>
356
+ </div>
357
+ </div>
358
+ <div>
359
+ <h3>Apply VSchema</h3>
360
+ <div class="panel">
361
+ <form class="form-grid" @submit.prevent="applyVSchema()">
362
+ <div class="field-block"><label for="vs-ks">Keyspace</label>
363
+ <select id="vs-ks" :value="vschemaKeyspace()" @change="vschemaKeyspace.set($event.target.value)">
364
+ <option value="">(select)</option>
365
+ <template :for="k in keyspaces()"><option :value="k">{{ k }}</option></template>
366
+ </select>
367
+ </div>
368
+ <div class="field-block wide"><label for="vs-json">VSchema JSON</label><input id="vs-json" placeholder='paste database/vschema.json' autocomplete="off" :value="vschemaJson()" @input="vschemaJson.set($event.target.value)"></div>
369
+ <div class="form-actions"><button class="btn" type="submit" :disabled="busy()">Apply VSchema</button></div>
370
+ </form>
371
+ <p class="note">Generate it with <span class="mono">buddy generate:vschema</span>, which derives the sharding topology from your model relationships.</p>
372
+ </div>
373
+ </div>
374
+ </div>
375
+ <div class="panel">
376
+ <div class="kv"><span>Control plane</span> <button class="btn ghost sm" type="button" :disabled="busy()" @click="installClient()">Install vtctldclient</button></div>
377
+ <p class="note">Creating a keyspace and applying a VSchema are vtctld operations, so they need <span class="mono">vitess.vtctldAddr</span> configured and the client installed. Everything else on this page works through vtgate alone.</p>
378
+ </div>
379
+ </div>
380
+
381
+ <div class="section" @show="isVitess()">
382
+ <h2>Setup</h2>
383
+ <div class="panel">
384
+ <div class="kv"><span>1. Point the app at vtgate</span> <b class="mono">DB_CONNECTION=vitess, DB_PORT=15306</b></div>
385
+ <div class="kv"><span>2. Generate the VSchema</span> <b class="mono">buddy generate:vschema</b></div>
386
+ <div class="kv"><span>3. Apply it</span> <b class="mono">vtctldclient ApplyVSchema --vschema-file database/vschema.json &lt;keyspace&gt;</b></div>
387
+ <div class="kv"><span>4. Apply schema changes</span> <b class="mono">vtctldclient ApplySchema --sql-file &lt;file&gt; &lt;keyspace&gt;</b></div>
388
+ <p class="note">Port 15306 is vtgate's. Connecting on 3306 reaches one tablet's MySQL and bypasses sharding, which looks like a working connection. A sharded keyspace also rejects foreign keys and AUTO_INCREMENT, so models need <span class="mono">useUuid</span>.</p>
389
+ </div>
143
390
  </div>
144
- <div class="section">
391
+ <div class="section" @show="!isExternal()">
145
392
  <h2>Backups</h2>
146
393
  <div class="panel">
147
394
  <template :for="b in backups()">
@@ -151,7 +398,8 @@ onMount(() => { load(); loadBackups() })
151
398
  </div>
152
399
  </div>
153
400
 
154
- <p class="note">Lists are read live from the engine via the app server. Create operations run as root over the pantry socket; apps connect with the credentials you set here. Per-database dumps are written to <span class="mono">/var/backups/ts-cloud/databases</span>.</p>
401
+ <p class="note" @show="!isExternal()">Lists are read live from the engine via the app server. Create operations run as root over the pantry socket; apps connect with the credentials you set here. Per-database dumps are written to <span class="mono">/var/backups/ts-cloud/databases</span>.</p>
402
+ <p class="note" @show="isExternal()">Read live from the cluster through the app server. Creating databases or users, and taking backups, are cluster-side operations and are not offered here - this box hosts no engine to administer.</p>
155
403
  </div>
156
404
  </body>
157
405
  </html>
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@stacksjs/ts-cloud",
3
3
  "type": "module",
4
- "version": "0.7.93",
4
+ "version": "0.7.95",
5
5
  "description": "A lightweight, performant infrastructure-as-code library and CLI for deploying both server-based (EC2) and serverless applications.",
6
6
  "author": "Chris Breuer <chris@stacksjs.com>",
7
7
  "license": "MIT",
@@ -90,8 +90,8 @@
90
90
  },
91
91
  "dependencies": {
92
92
  "@stacksjs/ts-xml": "^0.1.0",
93
- "@ts-cloud/aws-types": "0.7.93",
94
- "@ts-cloud/core": "0.7.93"
93
+ "@ts-cloud/aws-types": "0.7.95",
94
+ "@ts-cloud/core": "0.7.95"
95
95
  },
96
96
  "devDependencies": {
97
97
  "better-dx": "^0.2.16",