vortex-cli 6.2.1__tar.gz → 6.4.0__tar.gz
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.
- {vortex_cli-6.2.1 → vortex_cli-6.4.0}/PKG-INFO +53 -2
- {vortex_cli-6.2.1 → vortex_cli-6.4.0}/README.md +52 -1
- {vortex_cli-6.2.1 → vortex_cli-6.4.0}/pyproject.toml +1 -1
- {vortex_cli-6.2.1 → vortex_cli-6.4.0}/vortex/cli.py +84 -1
- vortex_cli-6.4.0/vortex/commands/keyword.py +309 -0
- vortex_cli-6.4.0/vortex/commands/push.py +303 -0
- {vortex_cli-6.2.1 → vortex_cli-6.4.0}/vortex/gateway.py +20 -0
- {vortex_cli-6.2.1 → vortex_cli-6.4.0}/vortex/main.py +20 -2
- {vortex_cli-6.2.1 → vortex_cli-6.4.0}/vortex_cli.egg-info/PKG-INFO +53 -2
- {vortex_cli-6.2.1 → vortex_cli-6.4.0}/vortex_cli.egg-info/SOURCES.txt +1 -0
- vortex_cli-6.2.1/vortex/commands/push.py +0 -132
- {vortex_cli-6.2.1 → vortex_cli-6.4.0}/LICENSE +0 -0
- {vortex_cli-6.2.1 → vortex_cli-6.4.0}/setup.cfg +0 -0
- {vortex_cli-6.2.1 → vortex_cli-6.4.0}/vortex/__init__.py +0 -0
- {vortex_cli-6.2.1 → vortex_cli-6.4.0}/vortex/__main__.py +0 -0
- {vortex_cli-6.2.1 → vortex_cli-6.4.0}/vortex/colour.py +0 -0
- {vortex_cli-6.2.1 → vortex_cli-6.4.0}/vortex/commands/__init__.py +0 -0
- {vortex_cli-6.2.1 → vortex_cli-6.4.0}/vortex/commands/agenda.py +0 -0
- {vortex_cli-6.2.1 → vortex_cli-6.4.0}/vortex/commands/clean.py +0 -0
- {vortex_cli-6.2.1 → vortex_cli-6.4.0}/vortex/commands/clone.py +0 -0
- {vortex_cli-6.2.1 → vortex_cli-6.4.0}/vortex/commands/code.py +0 -0
- {vortex_cli-6.2.1 → vortex_cli-6.4.0}/vortex/commands/compile.py +0 -0
- {vortex_cli-6.2.1 → vortex_cli-6.4.0}/vortex/commands/config.py +0 -0
- {vortex_cli-6.2.1 → vortex_cli-6.4.0}/vortex/commands/copy.py +0 -0
- {vortex_cli-6.2.1 → vortex_cli-6.4.0}/vortex/commands/db.py +0 -0
- {vortex_cli-6.2.1 → vortex_cli-6.4.0}/vortex/commands/delete.py +0 -0
- {vortex_cli-6.2.1 → vortex_cli-6.4.0}/vortex/commands/docs.py +0 -0
- {vortex_cli-6.2.1 → vortex_cli-6.4.0}/vortex/commands/execute.py +0 -0
- {vortex_cli-6.2.1 → vortex_cli-6.4.0}/vortex/commands/export.py +0 -0
- {vortex_cli-6.2.1 → vortex_cli-6.4.0}/vortex/commands/find.py +0 -0
- {vortex_cli-6.2.1 → vortex_cli-6.4.0}/vortex/commands/grep.py +0 -0
- {vortex_cli-6.2.1 → vortex_cli-6.4.0}/vortex/commands/import_.py +0 -0
- {vortex_cli-6.2.1 → vortex_cli-6.4.0}/vortex/commands/libs.py +0 -0
- {vortex_cli-6.2.1 → vortex_cli-6.4.0}/vortex/commands/list.py +0 -0
- {vortex_cli-6.2.1 → vortex_cli-6.4.0}/vortex/commands/log.py +0 -0
- {vortex_cli-6.2.1 → vortex_cli-6.4.0}/vortex/commands/new.py +0 -0
- {vortex_cli-6.2.1 → vortex_cli-6.4.0}/vortex/commands/pull.py +0 -0
- {vortex_cli-6.2.1 → vortex_cli-6.4.0}/vortex/commands/render.py +0 -0
- {vortex_cli-6.2.1 → vortex_cli-6.4.0}/vortex/commands/schema.py +0 -0
- {vortex_cli-6.2.1 → vortex_cli-6.4.0}/vortex/commands/status.py +0 -0
- {vortex_cli-6.2.1 → vortex_cli-6.4.0}/vortex/commands/undo.py +0 -0
- {vortex_cli-6.2.1 → vortex_cli-6.4.0}/vortex/commands/use.py +0 -0
- {vortex_cli-6.2.1 → vortex_cli-6.4.0}/vortex/commands/watch.py +0 -0
- {vortex_cli-6.2.1 → vortex_cli-6.4.0}/vortex/constants.py +0 -0
- {vortex_cli-6.2.1 → vortex_cli-6.4.0}/vortex/docs/Blackbook v2.md +0 -0
- {vortex_cli-6.2.1 → vortex_cli-6.4.0}/vortex/docs/Blackbook.pdf +0 -0
- {vortex_cli-6.2.1 → vortex_cli-6.4.0}/vortex/docs/index.html +0 -0
- {vortex_cli-6.2.1 → vortex_cli-6.4.0}/vortex/docs/marked.min.js +0 -0
- {vortex_cli-6.2.1 → vortex_cli-6.4.0}/vortex/lib/puakma-6.0.40.jar +0 -0
- {vortex_cli-6.2.1 → vortex_cli-6.4.0}/vortex/libs.py +0 -0
- {vortex_cli-6.2.1 → vortex_cli-6.4.0}/vortex/logging.py +0 -0
- {vortex_cli-6.2.1 → vortex_cli-6.4.0}/vortex/models.py +0 -0
- {vortex_cli-6.2.1 → vortex_cli-6.4.0}/vortex/soap.py +0 -0
- {vortex_cli-6.2.1 → vortex_cli-6.4.0}/vortex/spinner.py +0 -0
- {vortex_cli-6.2.1 → vortex_cli-6.4.0}/vortex/util.py +0 -0
- {vortex_cli-6.2.1 → vortex_cli-6.4.0}/vortex/webdesign.py +0 -0
- {vortex_cli-6.2.1 → vortex_cli-6.4.0}/vortex/workspace.py +0 -0
- {vortex_cli-6.2.1 → vortex_cli-6.4.0}/vortex_cli.egg-info/dependency_links.txt +0 -0
- {vortex_cli-6.2.1 → vortex_cli-6.4.0}/vortex_cli.egg-info/entry_points.txt +0 -0
- {vortex_cli-6.2.1 → vortex_cli-6.4.0}/vortex_cli.egg-info/requires.txt +0 -0
- {vortex_cli-6.2.1 → vortex_cli-6.4.0}/vortex_cli.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: vortex_cli
|
|
3
|
-
Version: 6.
|
|
3
|
+
Version: 6.4.0
|
|
4
4
|
Summary: Vortex CLI
|
|
5
5
|
Author-email: Jordan Amos <jordan.amos@gmail.com>
|
|
6
6
|
License: MIT License
|
|
@@ -108,6 +108,18 @@ While it is possible to use without it, this software has been purposefully desi
|
|
|
108
108
|
java_environment_name = JavaSE-17 ; Java Execution Environment name https://docs.osgi.org/reference/eenames.html
|
|
109
109
|
```
|
|
110
110
|
|
|
111
|
+
## Upgrading to 6.3
|
|
112
|
+
|
|
113
|
+
- **`vortex push` only uploads what changed.** On a `backend = gateway` server the push first
|
|
114
|
+
reads the server's element hashes and skips every file that already matches - a full-app push
|
|
115
|
+
that used to re-send (and journal) hundreds of identical files now sends the few that differ.
|
|
116
|
+
`--all` restores the old push-everything behaviour; `backend = soap` still pushes everything.
|
|
117
|
+
- **Uploads run concurrently** - up to `--jobs N` elements at once (default 8; `-j 1` is the
|
|
118
|
+
old one-at-a-time push). A Java element's source and class still go in that order, and a
|
|
119
|
+
failed source stops that element's class from being uploaded over it.
|
|
120
|
+
- **`push --name` prefers an exact match.** Previously `--name dashboard` also pushed a
|
|
121
|
+
`Dashboard` element; now the case-insensitive match only applies when nothing matches exactly.
|
|
122
|
+
|
|
111
123
|
## Upgrading to 6.0
|
|
112
124
|
|
|
113
125
|
6.0 is additive - **the default behaviour is unchanged**. Every server keeps using the SOAP
|
|
@@ -163,13 +175,14 @@ For a full list of commands see `--help`.
|
|
|
163
175
|
- `log`: View the server log.
|
|
164
176
|
- `status`: Show the server status - via the agent gateway when available, otherwise the raw console `status` output.
|
|
165
177
|
- `agenda`: List every scheduled action with its decoded schedule, last and next run, and whether it is overdue (read-only, gateway only).
|
|
166
|
-
- `push`: Upload local design files (source, compiled classes, pages, resources) to the server without a watch session - journaled and deploy-confirmed when routed via the gateway.
|
|
178
|
+
- `push`: Upload local design files (source, compiled classes, pages, resources) to the server without a watch session - journaled and deploy-confirmed when routed via the gateway. On a gateway server only files whose content differs from the server's are uploaded (compared by the server's element hashes; `--all` pushes everything), and elements upload concurrently (`--jobs N`, default 8) while each Java element's source always lands before its class. `--name` matches exactly first, case-insensitively only when nothing matches exactly.
|
|
167
179
|
- `pull`: Refresh cloned applications in place via the gateway's incremental sync - only elements changed since the last sync are downloaded. Refuses to overwrite locally modified files without `--force`.
|
|
168
180
|
- `render`: Render a PAGE server-side as your identity and print it (or `--out FILE`) - see a change without a browser login (gateway only).
|
|
169
181
|
- `undo`: The server-side undo journal. No arguments lists journal entries; with an entry id it dry-runs a restore, and `--confirm` writes the journaled version back (gateway only).
|
|
170
182
|
- `find`: Find Design Objects of cloned applications by name.
|
|
171
183
|
- `grep`: Search the contents of cloned Design Objects using a Regular Expression.
|
|
172
184
|
- `new`: Create new Design Objects, Applications, or Keywords. Use `--update <ID>` to update instead. Run without flags to launch an interactive wizard.
|
|
185
|
+
- `keyword` (or `kw`): Read or update an application's Keyword values. `vortex keyword 9` lists them, `-n <Name>` filters to one, and `-n <Name> --values ...` updates it, printing the prior value beside the new one before asking to continue. Secret-looking names are redacted unless `--reveal` is given - see [Keyword values and secrets](#keyword-values-and-secrets).
|
|
173
186
|
- `copy`: Copy a Design Object from one application to another.
|
|
174
187
|
- `delete`: Delete Design Objects by ID.
|
|
175
188
|
- `db`: Interact with Database Connections. Accepts `--server`/`-s` like other commands for one-off queries against another server.
|
|
@@ -179,6 +192,42 @@ For a full list of commands see `--help`.
|
|
|
179
192
|
- `docs`: Open the Tornado Server Blackbook.
|
|
180
193
|
- `execute`: Execute a command on the server.
|
|
181
194
|
|
|
195
|
+
### Keyword values and secrets
|
|
196
|
+
|
|
197
|
+
Keywords are an application's live configuration, and in practice they hold credentials in
|
|
198
|
+
cleartext - API keys, passwords, vendor secrets, signing keys. `vortex keyword` therefore
|
|
199
|
+
**redacts by default**, following the same precedent as `vortex config --show-server`:
|
|
200
|
+
|
|
201
|
+
```
|
|
202
|
+
vortex keyword 9 # list every Keyword (secrets redacted)
|
|
203
|
+
vortex keyword 9 -n AppVersion # just this one
|
|
204
|
+
vortex keyword 9 -n AppVersion --values 3.7.0 # update it
|
|
205
|
+
vortex keyword 9 --reveal # print secret values in full
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
- A Keyword is treated as secret when its **name** contains a marker such as `password`,
|
|
209
|
+
`passwd`, `passphrase`, `secret`, `credential`, `signature`, `apikey`, `privatekey`,
|
|
210
|
+
`keystore`, `webhook`, `connectionstring` or `mnemonic`, or when any *word* of the name
|
|
211
|
+
is one of `key`, `keys`, `pwd`, `token`, `salt`, `hash`, `private`, `auth`, `bearer`,
|
|
212
|
+
`cert`, `pem`, `jwt`, `dsn`, `otp`, `pin`, `seed` or `sig`. Names are split on
|
|
213
|
+
camelCase/snake_case/kebab-case, so `AccessKeyId` and `API_KEY` are redacted while
|
|
214
|
+
`Monkey` and `Concert` are not. The match is on the name only - a value-based heuristic
|
|
215
|
+
would leak the thing it is trying to hide on every near miss. Classification is
|
|
216
|
+
deliberately biased towards over-redaction: a false positive costs one `--reveal`, a
|
|
217
|
+
false negative prints a live credential.
|
|
218
|
+
- Redacted values print as `<redacted>`, one per value, so the value *count* stays visible.
|
|
219
|
+
- `--reveal` is the explicit opt-in. Without it, neither a listing nor an update's
|
|
220
|
+
before/after diff can put a live credential into your terminal scrollback, your shell
|
|
221
|
+
history or a CI log.
|
|
222
|
+
|
|
223
|
+
An update always prints the prior value beside the new one and waits for `[Y/y]` on stdin
|
|
224
|
+
before writing; on a `protected = true` server the server name must also be typed back
|
|
225
|
+
(`--yes` does not bypass that). The write uses the gateway's keyword upsert, so it never
|
|
226
|
+
inserts a duplicate KEYWORD row.
|
|
227
|
+
|
|
228
|
+
Reading does **not** require the application to be cloned locally - diagnosing a bad
|
|
229
|
+
configuration value should not depend on having a working copy.
|
|
230
|
+
|
|
182
231
|
### Cloning a whole group
|
|
183
232
|
|
|
184
233
|
`vortex clone` takes an ID, a TemplateName, an application group, or `group/name`:
|
|
@@ -353,6 +402,8 @@ needs no change to the gateway.
|
|
|
353
402
|
| `copy`, `new object` | webdesign `POST`/`PUT design`, `PUT design/params` | SOAP |
|
|
354
403
|
| `new app` | webdesign `POST vortex` | SOAP `saveApplication` |
|
|
355
404
|
| `new keyword` | gateway `keyword` (upsert) | SOAP `saveKeyword` |
|
|
405
|
+
| `keyword` (read) | gateway `download` (metadata window; `GatewayDesignRead`) | **unsupported** - SOAP has no keyword read call |
|
|
406
|
+
| `keyword --values` (write) | gateway `keyword` (upsert) | **unsupported** |
|
|
356
407
|
| `schema`, `db --list`, `db --schema` | gateway `dictionary` (webdesign's `database`/`table`/`column` routes run server-side; `GatewayDBRead`/`GatewayDBWrite`) | SOAP SQL + `savePuakma*` |
|
|
357
408
|
| `export` | webdesign `ExportPMX` action | SOAP `downloadPmx` |
|
|
358
409
|
| `import` | **still SOAP** - webdesign has no import route yet | SOAP `uploadPmx` |
|
|
@@ -64,6 +64,18 @@ While it is possible to use without it, this software has been purposefully desi
|
|
|
64
64
|
java_environment_name = JavaSE-17 ; Java Execution Environment name https://docs.osgi.org/reference/eenames.html
|
|
65
65
|
```
|
|
66
66
|
|
|
67
|
+
## Upgrading to 6.3
|
|
68
|
+
|
|
69
|
+
- **`vortex push` only uploads what changed.** On a `backend = gateway` server the push first
|
|
70
|
+
reads the server's element hashes and skips every file that already matches - a full-app push
|
|
71
|
+
that used to re-send (and journal) hundreds of identical files now sends the few that differ.
|
|
72
|
+
`--all` restores the old push-everything behaviour; `backend = soap` still pushes everything.
|
|
73
|
+
- **Uploads run concurrently** - up to `--jobs N` elements at once (default 8; `-j 1` is the
|
|
74
|
+
old one-at-a-time push). A Java element's source and class still go in that order, and a
|
|
75
|
+
failed source stops that element's class from being uploaded over it.
|
|
76
|
+
- **`push --name` prefers an exact match.** Previously `--name dashboard` also pushed a
|
|
77
|
+
`Dashboard` element; now the case-insensitive match only applies when nothing matches exactly.
|
|
78
|
+
|
|
67
79
|
## Upgrading to 6.0
|
|
68
80
|
|
|
69
81
|
6.0 is additive - **the default behaviour is unchanged**. Every server keeps using the SOAP
|
|
@@ -119,13 +131,14 @@ For a full list of commands see `--help`.
|
|
|
119
131
|
- `log`: View the server log.
|
|
120
132
|
- `status`: Show the server status - via the agent gateway when available, otherwise the raw console `status` output.
|
|
121
133
|
- `agenda`: List every scheduled action with its decoded schedule, last and next run, and whether it is overdue (read-only, gateway only).
|
|
122
|
-
- `push`: Upload local design files (source, compiled classes, pages, resources) to the server without a watch session - journaled and deploy-confirmed when routed via the gateway.
|
|
134
|
+
- `push`: Upload local design files (source, compiled classes, pages, resources) to the server without a watch session - journaled and deploy-confirmed when routed via the gateway. On a gateway server only files whose content differs from the server's are uploaded (compared by the server's element hashes; `--all` pushes everything), and elements upload concurrently (`--jobs N`, default 8) while each Java element's source always lands before its class. `--name` matches exactly first, case-insensitively only when nothing matches exactly.
|
|
123
135
|
- `pull`: Refresh cloned applications in place via the gateway's incremental sync - only elements changed since the last sync are downloaded. Refuses to overwrite locally modified files without `--force`.
|
|
124
136
|
- `render`: Render a PAGE server-side as your identity and print it (or `--out FILE`) - see a change without a browser login (gateway only).
|
|
125
137
|
- `undo`: The server-side undo journal. No arguments lists journal entries; with an entry id it dry-runs a restore, and `--confirm` writes the journaled version back (gateway only).
|
|
126
138
|
- `find`: Find Design Objects of cloned applications by name.
|
|
127
139
|
- `grep`: Search the contents of cloned Design Objects using a Regular Expression.
|
|
128
140
|
- `new`: Create new Design Objects, Applications, or Keywords. Use `--update <ID>` to update instead. Run without flags to launch an interactive wizard.
|
|
141
|
+
- `keyword` (or `kw`): Read or update an application's Keyword values. `vortex keyword 9` lists them, `-n <Name>` filters to one, and `-n <Name> --values ...` updates it, printing the prior value beside the new one before asking to continue. Secret-looking names are redacted unless `--reveal` is given - see [Keyword values and secrets](#keyword-values-and-secrets).
|
|
129
142
|
- `copy`: Copy a Design Object from one application to another.
|
|
130
143
|
- `delete`: Delete Design Objects by ID.
|
|
131
144
|
- `db`: Interact with Database Connections. Accepts `--server`/`-s` like other commands for one-off queries against another server.
|
|
@@ -135,6 +148,42 @@ For a full list of commands see `--help`.
|
|
|
135
148
|
- `docs`: Open the Tornado Server Blackbook.
|
|
136
149
|
- `execute`: Execute a command on the server.
|
|
137
150
|
|
|
151
|
+
### Keyword values and secrets
|
|
152
|
+
|
|
153
|
+
Keywords are an application's live configuration, and in practice they hold credentials in
|
|
154
|
+
cleartext - API keys, passwords, vendor secrets, signing keys. `vortex keyword` therefore
|
|
155
|
+
**redacts by default**, following the same precedent as `vortex config --show-server`:
|
|
156
|
+
|
|
157
|
+
```
|
|
158
|
+
vortex keyword 9 # list every Keyword (secrets redacted)
|
|
159
|
+
vortex keyword 9 -n AppVersion # just this one
|
|
160
|
+
vortex keyword 9 -n AppVersion --values 3.7.0 # update it
|
|
161
|
+
vortex keyword 9 --reveal # print secret values in full
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
- A Keyword is treated as secret when its **name** contains a marker such as `password`,
|
|
165
|
+
`passwd`, `passphrase`, `secret`, `credential`, `signature`, `apikey`, `privatekey`,
|
|
166
|
+
`keystore`, `webhook`, `connectionstring` or `mnemonic`, or when any *word* of the name
|
|
167
|
+
is one of `key`, `keys`, `pwd`, `token`, `salt`, `hash`, `private`, `auth`, `bearer`,
|
|
168
|
+
`cert`, `pem`, `jwt`, `dsn`, `otp`, `pin`, `seed` or `sig`. Names are split on
|
|
169
|
+
camelCase/snake_case/kebab-case, so `AccessKeyId` and `API_KEY` are redacted while
|
|
170
|
+
`Monkey` and `Concert` are not. The match is on the name only - a value-based heuristic
|
|
171
|
+
would leak the thing it is trying to hide on every near miss. Classification is
|
|
172
|
+
deliberately biased towards over-redaction: a false positive costs one `--reveal`, a
|
|
173
|
+
false negative prints a live credential.
|
|
174
|
+
- Redacted values print as `<redacted>`, one per value, so the value *count* stays visible.
|
|
175
|
+
- `--reveal` is the explicit opt-in. Without it, neither a listing nor an update's
|
|
176
|
+
before/after diff can put a live credential into your terminal scrollback, your shell
|
|
177
|
+
history or a CI log.
|
|
178
|
+
|
|
179
|
+
An update always prints the prior value beside the new one and waits for `[Y/y]` on stdin
|
|
180
|
+
before writing; on a `protected = true` server the server name must also be typed back
|
|
181
|
+
(`--yes` does not bypass that). The write uses the gateway's keyword upsert, so it never
|
|
182
|
+
inserts a duplicate KEYWORD row.
|
|
183
|
+
|
|
184
|
+
Reading does **not** require the application to be cloned locally - diagnosing a bad
|
|
185
|
+
configuration value should not depend on having a working copy.
|
|
186
|
+
|
|
138
187
|
### Cloning a whole group
|
|
139
188
|
|
|
140
189
|
`vortex clone` takes an ID, a TemplateName, an application group, or `group/name`:
|
|
@@ -309,6 +358,8 @@ needs no change to the gateway.
|
|
|
309
358
|
| `copy`, `new object` | webdesign `POST`/`PUT design`, `PUT design/params` | SOAP |
|
|
310
359
|
| `new app` | webdesign `POST vortex` | SOAP `saveApplication` |
|
|
311
360
|
| `new keyword` | gateway `keyword` (upsert) | SOAP `saveKeyword` |
|
|
361
|
+
| `keyword` (read) | gateway `download` (metadata window; `GatewayDesignRead`) | **unsupported** - SOAP has no keyword read call |
|
|
362
|
+
| `keyword --values` (write) | gateway `keyword` (upsert) | **unsupported** |
|
|
312
363
|
| `schema`, `db --list`, `db --schema` | gateway `dictionary` (webdesign's `database`/`table`/`column` routes run server-side; `GatewayDBRead`/`GatewayDBWrite`) | SOAP SQL + `savePuakma*` |
|
|
313
364
|
| `export` | webdesign `ExportPMX` action | SOAP `downloadPmx` |
|
|
314
365
|
| `import` | **still SOAP** - webdesign has no import route yet | SOAP `uploadPmx` |
|
|
@@ -14,6 +14,7 @@ from typing import NamedTuple
|
|
|
14
14
|
from vortex.commands.db import DEFAULT_LIMIT_N_RESULTS
|
|
15
15
|
from vortex.commands.db import MAX_LIMIT_N_RESULTS
|
|
16
16
|
from vortex.commands.db import MIN_LIMIT_N_RESULTS
|
|
17
|
+
from vortex.commands.push import DEFAULT_JOBS as DEFAULT_PUSH_JOBS
|
|
17
18
|
from vortex.models import DesignType
|
|
18
19
|
from vortex.models import PuakmaApplication
|
|
19
20
|
|
|
@@ -197,6 +198,7 @@ def validate_args(
|
|
|
197
198
|
new_parser: ArgumentParser,
|
|
198
199
|
clone_parser: ArgumentParser,
|
|
199
200
|
db_parser: ArgumentParser,
|
|
201
|
+
keyword_parser: ArgumentParser,
|
|
200
202
|
) -> None:
|
|
201
203
|
if args.command == "new":
|
|
202
204
|
msg = None
|
|
@@ -264,6 +266,11 @@ def validate_args(
|
|
|
264
266
|
clone_parser.error(
|
|
265
267
|
f"--group expects group names, not {', '.join(not_groups)}"
|
|
266
268
|
)
|
|
269
|
+
elif args.command in ("keyword", "kw"):
|
|
270
|
+
# A write needs a target: '--values' with no '--name' would
|
|
271
|
+
# otherwise be a silent no-op, or worse, ambiguous
|
|
272
|
+
if args.values is not None and not args.name:
|
|
273
|
+
keyword_parser.error("--values requires --name")
|
|
267
274
|
elif args.command == "db" and args.sql is not None:
|
|
268
275
|
try:
|
|
269
276
|
args.database = int(args.database)
|
|
@@ -718,7 +725,31 @@ def add_push_parser(command_parser: _SubParsersAction[ArgumentParser]) -> None:
|
|
|
718
725
|
"-n",
|
|
719
726
|
dest="name_filter",
|
|
720
727
|
default=None,
|
|
721
|
-
help=
|
|
728
|
+
help=(
|
|
729
|
+
"Only push the design element with this name (an exact match "
|
|
730
|
+
"wins; case-insensitive only when nothing matches exactly)"
|
|
731
|
+
),
|
|
732
|
+
)
|
|
733
|
+
push_parser.add_argument(
|
|
734
|
+
"--all",
|
|
735
|
+
"-a",
|
|
736
|
+
action="store_true",
|
|
737
|
+
help=(
|
|
738
|
+
"Push every local file, including ones whose content already "
|
|
739
|
+
"matches the server. By default (gateway backend) unchanged "
|
|
740
|
+
"files are skipped using the server's element hashes"
|
|
741
|
+
),
|
|
742
|
+
)
|
|
743
|
+
push_parser.add_argument(
|
|
744
|
+
"--jobs",
|
|
745
|
+
"-j",
|
|
746
|
+
type=int,
|
|
747
|
+
default=DEFAULT_PUSH_JOBS,
|
|
748
|
+
metavar="N",
|
|
749
|
+
help=(
|
|
750
|
+
"Maximum elements uploaded concurrently "
|
|
751
|
+
f"(default {DEFAULT_PUSH_JOBS}; 1 uploads one at a time)"
|
|
752
|
+
),
|
|
722
753
|
)
|
|
723
754
|
_add_server_option(push_parser)
|
|
724
755
|
|
|
@@ -981,6 +1012,58 @@ def add_new_parser(
|
|
|
981
1012
|
return new_parser
|
|
982
1013
|
|
|
983
1014
|
|
|
1015
|
+
def add_keyword_parser(
|
|
1016
|
+
command_parser: _SubParsersAction[ArgumentParser],
|
|
1017
|
+
) -> ArgumentParser:
|
|
1018
|
+
keyword_parser = command_parser.add_parser(
|
|
1019
|
+
"keyword",
|
|
1020
|
+
aliases=("kw",),
|
|
1021
|
+
help=(
|
|
1022
|
+
"Read or update an application's Keyword values. With no "
|
|
1023
|
+
"--values it lists them; with --values it updates one, "
|
|
1024
|
+
"showing the prior value first (gateway only)"
|
|
1025
|
+
),
|
|
1026
|
+
)
|
|
1027
|
+
keyword_parser.add_argument(
|
|
1028
|
+
"app_id",
|
|
1029
|
+
metavar="APP_ID",
|
|
1030
|
+
type=int,
|
|
1031
|
+
help="The application id whose Keywords to read or update",
|
|
1032
|
+
)
|
|
1033
|
+
keyword_parser.add_argument(
|
|
1034
|
+
"--name",
|
|
1035
|
+
"-n",
|
|
1036
|
+
help=(
|
|
1037
|
+
"Only this Keyword (exact, case-insensitive). Required when "
|
|
1038
|
+
"--values is given"
|
|
1039
|
+
),
|
|
1040
|
+
)
|
|
1041
|
+
keyword_parser.add_argument(
|
|
1042
|
+
"--values",
|
|
1043
|
+
nargs="*",
|
|
1044
|
+
default=None,
|
|
1045
|
+
metavar="VALUE",
|
|
1046
|
+
help=(
|
|
1047
|
+
"Set --name to these value(s), replacing what is there. Pass "
|
|
1048
|
+
"with no values to set it empty. Requires --name"
|
|
1049
|
+
),
|
|
1050
|
+
)
|
|
1051
|
+
keyword_parser.add_argument(
|
|
1052
|
+
"--reveal",
|
|
1053
|
+
action="store_true",
|
|
1054
|
+
help=(
|
|
1055
|
+
"Print secret-looking Keyword values in full. Values of "
|
|
1056
|
+
"Keywords whose NAME looks like a credential (password, "
|
|
1057
|
+
"secret, key, token, auth, cert, hash, salt, pin, otp, "
|
|
1058
|
+
"webhook, connection string and similar) are redacted by "
|
|
1059
|
+
"default so a plain listing cannot leak a live credential "
|
|
1060
|
+
"into the terminal, shell history or a CI log"
|
|
1061
|
+
),
|
|
1062
|
+
)
|
|
1063
|
+
_add_server_option(keyword_parser)
|
|
1064
|
+
return keyword_parser
|
|
1065
|
+
|
|
1066
|
+
|
|
984
1067
|
def add_copy_parser(command_parser: _SubParsersAction[ArgumentParser]) -> None:
|
|
985
1068
|
copy_parser = command_parser.add_parser(
|
|
986
1069
|
"copy", help="Copy a Design Object from one application to another"
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import re
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import tabulate
|
|
8
|
+
|
|
9
|
+
from vortex.colour import Colour
|
|
10
|
+
from vortex.gateway import GatewayClient
|
|
11
|
+
from vortex.gateway import GatewayRefused
|
|
12
|
+
from vortex.gateway import GatewayUnavailable
|
|
13
|
+
from vortex.gateway import refusal_detail
|
|
14
|
+
from vortex.models import PuakmaServer
|
|
15
|
+
from vortex.util import confirm_protected
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger("vortex")
|
|
18
|
+
|
|
19
|
+
_REFUSAL_HELP = {
|
|
20
|
+
"FORBIDDEN": (
|
|
21
|
+
"this identity does not hold the required gateway role on this "
|
|
22
|
+
"server (listing needs GatewayDesignRead; setting a value needs "
|
|
23
|
+
"GatewayDesignWrite - or Admin)"
|
|
24
|
+
),
|
|
25
|
+
"NOT_FOUND": "no application with that id on this server",
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
# Keyword values are an application's live configuration and routinely
|
|
29
|
+
# hold credentials in cleartext (API keys, passwords, vendor secrets,
|
|
30
|
+
# signing keys). Printing them by default would put them in the terminal
|
|
31
|
+
# scrollback, the shell history and any CI log that captured the run, so
|
|
32
|
+
# a Keyword whose NAME matches this is redacted unless --reveal is given.
|
|
33
|
+
# This follows the gateway's own 'server_config' precedent, which
|
|
34
|
+
# redacts the server's secrets before they ever reach the wire.
|
|
35
|
+
# Long, unambiguous markers - matched anywhere in the name.
|
|
36
|
+
SECRET_SUBSTRINGS = (
|
|
37
|
+
"password",
|
|
38
|
+
"passwd",
|
|
39
|
+
"passphrase",
|
|
40
|
+
"secret",
|
|
41
|
+
"credential",
|
|
42
|
+
"signature",
|
|
43
|
+
"mnemonic",
|
|
44
|
+
"keystore",
|
|
45
|
+
"truststore",
|
|
46
|
+
"keyphrase",
|
|
47
|
+
"connectionstring",
|
|
48
|
+
"connstring",
|
|
49
|
+
"webhook",
|
|
50
|
+
"apikey",
|
|
51
|
+
"privatekey",
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
# Short/ambiguous markers - matched only as a whole word of the name, so
|
|
55
|
+
# 'Monkey' and 'Spinner' are not mistaken for 'key' and 'pin'.
|
|
56
|
+
SECRET_WORDS = frozenset(
|
|
57
|
+
{
|
|
58
|
+
"pwd",
|
|
59
|
+
"token",
|
|
60
|
+
"tokens",
|
|
61
|
+
"salt",
|
|
62
|
+
"hash",
|
|
63
|
+
"key",
|
|
64
|
+
"keys",
|
|
65
|
+
"private",
|
|
66
|
+
"bearer",
|
|
67
|
+
"auth",
|
|
68
|
+
"authorization",
|
|
69
|
+
"authentication",
|
|
70
|
+
"cert",
|
|
71
|
+
"certificate",
|
|
72
|
+
"pem",
|
|
73
|
+
"pfx",
|
|
74
|
+
"jwt",
|
|
75
|
+
"dsn",
|
|
76
|
+
"otp",
|
|
77
|
+
"pin",
|
|
78
|
+
"seed",
|
|
79
|
+
"sig",
|
|
80
|
+
}
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
# camelCase / PascalCase / snake_case / kebab-case / ALLCAPS -> words
|
|
84
|
+
_WORD_PATTERN = re.compile(r"[A-Z]+(?![a-z])|[A-Z][a-z]*|[a-z]+|\d+")
|
|
85
|
+
|
|
86
|
+
REDACTED = "<redacted>"
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def name_words(name: str) -> list[str]:
|
|
90
|
+
"""The name split into lowercased words, however it is cased."""
|
|
91
|
+
return [w.casefold() for w in _WORD_PATTERN.findall(name)]
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def is_secret_name(name: str) -> bool:
|
|
95
|
+
"""
|
|
96
|
+
True when a Keyword name looks like it holds a credential. Matched
|
|
97
|
+
on the NAME only - a value-based heuristic would leak the very thing
|
|
98
|
+
it is trying to hide on every near miss.
|
|
99
|
+
|
|
100
|
+
Deliberately biased towards over-redaction: a false positive costs
|
|
101
|
+
one '--reveal', a false negative prints a live credential into the
|
|
102
|
+
terminal scrollback, the shell history and any CI log.
|
|
103
|
+
"""
|
|
104
|
+
folded = name.casefold()
|
|
105
|
+
if any(marker in folded for marker in SECRET_SUBSTRINGS):
|
|
106
|
+
return True
|
|
107
|
+
return any(word in SECRET_WORDS for word in name_words(name))
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def redact_values(name: str, values: list[str], reveal: bool) -> list[str]:
|
|
111
|
+
"""
|
|
112
|
+
What to display for a Keyword: the real values, or one <redacted>
|
|
113
|
+
marker per value (the COUNT is safe and useful; the content is not).
|
|
114
|
+
"""
|
|
115
|
+
if reveal or not is_secret_name(name):
|
|
116
|
+
return list(values)
|
|
117
|
+
return [REDACTED for _ in values]
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _format_values(values: list[str]) -> str:
|
|
121
|
+
return " | ".join(values)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _app_label(app: dict[str, Any], app_id: int) -> str:
|
|
125
|
+
group = str(app.get("group") or "")
|
|
126
|
+
name = str(app.get("name") or "")
|
|
127
|
+
label = f"{group}/{name}".strip("/")
|
|
128
|
+
return f"{label} [{app_id}]" if label else f"[{app_id}]"
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _fetch(
|
|
132
|
+
client: GatewayClient, app_id: int, name: str | None
|
|
133
|
+
) -> tuple[dict[str, Any], list[dict[str, Any]]]:
|
|
134
|
+
app, rows = client.get_keywords(app_id)
|
|
135
|
+
if name is not None:
|
|
136
|
+
needle = name.casefold()
|
|
137
|
+
rows = [r for r in rows if str(r.get("name", "")).casefold() == needle]
|
|
138
|
+
return app, rows
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _client_or_error(server: PuakmaServer) -> GatewayClient | None:
|
|
142
|
+
"""
|
|
143
|
+
The gateway client, or None with the reason already logged. Keywords
|
|
144
|
+
are gateway-only here: the legacy SOAP designer has no keyword READ
|
|
145
|
+
call at all, which is exactly why reading one was impossible before
|
|
146
|
+
this command existed.
|
|
147
|
+
"""
|
|
148
|
+
try:
|
|
149
|
+
client = server.negotiate_gateway()
|
|
150
|
+
except GatewayUnavailable as e:
|
|
151
|
+
logger.error(e)
|
|
152
|
+
return None
|
|
153
|
+
if client is None:
|
|
154
|
+
logger.error(
|
|
155
|
+
"'vortex keyword' needs the gateway backend "
|
|
156
|
+
f"(set 'backend = gateway' for '{server.name}') - the legacy "
|
|
157
|
+
"SOAP designer has no keyword read call"
|
|
158
|
+
)
|
|
159
|
+
return None
|
|
160
|
+
return client
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def keyword(
|
|
164
|
+
server: PuakmaServer,
|
|
165
|
+
app_id: int,
|
|
166
|
+
*,
|
|
167
|
+
name: str | None = None,
|
|
168
|
+
values: list[str] | None = None,
|
|
169
|
+
reveal: bool = False,
|
|
170
|
+
yes: bool = False,
|
|
171
|
+
) -> int:
|
|
172
|
+
"""
|
|
173
|
+
Read or update an application's Keyword values.
|
|
174
|
+
|
|
175
|
+
With no --values this LISTS Keywords (all of them, or just --name).
|
|
176
|
+
With --values it UPDATES --name, showing the prior value beside the
|
|
177
|
+
new one before writing, so the change is visible rather than blind;
|
|
178
|
+
the write goes through the gateway's keyword upsert, which never
|
|
179
|
+
inserts a duplicate row.
|
|
180
|
+
|
|
181
|
+
The application does NOT need to be cloned locally - diagnosing a
|
|
182
|
+
bad configuration value must not require a working copy.
|
|
183
|
+
"""
|
|
184
|
+
if values is not None:
|
|
185
|
+
if not name:
|
|
186
|
+
# argparse rejects this first; belt-and-braces so a direct
|
|
187
|
+
# call cannot upsert a nameless Keyword
|
|
188
|
+
logger.error("--values requires --name")
|
|
189
|
+
return 1
|
|
190
|
+
return _set_keyword(server, app_id, name, values, reveal=reveal, yes=yes)
|
|
191
|
+
return _list_keywords(server, app_id, name, reveal=reveal)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _list_keywords(
|
|
195
|
+
server: PuakmaServer,
|
|
196
|
+
app_id: int,
|
|
197
|
+
name: str | None,
|
|
198
|
+
*,
|
|
199
|
+
reveal: bool,
|
|
200
|
+
) -> int:
|
|
201
|
+
with server as s:
|
|
202
|
+
client = _client_or_error(s)
|
|
203
|
+
if client is None:
|
|
204
|
+
return 1
|
|
205
|
+
try:
|
|
206
|
+
app, rows = _fetch(client, app_id, name)
|
|
207
|
+
except GatewayRefused as e:
|
|
208
|
+
logger.error(refusal_detail(e, _REFUSAL_HELP))
|
|
209
|
+
return 1
|
|
210
|
+
except GatewayUnavailable as e:
|
|
211
|
+
logger.error(e)
|
|
212
|
+
return 1
|
|
213
|
+
|
|
214
|
+
label = _app_label(app, app_id)
|
|
215
|
+
if not rows:
|
|
216
|
+
if name:
|
|
217
|
+
logger.error(f"No Keyword named '{name}' in {label}")
|
|
218
|
+
return 1
|
|
219
|
+
print(f"[{server.name}] {label} has no Keywords.")
|
|
220
|
+
return 0
|
|
221
|
+
|
|
222
|
+
redacted = 0
|
|
223
|
+
table = []
|
|
224
|
+
for row in sorted(rows, key=lambda r: str(r.get("name", "")).casefold()):
|
|
225
|
+
kw_name = str(row.get("name") or "")
|
|
226
|
+
raw = [str(v) for v in (row.get("values") or [])]
|
|
227
|
+
shown = redact_values(kw_name, raw, reveal)
|
|
228
|
+
if shown != raw:
|
|
229
|
+
redacted += 1
|
|
230
|
+
flag = "DUPLICATE" if row.get("duplicate") else ""
|
|
231
|
+
table.append([row.get("id"), kw_name, _format_values(shown), flag])
|
|
232
|
+
|
|
233
|
+
header = f"[{server.name}] {label} - {len(table)} Keyword(s)"
|
|
234
|
+
if redacted:
|
|
235
|
+
header += f", {redacted} redacted (--reveal to show)"
|
|
236
|
+
print(header)
|
|
237
|
+
print(tabulate.tabulate(table, headers=["ID", "Name", "Values", ""]))
|
|
238
|
+
return 0
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _set_keyword(
|
|
242
|
+
server: PuakmaServer,
|
|
243
|
+
app_id: int,
|
|
244
|
+
name: str,
|
|
245
|
+
values: list[str],
|
|
246
|
+
*,
|
|
247
|
+
reveal: bool,
|
|
248
|
+
yes: bool,
|
|
249
|
+
) -> int:
|
|
250
|
+
with server as s:
|
|
251
|
+
client = _client_or_error(s)
|
|
252
|
+
if client is None:
|
|
253
|
+
return 1
|
|
254
|
+
|
|
255
|
+
try:
|
|
256
|
+
app, existing = _fetch(client, app_id, name)
|
|
257
|
+
except GatewayRefused as e:
|
|
258
|
+
logger.error(refusal_detail(e, _REFUSAL_HELP))
|
|
259
|
+
return 1
|
|
260
|
+
except GatewayUnavailable as e:
|
|
261
|
+
logger.error(e)
|
|
262
|
+
return 1
|
|
263
|
+
|
|
264
|
+
prior = (
|
|
265
|
+
[str(v) for v in (existing[0].get("values") or [])] if existing else None
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
# Showing the prior value IS the feature: a blind write is how a
|
|
269
|
+
# bad Keyword value becomes an outage nobody can diagnose. A
|
|
270
|
+
# secret-looking name stays redacted even in the diff.
|
|
271
|
+
label = _app_label(app, app_id)
|
|
272
|
+
verb = (
|
|
273
|
+
Colour.colour("UPDATED", Colour.YELLOW)
|
|
274
|
+
if existing
|
|
275
|
+
else Colour.colour("CREATED", Colour.GREEN)
|
|
276
|
+
)
|
|
277
|
+
print(f"Keyword '{name}' in {label} will be {verb}:\n")
|
|
278
|
+
if prior is None:
|
|
279
|
+
print(" from: (does not exist)")
|
|
280
|
+
else:
|
|
281
|
+
print(f" from: {_format_values(redact_values(name, prior, reveal))}")
|
|
282
|
+
print(f" to: {_format_values(redact_values(name, values, reveal))}")
|
|
283
|
+
if prior is not None and prior == values:
|
|
284
|
+
print("\nThe Keyword already has this value; nothing to do.")
|
|
285
|
+
return 0
|
|
286
|
+
|
|
287
|
+
if not yes and input("\n[Y/y] to continue:") not in ["Y", "y"]:
|
|
288
|
+
logger.error("Operation Cancelled")
|
|
289
|
+
return 1
|
|
290
|
+
# Deliberately NOT bypassed by --yes: on a protected server the
|
|
291
|
+
# name must still be typed back
|
|
292
|
+
if not confirm_protected(s, f"update the Keyword '{name}'"):
|
|
293
|
+
return 1
|
|
294
|
+
|
|
295
|
+
try:
|
|
296
|
+
reply = client.keyword_upsert(app_id, name, values)
|
|
297
|
+
except GatewayRefused as e:
|
|
298
|
+
logger.error(refusal_detail(e, _REFUSAL_HELP))
|
|
299
|
+
return 1
|
|
300
|
+
except GatewayUnavailable as e:
|
|
301
|
+
logger.error(e)
|
|
302
|
+
return 1
|
|
303
|
+
|
|
304
|
+
dupes = int(reply.get("duplicatesConsolidated") or 0)
|
|
305
|
+
extra = f" ({dupes} duplicate row(s) consolidated)" if dupes else ""
|
|
306
|
+
logger.info(
|
|
307
|
+
f"Upserted Keyword '{name}' [{reply.get('keywordId')}] via gateway{extra}"
|
|
308
|
+
)
|
|
309
|
+
return 0
|