@zenifra/cli 0.2.4 → 0.2.5
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 +22 -3
- package/bin/zenifra.mjs +533 -27
- package/examples/valkey-cache-project.json +14 -0
- package/examples/valkey-key-value-project.json +18 -0
- package/examples/valkey-queue-project.json +18 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -51,6 +51,7 @@ zenifra orgs
|
|
|
51
51
|
zenifra org set
|
|
52
52
|
zenifra plans
|
|
53
53
|
zenifra plans --type http
|
|
54
|
+
zenifra plans --type valkey
|
|
54
55
|
zenifra plans --type storage --json
|
|
55
56
|
zenifra create project
|
|
56
57
|
zenifra create project --name <name> --plan free --payment-mode hourly --config @examples/http-project.json
|
|
@@ -58,8 +59,16 @@ zenifra create project --name <name> --plan basic --payment-mode hourly --config
|
|
|
58
59
|
zenifra create project --name <name> --plan premium --payment-mode hourly --config @examples/http-autoscaling-project.json
|
|
59
60
|
zenifra create project --name <name> --plan db-basic --payment-mode monthly --config @examples/postgresql-project.json
|
|
60
61
|
zenifra create project --name <name> --plan db-basic --payment-mode monthly --config @examples/mariadb-project.json
|
|
62
|
+
zenifra create project --name <name> --plan db-free --payment-mode hourly --config @examples/valkey-key-value-project.json
|
|
63
|
+
zenifra create project --name <name> --plan cache-free --payment-mode hourly --config @examples/valkey-cache-project.json
|
|
64
|
+
zenifra create project --name <name> --plan queue-free --payment-mode hourly --config @examples/valkey-queue-project.json
|
|
61
65
|
zenifra projects --type http --page 1 --limit 15
|
|
66
|
+
zenifra projects --type valkey --page 1 --limit 15
|
|
62
67
|
zenifra project info --project <project-id>
|
|
68
|
+
zenifra valkey status --project <project-id>
|
|
69
|
+
zenifra valkey connection --project <project-id>
|
|
70
|
+
zenifra valkey credentials rotate --project <project-id> --wait
|
|
71
|
+
zenifra valkey credentials status --project <project-id> --operation <operation-id>
|
|
63
72
|
zenifra project url --project <project-id>
|
|
64
73
|
zenifra project logs --project <project-id> --instance <instance-id>
|
|
65
74
|
zenifra project metrics --project <project-id> --instance <instance-id>
|
|
@@ -125,7 +134,7 @@ zenifra plans --type database
|
|
|
125
134
|
zenifra plans --type storage --json
|
|
126
135
|
```
|
|
127
136
|
|
|
128
|
-
`zenifra plans` funciona sem autenticacao e mostra os catalogos publicos de HTTP, banco e
|
|
137
|
+
`zenifra plans` funciona sem autenticacao e mostra os catalogos publicos de HTTP, banco, armazenamento e Valkey. Use `--type valkey` para consultar Key Value, Cache e Queue.
|
|
129
138
|
|
|
130
139
|
Valores de variaveis de ambiente sao mascarados por padrao, inclusive em `--json`.
|
|
131
140
|
Use `--show-values` apenas quando precisar inspecionar os valores completos.
|
|
@@ -193,6 +202,9 @@ Use os arquivos em `examples/` como base para `zenifra create project`:
|
|
|
193
202
|
- `examples/http-autoscaling-project.json`: projeto HTTP pago criado com auto-scaling
|
|
194
203
|
- `examples/postgresql-project.json`: projeto PostgreSQL
|
|
195
204
|
- `examples/mariadb-project.json`: projeto MariaDB
|
|
205
|
+
- `examples/valkey-key-value-project.json`: projeto Valkey Key Value com armazenamento persistente
|
|
206
|
+
- `examples/valkey-cache-project.json`: projeto Valkey Cache sem armazenamento persistente
|
|
207
|
+
- `examples/valkey-queue-project.json`: projeto Valkey Queue com armazenamento persistente
|
|
196
208
|
|
|
197
209
|
Se voce rodar apenas `zenifra create project`, a CLI abre um wizard interativo estilo `npm init` e pergunta todos os campos guiados. Cada pergunta mostra:
|
|
198
210
|
|
|
@@ -205,6 +217,7 @@ O wizard atual cobre:
|
|
|
205
217
|
- projetos `http` com origem `github` ou `oci`
|
|
206
218
|
- projetos `postgresql`
|
|
207
219
|
- projetos `mariadb`
|
|
220
|
+
- projetos `valkey` nos perfis `key_value`, `cache` e `queue`
|
|
208
221
|
|
|
209
222
|
`zenifra create project` nao assume valores default para `--plan` e `--payment-mode`.
|
|
210
223
|
Configs HTTP nao interativas tambem devem informar `config.exposure`; use `public` para criar rota/dominio publico ou `private` para manter a aplicacao sem exposicao na internet.
|
|
@@ -213,9 +226,12 @@ Antes de escolher um plano com o usuario, compare os catalogos com `zenifra plan
|
|
|
213
226
|
Valores aceitos:
|
|
214
227
|
|
|
215
228
|
- `payment_mode`: `hourly`, `monthly`, `yearly`
|
|
216
|
-
- `type_project` no `config`: `http`, `postgresql`, `mariadb`
|
|
229
|
+
- `type_project` no `config`: `http`, `postgresql`, `mariadb`, `valkey`
|
|
217
230
|
- `exposure` no `config` HTTP: `public`, `private`
|
|
218
|
-
- `plan`:
|
|
231
|
+
- `plan`: consulte `zenifra plans` para os planos atuais; Valkey usa `db-*` para Key Value, `cache-*` para Cache e `queue-*` para Queue
|
|
232
|
+
- `config.profile` em projetos Valkey: `key_value`, `cache` ou `queue`
|
|
233
|
+
- `config.version` em projetos Valkey: a versão retornada por `zenifra plans --type valkey`
|
|
234
|
+
- `config.storage` em projetos Valkey: obrigatório e persistente para Key Value/Queue; omitido para Cache
|
|
219
235
|
- `config.github.runtime` (quando houver GitHub em projeto HTTP): `nodejs` ou `python`
|
|
220
236
|
- `config.autoscaling` (somente HTTP pago): `enabled: true`, `max_instances` maior ou igual a `config.instances` e alvos opcionais de CPU/memoria entre 1 e 100
|
|
221
237
|
|
|
@@ -226,6 +242,9 @@ Observacoes do wizard:
|
|
|
226
242
|
- na criacao com auto-scaling, `config.instances` e o minimo inicial e `config.autoscaling.max_instances` e o maximo
|
|
227
243
|
- em projetos de banco, o wizard nao pergunta `username`, `password` nem `database name`
|
|
228
244
|
- em projetos de banco, a CLI preenche apenas campos tecnicos minimos exigidos pela validacao atual da API
|
|
245
|
+
- em projetos Valkey, a capacidade é definida pelo plano e a CLI não pergunta instâncias, imagem, variáveis de ambiente ou exposição HTTP
|
|
246
|
+
- a conexão mascarada pode ser consultada a qualquer momento; a credencial completa aparece apenas na criação ou em uma rotação concluída
|
|
247
|
+
- `valkey credentials rotate` retorna uma operação assíncrona; use `--wait` ou `valkey credentials status` para acompanhar
|
|
229
248
|
|
|
230
249
|
## Regressao manual de auto-scaling em staging
|
|
231
250
|
|
package/bin/zenifra.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
3
4
|
import { mkdir, readFile, rm, writeFile, chmod } from 'node:fs/promises';
|
|
4
5
|
import { existsSync } from 'node:fs';
|
|
5
6
|
import { dirname, join } from 'node:path';
|
|
@@ -13,6 +14,7 @@ const DOCS_RUNTIME_URL = `${DOCS_BASE_URL}/docs/runtime`;
|
|
|
13
14
|
const DOCS_CREATE_HTTP_URL = `${DOCS_BASE_URL}/docs/how-use-zenifra/http/how-to-create-a-http-project-at-zenifra-via-console`;
|
|
14
15
|
const DOCS_CREATE_POSTGRESQL_URL = `${DOCS_BASE_URL}/docs/how-use-zenifra/database/how-to-create-a-postgresql-project-at-zenifra-via-console`;
|
|
15
16
|
const DOCS_CREATE_MARIADB_URL = `${DOCS_BASE_URL}/docs/how-use-zenifra/database/how-to-create-a-mariadb-project-at-zenifra-via-console`;
|
|
17
|
+
const DOCS_CREATE_VALKEY_URL = `${DOCS_BASE_URL}/docs/managed-services/valkey`;
|
|
16
18
|
const DOCS_CONFIGURATION_URL = `${DOCS_BASE_URL}/docs/configuration`;
|
|
17
19
|
const DOCS_DATABASE_CONFIGURATION_URL = `${DOCS_BASE_URL}/docs/database/configuration/configuration`;
|
|
18
20
|
const DOCS_PAYMENTS_URL = `${DOCS_BASE_URL}/docs/payments/payments`;
|
|
@@ -41,6 +43,7 @@ const KNOWN_FLAG_NAMES = new Set([
|
|
|
41
43
|
'from',
|
|
42
44
|
'help',
|
|
43
45
|
'image',
|
|
46
|
+
'idempotencyKey',
|
|
44
47
|
'instance',
|
|
45
48
|
'interval',
|
|
46
49
|
'json',
|
|
@@ -52,6 +55,7 @@ const KNOWN_FLAG_NAMES = new Set([
|
|
|
52
55
|
'mode',
|
|
53
56
|
'name',
|
|
54
57
|
'org',
|
|
58
|
+
'operation',
|
|
55
59
|
'page',
|
|
56
60
|
'password',
|
|
57
61
|
'paymentMode',
|
|
@@ -67,6 +71,7 @@ const KNOWN_FLAG_NAMES = new Set([
|
|
|
67
71
|
'type',
|
|
68
72
|
'value',
|
|
69
73
|
'view',
|
|
74
|
+
'wait',
|
|
70
75
|
]);
|
|
71
76
|
const ALLOWED_PLAN_VALUES = new Set([
|
|
72
77
|
'free',
|
|
@@ -82,9 +87,19 @@ const ALLOWED_PLAN_VALUES = new Set([
|
|
|
82
87
|
'db-basic',
|
|
83
88
|
'db-premium',
|
|
84
89
|
'db-enterprise',
|
|
90
|
+
'cache-free',
|
|
91
|
+
'cache-starter',
|
|
92
|
+
'cache-basic',
|
|
93
|
+
'cache-premium',
|
|
94
|
+
'cache-enterprise',
|
|
95
|
+
'queue-free',
|
|
96
|
+
'queue-starter',
|
|
97
|
+
'queue-basic',
|
|
98
|
+
'queue-premium',
|
|
99
|
+
'queue-enterprise',
|
|
85
100
|
]);
|
|
86
101
|
const ALLOWED_PAYMENT_MODE_VALUES = new Set(['hourly', 'monthly', 'yearly']);
|
|
87
|
-
const ALLOWED_TYPE_PROJECT_VALUES = new Set(['http', 'postgresql', 'mariadb']);
|
|
102
|
+
const ALLOWED_TYPE_PROJECT_VALUES = new Set(['http', 'postgresql', 'mariadb', 'valkey']);
|
|
88
103
|
const ALLOWED_RUNTIME_VALUES = new Set(['nodejs', 'python']);
|
|
89
104
|
const ALLOWED_EXPOSURE_VALUES = new Set(['public', 'private']);
|
|
90
105
|
const GITHUB_RUNTIME_VERSIONS = {
|
|
@@ -115,7 +130,8 @@ const WIZARD_DATABASE_PLAN_OPTIONS = [
|
|
|
115
130
|
'db-free',
|
|
116
131
|
];
|
|
117
132
|
const WIZARD_PAYMENT_MODE_OPTIONS = ['hourly', 'monthly', 'yearly'];
|
|
118
|
-
const WIZARD_TYPE_PROJECT_OPTIONS = ['http', 'postgresql', 'mariadb'];
|
|
133
|
+
const WIZARD_TYPE_PROJECT_OPTIONS = ['http', 'postgresql', 'mariadb', 'valkey'];
|
|
134
|
+
const WIZARD_VALKEY_PROFILE_OPTIONS = ['key_value', 'cache', 'queue'];
|
|
119
135
|
const WIZARD_HTTP_EXPOSURE_OPTIONS = ['public', 'private'];
|
|
120
136
|
let promptInterface;
|
|
121
137
|
let pipedInputPromise;
|
|
@@ -153,10 +169,14 @@ Usage:
|
|
|
153
169
|
zenifra profile remove <name> [--json]
|
|
154
170
|
zenifra orgs [--json]
|
|
155
171
|
zenifra org set [--org <id>]
|
|
156
|
-
zenifra plans [--type <all|http|database|storage>] [--json]
|
|
172
|
+
zenifra plans [--type <all|http|database|storage|valkey>] [--json]
|
|
157
173
|
zenifra create project
|
|
158
174
|
zenifra create project --name <name> --plan <plan> --payment-mode <mode> --config <json|@file> [--description <text>] [--org <id>] [--json]
|
|
159
|
-
zenifra projects [--json] [--org <id>] [--type <http|postgresql|mariadb>] [--page <n>] [--limit <n>]
|
|
175
|
+
zenifra projects [--json] [--org <id>] [--type <http|postgresql|mariadb|valkey>] [--page <n>] [--limit <n>]
|
|
176
|
+
zenifra valkey status --project <id> [--json]
|
|
177
|
+
zenifra valkey connection --project <id> [--json]
|
|
178
|
+
zenifra valkey credentials rotate --project <id> [--idempotency-key <key>] [--wait] [--interval <seconds>] [--timeout <seconds>] [--json]
|
|
179
|
+
zenifra valkey credentials status --project <id> --operation <id> [--json]
|
|
160
180
|
zenifra project info --project <id> [--json]
|
|
161
181
|
zenifra project url --project <id> [--json]
|
|
162
182
|
zenifra project logs --project <id> [--instance <id>] [--json]
|
|
@@ -319,16 +339,65 @@ const HELP_SPECS = [
|
|
|
319
339
|
},
|
|
320
340
|
{
|
|
321
341
|
command: 'plans',
|
|
322
|
-
usage: 'zenifra plans [--type <all|http|database|storage>] [--json]',
|
|
323
|
-
description: 'Lista os catalogos publicos de preco de planos HTTP, banco e
|
|
324
|
-
flags: ['--type <type> Filtra o catalogo: all, http, database ou
|
|
325
|
-
examples: ['zenifra plans', 'zenifra plans --type http', 'zenifra plans --type
|
|
342
|
+
usage: 'zenifra plans [--type <all|http|database|storage|valkey>] [--json]',
|
|
343
|
+
description: 'Lista os catalogos publicos de preco de planos HTTP, banco, armazenamento e Valkey.',
|
|
344
|
+
flags: ['--type <type> Filtra o catalogo: all, http, database, storage, valkey, key-value, cache ou queue.', '--json Imprime a resposta em JSON.'],
|
|
345
|
+
examples: ['zenifra plans', 'zenifra plans --type http', 'zenifra plans --type valkey --json'],
|
|
326
346
|
output: 'HTTP\nPlano Hora Mes Ano Recursos\nfree R$ 0,00 R$ 0,00 R$ 0,00 1 GB Armazenamento Efemero',
|
|
327
347
|
jsonOutput: '{"http":[{"plan":"free","prices":{"hourly":0,"monthly":0,"yearly":0},"features":["1 GB Armazenamento Efemero"]}],"database":[],"storage":[]}',
|
|
328
348
|
},
|
|
349
|
+
{
|
|
350
|
+
command: 'valkey',
|
|
351
|
+
usage: 'zenifra valkey\n zenifra valkey status --project <id> [--json]\n zenifra valkey connection --project <id> [--json]\n zenifra valkey credentials rotate --project <id> [--idempotency-key <key>] [--wait] [--interval <seconds>] [--timeout <seconds>] [--json]\n zenifra valkey credentials status --project <id> --operation <id> [--json]',
|
|
352
|
+
description: 'Consulta projetos Valkey e gerencia suas conexoes e credenciais.',
|
|
353
|
+
examples: ['zenifra valkey status --project proj_1', 'zenifra valkey connection --project proj_1', 'zenifra valkey credentials rotate --project proj_1 --wait'],
|
|
354
|
+
output: 'Zenifra CLI - valkey',
|
|
355
|
+
notes: ['A string de conexao completa aparece somente quando uma criacao ou rotacao a disponibiliza.'],
|
|
356
|
+
},
|
|
357
|
+
{
|
|
358
|
+
command: 'valkey status',
|
|
359
|
+
usage: 'zenifra valkey status --project <id> [--json]',
|
|
360
|
+
description: 'Mostra estado, perfil, versao e persistencia de um projeto Valkey.',
|
|
361
|
+
flags: ['--project <id> ID do projeto.', '--json Imprime a resposta em JSON.'],
|
|
362
|
+
examples: ['zenifra valkey status --project proj_1'],
|
|
363
|
+
output: 'Status Perfil Versao Persistente Instancias\nrunning cache 9.1.1 nao 1',
|
|
364
|
+
},
|
|
365
|
+
{
|
|
366
|
+
command: 'valkey connection',
|
|
367
|
+
usage: 'zenifra valkey connection --project <id> [--json]',
|
|
368
|
+
description: 'Mostra os dados de conexao de um projeto Valkey sem revelar a credencial.',
|
|
369
|
+
flags: ['--project <id> ID do projeto.', '--json Imprime a resposta em JSON.'],
|
|
370
|
+
examples: ['zenifra valkey connection --project proj_1'],
|
|
371
|
+
output: 'Host Porta TLS Conexao\nvalkey.example.com 6380 sim valkeys://default:********@valkey.example.com:6380/0',
|
|
372
|
+
notes: ['Use a credencial recebida na criacao ou em uma rotacao concluida.'],
|
|
373
|
+
},
|
|
374
|
+
{
|
|
375
|
+
command: 'valkey credentials',
|
|
376
|
+
usage: 'zenifra valkey credentials\n zenifra valkey credentials rotate --project <id> [--idempotency-key <key>] [--wait] [--interval <seconds>] [--timeout <seconds>] [--json]\n zenifra valkey credentials status --project <id> --operation <id> [--json]',
|
|
377
|
+
description: 'Agrupa comandos de renovacao e acompanhamento de credenciais Valkey.',
|
|
378
|
+
examples: ['zenifra valkey credentials rotate --project proj_1', 'zenifra valkey credentials status --project proj_1 --operation operation_1'],
|
|
379
|
+
output: 'Zenifra CLI - valkey credentials',
|
|
380
|
+
},
|
|
381
|
+
{
|
|
382
|
+
command: 'valkey credentials rotate',
|
|
383
|
+
usage: 'zenifra valkey credentials rotate --project <id> [--idempotency-key <key>] [--wait] [--interval <seconds>] [--timeout <seconds>] [--json]',
|
|
384
|
+
description: 'Solicita a renovacao da credencial e retorna uma operacao acompanhavel.',
|
|
385
|
+
flags: ['--project <id> ID do projeto.', '--idempotency-key <key> Chave para repetir a mesma solicitacao.', '--wait Aguarda a conclusao.', '--interval <seconds> Intervalo do polling. Padrao: 2.', '--timeout <seconds> Timeout total. Padrao: 900.', '--json Imprime a resposta em JSON.'],
|
|
386
|
+
examples: ['zenifra valkey credentials rotate --project proj_1', 'zenifra valkey credentials rotate --project proj_1 --wait --timeout 120'],
|
|
387
|
+
output: 'Operacao aceita: rotation_123\nEstado: accepted',
|
|
388
|
+
notes: ['Salve a nova string de conexao quando a operacao atingir completed.'],
|
|
389
|
+
},
|
|
390
|
+
{
|
|
391
|
+
command: 'valkey credentials status',
|
|
392
|
+
usage: 'zenifra valkey credentials status --project <id> --operation <id> [--json]',
|
|
393
|
+
description: 'Consulta o estado de uma renovacao de credencial Valkey.',
|
|
394
|
+
flags: ['--project <id> ID do projeto.', '--operation <id> ID da operacao.', '--json Imprime a resposta em JSON.'],
|
|
395
|
+
examples: ['zenifra valkey credentials status --project proj_1 --operation rotation_123'],
|
|
396
|
+
output: 'Operacao: rotation_123\nEstado: completed',
|
|
397
|
+
},
|
|
329
398
|
{
|
|
330
399
|
command: 'projects',
|
|
331
|
-
usage: 'zenifra projects [--json] [--org <id>] [--type <http|postgresql|mariadb>] [--page <n>] [--limit <n>]',
|
|
400
|
+
usage: 'zenifra projects [--json] [--org <id>] [--type <http|postgresql|mariadb|valkey>] [--page <n>] [--limit <n>]',
|
|
332
401
|
description: 'Lista projetos da organizacao ativa ou da API key.',
|
|
333
402
|
flags: ['--json Imprime a resposta em JSON.', '--org <id> Usa uma organizacao especifica com sessao de usuario.', '--type <type> Filtra por tipo de projeto.', '--page <n> Pagina. Padrao: 1.', '--limit <n> Itens por pagina. Padrao: 15.'],
|
|
334
403
|
examples: ['zenifra projects --type http --page 1 --limit 15', 'zenifra projects --json'],
|
|
@@ -337,9 +406,9 @@ const HELP_SPECS = [
|
|
|
337
406
|
},
|
|
338
407
|
{
|
|
339
408
|
command: 'create project',
|
|
340
|
-
usage: 'zenifra create project\n zenifra create project --name <name> --plan <plan> --payment-mode <mode> --config <json|@file> [--description <text>] [--org <id>] [--json]',
|
|
409
|
+
usage: 'zenifra create project\n zenifra create project --name <name> --plan <plan> --payment-mode <mode> --config <json|@file> [--description <text>] [--idempotency-key <key>] [--org <id>] [--json]',
|
|
341
410
|
description: 'Cria um projeto via wizard interativo ou via payload de configuracao JSON.',
|
|
342
|
-
flags: ['--name <name>
|
|
411
|
+
flags: ['--name <name> Nome do projeto.', '--plan <plan> Plano do projeto.', '--payment-mode <mode> Modo de pagamento.', '--config <json|@file> JSON inline ou arquivo.', '--description <text> Descricao opcional.', '--idempotency-key <key> Chave para repetir a mesma criacao com seguranca.', '--json Imprime a resposta em JSON.'],
|
|
343
412
|
examples: ['zenifra create project', 'zenifra create project --name api-web --plan free --payment-mode hourly --config @examples/http-project.json'],
|
|
344
413
|
output: 'Campo Valor\n------- --------------------------------------\nProjeto 507f1f77bcf86cd799439012\nDominio https://api-web.client.zenifra.com',
|
|
345
414
|
jsonOutput: '{"status":"success","data":{"id":"507f1f77bcf86cd799439012","name":"api-web"}}',
|
|
@@ -654,7 +723,7 @@ function removedProjectsCreateMessage() {
|
|
|
654
723
|
}
|
|
655
724
|
|
|
656
725
|
function isNamespaceCommand(command) {
|
|
657
|
-
return ['auth', 'profile', 'project', 'org'].includes(command);
|
|
726
|
+
return ['auth', 'profile', 'project', 'org', 'valkey'].includes(command);
|
|
658
727
|
}
|
|
659
728
|
|
|
660
729
|
function parseArgs(argv) {
|
|
@@ -1029,6 +1098,11 @@ function estimateWizardTotal(state = {}) {
|
|
|
1029
1098
|
return total;
|
|
1030
1099
|
}
|
|
1031
1100
|
|
|
1101
|
+
if (typeProject === 'valkey') {
|
|
1102
|
+
total += 3;
|
|
1103
|
+
return total;
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1032
1106
|
return 18;
|
|
1033
1107
|
}
|
|
1034
1108
|
|
|
@@ -1346,6 +1420,7 @@ async function request(session, flags, method, path, {
|
|
|
1346
1420
|
orgId,
|
|
1347
1421
|
tokenRequired = true,
|
|
1348
1422
|
credential: credentialOverride,
|
|
1423
|
+
headers: extraHeaders = {},
|
|
1349
1424
|
} = {}) {
|
|
1350
1425
|
const credential = credentialOverride || resolveCredential(session);
|
|
1351
1426
|
if (tokenRequired && !credential) {
|
|
@@ -1356,6 +1431,14 @@ async function request(session, flags, method, path, {
|
|
|
1356
1431
|
if (body !== undefined) {
|
|
1357
1432
|
headers['Content-Type'] = 'application/json';
|
|
1358
1433
|
}
|
|
1434
|
+
for (const [name, value] of Object.entries(extraHeaders)) {
|
|
1435
|
+
if (value === undefined) continue;
|
|
1436
|
+
const normalizedName = name.toLowerCase();
|
|
1437
|
+
if (['authorization', 'x-organization-id', 'content-type'].includes(normalizedName)) {
|
|
1438
|
+
throw new CliError(`Header nao permitido: ${name}.`);
|
|
1439
|
+
}
|
|
1440
|
+
headers[name] = String(value);
|
|
1441
|
+
}
|
|
1359
1442
|
if (credential?.token) {
|
|
1360
1443
|
headers.Authorization = `Bearer ${credential.token}`;
|
|
1361
1444
|
}
|
|
@@ -1508,20 +1591,33 @@ function printEnvs(envs, flags) {
|
|
|
1508
1591
|
}
|
|
1509
1592
|
|
|
1510
1593
|
function printProject(project) {
|
|
1511
|
-
const
|
|
1594
|
+
const managedService = project.managed_service;
|
|
1595
|
+
const isValkey = project.type_project === 'valkey' || managedService?.engine === 'valkey';
|
|
1512
1596
|
const rows = [
|
|
1513
1597
|
{ label: 'ID', value: projectIdOf(project) || '-' },
|
|
1514
1598
|
{ label: 'Nome', value: project.name || '-' },
|
|
1515
1599
|
{ label: 'Status', value: project.status || '-' },
|
|
1516
1600
|
{ label: 'Tipo', value: project.type_project || '-' },
|
|
1517
|
-
{ label: 'Exposicao', value: project.exposure || '-' },
|
|
1518
1601
|
{ label: 'Plano', value: project.plan || '-' },
|
|
1519
|
-
{ label: 'URL', value: url || '-' },
|
|
1520
|
-
{ label: 'Imagem', value: project.image || '-' },
|
|
1521
|
-
{ label: 'Instancias', value: project.instances ?? '-' },
|
|
1522
|
-
{ label: 'Instancias atuais', value: project.additional_info?.current_instances ?? '-' },
|
|
1523
1602
|
];
|
|
1524
1603
|
|
|
1604
|
+
if (isValkey) {
|
|
1605
|
+
rows.push(
|
|
1606
|
+
{ label: 'Perfil', value: managedService?.profile || '-' },
|
|
1607
|
+
{ label: 'Versao', value: managedService?.version || '-' },
|
|
1608
|
+
{ label: 'Persistente', value: managedService?.topology?.persistent === true ? 'sim' : 'nao' },
|
|
1609
|
+
{ label: 'Instancias', value: project.instances ?? '-' },
|
|
1610
|
+
);
|
|
1611
|
+
} else {
|
|
1612
|
+
rows.push(
|
|
1613
|
+
{ label: 'Exposicao', value: project.exposure || '-' },
|
|
1614
|
+
{ label: 'URL', value: projectUrlOf(project) || '-' },
|
|
1615
|
+
{ label: 'Imagem', value: project.image || '-' },
|
|
1616
|
+
{ label: 'Instancias', value: project.instances ?? '-' },
|
|
1617
|
+
{ label: 'Instancias atuais', value: project.additional_info?.current_instances ?? '-' },
|
|
1618
|
+
);
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1525
1621
|
for (const row of rows) {
|
|
1526
1622
|
process.stdout.write(`${row.label}: ${row.value}\n`);
|
|
1527
1623
|
}
|
|
@@ -1937,6 +2033,26 @@ function normalizePlan(value) {
|
|
|
1937
2033
|
['dbpremium', 'db-premium'],
|
|
1938
2034
|
['db_enterprise', 'db-enterprise'],
|
|
1939
2035
|
['dbenterprise', 'db-enterprise'],
|
|
2036
|
+
['cache_free', 'cache-free'],
|
|
2037
|
+
['cachefree', 'cache-free'],
|
|
2038
|
+
['cache_starter', 'cache-starter'],
|
|
2039
|
+
['cachestarter', 'cache-starter'],
|
|
2040
|
+
['cache_basic', 'cache-basic'],
|
|
2041
|
+
['cachebasic', 'cache-basic'],
|
|
2042
|
+
['cache_premium', 'cache-premium'],
|
|
2043
|
+
['cachepremium', 'cache-premium'],
|
|
2044
|
+
['cache_enterprise', 'cache-enterprise'],
|
|
2045
|
+
['cacheenterprise', 'cache-enterprise'],
|
|
2046
|
+
['queue_free', 'queue-free'],
|
|
2047
|
+
['queuefree', 'queue-free'],
|
|
2048
|
+
['queue_starter', 'queue-starter'],
|
|
2049
|
+
['queuestarter', 'queue-starter'],
|
|
2050
|
+
['queue_basic', 'queue-basic'],
|
|
2051
|
+
['queuebasic', 'queue-basic'],
|
|
2052
|
+
['queue_premium', 'queue-premium'],
|
|
2053
|
+
['queuepremium', 'queue-premium'],
|
|
2054
|
+
['queue_enterprise', 'queue-enterprise'],
|
|
2055
|
+
['queueenterprise', 'queue-enterprise'],
|
|
1940
2056
|
]);
|
|
1941
2057
|
|
|
1942
2058
|
if (aliases.has(normalized)) return aliases.get(normalized);
|
|
@@ -1973,6 +2089,21 @@ function normalizeTypeProject(value) {
|
|
|
1973
2089
|
['postgresql', 'postgresql'],
|
|
1974
2090
|
['mariadb', 'mariadb'],
|
|
1975
2091
|
['maria db', 'mariadb'],
|
|
2092
|
+
['valkey', 'valkey'],
|
|
2093
|
+
['key value', 'valkey'],
|
|
2094
|
+
['key-value', 'valkey'],
|
|
2095
|
+
]);
|
|
2096
|
+
return aliases.get(raw) || raw;
|
|
2097
|
+
}
|
|
2098
|
+
|
|
2099
|
+
function normalizeValkeyProfile(value) {
|
|
2100
|
+
const raw = normalizeFreeText(value);
|
|
2101
|
+
const aliases = new Map([
|
|
2102
|
+
['key_value', 'key_value'],
|
|
2103
|
+
['key value', 'key_value'],
|
|
2104
|
+
['key-value', 'key_value'],
|
|
2105
|
+
['cache', 'cache'],
|
|
2106
|
+
['queue', 'queue'],
|
|
1976
2107
|
]);
|
|
1977
2108
|
return aliases.get(raw) || raw;
|
|
1978
2109
|
}
|
|
@@ -2075,7 +2206,6 @@ async function fetchWizardCatalogs(session, flags) {
|
|
|
2075
2206
|
unwrapData(await request(session, flags, 'GET', '/project/database/plans')),
|
|
2076
2207
|
'os planos de banco',
|
|
2077
2208
|
);
|
|
2078
|
-
|
|
2079
2209
|
return { httpPlans, availableInstances, databasePlans };
|
|
2080
2210
|
}
|
|
2081
2211
|
|
|
@@ -2084,6 +2214,10 @@ function normalizePlansCatalogType(value) {
|
|
|
2084
2214
|
if (!normalized || normalized === 'all') return 'all';
|
|
2085
2215
|
if (normalized === 'http') return 'http';
|
|
2086
2216
|
if (['database', 'db', 'postgresql', 'mariadb'].includes(normalized)) return 'database';
|
|
2217
|
+
if (normalized === 'valkey') return 'valkey';
|
|
2218
|
+
if (normalized === 'key-value' || normalized === 'key value' || normalized === 'key_value') return 'valkey-key_value';
|
|
2219
|
+
if (normalized === 'cache') return 'valkey-cache';
|
|
2220
|
+
if (normalized === 'queue') return 'valkey-queue';
|
|
2087
2221
|
if (normalized === 'storage') return 'storage';
|
|
2088
2222
|
return null;
|
|
2089
2223
|
}
|
|
@@ -2116,6 +2250,49 @@ function humanizeStorageName(value) {
|
|
|
2116
2250
|
return { storage: raw, type: '-' };
|
|
2117
2251
|
}
|
|
2118
2252
|
|
|
2253
|
+
const VALKEY_PROFILE_LABELS = {
|
|
2254
|
+
key_value: 'Key Value',
|
|
2255
|
+
cache: 'Cache',
|
|
2256
|
+
queue: 'Queue',
|
|
2257
|
+
};
|
|
2258
|
+
|
|
2259
|
+
function requireValkeyCatalog(value) {
|
|
2260
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)
|
|
2261
|
+
|| value.engine !== 'valkey'
|
|
2262
|
+
|| typeof value.version !== 'string'
|
|
2263
|
+
|| typeof value.currency !== 'string'
|
|
2264
|
+
|| !Array.isArray(value.profiles)
|
|
2265
|
+
|| !Array.isArray(value.plans)
|
|
2266
|
+
|| value.profiles.length === 0
|
|
2267
|
+
|| value.plans.length === 0) {
|
|
2268
|
+
throw new CliError('Falha ao carregar o catalogo Valkey. Tente novamente mais tarde.');
|
|
2269
|
+
}
|
|
2270
|
+
|
|
2271
|
+
const validProfiles = new Set(Object.keys(VALKEY_PROFILE_LABELS));
|
|
2272
|
+
if (value.profiles.some((profile) => !validProfiles.has(profile?.id) || typeof profile.persistence !== 'boolean')) {
|
|
2273
|
+
throw new CliError('Falha ao carregar o catalogo Valkey. Tente novamente mais tarde.');
|
|
2274
|
+
}
|
|
2275
|
+
if (value.plans.some((plan) => !validProfiles.has(plan?.profile) || typeof plan.id !== 'string' || !plan.prices || typeof plan.prices !== 'object')) {
|
|
2276
|
+
throw new CliError('Falha ao carregar o catalogo Valkey. Tente novamente mais tarde.');
|
|
2277
|
+
}
|
|
2278
|
+
|
|
2279
|
+
return value;
|
|
2280
|
+
}
|
|
2281
|
+
|
|
2282
|
+
function valkeyProfileFilter(type) {
|
|
2283
|
+
if (!type.startsWith('valkey-')) return null;
|
|
2284
|
+
return type.slice('valkey-'.length);
|
|
2285
|
+
}
|
|
2286
|
+
|
|
2287
|
+
function filterValkeyCatalog(catalog, profile) {
|
|
2288
|
+
if (!profile) return catalog;
|
|
2289
|
+
return {
|
|
2290
|
+
...catalog,
|
|
2291
|
+
profiles: catalog.profiles.filter((entry) => entry.id === profile),
|
|
2292
|
+
plans: catalog.plans.filter((plan) => plan.profile === profile),
|
|
2293
|
+
};
|
|
2294
|
+
}
|
|
2295
|
+
|
|
2119
2296
|
async function fetchPlansCatalogs(session, flags, type) {
|
|
2120
2297
|
if (type === 'http') {
|
|
2121
2298
|
return {
|
|
@@ -2150,19 +2327,51 @@ async function fetchPlansCatalogs(session, flags, type) {
|
|
|
2150
2327
|
};
|
|
2151
2328
|
}
|
|
2152
2329
|
|
|
2153
|
-
|
|
2330
|
+
if (type === 'valkey' || type.startsWith('valkey-')) {
|
|
2331
|
+
const catalog = requireValkeyCatalog(
|
|
2332
|
+
unwrapData(await request(session, flags, 'GET', '/managed-services/catalog', { tokenRequired: false })),
|
|
2333
|
+
);
|
|
2334
|
+
return {
|
|
2335
|
+
http: [],
|
|
2336
|
+
database: [],
|
|
2337
|
+
storage: [],
|
|
2338
|
+
valkey: filterValkeyCatalog(catalog, valkeyProfileFilter(type)),
|
|
2339
|
+
};
|
|
2340
|
+
}
|
|
2341
|
+
|
|
2342
|
+
const [httpPayload, databasePayload, storagePayload, valkeyPayload] = await Promise.all([
|
|
2154
2343
|
request(session, flags, 'GET', '/project/plans', { tokenRequired: false }),
|
|
2155
2344
|
request(session, flags, 'GET', '/project/database/plans', { tokenRequired: false }),
|
|
2156
2345
|
request(session, flags, 'GET', '/project/storage/plans', { tokenRequired: false }),
|
|
2346
|
+
request(session, flags, 'GET', '/managed-services/catalog', { tokenRequired: false }),
|
|
2157
2347
|
]);
|
|
2158
2348
|
|
|
2159
2349
|
return {
|
|
2160
2350
|
http: requireWizardCatalogArray(unwrapData(httpPayload), 'os planos HTTP'),
|
|
2161
2351
|
database: requireWizardCatalogArray(unwrapData(databasePayload), 'os planos de banco'),
|
|
2162
2352
|
storage: requireWizardCatalogArray(unwrapData(storagePayload), 'os planos de storage'),
|
|
2353
|
+
valkey: requireValkeyCatalog(unwrapData(valkeyPayload)),
|
|
2163
2354
|
};
|
|
2164
2355
|
}
|
|
2165
2356
|
|
|
2357
|
+
function printValkeyCatalog(catalog, profileFilter) {
|
|
2358
|
+
const profiles = profileFilter ? [profileFilter] : Object.keys(VALKEY_PROFILE_LABELS);
|
|
2359
|
+
profiles.forEach((profile, index) => {
|
|
2360
|
+
if (index > 0) process.stdout.write('\n');
|
|
2361
|
+
process.stdout.write(`${VALKEY_PROFILE_LABELS[profile]}\n`);
|
|
2362
|
+
const plans = catalog.plans.filter((plan) => plan.profile === profile);
|
|
2363
|
+
printTable(plans, [
|
|
2364
|
+
{ label: 'Plano', value: (plan) => plan.id || '-' },
|
|
2365
|
+
{ label: 'Hora', value: (plan) => formatBrl(plan.prices?.hourly, catalog.currency) },
|
|
2366
|
+
{ label: 'Mes', value: (plan) => formatBrl(plan.prices?.monthly, catalog.currency) },
|
|
2367
|
+
{ label: 'Ano', value: (plan) => formatBrl(plan.prices?.yearly, catalog.currency) },
|
|
2368
|
+
{ label: 'Capacidade', value: (plan) => `${plan.resources?.cpu || '-'} / ${plan.resources?.memory || '-'}` },
|
|
2369
|
+
{ label: 'Storage incluído', value: (plan) => `${plan.included_storage_gb ?? 0} GB` },
|
|
2370
|
+
{ label: 'Alta disponibilidade', value: (plan) => plan.high_availability?.[profile] ? 'sim' : 'nao' },
|
|
2371
|
+
]);
|
|
2372
|
+
});
|
|
2373
|
+
}
|
|
2374
|
+
|
|
2166
2375
|
function printPlansCatalogs(payload, type) {
|
|
2167
2376
|
if (type === 'all' || type === 'http') {
|
|
2168
2377
|
process.stdout.write('HTTP\n');
|
|
@@ -2198,12 +2407,17 @@ function printPlansCatalogs(payload, type) {
|
|
|
2198
2407
|
{ label: 'Ano', value: (storage) => formatBrl(storage.prices?.yearly) },
|
|
2199
2408
|
]);
|
|
2200
2409
|
}
|
|
2410
|
+
|
|
2411
|
+
if (type === 'all' || type === 'valkey' || type.startsWith('valkey-')) {
|
|
2412
|
+
if (type === 'all') process.stdout.write('\n');
|
|
2413
|
+
printValkeyCatalog(payload.valkey, valkeyProfileFilter(type));
|
|
2414
|
+
}
|
|
2201
2415
|
}
|
|
2202
2416
|
|
|
2203
2417
|
async function handlePlans(session, flags) {
|
|
2204
2418
|
const type = normalizePlansCatalogType(flags.type);
|
|
2205
2419
|
if (!type) {
|
|
2206
|
-
throw new CliError('Tipo de catalogo invalido. Valores aceitos: all, http, database, storage.');
|
|
2420
|
+
throw new CliError('Tipo de catalogo invalido. Valores aceitos: all, http, database, storage, valkey.');
|
|
2207
2421
|
}
|
|
2208
2422
|
|
|
2209
2423
|
const payload = await fetchPlansCatalogs(session, flags, type);
|
|
@@ -2261,9 +2475,53 @@ function validateCreateAutoscaling(config, plan, typeProject) {
|
|
|
2261
2475
|
return { ...config, autoscaling: nextAutoscaling };
|
|
2262
2476
|
}
|
|
2263
2477
|
|
|
2264
|
-
function
|
|
2478
|
+
function validateValkeyConfig(config, plan, catalog) {
|
|
2479
|
+
const valkeyCatalog = requireValkeyCatalog(catalog);
|
|
2480
|
+
const profile = normalizeValkeyProfile(config?.profile);
|
|
2481
|
+
const profileDefinition = valkeyCatalog.profiles.find((entry) => entry.id === profile);
|
|
2482
|
+
if (!profileDefinition) {
|
|
2483
|
+
throw new CliError('config.profile invalido. Use key_value, cache ou queue.');
|
|
2484
|
+
}
|
|
2485
|
+
|
|
2486
|
+
const planDefinition = valkeyCatalog.plans.find((entry) => entry.id === plan && entry.profile === profile);
|
|
2487
|
+
if (!planDefinition) {
|
|
2488
|
+
throw new CliError(`Plano ${plan} nao pode ser usado com o perfil ${profile}. Consulte "zenifra plans --type valkey".`);
|
|
2489
|
+
}
|
|
2490
|
+
|
|
2491
|
+
if (config.version !== valkeyCatalog.version) {
|
|
2492
|
+
throw new CliError(`config.version invalida: use ${valkeyCatalog.version}, conforme o catalogo Valkey.`);
|
|
2493
|
+
}
|
|
2494
|
+
|
|
2495
|
+
const forbiddenFields = ['instances', 'envs', 'image', 'github', 'port', 'exposure', 'autoscaling'];
|
|
2496
|
+
const forbiddenField = forbiddenFields.find((field) => config[field] !== undefined);
|
|
2497
|
+
if (forbiddenField) {
|
|
2498
|
+
throw new CliError(`Valkey nao aceita config.${forbiddenField}. A capacidade e definida pelo plano.`);
|
|
2499
|
+
}
|
|
2500
|
+
|
|
2501
|
+
if (profileDefinition.persistence) {
|
|
2502
|
+
if (!isRecord(config.storage) || config.storage.persistent !== true) {
|
|
2503
|
+
throw new CliError('Perfis Valkey persistentes exigem storage.persistent=true.');
|
|
2504
|
+
}
|
|
2505
|
+
const capacity = Number(config.storage.capacity);
|
|
2506
|
+
if (!Number.isInteger(capacity) || capacity < 1 || capacity > 250) {
|
|
2507
|
+
throw new CliError('storage.capacity deve ser um inteiro entre 1 e 250 GiB.');
|
|
2508
|
+
}
|
|
2509
|
+
} else if (config.storage !== undefined) {
|
|
2510
|
+
throw new CliError('O perfil Cache nao aceita storage.');
|
|
2511
|
+
}
|
|
2512
|
+
|
|
2513
|
+
return {
|
|
2514
|
+
...config,
|
|
2515
|
+
type_project: 'valkey',
|
|
2516
|
+
profile,
|
|
2517
|
+
version: valkeyCatalog.version,
|
|
2518
|
+
};
|
|
2519
|
+
}
|
|
2520
|
+
|
|
2521
|
+
function validateCreateInput({ plan, paymentMode, config, valkeyCatalog }) {
|
|
2522
|
+
const nextTypeProject = normalizeTypeProject(config?.type_project);
|
|
2265
2523
|
const nextPlan = normalizePlan(plan);
|
|
2266
|
-
if (!nextPlan || !ALLOWED_PLAN_VALUES.has(nextPlan)) {
|
|
2524
|
+
if (!nextPlan || (nextTypeProject !== 'valkey' && !ALLOWED_PLAN_VALUES.has(nextPlan))) {
|
|
2267
2525
|
throw new CliError(`Plano invalido: "${plan}". Valores aceitos: ${formatAllowedValues(ALLOWED_PLAN_VALUES)}. Docs: ${DOCS_CREATE_HTTP_URL}`);
|
|
2268
2526
|
}
|
|
2269
2527
|
|
|
@@ -2272,12 +2530,19 @@ function validateCreateInput({ plan, paymentMode, config }) {
|
|
|
2272
2530
|
throw new CliError(`Modo de pagamento invalido: "${paymentMode}". Valores aceitos: ${formatAllowedValues(ALLOWED_PAYMENT_MODE_VALUES)}. Docs: ${DOCS_PAYMENTS_URL}`);
|
|
2273
2531
|
}
|
|
2274
2532
|
|
|
2275
|
-
const nextTypeProject = normalizeTypeProject(config?.type_project);
|
|
2276
2533
|
if (!nextTypeProject || !ALLOWED_TYPE_PROJECT_VALUES.has(nextTypeProject)) {
|
|
2277
2534
|
throw new CliError(`type_project invalido: "${config?.type_project}". Valores aceitos: ${formatAllowedValues(ALLOWED_TYPE_PROJECT_VALUES)}. Docs: ${DOCS_CREATE_HTTP_URL}`);
|
|
2278
2535
|
}
|
|
2279
2536
|
|
|
2280
2537
|
const nextConfig = { ...config, type_project: nextTypeProject };
|
|
2538
|
+
if (nextTypeProject === 'valkey') {
|
|
2539
|
+
return {
|
|
2540
|
+
plan: nextPlan,
|
|
2541
|
+
paymentMode: nextPaymentMode,
|
|
2542
|
+
config: validateValkeyConfig(nextConfig, nextPlan, valkeyCatalog),
|
|
2543
|
+
};
|
|
2544
|
+
}
|
|
2545
|
+
|
|
2281
2546
|
if (nextTypeProject === 'http' && nextConfig.github) {
|
|
2282
2547
|
const nextRuntime = normalizeRuntime(nextConfig.github.runtime);
|
|
2283
2548
|
if (!nextRuntime || !ALLOWED_RUNTIME_VALUES.has(nextRuntime)) {
|
|
@@ -2344,6 +2609,10 @@ function printWizardSummary(payload) {
|
|
|
2344
2609
|
lines.push(` banco: versao ${masked.config.version} | instancias ${masked.config.instances} | storage ${masked.config.storage.capacity}Gi`);
|
|
2345
2610
|
}
|
|
2346
2611
|
|
|
2612
|
+
if (masked.config.type_project === 'valkey') {
|
|
2613
|
+
lines.push(` Valkey: perfil ${masked.config.profile} | versao ${masked.config.version}${masked.config.storage ? ` | storage ${masked.config.storage.capacity}Gi persistente` : ' | sem storage persistente'}`);
|
|
2614
|
+
}
|
|
2615
|
+
|
|
2347
2616
|
process.stdout.write(`${lines.join('\n')}\n`);
|
|
2348
2617
|
}
|
|
2349
2618
|
|
|
@@ -2735,6 +3004,65 @@ async function buildMariadbConfig() {
|
|
|
2735
3004
|
};
|
|
2736
3005
|
}
|
|
2737
3006
|
|
|
3007
|
+
async function buildValkeyConfig({ profile, plan, catalog }) {
|
|
3008
|
+
const planDefinition = catalog.plans.find((entry) => entry.id === plan && entry.profile === profile);
|
|
3009
|
+
const profileDefinition = catalog.profiles.find((entry) => entry.id === profile);
|
|
3010
|
+
if (!planDefinition || !profileDefinition) {
|
|
3011
|
+
throw new CliError('O catalogo Valkey nao possui o plano ou perfil selecionado. Tente novamente.');
|
|
3012
|
+
}
|
|
3013
|
+
|
|
3014
|
+
let storage;
|
|
3015
|
+
if (profileDefinition.persistence) {
|
|
3016
|
+
storage = {
|
|
3017
|
+
persistent: true,
|
|
3018
|
+
capacity: await promptWizardNumber({
|
|
3019
|
+
label: `Capacidade de armazenamento persistente (Gi, inclui ${planDefinition.included_storage_gb ?? 0} Gi)`,
|
|
3020
|
+
docs: DOCS_CREATE_VALKEY_URL,
|
|
3021
|
+
examples: [String(Math.max(1, planDefinition.included_storage_gb ?? 1)), '10', '20'],
|
|
3022
|
+
min: 1,
|
|
3023
|
+
max: 250,
|
|
3024
|
+
}),
|
|
3025
|
+
};
|
|
3026
|
+
}
|
|
3027
|
+
|
|
3028
|
+
const whitelist = await promptWizardPairs({
|
|
3029
|
+
introLabel: 'Deseja configurar acesso de rede para este projeto?',
|
|
3030
|
+
docs: DOCS_CREATE_VALKEY_URL,
|
|
3031
|
+
examples: ['sim', 'nao'],
|
|
3032
|
+
stateKey: 'valkeyNetworkAccess',
|
|
3033
|
+
itemLabels: [
|
|
3034
|
+
{ key: 'cidr', label: 'CIDR permitido', required: true, examples: ['203.0.113.0/24'], docs: DOCS_CREATE_VALKEY_URL },
|
|
3035
|
+
{ key: 'description', label: 'Descricao do acesso', required: true, examples: ['Aplicacao'], docs: DOCS_CREATE_VALKEY_URL },
|
|
3036
|
+
],
|
|
3037
|
+
});
|
|
3038
|
+
|
|
3039
|
+
let blacklist = [];
|
|
3040
|
+
if (whitelist.length > 0) {
|
|
3041
|
+
blacklist = await promptWizardPairs({
|
|
3042
|
+
introLabel: 'Deseja adicionar bloqueios de rede?',
|
|
3043
|
+
docs: DOCS_CREATE_VALKEY_URL,
|
|
3044
|
+
examples: ['sim', 'nao'],
|
|
3045
|
+
itemLabels: [
|
|
3046
|
+
{ key: 'cidr', label: 'CIDR bloqueado', required: true, examples: ['198.51.100.0/24'], docs: DOCS_CREATE_VALKEY_URL },
|
|
3047
|
+
{ key: 'description', label: 'Descricao do bloqueio', required: true, examples: ['Bloqueio'], docs: DOCS_CREATE_VALKEY_URL },
|
|
3048
|
+
],
|
|
3049
|
+
});
|
|
3050
|
+
}
|
|
3051
|
+
|
|
3052
|
+
return {
|
|
3053
|
+
type_project: 'valkey',
|
|
3054
|
+
profile,
|
|
3055
|
+
version: catalog.version,
|
|
3056
|
+
...(storage ? { storage } : {}),
|
|
3057
|
+
...(whitelist.length > 0 ? {
|
|
3058
|
+
network_access: {
|
|
3059
|
+
ingress_white_list: whitelist,
|
|
3060
|
+
ingress_black_list: blacklist,
|
|
3061
|
+
},
|
|
3062
|
+
} : {}),
|
|
3063
|
+
};
|
|
3064
|
+
}
|
|
3065
|
+
|
|
2738
3066
|
async function runProjectCreateWizard(session, flags) {
|
|
2739
3067
|
const catalogs = await fetchWizardCatalogs(session, flags);
|
|
2740
3068
|
const ui = createWizardUi();
|
|
@@ -2765,16 +3093,34 @@ async function runProjectCreateWizard(session, flags) {
|
|
|
2765
3093
|
options: WIZARD_TYPE_PROJECT_OPTIONS.map((value) => ({ value })),
|
|
2766
3094
|
});
|
|
2767
3095
|
ui.state.typeProject = typeProject;
|
|
3096
|
+
const valkeyCatalog = typeProject === 'valkey'
|
|
3097
|
+
? requireValkeyCatalog(unwrapData(await request(session, flags, 'GET', '/managed-services/catalog')))
|
|
3098
|
+
: undefined;
|
|
3099
|
+
const profile = typeProject === 'valkey'
|
|
3100
|
+
? await promptWizardSelect({
|
|
3101
|
+
label: 'Perfil Valkey',
|
|
3102
|
+
docs: DOCS_CREATE_VALKEY_URL,
|
|
3103
|
+
examples: WIZARD_VALKEY_PROFILE_OPTIONS,
|
|
3104
|
+
options: WIZARD_VALKEY_PROFILE_OPTIONS.map((value) => ({ value })),
|
|
3105
|
+
})
|
|
3106
|
+
: undefined;
|
|
3107
|
+
ui.state.valkeyProfile = profile;
|
|
2768
3108
|
const planOptions = typeProject === 'http'
|
|
2769
3109
|
? sortWizardOptions(catalogs.httpPlans
|
|
2770
3110
|
.map((plan) => plan?.plan)
|
|
2771
3111
|
.filter((plan) => WIZARD_HTTP_PLAN_OPTIONS.includes(plan) && Number(catalogs.availableInstances?.[plan] || 0) > 0), WIZARD_HTTP_PLAN_OPTIONS)
|
|
3112
|
+
: typeProject === 'valkey'
|
|
3113
|
+
? valkeyCatalog.plans
|
|
3114
|
+
.filter((planDefinition) => planDefinition.profile === profile)
|
|
3115
|
+
.map((planDefinition) => planDefinition.id)
|
|
2772
3116
|
: sortWizardOptions(catalogs.databasePlans
|
|
2773
3117
|
.map((plan) => plan?.plan)
|
|
2774
3118
|
.filter((plan) => WIZARD_DATABASE_PLAN_OPTIONS.includes(plan) && (typeProject !== 'mariadb' || plan !== 'db-free')), WIZARD_DATABASE_PLAN_OPTIONS);
|
|
2775
3119
|
const plan = await promptWizardSelect({
|
|
2776
3120
|
label: 'Plano',
|
|
2777
|
-
docs: typeProject === 'http'
|
|
3121
|
+
docs: typeProject === 'http'
|
|
3122
|
+
? DOCS_CREATE_HTTP_URL
|
|
3123
|
+
: typeProject === 'valkey' ? DOCS_CREATE_VALKEY_URL : DOCS_DATABASE_CONFIGURATION_URL,
|
|
2778
3124
|
examples: [planOptions[0], planOptions.at(-1)],
|
|
2779
3125
|
options: planOptions.map((value) => ({ value })),
|
|
2780
3126
|
});
|
|
@@ -2800,6 +3146,8 @@ async function runProjectCreateWizard(session, flags) {
|
|
|
2800
3146
|
});
|
|
2801
3147
|
} else if (typeProject === 'postgresql') {
|
|
2802
3148
|
config = await buildPostgresqlConfig({ plan, databasePlans: catalogs.databasePlans });
|
|
3149
|
+
} else if (typeProject === 'valkey') {
|
|
3150
|
+
config = await buildValkeyConfig({ profile, plan, catalog: valkeyCatalog });
|
|
2803
3151
|
} else {
|
|
2804
3152
|
config = await buildMariadbConfig();
|
|
2805
3153
|
}
|
|
@@ -2815,7 +3163,9 @@ async function runProjectCreateWizard(session, flags) {
|
|
|
2815
3163
|
printWizardSummary(payload);
|
|
2816
3164
|
const confirmed = await promptWizardBoolean({
|
|
2817
3165
|
label: 'Confirmar criacao do projeto',
|
|
2818
|
-
docs: typeProject === 'http'
|
|
3166
|
+
docs: typeProject === 'http'
|
|
3167
|
+
? DOCS_CREATE_HTTP_URL
|
|
3168
|
+
: typeProject === 'valkey' ? DOCS_CREATE_VALKEY_URL : DOCS_DATABASE_CONFIGURATION_URL,
|
|
2819
3169
|
examples: ['sim', 'nao'],
|
|
2820
3170
|
});
|
|
2821
3171
|
if (!confirmed) return null;
|
|
@@ -2836,10 +3186,18 @@ async function handleProjectCreate(session, flags) {
|
|
|
2836
3186
|
const plan = wizardPayload?.plan || flags.plan || await prompt('Plano');
|
|
2837
3187
|
const paymentMode = wizardPayload?.payment_mode || flags.paymentMode || await prompt('Modo de pagamento');
|
|
2838
3188
|
const config = wizardPayload?.config || await parseConfig(flags.config || await prompt('Config JSON ou @arquivo'));
|
|
2839
|
-
const
|
|
3189
|
+
const typeProject = normalizeTypeProject(config?.type_project);
|
|
3190
|
+
const valkeyCatalog = typeProject === 'valkey'
|
|
3191
|
+
? requireValkeyCatalog(unwrapData(await request(session, flags, 'GET', '/managed-services/catalog')))
|
|
3192
|
+
: undefined;
|
|
3193
|
+
if (flags.idempotencyKey !== undefined && !/^[A-Za-z0-9._-]{16,200}$/.test(String(flags.idempotencyKey))) {
|
|
3194
|
+
throw new CliError('--idempotency-key deve ter entre 16 e 200 caracteres: letras, numeros, ponto, underscore ou hifen.');
|
|
3195
|
+
}
|
|
3196
|
+
const validated = validateCreateInput({ plan, paymentMode, config, valkeyCatalog });
|
|
2840
3197
|
|
|
2841
3198
|
const payload = await request(session, flags, 'POST', '/project', {
|
|
2842
3199
|
orgId,
|
|
3200
|
+
headers: flags.idempotencyKey === undefined ? {} : { 'Idempotency-Key': String(flags.idempotencyKey) },
|
|
2843
3201
|
body: {
|
|
2844
3202
|
name,
|
|
2845
3203
|
...(description ? { description: String(description) } : {}),
|
|
@@ -2859,6 +3217,21 @@ async function handleProjectCreate(session, flags) {
|
|
|
2859
3217
|
...(project.domain ? [{ field: 'Dominio', value: formatPublicUrl(project.domain) }] : []),
|
|
2860
3218
|
...(project.api_key ? [{ field: 'API key', value: project.api_key }] : []),
|
|
2861
3219
|
];
|
|
3220
|
+
if (validated.config.type_project === 'valkey') {
|
|
3221
|
+
const service = project.managed_service || {};
|
|
3222
|
+
rows.push(
|
|
3223
|
+
{ field: 'Perfil', value: service.profile || validated.config.profile },
|
|
3224
|
+
{ field: 'Host', value: service.host || '-' },
|
|
3225
|
+
{ field: 'Porta', value: service.port ?? '-' },
|
|
3226
|
+
{ field: 'TLS', value: service.tls === true ? 'sim' : 'nao' },
|
|
3227
|
+
);
|
|
3228
|
+
if (service.connection_string) {
|
|
3229
|
+
rows.push({ field: 'String de conexao', value: service.connection_string });
|
|
3230
|
+
process.stdout.write('Salve a string de conexao em um local seguro; ela pode nao ser exibida novamente.\n');
|
|
3231
|
+
} else {
|
|
3232
|
+
rows.push({ field: 'Credencial', value: 'nao retornada nesta resposta; use valkey credentials rotate' });
|
|
3233
|
+
}
|
|
3234
|
+
}
|
|
2862
3235
|
process.stdout.write('\n');
|
|
2863
3236
|
printTable(rows, [
|
|
2864
3237
|
{ label: 'Campo', value: (row) => row.field },
|
|
@@ -3330,6 +3703,131 @@ async function handleProjectBillingUsage(session, flags) {
|
|
|
3330
3703
|
printBillingUsage(data);
|
|
3331
3704
|
}
|
|
3332
3705
|
|
|
3706
|
+
function printValkeyStatus(data) {
|
|
3707
|
+
printTable([data], [
|
|
3708
|
+
{ label: 'Status', value: (service) => service.status || '-' },
|
|
3709
|
+
{ label: 'Perfil', value: (service) => service.profile || '-' },
|
|
3710
|
+
{ label: 'Versao', value: (service) => service.version || '-' },
|
|
3711
|
+
{ label: 'Persistente', value: (service) => service.persistence === true ? 'sim' : 'nao' },
|
|
3712
|
+
{ label: 'Instancias', value: (service) => service.instances ?? '-' },
|
|
3713
|
+
]);
|
|
3714
|
+
}
|
|
3715
|
+
|
|
3716
|
+
function printValkeyConnection(data) {
|
|
3717
|
+
printTable([data], [
|
|
3718
|
+
{ label: 'Host', value: (service) => service.host || '-' },
|
|
3719
|
+
{ label: 'Porta', value: (service) => service.port ?? '-' },
|
|
3720
|
+
{ label: 'Usuario', value: (service) => service.username || '-' },
|
|
3721
|
+
{ label: 'TLS', value: (service) => service.tls === true ? 'sim' : 'nao' },
|
|
3722
|
+
{ label: 'Conexao', value: (service) => service.connection_string || '-' },
|
|
3723
|
+
]);
|
|
3724
|
+
}
|
|
3725
|
+
|
|
3726
|
+
function printValkeyRotation(data, { accepted = false } = {}) {
|
|
3727
|
+
const rows = [
|
|
3728
|
+
{ field: accepted ? 'Operacao aceita' : 'Operacao', value: data.operation_id || '-' },
|
|
3729
|
+
{ field: 'Estado', value: data.state || '-' },
|
|
3730
|
+
];
|
|
3731
|
+
if (data.connection_string) {
|
|
3732
|
+
rows.push({ field: 'String de conexao', value: data.connection_string });
|
|
3733
|
+
process.stdout.write('Salve a nova string de conexao em um local seguro; ela pode nao ser exibida novamente.\n');
|
|
3734
|
+
}
|
|
3735
|
+
printTable(rows, [
|
|
3736
|
+
{ label: 'Campo', value: (row) => row.field },
|
|
3737
|
+
{ label: 'Valor', value: (row) => row.value },
|
|
3738
|
+
]);
|
|
3739
|
+
}
|
|
3740
|
+
|
|
3741
|
+
async function waitForValkeyRotation(session, flags, projectId, operationId, orgId) {
|
|
3742
|
+
const intervalMs = parseSecondsOption(flags.interval, '--interval', { defaultValue: 2, min: 0.1 }) * 1000;
|
|
3743
|
+
const timeoutMs = parseSecondsOption(flags.timeout, '--timeout', { defaultValue: 900, min: 1 }) * 1000;
|
|
3744
|
+
const startedAt = Date.now();
|
|
3745
|
+
let lastState = 'accepted';
|
|
3746
|
+
|
|
3747
|
+
while (true) {
|
|
3748
|
+
const data = unwrapData(await request(
|
|
3749
|
+
session,
|
|
3750
|
+
flags,
|
|
3751
|
+
'GET',
|
|
3752
|
+
`/managed-services/${projectId}/credential-rotations/${operationId}`,
|
|
3753
|
+
{ orgId },
|
|
3754
|
+
));
|
|
3755
|
+
lastState = data?.state || lastState;
|
|
3756
|
+
if (lastState === 'completed') return data;
|
|
3757
|
+
if (['failed', 'cancelled', 'canceled'].includes(lastState)) {
|
|
3758
|
+
throw new CliError(`A renovacao de credencial terminou com estado ${lastState}. Operacao: ${operationId}.`);
|
|
3759
|
+
}
|
|
3760
|
+
if (Date.now() - startedAt >= timeoutMs) {
|
|
3761
|
+
throw new CliError(`Timeout aguardando a renovacao ${operationId}. Ultimo estado: ${lastState}.`);
|
|
3762
|
+
}
|
|
3763
|
+
await new Promise((resolvePromise) => setTimeout(resolvePromise, intervalMs));
|
|
3764
|
+
}
|
|
3765
|
+
}
|
|
3766
|
+
|
|
3767
|
+
async function handleValkeyStatus(session, flags) {
|
|
3768
|
+
const projectId = requireProjectId(flags, 'valkey status');
|
|
3769
|
+
if (!projectId) return;
|
|
3770
|
+
const orgId = await resolveOrgId(session, flags);
|
|
3771
|
+
const data = unwrapData(await request(session, flags, 'GET', `/managed-services/${projectId}/status`, { orgId }));
|
|
3772
|
+
if (flags.json) return printJson(data);
|
|
3773
|
+
printValkeyStatus(data);
|
|
3774
|
+
}
|
|
3775
|
+
|
|
3776
|
+
async function handleValkeyConnection(session, flags) {
|
|
3777
|
+
const projectId = requireProjectId(flags, 'valkey connection');
|
|
3778
|
+
if (!projectId) return;
|
|
3779
|
+
const orgId = await resolveOrgId(session, flags);
|
|
3780
|
+
const data = unwrapData(await request(session, flags, 'GET', `/managed-services/${projectId}/connection`, { orgId }));
|
|
3781
|
+
if (flags.json) return printJson(data);
|
|
3782
|
+
printValkeyConnection(data);
|
|
3783
|
+
}
|
|
3784
|
+
|
|
3785
|
+
async function handleValkeyCredentialStatus(session, flags) {
|
|
3786
|
+
const projectId = requireProjectId(flags, 'valkey credentials status');
|
|
3787
|
+
if (!projectId) return;
|
|
3788
|
+
if (!flags.operation) {
|
|
3789
|
+
printCommandHelpAndFail('valkey credentials status');
|
|
3790
|
+
return;
|
|
3791
|
+
}
|
|
3792
|
+
const orgId = await resolveOrgId(session, flags);
|
|
3793
|
+
const data = unwrapData(await request(
|
|
3794
|
+
session,
|
|
3795
|
+
flags,
|
|
3796
|
+
'GET',
|
|
3797
|
+
`/managed-services/${projectId}/credential-rotations/${flags.operation}`,
|
|
3798
|
+
{ orgId },
|
|
3799
|
+
));
|
|
3800
|
+
if (flags.json) return printJson(data);
|
|
3801
|
+
printValkeyRotation(data);
|
|
3802
|
+
}
|
|
3803
|
+
|
|
3804
|
+
async function handleValkeyCredentialRotate(session, flags) {
|
|
3805
|
+
const projectId = requireProjectId(flags, 'valkey credentials rotate');
|
|
3806
|
+
if (!projectId) return;
|
|
3807
|
+
const idempotencyKey = String(flags.idempotencyKey || randomUUID());
|
|
3808
|
+
if (!/^[A-Za-z0-9._-]{16,200}$/.test(idempotencyKey)) {
|
|
3809
|
+
throw new CliError('--idempotency-key deve ter entre 16 e 200 caracteres: letras, numeros, ponto, underscore ou hifen.');
|
|
3810
|
+
}
|
|
3811
|
+
const orgId = await resolveOrgId(session, flags);
|
|
3812
|
+
const acceptedPayload = await request(session, flags, 'PATCH', `/managed-services/${projectId}/credentials`, {
|
|
3813
|
+
orgId,
|
|
3814
|
+
body: {},
|
|
3815
|
+
headers: { 'Idempotency-Key': idempotencyKey },
|
|
3816
|
+
});
|
|
3817
|
+
const accepted = unwrapData(acceptedPayload);
|
|
3818
|
+
if (!accepted?.operation_id) throw new CliError('A API nao retornou o identificador da renovacao de credencial.');
|
|
3819
|
+
|
|
3820
|
+
if (flags.wait) {
|
|
3821
|
+
const completed = await waitForValkeyRotation(session, flags, projectId, accepted.operation_id, orgId);
|
|
3822
|
+
if (flags.json) return printJson(completed);
|
|
3823
|
+
printValkeyRotation(completed);
|
|
3824
|
+
return;
|
|
3825
|
+
}
|
|
3826
|
+
|
|
3827
|
+
if (flags.json) return printJson(acceptedPayload);
|
|
3828
|
+
printValkeyRotation(accepted, { accepted: true });
|
|
3829
|
+
}
|
|
3830
|
+
|
|
3333
3831
|
function buildQuery(flags, allowedKeys) {
|
|
3334
3832
|
const params = new URLSearchParams();
|
|
3335
3833
|
for (const key of allowedKeys) {
|
|
@@ -3556,6 +4054,14 @@ async function main() {
|
|
|
3556
4054
|
if (command === 'orgs') return handleOrgs(session, flags);
|
|
3557
4055
|
if (command === 'org' && subcommand === 'set') return handleOrgSet(session, flags);
|
|
3558
4056
|
if (command === 'projects') return handleProjects(session, flags);
|
|
4057
|
+
if (command === 'valkey' && subcommand === 'credentials' && positional.length === 2) {
|
|
4058
|
+
process.stdout.write(commandHelp(['valkey', 'credentials']));
|
|
4059
|
+
return;
|
|
4060
|
+
}
|
|
4061
|
+
if (command === 'valkey' && subcommand === 'status') return handleValkeyStatus(session, flags);
|
|
4062
|
+
if (command === 'valkey' && subcommand === 'connection') return handleValkeyConnection(session, flags);
|
|
4063
|
+
if (command === 'valkey' && subcommand === 'credentials' && positional[2] === 'rotate') return handleValkeyCredentialRotate(session, flags);
|
|
4064
|
+
if (command === 'valkey' && subcommand === 'credentials' && positional[2] === 'status') return handleValkeyCredentialStatus(session, flags);
|
|
3559
4065
|
if (command === 'project' && subcommand === 'info') return handleProjectInfo(session, flags);
|
|
3560
4066
|
if (command === 'project' && subcommand === 'url') return handleProjectUrl(session, flags);
|
|
3561
4067
|
if (command === 'project' && subcommand === 'logs') return handleProjectLogs(session, flags);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"type_project": "valkey",
|
|
3
|
+
"profile": "key_value",
|
|
4
|
+
"version": "9.1.1",
|
|
5
|
+
"storage": {
|
|
6
|
+
"persistent": true,
|
|
7
|
+
"capacity": 1
|
|
8
|
+
},
|
|
9
|
+
"network_access": {
|
|
10
|
+
"ingress_white_list": [
|
|
11
|
+
{
|
|
12
|
+
"cidr": "203.0.113.0/24",
|
|
13
|
+
"description": "Application network"
|
|
14
|
+
}
|
|
15
|
+
],
|
|
16
|
+
"ingress_black_list": []
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"type_project": "valkey",
|
|
3
|
+
"profile": "queue",
|
|
4
|
+
"version": "9.1.1",
|
|
5
|
+
"storage": {
|
|
6
|
+
"persistent": true,
|
|
7
|
+
"capacity": 1
|
|
8
|
+
},
|
|
9
|
+
"network_access": {
|
|
10
|
+
"ingress_white_list": [
|
|
11
|
+
{
|
|
12
|
+
"cidr": "203.0.113.0/24",
|
|
13
|
+
"description": "Application network"
|
|
14
|
+
}
|
|
15
|
+
],
|
|
16
|
+
"ingress_black_list": []
|
|
17
|
+
}
|
|
18
|
+
}
|