argocd-axi 0.1.4 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -10
- package/ROADMAP.md +14 -39
- package/dist/src/argocd.js +4 -1
- package/dist/src/cli.js +6 -0
- package/dist/src/commands/apps.js +218 -6
- package/dist/src/commands/ctx.js +118 -2
- package/dist/src/config.js +50 -2
- package/dist/src/gate.js +2 -2
- package/dist/src/help.js +4 -1
- package/dist/src/registry.js +18 -21
- package/dist/src/skill.js +3 -3
- package/dist/src/toon.js +1 -1
- package/dist/src/version.js +1 -1
- package/package.json +1 -1
- package/skills/argocd-axi/SKILL.md +7 -4
package/README.md
CHANGED
|
@@ -219,9 +219,7 @@ specific app.
|
|
|
219
219
|
local-file and delete flags whatever a command passes, gives argocd no
|
|
220
220
|
stdin, and enforces a timeout and an output cap. Tests fail if a new
|
|
221
221
|
operation becomes reachable without updating the registry.
|
|
222
|
-
- **
|
|
223
|
-
`REFUSED` with a reason and the raw command a human can run instead of
|
|
224
|
-
failing blind.
|
|
222
|
+
- **Opt-in Application writes.** Sync, rollback, refresh and terminate-op require an explicit context, preview and `--confirm`; execution is limited by `writable-contexts` and Argo CD RBAC. Other writes and credential commands answer `REFUSED`.
|
|
225
223
|
- **Redaction.** Kubernetes Secret `data`, `stringData` and their
|
|
226
224
|
last-applied annotation, values under sensitive-looking keys, and env-var or
|
|
227
225
|
Helm-parameter pairs with sensitive-looking names are replaced with
|
|
@@ -232,13 +230,10 @@ specific app.
|
|
|
232
230
|
reach them.
|
|
233
231
|
- **Writable contexts.** argocd-axi's own config file,
|
|
234
232
|
`~/.config/argocd-axi/config` (or `$XDG_CONFIG_HOME/argocd-axi/config`),
|
|
235
|
-
holds the allowlist of contexts where
|
|
236
|
-
It defaults to none
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
writable-contexts:
|
|
240
|
-
- staging
|
|
241
|
-
```
|
|
233
|
+
holds the allowlist of contexts where Application writes may execute.
|
|
234
|
+
It defaults to none. To allow writes on a context, a human runs
|
|
235
|
+
`argocd-axi ctx writable add <context>` in a terminal and confirms the exact
|
|
236
|
+
context name. Use `argocd-axi ctx writable remove <context>` to revoke access.
|
|
242
237
|
|
|
243
238
|
- **Version cache.** The server version is checked at most once every 12
|
|
244
239
|
hours per context and server, and cached in `~/.cache/argocd-axi/`
|
package/ROADMAP.md
CHANGED
|
@@ -1,53 +1,29 @@
|
|
|
1
1
|
# Roadmap
|
|
2
2
|
|
|
3
|
-
argocd-axi
|
|
4
|
-
listed as writable. This file records
|
|
5
|
-
safety rules.
|
|
3
|
+
argocd-axi supports confirmed app sync, rollback, refresh and terminate-op on
|
|
4
|
+
contexts explicitly listed as writable. This file records remaining write
|
|
5
|
+
tier work and shared safety rules.
|
|
6
6
|
|
|
7
|
-
##
|
|
7
|
+
## Remaining write tier
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
| Command | Wraps | Notes |
|
|
13
|
-
| ------------------------------------------------ | -------------------------- | ------------------------------------------------------------------------------------------------ |
|
|
14
|
-
| `apps rollback <app> <history-id> --context <c>` | `argocd app rollback` | Preview synthesized from target history and current app state; refuse while auto-sync is enabled |
|
|
15
|
-
| `apps terminate-op <app> --context <c>` | `argocd app terminate-op` | No-op when no operation is running |
|
|
16
|
-
| `apps refresh <app> [--hard] --context <c>` | `argocd app get --refresh` | Triggers a reconcile |
|
|
17
|
-
|
|
18
|
-
Later, a third tier: `apps action run <app> <action> --kind <kind>` for
|
|
19
|
-
resource actions such as restart, and Argo Rollouts promote and abort
|
|
20
|
-
through the same path.
|
|
9
|
+
Resource actions such as restart, and Argo Rollouts promote and abort, remain
|
|
10
|
+
future work. The supported Application writes are listed in [README.md](README.md).
|
|
21
11
|
|
|
22
12
|
### Rules every write follows
|
|
23
13
|
|
|
24
|
-
The gate is
|
|
25
|
-
|
|
26
|
-
and calls the gate.
|
|
27
|
-
|
|
28
|
-
1. **Preview by default.** Without the command's execute flag, nothing is
|
|
29
|
-
sent. The preview shows the target context and server, the object, the
|
|
30
|
-
change set and the preflight checks, plus the exact command that executes
|
|
31
|
-
it. Previews work against any context.
|
|
32
|
-
2. **A local allowlist of writable contexts, default none.** Executing needs
|
|
33
|
-
the context listed in argocd-axi's own config file,
|
|
34
|
-
`~/.config/argocd-axi/config` (or `$XDG_CONFIG_HOME/argocd-axi/config`):
|
|
35
|
-
|
|
36
|
-
```yaml
|
|
37
|
-
writable-contexts:
|
|
38
|
-
- staging
|
|
39
|
-
```
|
|
14
|
+
The gate is built in `src/gate.ts`, with its allowlist loaded from
|
|
15
|
+
`src/config.ts`. New writes must use the gate and the registered operation path.
|
|
40
16
|
|
|
41
|
-
|
|
42
|
-
|
|
17
|
+
1. **Preview by default.** Without `--confirm`, no write is sent. Each write
|
|
18
|
+
previews the target action before execution.
|
|
19
|
+
2. **A local allowlist of writable contexts, default none.** See [README.md](README.md)
|
|
20
|
+
for how a human manages the allowlist.
|
|
43
21
|
|
|
44
22
|
3. **An explicit target.** Every write names `--context <name>`; the current
|
|
45
23
|
context is never used implicitly, and the result echoes the context and
|
|
46
24
|
server.
|
|
47
|
-
4. **
|
|
48
|
-
|
|
49
|
-
vocabulary. The exact execute-flag spelling is settled when each command is
|
|
50
|
-
built.
|
|
25
|
+
4. **Explicit confirmation.** Every supported Application write requires
|
|
26
|
+
`--confirm` to execute.
|
|
51
27
|
5. **Idempotent.** Asking for a state that already exists (syncing an app
|
|
52
28
|
that is already synced to the target revision, terminating when nothing
|
|
53
29
|
runs) is a no-op with exit 0. Exit 1 only when the intent cannot be met:
|
|
@@ -93,4 +69,3 @@ instances (version, SSO provider, app count) are known.
|
|
|
93
69
|
|
|
94
70
|
Submission to the [AXI community catalog](https://github.com/kunchenguid/axi)
|
|
95
71
|
comes after v1. npm releases are automated; see the Releases section in
|
|
96
|
-
[README.md](README.md).
|
package/dist/src/argocd.js
CHANGED
|
@@ -14,7 +14,10 @@ export function buildArgv(op, args, context) {
|
|
|
14
14
|
const operation = OPERATIONS[op];
|
|
15
15
|
for (const arg of args) {
|
|
16
16
|
const name = arg.startsWith("--") ? arg.split("=")[0] : arg;
|
|
17
|
-
if (name !== undefined &&
|
|
17
|
+
if (name !== undefined &&
|
|
18
|
+
FORBIDDEN_ARGS.includes(name) &&
|
|
19
|
+
!(op === "app refresh" &&
|
|
20
|
+
(name === "--refresh" || name === "--hard-refresh"))) {
|
|
18
21
|
throw axiError(`internal guard: argument ${name} is never passed to argocd`, CODES.REFUSED);
|
|
19
22
|
}
|
|
20
23
|
}
|
package/dist/src/cli.js
CHANGED
|
@@ -58,6 +58,12 @@ export const COMMANDS = Object.assign(Object.create(null), {
|
|
|
58
58
|
...Object.fromEntries(TOP_LEVEL_REFUSALS.map((key) => [key, refusalCommand(key)])),
|
|
59
59
|
});
|
|
60
60
|
export async function main(argv = process.argv.slice(2)) {
|
|
61
|
+
if (argv.length === 1 &&
|
|
62
|
+
argv[0] !== undefined &&
|
|
63
|
+
["--version", "-v", "-V"].includes(argv[0])) {
|
|
64
|
+
process.stdout.write(`${encode(VERSION)}\n`);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
61
67
|
await runAxiCli({
|
|
62
68
|
description: DESCRIPTION,
|
|
63
69
|
version: VERSION,
|
|
@@ -550,7 +550,7 @@ export function parseDiffHeaders(text) {
|
|
|
550
550
|
}
|
|
551
551
|
return rows;
|
|
552
552
|
}
|
|
553
|
-
export async function appsDiff(args) {
|
|
553
|
+
export async function appsDiff(args, includeHelp = true) {
|
|
554
554
|
const parsed = parseArgs(APPS_DIFF_SPEC, args);
|
|
555
555
|
const name = validateName("app", parsed.positionals[0]);
|
|
556
556
|
const revision = getString(parsed, "--revision");
|
|
@@ -584,9 +584,11 @@ export async function appsDiff(args) {
|
|
|
584
584
|
count: `${plural(rows.length, "resource")} ${rows.length === 1 ? "differs" : "differ"}`,
|
|
585
585
|
resources: redactor.deep(rows),
|
|
586
586
|
}),
|
|
587
|
-
], redactor,
|
|
588
|
-
|
|
589
|
-
|
|
587
|
+
], redactor, includeHelp
|
|
588
|
+
? [
|
|
589
|
+
`Run \`argocd-axi apps diff ${name} --full${scope}\` to see the line-level diff`,
|
|
590
|
+
]
|
|
591
|
+
: []);
|
|
590
592
|
}
|
|
591
593
|
const argv = [name, "--exit-code=false"];
|
|
592
594
|
if (revision !== undefined)
|
|
@@ -617,7 +619,7 @@ export async function appsDiff(args) {
|
|
|
617
619
|
const lines = redactor.text(text).split("\n");
|
|
618
620
|
blocks.push(renderTextBlock("diff", lines));
|
|
619
621
|
}
|
|
620
|
-
return finish(blocks, redactor, full
|
|
622
|
+
return finish(blocks, redactor, full || !includeHelp
|
|
621
623
|
? []
|
|
622
624
|
: [
|
|
623
625
|
`Run \`argocd-axi apps diff ${name}${revision ? ` --revision ${revision}` : ""} --full${scope}\` to see the line-level diff`,
|
|
@@ -1055,7 +1057,7 @@ export async function appsSync(args) {
|
|
|
1055
1057
|
executeFlag: "--confirm",
|
|
1056
1058
|
}, loadConfig().config);
|
|
1057
1059
|
const options = { context: gate.context };
|
|
1058
|
-
const diff = await appsDiff([name, "--context", gate.context]);
|
|
1060
|
+
const diff = await appsDiff([name, "--context", gate.context], false);
|
|
1059
1061
|
const app = await getApp(name, options);
|
|
1060
1062
|
const project = validateName("project", app.spec?.project ?? "default");
|
|
1061
1063
|
const windows = (await argocdJson("proj windows list", [project, "-o", "json"], options)) ?? [];
|
|
@@ -1188,6 +1190,213 @@ function refuseActionRun() {
|
|
|
1188
1190
|
"A human can run one with the raw CLI: `argocd app actions run <app> <action> --kind <kind>`",
|
|
1189
1191
|
]);
|
|
1190
1192
|
}
|
|
1193
|
+
// ---------------------------------------------------------------- app writes
|
|
1194
|
+
function writeSpec(path, summary, positionals) {
|
|
1195
|
+
return {
|
|
1196
|
+
path,
|
|
1197
|
+
summary,
|
|
1198
|
+
positionals,
|
|
1199
|
+
flags: [
|
|
1200
|
+
{ name: "--confirm", kind: "bool", description: "Execute after preview" },
|
|
1201
|
+
...(path === "apps refresh"
|
|
1202
|
+
? []
|
|
1203
|
+
: [
|
|
1204
|
+
{
|
|
1205
|
+
name: "--timeout",
|
|
1206
|
+
kind: "value",
|
|
1207
|
+
valueName: "s",
|
|
1208
|
+
description: "Maximum wait in seconds (default 120)",
|
|
1209
|
+
},
|
|
1210
|
+
]),
|
|
1211
|
+
...(path === "apps refresh"
|
|
1212
|
+
? [
|
|
1213
|
+
{
|
|
1214
|
+
name: "--hard",
|
|
1215
|
+
kind: "bool",
|
|
1216
|
+
description: "Hard refresh, bypassing the cache",
|
|
1217
|
+
},
|
|
1218
|
+
]
|
|
1219
|
+
: []),
|
|
1220
|
+
CONTEXT_FLAG,
|
|
1221
|
+
],
|
|
1222
|
+
examples: [
|
|
1223
|
+
`argocd-axi ${path} ${path === "apps rollback" ? "guestbook 3" : "guestbook"} --context staging`,
|
|
1224
|
+
],
|
|
1225
|
+
};
|
|
1226
|
+
}
|
|
1227
|
+
const WRITE_TIMEOUT = { default: 120, min: 1, max: 3600 };
|
|
1228
|
+
async function checkWritePermission(action, app, name, options) {
|
|
1229
|
+
const permission = await runArgocd("account can-i", [action, "applications", `${app.spec?.project ?? "default"}/${name}`], { ...options, allowFailure: true });
|
|
1230
|
+
if (permission.exitCode !== 0 || !/^yes\s*$/i.test(permission.stdout.trim()))
|
|
1231
|
+
throw axiError(`REFUSED: Argo CD RBAC denies ${action} for ${name}`, CODES.REFUSED);
|
|
1232
|
+
}
|
|
1233
|
+
async function runApplicationWrite(op, args, options, name) {
|
|
1234
|
+
try {
|
|
1235
|
+
await runArgocd(op, args, options);
|
|
1236
|
+
}
|
|
1237
|
+
catch (error) {
|
|
1238
|
+
if (error instanceof AxiError && error.code === CODES.FORBIDDEN)
|
|
1239
|
+
throw axiError(`REFUSED: Argo CD RBAC denied ${op.slice(4)} for ${name}`, CODES.REFUSED);
|
|
1240
|
+
throw error;
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
function writeOutcome(phase, timedOut, completed) {
|
|
1244
|
+
if (phase === "Failed" || phase === "Error")
|
|
1245
|
+
return "failed";
|
|
1246
|
+
if (phase === "Succeeded")
|
|
1247
|
+
return completed;
|
|
1248
|
+
if (timedOut)
|
|
1249
|
+
return "waiting";
|
|
1250
|
+
return phase ?? "unknown";
|
|
1251
|
+
}
|
|
1252
|
+
async function waitForWrite(name, timeout, options) {
|
|
1253
|
+
try {
|
|
1254
|
+
const result = await runArgocd("app wait", [name, "--operation", "--timeout", String(timeout)], { ...options, allowFailure: true, timeoutMs: (timeout + 10) * 1000 });
|
|
1255
|
+
if (result.exitCode !== 0) {
|
|
1256
|
+
const failure = mapArgocdFailure(result.stderr || result.stdout);
|
|
1257
|
+
if (!/timed? out|timeout/i.test(failure.message))
|
|
1258
|
+
throw failure;
|
|
1259
|
+
return { app: await getApp(name, options), timedOut: true };
|
|
1260
|
+
}
|
|
1261
|
+
return { app: await getApp(name, options), timedOut: false };
|
|
1262
|
+
}
|
|
1263
|
+
catch (error) {
|
|
1264
|
+
if (!(error instanceof AxiError) || error.code !== CODES.TIMEOUT)
|
|
1265
|
+
throw error;
|
|
1266
|
+
return { app: await getApp(name, options), timedOut: true };
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
const APPS_ROLLBACK_SPEC = writeSpec("apps rollback", "Preview and optionally roll back one Application", [
|
|
1270
|
+
APP_ARG,
|
|
1271
|
+
{
|
|
1272
|
+
name: "history-id",
|
|
1273
|
+
required: true,
|
|
1274
|
+
description: "Deployment history id",
|
|
1275
|
+
},
|
|
1276
|
+
]);
|
|
1277
|
+
export async function appsRollback(args) {
|
|
1278
|
+
const parsed = parseArgs(APPS_ROLLBACK_SPEC, args);
|
|
1279
|
+
const name = validateName("app", parsed.positionals[0]);
|
|
1280
|
+
const historyId = parsed.positionals[1];
|
|
1281
|
+
if (!/^\d+$/.test(historyId))
|
|
1282
|
+
throw validationError("history-id must be a non-negative decimal integer");
|
|
1283
|
+
const id = Number(historyId);
|
|
1284
|
+
if (!Number.isSafeInteger(id) || id < 0)
|
|
1285
|
+
throw validationError("history-id must be a non-negative integer");
|
|
1286
|
+
const context = getString(parsed, "--context"), confirm = getBool(parsed, "--confirm"), timeout = getInt(parsed, "--timeout", WRITE_TIMEOUT);
|
|
1287
|
+
const gate = evaluateWriteGate({
|
|
1288
|
+
command: "apps rollback",
|
|
1289
|
+
context,
|
|
1290
|
+
execute: confirm,
|
|
1291
|
+
executeFlag: "--confirm",
|
|
1292
|
+
}, loadConfig().config);
|
|
1293
|
+
const options = { context: gate.context }, app = await getApp(name, options);
|
|
1294
|
+
const history = app.status?.history?.find((entry) => entry.id === id);
|
|
1295
|
+
if (!history)
|
|
1296
|
+
throw axiError(`REFUSED: history id ${id} not found for ${name}`, CODES.REFUSED);
|
|
1297
|
+
if (autoSyncSummary(app) !== "manual")
|
|
1298
|
+
throw axiError(`REFUSED: disable auto-sync before rolling back ${name}`, CODES.REFUSED);
|
|
1299
|
+
const target = history.revisions?.join(", ") ?? history.revision ?? "unknown";
|
|
1300
|
+
const previewBlocks = [
|
|
1301
|
+
encodeObject({
|
|
1302
|
+
rollback: name,
|
|
1303
|
+
currentRevision: syncRevision(app),
|
|
1304
|
+
targetHistoryId: id,
|
|
1305
|
+
targetRevision: target,
|
|
1306
|
+
}),
|
|
1307
|
+
];
|
|
1308
|
+
if (!confirm)
|
|
1309
|
+
return finish(previewBlocks, undefined, [
|
|
1310
|
+
"No changes made; add --confirm to roll back",
|
|
1311
|
+
...(gate.mode === "preview" ? gate.help : []),
|
|
1312
|
+
]);
|
|
1313
|
+
await checkWritePermission("sync", app, name, options);
|
|
1314
|
+
await runApplicationWrite("app rollback", [name, String(id), "--info", "initiated-by=argocd-axi"], options, name);
|
|
1315
|
+
const current = await waitForWrite(name, timeout, options);
|
|
1316
|
+
const outcome = writeOutcome(current.app.status?.operationState?.phase, current.timedOut, "submitted");
|
|
1317
|
+
return finish([
|
|
1318
|
+
...previewBlocks,
|
|
1319
|
+
encodeObject({
|
|
1320
|
+
rollback: outcome,
|
|
1321
|
+
historyId: id,
|
|
1322
|
+
phase: current.app.status?.operationState?.phase,
|
|
1323
|
+
operation: current.app.status?.operationState,
|
|
1324
|
+
}),
|
|
1325
|
+
], undefined, outcome === "waiting" ? ["Wait again with `apps wait`"] : []);
|
|
1326
|
+
}
|
|
1327
|
+
const APPS_REFRESH_SPEC = writeSpec("apps refresh", "Preview and optionally refresh one Application", [APP_ARG]);
|
|
1328
|
+
export async function appsRefresh(args) {
|
|
1329
|
+
const parsed = parseArgs(APPS_REFRESH_SPEC, args);
|
|
1330
|
+
const name = validateName("app", parsed.positionals[0]);
|
|
1331
|
+
const context = getString(parsed, "--context"), confirm = getBool(parsed, "--confirm"), hard = getBool(parsed, "--hard");
|
|
1332
|
+
const gate = evaluateWriteGate({
|
|
1333
|
+
command: "apps refresh",
|
|
1334
|
+
context,
|
|
1335
|
+
execute: confirm,
|
|
1336
|
+
executeFlag: "--confirm",
|
|
1337
|
+
}, loadConfig().config);
|
|
1338
|
+
const options = { context: gate.context }, app = await getApp(name, options);
|
|
1339
|
+
const blocks = [
|
|
1340
|
+
encodeObject({
|
|
1341
|
+
refresh: name,
|
|
1342
|
+
mode: hard ? "hard" : "normal",
|
|
1343
|
+
reRead: hard ? "live state and source caches" : "live state",
|
|
1344
|
+
}),
|
|
1345
|
+
];
|
|
1346
|
+
if (!confirm)
|
|
1347
|
+
return finish(blocks, undefined, [
|
|
1348
|
+
"No changes made; add --confirm to refresh",
|
|
1349
|
+
...(gate.mode === "preview" ? gate.help : []),
|
|
1350
|
+
]);
|
|
1351
|
+
await checkWritePermission("update", app, name, options);
|
|
1352
|
+
await runApplicationWrite("app refresh", [name, hard ? "--hard-refresh" : "--refresh"], options, name);
|
|
1353
|
+
return finish([...blocks, encodeObject({ refresh: "submitted" })], undefined);
|
|
1354
|
+
}
|
|
1355
|
+
const APPS_TERMINATE_SPEC = writeSpec("apps terminate-op", "Preview and optionally terminate one running operation", [APP_ARG]);
|
|
1356
|
+
export async function appsTerminateOp(args) {
|
|
1357
|
+
const parsed = parseArgs(APPS_TERMINATE_SPEC, args);
|
|
1358
|
+
const name = validateName("app", parsed.positionals[0]);
|
|
1359
|
+
const context = getString(parsed, "--context"), confirm = getBool(parsed, "--confirm"), timeout = getInt(parsed, "--timeout", WRITE_TIMEOUT);
|
|
1360
|
+
const gate = evaluateWriteGate({
|
|
1361
|
+
command: "apps terminate-op",
|
|
1362
|
+
context,
|
|
1363
|
+
execute: confirm,
|
|
1364
|
+
executeFlag: "--confirm",
|
|
1365
|
+
}, loadConfig().config);
|
|
1366
|
+
const options = { context: gate.context }, app = await getApp(name, options), phase = app.status?.operationState?.phase;
|
|
1367
|
+
if (!isOperationRunning(app))
|
|
1368
|
+
return finish([
|
|
1369
|
+
encodeObject({
|
|
1370
|
+
terminateOperation: name,
|
|
1371
|
+
status: "no operation running",
|
|
1372
|
+
}),
|
|
1373
|
+
], undefined);
|
|
1374
|
+
const blocks = [
|
|
1375
|
+
encodeObject({
|
|
1376
|
+
terminateOperation: name,
|
|
1377
|
+
phase,
|
|
1378
|
+
startedAt: app.status?.operationState?.startedAt,
|
|
1379
|
+
message: app.status?.operationState?.message,
|
|
1380
|
+
}),
|
|
1381
|
+
];
|
|
1382
|
+
if (!confirm)
|
|
1383
|
+
return finish(blocks, undefined, [
|
|
1384
|
+
"No changes made; add --confirm to terminate the operation",
|
|
1385
|
+
...(gate.mode === "preview" ? gate.help : []),
|
|
1386
|
+
]);
|
|
1387
|
+
await checkWritePermission("update", app, name, options);
|
|
1388
|
+
await runApplicationWrite("app terminate-op", [name], options, name);
|
|
1389
|
+
const current = await waitForWrite(name, timeout, options);
|
|
1390
|
+
const outcome = writeOutcome(current.app.status?.operationState?.phase, current.timedOut, "submitted");
|
|
1391
|
+
return finish([
|
|
1392
|
+
...blocks,
|
|
1393
|
+
encodeObject({
|
|
1394
|
+
terminateOperation: outcome,
|
|
1395
|
+
phase: current.app.status?.operationState?.phase,
|
|
1396
|
+
operation: current.app.status?.operationState,
|
|
1397
|
+
}),
|
|
1398
|
+
], undefined, outcome === "waiting" ? ["Wait again with `apps wait`"] : []);
|
|
1399
|
+
}
|
|
1191
1400
|
// ---------------------------------------------------------------- dispatcher
|
|
1192
1401
|
const SUBCOMMANDS = {
|
|
1193
1402
|
view: { spec: APPS_VIEW_SPEC, run: appsView },
|
|
@@ -1199,6 +1408,9 @@ const SUBCOMMANDS = {
|
|
|
1199
1408
|
wait: { spec: APPS_WAIT_SPEC, run: appsWait },
|
|
1200
1409
|
actions: { spec: APPS_ACTIONS_SPEC, run: appsActions },
|
|
1201
1410
|
sync: { spec: APPS_SYNC_SPEC, run: appsSync },
|
|
1411
|
+
rollback: { spec: APPS_ROLLBACK_SPEC, run: appsRollback },
|
|
1412
|
+
refresh: { spec: APPS_REFRESH_SPEC, run: appsRefresh },
|
|
1413
|
+
"terminate-op": { spec: APPS_TERMINATE_SPEC, run: appsTerminateOp },
|
|
1202
1414
|
};
|
|
1203
1415
|
export const APPS_SPECS = [
|
|
1204
1416
|
APPS_LIST_SPEC,
|
package/dist/src/commands/ctx.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import { runArgocd } from "../argocd.js";
|
|
1
|
+
import { runArgocd, argocdJson } from "../argocd.js";
|
|
2
|
+
import { addWritableContext, removeWritableContext } from "../config.js";
|
|
3
|
+
import { createInterface } from "node:readline/promises";
|
|
2
4
|
import { CONTEXT_FLAG, getString, parseArgs, } from "../args.js";
|
|
3
5
|
import { loadConfig } from "../config.js";
|
|
4
6
|
import { describeTarget, effectiveServer, listContexts, resolveTarget, validateContextName, } from "../context.js";
|
|
@@ -135,13 +137,127 @@ export async function ctxUse(args) {
|
|
|
135
137
|
await runArgocd("context use", [name]);
|
|
136
138
|
return finish([encodeObject({ context: `switched to ${name}`, server: match.server })], undefined, ["Run `argocd-axi` for a live overview of this context"]);
|
|
137
139
|
}
|
|
140
|
+
const WRITABLE_SPEC = {
|
|
141
|
+
path: "ctx writable",
|
|
142
|
+
summary: "Manage contexts allowed to perform writes",
|
|
143
|
+
positionals: [],
|
|
144
|
+
flags: [],
|
|
145
|
+
examples: ["argocd-axi ctx writable list"],
|
|
146
|
+
};
|
|
147
|
+
const WRITABLE_ADD_SPEC = {
|
|
148
|
+
path: "ctx writable add",
|
|
149
|
+
summary: "Allow writes on a context after interactive confirmation",
|
|
150
|
+
positionals: [
|
|
151
|
+
{
|
|
152
|
+
name: "context",
|
|
153
|
+
required: true,
|
|
154
|
+
description: "Configured Argo CD context",
|
|
155
|
+
},
|
|
156
|
+
],
|
|
157
|
+
flags: [],
|
|
158
|
+
examples: ["argocd-axi ctx writable add staging"],
|
|
159
|
+
};
|
|
160
|
+
const WRITABLE_REMOVE_SPEC = {
|
|
161
|
+
path: "ctx writable remove",
|
|
162
|
+
summary: "Remove write access from a context",
|
|
163
|
+
positionals: [
|
|
164
|
+
{
|
|
165
|
+
name: "context",
|
|
166
|
+
required: true,
|
|
167
|
+
description: "Configured Argo CD context",
|
|
168
|
+
},
|
|
169
|
+
],
|
|
170
|
+
flags: [],
|
|
171
|
+
examples: ["argocd-axi ctx writable remove staging"],
|
|
172
|
+
};
|
|
173
|
+
const WRITABLE_LIST_SPEC = {
|
|
174
|
+
path: "ctx writable list",
|
|
175
|
+
summary: "List contexts allowed to perform writes",
|
|
176
|
+
positionals: [],
|
|
177
|
+
flags: [],
|
|
178
|
+
examples: ["argocd-axi ctx writable list"],
|
|
179
|
+
};
|
|
180
|
+
async function writableAdd(args) {
|
|
181
|
+
const parsed = parseArgs(WRITABLE_ADD_SPEC, args);
|
|
182
|
+
const name = validateContextName(parsed.positionals[0]);
|
|
183
|
+
const listing = await listContexts();
|
|
184
|
+
if (listing.notInstalled)
|
|
185
|
+
throw listing.notInstalled;
|
|
186
|
+
if (listing.unavailable) {
|
|
187
|
+
throw axiError(`cannot list Argo CD contexts: ${listing.unavailable}`, CODES.NO_CONTEXT, LOGIN_HELP);
|
|
188
|
+
}
|
|
189
|
+
const match = listing.contexts.find((context) => context.name === name);
|
|
190
|
+
if (!match)
|
|
191
|
+
throw axiError(`Argo CD context ${name} is not configured`, CODES.CONTEXT_NOT_FOUND);
|
|
192
|
+
if (loadConfig().config.writableContexts.includes(name))
|
|
193
|
+
return finish([
|
|
194
|
+
encodeObject({
|
|
195
|
+
writableContext: `${name} is already writable (no change)`,
|
|
196
|
+
}),
|
|
197
|
+
], undefined);
|
|
198
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY)
|
|
199
|
+
throw axiError("writable context changes require an interactive terminal", CODES.REFUSED);
|
|
200
|
+
const info = await argocdJson("account get-user-info", ["-o", "json"], { context: name });
|
|
201
|
+
process.stdout.write(`Context: ${name}\nServer: ${match.server}\nUser: ${info.username ?? "unknown"}\nType ${name} to allow writes: `);
|
|
202
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
203
|
+
let answer;
|
|
204
|
+
try {
|
|
205
|
+
answer = await rl.question("");
|
|
206
|
+
}
|
|
207
|
+
finally {
|
|
208
|
+
rl.close();
|
|
209
|
+
}
|
|
210
|
+
if (answer !== name)
|
|
211
|
+
throw axiError("confirmation did not match; no changes made", CODES.REFUSED);
|
|
212
|
+
const changed = addWritableContext(name);
|
|
213
|
+
return finish([
|
|
214
|
+
encodeObject({
|
|
215
|
+
writableContext: changed
|
|
216
|
+
? `added ${name}`
|
|
217
|
+
: `${name} is already writable (no change)`,
|
|
218
|
+
}),
|
|
219
|
+
], undefined);
|
|
220
|
+
}
|
|
221
|
+
async function writableRemove(args) {
|
|
222
|
+
const parsed = parseArgs(WRITABLE_REMOVE_SPEC, args);
|
|
223
|
+
const name = validateContextName(parsed.positionals[0]);
|
|
224
|
+
const changed = removeWritableContext(name);
|
|
225
|
+
return finish([
|
|
226
|
+
encodeObject({
|
|
227
|
+
writableContext: changed
|
|
228
|
+
? `removed ${name}`
|
|
229
|
+
: `${name} is not in writable-contexts (no change)`,
|
|
230
|
+
}),
|
|
231
|
+
], undefined);
|
|
232
|
+
}
|
|
233
|
+
async function writableList(args) {
|
|
234
|
+
parseArgs(WRITABLE_LIST_SPEC, args);
|
|
235
|
+
return finish([encodeObject({ writableContexts: loadConfig().config.writableContexts })], undefined);
|
|
236
|
+
}
|
|
237
|
+
const WRITABLE_SUBCOMMANDS = {
|
|
238
|
+
add: { spec: WRITABLE_ADD_SPEC, run: writableAdd },
|
|
239
|
+
remove: { spec: WRITABLE_REMOVE_SPEC, run: writableRemove },
|
|
240
|
+
list: { spec: WRITABLE_LIST_SPEC, run: writableList },
|
|
241
|
+
};
|
|
138
242
|
const SUBCOMMANDS = {
|
|
139
243
|
use: { spec: CTX_USE_SPEC, run: ctxUse },
|
|
244
|
+
writable: {
|
|
245
|
+
spec: WRITABLE_SPEC,
|
|
246
|
+
run: (args) => dispatch("ctx writable", args, WRITABLE_SUBCOMMANDS, {
|
|
247
|
+
spec: WRITABLE_SPEC,
|
|
248
|
+
run: async () => {
|
|
249
|
+
throw validationError("expected list, add, or remove for `ctx writable`");
|
|
250
|
+
},
|
|
251
|
+
}),
|
|
252
|
+
},
|
|
140
253
|
};
|
|
141
254
|
export const CTX_SPECS = [CTX_SPEC, CTX_USE_SPEC];
|
|
142
255
|
export async function ctxCommand(args) {
|
|
143
256
|
const first = args[0];
|
|
144
|
-
if (first !== undefined &&
|
|
257
|
+
if (first !== undefined &&
|
|
258
|
+
!first.startsWith("-") &&
|
|
259
|
+
first !== "use" &&
|
|
260
|
+
first !== "writable") {
|
|
145
261
|
throw validationError(`unexpected argument ${first} for \`ctx\``, [
|
|
146
262
|
`To switch contexts, run \`argocd-axi ctx use ${first}\``,
|
|
147
263
|
`To read another context without switching, run \`argocd-axi ctx --context ${first}\``,
|
package/dist/src/config.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs";
|
|
1
|
+
import { readFileSync, writeFileSync, renameSync, chmodSync, statSync, mkdirSync, unlinkSync, } from "node:fs";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
2
3
|
import { homedir } from "node:os";
|
|
3
4
|
import { join } from "node:path";
|
|
4
|
-
import { parse } from "yaml";
|
|
5
|
+
import { parse, parseDocument } from "yaml";
|
|
5
6
|
import { CODES, axiError } from "./errors.js";
|
|
6
7
|
const KNOWN_KEYS = ["writable-contexts"];
|
|
7
8
|
export function configPath() {
|
|
@@ -49,6 +50,53 @@ export function parseConfig(path, text) {
|
|
|
49
50
|
}
|
|
50
51
|
return { writableContexts: [...new Set(writableContexts)] };
|
|
51
52
|
}
|
|
53
|
+
// ponytail: concurrent CLI writers can lose updates; serialize if concurrent config mutations become supported.
|
|
54
|
+
function updateWritableContext(name, add) {
|
|
55
|
+
const path = configPath();
|
|
56
|
+
mkdirSync(join(path, ".."), { recursive: true });
|
|
57
|
+
const loaded = loadConfig();
|
|
58
|
+
const contexts = loaded.config.writableContexts;
|
|
59
|
+
const updated = add
|
|
60
|
+
? [...new Set([...contexts, name])]
|
|
61
|
+
: contexts.filter((item) => item !== name);
|
|
62
|
+
if (updated.length === contexts.length &&
|
|
63
|
+
updated.every((item, index) => item === contexts[index]))
|
|
64
|
+
return false;
|
|
65
|
+
mkdirSync(join(loaded.path, ".."), { recursive: true });
|
|
66
|
+
const source = loaded.exists ? readFileSync(loaded.path, "utf8") : "";
|
|
67
|
+
const doc = parseDocument(source || "{}");
|
|
68
|
+
if (doc.toJS() === null)
|
|
69
|
+
doc.contents = null;
|
|
70
|
+
const root = doc.contents;
|
|
71
|
+
if (root !== null && (root === undefined || !("set" in root)))
|
|
72
|
+
throw invalid(loaded.path, "the top level must be a mapping");
|
|
73
|
+
doc.set("writable-contexts", updated);
|
|
74
|
+
const temp = `${loaded.path}.${randomUUID()}.tmp`;
|
|
75
|
+
try {
|
|
76
|
+
writeFileSync(temp, doc.toString(), {
|
|
77
|
+
mode: loaded.exists ? statSync(loaded.path).mode & 0o777 : 0o600,
|
|
78
|
+
});
|
|
79
|
+
if (loaded.exists)
|
|
80
|
+
chmodSync(temp, statSync(loaded.path).mode & 0o777);
|
|
81
|
+
renameSync(temp, loaded.path);
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
try {
|
|
86
|
+
unlinkSync(temp);
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
/* no temporary file */
|
|
90
|
+
}
|
|
91
|
+
throw error;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
export function addWritableContext(name) {
|
|
95
|
+
return updateWritableContext(name, true);
|
|
96
|
+
}
|
|
97
|
+
export function removeWritableContext(name) {
|
|
98
|
+
return updateWritableContext(name, false);
|
|
99
|
+
}
|
|
52
100
|
export function loadConfig() {
|
|
53
101
|
const path = configPath();
|
|
54
102
|
let text;
|
package/dist/src/gate.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {} from "./config.js";
|
|
2
2
|
import { validateContextName } from "./context.js";
|
|
3
3
|
import { CODES, axiError, validationError } from "./errors.js";
|
|
4
4
|
/**
|
|
@@ -37,7 +37,7 @@ export function evaluateWriteGate(request, config) {
|
|
|
37
37
|
}
|
|
38
38
|
const context = validateContextName(request.context);
|
|
39
39
|
const writable = isWritableContext(config, context);
|
|
40
|
-
const allowHint = `A human
|
|
40
|
+
const allowHint = `A human can allow writes by running \`argocd-axi ctx writable add ${context}\` in a terminal`;
|
|
41
41
|
if (!request.execute) {
|
|
42
42
|
return {
|
|
43
43
|
mode: "preview",
|
package/dist/src/help.js
CHANGED
|
@@ -13,6 +13,9 @@ export const COMMAND_LINES = [
|
|
|
13
13
|
"apps resources <app> [--problems --orphaned]: managed resources with sync and health",
|
|
14
14
|
"apps diff <app> [--revision <rev>] [--full]: what differs from Git, per resource; raw diff with --full",
|
|
15
15
|
"apps sync <app> [--confirm] [--prune] [--timeout <s>]: preview, then optionally sync one app",
|
|
16
|
+
"apps rollback <app> <history-id> [--confirm] [--timeout <s>]: preview revision, then optionally roll back",
|
|
17
|
+
"apps refresh <app> [--hard] [--confirm]: preview, then optionally refresh one app",
|
|
18
|
+
"apps terminate-op <app> [--confirm] [--timeout <s>]: preview, then optionally stop a running operation",
|
|
16
19
|
"apps history <app>: deployments, newest first",
|
|
17
20
|
"apps manifests <app> [--source git|live] [--kind <k>] [--full]: rendered manifests, Secret data redacted",
|
|
18
21
|
"apps logs <app> [--kind --name --tail --since --filter -p]: recent pod logs, never follows",
|
|
@@ -39,7 +42,7 @@ export const TOP_HELP = `${renderBlocks([
|
|
|
39
42
|
"argocd-axi apps diff payments-api",
|
|
40
43
|
]),
|
|
41
44
|
renderHelpBlock("notes", [
|
|
42
|
-
"Read-only except opt-in writes on writable contexts: apps sync
|
|
45
|
+
"Read-only except opt-in writes on writable contexts: apps sync, rollback, refresh and terminate-op require --confirm; sync never prunes unless --prune is given",
|
|
43
46
|
"Output is TOON on stdout, errors included; exit 0 = success (an unhealthy app is data, not an error), 1 = error, 2 = usage",
|
|
44
47
|
"Secret-looking values are redacted and counted; no flag reveals them",
|
|
45
48
|
"Uses your existing argocd CLI session or ARGOCD_AUTH_TOKEN; never logs in, never reads the argocd config file",
|
package/dist/src/registry.js
CHANGED
|
@@ -32,6 +32,24 @@ export const OPERATIONS = {
|
|
|
32
32
|
network: true,
|
|
33
33
|
timeoutMs: READ_TIMEOUT_MS,
|
|
34
34
|
},
|
|
35
|
+
"app rollback": {
|
|
36
|
+
argv: ["app", "rollback"],
|
|
37
|
+
tier: "write",
|
|
38
|
+
network: true,
|
|
39
|
+
timeoutMs: READ_TIMEOUT_MS,
|
|
40
|
+
},
|
|
41
|
+
"app terminate-op": {
|
|
42
|
+
argv: ["app", "terminate-op"],
|
|
43
|
+
tier: "write",
|
|
44
|
+
network: true,
|
|
45
|
+
timeoutMs: READ_TIMEOUT_MS,
|
|
46
|
+
},
|
|
47
|
+
"app refresh": {
|
|
48
|
+
argv: ["app", "get"],
|
|
49
|
+
tier: "write",
|
|
50
|
+
network: true,
|
|
51
|
+
timeoutMs: READ_TIMEOUT_MS,
|
|
52
|
+
},
|
|
35
53
|
"app resources": {
|
|
36
54
|
argv: ["app", "resources"],
|
|
37
55
|
tier: "read",
|
|
@@ -178,27 +196,6 @@ const PLANNED = "planned for the write tier, behind preview-by-default and the w
|
|
|
178
196
|
* answer REFUSED with the raw argocd route a human can take today.
|
|
179
197
|
*/
|
|
180
198
|
export const PLANNED_WRITES = {
|
|
181
|
-
"apps rollback": {
|
|
182
|
-
reason: `apps rollback is ${PLANNED}`,
|
|
183
|
-
help: [
|
|
184
|
-
"List deployable history ids with `argocd-axi apps history <app>`",
|
|
185
|
-
"A human can roll back with the raw CLI: `argocd app rollback <app> <history-id>`",
|
|
186
|
-
],
|
|
187
|
-
},
|
|
188
|
-
"apps terminate-op": {
|
|
189
|
-
reason: `apps terminate-op is ${PLANNED}`,
|
|
190
|
-
help: [
|
|
191
|
-
"Inspect the running operation with `argocd-axi apps view <app>`",
|
|
192
|
-
"A human can stop it with the raw CLI: `argocd app terminate-op <app>`",
|
|
193
|
-
],
|
|
194
|
-
},
|
|
195
|
-
"apps refresh": {
|
|
196
|
-
reason: `apps refresh triggers a reconcile, so it counts as a write; it is ${PLANNED}`,
|
|
197
|
-
help: [
|
|
198
|
-
"Read the last reconciled state with `argocd-axi apps view <app>`",
|
|
199
|
-
"A human can refresh with the raw CLI: `argocd app get <app> --refresh`",
|
|
200
|
-
],
|
|
201
|
-
},
|
|
202
199
|
"apps action": {
|
|
203
200
|
reason: `running resource actions is ${PLANNED}`,
|
|
204
201
|
help: [
|
package/dist/src/skill.js
CHANGED
|
@@ -6,7 +6,7 @@ import { COMMAND_LINES, DESCRIPTION } from "./help.js";
|
|
|
6
6
|
* is stale.
|
|
7
7
|
*/
|
|
8
8
|
export const SKILL_DESCRIPTION = "Inspect Argo CD through the argocd-axi CLI - which applications are OutOfSync, Degraded or failing to sync and why, " +
|
|
9
|
-
"per-resource diffs against Git, deployment history, pod logs, project sync windows and RBAC checks, plus confirmed app
|
|
9
|
+
"per-resource diffs against Git, deployment history, pod logs, project sync windows and RBAC checks, plus confirmed app sync, rollback, refresh, and operation termination on writable contexts. " +
|
|
10
10
|
"Use whenever a task involves Argo CD or GitOps deployments: an app stuck or unhealthy, a sync that failed, " +
|
|
11
11
|
"what changed in a rollout, or whether a sync is allowed right now.";
|
|
12
12
|
export function createSkillMarkdown() {
|
|
@@ -28,12 +28,12 @@ argocd-axi wraps the \`argocd\` CLI, which must be on PATH and already logged in
|
|
|
28
28
|
It uses that session as is and adds no auth of its own; it never logs in, never prompts, and never reads the argocd config file.
|
|
29
29
|
Run \`npx -y argocd-axi auth guide\` for setup instructions; the README is the authority for credential setup. The tool never logs in. If a command answers \`AUTH_EXPIRED\`, show the exact operator login command when the server is known; otherwise show the fallback login syntax and ask for the server address. An unreachable server is a separate error.
|
|
30
30
|
|
|
31
|
-
It
|
|
31
|
+
It supports opt-in writes on contexts listed in \`writable-contexts\`: \`apps sync\`, \`apps rollback\`, \`apps refresh\`, and \`apps terminate-op\` preview first and require \`--confirm\` to act. Sync pruning also requires \`--prune\`. Other writes answer \`REFUSED\`.
|
|
32
32
|
For anything argocd-axi does not cover, the raw \`argocd\` CLI is the fallback.
|
|
33
33
|
|
|
34
34
|
## Setup
|
|
35
35
|
|
|
36
|
-
Run \`npx -y argocd-axi auth guide\` for offline instructions. A human operator logs in interactively with \`argocd login <server> --sso --grpc-web\`; agents and CI use \`ARGOCD_SERVER\` and \`ARGOCD_AUTH_TOKEN\` from a scoped account token. The user owns login; argocd-axi never logs in. Verify access with \`npx -y argocd-axi whoami\`.
|
|
36
|
+
Run \`npx -y argocd-axi auth guide\` for offline instructions. A human operator logs in interactively with \`argocd login <server> --sso --grpc-web\`; agents and CI use \`ARGOCD_SERVER\` and \`ARGOCD_AUTH_TOKEN\` from a scoped account token. The user owns login; argocd-axi never logs in. Verify access with \`npx -y argocd-axi whoami\`. To enable writes on a context, a human runs \`argocd-axi ctx writable add <context>\` in a terminal and confirms the context name.
|
|
37
37
|
|
|
38
38
|
## Workflow
|
|
39
39
|
|
package/dist/src/toon.js
CHANGED
|
@@ -20,7 +20,7 @@ export function encodeObject(value) {
|
|
|
20
20
|
}
|
|
21
21
|
/** Render next-step suggestions as a TOON list. */
|
|
22
22
|
export function renderHelp(lines) {
|
|
23
|
-
return encodeObject({ help: lines });
|
|
23
|
+
return lines.length === 0 ? "" : encodeObject({ help: lines });
|
|
24
24
|
}
|
|
25
25
|
/** Render help sections as TOON list items. */
|
|
26
26
|
export function renderHelpBlock(label, lines) {
|
package/dist/src/version.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: argocd-axi
|
|
3
|
-
description: "Inspect Argo CD through the argocd-axi CLI - which applications are OutOfSync, Degraded or failing to sync and why, per-resource diffs against Git, deployment history, pod logs, project sync windows and RBAC checks, plus confirmed app
|
|
3
|
+
description: "Inspect Argo CD through the argocd-axi CLI - which applications are OutOfSync, Degraded or failing to sync and why, per-resource diffs against Git, deployment history, pod logs, project sync windows and RBAC checks, plus confirmed app sync, rollback, refresh, and operation termination on writable contexts. Use whenever a task involves Argo CD or GitOps deployments: an app stuck or unhealthy, a sync that failed, what changed in a rollout, or whether a sync is allowed right now."
|
|
4
4
|
user-invocable: false
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -15,12 +15,12 @@ argocd-axi wraps the `argocd` CLI, which must be on PATH and already logged in (
|
|
|
15
15
|
It uses that session as is and adds no auth of its own; it never logs in, never prompts, and never reads the argocd config file.
|
|
16
16
|
Run `npx -y argocd-axi auth guide` for setup instructions; the README is the authority for credential setup. The tool never logs in. If a command answers `AUTH_EXPIRED`, show the exact operator login command when the server is known; otherwise show the fallback login syntax and ask for the server address. An unreachable server is a separate error.
|
|
17
17
|
|
|
18
|
-
It
|
|
18
|
+
It supports opt-in writes on contexts listed in `writable-contexts`: `apps sync`, `apps rollback`, `apps refresh`, and `apps terminate-op` preview first and require `--confirm` to act. Sync pruning also requires `--prune`. Other writes answer `REFUSED`.
|
|
19
19
|
For anything argocd-axi does not cover, the raw `argocd` CLI is the fallback.
|
|
20
20
|
|
|
21
21
|
## Setup
|
|
22
22
|
|
|
23
|
-
Run `npx -y argocd-axi auth guide` for offline instructions. A human operator logs in interactively with `argocd login <server> --sso --grpc-web`; agents and CI use `ARGOCD_SERVER` and `ARGOCD_AUTH_TOKEN` from a scoped account token. The user owns login; argocd-axi never logs in. Verify access with `npx -y argocd-axi whoami`.
|
|
23
|
+
Run `npx -y argocd-axi auth guide` for offline instructions. A human operator logs in interactively with `argocd login <server> --sso --grpc-web`; agents and CI use `ARGOCD_SERVER` and `ARGOCD_AUTH_TOKEN` from a scoped account token. The user owns login; argocd-axi never logs in. Verify access with `npx -y argocd-axi whoami`. To enable writes on a context, a human runs `argocd-axi ctx writable add <context>` in a terminal and confirms the context name.
|
|
24
24
|
|
|
25
25
|
## Workflow
|
|
26
26
|
|
|
@@ -34,7 +34,7 @@ Run `npx -y argocd-axi auth guide` for offline instructions. A human operator lo
|
|
|
34
34
|
## Commands
|
|
35
35
|
|
|
36
36
|
```
|
|
37
|
-
commands[
|
|
37
|
+
commands[23]:
|
|
38
38
|
(no command): live overview - context, server, user, apps needing attention with the reason, running operations
|
|
39
39
|
ctx [--context <c>]: current context, server and CLI version, local only (what the session hook prints)
|
|
40
40
|
ctx use <context>: switch the argocd CLI's current context (local config only)
|
|
@@ -46,6 +46,9 @@ commands[20]:
|
|
|
46
46
|
apps resources <app> [--problems --orphaned]: managed resources with sync and health
|
|
47
47
|
apps diff <app> [--revision <rev>] [--full]: what differs from Git, per resource; raw diff with --full
|
|
48
48
|
apps sync <app> [--confirm] [--prune] [--timeout <s>]: preview, then optionally sync one app
|
|
49
|
+
apps rollback <app> <history-id> [--confirm] [--timeout <s>]: preview revision, then optionally roll back
|
|
50
|
+
apps refresh <app> [--hard] [--confirm]: preview, then optionally refresh one app
|
|
51
|
+
apps terminate-op <app> [--confirm] [--timeout <s>]: preview, then optionally stop a running operation
|
|
49
52
|
apps history <app>: deployments, newest first
|
|
50
53
|
apps manifests <app> [--source git|live] [--kind <k>] [--full]: rendered manifests, Secret data redacted
|
|
51
54
|
apps logs <app> [--kind --name --tail --since --filter -p]: recent pod logs, never follows
|