@zenifra/cli 0.2.3 → 0.2.4
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 +30 -1
- package/bin/zenifra.mjs +637 -41
- package/examples/http-autoscaling-project.json +30 -0
- package/examples/http-github-project.json +1 -0
- package/examples/http-project.json +1 -0
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -40,6 +40,7 @@ zenifra auth login --profile staging --code 123456
|
|
|
40
40
|
zenifra auth api-key --key znf_sua_chave
|
|
41
41
|
zenifra auth api-key --profile prod --key znf_sua_chave
|
|
42
42
|
zenifra auth logout
|
|
43
|
+
zenifra auth logout --revoke
|
|
43
44
|
zenifra profile list
|
|
44
45
|
zenifra profile show
|
|
45
46
|
zenifra profile add --name staging --description Homologacao --api-base https://api-stg.zenifra.com/v1 --mode api-key --key znf_sua_chave
|
|
@@ -54,6 +55,7 @@ zenifra plans --type storage --json
|
|
|
54
55
|
zenifra create project
|
|
55
56
|
zenifra create project --name <name> --plan free --payment-mode hourly --config @examples/http-project.json
|
|
56
57
|
zenifra create project --name <name> --plan basic --payment-mode hourly --config @examples/http-github-project.json
|
|
58
|
+
zenifra create project --name <name> --plan premium --payment-mode hourly --config @examples/http-autoscaling-project.json
|
|
57
59
|
zenifra create project --name <name> --plan db-basic --payment-mode monthly --config @examples/postgresql-project.json
|
|
58
60
|
zenifra create project --name <name> --plan db-basic --payment-mode monthly --config @examples/mariadb-project.json
|
|
59
61
|
zenifra projects --type http --page 1 --limit 15
|
|
@@ -67,6 +69,11 @@ zenifra project envs --project <project-id>
|
|
|
67
69
|
zenifra project env add --project <project-id> --name NODE_ENV --value production
|
|
68
70
|
zenifra project env update --project <project-id> --name NODE_ENV --value staging
|
|
69
71
|
zenifra project env remove --project <project-id> --name NODE_ENV
|
|
72
|
+
zenifra project autoscaling --project <project-id>
|
|
73
|
+
zenifra project autoscaling set --project <project-id> --min 2 --max 8 --cpu 70 --memory 80
|
|
74
|
+
zenifra project autoscaling disable --project <project-id>
|
|
75
|
+
zenifra project autoscaling events --project <project-id> --direction scale_up --page 1 --limit 10
|
|
76
|
+
zenifra project billing usage --project <project-id> --from 2026-06-01T00:00:00Z --limit 20
|
|
70
77
|
zenifra project instances --project <project-id>
|
|
71
78
|
zenifra project instances set --project <project-id> --count 3
|
|
72
79
|
zenifra builds --project <project-id>
|
|
@@ -103,6 +110,7 @@ Se voce rodar apenas `zenifra deploy`, a CLI mostra a ajuda especifica do comand
|
|
|
103
110
|
- API padrao: `https://api.zenifra.com/v1`
|
|
104
111
|
- Override de API: `ZENIFRA_API_URL=https://api-stg.zenifra.com/v1`
|
|
105
112
|
- API key global: `ZENIFRA_API_KEY=znf_sua_chave`
|
|
113
|
+
- Timeout de cada request HTTP: `ZENIFRA_HTTP_TIMEOUT_MS=30000`
|
|
106
114
|
- Store local de perfis: `~/.config/zenifra-cli/profiles.json`
|
|
107
115
|
- Override do diretorio local: `ZENIFRA_CONFIG_DIR=/path/custom`
|
|
108
116
|
|
|
@@ -149,10 +157,12 @@ Regras de precedencia:
|
|
|
149
157
|
- `ZENIFRA_API_URL` sobrescreve a API base do perfil ativo apenas para a execucao atual
|
|
150
158
|
- `auth login` e `auth api-key` operam no perfil ativo por padrao
|
|
151
159
|
- `auth login --profile <name>` e `auth api-key --profile <name>` atualizam ou criam outro perfil e o tornam ativo
|
|
160
|
+
- `auth logout` remove somente a autenticacao local; `auth logout --revoke` tambem invalida as sessoes de login do usuario no servidor
|
|
161
|
+
- `auth logout --revoke` exige um perfil autenticado por login e nao revoga API keys
|
|
152
162
|
|
|
153
163
|
Migracao:
|
|
154
164
|
|
|
155
|
-
- se existir `session.json` legado e ainda nao existir `profiles.json`, a CLI migra automaticamente a sessao antiga para um perfil `default`
|
|
165
|
+
- se existir `session.json` legado e ainda nao existir `profiles.json`, a CLI migra automaticamente a sessao antiga para um perfil `default` e remove o arquivo legado
|
|
156
166
|
|
|
157
167
|
## Automacao com API key
|
|
158
168
|
|
|
@@ -180,6 +190,7 @@ Use os arquivos em `examples/` como base para `zenifra create project`:
|
|
|
180
190
|
|
|
181
191
|
- `examples/http-project.json`: projeto HTTP com imagem OCI publica
|
|
182
192
|
- `examples/http-github-project.json`: projeto HTTP com build via GitHub
|
|
193
|
+
- `examples/http-autoscaling-project.json`: projeto HTTP pago criado com auto-scaling
|
|
183
194
|
- `examples/postgresql-project.json`: projeto PostgreSQL
|
|
184
195
|
- `examples/mariadb-project.json`: projeto MariaDB
|
|
185
196
|
|
|
@@ -196,21 +207,39 @@ O wizard atual cobre:
|
|
|
196
207
|
- projetos `mariadb`
|
|
197
208
|
|
|
198
209
|
`zenifra create project` nao assume valores default para `--plan` e `--payment-mode`.
|
|
210
|
+
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.
|
|
199
211
|
Antes de escolher um plano com o usuario, compare os catalogos com `zenifra plans` para evitar suposicoes sobre custo.
|
|
200
212
|
|
|
201
213
|
Valores aceitos:
|
|
202
214
|
|
|
203
215
|
- `payment_mode`: `hourly`, `monthly`, `yearly`
|
|
204
216
|
- `type_project` no `config`: `http`, `postgresql`, `mariadb`
|
|
217
|
+
- `exposure` no `config` HTTP: `public`, `private`
|
|
205
218
|
- `plan`: `free`, `static`, `basic`, `premium`, `premium_plus`, `business`, `deep_learning_basic`, `deep_learning_premium`, `db-free`, `db-starter`, `db-basic`, `db-premium`, `db-enterprise`
|
|
206
219
|
- `config.github.runtime` (quando houver GitHub em projeto HTTP): `nodejs` ou `python`
|
|
220
|
+
- `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
|
|
207
221
|
|
|
208
222
|
Observacoes do wizard:
|
|
209
223
|
|
|
210
224
|
- conflitos entre documentacao e API sao validados pelo contrato real aceito pela API
|
|
225
|
+
- o wizard oferece auto-scaling apenas quando o plano selecionado informa essa disponibilidade
|
|
226
|
+
- na criacao com auto-scaling, `config.instances` e o minimo inicial e `config.autoscaling.max_instances` e o maximo
|
|
211
227
|
- em projetos de banco, o wizard nao pergunta `username`, `password` nem `database name`
|
|
212
228
|
- em projetos de banco, a CLI preenche apenas campos tecnicos minimos exigidos pela validacao atual da API
|
|
213
229
|
|
|
230
|
+
## Regressao manual de auto-scaling em staging
|
|
231
|
+
|
|
232
|
+
O teste de staging cria projetos, gera trafego, consulta consumo e remove somente os projetos criados pela propria execucao. Ele rejeita a API de producao e exige habilitacao explicita das mutacoes.
|
|
233
|
+
|
|
234
|
+
```bash
|
|
235
|
+
export ZENIFRA_API_URL_STG=https://api-stg.zenifra.com/v1
|
|
236
|
+
export ZENIFRA_API_KEY_STG=znf_sua_chave_de_staging
|
|
237
|
+
export ZENIFRA_STAGING_ALLOW_MUTATIONS=1
|
|
238
|
+
npm run test:staging:autoscaling
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
Use `ZENIFRA_STAGING_SKIP_HOURLY_WAIT=1` para pular a espera pelo fechamento da janela horaria. Quando a espera esta habilitada, o teste verifica moeda, totais de computacao e armazenamento, instancia-horas e armazenamento persistente faturavel. A execucao pode consumir capacidade e gerar custos no ambiente de staging; o resumo redigido fica em `artifacts/`, que nao e versionado.
|
|
242
|
+
|
|
214
243
|
## Publicacao no npm
|
|
215
244
|
|
|
216
245
|
Este pacote e publicado pelo GitHub Actions usando npm Trusted Publishing. O pacote precisa existir no npm antes de configurar Trusted Publishing.
|
package/bin/zenifra.mjs
CHANGED
|
@@ -21,6 +21,53 @@ const SESSION_FILE = join(SESSION_DIR, 'session.json');
|
|
|
21
21
|
const PROFILES_FILE = join(SESSION_DIR, 'profiles.json');
|
|
22
22
|
const PROFILE_STORE_VERSION = 1;
|
|
23
23
|
const DEFAULT_PROFILE_NAME = 'default';
|
|
24
|
+
const DEFAULT_HTTP_TIMEOUT_MS = 30_000;
|
|
25
|
+
const KNOWN_FLAG_NAMES = new Set([
|
|
26
|
+
'apiBase',
|
|
27
|
+
'branch',
|
|
28
|
+
'build',
|
|
29
|
+
'challengeToken',
|
|
30
|
+
'code',
|
|
31
|
+
'commitSha',
|
|
32
|
+
'config',
|
|
33
|
+
'count',
|
|
34
|
+
'cpu',
|
|
35
|
+
'cursor',
|
|
36
|
+
'description',
|
|
37
|
+
'direction',
|
|
38
|
+
'email',
|
|
39
|
+
'exposure',
|
|
40
|
+
'follow',
|
|
41
|
+
'from',
|
|
42
|
+
'help',
|
|
43
|
+
'image',
|
|
44
|
+
'instance',
|
|
45
|
+
'interval',
|
|
46
|
+
'json',
|
|
47
|
+
'key',
|
|
48
|
+
'limit',
|
|
49
|
+
'max',
|
|
50
|
+
'memory',
|
|
51
|
+
'min',
|
|
52
|
+
'mode',
|
|
53
|
+
'name',
|
|
54
|
+
'org',
|
|
55
|
+
'page',
|
|
56
|
+
'password',
|
|
57
|
+
'paymentMode',
|
|
58
|
+
'plan',
|
|
59
|
+
'profile',
|
|
60
|
+
'project',
|
|
61
|
+
'revoke',
|
|
62
|
+
'showValues',
|
|
63
|
+
'status',
|
|
64
|
+
'timeout',
|
|
65
|
+
'to',
|
|
66
|
+
'totp',
|
|
67
|
+
'type',
|
|
68
|
+
'value',
|
|
69
|
+
'view',
|
|
70
|
+
]);
|
|
24
71
|
const ALLOWED_PLAN_VALUES = new Set([
|
|
25
72
|
'free',
|
|
26
73
|
'static',
|
|
@@ -39,6 +86,7 @@ const ALLOWED_PLAN_VALUES = new Set([
|
|
|
39
86
|
const ALLOWED_PAYMENT_MODE_VALUES = new Set(['hourly', 'monthly', 'yearly']);
|
|
40
87
|
const ALLOWED_TYPE_PROJECT_VALUES = new Set(['http', 'postgresql', 'mariadb']);
|
|
41
88
|
const ALLOWED_RUNTIME_VALUES = new Set(['nodejs', 'python']);
|
|
89
|
+
const ALLOWED_EXPOSURE_VALUES = new Set(['public', 'private']);
|
|
42
90
|
const GITHUB_RUNTIME_VERSIONS = {
|
|
43
91
|
nodejs: ['24', '22', '20'],
|
|
44
92
|
python: ['3.13', '3.12', '3.11'],
|
|
@@ -68,6 +116,7 @@ const WIZARD_DATABASE_PLAN_OPTIONS = [
|
|
|
68
116
|
];
|
|
69
117
|
const WIZARD_PAYMENT_MODE_OPTIONS = ['hourly', 'monthly', 'yearly'];
|
|
70
118
|
const WIZARD_TYPE_PROJECT_OPTIONS = ['http', 'postgresql', 'mariadb'];
|
|
119
|
+
const WIZARD_HTTP_EXPOSURE_OPTIONS = ['public', 'private'];
|
|
71
120
|
let promptInterface;
|
|
72
121
|
let pipedInputPromise;
|
|
73
122
|
let pipedInputIndex = 0;
|
|
@@ -95,7 +144,7 @@ Usage:
|
|
|
95
144
|
zenifra help <command>
|
|
96
145
|
zenifra auth login [--profile <name>] [--api-base <url>] [--code <code>]
|
|
97
146
|
zenifra auth api-key --key <znf_key> [--profile <name>] [--api-base <url>]
|
|
98
|
-
zenifra auth logout [--profile <name>]
|
|
147
|
+
zenifra auth logout [--profile <name>] [--revoke]
|
|
99
148
|
zenifra profile list [--json]
|
|
100
149
|
zenifra profile show [<name>] [--json]
|
|
101
150
|
zenifra profile add [--name <name>] [--description <text>] [--api-base <url>] [--mode <api-key|login>] [--key <znf_key>] [--json]
|
|
@@ -114,10 +163,16 @@ Usage:
|
|
|
114
163
|
zenifra project metrics --project <id> [--instance <id>] [--json]
|
|
115
164
|
zenifra project network --project <id> [--view <summary|status-codes|routes|user-agents|request-events|source-ips>] [--json]
|
|
116
165
|
zenifra project image set --project <id> --image <image> [--json]
|
|
166
|
+
zenifra project exposure set --project <id> --exposure <public|private> [--json]
|
|
117
167
|
zenifra project envs --project <id> [--json] [--show-values]
|
|
118
168
|
zenifra project env add --project <id> --name <name> --value <value> [--json]
|
|
119
169
|
zenifra project env update --project <id> --name <name> --value <value> [--json]
|
|
120
170
|
zenifra project env remove --project <id> --name <name> [--json]
|
|
171
|
+
zenifra project autoscaling --project <id> [--json]
|
|
172
|
+
zenifra project autoscaling set --project <id> --min <n> --max <n> [--cpu <percent>] [--memory <percent>] [--json]
|
|
173
|
+
zenifra project autoscaling disable --project <id> [--json]
|
|
174
|
+
zenifra project autoscaling events --project <id> [--direction <scale_up|scale_down>] [--from <iso>] [--to <iso>] [--page <n>] [--limit <n>] [--json]
|
|
175
|
+
zenifra project billing usage --project <id> [--from <iso>] [--to <iso>] [--page <n>] [--limit <n>] [--json]
|
|
121
176
|
zenifra project instances --project <id> [--json]
|
|
122
177
|
zenifra project instances set --project <id> --count <n> [--json]
|
|
123
178
|
zenifra builds --project <id> [--page <n>] [--limit <n>] [--branch <name>] [--status <status>] [--json]
|
|
@@ -138,7 +193,7 @@ Run "zenifra help <command>" or "zenifra <command> --help" for command-specific
|
|
|
138
193
|
const HELP_SPECS = [
|
|
139
194
|
{
|
|
140
195
|
command: 'auth',
|
|
141
|
-
usage: 'zenifra auth\n zenifra auth login [--profile <name>] [--api-base <url>] [--code <code>]\n zenifra auth api-key --key <znf_key> [--profile <name>] [--api-base <url>]\n zenifra auth logout [--profile <name>]',
|
|
196
|
+
usage: 'zenifra auth\n zenifra auth login [--profile <name>] [--api-base <url>] [--code <code>]\n zenifra auth api-key --key <znf_key> [--profile <name>] [--api-base <url>]\n zenifra auth logout [--profile <name>] [--revoke]',
|
|
142
197
|
description: 'Gerencia a autenticacao dos perfis locais da CLI.',
|
|
143
198
|
examples: ['zenifra auth', 'zenifra auth login --profile staging', 'zenifra auth api-key --profile prod --key znf_0123456789abcdef01234567_abcd...'],
|
|
144
199
|
output: 'Zenifra CLI - auth',
|
|
@@ -168,11 +223,12 @@ const HELP_SPECS = [
|
|
|
168
223
|
},
|
|
169
224
|
{
|
|
170
225
|
command: 'auth logout',
|
|
171
|
-
usage: 'zenifra auth logout [--profile <name>]',
|
|
226
|
+
usage: 'zenifra auth logout [--profile <name>] [--revoke]',
|
|
172
227
|
description: 'Remove a autenticacao do perfil ativo ou do perfil informado.',
|
|
173
|
-
flags: ['--profile <name> Limpa outro perfil sem trocar o perfil ativo.'],
|
|
174
|
-
examples: ['zenifra auth logout', 'zenifra auth logout --profile staging'],
|
|
228
|
+
flags: ['--profile <name> Limpa outro perfil sem trocar o perfil ativo.', '--revoke Revoga as sessoes de usuario no servidor antes de limpar o perfil.'],
|
|
229
|
+
examples: ['zenifra auth logout', 'zenifra auth logout --profile staging', 'zenifra auth logout --revoke'],
|
|
175
230
|
output: 'Autenticacao removida do perfil active.',
|
|
231
|
+
notes: ['Sem --revoke, remove somente a credencial local. A revogacao invalida as sessoes de login do usuario e nao se aplica a API keys.'],
|
|
176
232
|
},
|
|
177
233
|
{
|
|
178
234
|
command: 'profile',
|
|
@@ -291,11 +347,11 @@ const HELP_SPECS = [
|
|
|
291
347
|
},
|
|
292
348
|
{
|
|
293
349
|
command: 'project',
|
|
294
|
-
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 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 instances --project <id> [--json]\n zenifra project instances set --project <id> --count <n> [--json]',
|
|
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]',
|
|
295
351
|
description: 'Agrupa comandos operacionais e de introspecao sobre um projeto especifico.',
|
|
296
352
|
examples: ['zenifra project', 'zenifra project info --project proj_1', 'zenifra project env add --project proj_1 --name NODE_ENV --value production'],
|
|
297
353
|
output: 'Zenifra CLI - project',
|
|
298
|
-
notes: ['Use "zenifra help project <subcomando>" para detalhes de info, url, logs, metrics, network, image, envs e instances.'],
|
|
354
|
+
notes: ['Use "zenifra help project <subcomando>" para detalhes de info, url, logs, metrics, network, image, exposure, autoscaling, billing, envs e instances.'],
|
|
299
355
|
},
|
|
300
356
|
{
|
|
301
357
|
command: 'project info',
|
|
@@ -352,6 +408,16 @@ const HELP_SPECS = [
|
|
|
352
408
|
output: 'Imagem atualizada com sucesso.',
|
|
353
409
|
jsonOutput: '{"status":"success","message":"updated with success"}',
|
|
354
410
|
},
|
|
411
|
+
{
|
|
412
|
+
command: 'project exposure set',
|
|
413
|
+
usage: 'zenifra project exposure set --project <id> --exposure <public|private> [--json]',
|
|
414
|
+
description: 'Altera se um projeto HTTP expoe rota publica e dominio.',
|
|
415
|
+
flags: ['--project <id> ID do projeto.', '--exposure <mode> Use public para criar rota publica ou private para remover exposicao.', '--json Imprime a resposta em JSON.'],
|
|
416
|
+
examples: ['zenifra project exposure set --project 507f1f77bcf86cd799439012 --exposure private', 'zenifra project exposure set --project 507f1f77bcf86cd799439012 --exposure public --json'],
|
|
417
|
+
output: 'Campo Valor\n--------- ----------------------------------\nExposicao private\nDominio -',
|
|
418
|
+
jsonOutput: '{"status":"success","message":"project exposure updated with success","exposure":"private","custom_domains":[]}',
|
|
419
|
+
notes: ['Ao mudar para private, a API remove a rota publica e dominios personalizados do projeto.'],
|
|
420
|
+
},
|
|
355
421
|
{
|
|
356
422
|
command: 'project envs',
|
|
357
423
|
usage: 'zenifra project envs --project <id> [--json] [--show-values]',
|
|
@@ -392,10 +458,55 @@ const HELP_SPECS = [
|
|
|
392
458
|
jsonOutput: '{"status":"success","message":"updated with success","envs":[]}',
|
|
393
459
|
notes: ['Falha se a variavel nao existir.'],
|
|
394
460
|
},
|
|
461
|
+
{
|
|
462
|
+
command: 'project autoscaling',
|
|
463
|
+
usage: 'zenifra project autoscaling --project <id> [--json]',
|
|
464
|
+
description: 'Mostra a configuracao de auto-scaling HTTP do projeto.',
|
|
465
|
+
flags: ['--project <id> ID do projeto.', '--json Imprime a resposta em JSON.'],
|
|
466
|
+
examples: ['zenifra project autoscaling --project 507f1f77bcf86cd799439012'],
|
|
467
|
+
output: 'Campo Valor\n---------- -----\nStatus ativo\nMinimo 2\nMaximo 8\nCPU alvo 70%\nMemoria 80%',
|
|
468
|
+
jsonOutput: '{"enabled":true,"min_instances":2,"max_instances":8,"target_cpu_utilization_percent":70,"target_memory_utilization_percent":80}',
|
|
469
|
+
},
|
|
470
|
+
{
|
|
471
|
+
command: 'project autoscaling set',
|
|
472
|
+
usage: 'zenifra project autoscaling set --project <id> --min <n> --max <n> [--cpu <percent>] [--memory <percent>] [--json]',
|
|
473
|
+
description: 'Ativa ou atualiza o auto-scaling HTTP do projeto.',
|
|
474
|
+
flags: ['--project <id> ID do projeto.', '--min <n> Minimo de instancias reservadas.', '--max <n> Maximo de instancias permitidas.', '--cpu <percent> CPU alvo. Padrao da API: 70.', '--memory <percent> Memoria alvo. Padrao da API: 80.', '--json Imprime a resposta em JSON.'],
|
|
475
|
+
examples: ['zenifra project autoscaling set --project 507f1f77bcf86cd799439012 --min 2 --max 8 --cpu 70 --memory 80'],
|
|
476
|
+
output: 'Auto-scaling atualizado: 2-8 instancias.',
|
|
477
|
+
jsonOutput: '{"status":"success","message":"project autoscaling updated with success","data":{"autoscaling":{"enabled":true,"min_instances":2,"max_instances":8}}}',
|
|
478
|
+
},
|
|
479
|
+
{
|
|
480
|
+
command: 'project autoscaling disable',
|
|
481
|
+
usage: 'zenifra project autoscaling disable --project <id> [--json]',
|
|
482
|
+
description: 'Desativa o auto-scaling HTTP e volta para instancias fixas no minimo configurado.',
|
|
483
|
+
flags: ['--project <id> ID do projeto.', '--json Imprime a resposta em JSON.'],
|
|
484
|
+
examples: ['zenifra project autoscaling disable --project 507f1f77bcf86cd799439012'],
|
|
485
|
+
output: 'Auto-scaling desativado.',
|
|
486
|
+
jsonOutput: '{"status":"success","message":"project autoscaling updated with success","data":{"autoscaling":{"enabled":false}}}',
|
|
487
|
+
},
|
|
488
|
+
{
|
|
489
|
+
command: 'project autoscaling events',
|
|
490
|
+
usage: 'zenifra project autoscaling events --project <id> [--direction <scale_up|scale_down>] [--from <iso>] [--to <iso>] [--page <n>] [--limit <n>] [--json]',
|
|
491
|
+
description: 'Lista o historico de scale up e scale down do auto-scaling HTTP.',
|
|
492
|
+
flags: ['--project <id> ID do projeto.', '--direction <valor> Filtra por scale_up ou scale_down.', '--from <iso> Inicio do periodo.', '--to <iso> Fim do periodo.', '--page <n> Pagina. Padrao da API: 1.', '--limit <n> Itens por pagina. Maximo da API: 100.', '--json Imprime a resposta em JSON.'],
|
|
493
|
+
examples: ['zenifra project autoscaling events --project 507f1f77bcf86cd799439012 --direction scale_up'],
|
|
494
|
+
output: 'Quando Direcao Instancias Limites Motivo\n05/06/2026, 18:40 aumento 2 -> 5 CPU 91%/70% capacidade aumentada',
|
|
495
|
+
jsonOutput: '{"events":[{"direction":"scale_up","previous_instances":2,"new_instances":5,"occurred_at":"2026-06-05T21:40:00.000Z"}],"pagination":{"page":1,"limit":10,"total":1,"total_pages":1}}',
|
|
496
|
+
},
|
|
497
|
+
{
|
|
498
|
+
command: 'project billing usage',
|
|
499
|
+
usage: 'zenifra project billing usage --project <id> [--from <iso>] [--to <iso>] [--page <n>] [--limit <n>] [--json]',
|
|
500
|
+
description: 'Lista o consumo horario consolidado de um projeto.',
|
|
501
|
+
flags: ['--project <id> ID do projeto.', '--from <iso> Inicio do periodo.', '--to <iso> Fim do periodo.', '--page <n> Pagina. Padrao da API: 1.', '--limit <n> Itens por pagina. Maximo: 50.', '--json Imprime a resposta em JSON.'],
|
|
502
|
+
examples: ['zenifra project billing usage --project 507f1f77bcf86cd799439012 --from 2026-06-01T00:00:00Z --limit 20'],
|
|
503
|
+
output: 'Inicio Fim Instancia-horas Armazenamento Total\n2026-06-01T10:00:00.000Z 2026-06-01T11:00:00.000Z 2 1.5 GB-h R$ 0,30',
|
|
504
|
+
jsonOutput: '{"hours":[],"summary":{"currency":"brl","compute_amount":0,"storage_amount":0,"total_amount":0},"pagination":{"page":1,"limit":10,"total":0,"total_pages":0}}',
|
|
505
|
+
},
|
|
395
506
|
{
|
|
396
507
|
command: 'project instances',
|
|
397
508
|
usage: 'zenifra project instances --project <id> [--json]',
|
|
398
|
-
description: 'Lista instancias
|
|
509
|
+
description: 'Lista instancias do projeto.',
|
|
399
510
|
flags: ['--project <id> ID do projeto.', '--json Imprime a resposta em JSON.'],
|
|
400
511
|
examples: ['zenifra project instances --project 507f1f77bcf86cd799439012'],
|
|
401
512
|
output: 'Instancia\nweb-1',
|
|
@@ -426,11 +537,11 @@ const HELP_SPECS = [
|
|
|
426
537
|
flags: [
|
|
427
538
|
'--project <id> ID do projeto.',
|
|
428
539
|
'--build <id> ID do build.',
|
|
429
|
-
'--cursor <n> Cursor inicial. Padrao: 0.',
|
|
430
|
-
'--limit <n> Quantidade de logs por request. Padrao: 200.',
|
|
540
|
+
'--cursor <n> Cursor inicial inteiro, maior ou igual a 0. Padrao: 0.',
|
|
541
|
+
'--limit <n> Quantidade de logs por request, entre 1 e 500. Padrao: 200.',
|
|
431
542
|
'--follow Continua em polling ate o build finalizar.',
|
|
432
|
-
'--interval <seconds> Intervalo de polling com --follow. Padrao: 5.',
|
|
433
|
-
'--timeout <seconds> Timeout total com --follow. Padrao: 900.',
|
|
543
|
+
'--interval <seconds> Intervalo de polling com --follow, minimo 0.1. Padrao: 5.',
|
|
544
|
+
'--timeout <seconds> Timeout total com --follow, minimo 1. Padrao: 900.',
|
|
434
545
|
'--json Imprime a resposta em JSON.',
|
|
435
546
|
],
|
|
436
547
|
examples: ['zenifra builds logs --project 507f1f77bcf86cd799439012 --build build_123', 'zenifra builds logs --project 507f1f77bcf86cd799439012 --build build_123 --follow'],
|
|
@@ -466,7 +577,7 @@ const HELP_SPECS = [
|
|
|
466
577
|
command: 'deploy watch',
|
|
467
578
|
usage: 'zenifra deploy watch --project <id> --build <id> [--interval <seconds>] [--timeout <seconds>] [--json]',
|
|
468
579
|
description: 'Acompanha um build em tempo real, fazendo polling do status e imprimindo os logs incrementais do build ate o estado terminal.',
|
|
469
|
-
flags: ['--project <id> ID do projeto.', '--build <id> ID do build.', '--interval <seconds> Intervalo de polling. Padrao: 5.', '--timeout <seconds> Timeout total. Padrao: 900.', '--json Imprime cada resposta em JSON.'],
|
|
580
|
+
flags: ['--project <id> ID do projeto.', '--build <id> ID do build.', '--interval <seconds> Intervalo de polling, minimo 0.1. Padrao: 5.', '--timeout <seconds> Timeout total, minimo 1. Padrao: 900.', '--json Imprime cada resposta em JSON.'],
|
|
470
581
|
examples: [
|
|
471
582
|
'zenifra deploy watch --project 507f1f77bcf86cd799439012 --build build_123',
|
|
472
583
|
'zenifra deploy watch --project 507f1f77bcf86cd799439012 --build build_123 --interval 2',
|
|
@@ -578,6 +689,17 @@ function parseArgs(argv) {
|
|
|
578
689
|
return { positional, flags };
|
|
579
690
|
}
|
|
580
691
|
|
|
692
|
+
function flagNameForDisplay(value) {
|
|
693
|
+
return `--${String(value).replace(/[A-Z]/g, (character) => `-${character.toLowerCase()}`)}`;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
function validateKnownFlags(flags) {
|
|
697
|
+
const unknown = Object.keys(flags).filter((name) => !KNOWN_FLAG_NAMES.has(name));
|
|
698
|
+
if (unknown.length > 0) {
|
|
699
|
+
throw new CliError(`Flag desconhecida: ${unknown.map(flagNameForDisplay).join(', ')}.`);
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
|
|
581
703
|
function emptyProfileStore() {
|
|
582
704
|
return {
|
|
583
705
|
version: PROFILE_STORE_VERSION,
|
|
@@ -667,7 +789,9 @@ async function migrateLegacySession() {
|
|
|
667
789
|
});
|
|
668
790
|
store.profiles[DEFAULT_PROFILE_NAME] = migratedProfile;
|
|
669
791
|
store.activeProfile = DEFAULT_PROFILE_NAME;
|
|
670
|
-
|
|
792
|
+
const normalized = await writeProfileStore(store);
|
|
793
|
+
await rm(SESSION_FILE, { force: true });
|
|
794
|
+
return normalized;
|
|
671
795
|
}
|
|
672
796
|
|
|
673
797
|
async function readProfileStore() {
|
|
@@ -861,9 +985,16 @@ function estimateWizardTotal(state = {}) {
|
|
|
861
985
|
if (state.httpStoragePersistent) total += 1;
|
|
862
986
|
}
|
|
863
987
|
|
|
988
|
+
if (state.planAllowsAutoscaling) {
|
|
989
|
+
total += 1;
|
|
990
|
+
if (state.httpAutoscalingEnabled) total += 3;
|
|
991
|
+
}
|
|
992
|
+
|
|
864
993
|
total += 1;
|
|
865
994
|
if (state.envs?.enabled) total += state.envs.count * 3;
|
|
866
995
|
|
|
996
|
+
total += 1;
|
|
997
|
+
|
|
867
998
|
if (state.httpSource === 'github') {
|
|
868
999
|
total += 9;
|
|
869
1000
|
} else if (state.httpSource === 'oci') {
|
|
@@ -877,13 +1008,13 @@ function estimateWizardTotal(state = {}) {
|
|
|
877
1008
|
total += 1;
|
|
878
1009
|
}
|
|
879
1010
|
|
|
880
|
-
if (state.planAllowsBlockIp) {
|
|
1011
|
+
if (state.httpExposure !== 'private' && state.planAllowsBlockIp) {
|
|
881
1012
|
total += 2;
|
|
882
1013
|
if (state.whitelist?.enabled) total += state.whitelist.count * 3;
|
|
883
1014
|
if (state.blacklist?.enabled) total += state.blacklist.count * 3;
|
|
884
1015
|
}
|
|
885
1016
|
|
|
886
|
-
if (state.planAllowsSubdomain) total += 1;
|
|
1017
|
+
if (state.httpExposure !== 'private' && state.planAllowsSubdomain) total += 1;
|
|
887
1018
|
return total;
|
|
888
1019
|
}
|
|
889
1020
|
|
|
@@ -1199,8 +1330,24 @@ async function promptHidden(question) {
|
|
|
1199
1330
|
});
|
|
1200
1331
|
}
|
|
1201
1332
|
|
|
1202
|
-
|
|
1203
|
-
const
|
|
1333
|
+
function httpTimeoutMs() {
|
|
1334
|
+
const configured = process.env.ZENIFRA_HTTP_TIMEOUT_MS;
|
|
1335
|
+
if (configured === undefined) return DEFAULT_HTTP_TIMEOUT_MS;
|
|
1336
|
+
|
|
1337
|
+
const value = Number(configured);
|
|
1338
|
+
if (!Number.isInteger(value) || value <= 0) {
|
|
1339
|
+
throw new CliError('ZENIFRA_HTTP_TIMEOUT_MS deve ser um inteiro positivo em milissegundos.');
|
|
1340
|
+
}
|
|
1341
|
+
return value;
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
async function request(session, flags, method, path, {
|
|
1345
|
+
body,
|
|
1346
|
+
orgId,
|
|
1347
|
+
tokenRequired = true,
|
|
1348
|
+
credential: credentialOverride,
|
|
1349
|
+
} = {}) {
|
|
1350
|
+
const credential = credentialOverride || resolveCredential(session);
|
|
1204
1351
|
if (tokenRequired && !credential) {
|
|
1205
1352
|
throw new CliError('Voce precisa autenticar primeiro: zenifra auth login, zenifra auth api-key --key <znf_key> ou ZENIFRA_API_KEY.');
|
|
1206
1353
|
}
|
|
@@ -1216,11 +1363,21 @@ async function request(session, flags, method, path, { body, orgId, tokenRequire
|
|
|
1216
1363
|
headers['x-organization-id'] = orgId;
|
|
1217
1364
|
}
|
|
1218
1365
|
|
|
1219
|
-
const
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1366
|
+
const timeoutMs = httpTimeoutMs();
|
|
1367
|
+
let response;
|
|
1368
|
+
try {
|
|
1369
|
+
response = await fetch(`${apiBaseUrl(session, flags)}${path}`, {
|
|
1370
|
+
method,
|
|
1371
|
+
headers,
|
|
1372
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
1373
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
1374
|
+
});
|
|
1375
|
+
} catch (error) {
|
|
1376
|
+
if (error?.name === 'TimeoutError' || error?.name === 'AbortError') {
|
|
1377
|
+
throw new CliError(`A API da Zenifra nao respondeu em ate ${timeoutMs} ms.`);
|
|
1378
|
+
}
|
|
1379
|
+
throw new CliError(`Nao foi possivel conectar a API da Zenifra: ${error?.message || 'erro de rede'}.`);
|
|
1380
|
+
}
|
|
1224
1381
|
|
|
1225
1382
|
const text = await response.text();
|
|
1226
1383
|
let payload = {};
|
|
@@ -1234,12 +1391,18 @@ async function request(session, flags, method, path, { body, orgId, tokenRequire
|
|
|
1234
1391
|
|
|
1235
1392
|
if (!response.ok) {
|
|
1236
1393
|
const message = payload?.message || payload?.error || `Zenifra API retornou HTTP ${response.status}`;
|
|
1237
|
-
if (response.status === 401
|
|
1394
|
+
if (response.status === 401) {
|
|
1238
1395
|
if (credential?.type === 'api_key') {
|
|
1239
|
-
throw new CliError(`${message}. Verifique se a API key esta ativa
|
|
1396
|
+
throw new CliError(`${message}. Verifique se a API key esta ativa e se o IP atual esta permitido.`);
|
|
1240
1397
|
}
|
|
1241
1398
|
throw new CliError(`${message}. Rode "zenifra auth login" para renovar sua sessao.`);
|
|
1242
1399
|
}
|
|
1400
|
+
if (response.status === 403) {
|
|
1401
|
+
throw new CliError(message);
|
|
1402
|
+
}
|
|
1403
|
+
if (response.status === 429 && Number.isFinite(Number(payload?.retry_after_seconds))) {
|
|
1404
|
+
throw new CliError(`${message} Tente novamente em ${Number(payload.retry_after_seconds)} segundo(s).`);
|
|
1405
|
+
}
|
|
1243
1406
|
if (String(message).includes('missing x-organization-id')) {
|
|
1244
1407
|
throw new CliError('Organizacao nao selecionada. Rode "zenifra org set" ou use --org <id>.');
|
|
1245
1408
|
}
|
|
@@ -1351,6 +1514,7 @@ function printProject(project) {
|
|
|
1351
1514
|
{ label: 'Nome', value: project.name || '-' },
|
|
1352
1515
|
{ label: 'Status', value: project.status || '-' },
|
|
1353
1516
|
{ label: 'Tipo', value: project.type_project || '-' },
|
|
1517
|
+
{ label: 'Exposicao', value: project.exposure || '-' },
|
|
1354
1518
|
{ label: 'Plano', value: project.plan || '-' },
|
|
1355
1519
|
{ label: 'URL', value: url || '-' },
|
|
1356
1520
|
{ label: 'Imagem', value: project.image || '-' },
|
|
@@ -1507,6 +1671,18 @@ async function handleLogout(session, flags) {
|
|
|
1507
1671
|
throw new CliError('Nenhum perfil ativo configurado. Crie um perfil com "zenifra profile add" ou autentique com "zenifra auth login".');
|
|
1508
1672
|
}
|
|
1509
1673
|
const target = requireExistingProfile(session, targetName);
|
|
1674
|
+
if (flags.revoke) {
|
|
1675
|
+
if (!target.accessToken) {
|
|
1676
|
+
throw new CliError('A revogacao exige um perfil autenticado por login de usuario; API keys devem ser revogadas na organizacao.');
|
|
1677
|
+
}
|
|
1678
|
+
await request(target, flags, 'DELETE', '/authentication', {
|
|
1679
|
+
credential: {
|
|
1680
|
+
token: target.accessToken,
|
|
1681
|
+
type: 'user',
|
|
1682
|
+
source: 'profile',
|
|
1683
|
+
},
|
|
1684
|
+
});
|
|
1685
|
+
}
|
|
1510
1686
|
target.accessToken = undefined;
|
|
1511
1687
|
target.apiKey = undefined;
|
|
1512
1688
|
target.selectedOrganizationId = undefined;
|
|
@@ -1516,7 +1692,7 @@ async function handleLogout(session, flags) {
|
|
|
1516
1692
|
if (target.__profileName === getProfileName(session)) {
|
|
1517
1693
|
Object.assign(session, buildSession(target.__store));
|
|
1518
1694
|
}
|
|
1519
|
-
process.stdout.write(`Autenticacao removida do perfil ${target.__profileName}.\n`);
|
|
1695
|
+
process.stdout.write(`Autenticacao removida do perfil ${target.__profileName}${flags.revoke ? ' e sessoes de usuario revogadas' : ''}.\n`);
|
|
1520
1696
|
}
|
|
1521
1697
|
|
|
1522
1698
|
function maskApiKey(value) {
|
|
@@ -1801,6 +1977,22 @@ function normalizeTypeProject(value) {
|
|
|
1801
1977
|
return aliases.get(raw) || raw;
|
|
1802
1978
|
}
|
|
1803
1979
|
|
|
1980
|
+
function normalizeExposure(value) {
|
|
1981
|
+
const raw = normalizeFreeText(value);
|
|
1982
|
+
const aliases = new Map([
|
|
1983
|
+
['public', 'public'],
|
|
1984
|
+
['publico', 'public'],
|
|
1985
|
+
['público', 'public'],
|
|
1986
|
+
['exposto', 'public'],
|
|
1987
|
+
['sim', 'public'],
|
|
1988
|
+
['private', 'private'],
|
|
1989
|
+
['privado', 'private'],
|
|
1990
|
+
['nao', 'private'],
|
|
1991
|
+
['não', 'private'],
|
|
1992
|
+
]);
|
|
1993
|
+
return aliases.get(raw) || raw;
|
|
1994
|
+
}
|
|
1995
|
+
|
|
1804
1996
|
function normalizeRuntime(value) {
|
|
1805
1997
|
const raw = normalizeFreeText(value);
|
|
1806
1998
|
const aliases = new Map([
|
|
@@ -1842,6 +2034,13 @@ function isIpAccessEnabled(planCatalog, planId) {
|
|
|
1842
2034
|
return planHasFeature(planCatalog, planId, 'Bloqueio de IPs');
|
|
1843
2035
|
}
|
|
1844
2036
|
|
|
2037
|
+
function isAutoscalingEnabledForPlan(planCatalog, planId) {
|
|
2038
|
+
const plan = Array.isArray(planCatalog)
|
|
2039
|
+
? planCatalog.find((item) => item?.plan === planId)
|
|
2040
|
+
: undefined;
|
|
2041
|
+
return plan?.permissions?.allow_autoscaling === 'true';
|
|
2042
|
+
}
|
|
2043
|
+
|
|
1845
2044
|
function getMaxReplicasFromFeatures(features) {
|
|
1846
2045
|
const replicaFeature = features.find((feature) => /réplicas?/i.test(String(feature)));
|
|
1847
2046
|
if (!replicaFeature) return 5;
|
|
@@ -2016,6 +2215,52 @@ function formatAllowedValues(values) {
|
|
|
2016
2215
|
return [...values].join(', ');
|
|
2017
2216
|
}
|
|
2018
2217
|
|
|
2218
|
+
function validateCreateAutoscaling(config, plan, typeProject) {
|
|
2219
|
+
if (config?.autoscaling === undefined) return config;
|
|
2220
|
+
if (typeProject !== 'http') {
|
|
2221
|
+
throw new CliError('config.autoscaling deve ser informado apenas para projetos HTTP.');
|
|
2222
|
+
}
|
|
2223
|
+
if (plan === 'free') {
|
|
2224
|
+
throw new CliError('Auto-scaling nao esta disponivel para projetos HTTP no plano free.');
|
|
2225
|
+
}
|
|
2226
|
+
|
|
2227
|
+
const autoscaling = config.autoscaling;
|
|
2228
|
+
if (!autoscaling || typeof autoscaling !== 'object' || Array.isArray(autoscaling)) {
|
|
2229
|
+
throw new CliError('config.autoscaling deve ser um objeto.');
|
|
2230
|
+
}
|
|
2231
|
+
if (autoscaling.enabled !== true) {
|
|
2232
|
+
throw new CliError('config.autoscaling.enabled deve ser true na criacao do projeto.');
|
|
2233
|
+
}
|
|
2234
|
+
if (autoscaling.min_instances !== undefined) {
|
|
2235
|
+
throw new CliError('Use config.instances como minimo inicial; config.autoscaling.min_instances nao e aceito na criacao.');
|
|
2236
|
+
}
|
|
2237
|
+
|
|
2238
|
+
const instances = Number(config.instances);
|
|
2239
|
+
if (!Number.isInteger(instances) || instances <= 0) {
|
|
2240
|
+
throw new CliError('config.instances deve ser um inteiro positivo ao criar um projeto com auto-scaling.');
|
|
2241
|
+
}
|
|
2242
|
+
|
|
2243
|
+
const maxInstances = Number(autoscaling.max_instances);
|
|
2244
|
+
if (!Number.isInteger(maxInstances) || maxInstances < instances) {
|
|
2245
|
+
throw new CliError('config.autoscaling.max_instances deve ser um inteiro maior ou igual a config.instances.');
|
|
2246
|
+
}
|
|
2247
|
+
|
|
2248
|
+
const nextAutoscaling = {
|
|
2249
|
+
enabled: true,
|
|
2250
|
+
max_instances: maxInstances,
|
|
2251
|
+
};
|
|
2252
|
+
for (const key of ['target_cpu_utilization_percent', 'target_memory_utilization_percent']) {
|
|
2253
|
+
if (autoscaling[key] === undefined) continue;
|
|
2254
|
+
const value = Number(autoscaling[key]);
|
|
2255
|
+
if (!Number.isInteger(value) || value < 1 || value > 100) {
|
|
2256
|
+
throw new CliError(`config.autoscaling.${key} deve ser um inteiro entre 1 e 100.`);
|
|
2257
|
+
}
|
|
2258
|
+
nextAutoscaling[key] = value;
|
|
2259
|
+
}
|
|
2260
|
+
|
|
2261
|
+
return { ...config, autoscaling: nextAutoscaling };
|
|
2262
|
+
}
|
|
2263
|
+
|
|
2019
2264
|
function validateCreateInput({ plan, paymentMode, config }) {
|
|
2020
2265
|
const nextPlan = normalizePlan(plan);
|
|
2021
2266
|
if (!nextPlan || !ALLOWED_PLAN_VALUES.has(nextPlan)) {
|
|
@@ -2041,10 +2286,22 @@ function validateCreateInput({ plan, paymentMode, config }) {
|
|
|
2041
2286
|
nextConfig.github = { ...nextConfig.github, runtime: nextRuntime };
|
|
2042
2287
|
}
|
|
2043
2288
|
|
|
2289
|
+
if (nextTypeProject === 'http') {
|
|
2290
|
+
const nextExposure = normalizeExposure(nextConfig.exposure);
|
|
2291
|
+
if (!nextExposure || !ALLOWED_EXPOSURE_VALUES.has(nextExposure)) {
|
|
2292
|
+
throw new CliError(`exposure invalido: "${nextConfig.exposure || ''}". Valores aceitos: ${formatAllowedValues(ALLOWED_EXPOSURE_VALUES)}. Docs: ${DOCS_CREATE_HTTP_URL}`);
|
|
2293
|
+
}
|
|
2294
|
+
nextConfig.exposure = nextExposure;
|
|
2295
|
+
} else if (nextConfig.exposure !== undefined) {
|
|
2296
|
+
throw new CliError('exposure deve ser informado apenas para projetos HTTP.');
|
|
2297
|
+
}
|
|
2298
|
+
|
|
2299
|
+
const validatedConfig = validateCreateAutoscaling(nextConfig, nextPlan, nextTypeProject);
|
|
2300
|
+
|
|
2044
2301
|
return {
|
|
2045
2302
|
plan: nextPlan,
|
|
2046
2303
|
paymentMode: nextPaymentMode,
|
|
2047
|
-
config:
|
|
2304
|
+
config: validatedConfig,
|
|
2048
2305
|
};
|
|
2049
2306
|
}
|
|
2050
2307
|
|
|
@@ -2072,7 +2329,10 @@ function printWizardSummary(payload) {
|
|
|
2072
2329
|
if (masked.description) lines.push(` descricao: ${masked.description}`);
|
|
2073
2330
|
|
|
2074
2331
|
if (masked.config.type_project === 'http') {
|
|
2075
|
-
lines.push(` http: porta ${masked.config.port} | instancias ${masked.config.instances} | storage ${masked.config.storage.capacity}Gi (${masked.config.storage.persistent ? 'persistente' : 'efemero'})`);
|
|
2332
|
+
lines.push(` http: exposicao ${masked.config.exposure} | porta ${masked.config.port} | instancias ${masked.config.instances} | storage ${masked.config.storage.capacity}Gi (${masked.config.storage.persistent ? 'persistente' : 'efemero'})`);
|
|
2333
|
+
if (masked.config.autoscaling?.enabled) {
|
|
2334
|
+
lines.push(` auto-scaling: ${masked.config.instances}-${masked.config.autoscaling.max_instances} instancias | CPU ${masked.config.autoscaling.target_cpu_utilization_percent}% | memoria ${masked.config.autoscaling.target_memory_utilization_percent}%`);
|
|
2335
|
+
}
|
|
2076
2336
|
if (masked.config.github) {
|
|
2077
2337
|
lines.push(` github: ${masked.config.github.repository_owner}/${masked.config.github.repository_name}@${masked.config.github.branch} | runtime ${masked.config.github.runtime}@${masked.config.github.version}`);
|
|
2078
2338
|
} else if (masked.config.image) {
|
|
@@ -2277,6 +2537,42 @@ async function buildHttpConfig({ plan, httpPlans, availableInstances }) {
|
|
|
2277
2537
|
max: Math.max(1, Number(availableInstances?.[plan] || 1)),
|
|
2278
2538
|
});
|
|
2279
2539
|
|
|
2540
|
+
let autoscaling;
|
|
2541
|
+
if (isAutoscalingEnabledForPlan(httpPlans, plan)) {
|
|
2542
|
+
const enabled = await promptWizardBoolean({
|
|
2543
|
+
label: 'Ativar auto-scaling',
|
|
2544
|
+
docs: DOCS_CONFIGURATION_URL,
|
|
2545
|
+
examples: ['sim', 'nao'],
|
|
2546
|
+
});
|
|
2547
|
+
promptWizardBoolean.ui.state.httpAutoscalingEnabled = enabled;
|
|
2548
|
+
if (enabled) {
|
|
2549
|
+
autoscaling = {
|
|
2550
|
+
enabled: true,
|
|
2551
|
+
max_instances: await promptWizardNumber({
|
|
2552
|
+
label: 'Maximo de instancias do auto-scaling',
|
|
2553
|
+
docs: DOCS_CONFIGURATION_URL,
|
|
2554
|
+
examples: [String(Math.max(instances, 2))],
|
|
2555
|
+
min: instances,
|
|
2556
|
+
max: Math.max(instances, Number(availableInstances?.[plan] || instances)),
|
|
2557
|
+
}),
|
|
2558
|
+
target_cpu_utilization_percent: await promptWizardNumber({
|
|
2559
|
+
label: 'CPU alvo do auto-scaling (%)',
|
|
2560
|
+
docs: DOCS_CONFIGURATION_URL,
|
|
2561
|
+
examples: ['70'],
|
|
2562
|
+
min: 1,
|
|
2563
|
+
max: 100,
|
|
2564
|
+
}),
|
|
2565
|
+
target_memory_utilization_percent: await promptWizardNumber({
|
|
2566
|
+
label: 'Memoria alvo do auto-scaling (%)',
|
|
2567
|
+
docs: DOCS_CONFIGURATION_URL,
|
|
2568
|
+
examples: ['80'],
|
|
2569
|
+
min: 1,
|
|
2570
|
+
max: 100,
|
|
2571
|
+
}),
|
|
2572
|
+
};
|
|
2573
|
+
}
|
|
2574
|
+
}
|
|
2575
|
+
|
|
2280
2576
|
let storage;
|
|
2281
2577
|
if (plan === 'free') {
|
|
2282
2578
|
storage = { persistent: false, capacity: 1 };
|
|
@@ -2320,8 +2616,17 @@ async function buildHttpConfig({ plan, httpPlans, availableInstances }) {
|
|
|
2320
2616
|
],
|
|
2321
2617
|
});
|
|
2322
2618
|
|
|
2619
|
+
const exposure = await promptWizardSelect({
|
|
2620
|
+
label: 'Exposicao HTTP',
|
|
2621
|
+
docs: DOCS_CREATE_HTTP_URL,
|
|
2622
|
+
examples: WIZARD_HTTP_EXPOSURE_OPTIONS,
|
|
2623
|
+
options: WIZARD_HTTP_EXPOSURE_OPTIONS.map((value) => ({ value })),
|
|
2624
|
+
});
|
|
2625
|
+
promptWizardSelect.ui.state.httpExposure = exposure;
|
|
2626
|
+
|
|
2627
|
+
const isPublicExposure = exposure === 'public';
|
|
2323
2628
|
const allowBlockIp = isIpAccessEnabled(httpPlans, plan);
|
|
2324
|
-
const customWhitelist = allowBlockIp
|
|
2629
|
+
const customWhitelist = isPublicExposure && allowBlockIp
|
|
2325
2630
|
? await promptWizardPairs({
|
|
2326
2631
|
introLabel: 'Deseja configurar whitelist personalizada de entrada?',
|
|
2327
2632
|
docs: DOCS_CONFIGURATION_URL,
|
|
@@ -2334,7 +2639,7 @@ async function buildHttpConfig({ plan, httpPlans, availableInstances }) {
|
|
|
2334
2639
|
})
|
|
2335
2640
|
: [];
|
|
2336
2641
|
|
|
2337
|
-
const ingress_black_list = allowBlockIp
|
|
2642
|
+
const ingress_black_list = isPublicExposure && allowBlockIp
|
|
2338
2643
|
? await promptWizardPairs({
|
|
2339
2644
|
introLabel: 'Deseja adicionar blacklist de entrada?',
|
|
2340
2645
|
docs: DOCS_CONFIGURATION_URL,
|
|
@@ -2347,7 +2652,7 @@ async function buildHttpConfig({ plan, httpPlans, availableInstances }) {
|
|
|
2347
2652
|
})
|
|
2348
2653
|
: [];
|
|
2349
2654
|
|
|
2350
|
-
const subdomain = isSubdomainCustomizable(httpPlans, plan)
|
|
2655
|
+
const subdomain = isPublicExposure && isSubdomainCustomizable(httpPlans, plan)
|
|
2351
2656
|
? await promptWizardText({
|
|
2352
2657
|
label: 'Subdomain personalizado',
|
|
2353
2658
|
examples: ['minha-api'],
|
|
@@ -2357,9 +2662,11 @@ async function buildHttpConfig({ plan, httpPlans, availableInstances }) {
|
|
|
2357
2662
|
|
|
2358
2663
|
return {
|
|
2359
2664
|
type_project: 'http',
|
|
2665
|
+
exposure,
|
|
2360
2666
|
...(source === 'github' ? { github: await buildHttpGithubConfig() } : { image: await buildHttpImageConfig() }),
|
|
2361
2667
|
port,
|
|
2362
2668
|
instances,
|
|
2669
|
+
...(autoscaling ? { autoscaling } : {}),
|
|
2363
2670
|
storage,
|
|
2364
2671
|
envs,
|
|
2365
2672
|
...(subdomain ? { subdomain } : {}),
|
|
@@ -2475,6 +2782,7 @@ async function runProjectCreateWizard(session, flags) {
|
|
|
2475
2782
|
if (typeProject === 'http') {
|
|
2476
2783
|
ui.state.planAllowsBlockIp = isIpAccessEnabled(catalogs.httpPlans, plan);
|
|
2477
2784
|
ui.state.planAllowsSubdomain = isSubdomainCustomizable(catalogs.httpPlans, plan);
|
|
2785
|
+
ui.state.planAllowsAutoscaling = isAutoscalingEnabledForPlan(catalogs.httpPlans, plan);
|
|
2478
2786
|
}
|
|
2479
2787
|
const paymentMode = await promptWizardSelect({
|
|
2480
2788
|
label: 'Modo de pagamento',
|
|
@@ -2654,6 +2962,33 @@ async function handleProjectImageSet(session, flags) {
|
|
|
2654
2962
|
process.stdout.write('Imagem atualizada com sucesso.\n');
|
|
2655
2963
|
}
|
|
2656
2964
|
|
|
2965
|
+
async function handleProjectExposureSet(session, flags) {
|
|
2966
|
+
const projectId = requireProjectId(flags, 'project exposure set');
|
|
2967
|
+
if (!projectId) return
|
|
2968
|
+
const exposure = normalizeExposure(flags.exposure);
|
|
2969
|
+
|
|
2970
|
+
if (!ALLOWED_EXPOSURE_VALUES.has(exposure)) {
|
|
2971
|
+
throw new CliError('exposure invalido. Use public ou private.');
|
|
2972
|
+
}
|
|
2973
|
+
|
|
2974
|
+
const orgId = await resolveOrgId(session, flags);
|
|
2975
|
+
const payload = await request(session, flags, 'PATCH', `/project/${projectId}/exposure`, {
|
|
2976
|
+
orgId,
|
|
2977
|
+
body: { exposure },
|
|
2978
|
+
});
|
|
2979
|
+
|
|
2980
|
+
if (flags.json) return printJson(payload);
|
|
2981
|
+
|
|
2982
|
+
printTable([
|
|
2983
|
+
{ field: 'Exposicao', value: payload.exposure || exposure },
|
|
2984
|
+
{ field: 'Dominio', value: payload.domain ? formatPublicUrl(payload.domain) : '-' },
|
|
2985
|
+
{ field: 'Dominios customizados', value: asArray(payload.custom_domains).join(', ') || '-' },
|
|
2986
|
+
], [
|
|
2987
|
+
{ label: 'Campo', value: (row) => row.field },
|
|
2988
|
+
{ label: 'Valor', value: (row) => row.value },
|
|
2989
|
+
]);
|
|
2990
|
+
}
|
|
2991
|
+
|
|
2657
2992
|
async function getProjectEnvs(session, flags, projectId, orgId) {
|
|
2658
2993
|
return asArray(unwrapData(await request(session, flags, 'GET', `/project/${projectId}/envs`, { orgId })));
|
|
2659
2994
|
}
|
|
@@ -2760,6 +3095,241 @@ async function handleProjectInstancesSet(session, flags) {
|
|
|
2760
3095
|
process.stdout.write(`Quantidade de instancias atualizada para ${instances}.\n`);
|
|
2761
3096
|
}
|
|
2762
3097
|
|
|
3098
|
+
function parseAutoscalingPositiveInteger(value, flagName) {
|
|
3099
|
+
const parsed = Number(value);
|
|
3100
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
3101
|
+
throw new CliError(`Informe ${flagName} <n> com um inteiro positivo.`);
|
|
3102
|
+
}
|
|
3103
|
+
return parsed;
|
|
3104
|
+
}
|
|
3105
|
+
|
|
3106
|
+
function parseAutoscalingTargetPercent(value, flagName) {
|
|
3107
|
+
const parsed = parseAutoscalingPositiveInteger(value, flagName);
|
|
3108
|
+
if (parsed > 100) {
|
|
3109
|
+
throw new CliError(`Informe ${flagName} entre 1 e 100.`);
|
|
3110
|
+
}
|
|
3111
|
+
return parsed;
|
|
3112
|
+
}
|
|
3113
|
+
|
|
3114
|
+
function autoscalingFromProject(project) {
|
|
3115
|
+
return project?.additional_info?.autoscaling || project?.autoscaling || { enabled: false };
|
|
3116
|
+
}
|
|
3117
|
+
|
|
3118
|
+
function printAutoscaling(autoscaling) {
|
|
3119
|
+
printTable([
|
|
3120
|
+
{
|
|
3121
|
+
status: autoscaling.enabled ? 'ativo' : 'inativo',
|
|
3122
|
+
min: autoscaling.min_instances || '-',
|
|
3123
|
+
max: autoscaling.max_instances || '-',
|
|
3124
|
+
cpu: autoscaling.target_cpu_utilization_percent ? `${autoscaling.target_cpu_utilization_percent}%` : '-',
|
|
3125
|
+
memory: autoscaling.target_memory_utilization_percent ? `${autoscaling.target_memory_utilization_percent}%` : '-',
|
|
3126
|
+
},
|
|
3127
|
+
], [
|
|
3128
|
+
{ label: 'Status', value: (item) => item.status },
|
|
3129
|
+
{ label: 'Minimo', value: (item) => item.min },
|
|
3130
|
+
{ label: 'Maximo', value: (item) => item.max },
|
|
3131
|
+
{ label: 'CPU alvo', value: (item) => item.cpu },
|
|
3132
|
+
{ label: 'Memoria alvo', value: (item) => item.memory },
|
|
3133
|
+
]);
|
|
3134
|
+
}
|
|
3135
|
+
|
|
3136
|
+
async function handleProjectAutoscaling(session, flags) {
|
|
3137
|
+
const projectId = requireProjectId(flags, 'project autoscaling');
|
|
3138
|
+
if (!projectId) return
|
|
3139
|
+
const orgId = await resolveOrgId(session, flags);
|
|
3140
|
+
const project = await getProject(session, flags, projectId, orgId);
|
|
3141
|
+
const autoscaling = autoscalingFromProject(project);
|
|
3142
|
+
|
|
3143
|
+
if (flags.json) return printJson(autoscaling);
|
|
3144
|
+
printAutoscaling(autoscaling);
|
|
3145
|
+
}
|
|
3146
|
+
|
|
3147
|
+
async function handleProjectAutoscalingSet(session, flags) {
|
|
3148
|
+
const projectId = requireProjectId(flags, 'project autoscaling set');
|
|
3149
|
+
if (!projectId) return
|
|
3150
|
+
if (flags.min === undefined || flags.max === undefined) {
|
|
3151
|
+
printCommandHelpAndFail('project autoscaling set')
|
|
3152
|
+
return
|
|
3153
|
+
}
|
|
3154
|
+
|
|
3155
|
+
const minInstances = parseAutoscalingPositiveInteger(flags.min, '--min');
|
|
3156
|
+
const maxInstances = parseAutoscalingPositiveInteger(flags.max, '--max');
|
|
3157
|
+
if (maxInstances < minInstances) {
|
|
3158
|
+
throw new CliError('Informe --max maior ou igual a --min.');
|
|
3159
|
+
}
|
|
3160
|
+
|
|
3161
|
+
const body = {
|
|
3162
|
+
enabled: true,
|
|
3163
|
+
min_instances: minInstances,
|
|
3164
|
+
max_instances: maxInstances,
|
|
3165
|
+
};
|
|
3166
|
+
|
|
3167
|
+
if (flags.cpu !== undefined) {
|
|
3168
|
+
body.target_cpu_utilization_percent = parseAutoscalingTargetPercent(flags.cpu, '--cpu');
|
|
3169
|
+
}
|
|
3170
|
+
if (flags.memory !== undefined) {
|
|
3171
|
+
body.target_memory_utilization_percent = parseAutoscalingTargetPercent(flags.memory, '--memory');
|
|
3172
|
+
}
|
|
3173
|
+
|
|
3174
|
+
const orgId = await resolveOrgId(session, flags);
|
|
3175
|
+
const payload = await request(session, flags, 'PATCH', `/project/${projectId}/autoscaling`, {
|
|
3176
|
+
orgId,
|
|
3177
|
+
body,
|
|
3178
|
+
});
|
|
3179
|
+
|
|
3180
|
+
if (flags.json) return printJson(payload);
|
|
3181
|
+
process.stdout.write(`Auto-scaling atualizado: ${minInstances}-${maxInstances} instancias.\n`);
|
|
3182
|
+
}
|
|
3183
|
+
|
|
3184
|
+
async function handleProjectAutoscalingDisable(session, flags) {
|
|
3185
|
+
const projectId = requireProjectId(flags, 'project autoscaling disable');
|
|
3186
|
+
if (!projectId) return
|
|
3187
|
+
|
|
3188
|
+
const orgId = await resolveOrgId(session, flags);
|
|
3189
|
+
const payload = await request(session, flags, 'PATCH', `/project/${projectId}/autoscaling`, {
|
|
3190
|
+
orgId,
|
|
3191
|
+
body: { enabled: false },
|
|
3192
|
+
});
|
|
3193
|
+
|
|
3194
|
+
if (flags.json) return printJson(payload);
|
|
3195
|
+
process.stdout.write('Auto-scaling desativado.\n');
|
|
3196
|
+
}
|
|
3197
|
+
|
|
3198
|
+
function printAutoscalingEvents(data) {
|
|
3199
|
+
const directions = { scale_up: 'aumento', scale_down: 'reducao' };
|
|
3200
|
+
const reasons = { increased_capacity: 'capacidade aumentada', reduced_capacity: 'capacidade reduzida' };
|
|
3201
|
+
const events = asArray(data?.events);
|
|
3202
|
+
printTable(events, [
|
|
3203
|
+
{ label: 'Quando', value: (event) => event.occurred_at || '-' },
|
|
3204
|
+
{ label: 'Direcao', value: (event) => directions[event.direction] || event.direction || '-' },
|
|
3205
|
+
{
|
|
3206
|
+
label: 'Instancias',
|
|
3207
|
+
value: (event) => `${event.previous_instances ?? '-'} -> ${event.new_instances ?? '-'}`,
|
|
3208
|
+
},
|
|
3209
|
+
{
|
|
3210
|
+
label: 'Limites',
|
|
3211
|
+
value: (event) => {
|
|
3212
|
+
const cpu = typeof event.current_cpu_utilization_percent === 'number' || typeof event.target_cpu_utilization_percent === 'number'
|
|
3213
|
+
? `CPU ${event.current_cpu_utilization_percent ?? '-'}%/${event.target_cpu_utilization_percent ?? '-'}%`
|
|
3214
|
+
: null;
|
|
3215
|
+
const memory = typeof event.current_memory_utilization_percent === 'number' || typeof event.target_memory_utilization_percent === 'number'
|
|
3216
|
+
? `MEM ${event.current_memory_utilization_percent ?? '-'}%/${event.target_memory_utilization_percent ?? '-'}%`
|
|
3217
|
+
: null;
|
|
3218
|
+
return [cpu, memory].filter(Boolean).join(' ');
|
|
3219
|
+
},
|
|
3220
|
+
},
|
|
3221
|
+
{ label: 'Motivo', value: (event) => reasons[event.reason] || event.reason || event.trigger_metric || '-' },
|
|
3222
|
+
]);
|
|
3223
|
+
|
|
3224
|
+
printPagination(data?.pagination, 'evento');
|
|
3225
|
+
}
|
|
3226
|
+
|
|
3227
|
+
function isIsoDate(value) {
|
|
3228
|
+
const text = String(value);
|
|
3229
|
+
const match = text.match(/^(\d{4})-(\d{2})-(\d{2})(?:T(\d{2}):(\d{2})(?::(\d{2})(?:\.\d{1,9})?)?(?:Z|[+-]\d{2}:?\d{2})?)?$/);
|
|
3230
|
+
if (!match || Number.isNaN(Date.parse(text))) return false;
|
|
3231
|
+
|
|
3232
|
+
const [, year, month, day, hour = '00', minute = '00', second = '00'] = match;
|
|
3233
|
+
const calendarDate = new Date(Date.UTC(Number(year), Number(month) - 1, Number(day)));
|
|
3234
|
+
return calendarDate.getUTCFullYear() === Number(year)
|
|
3235
|
+
&& calendarDate.getUTCMonth() === Number(month) - 1
|
|
3236
|
+
&& calendarDate.getUTCDate() === Number(day)
|
|
3237
|
+
&& Number(hour) <= 23
|
|
3238
|
+
&& Number(minute) <= 59
|
|
3239
|
+
&& Number(second) <= 59;
|
|
3240
|
+
}
|
|
3241
|
+
|
|
3242
|
+
function validateProjectListFlags(flags, { maxLimit }) {
|
|
3243
|
+
for (const key of ['from', 'to']) {
|
|
3244
|
+
if (flags[key] !== undefined && !isIsoDate(flags[key])) {
|
|
3245
|
+
throw new CliError(`Informe --${key} <iso> com uma data ISO valida.`);
|
|
3246
|
+
}
|
|
3247
|
+
}
|
|
3248
|
+
if (flags.from !== undefined && flags.to !== undefined && Date.parse(String(flags.from)) > Date.parse(String(flags.to))) {
|
|
3249
|
+
throw new CliError('Informe --from anterior ou igual a --to.');
|
|
3250
|
+
}
|
|
3251
|
+
for (const key of ['page', 'limit']) {
|
|
3252
|
+
if (flags[key] === undefined) continue;
|
|
3253
|
+
const value = Number(flags[key]);
|
|
3254
|
+
if (!Number.isInteger(value) || value <= 0) {
|
|
3255
|
+
throw new CliError(`Informe --${key} <n> com um inteiro positivo.`);
|
|
3256
|
+
}
|
|
3257
|
+
if (key === 'limit' && value > maxLimit) {
|
|
3258
|
+
throw new CliError(`Informe --limit entre 1 e ${maxLimit}.`);
|
|
3259
|
+
}
|
|
3260
|
+
}
|
|
3261
|
+
}
|
|
3262
|
+
|
|
3263
|
+
function printPagination(pagination, singular = 'item') {
|
|
3264
|
+
if (!pagination) return;
|
|
3265
|
+
const { page, total_pages: totalPages, total } = pagination;
|
|
3266
|
+
process.stdout.write(`Pagina ${page} de ${totalPages || 1} - ${total} ${singular}(s)\n`);
|
|
3267
|
+
}
|
|
3268
|
+
|
|
3269
|
+
function formatMoney(value, currency = 'brl') {
|
|
3270
|
+
const amount = Number(value);
|
|
3271
|
+
if (!Number.isFinite(amount)) return '-';
|
|
3272
|
+
try {
|
|
3273
|
+
return new Intl.NumberFormat('pt-BR', {
|
|
3274
|
+
style: 'currency',
|
|
3275
|
+
currency: String(currency || 'brl').toUpperCase(),
|
|
3276
|
+
}).format(amount);
|
|
3277
|
+
} catch {
|
|
3278
|
+
return `${String(currency || 'brl').toUpperCase()} ${amount.toFixed(2)}`;
|
|
3279
|
+
}
|
|
3280
|
+
}
|
|
3281
|
+
|
|
3282
|
+
function printBillingUsage(data) {
|
|
3283
|
+
const hours = asArray(data?.hours);
|
|
3284
|
+
printTable(hours, [
|
|
3285
|
+
{ label: 'Inicio', value: (item) => item.hour_start || '-' },
|
|
3286
|
+
{ label: 'Fim', value: (item) => item.hour_end || '-' },
|
|
3287
|
+
{ label: 'Instancia-horas', value: (item) => item.compute_instance_hours ?? '-' },
|
|
3288
|
+
{ label: 'Armazenamento', value: (item) => item.storage_gb_hours === undefined ? '-' : `${item.storage_gb_hours} GB-h` },
|
|
3289
|
+
{ label: 'Computacao', value: (item) => formatMoney(item.compute_amount, item.currency) },
|
|
3290
|
+
{ label: 'Storage', value: (item) => formatMoney(item.storage_amount, item.currency) },
|
|
3291
|
+
{ label: 'Total', value: (item) => formatMoney(item.total_amount, item.currency) },
|
|
3292
|
+
{ label: 'Status', value: (item) => item.status || '-' },
|
|
3293
|
+
]);
|
|
3294
|
+
|
|
3295
|
+
if (data?.summary) {
|
|
3296
|
+
const summary = data.summary;
|
|
3297
|
+
process.stdout.write(`Resumo: computacao ${formatMoney(summary.compute_amount, summary.currency)}, armazenamento ${formatMoney(summary.storage_amount, summary.currency)}, total ${formatMoney(summary.total_amount, summary.currency)}\n`);
|
|
3298
|
+
}
|
|
3299
|
+
printPagination(data?.pagination, 'hora');
|
|
3300
|
+
}
|
|
3301
|
+
|
|
3302
|
+
|
|
3303
|
+
async function handleProjectAutoscalingEvents(session, flags) {
|
|
3304
|
+
const projectId = requireProjectId(flags, 'project autoscaling events');
|
|
3305
|
+
if (!projectId) return
|
|
3306
|
+
|
|
3307
|
+
if (flags.direction !== undefined && !['scale_up', 'scale_down'].includes(String(flags.direction))) {
|
|
3308
|
+
throw new CliError('direction invalido. Use scale_up ou scale_down.');
|
|
3309
|
+
}
|
|
3310
|
+
|
|
3311
|
+
validateProjectListFlags(flags, { maxLimit: 100 });
|
|
3312
|
+
|
|
3313
|
+
const orgId = await resolveOrgId(session, flags);
|
|
3314
|
+
const query = buildQuery(flags, ['direction', 'from', 'to', 'page', 'limit']);
|
|
3315
|
+
const data = unwrapData(await request(session, flags, 'GET', `/project/${projectId}/autoscaling/events${query}`, { orgId }));
|
|
3316
|
+
|
|
3317
|
+
if (flags.json) return printJson(data);
|
|
3318
|
+
printAutoscalingEvents(data);
|
|
3319
|
+
}
|
|
3320
|
+
|
|
3321
|
+
async function handleProjectBillingUsage(session, flags) {
|
|
3322
|
+
const projectId = requireProjectId(flags, 'project billing usage');
|
|
3323
|
+
if (!projectId) return;
|
|
3324
|
+
validateProjectListFlags(flags, { maxLimit: 50 });
|
|
3325
|
+
const orgId = await resolveOrgId(session, flags);
|
|
3326
|
+
const query = buildQuery(flags, ['from', 'to', 'page', 'limit']);
|
|
3327
|
+
const data = unwrapData(await request(session, flags, 'GET', `/project/${projectId}/billing/hourly-usage${query}`, { orgId }));
|
|
3328
|
+
|
|
3329
|
+
if (flags.json) return printJson(data);
|
|
3330
|
+
printBillingUsage(data);
|
|
3331
|
+
}
|
|
3332
|
+
|
|
2763
3333
|
function buildQuery(flags, allowedKeys) {
|
|
2764
3334
|
const params = new URLSearchParams();
|
|
2765
3335
|
for (const key of allowedKeys) {
|
|
@@ -2820,6 +3390,25 @@ async function getBuildLogs(session, flags, projectId, buildId, orgId, { cursor
|
|
|
2820
3390
|
return unwrapData(await request(session, flags, 'GET', `/project/${projectId}/github/builds/${buildId}/logs${query}`, { orgId }))
|
|
2821
3391
|
}
|
|
2822
3392
|
|
|
3393
|
+
function parseIntegerOption(value, flagName, { defaultValue, min, max }) {
|
|
3394
|
+
if (value === undefined) return defaultValue;
|
|
3395
|
+
const parsed = Number(value);
|
|
3396
|
+
if (!Number.isInteger(parsed) || parsed < min || (max !== undefined && parsed > max)) {
|
|
3397
|
+
const range = max === undefined ? `maior ou igual a ${min}` : `entre ${min} e ${max}`;
|
|
3398
|
+
throw new CliError(`Informe ${flagName} com um inteiro ${range}.`);
|
|
3399
|
+
}
|
|
3400
|
+
return parsed;
|
|
3401
|
+
}
|
|
3402
|
+
|
|
3403
|
+
function parseSecondsOption(value, flagName, { defaultValue, min }) {
|
|
3404
|
+
if (value === undefined) return defaultValue;
|
|
3405
|
+
const parsed = Number(value);
|
|
3406
|
+
if (!Number.isFinite(parsed) || parsed < min) {
|
|
3407
|
+
throw new CliError(`Informe ${flagName} com um numero maior ou igual a ${min}.`);
|
|
3408
|
+
}
|
|
3409
|
+
return parsed;
|
|
3410
|
+
}
|
|
3411
|
+
|
|
2823
3412
|
function printBuildLogs(logs) {
|
|
2824
3413
|
for (const log of asArray(logs)) {
|
|
2825
3414
|
process.stdout.write(`${buildLogLineOf(log)}\n`)
|
|
@@ -2838,10 +3427,10 @@ async function followBuildLogs(session, flags, projectId, buildId, orgId, {
|
|
|
2838
3427
|
intervalSeconds = 5,
|
|
2839
3428
|
timeoutSeconds = 900,
|
|
2840
3429
|
} = {}) {
|
|
2841
|
-
let cursor =
|
|
2842
|
-
const pollLimit =
|
|
2843
|
-
const intervalMs =
|
|
2844
|
-
const timeoutMs =
|
|
3430
|
+
let cursor = parseIntegerOption(initialCursor, '--cursor', { defaultValue: 0, min: 0 })
|
|
3431
|
+
const pollLimit = parseIntegerOption(limit, '--limit', { defaultValue: 200, min: 1, max: 500 })
|
|
3432
|
+
const intervalMs = parseSecondsOption(intervalSeconds, '--interval', { defaultValue: 5, min: 0.1 }) * 1000
|
|
3433
|
+
const timeoutMs = parseSecondsOption(timeoutSeconds, '--timeout', { defaultValue: 900, min: 1 }) * 1000
|
|
2845
3434
|
const startedAt = Date.now()
|
|
2846
3435
|
|
|
2847
3436
|
while (true) {
|
|
@@ -2887,15 +3476,15 @@ async function handleBuildLogs(session, flags) {
|
|
|
2887
3476
|
}
|
|
2888
3477
|
|
|
2889
3478
|
const orgId = await resolveOrgId(session, flags);
|
|
2890
|
-
const cursor =
|
|
2891
|
-
const limit =
|
|
3479
|
+
const cursor = parseIntegerOption(flags.cursor, '--cursor', { defaultValue: 0, min: 0 })
|
|
3480
|
+
const limit = parseIntegerOption(flags.limit, '--limit', { defaultValue: 200, min: 1, max: 500 })
|
|
2892
3481
|
|
|
2893
3482
|
if (flags.follow) {
|
|
2894
3483
|
return followBuildLogs(session, flags, projectId, buildId, orgId, {
|
|
2895
3484
|
initialCursor: cursor,
|
|
2896
3485
|
limit,
|
|
2897
|
-
intervalSeconds:
|
|
2898
|
-
timeoutSeconds:
|
|
3486
|
+
intervalSeconds: parseSecondsOption(flags.interval, '--interval', { defaultValue: 5, min: 0.1 }),
|
|
3487
|
+
timeoutSeconds: parseSecondsOption(flags.timeout, '--timeout', { defaultValue: 900, min: 1 }),
|
|
2899
3488
|
})
|
|
2900
3489
|
}
|
|
2901
3490
|
|
|
@@ -2916,13 +3505,14 @@ async function handleDeployWatch(session, flags) {
|
|
|
2916
3505
|
return followBuildLogs(session, flags, projectId, buildId, orgId, {
|
|
2917
3506
|
initialCursor: 0,
|
|
2918
3507
|
limit: 200,
|
|
2919
|
-
intervalSeconds:
|
|
2920
|
-
timeoutSeconds:
|
|
3508
|
+
intervalSeconds: parseSecondsOption(flags.interval, '--interval', { defaultValue: 5, min: 0.1 }),
|
|
3509
|
+
timeoutSeconds: parseSecondsOption(flags.timeout, '--timeout', { defaultValue: 900, min: 1 }),
|
|
2921
3510
|
})
|
|
2922
3511
|
}
|
|
2923
3512
|
|
|
2924
3513
|
async function main() {
|
|
2925
3514
|
const { positional, flags } = parseArgs(process.argv.slice(2));
|
|
3515
|
+
validateKnownFlags(flags);
|
|
2926
3516
|
const [command, subcommand] = positional;
|
|
2927
3517
|
const session = await readSession();
|
|
2928
3518
|
try {
|
|
@@ -2972,10 +3562,16 @@ async function main() {
|
|
|
2972
3562
|
if (command === 'project' && subcommand === 'metrics') return handleProjectMetrics(session, flags);
|
|
2973
3563
|
if (command === 'project' && subcommand === 'network') return handleProjectNetwork(session, flags);
|
|
2974
3564
|
if (command === 'project' && subcommand === 'image' && positional[2] === 'set') return handleProjectImageSet(session, flags);
|
|
3565
|
+
if (command === 'project' && subcommand === 'exposure' && positional[2] === 'set') return handleProjectExposureSet(session, flags);
|
|
2975
3566
|
if (command === 'project' && subcommand === 'envs') return handleProjectEnvs(session, flags);
|
|
2976
3567
|
if (command === 'project' && subcommand === 'env' && positional[2] === 'add') return handleProjectEnvMutation(session, flags, 'add');
|
|
2977
3568
|
if (command === 'project' && subcommand === 'env' && positional[2] === 'update') return handleProjectEnvMutation(session, flags, 'update');
|
|
2978
3569
|
if (command === 'project' && subcommand === 'env' && positional[2] === 'remove') return handleProjectEnvMutation(session, flags, 'remove');
|
|
3570
|
+
if (command === 'project' && subcommand === 'autoscaling' && positional[2] === 'set') return handleProjectAutoscalingSet(session, flags);
|
|
3571
|
+
if (command === 'project' && subcommand === 'autoscaling' && positional[2] === 'disable') return handleProjectAutoscalingDisable(session, flags);
|
|
3572
|
+
if (command === 'project' && subcommand === 'autoscaling' && positional[2] === 'events') return handleProjectAutoscalingEvents(session, flags);
|
|
3573
|
+
if (command === 'project' && subcommand === 'autoscaling') return handleProjectAutoscaling(session, flags);
|
|
3574
|
+
if (command === 'project' && subcommand === 'billing' && positional[2] === 'usage') return handleProjectBillingUsage(session, flags);
|
|
2979
3575
|
if (command === 'project' && subcommand === 'instances' && positional[2] === 'set') return handleProjectInstancesSet(session, flags);
|
|
2980
3576
|
if (command === 'project' && subcommand === 'instances') return handleProjectInstances(session, flags);
|
|
2981
3577
|
if (command === 'builds' && subcommand === 'logs') return handleBuildLogs(session, flags);
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"type_project": "http",
|
|
3
|
+
"exposure": "public",
|
|
4
|
+
"image": {
|
|
5
|
+
"url": "docker.io/nginx:perl",
|
|
6
|
+
"is_public": true
|
|
7
|
+
},
|
|
8
|
+
"port": 80,
|
|
9
|
+
"instances": 2,
|
|
10
|
+
"autoscaling": {
|
|
11
|
+
"enabled": true,
|
|
12
|
+
"max_instances": 8,
|
|
13
|
+
"target_cpu_utilization_percent": 70,
|
|
14
|
+
"target_memory_utilization_percent": 80
|
|
15
|
+
},
|
|
16
|
+
"storage": {
|
|
17
|
+
"persistent": false,
|
|
18
|
+
"capacity": 1
|
|
19
|
+
},
|
|
20
|
+
"envs": [],
|
|
21
|
+
"network_access": {
|
|
22
|
+
"ingress_white_list": [
|
|
23
|
+
{
|
|
24
|
+
"cidr": "0.0.0.0/0",
|
|
25
|
+
"description": "Allow all"
|
|
26
|
+
}
|
|
27
|
+
],
|
|
28
|
+
"ingress_black_list": []
|
|
29
|
+
}
|
|
30
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zenifra/cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
4
4
|
"description": "Official Zenifra command line interface.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -31,7 +31,9 @@
|
|
|
31
31
|
"README.md"
|
|
32
32
|
],
|
|
33
33
|
"scripts": {
|
|
34
|
-
"check": "node --check bin/zenifra.mjs",
|
|
34
|
+
"check": "node --check bin/zenifra.mjs && npm run check:staging-autoscaling-regression",
|
|
35
|
+
"check:staging-autoscaling-regression": "node --check scripts/staging-autoscaling-regression.mjs",
|
|
36
|
+
"test:staging:autoscaling": "node scripts/staging-autoscaling-regression.mjs",
|
|
35
37
|
"test": "node --test"
|
|
36
38
|
}
|
|
37
39
|
}
|