argocd-axi 0.1.4 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -12
- 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 +406 -15
- package/dist/src/commands/ctx.js +169 -2
- package/dist/src/config.js +50 -2
- package/dist/src/gate.js +2 -2
- package/dist/src/help.js +7 -2
- package/dist/src/registry.js +31 -31
- 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 +10 -5
package/README.md
CHANGED
|
@@ -48,8 +48,8 @@ argocd-axi turns that into small, definitive answers:
|
|
|
48
48
|
the raw text is one `--full` away.
|
|
49
49
|
- **Opt-in writes.** `apps sync <app>` previews the per-resource diff and
|
|
50
50
|
makes no change unless `--confirm` is passed. It never prunes unless
|
|
51
|
-
`--prune` is explicit
|
|
52
|
-
|
|
51
|
+
`--prune` is explicit. Resource actions and automated-sync changes also
|
|
52
|
+
preview by default and execute only on configured writable contexts.
|
|
53
53
|
- **Safe output.** Secret data, passwords, tokens and sensitive-looking Helm
|
|
54
54
|
parameters are redacted and counted. No flag reveals them.
|
|
55
55
|
- **Never hangs.** Nothing streams or follows; waits are bounded; no prompt can
|
|
@@ -146,11 +146,13 @@ argocd-axi apps view <app> [--full]
|
|
|
146
146
|
argocd-axi apps resources <app> [--problems | --orphaned]
|
|
147
147
|
argocd-axi apps diff <app> [--revision <rev>] [--full]
|
|
148
148
|
argocd-axi apps sync <app> [--confirm] [--prune] [--timeout <s>] --context <c>
|
|
149
|
+
argocd-axi apps auto-sync <app> <on|off> [--confirm] --context <c>
|
|
149
150
|
argocd-axi apps history <app> [--limit <n>]
|
|
150
151
|
argocd-axi apps manifests <app> [--source git|live] [--revision <rev>] [--kind <k>] [--full]
|
|
151
152
|
argocd-axi apps logs <app> [--kind <k> --name <n>] [--tail 100] [--since 15m] [--filter <s>] [--full] [-p]
|
|
152
153
|
argocd-axi apps wait <app> [--sync --health --operation] [--timeout 120]
|
|
153
154
|
argocd-axi apps actions <app> [--kind <k>]
|
|
155
|
+
argocd-axi apps actions run <app> <action> --kind <k> --name <resource> [--confirm] --context <c>
|
|
154
156
|
argocd-axi projects | projects view <p> | projects windows <p>
|
|
155
157
|
argocd-axi appsets | appsets view <name> | appsets generate <file>
|
|
156
158
|
argocd-axi repos | clusters
|
|
@@ -219,9 +221,7 @@ specific app.
|
|
|
219
221
|
local-file and delete flags whatever a command passes, gives argocd no
|
|
220
222
|
stdin, and enforces a timeout and an output cap. Tests fail if a new
|
|
221
223
|
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.
|
|
224
|
+
- **Opt-in Application writes.** Sync, rollback, refresh, terminate-op, resource actions and automated-sync changes 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
225
|
- **Redaction.** Kubernetes Secret `data`, `stringData` and their
|
|
226
226
|
last-applied annotation, values under sensitive-looking keys, and env-var or
|
|
227
227
|
Helm-parameter pairs with sensitive-looking names are replaced with
|
|
@@ -232,13 +232,12 @@ specific app.
|
|
|
232
232
|
reach them.
|
|
233
233
|
- **Writable contexts.** argocd-axi's own config file,
|
|
234
234
|
`~/.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
|
-
|
|
240
|
-
|
|
241
|
-
```
|
|
235
|
+
holds the allowlist of contexts where Application writes may execute.
|
|
236
|
+
It defaults to none. To allow writes, a human runs
|
|
237
|
+
`argocd-axi ctx writable add` in a terminal to select a configured context,
|
|
238
|
+
or passes `<context>` directly; either path requires an explicit y/yes
|
|
239
|
+
confirmation. Non-interactive runs are refused. Use
|
|
240
|
+
`argocd-axi ctx writable remove <context>` to revoke access.
|
|
242
241
|
|
|
243
242
|
- **Version cache.** The server version is checked at most once every 12
|
|
244
243
|
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,
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { AxiError } from "axi-sdk-js";
|
|
2
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
2
3
|
import { parseAllDocuments, stringify } from "yaml";
|
|
3
4
|
import { argocdJson, runArgocd } from "../argocd.js";
|
|
4
5
|
import { loadConfig } from "../config.js";
|
|
5
|
-
import { evaluateWriteGate } from "../gate.js";
|
|
6
|
+
import { attributionArgs, evaluateWriteGate } from "../gate.js";
|
|
6
7
|
import { CONTEXT_FLAG, getBool, getInt, getList, getString, parseArgs, parseDurationSeconds, validateName, } from "../args.js";
|
|
7
8
|
import { CODES, axiError, mapArgocdFailure, validationError, } from "../errors.js";
|
|
8
9
|
import { appName, appSources, attentionReasonPreview, attentionReasons, autoSyncSummary, destinationSummary, displayNames, formatAge, healthStatus, isOperationRunning, isResourceProblem, isUnhealthy, needsAttention, shortRevision, sortApps, syncRevision, syncStatus, plural, truncate, } from "../model.js";
|
|
@@ -550,7 +551,7 @@ export function parseDiffHeaders(text) {
|
|
|
550
551
|
}
|
|
551
552
|
return rows;
|
|
552
553
|
}
|
|
553
|
-
export async function appsDiff(args) {
|
|
554
|
+
export async function appsDiff(args, includeHelp = true) {
|
|
554
555
|
const parsed = parseArgs(APPS_DIFF_SPEC, args);
|
|
555
556
|
const name = validateName("app", parsed.positionals[0]);
|
|
556
557
|
const revision = getString(parsed, "--revision");
|
|
@@ -584,9 +585,11 @@ export async function appsDiff(args) {
|
|
|
584
585
|
count: `${plural(rows.length, "resource")} ${rows.length === 1 ? "differs" : "differ"}`,
|
|
585
586
|
resources: redactor.deep(rows),
|
|
586
587
|
}),
|
|
587
|
-
], redactor,
|
|
588
|
-
|
|
589
|
-
|
|
588
|
+
], redactor, includeHelp
|
|
589
|
+
? [
|
|
590
|
+
`Run \`argocd-axi apps diff ${name} --full${scope}\` to see the line-level diff`,
|
|
591
|
+
]
|
|
592
|
+
: []);
|
|
590
593
|
}
|
|
591
594
|
const argv = [name, "--exit-code=false"];
|
|
592
595
|
if (revision !== undefined)
|
|
@@ -617,7 +620,7 @@ export async function appsDiff(args) {
|
|
|
617
620
|
const lines = redactor.text(text).split("\n");
|
|
618
621
|
blocks.push(renderTextBlock("diff", lines));
|
|
619
622
|
}
|
|
620
|
-
return finish(blocks, redactor, full
|
|
623
|
+
return finish(blocks, redactor, full || !includeHelp
|
|
621
624
|
? []
|
|
622
625
|
: [
|
|
623
626
|
`Run \`argocd-axi apps diff ${name}${revision ? ` --revision ${revision}` : ""} --full${scope}\` to see the line-level diff`,
|
|
@@ -1055,7 +1058,7 @@ export async function appsSync(args) {
|
|
|
1055
1058
|
executeFlag: "--confirm",
|
|
1056
1059
|
}, loadConfig().config);
|
|
1057
1060
|
const options = { context: gate.context };
|
|
1058
|
-
const diff = await appsDiff([name, "--context", gate.context]);
|
|
1061
|
+
const diff = await appsDiff([name, "--context", gate.context], false);
|
|
1059
1062
|
const app = await getApp(name, options);
|
|
1060
1063
|
const project = validateName("project", app.spec?.project ?? "default");
|
|
1061
1064
|
const windows = (await argocdJson("proj windows list", [project, "-o", "json"], options)) ?? [];
|
|
@@ -1147,9 +1150,7 @@ export const APPS_ACTIONS_SPEC = {
|
|
|
1147
1150
|
"argocd-axi apps actions guestbook",
|
|
1148
1151
|
"argocd-axi apps actions guestbook --kind Rollout",
|
|
1149
1152
|
],
|
|
1150
|
-
notes: [
|
|
1151
|
-
"Running an action is planned for a later write tier; this command only lists them",
|
|
1152
|
-
],
|
|
1153
|
+
notes: ["List actions before running one with `apps actions run`"],
|
|
1153
1154
|
};
|
|
1154
1155
|
function pick(record, key) {
|
|
1155
1156
|
const found = Object.keys(record).find((k) => k.toLowerCase() === key);
|
|
@@ -1157,7 +1158,7 @@ function pick(record, key) {
|
|
|
1157
1158
|
}
|
|
1158
1159
|
export async function appsActions(args) {
|
|
1159
1160
|
if (args[0] === "run")
|
|
1160
|
-
|
|
1161
|
+
return appsActionRun(args.slice(1));
|
|
1161
1162
|
const parsed = parseArgs(APPS_ACTIONS_SPEC, args);
|
|
1162
1163
|
const name = validateName("app", parsed.positionals[0]);
|
|
1163
1164
|
const argv = [name, "-o", "json"];
|
|
@@ -1183,10 +1184,342 @@ export async function appsActions(args) {
|
|
|
1183
1184
|
}));
|
|
1184
1185
|
return finish([encodeObject({ count: plural(rows.length, "action"), actions: rows })], undefined);
|
|
1185
1186
|
}
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1187
|
+
const APPS_ACTION_RUN_SPEC = {
|
|
1188
|
+
path: "apps actions run",
|
|
1189
|
+
summary: "Preview and optionally run one action on one resource",
|
|
1190
|
+
positionals: [
|
|
1191
|
+
APP_ARG,
|
|
1192
|
+
{ name: "action", required: true, description: "Listed resource action" },
|
|
1193
|
+
],
|
|
1194
|
+
flags: [
|
|
1195
|
+
{
|
|
1196
|
+
name: "--kind",
|
|
1197
|
+
kind: "value",
|
|
1198
|
+
valueName: "kind",
|
|
1199
|
+
description: "Resource kind",
|
|
1200
|
+
},
|
|
1201
|
+
{
|
|
1202
|
+
name: "--name",
|
|
1203
|
+
kind: "value",
|
|
1204
|
+
valueName: "resource",
|
|
1205
|
+
description: "Resource name",
|
|
1206
|
+
},
|
|
1207
|
+
{
|
|
1208
|
+
name: "--group",
|
|
1209
|
+
kind: "value",
|
|
1210
|
+
valueName: "group",
|
|
1211
|
+
description: "Resource API group when needed to disambiguate",
|
|
1212
|
+
},
|
|
1213
|
+
{
|
|
1214
|
+
name: "--namespace",
|
|
1215
|
+
kind: "value",
|
|
1216
|
+
valueName: "namespace",
|
|
1217
|
+
description: "Resource namespace when needed to disambiguate",
|
|
1218
|
+
},
|
|
1219
|
+
{ name: "--confirm", kind: "bool", description: "Execute after preview" },
|
|
1220
|
+
{
|
|
1221
|
+
name: "--timeout",
|
|
1222
|
+
kind: "value",
|
|
1223
|
+
valueName: "s",
|
|
1224
|
+
description: "Maximum wait in seconds (default 120)",
|
|
1225
|
+
},
|
|
1226
|
+
CONTEXT_FLAG,
|
|
1227
|
+
],
|
|
1228
|
+
examples: [
|
|
1229
|
+
"argocd-axi apps actions run guestbook restart --kind Deployment --name guestbook --context staging",
|
|
1230
|
+
],
|
|
1231
|
+
};
|
|
1232
|
+
async function appsActionRun(args) {
|
|
1233
|
+
const parsed = parseArgs(APPS_ACTION_RUN_SPEC, args);
|
|
1234
|
+
const appNameArg = validateName("app", parsed.positionals[0]);
|
|
1235
|
+
const action = validateName("action", parsed.positionals[1]);
|
|
1236
|
+
const kind = getString(parsed, "--kind");
|
|
1237
|
+
const resourceName = getString(parsed, "--name");
|
|
1238
|
+
const group = getString(parsed, "--group");
|
|
1239
|
+
const namespace = getString(parsed, "--namespace");
|
|
1240
|
+
const timeout = getInt(parsed, "--timeout", WRITE_TIMEOUT);
|
|
1241
|
+
if (!kind || !resourceName)
|
|
1242
|
+
throw validationError("apps actions run needs --kind and --name");
|
|
1243
|
+
const context = getString(parsed, "--context"), confirm = getBool(parsed, "--confirm");
|
|
1244
|
+
const gate = evaluateWriteGate({
|
|
1245
|
+
command: "apps actions run",
|
|
1246
|
+
context,
|
|
1247
|
+
execute: confirm,
|
|
1248
|
+
executeFlag: "--confirm",
|
|
1249
|
+
}, loadConfig().config);
|
|
1250
|
+
const options = { context: gate.context };
|
|
1251
|
+
const app = await getApp(appNameArg, options);
|
|
1252
|
+
const resources = (app.status?.resources ?? []).filter((r) => r.kind === kind &&
|
|
1253
|
+
r.name === resourceName &&
|
|
1254
|
+
(group === undefined || r.group === group) &&
|
|
1255
|
+
(namespace === undefined || r.namespace === namespace));
|
|
1256
|
+
const resource = resources[0];
|
|
1257
|
+
if (!resource)
|
|
1258
|
+
throw axiError(`REFUSED: ${kind}/${resourceName} is not managed by ${appNameArg}`, CODES.REFUSED);
|
|
1259
|
+
if (resources.length > 1)
|
|
1260
|
+
throw axiError(`REFUSED: ${kind}/${resourceName} is ambiguous; candidates: ${resources
|
|
1261
|
+
.map((r) => `${r.group ?? "core"}/${r.kind} ${r.namespace ?? "_cluster"}/${r.name}`)
|
|
1262
|
+
.join(", ")}. Specify --group and/or --namespace`, CODES.REFUSED);
|
|
1263
|
+
const actions = await argocdJson("app actions list", [appNameArg, "--kind", kind, "-o", "json"], options);
|
|
1264
|
+
const matchingActions = actions.filter((entry) => String(pick(entry, "name") ?? "") === resourceName &&
|
|
1265
|
+
String(pick(entry, "group") ?? "") === (resource.group ?? "") &&
|
|
1266
|
+
String(pick(entry, "kind") ?? "") === kind &&
|
|
1267
|
+
String(pick(entry, "action") ?? "") === action);
|
|
1268
|
+
const namespacedActions = matchingActions.filter((entry) => pick(entry, "namespace") !== undefined);
|
|
1269
|
+
const unnamespacedAction = matchingActions.length === 1 ? matchingActions[0] : undefined;
|
|
1270
|
+
const actionAvailable = namespacedActions.length
|
|
1271
|
+
? namespacedActions.some((entry) => pick(entry, "namespace") === resource.namespace &&
|
|
1272
|
+
pick(entry, "disabled") !== true)
|
|
1273
|
+
: unnamespacedAction !== undefined &&
|
|
1274
|
+
pick(unnamespacedAction, "disabled") !== true;
|
|
1275
|
+
if (!actionAvailable)
|
|
1276
|
+
throw axiError(`REFUSED: action ${action} is unavailable for ${kind}/${resourceName}`, CODES.REFUSED);
|
|
1277
|
+
const blocks = [
|
|
1278
|
+
encodeObject({ action, app: appNameArg, kind, resource: resourceName }),
|
|
1279
|
+
];
|
|
1280
|
+
if (!confirm)
|
|
1281
|
+
return finish(blocks, undefined, [
|
|
1282
|
+
"No changes made; add --confirm to run the action",
|
|
1283
|
+
...(gate.mode === "preview" ? gate.help : []),
|
|
1284
|
+
]);
|
|
1285
|
+
await checkWritePermission(`action/${resource.group ?? ""}/${kind}/${action}`, app, appNameArg, options);
|
|
1286
|
+
await runApplicationWrite("app actions run", [
|
|
1287
|
+
appNameArg,
|
|
1288
|
+
action,
|
|
1289
|
+
"--kind",
|
|
1290
|
+
kind,
|
|
1291
|
+
"--resource-name",
|
|
1292
|
+
resourceName,
|
|
1293
|
+
...(resource.group ? ["--group", resource.group] : []),
|
|
1294
|
+
...(resource.namespace ? ["--namespace", resource.namespace] : []),
|
|
1295
|
+
...attributionArgs(),
|
|
1296
|
+
], options, appNameArg);
|
|
1297
|
+
const current = await waitForAppState(appNameArg, timeout, options, (fresh) => fresh.status?.resources?.some((r) => r.group === resource.group &&
|
|
1298
|
+
r.kind === resource.kind &&
|
|
1299
|
+
r.namespace === resource.namespace &&
|
|
1300
|
+
r.name === resource.name &&
|
|
1301
|
+
r.health?.status === "Healthy") ?? false);
|
|
1302
|
+
const state = current.status?.resources?.find((r) => r.group === resource.group &&
|
|
1303
|
+
r.kind === resource.kind &&
|
|
1304
|
+
r.namespace === resource.namespace &&
|
|
1305
|
+
r.name === resource.name)?.health?.status ?? "unknown";
|
|
1306
|
+
return finish([...blocks, encodeObject({ result: "submitted", state })], undefined);
|
|
1307
|
+
}
|
|
1308
|
+
// ---------------------------------------------------------------- app writes
|
|
1309
|
+
function writeSpec(path, summary, positionals) {
|
|
1310
|
+
return {
|
|
1311
|
+
path,
|
|
1312
|
+
summary,
|
|
1313
|
+
positionals,
|
|
1314
|
+
flags: [
|
|
1315
|
+
{ name: "--confirm", kind: "bool", description: "Execute after preview" },
|
|
1316
|
+
...(path === "apps refresh"
|
|
1317
|
+
? []
|
|
1318
|
+
: [
|
|
1319
|
+
{
|
|
1320
|
+
name: "--timeout",
|
|
1321
|
+
kind: "value",
|
|
1322
|
+
valueName: "s",
|
|
1323
|
+
description: "Maximum wait in seconds (default 120)",
|
|
1324
|
+
},
|
|
1325
|
+
]),
|
|
1326
|
+
...(path === "apps refresh"
|
|
1327
|
+
? [
|
|
1328
|
+
{
|
|
1329
|
+
name: "--hard",
|
|
1330
|
+
kind: "bool",
|
|
1331
|
+
description: "Hard refresh, bypassing the cache",
|
|
1332
|
+
},
|
|
1333
|
+
]
|
|
1334
|
+
: []),
|
|
1335
|
+
CONTEXT_FLAG,
|
|
1336
|
+
],
|
|
1337
|
+
examples: [
|
|
1338
|
+
`argocd-axi ${path} ${path === "apps rollback" ? "guestbook 3" : "guestbook"} --context staging`,
|
|
1339
|
+
],
|
|
1340
|
+
};
|
|
1341
|
+
}
|
|
1342
|
+
const WRITE_TIMEOUT = { default: 120, min: 1, max: 3600 };
|
|
1343
|
+
async function checkWritePermission(action, app, name, options) {
|
|
1344
|
+
const permission = await runArgocd("account can-i", [action, "applications", `${app.spec?.project ?? "default"}/${name}`], { ...options, allowFailure: true });
|
|
1345
|
+
if (permission.exitCode !== 0 || !/^yes\s*$/i.test(permission.stdout.trim()))
|
|
1346
|
+
throw axiError(`REFUSED: Argo CD RBAC denies ${action} for ${name}`, CODES.REFUSED);
|
|
1347
|
+
}
|
|
1348
|
+
async function runApplicationWrite(op, args, options, name) {
|
|
1349
|
+
try {
|
|
1350
|
+
await runArgocd(op, args, options);
|
|
1351
|
+
}
|
|
1352
|
+
catch (error) {
|
|
1353
|
+
if (error instanceof AxiError && error.code === CODES.FORBIDDEN)
|
|
1354
|
+
throw axiError(`REFUSED: Argo CD RBAC denied ${op.slice(4)} for ${name}`, CODES.REFUSED);
|
|
1355
|
+
throw error;
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
function writeOutcome(phase, timedOut, completed) {
|
|
1359
|
+
if (phase === "Failed" || phase === "Error")
|
|
1360
|
+
return "failed";
|
|
1361
|
+
if (phase === "Succeeded")
|
|
1362
|
+
return completed;
|
|
1363
|
+
if (timedOut)
|
|
1364
|
+
return "waiting";
|
|
1365
|
+
return phase ?? "unknown";
|
|
1366
|
+
}
|
|
1367
|
+
async function waitForAppState(name, timeout, options, settled) {
|
|
1368
|
+
const deadline = Date.now() + timeout * 1000;
|
|
1369
|
+
let app = await getApp(name, options);
|
|
1370
|
+
while (!settled(app) && Date.now() < deadline) {
|
|
1371
|
+
await delay(Math.min(1000, Math.max(0, deadline - Date.now())));
|
|
1372
|
+
app = await getApp(name, options);
|
|
1373
|
+
}
|
|
1374
|
+
return app;
|
|
1375
|
+
}
|
|
1376
|
+
async function waitForWrite(name, timeout, options) {
|
|
1377
|
+
try {
|
|
1378
|
+
const result = await runArgocd("app wait", [name, "--operation", "--timeout", String(timeout)], { ...options, allowFailure: true, timeoutMs: (timeout + 10) * 1000 });
|
|
1379
|
+
if (result.exitCode !== 0) {
|
|
1380
|
+
const failure = mapArgocdFailure(result.stderr || result.stdout);
|
|
1381
|
+
if (!/timed? out|timeout/i.test(failure.message))
|
|
1382
|
+
throw failure;
|
|
1383
|
+
return { app: await getApp(name, options), timedOut: true };
|
|
1384
|
+
}
|
|
1385
|
+
return { app: await getApp(name, options), timedOut: false };
|
|
1386
|
+
}
|
|
1387
|
+
catch (error) {
|
|
1388
|
+
if (!(error instanceof AxiError) || error.code !== CODES.TIMEOUT)
|
|
1389
|
+
throw error;
|
|
1390
|
+
return { app: await getApp(name, options), timedOut: true };
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
const APPS_ROLLBACK_SPEC = writeSpec("apps rollback", "Preview and optionally roll back one Application", [
|
|
1394
|
+
APP_ARG,
|
|
1395
|
+
{
|
|
1396
|
+
name: "history-id",
|
|
1397
|
+
required: true,
|
|
1398
|
+
description: "Deployment history id",
|
|
1399
|
+
},
|
|
1400
|
+
]);
|
|
1401
|
+
export async function appsRollback(args) {
|
|
1402
|
+
const parsed = parseArgs(APPS_ROLLBACK_SPEC, args);
|
|
1403
|
+
const name = validateName("app", parsed.positionals[0]);
|
|
1404
|
+
const historyId = parsed.positionals[1];
|
|
1405
|
+
if (!/^\d+$/.test(historyId))
|
|
1406
|
+
throw validationError("history-id must be a non-negative decimal integer");
|
|
1407
|
+
const id = Number(historyId);
|
|
1408
|
+
if (!Number.isSafeInteger(id) || id < 0)
|
|
1409
|
+
throw validationError("history-id must be a non-negative integer");
|
|
1410
|
+
const context = getString(parsed, "--context"), confirm = getBool(parsed, "--confirm"), timeout = getInt(parsed, "--timeout", WRITE_TIMEOUT);
|
|
1411
|
+
const gate = evaluateWriteGate({
|
|
1412
|
+
command: "apps rollback",
|
|
1413
|
+
context,
|
|
1414
|
+
execute: confirm,
|
|
1415
|
+
executeFlag: "--confirm",
|
|
1416
|
+
}, loadConfig().config);
|
|
1417
|
+
const options = { context: gate.context }, app = await getApp(name, options);
|
|
1418
|
+
const history = app.status?.history?.find((entry) => entry.id === id);
|
|
1419
|
+
if (!history)
|
|
1420
|
+
throw axiError(`REFUSED: history id ${id} not found for ${name}`, CODES.REFUSED);
|
|
1421
|
+
if (autoSyncSummary(app) !== "manual")
|
|
1422
|
+
throw axiError(`REFUSED: disable auto-sync before rolling back ${name}`, CODES.REFUSED);
|
|
1423
|
+
const target = history.revisions?.join(", ") ?? history.revision ?? "unknown";
|
|
1424
|
+
const previewBlocks = [
|
|
1425
|
+
encodeObject({
|
|
1426
|
+
rollback: name,
|
|
1427
|
+
currentRevision: syncRevision(app),
|
|
1428
|
+
targetHistoryId: id,
|
|
1429
|
+
targetRevision: target,
|
|
1430
|
+
}),
|
|
1431
|
+
];
|
|
1432
|
+
if (!confirm)
|
|
1433
|
+
return finish(previewBlocks, undefined, [
|
|
1434
|
+
"No changes made; add --confirm to roll back",
|
|
1435
|
+
...(gate.mode === "preview" ? gate.help : []),
|
|
1436
|
+
]);
|
|
1437
|
+
await checkWritePermission("sync", app, name, options);
|
|
1438
|
+
await runApplicationWrite("app rollback", [name, String(id), "--info", "initiated-by=argocd-axi"], options, name);
|
|
1439
|
+
const current = await waitForWrite(name, timeout, options);
|
|
1440
|
+
const outcome = writeOutcome(current.app.status?.operationState?.phase, current.timedOut, "submitted");
|
|
1441
|
+
return finish([
|
|
1442
|
+
...previewBlocks,
|
|
1443
|
+
encodeObject({
|
|
1444
|
+
rollback: outcome,
|
|
1445
|
+
historyId: id,
|
|
1446
|
+
phase: current.app.status?.operationState?.phase,
|
|
1447
|
+
operation: current.app.status?.operationState,
|
|
1448
|
+
}),
|
|
1449
|
+
], undefined, outcome === "waiting" ? ["Wait again with `apps wait`"] : []);
|
|
1450
|
+
}
|
|
1451
|
+
const APPS_REFRESH_SPEC = writeSpec("apps refresh", "Preview and optionally refresh one Application", [APP_ARG]);
|
|
1452
|
+
export async function appsRefresh(args) {
|
|
1453
|
+
const parsed = parseArgs(APPS_REFRESH_SPEC, args);
|
|
1454
|
+
const name = validateName("app", parsed.positionals[0]);
|
|
1455
|
+
const context = getString(parsed, "--context"), confirm = getBool(parsed, "--confirm"), hard = getBool(parsed, "--hard");
|
|
1456
|
+
const gate = evaluateWriteGate({
|
|
1457
|
+
command: "apps refresh",
|
|
1458
|
+
context,
|
|
1459
|
+
execute: confirm,
|
|
1460
|
+
executeFlag: "--confirm",
|
|
1461
|
+
}, loadConfig().config);
|
|
1462
|
+
const options = { context: gate.context }, app = await getApp(name, options);
|
|
1463
|
+
const blocks = [
|
|
1464
|
+
encodeObject({
|
|
1465
|
+
refresh: name,
|
|
1466
|
+
mode: hard ? "hard" : "normal",
|
|
1467
|
+
reRead: hard ? "live state and source caches" : "live state",
|
|
1468
|
+
}),
|
|
1469
|
+
];
|
|
1470
|
+
if (!confirm)
|
|
1471
|
+
return finish(blocks, undefined, [
|
|
1472
|
+
"No changes made; add --confirm to refresh",
|
|
1473
|
+
...(gate.mode === "preview" ? gate.help : []),
|
|
1474
|
+
]);
|
|
1475
|
+
await checkWritePermission("update", app, name, options);
|
|
1476
|
+
await runApplicationWrite("app refresh", [name, hard ? "--hard-refresh" : "--refresh"], options, name);
|
|
1477
|
+
return finish([...blocks, encodeObject({ refresh: "submitted" })], undefined);
|
|
1478
|
+
}
|
|
1479
|
+
const APPS_TERMINATE_SPEC = writeSpec("apps terminate-op", "Preview and optionally terminate one running operation", [APP_ARG]);
|
|
1480
|
+
export async function appsTerminateOp(args) {
|
|
1481
|
+
const parsed = parseArgs(APPS_TERMINATE_SPEC, args);
|
|
1482
|
+
const name = validateName("app", parsed.positionals[0]);
|
|
1483
|
+
const context = getString(parsed, "--context"), confirm = getBool(parsed, "--confirm"), timeout = getInt(parsed, "--timeout", WRITE_TIMEOUT);
|
|
1484
|
+
const gate = evaluateWriteGate({
|
|
1485
|
+
command: "apps terminate-op",
|
|
1486
|
+
context,
|
|
1487
|
+
execute: confirm,
|
|
1488
|
+
executeFlag: "--confirm",
|
|
1489
|
+
}, loadConfig().config);
|
|
1490
|
+
const options = { context: gate.context }, app = await getApp(name, options), phase = app.status?.operationState?.phase;
|
|
1491
|
+
if (!isOperationRunning(app))
|
|
1492
|
+
return finish([
|
|
1493
|
+
encodeObject({
|
|
1494
|
+
terminateOperation: name,
|
|
1495
|
+
status: "no operation running",
|
|
1496
|
+
}),
|
|
1497
|
+
], undefined);
|
|
1498
|
+
const blocks = [
|
|
1499
|
+
encodeObject({
|
|
1500
|
+
terminateOperation: name,
|
|
1501
|
+
phase,
|
|
1502
|
+
startedAt: app.status?.operationState?.startedAt,
|
|
1503
|
+
message: app.status?.operationState?.message,
|
|
1504
|
+
}),
|
|
1505
|
+
];
|
|
1506
|
+
if (!confirm)
|
|
1507
|
+
return finish(blocks, undefined, [
|
|
1508
|
+
"No changes made; add --confirm to terminate the operation",
|
|
1509
|
+
...(gate.mode === "preview" ? gate.help : []),
|
|
1510
|
+
]);
|
|
1511
|
+
await checkWritePermission("update", app, name, options);
|
|
1512
|
+
await runApplicationWrite("app terminate-op", [name], options, name);
|
|
1513
|
+
const current = await waitForWrite(name, timeout, options);
|
|
1514
|
+
const outcome = writeOutcome(current.app.status?.operationState?.phase, current.timedOut, "submitted");
|
|
1515
|
+
return finish([
|
|
1516
|
+
...blocks,
|
|
1517
|
+
encodeObject({
|
|
1518
|
+
terminateOperation: outcome,
|
|
1519
|
+
phase: current.app.status?.operationState?.phase,
|
|
1520
|
+
operation: current.app.status?.operationState,
|
|
1521
|
+
}),
|
|
1522
|
+
], undefined, outcome === "waiting" ? ["Wait again with `apps wait`"] : []);
|
|
1190
1523
|
}
|
|
1191
1524
|
// ---------------------------------------------------------------- dispatcher
|
|
1192
1525
|
const SUBCOMMANDS = {
|
|
@@ -1199,7 +1532,65 @@ const SUBCOMMANDS = {
|
|
|
1199
1532
|
wait: { spec: APPS_WAIT_SPEC, run: appsWait },
|
|
1200
1533
|
actions: { spec: APPS_ACTIONS_SPEC, run: appsActions },
|
|
1201
1534
|
sync: { spec: APPS_SYNC_SPEC, run: appsSync },
|
|
1535
|
+
rollback: { spec: APPS_ROLLBACK_SPEC, run: appsRollback },
|
|
1536
|
+
refresh: { spec: APPS_REFRESH_SPEC, run: appsRefresh },
|
|
1537
|
+
"terminate-op": { spec: APPS_TERMINATE_SPEC, run: appsTerminateOp },
|
|
1538
|
+
};
|
|
1539
|
+
const APPS_AUTO_SYNC_SPEC = {
|
|
1540
|
+
path: "apps auto-sync",
|
|
1541
|
+
summary: "Preview and optionally enable or disable automated sync",
|
|
1542
|
+
positionals: [
|
|
1543
|
+
APP_ARG,
|
|
1544
|
+
{ name: "mode", required: true, description: "on or off" },
|
|
1545
|
+
],
|
|
1546
|
+
flags: [
|
|
1547
|
+
{ name: "--confirm", kind: "bool", description: "Execute after preview" },
|
|
1548
|
+
{
|
|
1549
|
+
name: "--timeout",
|
|
1550
|
+
kind: "value",
|
|
1551
|
+
valueName: "s",
|
|
1552
|
+
description: "Maximum wait in seconds (default 120)",
|
|
1553
|
+
},
|
|
1554
|
+
CONTEXT_FLAG,
|
|
1555
|
+
],
|
|
1556
|
+
examples: ["argocd-axi apps auto-sync guestbook on --context staging"],
|
|
1202
1557
|
};
|
|
1558
|
+
async function appsAutoSync(args) {
|
|
1559
|
+
const parsed = parseArgs(APPS_AUTO_SYNC_SPEC, args);
|
|
1560
|
+
const name = validateName("app", parsed.positionals[0]);
|
|
1561
|
+
const mode = parsed.positionals[1];
|
|
1562
|
+
if (mode !== "on" && mode !== "off")
|
|
1563
|
+
throw validationError("mode must be on or off");
|
|
1564
|
+
const context = getString(parsed, "--context"), confirm = getBool(parsed, "--confirm"), timeout = getInt(parsed, "--timeout", WRITE_TIMEOUT);
|
|
1565
|
+
const gate = evaluateWriteGate({
|
|
1566
|
+
command: "apps auto-sync",
|
|
1567
|
+
context,
|
|
1568
|
+
execute: confirm,
|
|
1569
|
+
executeFlag: "--confirm",
|
|
1570
|
+
}, loadConfig().config);
|
|
1571
|
+
const options = { context: gate.context }, app = await getApp(name, options);
|
|
1572
|
+
const current = autoSyncSummary(app), blocks = [encodeObject({ autoSync: name, current, target: mode })];
|
|
1573
|
+
if (!confirm)
|
|
1574
|
+
return finish(blocks, undefined, [
|
|
1575
|
+
"No changes made; add --confirm to change automated sync",
|
|
1576
|
+
...(gate.mode === "preview" ? gate.help : []),
|
|
1577
|
+
]);
|
|
1578
|
+
await checkWritePermission("update", app, name, options);
|
|
1579
|
+
await runApplicationWrite("app set", [
|
|
1580
|
+
name,
|
|
1581
|
+
"--sync-policy",
|
|
1582
|
+
mode === "on" ? "automated" : "none",
|
|
1583
|
+
...attributionArgs(),
|
|
1584
|
+
], options, name);
|
|
1585
|
+
const observed = await waitForAppState(name, timeout, options, (fresh) => mode === "on"
|
|
1586
|
+
? autoSyncSummary(fresh).startsWith("automated")
|
|
1587
|
+
: autoSyncSummary(fresh) === "manual");
|
|
1588
|
+
return finish([
|
|
1589
|
+
...blocks,
|
|
1590
|
+
encodeObject({ result: "submitted", state: autoSyncSummary(observed) }),
|
|
1591
|
+
], undefined);
|
|
1592
|
+
}
|
|
1593
|
+
SUBCOMMANDS["auto-sync"] = { spec: APPS_AUTO_SYNC_SPEC, run: appsAutoSync };
|
|
1203
1594
|
export const APPS_SPECS = [
|
|
1204
1595
|
APPS_LIST_SPEC,
|
|
1205
1596
|
...Object.values(SUBCOMMANDS).map((s) => s.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,178 @@ 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: false,
|
|
154
|
+
description: "Configured Argo CD context",
|
|
155
|
+
},
|
|
156
|
+
],
|
|
157
|
+
flags: [],
|
|
158
|
+
examples: [
|
|
159
|
+
"argocd-axi ctx writable add",
|
|
160
|
+
"argocd-axi ctx writable add staging",
|
|
161
|
+
],
|
|
162
|
+
};
|
|
163
|
+
const WRITABLE_REMOVE_SPEC = {
|
|
164
|
+
path: "ctx writable remove",
|
|
165
|
+
summary: "Remove write access from a context",
|
|
166
|
+
positionals: [
|
|
167
|
+
{
|
|
168
|
+
name: "context",
|
|
169
|
+
required: true,
|
|
170
|
+
description: "Configured Argo CD context",
|
|
171
|
+
},
|
|
172
|
+
],
|
|
173
|
+
flags: [],
|
|
174
|
+
examples: ["argocd-axi ctx writable remove staging"],
|
|
175
|
+
};
|
|
176
|
+
const WRITABLE_LIST_SPEC = {
|
|
177
|
+
path: "ctx writable list",
|
|
178
|
+
summary: "List contexts allowed to perform writes",
|
|
179
|
+
positionals: [],
|
|
180
|
+
flags: [],
|
|
181
|
+
examples: ["argocd-axi ctx writable list"],
|
|
182
|
+
};
|
|
183
|
+
export async function readConfirmation(input, output, prompt) {
|
|
184
|
+
const rl = createInterface({ input, output });
|
|
185
|
+
const closed = new Promise((resolve) => rl.once("close", () => resolve("")));
|
|
186
|
+
try {
|
|
187
|
+
return await Promise.race([rl.question(prompt), closed]);
|
|
188
|
+
}
|
|
189
|
+
finally {
|
|
190
|
+
rl.close();
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
export async function confirmWritableContext(name, server, input, output) {
|
|
194
|
+
const answer = await readConfirmation(input, output, `Allow argocd-axi to sync, rollback, refresh, and terminate operations on context ${name} (${server})? [y/N] `);
|
|
195
|
+
return /^y(?:es)?$/i.test(answer.trim());
|
|
196
|
+
}
|
|
197
|
+
export async function contextDetails(contexts, lookup = async (name) => (await argocdJson("account get-user-info", ["-o", "json"], { context: name, timeoutMs: 1_500 })).username) {
|
|
198
|
+
const result = [];
|
|
199
|
+
for (let offset = 0; offset < contexts.length; offset += 3) {
|
|
200
|
+
const batch = await Promise.all(contexts.slice(offset, offset + 3).map(async (context) => ({
|
|
201
|
+
...context,
|
|
202
|
+
username: (await lookup(context.name).catch(() => undefined)) ?? "unknown",
|
|
203
|
+
})));
|
|
204
|
+
result.push(...batch);
|
|
205
|
+
}
|
|
206
|
+
return result;
|
|
207
|
+
}
|
|
208
|
+
export function addWritableContextIfConfirmed(name, confirmed) {
|
|
209
|
+
return confirmed ? addWritableContext(name) : false;
|
|
210
|
+
}
|
|
211
|
+
export async function chooseContext(contexts, writable, input, output) {
|
|
212
|
+
output.write(contexts
|
|
213
|
+
.map((context, index) => `${index + 1}. ${context.name} - ${context.server} - ${context.username}${writable.includes(context.name) ? " (already writable)" : ""}`)
|
|
214
|
+
.join("\n") + "\n");
|
|
215
|
+
const selected = await readConfirmation(input, output, "Select a context by number: ");
|
|
216
|
+
const index = Number(selected);
|
|
217
|
+
const context = Number.isInteger(index) && index >= 1 ? contexts[index - 1] : undefined;
|
|
218
|
+
if (!context)
|
|
219
|
+
throw axiError("invalid context selection; no changes made", CODES.REFUSED);
|
|
220
|
+
return context.name;
|
|
221
|
+
}
|
|
222
|
+
async function writableAdd(args) {
|
|
223
|
+
const parsed = parseArgs(WRITABLE_ADD_SPEC, args);
|
|
224
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY)
|
|
225
|
+
throw axiError("writable context changes require an interactive terminal; run this command in a terminal", CODES.REFUSED);
|
|
226
|
+
const listing = await listContexts();
|
|
227
|
+
if (listing.notInstalled)
|
|
228
|
+
throw listing.notInstalled;
|
|
229
|
+
if (listing.unavailable) {
|
|
230
|
+
throw axiError(`cannot list Argo CD contexts: ${listing.unavailable}`, CODES.NO_CONTEXT, LOGIN_HELP);
|
|
231
|
+
}
|
|
232
|
+
const writable = loadConfig().config.writableContexts;
|
|
233
|
+
let name = parsed.positionals[0]
|
|
234
|
+
? validateContextName(parsed.positionals[0])
|
|
235
|
+
: undefined;
|
|
236
|
+
let username;
|
|
237
|
+
if (name === undefined) {
|
|
238
|
+
if (listing.contexts.length === 0)
|
|
239
|
+
throw axiError("no configured Argo CD contexts to select", CODES.NO_CONTEXT);
|
|
240
|
+
const details = await contextDetails(listing.contexts);
|
|
241
|
+
name = await chooseContext(details, writable, process.stdin, process.stdout);
|
|
242
|
+
username = details.find((context) => context.name === name)?.username;
|
|
243
|
+
}
|
|
244
|
+
const selectedName = name;
|
|
245
|
+
const match = listing.contexts.find((context) => context.name === selectedName);
|
|
246
|
+
if (!match)
|
|
247
|
+
throw axiError(`Argo CD context ${name} is not configured`, CODES.CONTEXT_NOT_FOUND);
|
|
248
|
+
if (writable.includes(selectedName))
|
|
249
|
+
return finish([
|
|
250
|
+
encodeObject({
|
|
251
|
+
writableContext: `${selectedName} is already writable (no change)`,
|
|
252
|
+
}),
|
|
253
|
+
], undefined);
|
|
254
|
+
username ??= (await argocdJson("account get-user-info", ["-o", "json"], { context: selectedName })).username;
|
|
255
|
+
process.stdout.write(`Context: ${selectedName}\nServer: ${match.server}\nUser: ${username ?? "unknown"}\n`);
|
|
256
|
+
const confirmed = await confirmWritableContext(selectedName, match.server, process.stdin, process.stdout);
|
|
257
|
+
const changed = addWritableContextIfConfirmed(selectedName, confirmed);
|
|
258
|
+
if (!confirmed)
|
|
259
|
+
return finish([
|
|
260
|
+
encodeObject({
|
|
261
|
+
writableContext: `${selectedName} remains read-only (no change)`,
|
|
262
|
+
}),
|
|
263
|
+
], undefined);
|
|
264
|
+
return finish([
|
|
265
|
+
encodeObject({
|
|
266
|
+
writableContext: changed
|
|
267
|
+
? `added ${selectedName}`
|
|
268
|
+
: `${selectedName} is already writable (no change)`,
|
|
269
|
+
}),
|
|
270
|
+
], undefined);
|
|
271
|
+
}
|
|
272
|
+
async function writableRemove(args) {
|
|
273
|
+
const parsed = parseArgs(WRITABLE_REMOVE_SPEC, args);
|
|
274
|
+
const name = validateContextName(parsed.positionals[0]);
|
|
275
|
+
const changed = removeWritableContext(name);
|
|
276
|
+
return finish([
|
|
277
|
+
encodeObject({
|
|
278
|
+
writableContext: changed
|
|
279
|
+
? `removed ${name}`
|
|
280
|
+
: `${name} is not in writable-contexts (no change)`,
|
|
281
|
+
}),
|
|
282
|
+
], undefined);
|
|
283
|
+
}
|
|
284
|
+
async function writableList(args) {
|
|
285
|
+
parseArgs(WRITABLE_LIST_SPEC, args);
|
|
286
|
+
return finish([encodeObject({ writableContexts: loadConfig().config.writableContexts })], undefined);
|
|
287
|
+
}
|
|
288
|
+
const WRITABLE_SUBCOMMANDS = {
|
|
289
|
+
add: { spec: WRITABLE_ADD_SPEC, run: writableAdd },
|
|
290
|
+
remove: { spec: WRITABLE_REMOVE_SPEC, run: writableRemove },
|
|
291
|
+
list: { spec: WRITABLE_LIST_SPEC, run: writableList },
|
|
292
|
+
};
|
|
138
293
|
const SUBCOMMANDS = {
|
|
139
294
|
use: { spec: CTX_USE_SPEC, run: ctxUse },
|
|
295
|
+
writable: {
|
|
296
|
+
spec: WRITABLE_SPEC,
|
|
297
|
+
run: (args) => dispatch("ctx writable", args, WRITABLE_SUBCOMMANDS, {
|
|
298
|
+
spec: WRITABLE_SPEC,
|
|
299
|
+
run: async () => {
|
|
300
|
+
throw validationError("expected list, add, or remove for `ctx writable`");
|
|
301
|
+
},
|
|
302
|
+
}),
|
|
303
|
+
},
|
|
140
304
|
};
|
|
141
305
|
export const CTX_SPECS = [CTX_SPEC, CTX_USE_SPEC];
|
|
142
306
|
export async function ctxCommand(args) {
|
|
143
307
|
const first = args[0];
|
|
144
|
-
if (first !== undefined &&
|
|
308
|
+
if (first !== undefined &&
|
|
309
|
+
!first.startsWith("-") &&
|
|
310
|
+
first !== "use" &&
|
|
311
|
+
first !== "writable") {
|
|
145
312
|
throw validationError(`unexpected argument ${first} for \`ctx\``, [
|
|
146
313
|
`To switch contexts, run \`argocd-axi ctx use ${first}\``,
|
|
147
314
|
`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,11 +13,16 @@ 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",
|
|
19
|
+
"apps auto-sync <app> <on|off> [--confirm]: preview, then optionally change automated sync",
|
|
20
|
+
"apps actions run <app> <action> --kind <kind> --name <resource> [--confirm]: preview, then optionally run one listed resource action",
|
|
16
21
|
"apps history <app>: deployments, newest first",
|
|
17
22
|
"apps manifests <app> [--source git|live] [--kind <k>] [--full]: rendered manifests, Secret data redacted",
|
|
18
23
|
"apps logs <app> [--kind --name --tail --since --filter -p]: recent pod logs, never follows",
|
|
19
24
|
"apps wait <app> [--sync --health --operation] [--timeout <s>]: bounded wait, returns the state either way",
|
|
20
|
-
"apps actions <app>:
|
|
25
|
+
"apps actions <app>: list available resource actions",
|
|
21
26
|
"projects | projects view <p> | projects windows <p>: projects, their rules, and whether sync is allowed now",
|
|
22
27
|
"appsets | appsets view <name> | appsets generate <file>: ApplicationSets and what a spec would generate",
|
|
23
28
|
"repos | clusters: repositories and destination clusters with connection status",
|
|
@@ -39,7 +44,7 @@ export const TOP_HELP = `${renderBlocks([
|
|
|
39
44
|
"argocd-axi apps diff payments-api",
|
|
40
45
|
]),
|
|
41
46
|
renderHelpBlock("notes", [
|
|
42
|
-
"
|
|
47
|
+
"Writes on writable contexts require --confirm: apps sync, rollback, refresh, terminate-op, auto-sync and actions run; sync never prunes unless --prune is given",
|
|
43
48
|
"Output is TOON on stdout, errors included; exit 0 = success (an unhealthy app is data, not an error), 1 = error, 2 = usage",
|
|
44
49
|
"Secret-looking values are redacted and counted; no flag reveals them",
|
|
45
50
|
"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,36 @@ 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
|
+
},
|
|
53
|
+
"app actions run": {
|
|
54
|
+
argv: ["app", "actions", "run"],
|
|
55
|
+
tier: "write",
|
|
56
|
+
network: true,
|
|
57
|
+
timeoutMs: READ_TIMEOUT_MS,
|
|
58
|
+
},
|
|
59
|
+
"app set": {
|
|
60
|
+
argv: ["app", "set"],
|
|
61
|
+
tier: "write",
|
|
62
|
+
network: true,
|
|
63
|
+
timeoutMs: READ_TIMEOUT_MS,
|
|
64
|
+
},
|
|
35
65
|
"app resources": {
|
|
36
66
|
argv: ["app", "resources"],
|
|
37
67
|
tier: "read",
|
|
@@ -172,41 +202,11 @@ export const FORBIDDEN_ARGS = [
|
|
|
172
202
|
"--port-forward",
|
|
173
203
|
"--prompts-enabled",
|
|
174
204
|
];
|
|
175
|
-
const PLANNED = "planned for the write tier, behind preview-by-default and the writable-contexts allowlist; not in v1 (see ROADMAP.md)";
|
|
176
205
|
/**
|
|
177
206
|
* Writes the design names for the next tier (decision A2): known verbs that
|
|
178
207
|
* answer REFUSED with the raw argocd route a human can take today.
|
|
179
208
|
*/
|
|
180
|
-
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
|
-
"apps action": {
|
|
203
|
-
reason: `running resource actions is ${PLANNED}`,
|
|
204
|
-
help: [
|
|
205
|
-
"List available actions with `argocd-axi apps actions <app>`",
|
|
206
|
-
"A human can run one with the raw CLI: `argocd app actions run <app> <action> --kind <kind>`",
|
|
207
|
-
],
|
|
208
|
-
},
|
|
209
|
-
};
|
|
209
|
+
export const PLANNED_WRITES = {};
|
|
210
210
|
const HUMAN_ONLY = "A human runs this with the raw argocd CLI";
|
|
211
211
|
/**
|
|
212
212
|
* Known verbs that argocd-axi never wraps (design decisions S7 and A2), each
|
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\`. See the README's writable-context instructions before enabling writes.
|
|
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`. See the README's writable-context instructions before enabling writes.
|
|
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[25]:
|
|
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,11 +46,16 @@ 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
|
|
52
|
+
apps auto-sync <app> <on|off> [--confirm]: preview, then optionally change automated sync
|
|
53
|
+
apps actions run <app> <action> --kind <kind> --name <resource> [--confirm]: preview, then optionally run one listed resource action
|
|
49
54
|
apps history <app>: deployments, newest first
|
|
50
55
|
apps manifests <app> [--source git|live] [--kind <k>] [--full]: rendered manifests, Secret data redacted
|
|
51
56
|
apps logs <app> [--kind --name --tail --since --filter -p]: recent pod logs, never follows
|
|
52
57
|
apps wait <app> [--sync --health --operation] [--timeout <s>]: bounded wait, returns the state either way
|
|
53
|
-
apps actions <app>:
|
|
58
|
+
apps actions <app>: list available resource actions
|
|
54
59
|
projects | projects view <p> | projects windows <p>: projects, their rules, and whether sync is allowed now
|
|
55
60
|
appsets | appsets view <name> | appsets generate <file>: ApplicationSets and what a spec would generate
|
|
56
61
|
repos | clusters: repositories and destination clusters with connection status
|