argocd-axi 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +243 -2
- package/ROADMAP.md +96 -0
- package/dist/bin/argocd-axi-ctx.js +13 -0
- package/dist/bin/argocd-axi.js +7 -0
- package/dist/src/argocd.js +118 -0
- package/dist/src/args.js +198 -0
- package/dist/src/cli.js +69 -0
- package/dist/src/commands/account.js +115 -0
- package/dist/src/commands/apps.js +1041 -0
- package/dist/src/commands/appsets.js +236 -0
- package/dist/src/commands/common.js +97 -0
- package/dist/src/commands/ctx.js +151 -0
- package/dist/src/commands/home.js +107 -0
- package/dist/src/commands/inventory.js +84 -0
- package/dist/src/commands/projects.js +227 -0
- package/dist/src/commands/setup.js +69 -0
- package/dist/src/config.js +65 -0
- package/dist/src/context.js +124 -0
- package/dist/src/errors.js +159 -0
- package/dist/src/gate.js +62 -0
- package/dist/src/help.js +45 -0
- package/dist/src/model.js +182 -0
- package/dist/src/redact.js +161 -0
- package/dist/src/registry.js +364 -0
- package/dist/src/skew.js +117 -0
- package/dist/src/skill.js +62 -0
- package/dist/src/toon.js +39 -0
- package/dist/src/version.js +3 -0
- package/dist/src/windows.js +286 -0
- package/package.json +69 -4
- package/skills/argocd-axi/SKILL.md +64 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Laszlo Horanszky
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,244 @@
|
|
|
1
|
-
#
|
|
1
|
+
# argocd-axi
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Read-only Argo CD for agents. `argocd-axi` wraps the `argocd` CLI and answers
|
|
4
|
+
the questions an agent asks during a deployment problem (which apps need
|
|
5
|
+
attention and why, what differs from Git, what was deployed when, what the
|
|
6
|
+
pods are logging, whether a sync is allowed right now) in compact
|
|
7
|
+
[TOON](https://toonformat.dev/) output that follows the
|
|
8
|
+
[AXI](https://github.com/kunchenguid/axi) conventions.
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
$ argocd-axi
|
|
12
|
+
bin: ~/.local/bin/argocd-axi
|
|
13
|
+
description: Read-only Argo CD for agents: applications needing attention, diffs, history, health, logs, projects and sync windows
|
|
14
|
+
context: prod
|
|
15
|
+
server: argocd.example.com
|
|
16
|
+
auth: argocd CLI session for this context
|
|
17
|
+
user: alice
|
|
18
|
+
apps: "142 total, 3 need attention, 1 operation running"
|
|
19
|
+
writableContexts: none
|
|
20
|
+
attention[3]{app,project,sync,health,why}:
|
|
21
|
+
payments-api,payments,OutOfSync,Degraded,"OutOfSync; Degraded: Deployment \"payments-api\" exceeded its progress deadline; last sync Failed: ..."
|
|
22
|
+
legacy,default,Unknown,Unknown,"sync Unknown; health Unknown; ComparisonError: ..."
|
|
23
|
+
checkout,shop,Synced,Progressing,Progressing for 25m
|
|
24
|
+
operations[1]{app,phase,started}:
|
|
25
|
+
checkout,Running,25m ago
|
|
26
|
+
help[2]:
|
|
27
|
+
Run `argocd-axi apps view <app>` to diagnose an application
|
|
28
|
+
Run `argocd-axi apps` to list every application
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Why
|
|
32
|
+
|
|
33
|
+
The raw `argocd` CLI is built for people. An agent using it has to cope with
|
|
34
|
+
wide tables or very large JSON, `app diff` exiting 1 when it finds drift,
|
|
35
|
+
`app history` having no JSON output, prompts and browser logins, `logs -f` and
|
|
36
|
+
`app wait` that can hang, and Secret data in `app manifests --source live`.
|
|
37
|
+
argocd-axi turns that into small, definitive answers:
|
|
38
|
+
|
|
39
|
+
- **Problems first.** Every list sorts the apps that need attention to the
|
|
40
|
+
top and says why, in one line, with `count: N of M total`.
|
|
41
|
+
- **Summary first.** Diffs and manifests come back as a per-resource list;
|
|
42
|
+
the raw text is one `--full` away.
|
|
43
|
+
- **Read-only.** v1 cannot sync, roll back, refresh, delete or change
|
|
44
|
+
anything. Those verbs answer `REFUSED` with the raw `argocd` command a human
|
|
45
|
+
can run. Writes are on the [roadmap](ROADMAP.md).
|
|
46
|
+
- **Safe output.** Secret data, passwords, tokens and sensitive-looking Helm
|
|
47
|
+
parameters are redacted and counted. No flag reveals them.
|
|
48
|
+
- **Never hangs.** Nothing streams or follows; waits are bounded; no prompt can
|
|
49
|
+
block because argocd never gets a stdin.
|
|
50
|
+
|
|
51
|
+
## Install
|
|
52
|
+
|
|
53
|
+
```sh
|
|
54
|
+
npm install -g argocd-axi
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Or run it without installing: `npx -y argocd-axi`.
|
|
58
|
+
|
|
59
|
+
Requirements:
|
|
60
|
+
|
|
61
|
+
- Node.js 20 or newer.
|
|
62
|
+
- The [`argocd` CLI](https://argo-cd.readthedocs.io/en/stable/cli_installation/)
|
|
63
|
+
on `PATH`, ideally matching your server's version. Tested with argocd
|
|
64
|
+
v3.3.6. There is no hard minimum; `ctx` and the home view warn when the CLI
|
|
65
|
+
and server differ by a major version.
|
|
66
|
+
|
|
67
|
+
## Credentials
|
|
68
|
+
|
|
69
|
+
argocd-axi adds no auth of its own. It uses whatever the `argocd` CLI already
|
|
70
|
+
has: the current context's session, or `ARGOCD_SERVER` and
|
|
71
|
+
`ARGOCD_AUTH_TOKEN` from the environment. It never starts a login, never
|
|
72
|
+
prompts, and never reads the argocd config file (`~/.config/argocd/config`,
|
|
73
|
+
which holds clear-text tokens). When a session is missing or expired, it
|
|
74
|
+
answers `AUTH_REQUIRED` and tells the agent that a human runs
|
|
75
|
+
`argocd login <server> --sso` or `argocd relogin`.
|
|
76
|
+
|
|
77
|
+
An agent that uses your own SSO session has all of your Argo CD permissions.
|
|
78
|
+
The recommended agent credential is a **short-lived project-role token**,
|
|
79
|
+
scoped to read (and later, sync only where you want it), so the server bounds
|
|
80
|
+
what the agent can do whatever any tool does:
|
|
81
|
+
|
|
82
|
+
```sh
|
|
83
|
+
# A human runs these, once per project and role:
|
|
84
|
+
argocd proj role create payments agent-read
|
|
85
|
+
argocd proj role add-policy payments agent-read --action get --permission allow --object '*'
|
|
86
|
+
argocd proj role add-policy payments agent-read --action get --permission allow --object '*' --resource logs
|
|
87
|
+
export ARGOCD_AUTH_TOKEN="$(argocd proj role create-token payments agent-read --expires-in 8h --token-only)"
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
A project role only adds permissions. Argo CD also grants every token the
|
|
91
|
+
server's `policy.default` role (from `argocd-rbac-cm`), so with
|
|
92
|
+
`policy.default: role:readonly` this token can read every project, not just
|
|
93
|
+
`payments`. For a token that is really project-scoped, keep `policy.default`
|
|
94
|
+
empty or minimal and review the effective policies in `argocd-rbac-cm`.
|
|
95
|
+
`can-i` checks one object literally (`'other-project/*'` is not "any app in
|
|
96
|
+
other-project"), so a `no` there proves nothing about other apps; spot-check
|
|
97
|
+
concrete apps instead, e.g. `argocd-axi can-i get applications
|
|
98
|
+
'other-project/some-app'`.
|
|
99
|
+
|
|
100
|
+
argocd-axi itself refuses `proj role create-token`, `account generate-token`
|
|
101
|
+
and every other command that prints or changes a credential.
|
|
102
|
+
|
|
103
|
+
## Agent integration
|
|
104
|
+
|
|
105
|
+
Pick one; you do not need both.
|
|
106
|
+
|
|
107
|
+
- **Session hook (recommended).** `argocd-axi setup hooks` installs a
|
|
108
|
+
SessionStart hook for Claude Code, Codex and OpenCode. The hook runs
|
|
109
|
+
`argocd-axi-ctx`, which is local only: current context, server, CLI version
|
|
110
|
+
and the cached server version. It makes no network call, triggers no SSO
|
|
111
|
+
refresh and reads no credential, so a dead context can never hang a session
|
|
112
|
+
start. It needs a persistent install (`npm install -g argocd-axi`); run from
|
|
113
|
+
an npx cache, it answers `REFUSED` because the hook would point at a
|
|
114
|
+
temporary path.
|
|
115
|
+
- **Skill.** `npx skills add Dematom1/argocd-axi --skill argocd-axi`
|
|
116
|
+
installs [`skills/argocd-axi/SKILL.md`](skills/argocd-axi/SKILL.md), which
|
|
117
|
+
is generated from the CLI's own help and loads when a task is about Argo CD.
|
|
118
|
+
|
|
119
|
+
## Usage
|
|
120
|
+
|
|
121
|
+
```
|
|
122
|
+
argocd-axi live overview (needs the server)
|
|
123
|
+
argocd-axi ctx [--context <c>] current context, server, CLI version (local only)
|
|
124
|
+
argocd-axi ctx use <context> switch the argocd CLI's current context
|
|
125
|
+
argocd-axi whoami user, issuer and groups
|
|
126
|
+
argocd-axi can-i <action> <resource> <obj> ask Argo CD RBAC, e.g. can-i sync applications 'payments/payments-api'
|
|
127
|
+
argocd-axi apps [--problems --project --selector --repo --cluster --sync --health --limit --fields]
|
|
128
|
+
argocd-axi apps view <app> [--full]
|
|
129
|
+
argocd-axi apps resources <app> [--problems | --orphaned]
|
|
130
|
+
argocd-axi apps diff <app> [--revision <rev>] [--full]
|
|
131
|
+
argocd-axi apps history <app> [--limit <n>]
|
|
132
|
+
argocd-axi apps manifests <app> [--source git|live] [--revision <rev>] [--kind <k>] [--full]
|
|
133
|
+
argocd-axi apps logs <app> [--kind <k> --name <n>] [--tail 100] [--since 15m] [--filter <s>] [-p]
|
|
134
|
+
argocd-axi apps wait <app> [--sync --health --operation] [--timeout 120]
|
|
135
|
+
argocd-axi apps actions <app> [--kind <k>]
|
|
136
|
+
argocd-axi projects | projects view <p> | projects windows <p>
|
|
137
|
+
argocd-axi appsets | appsets view <name> | appsets generate <file>
|
|
138
|
+
argocd-axi repos | clusters
|
|
139
|
+
argocd-axi setup hooks
|
|
140
|
+
argocd-axi update [--check]
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Every command takes `--context <name>` to target another Argo CD context, and
|
|
144
|
+
`--help` for its flags and examples. Flags always come after the command.
|
|
145
|
+
Apps in other namespaces are addressed as `namespace/name`.
|
|
146
|
+
|
|
147
|
+
### What "needs attention" means
|
|
148
|
+
|
|
149
|
+
An app needs attention when its sync status is OutOfSync or Unknown; its
|
|
150
|
+
health is Degraded, Missing or Unknown; it has been Progressing for more than
|
|
151
|
+
10 minutes; its last sync Failed or Errored; or it has conditions. Both axes
|
|
152
|
+
and the reason are always shown, and Progressing is never folded into
|
|
153
|
+
healthy. When Argo CD does not report when health turned Progressing, the
|
|
154
|
+
duration is shown as unknown and the app is not counted as needing attention.
|
|
155
|
+
|
|
156
|
+
### Sync windows
|
|
157
|
+
|
|
158
|
+
`projects windows <project>` lists each window, whether it is active now, and
|
|
159
|
+
when an inactive one next opens, then says whether automated and manual syncs
|
|
160
|
+
are allowed right now. It evaluates the windows the way Argo CD does (5-field
|
|
161
|
+
cron schedule or a descriptor such as `@daily` or `@every 1h`, duration, the
|
|
162
|
+
window's time zone or UTC). When a window that could change the outcome cannot
|
|
163
|
+
be evaluated, the verdict is `unknown` and names that window. The verdict assumes
|
|
164
|
+
every window applies; check each window's apps, namespaces and clusters for a
|
|
165
|
+
specific app.
|
|
166
|
+
|
|
167
|
+
## Output and exit codes
|
|
168
|
+
|
|
169
|
+
- Output is TOON on stdout, errors included: `error`, `code`, and `help` with
|
|
170
|
+
the next command to run. There is no `--json`; the raw `argocd` CLI is the
|
|
171
|
+
fallback for anything argocd-axi does not cover.
|
|
172
|
+
- Exit 0 means the command did what was asked. An OutOfSync or Degraded app,
|
|
173
|
+
drift found by `apps diff`, a `can-i` answer of no, and an `apps wait` that
|
|
174
|
+
timed out are all data with exit 0.
|
|
175
|
+
- Exit 1 is an error, exit 2 a usage error (unknown flag, missing argument).
|
|
176
|
+
Unknown flags are rejected before any argocd call, with the valid flags
|
|
177
|
+
listed.
|
|
178
|
+
|
|
179
|
+
| Code | Meaning |
|
|
180
|
+
| --------------------------------- | -------------------------------------------------------------------------------------- |
|
|
181
|
+
| `VALIDATION_ERROR` | Bad flag or argument (exit 2) |
|
|
182
|
+
| `AUTH_REQUIRED` | No session or an expired one; a human logs in |
|
|
183
|
+
| `FORBIDDEN` | Argo CD RBAC denied it (Argo CD also says this for objects in projects you cannot see) |
|
|
184
|
+
| `NOT_FOUND` | No such app, project or ApplicationSet |
|
|
185
|
+
| `RATE_LIMITED` | The server asked to slow down |
|
|
186
|
+
| `SERVER_UNREACHABLE` | Network or TLS failure reaching the server |
|
|
187
|
+
| `REFUSED` | A verb argocd-axi does not run, with the reason and the human route |
|
|
188
|
+
| `NO_CONTEXT`, `CONTEXT_NOT_FOUND` | No current context, or `--context` names one that is not configured |
|
|
189
|
+
| `TIMEOUT` | argocd did not answer in time |
|
|
190
|
+
| `ARGOCD_NOT_INSTALLED` | No `argocd` on `PATH` |
|
|
191
|
+
| `CONFIG_INVALID` | argocd-axi's own config file is malformed |
|
|
192
|
+
| `UPSTREAM_ERROR` | Any other argocd failure, with its cleaned message |
|
|
193
|
+
|
|
194
|
+
## Safety model
|
|
195
|
+
|
|
196
|
+
- **One subprocess seam.** Every argocd call goes through `src/argocd.ts`,
|
|
197
|
+
which accepts only operations from the allowlist registry in
|
|
198
|
+
`src/registry.ts`. Every registered operation is a server read or a
|
|
199
|
+
local-config operation. The seam also refuses streaming, refresh, prompt,
|
|
200
|
+
local-file and delete flags whatever a command passes, gives argocd no
|
|
201
|
+
stdin, and enforces a timeout and an output cap. Tests fail if a new
|
|
202
|
+
operation becomes reachable without updating the registry.
|
|
203
|
+
- **A refusal registry.** Known writes and credential commands answer
|
|
204
|
+
`REFUSED` with a reason and the raw command a human can run instead of
|
|
205
|
+
failing blind.
|
|
206
|
+
- **Redaction.** Kubernetes Secret `data`, `stringData` and their
|
|
207
|
+
last-applied annotation, values under sensitive-looking keys, and env-var or
|
|
208
|
+
Helm-parameter pairs with sensitive-looking names are replaced with
|
|
209
|
+
`[redacted]`, and the output says how many values were hidden. Free text
|
|
210
|
+
(log lines, diff text) is redacted line by line on a best-effort basis:
|
|
211
|
+
`key=value` pairs, bearer tokens, JWTs and URL passwords. Repository and
|
|
212
|
+
cluster rows are built from an explicit field list, so credentials never
|
|
213
|
+
reach them.
|
|
214
|
+
- **Writable contexts.** argocd-axi's own config file,
|
|
215
|
+
`~/.config/argocd-axi/config` (or `$XDG_CONFIG_HOME/argocd-axi/config`),
|
|
216
|
+
holds the allowlist of contexts where the future write tier may execute.
|
|
217
|
+
It defaults to none, and v1 only displays it:
|
|
218
|
+
|
|
219
|
+
```yaml
|
|
220
|
+
writable-contexts:
|
|
221
|
+
- staging
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
- **Version cache.** The server version is checked at most once every 12
|
|
225
|
+
hours per context and server, and cached in `~/.cache/argocd-axi/`
|
|
226
|
+
(`$XDG_CACHE_HOME`). This file holds version strings only.
|
|
227
|
+
|
|
228
|
+
## Development
|
|
229
|
+
|
|
230
|
+
```sh
|
|
231
|
+
npm install
|
|
232
|
+
npm run check # format, lint, typecheck, tests, skill drift check
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Tests never touch a real Argo CD server, cluster, kubeconfig or credential.
|
|
236
|
+
They run the built CLI as a user would, with a fake `argocd`
|
|
237
|
+
(`test/fake-argocd/`) as the only executable on `PATH`. The fake answers from
|
|
238
|
+
the recorded fixtures in `test/fixtures/`, and each test gets a throwaway
|
|
239
|
+
`HOME`. After changing help text, run `npm run build:skill` to regenerate the
|
|
240
|
+
skill.
|
|
241
|
+
|
|
242
|
+
## License
|
|
243
|
+
|
|
244
|
+
[MIT](LICENSE)
|
package/ROADMAP.md
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# Roadmap
|
|
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.
|
|
6
|
+
|
|
7
|
+
## Next: the write tier
|
|
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.
|
|
22
|
+
|
|
23
|
+
### Rules every write follows
|
|
24
|
+
|
|
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
|
+
```
|
|
41
|
+
|
|
42
|
+
A production context stays read-only until a human opts it in locally.
|
|
43
|
+
`ctx` and the home view already show `writableContexts: none`.
|
|
44
|
+
|
|
45
|
+
3. **An explicit target.** Every write names `--context <name>`; the current
|
|
46
|
+
context is never used implicitly, and the result echoes the context and
|
|
47
|
+
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.
|
|
52
|
+
5. **Idempotent.** Asking for a state that already exists (syncing an app
|
|
53
|
+
that is already synced to the target revision, terminating when nothing
|
|
54
|
+
runs) is a no-op with exit 0. Exit 1 only when the intent cannot be met:
|
|
55
|
+
permission denied (`FORBIDDEN`), a sync window is closed
|
|
56
|
+
(`SYNC_WINDOW_BLOCKED`), auto-sync blocks a rollback.
|
|
57
|
+
6. **Attributed.** Every sync carries `--info initiated-by=argocd-axi`, so
|
|
58
|
+
the app's audit trail shows agent syncs. Whether `--info` is accepted
|
|
59
|
+
under restricted RBAC is checked in the first real verification pass.
|
|
60
|
+
7. **Bounded.** A write that waits does so for about 120 seconds (`--timeout`)
|
|
61
|
+
and then returns the current operation state with a hint to wait again.
|
|
62
|
+
Nothing streams.
|
|
63
|
+
8. **Registered.** Each new argocd invocation is added to the allowlist
|
|
64
|
+
registry in `src/registry.ts` under a write tier, and the registry test is
|
|
65
|
+
updated in the same change.
|
|
66
|
+
|
|
67
|
+
Everything else that writes (app create, set, patch, edit and delete,
|
|
68
|
+
resource patch and delete, project, repository, cluster, certificate and GPG
|
|
69
|
+
changes, credential and token commands, `admin`) stays refused.
|
|
70
|
+
|
|
71
|
+
## Before output shapes lock: a real verification pass
|
|
72
|
+
|
|
73
|
+
All tests run against a fake `argocd` with recorded fixtures. Before the
|
|
74
|
+
output shapes are considered stable, one pass against a real, disposable
|
|
75
|
+
Argo CD (a local kind or k3d cluster) checks what the fixtures could not:
|
|
76
|
+
|
|
77
|
+
- the JSON field coverage of `app get -o json` and `app list -o json`;
|
|
78
|
+
- the text layout of `app diff` section headers and the `app resources
|
|
79
|
+
--orphaned` table;
|
|
80
|
+
- the JSON shape of `app actions list -o json` and `proj windows list -o json`;
|
|
81
|
+
- how argocd reports sync-window blocks and expired sessions;
|
|
82
|
+
- `--info` under restricted RBAC.
|
|
83
|
+
|
|
84
|
+
Each such pass is a separate task that asks the maintainer first.
|
|
85
|
+
|
|
86
|
+
## Tuning once instance facts are known
|
|
87
|
+
|
|
88
|
+
Default list limits, the attention threshold for Progressing apps
|
|
89
|
+
(10 minutes) and which features need first-class handling (ApplicationSets,
|
|
90
|
+
app-of-apps, apps in any namespace) are tuned once the target Argo CD
|
|
91
|
+
instances (version, SSO provider, app count) are known.
|
|
92
|
+
|
|
93
|
+
## Publishing
|
|
94
|
+
|
|
95
|
+
npm publication and submission to the
|
|
96
|
+
[AXI community catalog](https://github.com/kunchenguid/axi) come after v1.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Dedicated SessionStart-hook entrypoint (decision S17). The SDK hook runner
|
|
3
|
+
// spawns a bare command with no arguments; pointing it here instead of at the
|
|
4
|
+
// networked home view guarantees ambient context never calls the server,
|
|
5
|
+
// never triggers an SSO refresh and never reads a credential.
|
|
6
|
+
import { ctxView } from "../src/commands/ctx.js";
|
|
7
|
+
try {
|
|
8
|
+
process.stdout.write(`${await ctxView([])}\n`);
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
// Ambient context must never break session start; degrade to silence.
|
|
12
|
+
process.exitCode = 0;
|
|
13
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { CODES, axiError, mapArgocdFailure, } from "./errors.js";
|
|
3
|
+
import { FORBIDDEN_ARGS, OPERATIONS } from "./registry.js";
|
|
4
|
+
/**
|
|
5
|
+
* The one subprocess seam (design decisions A1 and S5). Every argocd call in
|
|
6
|
+
* argocd-axi goes through `runArgocd`, which only accepts operations from the
|
|
7
|
+
* allowlist registry, refuses forbidden arguments, never connects stdin (so
|
|
8
|
+
* no prompt can ever block), and enforces a hard timeout and output cap.
|
|
9
|
+
*/
|
|
10
|
+
const MAX_OUTPUT_BYTES = 50 * 1024 * 1024;
|
|
11
|
+
const KILL_GRACE_MS = 2_000;
|
|
12
|
+
export function buildArgv(op, args, context) {
|
|
13
|
+
const operation = OPERATIONS[op];
|
|
14
|
+
for (const arg of args) {
|
|
15
|
+
const name = arg.startsWith("--") ? arg.split("=")[0] : arg;
|
|
16
|
+
if (name !== undefined && FORBIDDEN_ARGS.includes(name)) {
|
|
17
|
+
throw axiError(`internal guard: argument ${name} is never passed to argocd`, CODES.REFUSED);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
const argv = [...operation.argv, ...args];
|
|
21
|
+
if (context !== undefined) {
|
|
22
|
+
argv.push("--argocd-context", context);
|
|
23
|
+
}
|
|
24
|
+
return argv;
|
|
25
|
+
}
|
|
26
|
+
export async function runArgocd(op, args, options = {}) {
|
|
27
|
+
if (!Object.hasOwn(OPERATIONS, op)) {
|
|
28
|
+
throw axiError(`internal guard: ${String(op)} is not a registered argocd operation`, CODES.REFUSED);
|
|
29
|
+
}
|
|
30
|
+
const argv = buildArgv(op, args, options.context);
|
|
31
|
+
const timeoutMs = options.timeoutMs ?? OPERATIONS[op].timeoutMs;
|
|
32
|
+
const result = await spawnCollect("argocd", argv, timeoutMs);
|
|
33
|
+
if (result.timedOut) {
|
|
34
|
+
throw axiError(`argocd did not answer within ${Math.round(timeoutMs / 1000)}s`, CODES.TIMEOUT, [
|
|
35
|
+
"Run `argocd-axi ctx` to confirm which server this context targets",
|
|
36
|
+
"Retry the command; if it keeps timing out, check network access to the server",
|
|
37
|
+
]);
|
|
38
|
+
}
|
|
39
|
+
if (result.exitCode !== 0 && !options.allowFailure) {
|
|
40
|
+
throw mapArgocdFailure(result.stderr || result.stdout, options.failure);
|
|
41
|
+
}
|
|
42
|
+
return result;
|
|
43
|
+
}
|
|
44
|
+
/** Run an operation whose stdout is JSON and parse it. */
|
|
45
|
+
export async function argocdJson(op, args, options = {}) {
|
|
46
|
+
const result = await runArgocd(op, args, options);
|
|
47
|
+
try {
|
|
48
|
+
return JSON.parse(result.stdout);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
throw axiError(`argocd returned output that is not JSON for ${op}`, CODES.UPSTREAM_ERROR, [
|
|
52
|
+
"Check that the argocd CLI version matches the server (`argocd-axi ctx`)",
|
|
53
|
+
]);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function spawnCollect(bin, argv, timeoutMs) {
|
|
57
|
+
return new Promise((resolve, reject) => {
|
|
58
|
+
const child = spawn(bin, argv, {
|
|
59
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
60
|
+
env: process.env,
|
|
61
|
+
windowsHide: true,
|
|
62
|
+
});
|
|
63
|
+
const stdout = [];
|
|
64
|
+
const stderr = [];
|
|
65
|
+
let bytes = 0;
|
|
66
|
+
let timedOut = false;
|
|
67
|
+
let overflow = false;
|
|
68
|
+
let settled = false;
|
|
69
|
+
const kill = () => {
|
|
70
|
+
child.kill("SIGTERM");
|
|
71
|
+
setTimeout(() => child.kill("SIGKILL"), KILL_GRACE_MS).unref();
|
|
72
|
+
};
|
|
73
|
+
const timer = setTimeout(() => {
|
|
74
|
+
timedOut = true;
|
|
75
|
+
kill();
|
|
76
|
+
}, timeoutMs);
|
|
77
|
+
const collect = (sink) => (chunk) => {
|
|
78
|
+
bytes += chunk.length;
|
|
79
|
+
if (bytes > MAX_OUTPUT_BYTES) {
|
|
80
|
+
overflow = true;
|
|
81
|
+
kill();
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
sink.push(chunk);
|
|
85
|
+
};
|
|
86
|
+
child.stdout.on("data", collect(stdout));
|
|
87
|
+
child.stderr.on("data", collect(stderr));
|
|
88
|
+
child.on("error", (error) => {
|
|
89
|
+
clearTimeout(timer);
|
|
90
|
+
if (settled)
|
|
91
|
+
return;
|
|
92
|
+
settled = true;
|
|
93
|
+
if (error.code === "ENOENT") {
|
|
94
|
+
reject(axiError("The argocd CLI is not installed or not on PATH", CODES.ARGOCD_NOT_INSTALLED, [
|
|
95
|
+
"A human installs the argocd CLI, ideally the version matching the server: https://argo-cd.readthedocs.io/en/stable/cli_installation/",
|
|
96
|
+
]));
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
reject(axiError(`could not start argocd: ${error.message}`, CODES.UPSTREAM_ERROR));
|
|
100
|
+
});
|
|
101
|
+
child.on("close", (code) => {
|
|
102
|
+
clearTimeout(timer);
|
|
103
|
+
if (settled)
|
|
104
|
+
return;
|
|
105
|
+
settled = true;
|
|
106
|
+
if (overflow) {
|
|
107
|
+
reject(axiError(`argocd produced more than ${MAX_OUTPUT_BYTES / 1024 / 1024} MB of output`, CODES.UPSTREAM_ERROR, ["Narrow the request (for example with --project or --kind)"]));
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
resolve({
|
|
111
|
+
stdout: Buffer.concat(stdout).toString("utf8"),
|
|
112
|
+
stderr: Buffer.concat(stderr).toString("utf8"),
|
|
113
|
+
exitCode: code ?? 1,
|
|
114
|
+
timedOut,
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
}
|