@zenifra/cli 0.2.4 → 0.2.6
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 +58 -3
- package/bin/zenifra.mjs +708 -42
- 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,11 +59,20 @@ 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>
|
|
75
|
+
zenifra project metrics capabilities --project <project-id>
|
|
66
76
|
zenifra project network --project <project-id> --view summary
|
|
67
77
|
zenifra project image set --project <project-id> --image ghcr.io/zenifra/app:tag
|
|
68
78
|
zenifra project envs --project <project-id>
|
|
@@ -84,6 +94,41 @@ zenifra deploy --project <project-id> --branch main
|
|
|
84
94
|
zenifra deploy watch --project <project-id> --build <build-id>
|
|
85
95
|
```
|
|
86
96
|
|
|
97
|
+
## Metricas de projetos Valkey
|
|
98
|
+
|
|
99
|
+
O comando `zenifra project metrics` mostra as metricas do projeto e, para projetos Valkey, o snapshot da instancia selecionada:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
zenifra project metrics --project <project-id> --instance instance-1
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
A saida legivel inclui:
|
|
106
|
+
|
|
107
|
+
- recursos: CPU e memoria da instancia;
|
|
108
|
+
- capacidade: memoria usada, pico, limite e fragmentacao;
|
|
109
|
+
- clientes conectados e bloqueados;
|
|
110
|
+
- atividade: operacoes por segundo e trafego de entrada/saida;
|
|
111
|
+
- ciclo de vida: chaves expiradas e removidas;
|
|
112
|
+
- uptime e perfil (`cache`, `key-value` ou `queue`);
|
|
113
|
+
- confiabilidade: replicacao e persistencia, quando disponiveis.
|
|
114
|
+
|
|
115
|
+
Perfis especificos podem incluir:
|
|
116
|
+
|
|
117
|
+
- `cache`: hits, misses e hit ratio;
|
|
118
|
+
- `key-value`: total de chaves e chaves com expiracao;
|
|
119
|
+
- `queue`: metricas comuns, sem estimar profundidade quando ela nao esta disponivel.
|
|
120
|
+
|
|
121
|
+
Para consultar o nivel de acesso e os grupos disponiveis antes de buscar um snapshot:
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
zenifra project metrics capabilities --project <project-id>
|
|
125
|
+
zenifra project metrics capabilities --project <project-id> --json
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
`--json` preserva a resposta publica da API. Valores ausentes sao exibidos como `indisponivel` no modo legivel; `0` continua sendo um valor valido. O historico nao esta disponivel neste momento.
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
87
132
|
## Builds GitHub
|
|
88
133
|
|
|
89
134
|
Use `zenifra builds` para listar o historico de builds e `zenifra builds logs` para ler os logs do pipeline GitHub de um build especifico.
|
|
@@ -125,7 +170,7 @@ zenifra plans --type database
|
|
|
125
170
|
zenifra plans --type storage --json
|
|
126
171
|
```
|
|
127
172
|
|
|
128
|
-
`zenifra plans` funciona sem autenticacao e mostra os catalogos publicos de HTTP, banco e
|
|
173
|
+
`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
174
|
|
|
130
175
|
Valores de variaveis de ambiente sao mascarados por padrao, inclusive em `--json`.
|
|
131
176
|
Use `--show-values` apenas quando precisar inspecionar os valores completos.
|
|
@@ -193,6 +238,9 @@ Use os arquivos em `examples/` como base para `zenifra create project`:
|
|
|
193
238
|
- `examples/http-autoscaling-project.json`: projeto HTTP pago criado com auto-scaling
|
|
194
239
|
- `examples/postgresql-project.json`: projeto PostgreSQL
|
|
195
240
|
- `examples/mariadb-project.json`: projeto MariaDB
|
|
241
|
+
- `examples/valkey-key-value-project.json`: projeto Valkey Key Value com armazenamento persistente
|
|
242
|
+
- `examples/valkey-cache-project.json`: projeto Valkey Cache sem armazenamento persistente
|
|
243
|
+
- `examples/valkey-queue-project.json`: projeto Valkey Queue com armazenamento persistente
|
|
196
244
|
|
|
197
245
|
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
246
|
|
|
@@ -205,6 +253,7 @@ O wizard atual cobre:
|
|
|
205
253
|
- projetos `http` com origem `github` ou `oci`
|
|
206
254
|
- projetos `postgresql`
|
|
207
255
|
- projetos `mariadb`
|
|
256
|
+
- projetos `valkey` nos perfis `key_value`, `cache` e `queue`
|
|
208
257
|
|
|
209
258
|
`zenifra create project` nao assume valores default para `--plan` e `--payment-mode`.
|
|
210
259
|
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 +262,12 @@ Antes de escolher um plano com o usuario, compare os catalogos com `zenifra plan
|
|
|
213
262
|
Valores aceitos:
|
|
214
263
|
|
|
215
264
|
- `payment_mode`: `hourly`, `monthly`, `yearly`
|
|
216
|
-
- `type_project` no `config`: `http`, `postgresql`, `mariadb`
|
|
265
|
+
- `type_project` no `config`: `http`, `postgresql`, `mariadb`, `valkey`
|
|
217
266
|
- `exposure` no `config` HTTP: `public`, `private`
|
|
218
|
-
- `plan`:
|
|
267
|
+
- `plan`: consulte `zenifra plans` para os planos atuais; Valkey usa `db-*` para Key Value, `cache-*` para Cache e `queue-*` para Queue
|
|
268
|
+
- `config.profile` em projetos Valkey: `key_value`, `cache` ou `queue`
|
|
269
|
+
- `config.version` em projetos Valkey: a versão retornada por `zenifra plans --type valkey`
|
|
270
|
+
- `config.storage` em projetos Valkey: obrigatório e persistente para Key Value/Queue; omitido para Cache
|
|
219
271
|
- `config.github.runtime` (quando houver GitHub em projeto HTTP): `nodejs` ou `python`
|
|
220
272
|
- `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
273
|
|
|
@@ -226,6 +278,9 @@ Observacoes do wizard:
|
|
|
226
278
|
- na criacao com auto-scaling, `config.instances` e o minimo inicial e `config.autoscaling.max_instances` e o maximo
|
|
227
279
|
- em projetos de banco, o wizard nao pergunta `username`, `password` nem `database name`
|
|
228
280
|
- em projetos de banco, a CLI preenche apenas campos tecnicos minimos exigidos pela validacao atual da API
|
|
281
|
+
- 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
|
|
282
|
+
- 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
|
|
283
|
+
- `valkey credentials rotate` retorna uma operação assíncrona; use `--wait` ou `valkey credentials status` para acompanhar
|
|
229
284
|
|
|
230
285
|
## Regressao manual de auto-scaling em staging
|
|
231
286
|
|
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,14 +169,19 @@ 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]
|
|
163
183
|
zenifra project metrics --project <id> [--instance <id>] [--json]
|
|
184
|
+
zenifra project metrics capabilities --project <id> [--json]
|
|
164
185
|
zenifra project network --project <id> [--view <summary|status-codes|routes|user-agents|request-events|source-ips>] [--json]
|
|
165
186
|
zenifra project image set --project <id> --image <image> [--json]
|
|
166
187
|
zenifra project exposure set --project <id> --exposure <public|private> [--json]
|
|
@@ -319,16 +340,65 @@ const HELP_SPECS = [
|
|
|
319
340
|
},
|
|
320
341
|
{
|
|
321
342
|
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
|
|
343
|
+
usage: 'zenifra plans [--type <all|http|database|storage|valkey>] [--json]',
|
|
344
|
+
description: 'Lista os catalogos publicos de preco de planos HTTP, banco, armazenamento e Valkey.',
|
|
345
|
+
flags: ['--type <type> Filtra o catalogo: all, http, database, storage, valkey, key-value, cache ou queue.', '--json Imprime a resposta em JSON.'],
|
|
346
|
+
examples: ['zenifra plans', 'zenifra plans --type http', 'zenifra plans --type valkey --json'],
|
|
326
347
|
output: 'HTTP\nPlano Hora Mes Ano Recursos\nfree R$ 0,00 R$ 0,00 R$ 0,00 1 GB Armazenamento Efemero',
|
|
327
348
|
jsonOutput: '{"http":[{"plan":"free","prices":{"hourly":0,"monthly":0,"yearly":0},"features":["1 GB Armazenamento Efemero"]}],"database":[],"storage":[]}',
|
|
328
349
|
},
|
|
350
|
+
{
|
|
351
|
+
command: 'valkey',
|
|
352
|
+
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]',
|
|
353
|
+
description: 'Consulta projetos Valkey e gerencia suas conexoes e credenciais.',
|
|
354
|
+
examples: ['zenifra valkey status --project proj_1', 'zenifra valkey connection --project proj_1', 'zenifra valkey credentials rotate --project proj_1 --wait'],
|
|
355
|
+
output: 'Zenifra CLI - valkey',
|
|
356
|
+
notes: ['A string de conexao completa aparece somente quando uma criacao ou rotacao a disponibiliza.'],
|
|
357
|
+
},
|
|
358
|
+
{
|
|
359
|
+
command: 'valkey status',
|
|
360
|
+
usage: 'zenifra valkey status --project <id> [--json]',
|
|
361
|
+
description: 'Mostra estado, perfil, versao e persistencia de um projeto Valkey.',
|
|
362
|
+
flags: ['--project <id> ID do projeto.', '--json Imprime a resposta em JSON.'],
|
|
363
|
+
examples: ['zenifra valkey status --project proj_1'],
|
|
364
|
+
output: 'Status Perfil Versao Persistente Instancias\nrunning cache 9.1.1 nao 1',
|
|
365
|
+
},
|
|
366
|
+
{
|
|
367
|
+
command: 'valkey connection',
|
|
368
|
+
usage: 'zenifra valkey connection --project <id> [--json]',
|
|
369
|
+
description: 'Mostra os dados de conexao de um projeto Valkey sem revelar a credencial.',
|
|
370
|
+
flags: ['--project <id> ID do projeto.', '--json Imprime a resposta em JSON.'],
|
|
371
|
+
examples: ['zenifra valkey connection --project proj_1'],
|
|
372
|
+
output: 'Host Porta TLS Conexao\nvalkey.example.com 6380 sim valkeys://default:********@valkey.example.com:6380/0',
|
|
373
|
+
notes: ['Use a credencial recebida na criacao ou em uma rotacao concluida.'],
|
|
374
|
+
},
|
|
375
|
+
{
|
|
376
|
+
command: 'valkey credentials',
|
|
377
|
+
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]',
|
|
378
|
+
description: 'Agrupa comandos de renovacao e acompanhamento de credenciais Valkey.',
|
|
379
|
+
examples: ['zenifra valkey credentials rotate --project proj_1', 'zenifra valkey credentials status --project proj_1 --operation operation_1'],
|
|
380
|
+
output: 'Zenifra CLI - valkey credentials',
|
|
381
|
+
},
|
|
382
|
+
{
|
|
383
|
+
command: 'valkey credentials rotate',
|
|
384
|
+
usage: 'zenifra valkey credentials rotate --project <id> [--idempotency-key <key>] [--wait] [--interval <seconds>] [--timeout <seconds>] [--json]',
|
|
385
|
+
description: 'Solicita a renovacao da credencial e retorna uma operacao acompanhavel.',
|
|
386
|
+
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.'],
|
|
387
|
+
examples: ['zenifra valkey credentials rotate --project proj_1', 'zenifra valkey credentials rotate --project proj_1 --wait --timeout 120'],
|
|
388
|
+
output: 'Operacao aceita: rotation_123\nEstado: accepted',
|
|
389
|
+
notes: ['Salve a nova string de conexao quando a operacao atingir completed.'],
|
|
390
|
+
},
|
|
391
|
+
{
|
|
392
|
+
command: 'valkey credentials status',
|
|
393
|
+
usage: 'zenifra valkey credentials status --project <id> --operation <id> [--json]',
|
|
394
|
+
description: 'Consulta o estado de uma renovacao de credencial Valkey.',
|
|
395
|
+
flags: ['--project <id> ID do projeto.', '--operation <id> ID da operacao.', '--json Imprime a resposta em JSON.'],
|
|
396
|
+
examples: ['zenifra valkey credentials status --project proj_1 --operation rotation_123'],
|
|
397
|
+
output: 'Operacao: rotation_123\nEstado: completed',
|
|
398
|
+
},
|
|
329
399
|
{
|
|
330
400
|
command: 'projects',
|
|
331
|
-
usage: 'zenifra projects [--json] [--org <id>] [--type <http|postgresql|mariadb>] [--page <n>] [--limit <n>]',
|
|
401
|
+
usage: 'zenifra projects [--json] [--org <id>] [--type <http|postgresql|mariadb|valkey>] [--page <n>] [--limit <n>]',
|
|
332
402
|
description: 'Lista projetos da organizacao ativa ou da API key.',
|
|
333
403
|
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
404
|
examples: ['zenifra projects --type http --page 1 --limit 15', 'zenifra projects --json'],
|
|
@@ -337,9 +407,9 @@ const HELP_SPECS = [
|
|
|
337
407
|
},
|
|
338
408
|
{
|
|
339
409
|
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]',
|
|
410
|
+
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
411
|
description: 'Cria um projeto via wizard interativo ou via payload de configuracao JSON.',
|
|
342
|
-
flags: ['--name <name>
|
|
412
|
+
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
413
|
examples: ['zenifra create project', 'zenifra create project --name api-web --plan free --payment-mode hourly --config @examples/http-project.json'],
|
|
344
414
|
output: 'Campo Valor\n------- --------------------------------------\nProjeto 507f1f77bcf86cd799439012\nDominio https://api-web.client.zenifra.com',
|
|
345
415
|
jsonOutput: '{"status":"success","data":{"id":"507f1f77bcf86cd799439012","name":"api-web"}}',
|
|
@@ -347,11 +417,11 @@ const HELP_SPECS = [
|
|
|
347
417
|
},
|
|
348
418
|
{
|
|
349
419
|
command: 'project',
|
|
350
|
-
usage: 'zenifra project\n zenifra project info --project <id> [--json]\n zenifra project url --project <id> [--json]\n zenifra project logs --project <id> [--instance <id>] [--json]\n zenifra project metrics --project <id> [--instance <id>] [--json]\n zenifra project network --project <id> [--view <summary|status-codes|routes|user-agents|request-events|source-ips>] [--json]\n zenifra project image set --project <id> --image <image> [--json]\n zenifra project exposure set --project <id> --exposure <public|private> [--json]\n zenifra project envs --project <id> [--json] [--show-values]\n zenifra project env add --project <id> --name <name> --value <value> [--json]\n zenifra project env update --project <id> --name <name> --value <value> [--json]\n zenifra project env remove --project <id> --name <name> [--json]\n zenifra project autoscaling --project <id> [--json]\n zenifra project autoscaling set --project <id> --min <n> --max <n> [--cpu <percent>] [--memory <percent>] [--json]\n zenifra project autoscaling disable --project <id> [--json]\n zenifra project autoscaling events --project <id> [--direction <scale_up|scale_down>] [--from <iso>] [--to <iso>] [--page <n>] [--limit <n>] [--json]\n zenifra project billing usage --project <id> [--from <iso>] [--to <iso>] [--page <n>] [--limit <n>] [--json]\n zenifra project instances --project <id> [--json]\n zenifra project instances set --project <id> --count <n> [--json]',
|
|
420
|
+
usage: 'zenifra project\n zenifra project info --project <id> [--json]\n zenifra project url --project <id> [--json]\n zenifra project logs --project <id> [--instance <id>] [--json]\n zenifra project metrics --project <id> [--instance <id>] [--json]\n zenifra project metrics capabilities --project <id> [--json]\n zenifra project network --project <id> [--view <summary|status-codes|routes|user-agents|request-events|source-ips>] [--json]\n zenifra project image set --project <id> --image <image> [--json]\n zenifra project exposure set --project <id> --exposure <public|private> [--json]\n zenifra project envs --project <id> [--json] [--show-values]\n zenifra project env add --project <id> --name <name> --value <value> [--json]\n zenifra project env update --project <id> --name <name> --value <value> [--json]\n zenifra project env remove --project <id> --name <name> [--json]\n zenifra project autoscaling --project <id> [--json]\n zenifra project autoscaling set --project <id> --min <n> --max <n> [--cpu <percent>] [--memory <percent>] [--json]\n zenifra project autoscaling disable --project <id> [--json]\n zenifra project autoscaling events --project <id> [--direction <scale_up|scale_down>] [--from <iso>] [--to <iso>] [--page <n>] [--limit <n>] [--json]\n zenifra project billing usage --project <id> [--from <iso>] [--to <iso>] [--page <n>] [--limit <n>] [--json]\n zenifra project instances --project <id> [--json]\n zenifra project instances set --project <id> --count <n> [--json]',
|
|
351
421
|
description: 'Agrupa comandos operacionais e de introspecao sobre um projeto especifico.',
|
|
352
422
|
examples: ['zenifra project', 'zenifra project info --project proj_1', 'zenifra project env add --project proj_1 --name NODE_ENV --value production'],
|
|
353
423
|
output: 'Zenifra CLI - project',
|
|
354
|
-
notes: ['Use "zenifra help project <subcomando>" para detalhes de info, url, logs, metrics, network, image, exposure, autoscaling, billing, envs e instances.'],
|
|
424
|
+
notes: ['Use "zenifra help project <subcomando>" para detalhes de info, url, logs, metrics, capabilities, network, image, exposure, autoscaling, billing, envs e instances.'],
|
|
355
425
|
},
|
|
356
426
|
{
|
|
357
427
|
command: 'project info',
|
|
@@ -384,11 +454,26 @@ const HELP_SPECS = [
|
|
|
384
454
|
{
|
|
385
455
|
command: 'project metrics',
|
|
386
456
|
usage: 'zenifra project metrics --project <id> [--instance <id>] [--json]',
|
|
387
|
-
description: 'Mostra
|
|
388
|
-
flags: ['--project <id> ID do projeto.', '--instance <id> Filtra uma instancia.', '--json Imprime a resposta em JSON.'],
|
|
389
|
-
examples: ['zenifra project metrics --project 507f1f77bcf86cd799439012 --instance web-1'],
|
|
390
|
-
output: '
|
|
391
|
-
jsonOutput: '{"type":"
|
|
457
|
+
description: 'Mostra metricas de recursos do projeto e, para Valkey, o snapshot nativo da instancia.',
|
|
458
|
+
flags: ['--project <id> ID do projeto.', '--instance <id> Filtra uma instancia.', '--json Imprime a resposta em JSON sem formatacao adicional.'],
|
|
459
|
+
examples: ['zenifra project metrics --project 507f1f77bcf86cd799439012 --instance web-1', 'zenifra project metrics --project 507f1f77bcf86cd799439012 --instance instance-1'],
|
|
460
|
+
output: 'Campo Valor\n---------- ----------------\nPerfil cache\nDisponibilidade available\nMemoria usada 100 MB',
|
|
461
|
+
jsonOutput: '{"instance":"instance-1","type":"valkey","availability":"available","valkey":{"schema_version":1,"profile":"cache","memory":{"used_bytes":104857600}}}',
|
|
462
|
+
notes: [
|
|
463
|
+
'Para projetos Valkey, a saida legivel inclui recursos, capacidade, clientes, atividade, ciclo de vida de chaves, perfil e confiabilidade.',
|
|
464
|
+
'Valores ausentes sao exibidos como indisponivel; zero continua sendo um valor valido.',
|
|
465
|
+
'Use "zenifra project metrics capabilities --project <id>" para consultar o acesso e os grupos de metricas disponiveis.',
|
|
466
|
+
],
|
|
467
|
+
},
|
|
468
|
+
{
|
|
469
|
+
command: 'project metrics capabilities',
|
|
470
|
+
usage: 'zenifra project metrics capabilities --project <id> [--json]',
|
|
471
|
+
description: 'Mostra os grupos de metricas e o nivel de acesso disponivel para o projeto.',
|
|
472
|
+
flags: ['--project <id> ID do projeto.', '--json Imprime a resposta em JSON.'],
|
|
473
|
+
examples: ['zenifra project metrics capabilities --project 507f1f77bcf86cd799439012'],
|
|
474
|
+
output: 'Campo Valor\n---------- ------------------------------------------------------------\nAcesso snapshot\nAtualizacao 60 s\nHistorico indisponivel',
|
|
475
|
+
jsonOutput: '{"access":"snapshot","groups":["resources","capacity","clients","activity","key_lifecycle","profile","reliability"],"refresh_seconds":60,"history":null}',
|
|
476
|
+
notes: ['A API permanece como fonte de autorizacao. A CLI nao mantem uma lista local de planos.'],
|
|
392
477
|
},
|
|
393
478
|
{
|
|
394
479
|
command: 'project network',
|
|
@@ -654,7 +739,7 @@ function removedProjectsCreateMessage() {
|
|
|
654
739
|
}
|
|
655
740
|
|
|
656
741
|
function isNamespaceCommand(command) {
|
|
657
|
-
return ['auth', 'profile', 'project', 'org'].includes(command);
|
|
742
|
+
return ['auth', 'profile', 'project', 'org', 'valkey'].includes(command);
|
|
658
743
|
}
|
|
659
744
|
|
|
660
745
|
function parseArgs(argv) {
|
|
@@ -1029,6 +1114,11 @@ function estimateWizardTotal(state = {}) {
|
|
|
1029
1114
|
return total;
|
|
1030
1115
|
}
|
|
1031
1116
|
|
|
1117
|
+
if (typeProject === 'valkey') {
|
|
1118
|
+
total += 3;
|
|
1119
|
+
return total;
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1032
1122
|
return 18;
|
|
1033
1123
|
}
|
|
1034
1124
|
|
|
@@ -1346,6 +1436,7 @@ async function request(session, flags, method, path, {
|
|
|
1346
1436
|
orgId,
|
|
1347
1437
|
tokenRequired = true,
|
|
1348
1438
|
credential: credentialOverride,
|
|
1439
|
+
headers: extraHeaders = {},
|
|
1349
1440
|
} = {}) {
|
|
1350
1441
|
const credential = credentialOverride || resolveCredential(session);
|
|
1351
1442
|
if (tokenRequired && !credential) {
|
|
@@ -1356,6 +1447,14 @@ async function request(session, flags, method, path, {
|
|
|
1356
1447
|
if (body !== undefined) {
|
|
1357
1448
|
headers['Content-Type'] = 'application/json';
|
|
1358
1449
|
}
|
|
1450
|
+
for (const [name, value] of Object.entries(extraHeaders)) {
|
|
1451
|
+
if (value === undefined) continue;
|
|
1452
|
+
const normalizedName = name.toLowerCase();
|
|
1453
|
+
if (['authorization', 'x-organization-id', 'content-type'].includes(normalizedName)) {
|
|
1454
|
+
throw new CliError(`Header nao permitido: ${name}.`);
|
|
1455
|
+
}
|
|
1456
|
+
headers[name] = String(value);
|
|
1457
|
+
}
|
|
1359
1458
|
if (credential?.token) {
|
|
1360
1459
|
headers.Authorization = `Bearer ${credential.token}`;
|
|
1361
1460
|
}
|
|
@@ -1403,6 +1502,9 @@ async function request(session, flags, method, path, {
|
|
|
1403
1502
|
if (response.status === 429 && Number.isFinite(Number(payload?.retry_after_seconds))) {
|
|
1404
1503
|
throw new CliError(`${message} Tente novamente em ${Number(payload.retry_after_seconds)} segundo(s).`);
|
|
1405
1504
|
}
|
|
1505
|
+
if (response.status === 402 && path.includes('/metrics') && /support metrics/i.test(String(message))) {
|
|
1506
|
+
throw new CliError('Este projeto nao possui acesso a metricas.');
|
|
1507
|
+
}
|
|
1406
1508
|
if (String(message).includes('missing x-organization-id')) {
|
|
1407
1509
|
throw new CliError('Organizacao nao selecionada. Rode "zenifra org set" ou use --org <id>.');
|
|
1408
1510
|
}
|
|
@@ -1443,6 +1545,128 @@ function printTable(rows, columns) {
|
|
|
1443
1545
|
}
|
|
1444
1546
|
}
|
|
1445
1547
|
|
|
1548
|
+
function formatValkeyNumber(value) {
|
|
1549
|
+
if (value === null || value === undefined || !Number.isFinite(Number(value))) return 'indisponivel';
|
|
1550
|
+
return String(value);
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
function formatValkeyBytes(value, suffix = '') {
|
|
1554
|
+
if (value === null || value === undefined || !Number.isFinite(Number(value))) return 'indisponivel';
|
|
1555
|
+
|
|
1556
|
+
const numericValue = Number(value);
|
|
1557
|
+
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
1558
|
+
let unitIndex = 0;
|
|
1559
|
+
let normalizedValue = numericValue;
|
|
1560
|
+
while (normalizedValue >= 1024 && unitIndex < units.length - 1) {
|
|
1561
|
+
normalizedValue /= 1024;
|
|
1562
|
+
unitIndex += 1;
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
const digits = normalizedValue >= 100 ? 0 : normalizedValue >= 10 ? 1 : 2;
|
|
1566
|
+
const formattedValue = Number(normalizedValue.toFixed(digits)).toString();
|
|
1567
|
+
return `${formattedValue} ${units[unitIndex]}${suffix}`;
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
function formatValkeyPercent(value) {
|
|
1571
|
+
if (value === null || value === undefined || !Number.isFinite(Number(value))) return 'indisponivel';
|
|
1572
|
+
return `${Number((Number(value) * 100).toFixed(2))}%`;
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
function valkeyMetricRows(metrics) {
|
|
1576
|
+
const native = metrics?.valkey;
|
|
1577
|
+
const rows = [
|
|
1578
|
+
{ field: 'Instancia', value: metrics?.instance || '-' },
|
|
1579
|
+
{ field: 'Disponibilidade', value: metrics?.availability || 'indisponivel' },
|
|
1580
|
+
{ field: 'Observado em', value: metrics?.observed_at || 'indisponivel' },
|
|
1581
|
+
{ field: 'CPU', value: formatValkeyNumber(metrics?.cpu) },
|
|
1582
|
+
{ field: 'Memoria', value: formatValkeyBytes(metrics?.memory) },
|
|
1583
|
+
];
|
|
1584
|
+
|
|
1585
|
+
if (!native) {
|
|
1586
|
+
rows.push({ field: 'Metricas Valkey', value: 'indisponivel' });
|
|
1587
|
+
return rows;
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
rows.push(
|
|
1591
|
+
{ field: 'Perfil', value: native.profile || '-' },
|
|
1592
|
+
{ field: 'Memoria usada', value: formatValkeyBytes(native.memory?.used_bytes) },
|
|
1593
|
+
{ field: 'Pico de memoria', value: formatValkeyBytes(native.memory?.peak_bytes) },
|
|
1594
|
+
{ field: 'Limite de memoria', value: formatValkeyBytes(native.memory?.limit_bytes) },
|
|
1595
|
+
{ field: 'Fragmentacao', value: formatValkeyNumber(native.memory?.fragmentation_ratio) },
|
|
1596
|
+
{ field: 'Clientes conectados', value: formatValkeyNumber(native.clients?.connected) },
|
|
1597
|
+
{ field: 'Clientes bloqueados', value: formatValkeyNumber(native.clients?.blocked) },
|
|
1598
|
+
{ field: 'Operacoes por segundo', value: formatValkeyNumber(native.activity?.operations_per_second) },
|
|
1599
|
+
{ field: 'Entrada', value: formatValkeyBytes(native.activity?.input_bytes_per_second, '/s') },
|
|
1600
|
+
{ field: 'Saida', value: formatValkeyBytes(native.activity?.output_bytes_per_second, '/s') },
|
|
1601
|
+
{ field: 'Chaves expiradas', value: formatValkeyNumber(native.keys?.expired_total) },
|
|
1602
|
+
{ field: 'Chaves removidas', value: formatValkeyNumber(native.keys?.evicted_total) },
|
|
1603
|
+
{ field: 'Uptime', value: `${formatValkeyNumber(native.uptime_seconds)} s` },
|
|
1604
|
+
);
|
|
1605
|
+
|
|
1606
|
+
if (native.cache) {
|
|
1607
|
+
rows.push(
|
|
1608
|
+
{ field: 'Cache hits', value: formatValkeyNumber(native.cache.hits_total) },
|
|
1609
|
+
{ field: 'Cache misses', value: formatValkeyNumber(native.cache.misses_total) },
|
|
1610
|
+
{ field: 'Hit ratio', value: formatValkeyPercent(native.cache.hit_ratio) },
|
|
1611
|
+
);
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
if (native.key_value) {
|
|
1615
|
+
rows.push(
|
|
1616
|
+
{ field: 'Total de chaves', value: formatValkeyNumber(native.key_value.keys_total) },
|
|
1617
|
+
{ field: 'Chaves com expiracao', value: formatValkeyNumber(native.key_value.keys_with_expiration) },
|
|
1618
|
+
);
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
if (native.reliability) {
|
|
1622
|
+
const replication = native.reliability.replication;
|
|
1623
|
+
const persistence = native.reliability.persistence;
|
|
1624
|
+
if (replication) {
|
|
1625
|
+
rows.push(
|
|
1626
|
+
{ field: 'Replicacao', value: replication.status || 'indisponivel' },
|
|
1627
|
+
{ field: 'Repl. disponiveis', value: formatValkeyNumber(replication.replicas_available) },
|
|
1628
|
+
{ field: 'Repl. esperadas', value: formatValkeyNumber(replication.replicas_expected) },
|
|
1629
|
+
{ field: 'Lag', value: replication.lag_seconds === null || replication.lag_seconds === undefined ? 'indisponivel' : `${replication.lag_seconds} s` },
|
|
1630
|
+
);
|
|
1631
|
+
}
|
|
1632
|
+
if (persistence) {
|
|
1633
|
+
rows.push(
|
|
1634
|
+
{ field: 'Persistencia', value: persistence.status || 'indisponivel' },
|
|
1635
|
+
{ field: 'Persistencia ativa', value: persistence.enabled === null || persistence.enabled === undefined ? 'indisponivel' : persistence.enabled ? 'sim' : 'nao' },
|
|
1636
|
+
{ field: 'Ultimo sucesso', value: persistence.last_success_at || 'indisponivel' },
|
|
1637
|
+
);
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
return rows;
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
function printValkeyMetrics(metrics) {
|
|
1645
|
+
printTable(valkeyMetricRows(metrics), [
|
|
1646
|
+
{ label: 'Campo', value: (row) => row.field },
|
|
1647
|
+
{ label: 'Valor', value: (row) => row.value },
|
|
1648
|
+
]);
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
function printMetricsCapabilities(capabilities) {
|
|
1652
|
+
const groups = Array.isArray(capabilities?.groups) && capabilities.groups.length > 0
|
|
1653
|
+
? capabilities.groups.join(', ')
|
|
1654
|
+
: 'nenhum';
|
|
1655
|
+
const history = capabilities?.history
|
|
1656
|
+
? `${capabilities.history.retention_seconds} s de retencao`
|
|
1657
|
+
: 'indisponivel';
|
|
1658
|
+
|
|
1659
|
+
printTable([
|
|
1660
|
+
{ field: 'Acesso', value: capabilities?.access || 'indisponivel' },
|
|
1661
|
+
{ field: 'Grupos', value: groups },
|
|
1662
|
+
{ field: 'Atualizacao', value: capabilities?.refresh_seconds === null || capabilities?.refresh_seconds === undefined ? 'indisponivel' : `${capabilities.refresh_seconds} s` },
|
|
1663
|
+
{ field: 'Historico', value: history },
|
|
1664
|
+
], [
|
|
1665
|
+
{ label: 'Campo', value: (row) => row.field },
|
|
1666
|
+
{ label: 'Valor', value: (row) => row.value },
|
|
1667
|
+
]);
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1446
1670
|
function asArray(value) {
|
|
1447
1671
|
if (Array.isArray(value)) return value;
|
|
1448
1672
|
if (Array.isArray(value?.projects)) return value.projects;
|
|
@@ -1508,20 +1732,33 @@ function printEnvs(envs, flags) {
|
|
|
1508
1732
|
}
|
|
1509
1733
|
|
|
1510
1734
|
function printProject(project) {
|
|
1511
|
-
const
|
|
1735
|
+
const managedService = project.managed_service;
|
|
1736
|
+
const isValkey = project.type_project === 'valkey' || managedService?.engine === 'valkey';
|
|
1512
1737
|
const rows = [
|
|
1513
1738
|
{ label: 'ID', value: projectIdOf(project) || '-' },
|
|
1514
1739
|
{ label: 'Nome', value: project.name || '-' },
|
|
1515
1740
|
{ label: 'Status', value: project.status || '-' },
|
|
1516
1741
|
{ label: 'Tipo', value: project.type_project || '-' },
|
|
1517
|
-
{ label: 'Exposicao', value: project.exposure || '-' },
|
|
1518
1742
|
{ 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
1743
|
];
|
|
1524
1744
|
|
|
1745
|
+
if (isValkey) {
|
|
1746
|
+
rows.push(
|
|
1747
|
+
{ label: 'Perfil', value: managedService?.profile || '-' },
|
|
1748
|
+
{ label: 'Versao', value: managedService?.version || '-' },
|
|
1749
|
+
{ label: 'Persistente', value: managedService?.topology?.persistent === true ? 'sim' : 'nao' },
|
|
1750
|
+
{ label: 'Instancias', value: project.instances ?? '-' },
|
|
1751
|
+
);
|
|
1752
|
+
} else {
|
|
1753
|
+
rows.push(
|
|
1754
|
+
{ label: 'Exposicao', value: project.exposure || '-' },
|
|
1755
|
+
{ label: 'URL', value: projectUrlOf(project) || '-' },
|
|
1756
|
+
{ label: 'Imagem', value: project.image || '-' },
|
|
1757
|
+
{ label: 'Instancias', value: project.instances ?? '-' },
|
|
1758
|
+
{ label: 'Instancias atuais', value: project.additional_info?.current_instances ?? '-' },
|
|
1759
|
+
);
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1525
1762
|
for (const row of rows) {
|
|
1526
1763
|
process.stdout.write(`${row.label}: ${row.value}\n`);
|
|
1527
1764
|
}
|
|
@@ -1937,6 +2174,26 @@ function normalizePlan(value) {
|
|
|
1937
2174
|
['dbpremium', 'db-premium'],
|
|
1938
2175
|
['db_enterprise', 'db-enterprise'],
|
|
1939
2176
|
['dbenterprise', 'db-enterprise'],
|
|
2177
|
+
['cache_free', 'cache-free'],
|
|
2178
|
+
['cachefree', 'cache-free'],
|
|
2179
|
+
['cache_starter', 'cache-starter'],
|
|
2180
|
+
['cachestarter', 'cache-starter'],
|
|
2181
|
+
['cache_basic', 'cache-basic'],
|
|
2182
|
+
['cachebasic', 'cache-basic'],
|
|
2183
|
+
['cache_premium', 'cache-premium'],
|
|
2184
|
+
['cachepremium', 'cache-premium'],
|
|
2185
|
+
['cache_enterprise', 'cache-enterprise'],
|
|
2186
|
+
['cacheenterprise', 'cache-enterprise'],
|
|
2187
|
+
['queue_free', 'queue-free'],
|
|
2188
|
+
['queuefree', 'queue-free'],
|
|
2189
|
+
['queue_starter', 'queue-starter'],
|
|
2190
|
+
['queuestarter', 'queue-starter'],
|
|
2191
|
+
['queue_basic', 'queue-basic'],
|
|
2192
|
+
['queuebasic', 'queue-basic'],
|
|
2193
|
+
['queue_premium', 'queue-premium'],
|
|
2194
|
+
['queuepremium', 'queue-premium'],
|
|
2195
|
+
['queue_enterprise', 'queue-enterprise'],
|
|
2196
|
+
['queueenterprise', 'queue-enterprise'],
|
|
1940
2197
|
]);
|
|
1941
2198
|
|
|
1942
2199
|
if (aliases.has(normalized)) return aliases.get(normalized);
|
|
@@ -1973,6 +2230,21 @@ function normalizeTypeProject(value) {
|
|
|
1973
2230
|
['postgresql', 'postgresql'],
|
|
1974
2231
|
['mariadb', 'mariadb'],
|
|
1975
2232
|
['maria db', 'mariadb'],
|
|
2233
|
+
['valkey', 'valkey'],
|
|
2234
|
+
['key value', 'valkey'],
|
|
2235
|
+
['key-value', 'valkey'],
|
|
2236
|
+
]);
|
|
2237
|
+
return aliases.get(raw) || raw;
|
|
2238
|
+
}
|
|
2239
|
+
|
|
2240
|
+
function normalizeValkeyProfile(value) {
|
|
2241
|
+
const raw = normalizeFreeText(value);
|
|
2242
|
+
const aliases = new Map([
|
|
2243
|
+
['key_value', 'key_value'],
|
|
2244
|
+
['key value', 'key_value'],
|
|
2245
|
+
['key-value', 'key_value'],
|
|
2246
|
+
['cache', 'cache'],
|
|
2247
|
+
['queue', 'queue'],
|
|
1976
2248
|
]);
|
|
1977
2249
|
return aliases.get(raw) || raw;
|
|
1978
2250
|
}
|
|
@@ -2075,7 +2347,6 @@ async function fetchWizardCatalogs(session, flags) {
|
|
|
2075
2347
|
unwrapData(await request(session, flags, 'GET', '/project/database/plans')),
|
|
2076
2348
|
'os planos de banco',
|
|
2077
2349
|
);
|
|
2078
|
-
|
|
2079
2350
|
return { httpPlans, availableInstances, databasePlans };
|
|
2080
2351
|
}
|
|
2081
2352
|
|
|
@@ -2084,6 +2355,10 @@ function normalizePlansCatalogType(value) {
|
|
|
2084
2355
|
if (!normalized || normalized === 'all') return 'all';
|
|
2085
2356
|
if (normalized === 'http') return 'http';
|
|
2086
2357
|
if (['database', 'db', 'postgresql', 'mariadb'].includes(normalized)) return 'database';
|
|
2358
|
+
if (normalized === 'valkey') return 'valkey';
|
|
2359
|
+
if (normalized === 'key-value' || normalized === 'key value' || normalized === 'key_value') return 'valkey-key_value';
|
|
2360
|
+
if (normalized === 'cache') return 'valkey-cache';
|
|
2361
|
+
if (normalized === 'queue') return 'valkey-queue';
|
|
2087
2362
|
if (normalized === 'storage') return 'storage';
|
|
2088
2363
|
return null;
|
|
2089
2364
|
}
|
|
@@ -2096,6 +2371,11 @@ function formatBrl(value) {
|
|
|
2096
2371
|
}).format(Number.isFinite(number) ? number : 0);
|
|
2097
2372
|
}
|
|
2098
2373
|
|
|
2374
|
+
function formatBrlFromCents(value) {
|
|
2375
|
+
const number = Number(value ?? 0);
|
|
2376
|
+
return formatBrl(Number.isFinite(number) ? number / 100 : 0);
|
|
2377
|
+
}
|
|
2378
|
+
|
|
2099
2379
|
function humanizeStorageName(value) {
|
|
2100
2380
|
const raw = String(value || '').trim();
|
|
2101
2381
|
if (!raw) return { storage: '-', type: '-' };
|
|
@@ -2116,6 +2396,49 @@ function humanizeStorageName(value) {
|
|
|
2116
2396
|
return { storage: raw, type: '-' };
|
|
2117
2397
|
}
|
|
2118
2398
|
|
|
2399
|
+
const VALKEY_PROFILE_LABELS = {
|
|
2400
|
+
key_value: 'Key Value',
|
|
2401
|
+
cache: 'Cache',
|
|
2402
|
+
queue: 'Queue',
|
|
2403
|
+
};
|
|
2404
|
+
|
|
2405
|
+
function requireValkeyCatalog(value) {
|
|
2406
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)
|
|
2407
|
+
|| value.engine !== 'valkey'
|
|
2408
|
+
|| typeof value.version !== 'string'
|
|
2409
|
+
|| typeof value.currency !== 'string'
|
|
2410
|
+
|| !Array.isArray(value.profiles)
|
|
2411
|
+
|| !Array.isArray(value.plans)
|
|
2412
|
+
|| value.profiles.length === 0
|
|
2413
|
+
|| value.plans.length === 0) {
|
|
2414
|
+
throw new CliError('Falha ao carregar o catalogo Valkey. Tente novamente mais tarde.');
|
|
2415
|
+
}
|
|
2416
|
+
|
|
2417
|
+
const validProfiles = new Set(Object.keys(VALKEY_PROFILE_LABELS));
|
|
2418
|
+
if (value.profiles.some((profile) => !validProfiles.has(profile?.id) || typeof profile.persistence !== 'boolean')) {
|
|
2419
|
+
throw new CliError('Falha ao carregar o catalogo Valkey. Tente novamente mais tarde.');
|
|
2420
|
+
}
|
|
2421
|
+
if (value.plans.some((plan) => !validProfiles.has(plan?.profile) || typeof plan.id !== 'string' || !plan.prices || typeof plan.prices !== 'object')) {
|
|
2422
|
+
throw new CliError('Falha ao carregar o catalogo Valkey. Tente novamente mais tarde.');
|
|
2423
|
+
}
|
|
2424
|
+
|
|
2425
|
+
return value;
|
|
2426
|
+
}
|
|
2427
|
+
|
|
2428
|
+
function valkeyProfileFilter(type) {
|
|
2429
|
+
if (!type.startsWith('valkey-')) return null;
|
|
2430
|
+
return type.slice('valkey-'.length);
|
|
2431
|
+
}
|
|
2432
|
+
|
|
2433
|
+
function filterValkeyCatalog(catalog, profile) {
|
|
2434
|
+
if (!profile) return catalog;
|
|
2435
|
+
return {
|
|
2436
|
+
...catalog,
|
|
2437
|
+
profiles: catalog.profiles.filter((entry) => entry.id === profile),
|
|
2438
|
+
plans: catalog.plans.filter((plan) => plan.profile === profile),
|
|
2439
|
+
};
|
|
2440
|
+
}
|
|
2441
|
+
|
|
2119
2442
|
async function fetchPlansCatalogs(session, flags, type) {
|
|
2120
2443
|
if (type === 'http') {
|
|
2121
2444
|
return {
|
|
@@ -2150,27 +2473,59 @@ async function fetchPlansCatalogs(session, flags, type) {
|
|
|
2150
2473
|
};
|
|
2151
2474
|
}
|
|
2152
2475
|
|
|
2153
|
-
|
|
2476
|
+
if (type === 'valkey' || type.startsWith('valkey-')) {
|
|
2477
|
+
const catalog = requireValkeyCatalog(
|
|
2478
|
+
unwrapData(await request(session, flags, 'GET', '/managed-services/catalog', { tokenRequired: false })),
|
|
2479
|
+
);
|
|
2480
|
+
return {
|
|
2481
|
+
http: [],
|
|
2482
|
+
database: [],
|
|
2483
|
+
storage: [],
|
|
2484
|
+
valkey: filterValkeyCatalog(catalog, valkeyProfileFilter(type)),
|
|
2485
|
+
};
|
|
2486
|
+
}
|
|
2487
|
+
|
|
2488
|
+
const [httpPayload, databasePayload, storagePayload, valkeyPayload] = await Promise.all([
|
|
2154
2489
|
request(session, flags, 'GET', '/project/plans', { tokenRequired: false }),
|
|
2155
2490
|
request(session, flags, 'GET', '/project/database/plans', { tokenRequired: false }),
|
|
2156
2491
|
request(session, flags, 'GET', '/project/storage/plans', { tokenRequired: false }),
|
|
2492
|
+
request(session, flags, 'GET', '/managed-services/catalog', { tokenRequired: false }),
|
|
2157
2493
|
]);
|
|
2158
2494
|
|
|
2159
2495
|
return {
|
|
2160
2496
|
http: requireWizardCatalogArray(unwrapData(httpPayload), 'os planos HTTP'),
|
|
2161
2497
|
database: requireWizardCatalogArray(unwrapData(databasePayload), 'os planos de banco'),
|
|
2162
2498
|
storage: requireWizardCatalogArray(unwrapData(storagePayload), 'os planos de storage'),
|
|
2499
|
+
valkey: requireValkeyCatalog(unwrapData(valkeyPayload)),
|
|
2163
2500
|
};
|
|
2164
2501
|
}
|
|
2165
2502
|
|
|
2503
|
+
function printValkeyCatalog(catalog, profileFilter) {
|
|
2504
|
+
const profiles = profileFilter ? [profileFilter] : Object.keys(VALKEY_PROFILE_LABELS);
|
|
2505
|
+
profiles.forEach((profile, index) => {
|
|
2506
|
+
if (index > 0) process.stdout.write('\n');
|
|
2507
|
+
process.stdout.write(`${VALKEY_PROFILE_LABELS[profile]}\n`);
|
|
2508
|
+
const plans = catalog.plans.filter((plan) => plan.profile === profile);
|
|
2509
|
+
printTable(plans, [
|
|
2510
|
+
{ label: 'Plano', value: (plan) => plan.id || '-' },
|
|
2511
|
+
{ label: 'Hora', value: (plan) => formatBrl(plan.prices?.hourly, catalog.currency) },
|
|
2512
|
+
{ label: 'Mes', value: (plan) => formatBrl(plan.prices?.monthly, catalog.currency) },
|
|
2513
|
+
{ label: 'Ano', value: (plan) => formatBrl(plan.prices?.yearly, catalog.currency) },
|
|
2514
|
+
{ label: 'Capacidade', value: (plan) => `${plan.resources?.cpu || '-'} / ${plan.resources?.memory || '-'}` },
|
|
2515
|
+
{ label: 'Storage incluído', value: (plan) => `${plan.included_storage_gb ?? 0} GB` },
|
|
2516
|
+
{ label: 'Alta disponibilidade', value: (plan) => plan.high_availability?.[profile] ? 'sim' : 'nao' },
|
|
2517
|
+
]);
|
|
2518
|
+
});
|
|
2519
|
+
}
|
|
2520
|
+
|
|
2166
2521
|
function printPlansCatalogs(payload, type) {
|
|
2167
2522
|
if (type === 'all' || type === 'http') {
|
|
2168
2523
|
process.stdout.write('HTTP\n');
|
|
2169
2524
|
printTable(asArray(payload.http), [
|
|
2170
2525
|
{ label: 'Plano', value: (plan) => plan.plan || '-' },
|
|
2171
|
-
{ label: 'Hora', value: (plan) =>
|
|
2172
|
-
{ label: 'Mes', value: (plan) =>
|
|
2173
|
-
{ label: 'Ano', value: (plan) =>
|
|
2526
|
+
{ label: 'Hora', value: (plan) => formatBrlFromCents(plan.prices?.hourly) },
|
|
2527
|
+
{ label: 'Mes', value: (plan) => formatBrlFromCents(plan.prices?.monthly) },
|
|
2528
|
+
{ label: 'Ano', value: (plan) => formatBrlFromCents(plan.prices?.yearly) },
|
|
2174
2529
|
{ label: 'Recursos', value: (plan) => asArray(plan.features).join(', ') || '-' },
|
|
2175
2530
|
]);
|
|
2176
2531
|
}
|
|
@@ -2180,9 +2535,9 @@ function printPlansCatalogs(payload, type) {
|
|
|
2180
2535
|
process.stdout.write('PostgreSQL / MariaDB\n');
|
|
2181
2536
|
printTable(asArray(payload.database), [
|
|
2182
2537
|
{ label: 'Plano', value: (plan) => plan.plan || '-' },
|
|
2183
|
-
{ label: 'Hora', value: (plan) =>
|
|
2184
|
-
{ label: 'Mes', value: (plan) =>
|
|
2185
|
-
{ label: 'Ano', value: (plan) =>
|
|
2538
|
+
{ label: 'Hora', value: (plan) => formatBrlFromCents(plan.prices?.hourly) },
|
|
2539
|
+
{ label: 'Mes', value: (plan) => formatBrlFromCents(plan.prices?.monthly) },
|
|
2540
|
+
{ label: 'Ano', value: (plan) => formatBrlFromCents(plan.prices?.yearly) },
|
|
2186
2541
|
{ label: 'Recursos', value: (plan) => asArray(plan.features).join(', ') || '-' },
|
|
2187
2542
|
]);
|
|
2188
2543
|
}
|
|
@@ -2198,12 +2553,17 @@ function printPlansCatalogs(payload, type) {
|
|
|
2198
2553
|
{ label: 'Ano', value: (storage) => formatBrl(storage.prices?.yearly) },
|
|
2199
2554
|
]);
|
|
2200
2555
|
}
|
|
2556
|
+
|
|
2557
|
+
if (type === 'all' || type === 'valkey' || type.startsWith('valkey-')) {
|
|
2558
|
+
if (type === 'all') process.stdout.write('\n');
|
|
2559
|
+
printValkeyCatalog(payload.valkey, valkeyProfileFilter(type));
|
|
2560
|
+
}
|
|
2201
2561
|
}
|
|
2202
2562
|
|
|
2203
2563
|
async function handlePlans(session, flags) {
|
|
2204
2564
|
const type = normalizePlansCatalogType(flags.type);
|
|
2205
2565
|
if (!type) {
|
|
2206
|
-
throw new CliError('Tipo de catalogo invalido. Valores aceitos: all, http, database, storage.');
|
|
2566
|
+
throw new CliError('Tipo de catalogo invalido. Valores aceitos: all, http, database, storage, valkey.');
|
|
2207
2567
|
}
|
|
2208
2568
|
|
|
2209
2569
|
const payload = await fetchPlansCatalogs(session, flags, type);
|
|
@@ -2261,9 +2621,53 @@ function validateCreateAutoscaling(config, plan, typeProject) {
|
|
|
2261
2621
|
return { ...config, autoscaling: nextAutoscaling };
|
|
2262
2622
|
}
|
|
2263
2623
|
|
|
2264
|
-
function
|
|
2624
|
+
function validateValkeyConfig(config, plan, catalog) {
|
|
2625
|
+
const valkeyCatalog = requireValkeyCatalog(catalog);
|
|
2626
|
+
const profile = normalizeValkeyProfile(config?.profile);
|
|
2627
|
+
const profileDefinition = valkeyCatalog.profiles.find((entry) => entry.id === profile);
|
|
2628
|
+
if (!profileDefinition) {
|
|
2629
|
+
throw new CliError('config.profile invalido. Use key_value, cache ou queue.');
|
|
2630
|
+
}
|
|
2631
|
+
|
|
2632
|
+
const planDefinition = valkeyCatalog.plans.find((entry) => entry.id === plan && entry.profile === profile);
|
|
2633
|
+
if (!planDefinition) {
|
|
2634
|
+
throw new CliError(`Plano ${plan} nao pode ser usado com o perfil ${profile}. Consulte "zenifra plans --type valkey".`);
|
|
2635
|
+
}
|
|
2636
|
+
|
|
2637
|
+
if (config.version !== valkeyCatalog.version) {
|
|
2638
|
+
throw new CliError(`config.version invalida: use ${valkeyCatalog.version}, conforme o catalogo Valkey.`);
|
|
2639
|
+
}
|
|
2640
|
+
|
|
2641
|
+
const forbiddenFields = ['instances', 'envs', 'image', 'github', 'port', 'exposure', 'autoscaling'];
|
|
2642
|
+
const forbiddenField = forbiddenFields.find((field) => config[field] !== undefined);
|
|
2643
|
+
if (forbiddenField) {
|
|
2644
|
+
throw new CliError(`Valkey nao aceita config.${forbiddenField}. A capacidade e definida pelo plano.`);
|
|
2645
|
+
}
|
|
2646
|
+
|
|
2647
|
+
if (profileDefinition.persistence) {
|
|
2648
|
+
if (!isRecord(config.storage) || config.storage.persistent !== true) {
|
|
2649
|
+
throw new CliError('Perfis Valkey persistentes exigem storage.persistent=true.');
|
|
2650
|
+
}
|
|
2651
|
+
const capacity = Number(config.storage.capacity);
|
|
2652
|
+
if (!Number.isInteger(capacity) || capacity < 1 || capacity > 250) {
|
|
2653
|
+
throw new CliError('storage.capacity deve ser um inteiro entre 1 e 250 GiB.');
|
|
2654
|
+
}
|
|
2655
|
+
} else if (config.storage !== undefined) {
|
|
2656
|
+
throw new CliError('O perfil Cache nao aceita storage.');
|
|
2657
|
+
}
|
|
2658
|
+
|
|
2659
|
+
return {
|
|
2660
|
+
...config,
|
|
2661
|
+
type_project: 'valkey',
|
|
2662
|
+
profile,
|
|
2663
|
+
version: valkeyCatalog.version,
|
|
2664
|
+
};
|
|
2665
|
+
}
|
|
2666
|
+
|
|
2667
|
+
function validateCreateInput({ plan, paymentMode, config, valkeyCatalog }) {
|
|
2668
|
+
const nextTypeProject = normalizeTypeProject(config?.type_project);
|
|
2265
2669
|
const nextPlan = normalizePlan(plan);
|
|
2266
|
-
if (!nextPlan || !ALLOWED_PLAN_VALUES.has(nextPlan)) {
|
|
2670
|
+
if (!nextPlan || (nextTypeProject !== 'valkey' && !ALLOWED_PLAN_VALUES.has(nextPlan))) {
|
|
2267
2671
|
throw new CliError(`Plano invalido: "${plan}". Valores aceitos: ${formatAllowedValues(ALLOWED_PLAN_VALUES)}. Docs: ${DOCS_CREATE_HTTP_URL}`);
|
|
2268
2672
|
}
|
|
2269
2673
|
|
|
@@ -2272,12 +2676,19 @@ function validateCreateInput({ plan, paymentMode, config }) {
|
|
|
2272
2676
|
throw new CliError(`Modo de pagamento invalido: "${paymentMode}". Valores aceitos: ${formatAllowedValues(ALLOWED_PAYMENT_MODE_VALUES)}. Docs: ${DOCS_PAYMENTS_URL}`);
|
|
2273
2677
|
}
|
|
2274
2678
|
|
|
2275
|
-
const nextTypeProject = normalizeTypeProject(config?.type_project);
|
|
2276
2679
|
if (!nextTypeProject || !ALLOWED_TYPE_PROJECT_VALUES.has(nextTypeProject)) {
|
|
2277
2680
|
throw new CliError(`type_project invalido: "${config?.type_project}". Valores aceitos: ${formatAllowedValues(ALLOWED_TYPE_PROJECT_VALUES)}. Docs: ${DOCS_CREATE_HTTP_URL}`);
|
|
2278
2681
|
}
|
|
2279
2682
|
|
|
2280
2683
|
const nextConfig = { ...config, type_project: nextTypeProject };
|
|
2684
|
+
if (nextTypeProject === 'valkey') {
|
|
2685
|
+
return {
|
|
2686
|
+
plan: nextPlan,
|
|
2687
|
+
paymentMode: nextPaymentMode,
|
|
2688
|
+
config: validateValkeyConfig(nextConfig, nextPlan, valkeyCatalog),
|
|
2689
|
+
};
|
|
2690
|
+
}
|
|
2691
|
+
|
|
2281
2692
|
if (nextTypeProject === 'http' && nextConfig.github) {
|
|
2282
2693
|
const nextRuntime = normalizeRuntime(nextConfig.github.runtime);
|
|
2283
2694
|
if (!nextRuntime || !ALLOWED_RUNTIME_VALUES.has(nextRuntime)) {
|
|
@@ -2344,6 +2755,10 @@ function printWizardSummary(payload) {
|
|
|
2344
2755
|
lines.push(` banco: versao ${masked.config.version} | instancias ${masked.config.instances} | storage ${masked.config.storage.capacity}Gi`);
|
|
2345
2756
|
}
|
|
2346
2757
|
|
|
2758
|
+
if (masked.config.type_project === 'valkey') {
|
|
2759
|
+
lines.push(` Valkey: perfil ${masked.config.profile} | versao ${masked.config.version}${masked.config.storage ? ` | storage ${masked.config.storage.capacity}Gi persistente` : ' | sem storage persistente'}`);
|
|
2760
|
+
}
|
|
2761
|
+
|
|
2347
2762
|
process.stdout.write(`${lines.join('\n')}\n`);
|
|
2348
2763
|
}
|
|
2349
2764
|
|
|
@@ -2735,6 +3150,65 @@ async function buildMariadbConfig() {
|
|
|
2735
3150
|
};
|
|
2736
3151
|
}
|
|
2737
3152
|
|
|
3153
|
+
async function buildValkeyConfig({ profile, plan, catalog }) {
|
|
3154
|
+
const planDefinition = catalog.plans.find((entry) => entry.id === plan && entry.profile === profile);
|
|
3155
|
+
const profileDefinition = catalog.profiles.find((entry) => entry.id === profile);
|
|
3156
|
+
if (!planDefinition || !profileDefinition) {
|
|
3157
|
+
throw new CliError('O catalogo Valkey nao possui o plano ou perfil selecionado. Tente novamente.');
|
|
3158
|
+
}
|
|
3159
|
+
|
|
3160
|
+
let storage;
|
|
3161
|
+
if (profileDefinition.persistence) {
|
|
3162
|
+
storage = {
|
|
3163
|
+
persistent: true,
|
|
3164
|
+
capacity: await promptWizardNumber({
|
|
3165
|
+
label: `Capacidade de armazenamento persistente (Gi, inclui ${planDefinition.included_storage_gb ?? 0} Gi)`,
|
|
3166
|
+
docs: DOCS_CREATE_VALKEY_URL,
|
|
3167
|
+
examples: [String(Math.max(1, planDefinition.included_storage_gb ?? 1)), '10', '20'],
|
|
3168
|
+
min: 1,
|
|
3169
|
+
max: 250,
|
|
3170
|
+
}),
|
|
3171
|
+
};
|
|
3172
|
+
}
|
|
3173
|
+
|
|
3174
|
+
const whitelist = await promptWizardPairs({
|
|
3175
|
+
introLabel: 'Deseja configurar acesso de rede para este projeto?',
|
|
3176
|
+
docs: DOCS_CREATE_VALKEY_URL,
|
|
3177
|
+
examples: ['sim', 'nao'],
|
|
3178
|
+
stateKey: 'valkeyNetworkAccess',
|
|
3179
|
+
itemLabels: [
|
|
3180
|
+
{ key: 'cidr', label: 'CIDR permitido', required: true, examples: ['203.0.113.0/24'], docs: DOCS_CREATE_VALKEY_URL },
|
|
3181
|
+
{ key: 'description', label: 'Descricao do acesso', required: true, examples: ['Aplicacao'], docs: DOCS_CREATE_VALKEY_URL },
|
|
3182
|
+
],
|
|
3183
|
+
});
|
|
3184
|
+
|
|
3185
|
+
let blacklist = [];
|
|
3186
|
+
if (whitelist.length > 0) {
|
|
3187
|
+
blacklist = await promptWizardPairs({
|
|
3188
|
+
introLabel: 'Deseja adicionar bloqueios de rede?',
|
|
3189
|
+
docs: DOCS_CREATE_VALKEY_URL,
|
|
3190
|
+
examples: ['sim', 'nao'],
|
|
3191
|
+
itemLabels: [
|
|
3192
|
+
{ key: 'cidr', label: 'CIDR bloqueado', required: true, examples: ['198.51.100.0/24'], docs: DOCS_CREATE_VALKEY_URL },
|
|
3193
|
+
{ key: 'description', label: 'Descricao do bloqueio', required: true, examples: ['Bloqueio'], docs: DOCS_CREATE_VALKEY_URL },
|
|
3194
|
+
],
|
|
3195
|
+
});
|
|
3196
|
+
}
|
|
3197
|
+
|
|
3198
|
+
return {
|
|
3199
|
+
type_project: 'valkey',
|
|
3200
|
+
profile,
|
|
3201
|
+
version: catalog.version,
|
|
3202
|
+
...(storage ? { storage } : {}),
|
|
3203
|
+
...(whitelist.length > 0 ? {
|
|
3204
|
+
network_access: {
|
|
3205
|
+
ingress_white_list: whitelist,
|
|
3206
|
+
ingress_black_list: blacklist,
|
|
3207
|
+
},
|
|
3208
|
+
} : {}),
|
|
3209
|
+
};
|
|
3210
|
+
}
|
|
3211
|
+
|
|
2738
3212
|
async function runProjectCreateWizard(session, flags) {
|
|
2739
3213
|
const catalogs = await fetchWizardCatalogs(session, flags);
|
|
2740
3214
|
const ui = createWizardUi();
|
|
@@ -2765,16 +3239,34 @@ async function runProjectCreateWizard(session, flags) {
|
|
|
2765
3239
|
options: WIZARD_TYPE_PROJECT_OPTIONS.map((value) => ({ value })),
|
|
2766
3240
|
});
|
|
2767
3241
|
ui.state.typeProject = typeProject;
|
|
3242
|
+
const valkeyCatalog = typeProject === 'valkey'
|
|
3243
|
+
? requireValkeyCatalog(unwrapData(await request(session, flags, 'GET', '/managed-services/catalog')))
|
|
3244
|
+
: undefined;
|
|
3245
|
+
const profile = typeProject === 'valkey'
|
|
3246
|
+
? await promptWizardSelect({
|
|
3247
|
+
label: 'Perfil Valkey',
|
|
3248
|
+
docs: DOCS_CREATE_VALKEY_URL,
|
|
3249
|
+
examples: WIZARD_VALKEY_PROFILE_OPTIONS,
|
|
3250
|
+
options: WIZARD_VALKEY_PROFILE_OPTIONS.map((value) => ({ value })),
|
|
3251
|
+
})
|
|
3252
|
+
: undefined;
|
|
3253
|
+
ui.state.valkeyProfile = profile;
|
|
2768
3254
|
const planOptions = typeProject === 'http'
|
|
2769
3255
|
? sortWizardOptions(catalogs.httpPlans
|
|
2770
3256
|
.map((plan) => plan?.plan)
|
|
2771
3257
|
.filter((plan) => WIZARD_HTTP_PLAN_OPTIONS.includes(plan) && Number(catalogs.availableInstances?.[plan] || 0) > 0), WIZARD_HTTP_PLAN_OPTIONS)
|
|
3258
|
+
: typeProject === 'valkey'
|
|
3259
|
+
? valkeyCatalog.plans
|
|
3260
|
+
.filter((planDefinition) => planDefinition.profile === profile)
|
|
3261
|
+
.map((planDefinition) => planDefinition.id)
|
|
2772
3262
|
: sortWizardOptions(catalogs.databasePlans
|
|
2773
3263
|
.map((plan) => plan?.plan)
|
|
2774
3264
|
.filter((plan) => WIZARD_DATABASE_PLAN_OPTIONS.includes(plan) && (typeProject !== 'mariadb' || plan !== 'db-free')), WIZARD_DATABASE_PLAN_OPTIONS);
|
|
2775
3265
|
const plan = await promptWizardSelect({
|
|
2776
3266
|
label: 'Plano',
|
|
2777
|
-
docs: typeProject === 'http'
|
|
3267
|
+
docs: typeProject === 'http'
|
|
3268
|
+
? DOCS_CREATE_HTTP_URL
|
|
3269
|
+
: typeProject === 'valkey' ? DOCS_CREATE_VALKEY_URL : DOCS_DATABASE_CONFIGURATION_URL,
|
|
2778
3270
|
examples: [planOptions[0], planOptions.at(-1)],
|
|
2779
3271
|
options: planOptions.map((value) => ({ value })),
|
|
2780
3272
|
});
|
|
@@ -2800,6 +3292,8 @@ async function runProjectCreateWizard(session, flags) {
|
|
|
2800
3292
|
});
|
|
2801
3293
|
} else if (typeProject === 'postgresql') {
|
|
2802
3294
|
config = await buildPostgresqlConfig({ plan, databasePlans: catalogs.databasePlans });
|
|
3295
|
+
} else if (typeProject === 'valkey') {
|
|
3296
|
+
config = await buildValkeyConfig({ profile, plan, catalog: valkeyCatalog });
|
|
2803
3297
|
} else {
|
|
2804
3298
|
config = await buildMariadbConfig();
|
|
2805
3299
|
}
|
|
@@ -2815,7 +3309,9 @@ async function runProjectCreateWizard(session, flags) {
|
|
|
2815
3309
|
printWizardSummary(payload);
|
|
2816
3310
|
const confirmed = await promptWizardBoolean({
|
|
2817
3311
|
label: 'Confirmar criacao do projeto',
|
|
2818
|
-
docs: typeProject === 'http'
|
|
3312
|
+
docs: typeProject === 'http'
|
|
3313
|
+
? DOCS_CREATE_HTTP_URL
|
|
3314
|
+
: typeProject === 'valkey' ? DOCS_CREATE_VALKEY_URL : DOCS_DATABASE_CONFIGURATION_URL,
|
|
2819
3315
|
examples: ['sim', 'nao'],
|
|
2820
3316
|
});
|
|
2821
3317
|
if (!confirmed) return null;
|
|
@@ -2836,10 +3332,18 @@ async function handleProjectCreate(session, flags) {
|
|
|
2836
3332
|
const plan = wizardPayload?.plan || flags.plan || await prompt('Plano');
|
|
2837
3333
|
const paymentMode = wizardPayload?.payment_mode || flags.paymentMode || await prompt('Modo de pagamento');
|
|
2838
3334
|
const config = wizardPayload?.config || await parseConfig(flags.config || await prompt('Config JSON ou @arquivo'));
|
|
2839
|
-
const
|
|
3335
|
+
const typeProject = normalizeTypeProject(config?.type_project);
|
|
3336
|
+
const valkeyCatalog = typeProject === 'valkey'
|
|
3337
|
+
? requireValkeyCatalog(unwrapData(await request(session, flags, 'GET', '/managed-services/catalog')))
|
|
3338
|
+
: undefined;
|
|
3339
|
+
if (flags.idempotencyKey !== undefined && !/^[A-Za-z0-9._-]{16,200}$/.test(String(flags.idempotencyKey))) {
|
|
3340
|
+
throw new CliError('--idempotency-key deve ter entre 16 e 200 caracteres: letras, numeros, ponto, underscore ou hifen.');
|
|
3341
|
+
}
|
|
3342
|
+
const validated = validateCreateInput({ plan, paymentMode, config, valkeyCatalog });
|
|
2840
3343
|
|
|
2841
3344
|
const payload = await request(session, flags, 'POST', '/project', {
|
|
2842
3345
|
orgId,
|
|
3346
|
+
headers: flags.idempotencyKey === undefined ? {} : { 'Idempotency-Key': String(flags.idempotencyKey) },
|
|
2843
3347
|
body: {
|
|
2844
3348
|
name,
|
|
2845
3349
|
...(description ? { description: String(description) } : {}),
|
|
@@ -2859,6 +3363,21 @@ async function handleProjectCreate(session, flags) {
|
|
|
2859
3363
|
...(project.domain ? [{ field: 'Dominio', value: formatPublicUrl(project.domain) }] : []),
|
|
2860
3364
|
...(project.api_key ? [{ field: 'API key', value: project.api_key }] : []),
|
|
2861
3365
|
];
|
|
3366
|
+
if (validated.config.type_project === 'valkey') {
|
|
3367
|
+
const service = project.managed_service || {};
|
|
3368
|
+
rows.push(
|
|
3369
|
+
{ field: 'Perfil', value: service.profile || validated.config.profile },
|
|
3370
|
+
{ field: 'Host', value: service.host || '-' },
|
|
3371
|
+
{ field: 'Porta', value: service.port ?? '-' },
|
|
3372
|
+
{ field: 'TLS', value: service.tls === true ? 'sim' : 'nao' },
|
|
3373
|
+
);
|
|
3374
|
+
if (service.connection_string) {
|
|
3375
|
+
rows.push({ field: 'String de conexao', value: service.connection_string });
|
|
3376
|
+
process.stdout.write('Salve a string de conexao em um local seguro; ela pode nao ser exibida novamente.\n');
|
|
3377
|
+
} else {
|
|
3378
|
+
rows.push({ field: 'Credencial', value: 'nao retornada nesta resposta; use valkey credentials rotate' });
|
|
3379
|
+
}
|
|
3380
|
+
}
|
|
2862
3381
|
process.stdout.write('\n');
|
|
2863
3382
|
printTable(rows, [
|
|
2864
3383
|
{ label: 'Campo', value: (row) => row.field },
|
|
@@ -2908,6 +3427,16 @@ async function handleProjectLogs(session, flags) {
|
|
|
2908
3427
|
printLogs(logs, flags);
|
|
2909
3428
|
}
|
|
2910
3429
|
|
|
3430
|
+
async function handleProjectMetricsCapabilities(session, flags) {
|
|
3431
|
+
const projectId = requireProjectId(flags, 'project metrics capabilities');
|
|
3432
|
+
if (!projectId) return;
|
|
3433
|
+
const orgId = await resolveOrgId(session, flags);
|
|
3434
|
+
const capabilities = unwrapData(await request(session, flags, 'GET', `/project/${projectId}/metrics/capabilities`, { orgId }));
|
|
3435
|
+
|
|
3436
|
+
if (flags.json) return printJson(capabilities);
|
|
3437
|
+
printMetricsCapabilities(capabilities);
|
|
3438
|
+
}
|
|
3439
|
+
|
|
2911
3440
|
async function handleProjectMetrics(session, flags) {
|
|
2912
3441
|
const projectId = requireProjectId(flags, 'project metrics');
|
|
2913
3442
|
if (!projectId) return
|
|
@@ -2916,6 +3445,9 @@ async function handleProjectMetrics(session, flags) {
|
|
|
2916
3445
|
const metrics = unwrapData(await request(session, flags, 'GET', `/project/${projectId}/metrics${query}`, { orgId }));
|
|
2917
3446
|
|
|
2918
3447
|
if (flags.json) return printJson(metrics);
|
|
3448
|
+
if (metrics?.type === 'valkey') {
|
|
3449
|
+
return printValkeyMetrics(metrics);
|
|
3450
|
+
}
|
|
2919
3451
|
if (Array.isArray(metrics)) {
|
|
2920
3452
|
return printTable(metrics, [
|
|
2921
3453
|
{ label: 'Instancia', value: (metric) => metric.instance || '-' },
|
|
@@ -3330,6 +3862,131 @@ async function handleProjectBillingUsage(session, flags) {
|
|
|
3330
3862
|
printBillingUsage(data);
|
|
3331
3863
|
}
|
|
3332
3864
|
|
|
3865
|
+
function printValkeyStatus(data) {
|
|
3866
|
+
printTable([data], [
|
|
3867
|
+
{ label: 'Status', value: (service) => service.status || '-' },
|
|
3868
|
+
{ label: 'Perfil', value: (service) => service.profile || '-' },
|
|
3869
|
+
{ label: 'Versao', value: (service) => service.version || '-' },
|
|
3870
|
+
{ label: 'Persistente', value: (service) => service.persistence === true ? 'sim' : 'nao' },
|
|
3871
|
+
{ label: 'Instancias', value: (service) => service.instances ?? '-' },
|
|
3872
|
+
]);
|
|
3873
|
+
}
|
|
3874
|
+
|
|
3875
|
+
function printValkeyConnection(data) {
|
|
3876
|
+
printTable([data], [
|
|
3877
|
+
{ label: 'Host', value: (service) => service.host || '-' },
|
|
3878
|
+
{ label: 'Porta', value: (service) => service.port ?? '-' },
|
|
3879
|
+
{ label: 'Usuario', value: (service) => service.username || '-' },
|
|
3880
|
+
{ label: 'TLS', value: (service) => service.tls === true ? 'sim' : 'nao' },
|
|
3881
|
+
{ label: 'Conexao', value: (service) => service.connection_string || '-' },
|
|
3882
|
+
]);
|
|
3883
|
+
}
|
|
3884
|
+
|
|
3885
|
+
function printValkeyRotation(data, { accepted = false } = {}) {
|
|
3886
|
+
const rows = [
|
|
3887
|
+
{ field: accepted ? 'Operacao aceita' : 'Operacao', value: data.operation_id || '-' },
|
|
3888
|
+
{ field: 'Estado', value: data.state || '-' },
|
|
3889
|
+
];
|
|
3890
|
+
if (data.connection_string) {
|
|
3891
|
+
rows.push({ field: 'String de conexao', value: data.connection_string });
|
|
3892
|
+
process.stdout.write('Salve a nova string de conexao em um local seguro; ela pode nao ser exibida novamente.\n');
|
|
3893
|
+
}
|
|
3894
|
+
printTable(rows, [
|
|
3895
|
+
{ label: 'Campo', value: (row) => row.field },
|
|
3896
|
+
{ label: 'Valor', value: (row) => row.value },
|
|
3897
|
+
]);
|
|
3898
|
+
}
|
|
3899
|
+
|
|
3900
|
+
async function waitForValkeyRotation(session, flags, projectId, operationId, orgId) {
|
|
3901
|
+
const intervalMs = parseSecondsOption(flags.interval, '--interval', { defaultValue: 2, min: 0.1 }) * 1000;
|
|
3902
|
+
const timeoutMs = parseSecondsOption(flags.timeout, '--timeout', { defaultValue: 900, min: 1 }) * 1000;
|
|
3903
|
+
const startedAt = Date.now();
|
|
3904
|
+
let lastState = 'accepted';
|
|
3905
|
+
|
|
3906
|
+
while (true) {
|
|
3907
|
+
const data = unwrapData(await request(
|
|
3908
|
+
session,
|
|
3909
|
+
flags,
|
|
3910
|
+
'GET',
|
|
3911
|
+
`/managed-services/${projectId}/credential-rotations/${operationId}`,
|
|
3912
|
+
{ orgId },
|
|
3913
|
+
));
|
|
3914
|
+
lastState = data?.state || lastState;
|
|
3915
|
+
if (lastState === 'completed') return data;
|
|
3916
|
+
if (['failed', 'cancelled', 'canceled'].includes(lastState)) {
|
|
3917
|
+
throw new CliError(`A renovacao de credencial terminou com estado ${lastState}. Operacao: ${operationId}.`);
|
|
3918
|
+
}
|
|
3919
|
+
if (Date.now() - startedAt >= timeoutMs) {
|
|
3920
|
+
throw new CliError(`Timeout aguardando a renovacao ${operationId}. Ultimo estado: ${lastState}.`);
|
|
3921
|
+
}
|
|
3922
|
+
await new Promise((resolvePromise) => setTimeout(resolvePromise, intervalMs));
|
|
3923
|
+
}
|
|
3924
|
+
}
|
|
3925
|
+
|
|
3926
|
+
async function handleValkeyStatus(session, flags) {
|
|
3927
|
+
const projectId = requireProjectId(flags, 'valkey status');
|
|
3928
|
+
if (!projectId) return;
|
|
3929
|
+
const orgId = await resolveOrgId(session, flags);
|
|
3930
|
+
const data = unwrapData(await request(session, flags, 'GET', `/managed-services/${projectId}/status`, { orgId }));
|
|
3931
|
+
if (flags.json) return printJson(data);
|
|
3932
|
+
printValkeyStatus(data);
|
|
3933
|
+
}
|
|
3934
|
+
|
|
3935
|
+
async function handleValkeyConnection(session, flags) {
|
|
3936
|
+
const projectId = requireProjectId(flags, 'valkey connection');
|
|
3937
|
+
if (!projectId) return;
|
|
3938
|
+
const orgId = await resolveOrgId(session, flags);
|
|
3939
|
+
const data = unwrapData(await request(session, flags, 'GET', `/managed-services/${projectId}/connection`, { orgId }));
|
|
3940
|
+
if (flags.json) return printJson(data);
|
|
3941
|
+
printValkeyConnection(data);
|
|
3942
|
+
}
|
|
3943
|
+
|
|
3944
|
+
async function handleValkeyCredentialStatus(session, flags) {
|
|
3945
|
+
const projectId = requireProjectId(flags, 'valkey credentials status');
|
|
3946
|
+
if (!projectId) return;
|
|
3947
|
+
if (!flags.operation) {
|
|
3948
|
+
printCommandHelpAndFail('valkey credentials status');
|
|
3949
|
+
return;
|
|
3950
|
+
}
|
|
3951
|
+
const orgId = await resolveOrgId(session, flags);
|
|
3952
|
+
const data = unwrapData(await request(
|
|
3953
|
+
session,
|
|
3954
|
+
flags,
|
|
3955
|
+
'GET',
|
|
3956
|
+
`/managed-services/${projectId}/credential-rotations/${flags.operation}`,
|
|
3957
|
+
{ orgId },
|
|
3958
|
+
));
|
|
3959
|
+
if (flags.json) return printJson(data);
|
|
3960
|
+
printValkeyRotation(data);
|
|
3961
|
+
}
|
|
3962
|
+
|
|
3963
|
+
async function handleValkeyCredentialRotate(session, flags) {
|
|
3964
|
+
const projectId = requireProjectId(flags, 'valkey credentials rotate');
|
|
3965
|
+
if (!projectId) return;
|
|
3966
|
+
const idempotencyKey = String(flags.idempotencyKey || randomUUID());
|
|
3967
|
+
if (!/^[A-Za-z0-9._-]{16,200}$/.test(idempotencyKey)) {
|
|
3968
|
+
throw new CliError('--idempotency-key deve ter entre 16 e 200 caracteres: letras, numeros, ponto, underscore ou hifen.');
|
|
3969
|
+
}
|
|
3970
|
+
const orgId = await resolveOrgId(session, flags);
|
|
3971
|
+
const acceptedPayload = await request(session, flags, 'PATCH', `/managed-services/${projectId}/credentials`, {
|
|
3972
|
+
orgId,
|
|
3973
|
+
body: {},
|
|
3974
|
+
headers: { 'Idempotency-Key': idempotencyKey },
|
|
3975
|
+
});
|
|
3976
|
+
const accepted = unwrapData(acceptedPayload);
|
|
3977
|
+
if (!accepted?.operation_id) throw new CliError('A API nao retornou o identificador da renovacao de credencial.');
|
|
3978
|
+
|
|
3979
|
+
if (flags.wait) {
|
|
3980
|
+
const completed = await waitForValkeyRotation(session, flags, projectId, accepted.operation_id, orgId);
|
|
3981
|
+
if (flags.json) return printJson(completed);
|
|
3982
|
+
printValkeyRotation(completed);
|
|
3983
|
+
return;
|
|
3984
|
+
}
|
|
3985
|
+
|
|
3986
|
+
if (flags.json) return printJson(acceptedPayload);
|
|
3987
|
+
printValkeyRotation(accepted, { accepted: true });
|
|
3988
|
+
}
|
|
3989
|
+
|
|
3333
3990
|
function buildQuery(flags, allowedKeys) {
|
|
3334
3991
|
const params = new URLSearchParams();
|
|
3335
3992
|
for (const key of allowedKeys) {
|
|
@@ -3540,7 +4197,7 @@ async function main() {
|
|
|
3540
4197
|
return;
|
|
3541
4198
|
}
|
|
3542
4199
|
|
|
3543
|
-
if (command === 'auth' && subcommand === 'login') return handleLogin(session, flags);
|
|
4200
|
+
if (command === 'auth' && subcommand === 'login') return await handleLogin(session, flags);
|
|
3544
4201
|
if (command === 'auth' && subcommand === 'api-key') return handleApiKeyLogin(session, flags);
|
|
3545
4202
|
if (command === 'auth' && subcommand === 'logout') return handleLogout(session, flags);
|
|
3546
4203
|
if (command === 'profile' && subcommand === 'list') return handleProfileList(session, flags);
|
|
@@ -3549,16 +4206,25 @@ async function main() {
|
|
|
3549
4206
|
if (command === 'profile' && subcommand === 'edit') return handleProfileEdit(session, flags, positional);
|
|
3550
4207
|
if (command === 'profile' && subcommand === 'use') return handleProfileUse(session, flags, positional);
|
|
3551
4208
|
if (command === 'profile' && subcommand === 'remove') return handleProfileRemove(session, flags, positional);
|
|
3552
|
-
if (command === 'login') return handleLogin(session, flags);
|
|
4209
|
+
if (command === 'login') return await handleLogin(session, flags);
|
|
3553
4210
|
if (command === 'logout') return handleLogout(session, flags);
|
|
3554
4211
|
if (command === 'plans') return handlePlans(session, flags);
|
|
3555
4212
|
if (command === 'create' && subcommand === 'project') return handleProjectCreate(session, flags);
|
|
3556
4213
|
if (command === 'orgs') return handleOrgs(session, flags);
|
|
3557
4214
|
if (command === 'org' && subcommand === 'set') return handleOrgSet(session, flags);
|
|
3558
4215
|
if (command === 'projects') return handleProjects(session, flags);
|
|
4216
|
+
if (command === 'valkey' && subcommand === 'credentials' && positional.length === 2) {
|
|
4217
|
+
process.stdout.write(commandHelp(['valkey', 'credentials']));
|
|
4218
|
+
return;
|
|
4219
|
+
}
|
|
4220
|
+
if (command === 'valkey' && subcommand === 'status') return handleValkeyStatus(session, flags);
|
|
4221
|
+
if (command === 'valkey' && subcommand === 'connection') return handleValkeyConnection(session, flags);
|
|
4222
|
+
if (command === 'valkey' && subcommand === 'credentials' && positional[2] === 'rotate') return handleValkeyCredentialRotate(session, flags);
|
|
4223
|
+
if (command === 'valkey' && subcommand === 'credentials' && positional[2] === 'status') return handleValkeyCredentialStatus(session, flags);
|
|
3559
4224
|
if (command === 'project' && subcommand === 'info') return handleProjectInfo(session, flags);
|
|
3560
4225
|
if (command === 'project' && subcommand === 'url') return handleProjectUrl(session, flags);
|
|
3561
4226
|
if (command === 'project' && subcommand === 'logs') return handleProjectLogs(session, flags);
|
|
4227
|
+
if (command === 'project' && subcommand === 'metrics' && positional[2] === 'capabilities') return handleProjectMetricsCapabilities(session, flags);
|
|
3562
4228
|
if (command === 'project' && subcommand === 'metrics') return handleProjectMetrics(session, flags);
|
|
3563
4229
|
if (command === 'project' && subcommand === 'network') return handleProjectNetwork(session, flags);
|
|
3564
4230
|
if (command === 'project' && subcommand === 'image' && positional[2] === 'set') return handleProjectImageSet(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
|
+
}
|