sealos-cli 1.1.2 → 1.1.3

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.
@@ -1,6 +1,7 @@
1
1
  import { Command } from 'commander'
2
2
  import chalk from 'chalk'
3
- import { createDatabaseClient } from '../../lib/api-client.ts'
3
+ import { createDatabaseClient, resolveDbproviderHost } from '../../lib/api-client.ts'
4
+ import { DEFAULT_SEALOS_REGION, loadAuth } from '../../lib/auth.ts'
4
5
  import { type ApiErrorBody, mapApiError } from '../../lib/errors.ts'
5
6
  import { outputJson, outputTable } from '../../lib/output.ts'
6
7
  import { withAuth, withErrorHandling } from '../../lib/with-auth.ts'
@@ -37,6 +38,46 @@ type DatabaseType = typeof SUPPORTED_DATABASE_TYPES[number]
37
38
  type LogDbType = typeof SUPPORTED_LOG_DB_TYPES[number]
38
39
  type LogType = typeof SUPPORTED_LOG_TYPES[number]
39
40
 
41
+ interface LegacyApiResponse<T> {
42
+ code: number
43
+ message?: string
44
+ data?: T
45
+ error?: unknown
46
+ }
47
+
48
+ interface SecretResponse {
49
+ username: string
50
+ password: string
51
+ host: string
52
+ port: string
53
+ connection: string
54
+ }
55
+
56
+ interface ClientAppConfigResponse {
57
+ domain?: string
58
+ }
59
+
60
+ interface ServiceResponse {
61
+ spec?: {
62
+ ports?: Array<{
63
+ nodePort?: number
64
+ port?: number
65
+ }>
66
+ }
67
+ }
68
+
69
+ interface ConnectionDetails {
70
+ privateConnection?: {
71
+ endpoint?: string
72
+ host?: string
73
+ port?: string
74
+ username?: string
75
+ password?: string
76
+ connectionString?: string
77
+ } | null
78
+ publicConnection?: string | null
79
+ }
80
+
40
81
  export function collectOption (value: string, previous: string[]): string[] {
41
82
  return [...previous, value]
42
83
  }
@@ -70,6 +111,52 @@ export function normalizeDatabaseType (type: string): DatabaseType {
70
111
  return resolved
71
112
  }
72
113
 
