@cat-indev/catops-cli 0.0.1-alpha.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +397 -0
- package/dist/bin/devops-cli.d.ts +2 -0
- package/dist/bin/devops-cli.js +132 -0
- package/dist/bin/devops-cli.js.map +1 -0
- package/dist/core/Context.d.ts +42 -0
- package/dist/core/Context.js +153 -0
- package/dist/core/Context.js.map +1 -0
- package/dist/core/Menu.d.ts +23 -0
- package/dist/core/Menu.js +159 -0
- package/dist/core/Menu.js.map +1 -0
- package/dist/core/Notifier.d.ts +27 -0
- package/dist/core/Notifier.js +93 -0
- package/dist/core/Notifier.js.map +1 -0
- package/dist/core/classifiers.d.ts +31 -0
- package/dist/core/classifiers.js +53 -0
- package/dist/core/classifiers.js.map +1 -0
- package/dist/core/logger.d.ts +8 -0
- package/dist/core/logger.js +37 -0
- package/dist/core/logger.js.map +1 -0
- package/dist/core/prompt.d.ts +10 -0
- package/dist/core/prompt.js +87 -0
- package/dist/core/prompt.js.map +1 -0
- package/dist/core/senders.d.ts +27 -0
- package/dist/core/senders.js +109 -0
- package/dist/core/senders.js.map +1 -0
- package/dist/core/types.d.ts +105 -0
- package/dist/core/types.js +3 -0
- package/dist/core/types.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +50 -0
- package/dist/index.js.map +1 -0
- package/dist/services/ansible.d.ts +11 -0
- package/dist/services/ansible.js +35 -0
- package/dist/services/ansible.js.map +1 -0
- package/dist/services/archive.d.ts +4 -0
- package/dist/services/archive.js +16 -0
- package/dist/services/archive.js.map +1 -0
- package/dist/services/argocd.d.ts +20 -0
- package/dist/services/argocd.js +48 -0
- package/dist/services/argocd.js.map +1 -0
- package/dist/services/az.d.ts +33 -0
- package/dist/services/az.js +48 -0
- package/dist/services/az.js.map +1 -0
- package/dist/services/azdo.d.ts +13 -0
- package/dist/services/azdo.js +57 -0
- package/dist/services/azdo.js.map +1 -0
- package/dist/services/docker.d.ts +18 -0
- package/dist/services/docker.js +36 -0
- package/dist/services/docker.js.map +1 -0
- package/dist/services/git.d.ts +9 -0
- package/dist/services/git.js +36 -0
- package/dist/services/git.js.map +1 -0
- package/dist/services/helm.d.ts +4 -0
- package/dist/services/helm.js +24 -0
- package/dist/services/helm.js.map +1 -0
- package/dist/services/index.d.ts +32 -0
- package/dist/services/index.js +81 -0
- package/dist/services/index.js.map +1 -0
- package/dist/services/kubectl.d.ts +135 -0
- package/dist/services/kubectl.js +266 -0
- package/dist/services/kubectl.js.map +1 -0
- package/dist/services/npm.d.ts +5 -0
- package/dist/services/npm.js +20 -0
- package/dist/services/npm.js.map +1 -0
- package/dist/services/oc.d.ts +146 -0
- package/dist/services/oc.js +296 -0
- package/dist/services/oc.js.map +1 -0
- package/dist/services/shell.d.ts +20 -0
- package/dist/services/shell.js +195 -0
- package/dist/services/shell.js.map +1 -0
- package/dist/services/tekton.d.ts +14 -0
- package/dist/services/tekton.js +46 -0
- package/dist/services/tekton.js.map +1 -0
- package/dist/services/terraform.d.ts +26 -0
- package/dist/services/terraform.js +63 -0
- package/dist/services/terraform.js.map +1 -0
- package/package.json +58 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
# devops-cli
|
|
2
|
+
|
|
3
|
+
Framework interno para pipelines DevOps, empaquetado como librería npm instalable en cualquier proyecto.
|
|
4
|
+
|
|
5
|
+
## Instalación
|
|
6
|
+
|
|
7
|
+
**Opción A — publicado en tu registro npm (público o privado tipo Verdaccio/Artifactory/GitHub Packages):**
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install devops-cli
|
|
11
|
+
# o si lo publicas con scope propio, p.ej. @miorg/devops-cli
|
|
12
|
+
npm install @miorg/devops-cli
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
**Opción B — sin publicar, directo desde este proyecto (útil mientras lo maduras):**
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
# Dentro del repo de devops-cli
|
|
19
|
+
npm pack # genera devops-cli-0.1.0.tgz
|
|
20
|
+
|
|
21
|
+
# Dentro del proyecto que lo va a consumir
|
|
22
|
+
npm install /ruta/a/devops-cli-0.1.0.tgz
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
**Opción C — enlazado local con `npm link` (para desarrollar la librería y el proyecto que la consume al mismo tiempo):**
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
# Dentro del repo de devops-cli
|
|
29
|
+
npm link
|
|
30
|
+
|
|
31
|
+
# Dentro del proyecto consumidor
|
|
32
|
+
npm link devops-cli
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
**Opción D — como dependencia de Git (monorepo o repo privado, sin registro npm):**
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
npm install git+https://github.com/tu-org/devops-cli.git
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Cualquiera de las 4 deja disponibles dos cosas en el proyecto consumidor:
|
|
42
|
+
|
|
43
|
+
1. La librería: `const { Context, Menu, services } = require("devops-cli");`
|
|
44
|
+
2. El binario: `npx devops-cli` (o `devops-cli` si lo instalaste global con `-g`).
|
|
45
|
+
|
|
46
|
+
## Publicar una nueva versión
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
npm version patch # o minor / major
|
|
50
|
+
npm publish # agrega --access public si usas un scope (@miorg/devops-cli)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Piezas que integra
|
|
54
|
+
|
|
55
|
+
Une dos piezas que ya tenías:
|
|
56
|
+
|
|
57
|
+
1. El **ExecutionContext** (estado global: flags, params, env, vars, results, logger, prompt).
|
|
58
|
+
2. Los **servicios de shell** (`shell`, `docker`, `git`, `kubectl`, `helm`, `npm`, `archive`), integrados **tal cual** los diste, sin reescribir su lógica.
|
|
59
|
+
|
|
60
|
+
La única pieza nueva es el pegamento: `ctx.services` apunta directamente a los módulos de `src/services`, así que cualquier task puede hacer `ctx.services.docker.build(...)` sin recibir nada por parámetro, tal como describías.
|
|
61
|
+
|
|
62
|
+
## Estructura
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
src/
|
|
66
|
+
core/
|
|
67
|
+
Context.js -> ExecutionContext singleton (Context.current())
|
|
68
|
+
Menu.js -> Renderer/Menu.render() para navegación jerárquica
|
|
69
|
+
prompt.js -> ctx.ask / ctx.confirm / ctx.select (sin dependencias externas)
|
|
70
|
+
logger.js -> logger usado por Context y por shell.js
|
|
71
|
+
services/
|
|
72
|
+
shell.js -> motor base (spawn), con retry/timeout/dryRun
|
|
73
|
+
docker.js -> tal cual el original
|
|
74
|
+
git.js -> tal cual el original
|
|
75
|
+
kubectl.js -> tal cual el original
|
|
76
|
+
helm.js -> tal cual el original
|
|
77
|
+
npm.js -> tal cual el original
|
|
78
|
+
archive.js -> tal cual el original
|
|
79
|
+
terraform.js -> init/plan/apply/destroy/output/validate/fmt
|
|
80
|
+
ansible.js -> playbook/adhoc/vaultEncrypt/vaultDecrypt/galaxyInstall
|
|
81
|
+
argocd.js -> login/appSync/appGet/appWait/appSet/appList/appRollback
|
|
82
|
+
tekton.js -> pipelineStart/pipelinerunList/pipelinerunLogs/taskStart/taskrunLogs
|
|
83
|
+
oc.js -> login/project/apply/get/rollout/newApp/startBuild/logs
|
|
84
|
+
az.js -> loginServicePrincipal/acrBuild/webappDeploy/aksGetCredentials/...
|
|
85
|
+
azdo.js -> logging commands de Azure Pipelines (##vso)
|
|
86
|
+
index.js -> registra todos los servicios anteriores
|
|
87
|
+
index.js -> exporta { Context, Menu, logger, prompt, services }
|
|
88
|
+
examples/
|
|
89
|
+
pipeline-example.js -> pipeline + menú de ejemplo
|
|
90
|
+
test/
|
|
91
|
+
context.test.js -> Context, flags, vars, parseArgv, dryRun global
|
|
92
|
+
shell.test.js -> retry, timeout, dryRun del motor shell.exec
|
|
93
|
+
services.test.js -> verifica que docker/terraform/argocd arman bien los args
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Qué se corrigió para que "convivan"
|
|
97
|
+
|
|
98
|
+
- `shell.js` usaba `logger.info(...)` / `logger.error(...)` sin importarlo. Se agregó `const logger = require("../core/logger")`.
|
|
99
|
+
- `Context.js` hacía `this.logger = logger` sin importar `logger` tampoco. Ahora importa `./logger`.
|
|
100
|
+
- Se agregó `this.services = services` dentro del constructor de `Context`, cableando exactamente el "Registry" que proponías al final de tu mensaje:
|
|
101
|
+
|
|
102
|
+
```javascript
|
|
103
|
+
ctx.services.git.clone(...)
|
|
104
|
+
ctx.services.docker.build(...)
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
- `Context.instance` pasó de crearse en la definición de la clase a crearse de forma perezosa en `Context.current()`, para evitar problemas de orden de carga con los `require` circulares entre `Context` → `services` → `shell` → `logger`.
|
|
108
|
+
|
|
109
|
+
## Uso dentro de un proyecto que lo instaló
|
|
110
|
+
|
|
111
|
+
Crea un `devops.pipeline.js` (o `devops.config.js` / `.devops-cli.js`) en la raíz de tu proyecto:
|
|
112
|
+
|
|
113
|
+
```javascript
|
|
114
|
+
// devops.pipeline.js
|
|
115
|
+
module.exports = (ctx) => ({
|
|
116
|
+
title: "Pipeline",
|
|
117
|
+
options: {
|
|
118
|
+
Build: async () => {
|
|
119
|
+
await ctx.services.docker.build({
|
|
120
|
+
image: "registry/app:v1",
|
|
121
|
+
dockerfile: "Dockerfile"
|
|
122
|
+
});
|
|
123
|
+
},
|
|
124
|
+
Deploy: async () => {
|
|
125
|
+
await ctx.services.kubectl.apply("deployment.yaml");
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Y ejecuta:
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
npx devops-cli --debug --env=prod
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
`devops-cli` detecta el archivo, arma el `Context` a partir de los flags/params de `argv`, y renderiza el menú.
|
|
138
|
+
|
|
139
|
+
## Uso como librería (sin el menú interactivo)
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
npm run example -- --debug --env=prod
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
```javascript
|
|
146
|
+
const { Context, Menu } = require("./src");
|
|
147
|
+
|
|
148
|
+
const ctx = Context.parseArgv(); // llena flags/params desde argv
|
|
149
|
+
|
|
150
|
+
ctx.set("image", "registry/api:v1");
|
|
151
|
+
|
|
152
|
+
await ctx.services.git.checkout("develop");
|
|
153
|
+
await ctx.services.npm.ci();
|
|
154
|
+
await ctx.services.docker.build({ image: ctx.get("image"), dockerfile: "Dockerfile" });
|
|
155
|
+
await ctx.services.docker.push(ctx.get("image"));
|
|
156
|
+
await ctx.services.kubectl.apply("deployment.yaml");
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
O con menús interactivos anidados:
|
|
160
|
+
|
|
161
|
+
```javascript
|
|
162
|
+
await Menu.render({
|
|
163
|
+
title: "Deploy",
|
|
164
|
+
options: {
|
|
165
|
+
Build: buildTask,
|
|
166
|
+
Docker: dockerMenu, // submenú anidado
|
|
167
|
+
Publish: publishTask
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## retry / timeout / dryRun en shell.exec
|
|
173
|
+
|
|
174
|
+
`shell.exec(command, ...args)` sigue aceptando exactamente los mismos argumentos que antes (por eso `docker.js`, `git.js`, etc. no necesitaron cambiar). Ahora, si el último argumento es un objeto plano, se interpreta como opciones **solo para esa llamada**:
|
|
175
|
+
|
|
176
|
+
```javascript
|
|
177
|
+
await ctx.services.docker.push(image); // igual que siempre
|
|
178
|
+
|
|
179
|
+
await ctx.services.shell.exec("curl", "https://flaky-api.internal", {
|
|
180
|
+
retry: 3, // reintentos totales (default: 1 = sin retry)
|
|
181
|
+
retryDelay: 1000,// ms entre reintentos
|
|
182
|
+
timeout: 5000, // ms antes de matar el proceso con SIGTERM
|
|
183
|
+
dryRun: true // solo loguea el comando, no lo ejecuta
|
|
184
|
+
});
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
También puedes fijar defaults globales para todo el proceso:
|
|
188
|
+
|
|
189
|
+
```javascript
|
|
190
|
+
ctx.services.shell.configure({ retry: 3, timeout: 30000 });
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Y `Context.parseArgv()` ya conecta flags de línea de comandos automáticamente:
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
npx devops-cli --dry-run # activa dryRun global
|
|
197
|
+
npx devops-cli --retry=3 --timeout=15000
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
## Logging commands de Azure Pipelines (`ctx.services.azdo`)
|
|
201
|
+
|
|
202
|
+
```javascript
|
|
203
|
+
ctx.services.azdo.setVariable("BUILD_TAG", "v1.2.3");
|
|
204
|
+
ctx.services.azdo.logWarning("El caché de npm no se encontró, se reconstruye desde cero.");
|
|
205
|
+
ctx.services.azdo.group("Build");
|
|
206
|
+
// ... pasos ...
|
|
207
|
+
ctx.services.azdo.endGroup();
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
## Servicios de infraestructura ya integrados
|
|
211
|
+
|
|
212
|
+
```javascript
|
|
213
|
+
await ctx.services.terraform.plan({ varFile: "prod.tfvars" });
|
|
214
|
+
await ctx.services.terraform.apply();
|
|
215
|
+
|
|
216
|
+
await ctx.services.ansible.playbook("site.yml", { inventory: "hosts.ini" });
|
|
217
|
+
|
|
218
|
+
await ctx.services.argocd.appSync("mi-app", { prune: true });
|
|
219
|
+
|
|
220
|
+
await ctx.services.tekton.pipelineStart("build-pipeline", { params: { image: "app:v1" } });
|
|
221
|
+
|
|
222
|
+
await ctx.services.oc.login({ server: "https://api.cluster:6443", token: process.env.OC_TOKEN });
|
|
223
|
+
await ctx.services.oc.rollout("mi-app");
|
|
224
|
+
|
|
225
|
+
await ctx.services.az.acrBuild({ registry: "miregistro", image: "app:v1" });
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
## Callbacks de éxito/error por tarea + notificaciones por área de TI
|
|
229
|
+
|
|
230
|
+
Cada tarea (`ctx.run()` o un item de menú) puede llevar sus propios callbacks, y además reporta automáticamente al `notifier` global del `Context`.
|
|
231
|
+
|
|
232
|
+
### Callbacks por tarea
|
|
233
|
+
|
|
234
|
+
```javascript
|
|
235
|
+
await ctx.run(
|
|
236
|
+
"deploy",
|
|
237
|
+
() => ctx.services.kubectl.apply("deployment.yaml"),
|
|
238
|
+
{
|
|
239
|
+
onSuccess: (result, ctx) => ctx.logger.success("Deploy OK"),
|
|
240
|
+
onError: (error, ctx) => ctx.logger.error(`Deploy falló: ${error.message}`)
|
|
241
|
+
}
|
|
242
|
+
);
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
En un menú, los mismos campos van directo en el item:
|
|
246
|
+
|
|
247
|
+
```javascript
|
|
248
|
+
options: {
|
|
249
|
+
Deploy: {
|
|
250
|
+
selector: "deploy",
|
|
251
|
+
action: () => ctx.services.kubectl.apply("deployment.yaml"),
|
|
252
|
+
onSuccess: (result, ctx) => {...},
|
|
253
|
+
onError: (error, ctx) => {...}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
### Clasificar el error por área de TI y enrutarlo a canales
|
|
259
|
+
|
|
260
|
+
`ctx.notifier` clasifica cada error (con la función que le des) y lo manda a los `senders` que hayas registrado para esa área:
|
|
261
|
+
|
|
262
|
+
```javascript
|
|
263
|
+
const { classifiers, senders } = require("devops-cli");
|
|
264
|
+
|
|
265
|
+
// 1. ¿A qué área de TI pertenece este error?
|
|
266
|
+
ctx.notifier.classify(classifiers.byCommand({
|
|
267
|
+
docker: "containers",
|
|
268
|
+
kubectl: "kubernetes",
|
|
269
|
+
oc: "kubernetes",
|
|
270
|
+
terraform: "infra",
|
|
271
|
+
ansible: "infra",
|
|
272
|
+
git: "scm",
|
|
273
|
+
argocd: "cd-pipeline",
|
|
274
|
+
tkn: "cd-pipeline",
|
|
275
|
+
az: "cloud-azure"
|
|
276
|
+
}));
|
|
277
|
+
|
|
278
|
+
// también puedes clasificar por el texto del error:
|
|
279
|
+
ctx.notifier.classify(classifiers.byPattern([
|
|
280
|
+
[/permission denied|unauthorized/i, "security"],
|
|
281
|
+
[/timeout|ECONNREFUSED/i, "networking"],
|
|
282
|
+
[/no space left|ENOSPC/i, "infra"]
|
|
283
|
+
]));
|
|
284
|
+
|
|
285
|
+
// 2. ¿A dónde se manda cada área?
|
|
286
|
+
ctx.notifier.channel("kubernetes", senders.webhook({ url: process.env.SLACK_K8S_WEBHOOK }));
|
|
287
|
+
ctx.notifier.channel("security", senders.http({ url: "https://security.miempresa.com/incidents" }));
|
|
288
|
+
ctx.notifier.channel("*", senders.file({ path: "./devops-cli-errors.log" })); // TODO error, sin importar el área
|
|
289
|
+
|
|
290
|
+
// 3. (opcional) éxito, sin clasificación de área
|
|
291
|
+
ctx.notifier.onSuccess(senders.log());
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
A partir de aquí, cualquier `ctx.run(...)` o item de menú con `action` reporta automáticamente al notifier — no hay que llamarlo a mano en cada task.
|
|
295
|
+
|
|
296
|
+
### Senders incluidos
|
|
297
|
+
|
|
298
|
+
| Sender | Uso |
|
|
299
|
+
|---|---|
|
|
300
|
+
| `senders.log()` | Usa el logger interno (consola) |
|
|
301
|
+
| `senders.file({ path })` | Agrega el evento como una línea JSON al archivo |
|
|
302
|
+
| `senders.http({ url, method?, headers?, formatBody? })` | `POST` genérico del evento como JSON |
|
|
303
|
+
| `senders.webhook({ url, format? })` | Como `http`, pero formatea `{ text: "❌ ..." }` por defecto (Slack/Teams/Discord-friendly) |
|
|
304
|
+
| `senders.websocket({ url, timeout? })` | Abre una conexión WS, manda el evento como JSON y cierra. Requiere Node ≥21 (usa el `WebSocket` global) |
|
|
305
|
+
|
|
306
|
+
Puedes escribir tu propio sender: es cualquier función `(event) => void | Promise<void>` — recibe `{ type, taskId, area?, error?, result?, message, timestamp }`.
|
|
307
|
+
|
|
308
|
+
## kubectl / oc: kubeconfig, namespace y espera cíclica del rollout
|
|
309
|
+
|
|
310
|
+
`kubectl` y `oc` ahora aceptan `{ kubeconfig, namespace }` como último argumento en **todos** sus comandos (compatible con las llamadas de antes, que siguen funcionando sin ese argumento):
|
|
311
|
+
|
|
312
|
+
```javascript
|
|
313
|
+
await ctx.services.kubectl.apply("deploy.yaml", { kubeconfig: "/etc/kube/prod.yaml", namespace: "prod" });
|
|
314
|
+
await ctx.services.kubectl.get("pods", "-o", "wide", { namespace: "staging" });
|
|
315
|
+
|
|
316
|
+
await ctx.services.oc.login({ server: "https://api.cluster:6443", token, namespace: "prod" });
|
|
317
|
+
await ctx.services.oc.apply("deploy.yaml", { namespace: "prod" });
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
### `waitForDeployment` — validación cíclica del rollout
|
|
321
|
+
|
|
322
|
+
Sondea el Deployment (o `DeploymentConfig` con `oc`) hasta que:
|
|
323
|
+
|
|
324
|
+
- llega a **estado exitoso** (réplicas listas/actualizadas == deseadas) → resuelve con `{ status: "success", ... }`,
|
|
325
|
+
- se queda en **estado Failed** más de `failedGracePeriod` sin recuperarse → lanza `DeploymentRolloutError`,
|
|
326
|
+
- supera **`maxRestarts`** reinicios acumulados entre todos sus pods → lanza `DeploymentRolloutError` de inmediato, sin esperar el grace period,
|
|
327
|
+
- o se cumple el **`timeout`** global sin éxito → lanza `DeploymentRolloutError`.
|
|
328
|
+
|
|
329
|
+
En los tres casos de fallo, el polling se detiene ("se mata el proceso") y el error se re-lanza — listo para que `ctx.run(...)` lo capture y lo reporte automáticamente vía `ctx.notifier` (el error ya trae `command: "kubectl"` / `command: "oc"`, así que `classifiers.byCommand({ kubectl: "kubernetes" })` lo clasifica sin configuración extra).
|
|
330
|
+
|
|
331
|
+
```javascript
|
|
332
|
+
const { classifiers } = require("devops-cli");
|
|
333
|
+
|
|
334
|
+
ctx.notifier.classify(classifiers.byCommand({ kubectl: "kubernetes", oc: "kubernetes" }));
|
|
335
|
+
ctx.notifier.channel("kubernetes", senders.webhook({ url: process.env.SLACK_K8S_WEBHOOK }));
|
|
336
|
+
|
|
337
|
+
await ctx.run("deploy-api", async () => {
|
|
338
|
+
await ctx.services.kubectl.apply("deployment.yaml", { namespace: "prod" });
|
|
339
|
+
|
|
340
|
+
return ctx.services.kubectl.waitForDeployment({
|
|
341
|
+
deployment: "api",
|
|
342
|
+
namespace: "prod",
|
|
343
|
+
timeout: 5 * 60 * 1000, // 5 min totales antes de abortar
|
|
344
|
+
pollInterval: 5000, // chequea cada 5s
|
|
345
|
+
failedGracePeriod: 30_000, // si entra en Failed, espera 30s a que se recupere
|
|
346
|
+
maxRestarts: 5 // si supera 5 reinicios acumulados, aborta ya
|
|
347
|
+
});
|
|
348
|
+
});
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
Con `oc`, usa `resourceType: "dc"` para apuntar a un `DeploymentConfig` clásico de OpenShift en vez de un `Deployment` nativo (default: `"deployment"`).
|
|
352
|
+
|
|
353
|
+
### `waitForDeploymentGroup` — validar todas las instancias de un mismo despliegue GitOps
|
|
354
|
+
|
|
355
|
+
Pensada para el caso de GitOps donde un mismo repo termina desplegado como **varios Deployments** (una instancia por región/config/cliente, etc.), todos marcados con un label común, por ejemplo:
|
|
356
|
+
|
|
357
|
+
```yaml
|
|
358
|
+
metadata:
|
|
359
|
+
labels:
|
|
360
|
+
deployment-group: repository-14
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
`waitForDeploymentGroup` descubre todas las instancias que compartan ese label y corre `waitForDeployment` sobre **cada una en paralelo**, con el mismo `timeout`/`pollInterval`/`failedGracePeriod`/`maxRestarts` para todas:
|
|
364
|
+
|
|
365
|
+
```javascript
|
|
366
|
+
await ctx.run("deploy-repo-14", () =>
|
|
367
|
+
ctx.services.kubectl.waitForDeploymentGroup({
|
|
368
|
+
label: { "deployment-group": "repository-14" }, // o el string ya armado: "deployment-group=repository-14"
|
|
369
|
+
namespace: "prod",
|
|
370
|
+
timeout: 5 * 60 * 1000,
|
|
371
|
+
pollInterval: 5000,
|
|
372
|
+
failedGracePeriod: 30_000,
|
|
373
|
+
maxRestarts: 5
|
|
374
|
+
})
|
|
375
|
+
);
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
- Si **todas** llegan a estado exitoso → resuelve con `{ status: "success", deployments: [...] }` (el detalle de cada una).
|
|
379
|
+
- Si **alguna falla** (timeout individual, Failed sin recuperarse, o maxRestarts) → espera a que las demás terminen, y lanza `DeploymentGroupRolloutError` con `succeeded` (nombres que sí llegaron) y `failed` (nombres + motivo de cada una que no llegó).
|
|
380
|
+
- Si el label **no matchea ningún deployment**, también lanza `DeploymentGroupRolloutError` (grupo vacío = error, no éxito silencioso).
|
|
381
|
+
|
|
382
|
+
Igual que con `waitForDeployment`, el error lleva `command: "kubectl"` / `command: "oc"`, así que se clasifica solo con `classifiers.byCommand(...)` si usas el `notifier`. Con `oc`, también acepta `resourceType: "dc"` para agrupar `DeploymentConfig`s.
|
|
383
|
+
|
|
384
|
+
## Tests
|
|
385
|
+
|
|
386
|
+
```bash
|
|
387
|
+
npm test
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
Corre sobre `node:test` (sin dependencias externas): valida el `Context`, el motor `shell.exec` (retry/timeout/dryRun) y que los servicios armen los comandos correctos, interceptando `shell.exec` en vez de ejecutar binarios reales.
|
|
391
|
+
|
|
392
|
+
## Siguientes pasos posibles
|
|
393
|
+
|
|
394
|
+
- Publicar en un registro privado (Verdaccio/Artifactory/GitHub Packages) para instalarlo con scope, p.ej. `@miorg/devops-cli`.
|
|
395
|
+
- Agregar tipos (`.d.ts`) si el equipo usa TypeScript.
|
|
396
|
+
- Agregar más plugins (`ansible-lint`, `trivy`, `sonar-scanner`) con el mismo patrón.
|
|
397
|
+
- CI propio (GitHub Actions/Azure Pipelines) que corra `npm test` en cada PR antes de `npm publish`.
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
4
|
+
if (k2 === undefined) k2 = k;
|
|
5
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
6
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
7
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
8
|
+
}
|
|
9
|
+
Object.defineProperty(o, k2, desc);
|
|
10
|
+
}) : (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
o[k2] = m[k];
|
|
13
|
+
}));
|
|
14
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
15
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
16
|
+
}) : function(o, v) {
|
|
17
|
+
o["default"] = v;
|
|
18
|
+
});
|
|
19
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
20
|
+
var ownKeys = function(o) {
|
|
21
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
22
|
+
var ar = [];
|
|
23
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
24
|
+
return ar;
|
|
25
|
+
};
|
|
26
|
+
return ownKeys(o);
|
|
27
|
+
};
|
|
28
|
+
return function (mod) {
|
|
29
|
+
if (mod && mod.__esModule) return mod;
|
|
30
|
+
var result = {};
|
|
31
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
32
|
+
__setModuleDefault(result, mod);
|
|
33
|
+
return result;
|
|
34
|
+
};
|
|
35
|
+
})();
|
|
36
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
37
|
+
const path = __importStar(require("path"));
|
|
38
|
+
const fs = __importStar(require("fs"));
|
|
39
|
+
const Context_1 = require("../core/Context");
|
|
40
|
+
const Menu_1 = require("../core/Menu");
|
|
41
|
+
const logger_1 = require("../core/logger");
|
|
42
|
+
const CONFIG_CANDIDATES = [
|
|
43
|
+
"devops.pipeline.js",
|
|
44
|
+
"devops.config.js",
|
|
45
|
+
".catops-cli.js"
|
|
46
|
+
];
|
|
47
|
+
function findConfig() {
|
|
48
|
+
const cwd = process.cwd();
|
|
49
|
+
for (const file of CONFIG_CANDIDATES) {
|
|
50
|
+
const fullPath = path.join(cwd, file);
|
|
51
|
+
if (fs.existsSync(fullPath)) {
|
|
52
|
+
return fullPath;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
function printHelp() {
|
|
58
|
+
console.log(`
|
|
59
|
+
catops-cli — framework CLI para pipelines DevOps
|
|
60
|
+
|
|
61
|
+
Uso:
|
|
62
|
+
catops-cli [--flag] [--param=valor]
|
|
63
|
+
|
|
64
|
+
Este comando busca, en el directorio actual, uno de estos archivos:
|
|
65
|
+
${CONFIG_CANDIDATES.join("\n ")}
|
|
66
|
+
|
|
67
|
+
Ese archivo debe exportar una definición de menú, por ejemplo:
|
|
68
|
+
|
|
69
|
+
// devops.pipeline.js
|
|
70
|
+
module.exports = (ctx) => ({
|
|
71
|
+
title: "Pipeline",
|
|
72
|
+
"flag-selector": "--menu-selector",
|
|
73
|
+
options: {
|
|
74
|
+
Build: {
|
|
75
|
+
selector: "build",
|
|
76
|
+
action: async () => {
|
|
77
|
+
await ctx.services.docker.build({
|
|
78
|
+
image: "registry/app:v1",
|
|
79
|
+
dockerfile: "Dockerfile"
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
Deploy: {
|
|
84
|
+
selector: "deploy",
|
|
85
|
+
action: async () => {
|
|
86
|
+
await ctx.services.kubectl.apply("deployment.yaml");
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
Con esa definición, ambas formas funcionan:
|
|
93
|
+
|
|
94
|
+
catops-cli # menú interactivo
|
|
95
|
+
catops-cli --menu-selector=build # ejecuta "Build" directamente, sin prompts
|
|
96
|
+
|
|
97
|
+
Si "Deploy" fuera un submenú anidado con su propio "flag-selector", se puede
|
|
98
|
+
encadenar en un solo comando, por ejemplo:
|
|
99
|
+
|
|
100
|
+
catops-cli --menu-selector=deploy --deploy-target=staging
|
|
101
|
+
|
|
102
|
+
También puedes usar la librería directamente en tu código:
|
|
103
|
+
|
|
104
|
+
const { Context, Menu } = require("catops-cli");
|
|
105
|
+
import { Context, Menu } from "catops-cli"; // con tipados, en TS
|
|
106
|
+
`);
|
|
107
|
+
}
|
|
108
|
+
async function main() {
|
|
109
|
+
const configPath = findConfig();
|
|
110
|
+
if (!configPath) {
|
|
111
|
+
printHelp();
|
|
112
|
+
process.exit(0);
|
|
113
|
+
}
|
|
114
|
+
const ctx = Context_1.Context.parseArgv();
|
|
115
|
+
if (ctx.hasFlag("help") || ctx.hasFlag("h")) {
|
|
116
|
+
printHelp();
|
|
117
|
+
process.exit(0);
|
|
118
|
+
}
|
|
119
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires, global-require
|
|
120
|
+
const definitionFactory = require(configPath);
|
|
121
|
+
const definition = typeof definitionFactory === "function"
|
|
122
|
+
? await definitionFactory(ctx)
|
|
123
|
+
: definitionFactory;
|
|
124
|
+
logger_1.logger.info(`Usando configuración: ${path.relative(process.cwd(), configPath)}`);
|
|
125
|
+
await Menu_1.Menu.render(definition, ctx);
|
|
126
|
+
}
|
|
127
|
+
main().catch(error => {
|
|
128
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
129
|
+
logger_1.logger.error(message);
|
|
130
|
+
process.exit(1);
|
|
131
|
+
});
|
|
132
|
+
//# sourceMappingURL=devops-cli.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"devops-cli.js","sourceRoot":"","sources":["../../src/bin/devops-cli.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAEA,2CAA6B;AAC7B,uCAAyB;AACzB,6CAA0C;AAC1C,uCAAoC;AACpC,2CAAwC;AAGxC,MAAM,iBAAiB,GAAG;IACtB,oBAAoB;IACpB,kBAAkB;IAClB,gBAAgB;CACnB,CAAC;AAEF,SAAS,UAAU;IAEf,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC;IAE1B,KAAK,MAAM,IAAI,IAAI,iBAAiB,EAAE,CAAC;QAEnC,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;QAEtC,IAAI,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC1B,OAAO,QAAQ,CAAC;QACpB,CAAC;IAEL,CAAC;IAED,OAAO,IAAI,CAAC;AAEhB,CAAC;AAED,SAAS,SAAS;IAEd,OAAO,CAAC,GAAG,CAAC;;;;;;;IAOZ,iBAAiB,CAAC,IAAI,CAAC,MAAM,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAyCjC,CAAC,CAAC;AAEH,CAAC;AAMD,KAAK,UAAU,IAAI;IAEf,MAAM,UAAU,GAAG,UAAU,EAAE,CAAC;IAEhC,IAAI,CAAC,UAAU,EAAE,CAAC;QACd,SAAS,EAAE,CAAC;QACZ,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACpB,CAAC;IAED,MAAM,GAAG,GAAG,iBAAO,CAAC,SAAS,EAAE,CAAC;IAEhC,IAAI,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;QAC1C,SAAS,EAAE,CAAC;QACZ,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACpB,CAAC;IAED,8EAA8E;IAC9E,MAAM,iBAAiB,GAAG,OAAO,CAAC,UAAU,CAAoC,CAAC;IAEjF,MAAM,UAAU,GAAG,OAAO,iBAAiB,KAAK,UAAU;QACtD,CAAC,CAAC,MAAM,iBAAiB,CAAC,GAAG,CAAC;QAC9B,CAAC,CAAC,iBAAiB,CAAC;IAExB,eAAM,CAAC,IAAI,CAAC,yBAAyB,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,UAAU,CAAC,EAAE,CAAC,CAAC;IAEjF,MAAM,WAAI,CAAC,MAAM,CAAC,UAAU,EAAE,GAAG,CAAC,CAAC;AAEvC,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE;IACjB,MAAM,OAAO,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IACvE,eAAM,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IACtB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AACpB,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { type Logger } from "./logger";
|
|
2
|
+
import { type ServicesRegistry } from "../services";
|
|
3
|
+
import { Notifier } from "./Notifier";
|
|
4
|
+
import type { AskOptions, ConfirmOptions, SelectChoices } from "./prompt";
|
|
5
|
+
import type { TaskHooks } from "./types";
|
|
6
|
+
export type FlagValue = boolean | string;
|
|
7
|
+
export declare class Context {
|
|
8
|
+
private static instance;
|
|
9
|
+
flags: Record<string, FlagValue>;
|
|
10
|
+
params: Record<string, string>;
|
|
11
|
+
env: NodeJS.ProcessEnv;
|
|
12
|
+
vars: Record<string, unknown>;
|
|
13
|
+
results: Record<string, unknown>;
|
|
14
|
+
logger: Logger;
|
|
15
|
+
services: ServicesRegistry;
|
|
16
|
+
notifier: Notifier;
|
|
17
|
+
config: Record<string, unknown>;
|
|
18
|
+
state: Record<string, unknown>;
|
|
19
|
+
static current(): Context;
|
|
20
|
+
/** Reinicia el singleton. Útil sobre todo en tests. */
|
|
21
|
+
static reset(): Context;
|
|
22
|
+
setFlag(name: string, value?: FlagValue): this;
|
|
23
|
+
getFlag(name: string): FlagValue | undefined;
|
|
24
|
+
hasFlag(name: string): boolean;
|
|
25
|
+
set<T = unknown>(name: string, value: T): this;
|
|
26
|
+
get<T = unknown>(name: string): T | undefined;
|
|
27
|
+
/**
|
|
28
|
+
* Ejecuta una task, guarda su resultado en `ctx.results[taskId]`, y
|
|
29
|
+
* dispara los callbacks de éxito/error correspondientes:
|
|
30
|
+
* 1. `hooks.onSuccess` / `hooks.onError` (si los pasaste), y
|
|
31
|
+
* 2. el `notifier` global (clasifica el error por área de TI y
|
|
32
|
+
* lo reporta a los canales registrados con `ctx.notifier.channel(...)`).
|
|
33
|
+
*
|
|
34
|
+
* Si la task lanza, el error se re-lanza después de notificar, para no
|
|
35
|
+
* esconder fallas silenciosamente.
|
|
36
|
+
*/
|
|
37
|
+
run<T>(taskId: string, callback: (ctx: Context) => Promise<T> | T, hooks?: TaskHooks<T>): Promise<T>;
|
|
38
|
+
ask(question: string, options?: AskOptions): Promise<string | undefined>;
|
|
39
|
+
confirm(question: string, options?: ConfirmOptions): Promise<boolean>;
|
|
40
|
+
select(title: string, choices: SelectChoices): Promise<string>;
|
|
41
|
+
static parseArgv(argv?: string[]): Context;
|
|
42
|
+
}
|