argocd-axi 0.1.3 → 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 CHANGED
@@ -8,7 +8,7 @@
8
8
  <a href="https://x.com/dematom1"><img alt="X" src="https://img.shields.io/badge/X-@dematom1-black?style=flat-square" /></a>
9
9
  </p>
10
10
 
11
- Read-only Argo CD for agents. `argocd-axi` wraps the `argocd` CLI and answers
11
+ Argo CD for agents, read-only except opt-in writes on writable contexts. `argocd-axi` wraps the `argocd` CLI and answers
12
12
  the questions an agent asks during a deployment problem (which apps need
13
13
  attention and why, what differs from Git, what was deployed when, what the
14
14
  pods are logging, whether a sync is allowed right now) in compact
@@ -18,7 +18,7 @@ pods are logging, whether a sync is allowed right now) in compact
18
18
  ```
19
19
  $ argocd-axi
20
20
  bin: ~/.local/bin/argocd-axi
21
- description: Read-only Argo CD for agents: applications needing attention, diffs, history, health, logs, projects and sync windows
21
+ description: Argo CD for agents: read operations plus opt-in writes on writable contexts
22
22
  context: prod
23
23
  server: argocd.example.com
24
24
  auth: argocd CLI session for this context
@@ -31,9 +31,7 @@ attention[3]{app,project,sync,health,why}:
31
31
  checkout,shop,Synced,Progressing,Progressing for 25m
32
32
  operations[1]{app,phase,started}:
33
33
  checkout,Running,25m ago
34
- help[2]:
35
- Run `argocd-axi apps view <app>` to diagnose an application
36
- Run `argocd-axi apps` to list every application
34
+ help[2]: Run `argocd-axi apps view <app>` to diagnose an application,Run `argocd-axi apps` to list every application
37
35
  ```
38
36
 
39
37
  ## Why
@@ -48,9 +46,10 @@ argocd-axi turns that into small, definitive answers:
48
46
  top and says why, in one line, with `count: N of M total`.
49
47
  - **Summary first.** Diffs and manifests come back as a per-resource list;
50
48
  the raw text is one `--full` away.
51
- - **Read-only.** v1 cannot sync, roll back, refresh, delete or change
52
- anything. Those verbs answer `REFUSED` with the raw `argocd` command a human
53
- can run. Writes are on the [roadmap](ROADMAP.md).
49
+ - **Opt-in writes.** `apps sync <app>` previews the per-resource diff and
50
+ makes no change unless `--confirm` is passed. It never prunes unless
51
+ `--prune` is explicit, and execution is limited to configured writable
52
+ contexts. Other writes remain refused.
54
53
  - **Safe output.** Secret data, passwords, tokens and sensitive-looking Helm
55
54
  parameters are redacted and counted. No flag reveals them.
56
55
  - **Never hangs.** Nothing streams or follows; waits are bounded; no prompt can
@@ -75,15 +74,22 @@ Requirements:
75
74
  v3.3.6. There is no hard minimum; `ctx` and the home view warn when the CLI
76
75
  and server differ by a major version.
77
76
 
78
- ## Credentials
77
+ ## Setup
79
78
 
80
79
  argocd-axi adds no auth of its own. It uses whatever the `argocd` CLI already
81
80
  has: the current context's session, or `ARGOCD_SERVER` and
82
81
  `ARGOCD_AUTH_TOKEN` from the environment. It never starts a login, never
83
82
  prompts, and never reads the argocd config file (`~/.config/argocd/config`,
84
- which holds clear-text tokens). When a session is missing or expired, it
85
- answers `AUTH_REQUIRED` and tells the agent that a human runs
86
- `argocd login <server> --sso` or `argocd relogin`.
83
+ which holds clear-text tokens). A missing session answers `AUTH_REQUIRED`; an
84
+ expired or rejected login answers `AUTH_EXPIRED`. When the target server is
85
+ known, help includes the exact operator login command; otherwise it shows the
86
+ login syntax and asks for the server address. Run
87
+ `argocd-axi auth guide` for offline setup instructions. Human operators run
88
+ `argocd login <server> --sso --grpc-web` interactively themselves. Agents and
89
+ CI should set `ARGOCD_SERVER` and `ARGOCD_AUTH_TOKEN` from a scoped account
90
+ token. Verify the selected context with `argocd-axi ctx`. The tool never logs in.
91
+
92
+ ### Scoped agent credentials
87
93
 