114
+ function getDatabaseConnectScheme (type: string): string {
115
+ const databaseType = normalizeDatabaseType(type)
116
+ const schemes: Record<DatabaseType, string> = {
117
+ postgresql: 'postgresql',
118
+ mongodb: 'mongodb',
119
+ 'apecloud-mysql': 'mysql',
120
+ mysql: 'mysql',
121
+ redis: 'redis',
122
+ kafka: 'kafka',
123
+ qdrant: 'qdrant',
124
+ nebula: 'nebula',
125
+ weaviate: 'weaviate',
126
+ milvus: 'milvus',
127
+ pulsar: 'pulsar',
128
+ clickhouse: 'clickhouse'
129
+ }
130
+
131
+ return schemes[databaseType]
132
+ }
133
+
134
+ export function buildConsolePublicConnection (options: {
135
+ dbType: string
136
+ username?: string
137
+ password?: string
138
+ domain?: string
139
+ nodePort?: number | string | null
140
+ }): string | null {
141
+ if (!options.domain || !options.nodePort) return null
142
+
143
+ const scheme = getDatabaseConnectScheme(options.dbType)
144
+ const port = String(options.nodePort)
145
+
146
+ if (scheme === 'kafka' || scheme === 'milvus') {
147
+ return `${options.domain}:${port}`
148
+ }
149
+
150
+ if (!options.username || !options.password) return null
151
+
152
+ let connection = `${scheme}://${options.username}:${options.password}@${options.domain}:${port}`
153
+ if (scheme === 'mongodb' || scheme === 'postgresql') {
154
+ connection += '/?directConnection=true'
155
+ }
156
+
157
+ return connection
158
+ }
159
+
73
160
  export function normalizeLogDbType (type: string): LogDbType {
74
161
  const normalized = normalizeDatabaseType(type)
75
162
  if (!SUPPORTED_LOG_DB_TYPES.includes(normalized as LogDbType)) {
@@ -260,6 +347,103 @@ function printConnectionDetail (connection: any): void {
260
347
  outputTable(rows)
261
348
  }
262
349
 
350
+ function buildPublicAccessResult (action: 'enable-public' | 'disable-public', name: string, connection?: ConnectionDetails | null): Record<string, unknown> {
351
+ return {
352
+ success: true,
353
+ action,
354
+ resource: 'database',
355
+ name,
356
+ status: action === 'enable-public' ? 'enabled' : 'disabled',
357
+ publicConnection: connection?.publicConnection ?? null,
358
+ connection: connection ?? null
359
+ }
360
+ }
361
+
362
+ function getDatabaseProviderHost (): string {
363
+ const override = process.env.SEALOS_DATABASE_HOST?.trim()
364
+ if (override) {
365
+ return override.replace(/\/+$/, '')
366
+ }
367
+
368
+ let authRegion: string | undefined
369
+ try {
370
+ authRegion = loadAuth().region
371
+ } catch {
372
+ authRegion = undefined
373
+ }
374
+
375
+ return resolveDbproviderHost(process.env.SEALOS_REGION || authRegion || DEFAULT_SEALOS_REGION)
376
+ }
377
+
378
+ async function getLegacyApiData<T> (path: string, headers: { Authorization: string }): Promise<T> {
379
+ const url = new URL(path, getDatabaseProviderHost())
380
+ const response = await fetch(url, {
381
+ headers: {
382
+ Authorization: headers.Authorization
383
+ }
384
+ })
385
+ const body = await response.json() as LegacyApiResponse<T>
386
+
387
+ if (!response.ok || body.code !== 200) {
388
+ throw new Error(body.message || `Request failed: ${url.pathname}`)
389
+ }
390
+
391
+ return body.data as T
392
+ }
393
+
394
+ async function fetchConsoleConnectionDetails (
395
+ name: string,
396
+ dbType: string,
397
+ fallbackConnection: ConnectionDetails | null | undefined,
398
+ headers: { Authorization: string }
399
+ ): Promise<ConnectionDetails> {
400
+ const [secret, service, config] = await Promise.all([
401
+ getLegacyApiData<SecretResponse>(`/api/getSecretByName?dbName=${encodeURIComponent(name)}&dbType=${encodeURIComponent(dbType)}&mock=false`, headers),
402
+ getLegacyApiData<ServiceResponse>(`/api/getServiceByName?name=${encodeURIComponent(`${name}-export`)}`, headers).catch(() => null),
403
+ getLegacyApiData<ClientAppConfigResponse>('/api/platform/getClientAppConfig', headers).catch(() => null)
404
+ ])
405
+
406
+ const nodePort = service?.spec?.ports?.find(port => port.nodePort)?.nodePort
407
+ const publicConnection = buildConsolePublicConnection({
408
+ dbType,
409
+ username: secret.username,
410
+ password: secret.password,
411
+ domain: config?.domain,
412
+ nodePort
413
+ }) ?? fallbackConnection?.publicConnection ?? null
414
+
415
+ return {
416
+ privateConnection: {
417
+ endpoint: `${secret.host}:${secret.port}`,
418
+ host: secret.host,
419
+ port: secret.port,
420
+ username: secret.username,
421
+ password: secret.password,
422
+ connectionString: secret.connection
423
+ },
424
+ publicConnection
425
+ }
426
+ }
427
+
428
+ async function loadDatabaseConnectionDetails (
429
+ name: string,
430
+ headers: { Authorization: string }
431
+ ): Promise<ConnectionDetails | null> {
432
+ const client = createDatabaseClient()
433
+ const { data, error, response } = await client.GET('/databases/{databaseName}', {
434
+ headers,
435
+ params: {
436
+ path: { databaseName: name }
437
+ }
438
+ })
439
+
440
+ if (error) throw mapApiError(response.status, error as ApiErrorBody)
441
+
442
+ if (!data.type) return data.connection ?? null
443
+
444
+ return await fetchConsoleConnectionDetails(name, data.type, data.connection, headers)
445
+ }
446
+
263
447
  export function createDatabaseCommand (): Command {
264
448
  const dbCmd = new Command('database')
265
449
  .alias('db')
@@ -479,24 +663,16 @@ export function createDatabaseCommand (): Command {
479
663
  .description('Show database connection details')
480
664
  .option('-o, --output <format>', 'Output format (json|table)', 'json')
481
665
  .action(withAuth({ spinnerText: 'Loading connection details...' }, async (ctx, name: string, options: { output: string }) => {
482
- const client = createDatabaseClient()
483
- const { data, error, response } = await client.GET('/databases/{databaseName}', {
484
- headers: ctx.auth,
485
- params: {
486
- path: { databaseName: name }
487
- }
488
- })
489
-
490
- if (error) throw mapApiError(response.status, error as ApiErrorBody)
666
+ const connection = await loadDatabaseConnectionDetails(name, ctx.auth)
491
667
 
492
668
  ctx.spinner.stop()
493
669
 
494
670
  if (options.output === 'json') {
495
- outputJson(data.connection ?? null)
671
+ outputJson(connection)
496
672
  return
497
673
  }
498
674
 
499
- printConnectionDetail(data.connection)
675
+ printConnectionDetail(connection)
500
676
  }))
501
677
 
502
678
  dbCmd
@@ -823,6 +999,7 @@ export function createDatabaseCommand (): Command {
823
999
 
824
1000
  dbCmd
825
1001
  .command('enable-public <name>')
1002
+ .alias('expose')
826
1003
  .description('Enable public access for a database')
827
1004
  .option('-o, --output <format>', 'Output format (json|table)', 'json')
828
1005
  .action(withAuth({ spinnerText: 'Enabling public access...' }, async (ctx, name: string, options: { output: string }) => {
@@ -835,22 +1012,20 @@ export function createDatabaseCommand (): Command {
835
1012
  })
836
1013
 
837
1014
  if (error) throw mapApiError(response.status, error as ApiErrorBody)
1015
+ const connection = await loadDatabaseConnectionDetails(name, ctx.auth)
1016
+
838
1017
  if (options.output === 'json') {
839
1018
  ctx.spinner.stop()
840
- outputJson({
841
- success: true,
842
- action: 'enable-public',
843
- resource: 'database',
844
- name,
845
- status: 'enabled'
846
- })
1019
+ outputJson(buildPublicAccessResult('enable-public', name, connection))
847
1020
  return
848
1021
  }
849
1022
  ctx.spinner.succeed(`Public access enabled for "${name}"`)
1023
+ printConnectionDetail(connection)
850
1024
  }))
851
1025
 
852
1026
  dbCmd
853
1027
  .command('disable-public <name>')
1028
+ .alias('unexpose')
854
1029
  .description('Disable public access for a database')
855
1030
  .option('-o, --output <format>', 'Output format (json|table)', 'json')
856
1031
  .action(withAuth({ spinnerText: 'Disabling public access...' }, async (ctx, name: string, options: { output: string }) => {
@@ -865,13 +1040,7 @@ export function createDatabaseCommand (): Command {
865
1040
  if (error) throw mapApiError(response.status, error as ApiErrorBody)
866
1041
  if (options.output === 'json') {
867
1042
  ctx.spinner.stop()
868
- outputJson({
869
- success: true,
870
- action: 'disable-public',
871
- resource: 'database',
872
- name,
873
- status: 'disabled'
874
- })
1043
+ outputJson(buildPublicAccessResult('disable-public', name))
875
1044
  return
876
1045
  }
877
1046
  ctx.spinner.succeed(`Public access disabled for "${name}"`)
@@ -1001,5 +1170,27 @@ export function createDatabaseCommand (): Command {
1001
1170
  outputTable(rows)
1002
1171
  }))
1003
1172
 
1173
+ dbCmd
1174
+ .command('* [args...]', { hidden: true })
1175
+ .option('-o, --output <format>', 'Output format (json|table)', 'json')
1176
+ .allowUnknownOption()
1177
+ .action(async (args: string[], options: { output: string }) => {
1178
+ const [name, operation, ...rest] = args
1179
+ const aliases: Record<string, string> = {
1180
+ connection: 'connection',
1181
+ connect: 'connection',
1182
+ 'enable-public': 'enable-public',
1183
+ expose: 'expose',
1184
+ 'disable-public': 'disable-public',
1185
+ unexpose: 'unexpose'
1186
+ }
1187
+ const command = operation ? aliases[operation] : undefined
1188
+ if (!name || !command) {
1189
+ throw new Error('Unknown database command. Use "sealos-cli database --help" to list supported commands.')
1190
+ }
1191
+
1192
+ await dbCmd.parseAsync([command, name, ...rest, '--output', options.output], { from: 'user' })
1193
+ })
1194
+
1004
1195
  return dbCmd
1005
1196
  }
@@ -2,7 +2,7 @@ import { Command } from 'commander'
2
2
  import chalk from 'chalk'
3
3
  import { readFileSync } from 'node:fs'
4
4
  import { createTemplateClient } from '../../lib/api-client.ts'
5
- import { type ApiErrorBody, mapApiError } from '../../lib/errors.ts'
5
+ import { type ApiErrorBody, handleError, mapApiError } from '../../lib/errors.ts'
6
6
  import { outputJson, outputTable } from '../../lib/output.ts'
7
7
  import { withAuth, withErrorHandling } from '../../lib/with-auth.ts'
8
8
 
@@ -83,18 +83,16 @@ export function resolveTemplateDeployMode (
83
83
  options: TemplateDeployOptions,
84
84
  stdinIsTTY: boolean = process.stdin.isTTY
85
85
  ): TemplateDeployMode {
86
- const isRaw = !!(options.file || options.yaml || !stdinIsTTY)
86
+ const hasExplicitRawInput = !!(options.file || options.yaml)
87
+ const isRaw = hasExplicitRawInput || (!template && !stdinIsTTY)
87
88
 
88
- if (template && isRaw) {
89
- throw new Error('Cannot specify both a template name and --file/--yaml/stdin. Use one or the other.')
89
+ if (template && hasExplicitRawInput) {
90
+ throw new Error('Cannot specify both a template name and --file/--yaml. Use one or the other.')
90
91
  }
91
92
  if (!template && !isRaw) {
92
93
  throw new Error('Provide a template name or use --file/--yaml/stdin to supply raw YAML.')
93
94
  }
94
95
  if (template) {
95
- if (!options.name) {
96
- throw new Error('--name is required when deploying from the template catalog.')
97
- }
98
96
  if (options.dryRun) {
99
97
  throw new Error('--dry-run is only supported for raw template deploys (--file, --yaml, or stdin).')
100
98
  }
@@ -109,7 +107,7 @@ export function buildCatalogTemplateDeployBody (
109
107
  options: Pick<TemplateDeployOptions, 'name' | 'set'>
110
108
  ): { name: string; template: string; args?: Record<string, string> } {
111
109
  const body: { name: string; template: string; args?: Record<string, string> } = {
112
- name: options.name!,
110
+ name: options.name ?? template,
113
111
  template
114
112
  }
115
113
  if (options.set.length > 0) {
@@ -380,7 +378,7 @@ export function createTemplateCommand (): Command {
380
378
  tplCmd
381
379
  .command('deploy [template]')
382
380
  .description('Deploy a template (from catalog or raw YAML)')
383
- .option('--name <name>', 'Instance name (required when deploying from catalog)')
381
+ .option('--name <name>', 'Instance name (defaults to the catalog template name)')
384
382
  .option('--file <path>', 'Path to template YAML file')
385
383
  .option('--yaml <yaml>', 'Template YAML string')
386
384
  .option('--set <KEY=VALUE...>', 'Set template arguments', (val: string, prev: string[]) => [...prev, val], [] as string[])
@@ -389,6 +387,7 @@ export function createTemplateCommand (): Command {
389
387
  .addHelpText('after', `
390
388
  Examples:
391
389
  Catalog:
390
+ sealos-cli template deploy rybbit
392
391
  sealos-cli template deploy perplexica --name my-app --set OPENAI_API_KEY=xxx
393
392
 
394
393
  Raw:
@@ -397,8 +396,12 @@ Examples:
397
396
  cat template.yaml | sealos-cli template deploy --dry-run
398
397
  `)
399
398
  .action(async (template: string | undefined, options: TemplateDeployOptions) => {
400
- const mode = resolveTemplateDeployMode(template, options)
401
- await deployTemplate(template, options, mode)
399
+ try {
400
+ const mode = resolveTemplateDeployMode(template, options)
401
+ await deployTemplate(template, options, mode)
402
+ } catch (error) {
403
+ handleError(error)
404
+ }
402
405
  })
403
406
 
404
407
  return tplCmd