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.
- package/README.md +3 -0
- package/dist/bin/cli.cjs +153 -37
- package/dist/bin/cli.mjs +153 -37
- package/dist/main.cjs +153 -37
- package/dist/main.mjs +153 -37
- package/package.json +1 -1
- package/src/commands/database/index.ts +217 -26
- package/src/commands/template/index.ts +14 -11
|
@@ -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
|
|
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(
|
|
671
|
+
outputJson(connection)
|
|
496
672
|
return
|
|
497
673
|
}
|
|
498
674
|
|
|
499
|
-
printConnectionDetail(
|
|
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
|
|
86
|
+
const hasExplicitRawInput = !!(options.file || options.yaml)
|
|
87
|
+
const isRaw = hasExplicitRawInput || (!template && !stdinIsTTY)
|
|
87
88
|
|
|
88
|
-
if (template &&
|
|
89
|
-
throw new Error('Cannot specify both a template name and --file/--yaml
|
|
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 (
|
|
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
|
-
|
|
401
|
-
|
|
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
|