88
94
  An agent that uses your own SSO session has all of your Argo CD permissions.
89
95
  The recommended agent credential is a **short-lived project-role token**,
@@ -139,9 +145,10 @@ argocd-axi apps [--problems --project --selector --repo --cluster --sync --healt
139
145
  argocd-axi apps view <app> [--full]
140
146
  argocd-axi apps resources <app> [--problems | --orphaned]
141
147
  argocd-axi apps diff <app> [--revision <rev>] [--full]
148
+ argocd-axi apps sync <app> [--confirm] [--prune] [--timeout <s>] --context <c>
142
149
  argocd-axi apps history <app> [--limit <n>]
143
150
  argocd-axi apps manifests <app> [--source git|live] [--revision <rev>] [--kind <k>] [--full]
144
- argocd-axi apps logs <app> [--kind <k> --name <n>] [--tail 100] [--since 15m] [--filter <s>] [-p]
151
+ argocd-axi apps logs <app> [--kind <k> --name <n>] [--tail 100] [--since 15m] [--filter <s>] [--full] [-p]
145
152
  argocd-axi apps wait <app> [--sync --health --operation] [--timeout 120]
146
153
  argocd-axi apps actions <app> [--kind <k>]
147
154
  argocd-axi projects | projects view <p> | projects windows <p>
@@ -187,20 +194,21 @@ specific app.
187
194
  Unknown flags are rejected before any argocd call, with the valid flags
188
195
  listed.
189
196
 
190
- | Code | Meaning |
191
- | --------------------------------- | -------------------------------------------------------------------------------------- |
192
- | `VALIDATION_ERROR` | Bad flag or argument (exit 2) |
193
- | `AUTH_REQUIRED` | No session or an expired one; a human logs in |
194
- | `FORBIDDEN` | Argo CD RBAC denied it (Argo CD also says this for objects in projects you cannot see) |
195
- | `NOT_FOUND` | No such app, project or ApplicationSet |
196
- | `RATE_LIMITED` | The server asked to slow down |
197
- | `SERVER_UNREACHABLE` | Network or TLS failure reaching the server |
198
- | `REFUSED` | A verb argocd-axi does not run, with the reason and the human route |
199
- | `NO_CONTEXT`, `CONTEXT_NOT_FOUND` | No current context, or `--context` names one that is not configured |
200
- | `TIMEOUT` | argocd did not answer in time |
201
- | `ARGOCD_NOT_INSTALLED` | No `argocd` on `PATH` |
202
- | `CONFIG_INVALID` | argocd-axi's own config file is malformed |
203
- | `UPSTREAM_ERROR` | Any other argocd failure, with its cleaned message |
197
+ | Code | Meaning |
198
+ | --------------------------------- | --------------------------------------------------------------------------------------------- |
199
+ | `VALIDATION_ERROR` | Bad flag or argument (exit 2) |
200
+ | `AUTH_REQUIRED` | No session is configured |
201
+ | `AUTH_EXPIRED` | Argo CD rejected or expired the login; help prints the exact command when the server is known |
202
+ | `FORBIDDEN` | Argo CD RBAC denied it (Argo CD also says this for objects in projects you cannot see) |
203
+ | `NOT_FOUND` | No such app, project or ApplicationSet |
204
+ | `RATE_LIMITED` | The server asked to slow down |
205
+ | `SERVER_UNREACHABLE` | Network or TLS failure reaching the server |
206
+ | `REFUSED` | A verb argocd-axi does not run, with the reason and the human route |
207
+ | `NO_CONTEXT`, `CONTEXT_NOT_FOUND` | No current context, or `--context` names one that is not configured |
208
+ | `TIMEOUT` | argocd did not answer in time |
209
+ | `ARGOCD_NOT_INSTALLED` | No `argocd` on `PATH` |
210
+ | `CONFIG_INVALID` | argocd-axi's own config file is malformed |
211
+ | `UPSTREAM_ERROR` | Any other argocd failure, with its cleaned message |
204
212
 
205
213
  ## Safety model
206
214
 
@@ -211,9 +219,7 @@ specific app.
211
219
  local-file and delete flags whatever a command passes, gives argocd no
212
220
  stdin, and enforces a timeout and an output cap. Tests fail if a new
213
221
  operation becomes reachable without updating the registry.
214
- - **A refusal registry.** Known writes and credential commands answer
215
- `REFUSED` with a reason and the raw command a human can run instead of
216
- 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`.
217
223
  - **Redaction.** Kubernetes Secret `data`, `stringData` and their
218
224
  last-applied annotation, values under sensitive-looking keys, and env-var or
219
225
  Helm-parameter pairs with sensitive-looking names are replaced with
@@ -224,13 +230,10 @@ specific app.
224
230
  reach them.
225
231
  - **Writable contexts.** argocd-axi's own config file,
226
232
  `~/.config/argocd-axi/config` (or `$XDG_CONFIG_HOME/argocd-axi/config`),
227
- holds the allowlist of contexts where the future write tier may execute.
228
- It defaults to none, and v1 only displays it:
229
-
230
- ```yaml
231
- writable-contexts:
232
- - staging
233
- ```
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.
234
237
 
235
238
  - **Version cache.** The server version is checked at most once every 12
236
239
  hours per context and server, and cached in `~/.cache/argocd-axi/`
package/ROADMAP.md CHANGED
@@ -1,54 +1,29 @@
1
1
  # Roadmap
2
2
 
3
- argocd-axi v1 is read-only by design: the read surface settles first, and the
4
- two commands that can hurt production (sync and rollback) come next, behind a
5
- gate. This file records what is planned and the rules each step must follow.
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
- ## Next: the write tier
7
+ ## Remaining write tier
8
8
 
9
- These verbs exist in v1 only as structured `REFUSED` answers that name the raw
10
- `argocd` command a human can run. The write tier turns them into commands:
11
-
12
- | Command | Wraps | Notes |
13
- | ------------------------------------------------------------------------------------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
14
- | `apps sync <app> --context <c> [--prune] [--revision <rev>] [--resource <group:kind:name>]` | `argocd app sync` | Preview from `--dry-run`, the per-resource diff, the sync-window verdict and an RBAC check (`account can-i sync`) |
15
- | `apps rollback <app> <history-id> --context <c>` | `argocd app rollback` | No native dry run, so the preview is synthesized: target history entry against the current one. Refused while auto-sync is enabled, as Argo CD requires |
16
- | `apps terminate-op <app> --context <c>` | `argocd app terminate-op` | No-op with exit 0 when no operation is running |
17
- | `apps refresh <app> [--hard] --context <c>` | `argocd app get --refresh` | Triggers a reconcile, so it is a write, not a read |
18
-
19
- Later, a third tier: `apps action run <app> <action> --kind <kind>` for
20
- resource actions such as restart, and Argo Rollouts promote and abort
21
- 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).
22
11
 
23
12
  ### Rules every write follows
24
13
 
25
- The gate is already built and unit-tested in `src/gate.ts`, with its
26
- allowlist loaded from `src/config.ts`. The write tier only adds the commands
27
- and calls the gate.
28
-
29
- 1. **Preview by default.** Without the command's execute flag, nothing is
30
- sent. The preview shows the target context and server, the object, the
31
- change set and the preflight checks, plus the exact command that executes
32
- it. Previews work against any context.
33
- 2. **A local allowlist of writable contexts, default none.** Executing needs
34
- the context listed in argocd-axi's own config file,
35
- `~/.config/argocd-axi/config` (or `$XDG_CONFIG_HOME/argocd-axi/config`):
36
-
37
- ```yaml
38
- writable-contexts:
39
- - staging
40
- ```
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.
41
16
 
42
- A production context stays read-only until a human opts it in locally.
43
- `ctx` and the home view already show `writableContexts: none`.
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.
44
21
 
45
22
  3. **An explicit target.** Every write names `--context <name>`; the current
46
23
  context is never used implicitly, and the result echoes the context and
47
24
  server.
48
- 4. **Argo CD's own vocabulary.** The execute flag and every other flag are
49
- named after Argo CD's terms for each command, not a shared cross-tool
50
- vocabulary. The exact execute-flag spelling is settled when each command is
51
- built.
25
+ 4. **Explicit confirmation.** Every supported Application write requires
26
+ `--confirm` to execute.
52
27
  5. **Idempotent.** Asking for a state that already exists (syncing an app
53
28
  that is already synced to the target revision, terminating when nothing
54
29
  runs) is a no-op with exit 0. Exit 1 only when the intent cannot be met:
@@ -94,4 +69,3 @@ instances (version, SSO provider, app count) are known.
94
69
 
95
70
  Submission to the [AXI community catalog](https://github.com/kunchenguid/axi)
96
71
  comes after v1. npm releases are automated; see the Releases section in
97
- [README.md](README.md).
@@ -1,4 +1,5 @@
1
1
  import { spawn } from "node:child_process";
2
+ import { resolveTarget } from "./context.js";
2
3
  import { CODES, axiError, mapArgocdFailure, } from "./errors.js";
3
4
  import { FORBIDDEN_ARGS, OPERATIONS } from "./registry.js";
4
5
  /**
@@ -13,7 +14,10 @@ export function buildArgv(op, args, context) {
13
14
  const operation = OPERATIONS[op];
14
15
  for (const arg of args) {
15
16
  const name = arg.startsWith("--") ? arg.split("=")[0] : arg;
16
- if (name !== undefined && FORBIDDEN_ARGS.includes(name)) {
17
+ if (name !== undefined &&
18
+ FORBIDDEN_ARGS.includes(name) &&
19
+ !(op === "app refresh" &&
20
+ (name === "--refresh" || name === "--hard-refresh"))) {
17
21
  throw axiError(`internal guard: argument ${name} is never passed to argocd`, CODES.REFUSED);
18
22
  }
19
23
  }
@@ -37,10 +41,28 @@ export async function runArgocd(op, args, options = {}) {
37
41
  ]);
38
42
  }
39
43
  if (result.exitCode !== 0 && !options.allowFailure) {
40
- throw mapArgocdFailure(result.stderr || result.stdout, options.failure);
44
+ const failure = options.failure ?? {};
45
+ const cleaned = result.stderr || result.stdout;
46
+ if (!failure.loginCommand && isAuthenticationFailure(cleaned)) {
47
+ failure.agentCredentials = Boolean(process.env.ARGOCD_AUTH_TOKEN?.trim());
48
+ try {
49
+ const { target } = await resolveTarget(options.context);
50
+ const server = target.envServer ?? target.server;
51
+ failure.loginCommand = server
52
+ ? `argocd login '${server.replaceAll("'", "'\\''")}' --sso --grpc-web`
53
+ : undefined;
54
+ }
55
+ catch {
56
+ failure.loginCommand = undefined;
57
+ }
58
+ }
59
+ throw mapArgocdFailure(cleaned, failure);
41
60
  }
42
61
  return result;
43
62
  }
63
+ function isAuthenticationFailure(message) {
64
+ return /rpc error: code = Unauthenticated|invalid session|token is expired|token has expired|no session information|unauthenticated|please login|login required|failed to get token|session (?:has )?expired/i.test(message);
65
+ }
44
66
  /** Run an operation whose stdout is JSON and parse it. */
45
67
  export async function argocdJson(op, args, options = {}) {
46
68
  const result = await runArgocd(op, args, options);
package/dist/src/cli.js CHANGED
@@ -5,6 +5,7 @@ import { appsCommand } from "./commands/apps.js";
5
5
  import { appsetsCommand } from "./commands/appsets.js";
6
6
  import { canICommand, whoamiCommand } from "./commands/account.js";
7
7
  import { ctxCommand } from "./commands/ctx.js";
8
+ import { authCommand } from "./commands/auth.js";
8
9
  import { homeCommand } from "./commands/home.js";
9
10
  import { clustersCommand, reposCommand } from "./commands/inventory.js";
10
11
  import { projectsCommand } from "./commands/projects.js";
@@ -45,6 +46,7 @@ const refusalCommand = (key) => async () => refuse(key);
45
46
  const TOP_LEVEL_REFUSALS = Object.keys(REFUSALS).filter((key) => !key.includes(" "));
46
47
  export const COMMANDS = Object.assign(Object.create(null), {
47
48
  ctx: ctxCommand,
49
+ auth: authCommand,
48
50
  whoami: whoamiCommand,
49
51
  "can-i": canICommand,
50
52
  apps: appsCommand,
@@ -56,6 +58,12 @@ export const COMMANDS = Object.assign(Object.create(null), {
56
58
  ...Object.fromEntries(TOP_LEVEL_REFUSALS.map((key) => [key, refusalCommand(key)])),
57
59
  });
58
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
+ }
59
67
  await runAxiCli({
60
68
  description: DESCRIPTION,
61
69
  version: VERSION,