devlift-cli 0.1.4__tar.gz → 0.1.6__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.
- {devlift_cli-0.1.4 → devlift_cli-0.1.6}/PKG-INFO +1 -1
- {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli/MANUAL.md +36 -15
- {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli/__init__.py +1 -1
- {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli/commands/catalog.py +28 -21
- {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli/commands/clusters.py +16 -22
- {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli/commands/eks.py +28 -4
- {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli/ops/resources.py +65 -3
- {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli/skill/SKILL.md +17 -0
- {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli/skill/reference/commands.md +2 -2
- {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli/skill/reference/workflows.md +3 -1
- {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli.egg-info/PKG-INFO +1 -1
- {devlift_cli-0.1.4 → devlift_cli-0.1.6}/README.md +0 -0
- {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli/__main__.py +0 -0
- {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli/api/__init__.py +0 -0
- {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli/api/approvals.py +0 -0
- {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli/api/catalog.py +0 -0
- {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli/api/client.py +0 -0
- {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli/api/context.py +0 -0
- {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli/api/deployments.py +0 -0
- {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli/api/infra.py +0 -0
- {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli/api/infra_list.py +0 -0
- {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli/api/kong.py +0 -0
- {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli/api/services.py +0 -0
- {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli/api/vpc.py +0 -0
- {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli/app.py +0 -0
- {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli/auth/__init__.py +0 -0
- {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli/auth/oauth.py +0 -0
- {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli/auth/session.py +0 -0
- {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli/auth/storage.py +0 -0
- {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli/commands/__init__.py +0 -0
- {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli/commands/approval.py +0 -0
- {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli/commands/auth.py +0 -0
- {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli/commands/deployment.py +0 -0
- {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli/commands/dynamodb.py +0 -0
- {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli/commands/kong.py +0 -0
- {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli/commands/languages.py +0 -0
- {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli/commands/manual.py +0 -0
- {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli/commands/repositories.py +0 -0
- {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli/commands/request.py +0 -0
- {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli/commands/s3.py +0 -0
- {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli/commands/skill.py +0 -0
- {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli/commands/sqs.py +0 -0
- {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli/config.py +0 -0
- {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli/context.py +0 -0
- {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli/data/placement/vance.json +0 -0
- {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli/errors.py +0 -0
- {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli/ops/__init__.py +0 -0
- {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli/ops/approvals.py +0 -0
- {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli/ops/eks.py +0 -0
- {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli/ops/kong.py +0 -0
- {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli/ops/placement.py +0 -0
- {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli/ops/status.py +0 -0
- {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli/ops/wait.py +0 -0
- {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli/render/__init__.py +0 -0
- {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli/render/output.py +0 -0
- {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli/resolve/__init__.py +0 -0
- {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli/resolve/allowlist.py +0 -0
- {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli/resolve/names.py +0 -0
- {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli/skill/reference/exit-codes.md +0 -0
- {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli.egg-info/SOURCES.txt +0 -0
- {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli.egg-info/dependency_links.txt +0 -0
- {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli.egg-info/entry_points.txt +0 -0
- {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli.egg-info/requires.txt +0 -0
- {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli.egg-info/top_level.txt +0 -0
- {devlift_cli-0.1.4 → devlift_cli-0.1.6}/pyproject.toml +0 -0
- {devlift_cli-0.1.4 → devlift_cli-0.1.6}/setup.cfg +0 -0
|
@@ -136,17 +136,33 @@ error: s3 is not offered in 'canada' for core/stage.
|
|
|
136
136
|
Available there: Mumbai.
|
|
137
137
|
```
|
|
138
138
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
139
|
+
**Every list is filtered by it**, so what you are shown is what you can
|
|
140
|
+
actually work with: `applications list`, `environments list`, `regions list`,
|
|
141
|
+
`resource-groups list`, `clusters list`, `services list`, and the resource
|
|
142
|
+
lists `s3 list`, `sqs list` and `dynamodb list`. Each says how many rows it
|
|
143
|
+
withheld:
|
|
142
144
|
|
|
143
145
|
```
|
|
144
|
-
devlift applications list core
|
|
146
|
+
devlift applications list core
|
|
145
147
|
devlift environments list core/stage: Mumbai
|
|
146
148
|
devlift regions list --for s3 Mumbai
|
|
147
|
-
devlift
|
|
149
|
+
devlift s3 list 63 of 212 hidden: not a placement your allowlist defines.
|
|
148
150
|
```
|
|
149
151
|
|
|
152
|
+
There is no flag to see past it. A resource in a placement the allowlist does
|
|
153
|
+
not define cannot be created there, so listing it only invites someone to
|
|
154
|
+
pick it. Looking one up by name is refused for the same reason, naming the
|
|
155
|
+
placement rather than claiming the resource does not exist:
|
|
156
|
+
|
|
157
|
+
```
|
|
158
|
+
error: 'email-platform' is in prod/region-aspora-london, which your placement
|
|
159
|
+
allowlist does not define.
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
`services list` is the one partial case. A service row carries an application
|
|
163
|
+
but no environment or region, because a service is not placed anywhere until
|
|
164
|
+
it has a configuration, so only the product can be matched there.
|
|
165
|
+
|
|
150
166
|
Service keys are `s3`, `sqs`, `dynamo`, `eks`, `gateway` and `database`.
|
|
151
167
|
Matching is on names, not codes: product `core`, environment `stage`, region
|
|
152
168
|
`ap-south-1` resolved to its city label. A tenant with no file is not
|
|
@@ -185,11 +201,10 @@ login waits up to five minutes for the redirect.
|
|
|
185
201
|
### applications, environments, regions, resource-types, resource-groups, repositories, languages
|
|
186
202
|
|
|
187
203
|
```
|
|
188
|
-
devlift applications list [--active]
|
|
189
|
-
devlift environments list [--app NAME]
|
|
204
|
+
devlift applications list [--active]
|
|
205
|
+
devlift environments list [--app NAME] Environments and their regions per application.
|
|
190
206
|
devlift regions list [--app NAME] [--env ENV] Regions you can place resources in (the --region values).
|
|
191
207
|
devlift regions list --for s3 Only the regions that really offer that service.
|
|
192
|
-
devlift regions list --all Skip the placement allowlist (section 5).
|
|
193
208
|
devlift regions list --vendor aws The cloud vendor's raw region list.
|
|
194
209
|
devlift resource-types list [--family FAMILY] Infrastructure types (storage, compute, network, …).
|
|
195
210
|
devlift resource-groups list [--app NAME] Resource groups; services are created inside one.
|
|
@@ -223,7 +238,7 @@ devlift languages list Languages and versions a service can use (--langua
|
|
|
223
238
|
### clusters
|
|
224
239
|
|
|
225
240
|
```
|
|
226
|
-
devlift clusters list [--app NAME] [--env ENV] [--region REGION] [--type eks|ecs] [--search TEXT]
|
|
241
|
+
devlift clusters list [--app NAME] [--env ENV] [--region REGION] [--type eks|ecs] [--search TEXT]
|
|
227
242
|
```
|
|
228
243
|
|
|
229
244
|
The clusters services run on, and the `--cluster` values for `eks create`.
|
|
@@ -231,16 +246,14 @@ Narrowed by three filters, the same ones a create goes through: the backend
|
|
|
231
246
|
reports only the environments this deployment serves, the placement allowlist
|
|
232
247
|
(section 5) says which of those offer EKS or ECS, and only a **registered**
|
|
233
248
|
cluster may take a new service. Anything outside those is real but can never
|
|
234
|
-
be chosen, so it is hidden and counted.
|
|
235
|
-
everything the backend reports, for checking what exists rather than for
|
|
236
|
-
choosing.
|
|
249
|
+
be chosen, so it is hidden and counted. There is no flag to see past it.
|
|
237
250
|
|
|
238
251
|
```
|
|
239
252
|
devlift clusters list --type eks --app core --env stage
|
|
240
253
|
```
|
|
241
254
|
|
|
242
255
|
```
|
|
243
|
-
22 of 25 hidden: not a placement
|
|
256
|
+
22 of 25 hidden: not a placement your allowlist defines.
|
|
244
257
|
2 more hidden: not registered, so no service can be created on them.
|
|
245
258
|
|
|
246
259
|
Cluster Type Environment Region Application Registered
|
|
@@ -509,7 +522,7 @@ devlift eks create --name NAME --app APP --env ENV --region REGION --type api|wo
|
|
|
509
522
|
[--set key=value ...] [--public] [-y]
|
|
510
523
|
devlift eks settings Every key --set accepts, and when each applies.
|
|
511
524
|
devlift eks settings <service> Only the keys that apply to it, with its current values.
|
|
512
|
-
devlift eks show <service> --env ENV The configuration
|
|
525
|
+
devlift eks show <service> --env ENV The configuration, with any pending draft over it; pending rows name the deployed value too.
|
|
513
526
|
devlift eks diff <service> --env ENV Preview the pending change: Field | Deployed | Requested.
|
|
514
527
|
devlift eks diff <service> --deployed The live configuration versus the last deployment.
|
|
515
528
|
devlift eks edit <service> --env ENV [--set key=value ...]
|
|
@@ -752,7 +765,15 @@ last deployed. That one ignores drafts entirely, so on a service whose
|
|
|
752
765
|
change is still a draft it shows the live row and its defaults, not what the
|
|
753
766
|
draft proposes.
|
|
754
767
|
|
|
755
|
-
|
|
768
|
+
**`eks show` is not "what is deployed" while a request is open.** Its
|
|
769
|
+
Configuration table is the live configuration with the open draft laid over
|
|
770
|
+
it, so a pending row shows the value the draft *proposes*. Each such row is
|
|
771
|
+
marked `pending (<status>) — deployed <value>`, so both numbers are visible.
|
|
772
|
+
|
|
773
|
+
In `-o json` the same thing is spelled out: `settings` is the merged view,
|
|
774
|
+
`deployed_settings` is what is actually running, and `pending` gives
|
|
775
|
+
`{deployed, requested}` for each changed key. When `pending_request` is null
|
|
776
|
+
there is no draft and `settings` is simply the deployed configuration.
|
|
756
777
|
|
|
757
778
|
### kong
|
|
758
779
|
|
|
@@ -20,7 +20,6 @@ resource_groups_app = typer.Typer(help="Resource groups (the permission boundary
|
|
|
20
20
|
services_app = typer.Typer(help="Services in your tenant.", no_args_is_help=True)
|
|
21
21
|
|
|
22
22
|
_NO_CACHE = typer.Option(False, "--no-cache", help="Refetch instead of using the short-lived local cache.")
|
|
23
|
-
_ALL = typer.Option(False, "--all", help="Everything the backend reports, without the placement allowlist.")
|
|
24
23
|
|
|
25
24
|
|
|
26
25
|
def _resolver(inv: Invocation, no_cache: bool) -> Resolver:
|
|
@@ -30,21 +29,20 @@ def _resolver(inv: Invocation, no_cache: bool) -> Resolver:
|
|
|
30
29
|
def _hidden(before: int, after: int) -> None:
|
|
31
30
|
"""Say when the allowlist removed rows, so the absence is never a mystery."""
|
|
32
31
|
if after < before:
|
|
33
|
-
output.info(f"{before - after} of {before} hidden
|
|
32
|
+
output.info(f"{before - after} of {before} hidden: not a placement your allowlist defines.")
|
|
34
33
|
|
|
35
34
|
|
|
36
35
|
@applications_app.command("list")
|
|
37
|
-
def applications_list(ctx: typer.Context, active_only: bool = typer.Option(False, "--active", help="Only active applications."),
|
|
36
|
+
def applications_list(ctx: typer.Context, active_only: bool = typer.Option(False, "--active", help="Only active applications."), no_cache: bool = _NO_CACHE):
|
|
38
37
|
"""List applications that have a real placement (see section 5)."""
|
|
39
38
|
inv: Invocation = ctx.obj
|
|
40
39
|
res = _resolver(inv, no_cache)
|
|
41
40
|
apps = res.applications()
|
|
42
41
|
if active_only:
|
|
43
42
|
apps = [a for a in apps if a.get("is_active")]
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
_hidden(before, len(apps))
|
|
43
|
+
before = len(apps)
|
|
44
|
+
apps = allowlist.filter_applications(apps, res.placements(), res.tenant_code())
|
|
45
|
+
_hidden(before, len(apps))
|
|
48
46
|
inv.emit(apps, lambda d: rows_table(
|
|
49
47
|
["Name", "Code", "Services", "Groups", "Status"],
|
|
50
48
|
[(a.get("application_name"), a.get("application_code"), a.get("services_count"), a.get("resource_groups_count"), a.get("status")) for a in d],
|
|
@@ -52,16 +50,15 @@ def applications_list(ctx: typer.Context, active_only: bool = typer.Option(False
|
|
|
52
50
|
|
|
53
51
|
|
|
54
52
|
@environments_app.command("list")
|
|
55
|
-
def environments_list(ctx: typer.Context, app: str | None = typer.Option(None, "--app", "--product", help="Only this application."),
|
|
53
|
+
def environments_list(ctx: typer.Context, app: str | None = typer.Option(None, "--app", "--product", help="Only this application."), no_cache: bool = _NO_CACHE):
|
|
56
54
|
"""List environments, and for each application the regions it can use."""
|
|
57
55
|
inv: Invocation = ctx.obj
|
|
58
56
|
res = _resolver(inv, no_cache)
|
|
59
57
|
app_code = res.application(app)["application_code"] if app else None
|
|
60
58
|
rows = res.regions_for(app_code)
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
_hidden(before, len(rows))
|
|
59
|
+
before = len(rows)
|
|
60
|
+
rows = allowlist.filter_placements(rows, res.tenant_code())
|
|
61
|
+
_hidden(before, len(rows))
|
|
65
62
|
grouped: dict[tuple[str, str], list[str]] = {}
|
|
66
63
|
for r in rows:
|
|
67
64
|
grouped.setdefault((r["product"], r["environment"]), []).append(r["region"])
|
|
@@ -79,14 +76,13 @@ def regions_list(
|
|
|
79
76
|
env: str | None = typer.Option(None, "--env", help="Only this environment."),
|
|
80
77
|
vendor: str | None = typer.Option(None, "--vendor", help="Show the cloud vendor's raw region list instead (e.g. aws)."),
|
|
81
78
|
for_service: str | None = typer.Option(None, "--for", help="Only regions that offer this: s3 | sqs | dynamo | eks | gateway | database."),
|
|
82
|
-
show_all: bool = _ALL,
|
|
83
79
|
no_cache: bool = _NO_CACHE,
|
|
84
80
|
):
|
|
85
81
|
"""List the regions (placements) available to you. These are the --region values.
|
|
86
82
|
|
|
87
83
|
Filtered by the placement allowlist (section 5), which is narrower than
|
|
88
84
|
what the backend reports. `--for s3` narrows further to one service, the
|
|
89
|
-
same check a create command makes.
|
|
85
|
+
same check a create command makes.
|
|
90
86
|
"""
|
|
91
87
|
inv: Invocation = ctx.obj
|
|
92
88
|
if vendor:
|
|
@@ -97,10 +93,9 @@ def regions_list(
|
|
|
97
93
|
app_code = res.application(app)["application_code"] if app else None
|
|
98
94
|
environment = res.environment(env) if env else None
|
|
99
95
|
rows = res.regions_for(app_code, environment)
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
_hidden(before, len(rows))
|
|
96
|
+
before = len(rows)
|
|
97
|
+
rows = allowlist.filter_placements(rows, res.tenant_code(), for_service.strip().lower() if for_service else None)
|
|
98
|
+
_hidden(before, len(rows))
|
|
104
99
|
seen = {}
|
|
105
100
|
for r in rows:
|
|
106
101
|
seen.setdefault(r["geo_loc_mst_code"], {"region": r["region"], "geo_loc_mst_code": r["geo_loc_mst_code"], "placements": []})
|
|
@@ -120,7 +115,7 @@ def resource_types_list(ctx: typer.Context, family: str | None = typer.Option(No
|
|
|
120
115
|
|
|
121
116
|
|
|
122
117
|
@resource_groups_app.command("list")
|
|
123
|
-
def resource_groups_list(ctx: typer.Context, app: str | None = typer.Option(None, "--app", "--product", help="Only groups of this application."),
|
|
118
|
+
def resource_groups_list(ctx: typer.Context, app: str | None = typer.Option(None, "--app", "--product", help="Only groups of this application."), no_cache: bool = _NO_CACHE):
|
|
124
119
|
"""List resource groups. Services are created inside one.
|
|
125
120
|
|
|
126
121
|
Only groups of applications you can actually place something in, so the
|
|
@@ -134,7 +129,7 @@ def resource_groups_list(ctx: typer.Context, app: str | None = typer.Option(None
|
|
|
134
129
|
if app:
|
|
135
130
|
code = res.application(app)["application_code"]
|
|
136
131
|
groups = [g for g in groups if g.get("applications_mst_code") == code]
|
|
137
|
-
|
|
132
|
+
else:
|
|
138
133
|
before = len(groups)
|
|
139
134
|
allowed = {r["application_code"] for r in allowlist.filter_placements(res.placements(), res.tenant_code())}
|
|
140
135
|
if allowed:
|
|
@@ -150,13 +145,25 @@ def services_list(
|
|
|
150
145
|
search: str | None = typer.Option(None, "--search", help="Name contains this text."),
|
|
151
146
|
no_cache: bool = _NO_CACHE,
|
|
152
147
|
):
|
|
153
|
-
"""List services."""
|
|
148
|
+
"""List services of applications your placement allowlist defines (section 5)."""
|
|
154
149
|
inv: Invocation = ctx.obj
|
|
155
150
|
res = _resolver(inv, no_cache)
|
|
156
151
|
services = res.services()
|
|
157
152
|
if app:
|
|
158
153
|
code = res.application(app)["application_code"]
|
|
159
154
|
services = [s for s in services if s.get("application_code") == code]
|
|
155
|
+
else:
|
|
156
|
+
# Application is the only placement dimension a service row carries: a
|
|
157
|
+
# service is not placed anywhere until it has a configuration per
|
|
158
|
+
# environment, so there is no environment or region here to match on.
|
|
159
|
+
# Filtering on the product alone is still worth doing — otherwise
|
|
160
|
+
# `applications list` says a product does not exist while this command
|
|
161
|
+
# lists its services.
|
|
162
|
+
before = len(services)
|
|
163
|
+
allowed = {r["application_code"] for r in allowlist.filter_placements(res.placements(), res.tenant_code())}
|
|
164
|
+
if allowed:
|
|
165
|
+
services = [s for s in services if s.get("application_code") in allowed]
|
|
166
|
+
_hidden(before, len(services))
|
|
160
167
|
if search:
|
|
161
168
|
services = [s for s in services if search.lower() in (s.get("service_name") or "").lower()]
|
|
162
169
|
inv.emit(services, lambda d: rows_table(
|
|
@@ -62,7 +62,6 @@ def clusters_list(
|
|
|
62
62
|
region: str | None = typer.Option(None, "--region", help="Only this region."),
|
|
63
63
|
kind: str | None = typer.Option(None, "--type", help="eks | ecs. Both when omitted."),
|
|
64
64
|
search: str | None = typer.Option(None, "--search", help="Match on the cluster name."),
|
|
65
|
-
show_all: bool = typer.Option(False, "--all", help="Every cluster the backend reports, without the placement filter."),
|
|
66
65
|
):
|
|
67
66
|
"""List the clusters a service can run on.
|
|
68
67
|
|
|
@@ -70,10 +69,7 @@ def clusters_list(
|
|
|
70
69
|
deployment serves, placements the allowlist permits, and registration.
|
|
71
70
|
Only a registered cluster may take a new service, so an unregistered one
|
|
72
71
|
is never a candidate and is not listed. Everything shown here is a cluster
|
|
73
|
-
`eks create` would actually accept.
|
|
74
|
-
|
|
75
|
-
`--all` drops all three and shows everything the backend reports, which is
|
|
76
|
-
for checking what exists rather than for choosing.
|
|
72
|
+
`eks create` would actually accept, and there is no flag to see past it.
|
|
77
73
|
|
|
78
74
|
A cluster with no application is tenant-level and is the fallback for
|
|
79
75
|
every product that has none of its own.
|
|
@@ -95,23 +91,21 @@ def clusters_list(
|
|
|
95
91
|
rows = [r for r in rows if wanted in (str(r.get("geo_loc_name", "")).lower(), str(r.get("geo_loc_mst_code", "")).lower())]
|
|
96
92
|
if app_code:
|
|
97
93
|
rows = [r for r in rows if r.get("applications_mst_code") in (app_code, None)]
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
if unregistered_hidden:
|
|
114
|
-
output.info(f"{unregistered_hidden} more hidden: not registered, so no service can be created on them.")
|
|
94
|
+
before = len(rows)
|
|
95
|
+
rows = _placeable(res, rows)
|
|
96
|
+
placement_hidden = before - len(rows)
|
|
97
|
+
|
|
98
|
+
# Unregistered clusters are not offerable, so listing them is worse than
|
|
99
|
+
# useless: a service cannot be created on one, and naming it invites a
|
|
100
|
+
# comparison against the registered cluster that reads as a discrepancy.
|
|
101
|
+
registered = [r for r in rows if r.get("is_registered")]
|
|
102
|
+
unregistered_hidden = len(rows) - len(registered)
|
|
103
|
+
rows = registered
|
|
104
|
+
|
|
105
|
+
if placement_hidden:
|
|
106
|
+
output.info(f"{placement_hidden} of {before} hidden: not a placement your allowlist defines.")
|
|
107
|
+
if unregistered_hidden:
|
|
108
|
+
output.info(f"{unregistered_hidden} more hidden: not registered, so no service can be created on them.")
|
|
115
109
|
inv.emit(rows, lambda d: rows_table(
|
|
116
110
|
["Cluster", "Type", "Environment", "Region", "Application", "Registered", "Status", "Code"],
|
|
117
111
|
[(r.get("cluster_name") or r.get("name"), r.get("cluster_type"), r.get("environment"),
|
|
@@ -110,13 +110,19 @@ def eks_show(
|
|
|
110
110
|
region: str | None = typer.Option(None, "--region", help="Region, when the service runs in several."),
|
|
111
111
|
):
|
|
112
112
|
"""Show a service's configuration as it stands, marking values pending in an open request."""
|
|
113
|
-
from devlift_cli.ops.eks import LABELS, load_effective, _show
|
|
113
|
+
from devlift_cli.ops.eks import LABELS, load_effective, settings_from_config, _show
|
|
114
114
|
from devlift_cli.render import output
|
|
115
115
|
|
|
116
116
|
inv: Invocation = ctx.obj
|
|
117
117
|
eff = load_effective(inv, Resolver(inv.api, inv.profile.name), service, env, region, for_edit=False)
|
|
118
118
|
live = eff.live
|
|
119
119
|
request = eff.draft
|
|
120
|
+
# `eff.settings` is the live config with the open draft laid OVER it, so for
|
|
121
|
+
# any pending key its value is what the draft proposes, not what is running.
|
|
122
|
+
# Reading it as "the current configuration" is the obvious mistake, and one
|
|
123
|
+
# that has been made, so the deployed values are published beside it rather
|
|
124
|
+
# than left to be inferred from pending_keys.
|
|
125
|
+
deployed_settings = settings_from_config(dict(live.get("config") or {}))
|
|
120
126
|
data = {
|
|
121
127
|
"service": eff.target.service_name, "service_config_code": eff.target.config_code,
|
|
122
128
|
"environment": live.get("environment"), "region": eff.target.region_name,
|
|
@@ -129,8 +135,19 @@ def eks_show(
|
|
|
129
135
|
"type": eff.service.get("service_type"), "repository": eff.config.get("repository"),
|
|
130
136
|
"branches": eff.config.get("branches") or eff.config.get("selected_branches"),
|
|
131
137
|
"language": f"{eff.language_label or ''} {eff.language_version or ''}".strip() or None,
|
|
132
|
-
|
|
138
|
+
# effective = deployed, with the pending draft laid over it.
|
|
139
|
+
"settings": eff.settings,
|
|
140
|
+
# what is actually running right now, whatever is pending.
|
|
141
|
+
"deployed_settings": deployed_settings,
|
|
142
|
+
"pending_request": {"code": request["code"], "status": request["status"]} if request else None,
|
|
133
143
|
"pending_keys": sorted(eff.pending_keys),
|
|
144
|
+
# Both sides per changed setting, so "what is deployed" needs no
|
|
145
|
+
# cross-referencing of two dicts against a list of keys.
|
|
146
|
+
"pending": {
|
|
147
|
+
k: {"deployed": deployed_settings.get(k), "requested": eff.settings.get(k)}
|
|
148
|
+
for k in sorted(eff.settings)
|
|
149
|
+
if k in eff.pending_keys or (k in ("hpa_enabled", "min_replicas", "max_replicas") and "hpa" in eff.pending_keys)
|
|
150
|
+
} if request else {},
|
|
134
151
|
}
|
|
135
152
|
|
|
136
153
|
def table(d):
|
|
@@ -145,8 +162,15 @@ def eks_show(
|
|
|
145
162
|
pend = set(d["pending_keys"])
|
|
146
163
|
def mark(k):
|
|
147
164
|
hit = k in pend or (k in ("hpa_enabled", "min_replicas", "max_replicas") and "hpa" in pend)
|
|
148
|
-
|
|
149
|
-
|
|
165
|
+
if not (hit and d["pending_request"]):
|
|
166
|
+
return ""
|
|
167
|
+
# Name the value that is actually running. Marking the row "pending"
|
|
168
|
+
# while showing only the draft's number tells the reader the setting
|
|
169
|
+
# is changing but not what it is changing FROM, which is the half
|
|
170
|
+
# they need to judge it.
|
|
171
|
+
return f"pending ({d['pending_request']['status']}) — deployed {_show(d['deployed_settings'].get(k))}"
|
|
172
|
+
header = "Value (deployed)" if not d["pending_request"] else "Value (with pending draft)"
|
|
173
|
+
return rows_table(["Setting", header, ""], [(LABELS.get(k, k), _show(v), mark(k)) for k, v in d["settings"].items()], title="Configuration")
|
|
150
174
|
inv.emit(data, table)
|
|
151
175
|
|
|
152
176
|
|
|
@@ -373,7 +373,40 @@ def update_resource(
|
|
|
373
373
|
return UpdateResult(resource_code=resource_code, name=shown, changed=True, diff=diff, result=result)
|
|
374
374
|
|
|
375
375
|
|
|
376
|
-
def
|
|
376
|
+
def placeable(res: Resolver, rows: list[dict], service: str | None) -> list[dict]:
|
|
377
|
+
"""Keep only rows in a placement the allowlist actually defines.
|
|
378
|
+
|
|
379
|
+
Infrastructure rows carry the same placement triple as the placement
|
|
380
|
+
options (`applications_mst_code`, `environment`, `geo_loc_mst_code`), so
|
|
381
|
+
the allowlist applies to them unchanged. A row with no application is
|
|
382
|
+
tenant-level and matches on environment and region alone.
|
|
383
|
+
|
|
384
|
+
This is narrower than the backend's ALLOWED_ENVIRONMENTS, and deliberately
|
|
385
|
+
so: that setting is a flat list of environment names and cannot say that
|
|
386
|
+
stage exists only in ap-south-1, or that one product is in scope and
|
|
387
|
+
another is not. The environment gate is the backend's job; deciding which
|
|
388
|
+
product, region and service each environment really offers is this file's.
|
|
389
|
+
|
|
390
|
+
Fails closed, like every other use of the allowlist: a tenant WITH a
|
|
391
|
+
config sees only what it lists. A tenant with no config is unfiltered.
|
|
392
|
+
"""
|
|
393
|
+
if not allowlist.load(res.tenant_code()):
|
|
394
|
+
return rows
|
|
395
|
+
allowed = {
|
|
396
|
+
(p["application_code"], p["environment"], p["geo_loc_mst_code"])
|
|
397
|
+
for p in allowlist.filter_placements(res.placements(), res.tenant_code(), service)
|
|
398
|
+
}
|
|
399
|
+
kept = []
|
|
400
|
+
for row in rows:
|
|
401
|
+
here = (row.get("environment"), row.get("geo_loc_mst_code"))
|
|
402
|
+
app_code = row.get("applications_mst_code")
|
|
403
|
+
if (app_code, *here) in allowed if app_code else any((e, g) == here for _, e, g in allowed):
|
|
404
|
+
kept.append(row)
|
|
405
|
+
return kept
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def list_resources(inv: Invocation, res: Resolver, kind: ResourceKind, *, app: str | None, env: str | None,
|
|
409
|
+
region: str | None) -> list[dict]:
|
|
377
410
|
application_code = res.application(app)["application_code"] if app else None
|
|
378
411
|
environment = res.environment(env) if env else None
|
|
379
412
|
geo = None
|
|
@@ -383,15 +416,44 @@ def list_resources(inv: Invocation, res: Resolver, kind: ResourceKind, *, app: s
|
|
|
383
416
|
if not match:
|
|
384
417
|
raise InputError(f"Unknown region '{region}'. See `devlift regions list`.")
|
|
385
418
|
geo = match[0]["geo_loc_mst_code"]
|
|
386
|
-
|
|
419
|
+
rows = infra_list.list_infrastructures(inv.api, infra_type=kind.infra_type, environment=environment, geo_loc_mst_code=geo, application_code=application_code)
|
|
420
|
+
# Always. There is deliberately no flag to see past this: a resource in a
|
|
421
|
+
# placement the allowlist does not define cannot be created, and listing it
|
|
422
|
+
# only invites someone to pick it. An escape hatch would be used — most
|
|
423
|
+
# likely by an agent reading the help text and passing it to be thorough —
|
|
424
|
+
# which is exactly how the unusable rows got in front of a user before.
|
|
425
|
+
before = len(rows)
|
|
426
|
+
rows = placeable(res, rows, kind.service or None)
|
|
427
|
+
if before > len(rows):
|
|
428
|
+
output.info(f"{before - len(rows)} of {before} hidden: not a placement your allowlist defines.")
|
|
429
|
+
return rows
|
|
387
430
|
|
|
388
431
|
|
|
389
432
|
def find_resource(inv: Invocation, res: Resolver, kind: ResourceKind, name: str, *, env: str | None = None) -> dict:
|
|
390
|
-
"""One row by user-typed name (or code); env narrows when the name repeats.
|
|
433
|
+
"""One row by user-typed name (or code); env narrows when the name repeats.
|
|
434
|
+
|
|
435
|
+
Scoped to the allowlist, exactly like the listings. Without this a name
|
|
436
|
+
typed directly reached a resource the lists no longer show, so `describe`
|
|
437
|
+
and `update` were the one way back to a placement nothing can be created
|
|
438
|
+
in — and `update` would have queued a deployment there.
|
|
439
|
+
"""
|
|
391
440
|
environment = res.environment(env) if env else None
|
|
392
441
|
rows = infra_list.list_infrastructures(inv.api, infra_type=kind.infra_type, environment=environment)
|
|
393
442
|
needle = name.strip().lower()
|
|
394
443
|
hits = [r for r in rows if needle in (infra_list.display_name(r).lower(), str(r.get("code") or "").lower(), str(r.get("name") or "").lower())]
|
|
444
|
+
if hits:
|
|
445
|
+
# Narrow AFTER matching, so a name that exists but sits outside the
|
|
446
|
+
# allowlist says so, instead of the bare "no such bucket" that sends
|
|
447
|
+
# someone hunting for a typo.
|
|
448
|
+
allowed = placeable(res, hits, kind.service or None)
|
|
449
|
+
if not allowed:
|
|
450
|
+
where = ", ".join(sorted({f"{r.get('environment')}/{r.get('geo_loc_mst_code')}" for r in hits}))
|
|
451
|
+
raise CliError(
|
|
452
|
+
f"'{name}' is in {where}, which your placement allowlist does not define.",
|
|
453
|
+
EXIT_NOT_FOUND,
|
|
454
|
+
hint="`devlift regions list` shows the placements you can work in.",
|
|
455
|
+
)
|
|
456
|
+
hits = allowed
|
|
395
457
|
if not hits:
|
|
396
458
|
raise CliError(f"No {kind.noun} named '{name}'" + (f" in {environment}" if environment else "") + ".", EXIT_NOT_FOUND)
|
|
397
459
|
if len(hits) > 1:
|
|
@@ -31,6 +31,13 @@ Full reference: `devlift manual` (or `reference/commands.md` in this skill).
|
|
|
31
31
|
application, environment or region survives, a create resolves it without
|
|
32
32
|
being told — do not pass `--app/--env/--region` unless the user named one
|
|
33
33
|
or a command asks for it.
|
|
34
|
+
**Every list is the complete answer.** Applications, environments, regions,
|
|
35
|
+
resource groups, clusters, services, and the s3/sqs/dynamodb lists are all
|
|
36
|
+
filtered to the tenant's placement allowlist, and there is no flag to widen
|
|
37
|
+
them. A line such as `63 of 212 hidden: not a placement your allowlist
|
|
38
|
+
defines` is normal and correct, not a problem to route around. Do not go
|
|
39
|
+
looking for a resource the lists withheld: naming one directly is refused
|
|
40
|
+
too, because nothing can be created or changed there.
|
|
34
41
|
6. **Exit codes decide the next step** (see `reference/exit-codes.md`):
|
|
35
42
|
0 ok · 1 error · 2 not signed in → tell the user to run `devlift login`
|
|
36
43
|
(needs a browser; do not run it unattended) · 3 bad or missing input ·
|
|
@@ -239,6 +246,16 @@ the approved rows. The summary is printed on every dry run, in json mode too.
|
|
|
239
246
|
finished, and `eks status <service> --env <env>` also asks ArgoCD whether the
|
|
240
247
|
pod is actually serving, which is the real "is it up" question. A finished
|
|
241
248
|
pipeline is not a running application.
|
|
249
|
+
- **`eks show` is not "what is deployed" when a request is open.** Its
|
|
250
|
+
`settings` are the live configuration with the open draft laid OVER it, so
|
|
251
|
+
for any pending key the value shown is what the draft PROPOSES. Quoting one
|
|
252
|
+
as the running value is wrong and has misled a user.
|
|
253
|
+
Check `pending_request` first. If it is null, `settings` is the deployed
|
|
254
|
+
configuration and you can quote it freely. If it is set, use
|
|
255
|
+
`deployed_settings` for what is running, or `pending` which gives
|
|
256
|
+
`{deployed, requested}` per changed key, or run `devlift eks diff <service>
|
|
257
|
+
--env <env>`. Never describe a value as current, running or deployed on the
|
|
258
|
+
strength of `settings` alone while something is pending.
|
|
242
259
|
- **Never compare a cluster code with a cluster name.** `eks show` returns both
|
|
243
260
|
`cluster` (the name, e.g. `acme-core-stage-mumbai-01-application-cluster`)
|
|
244
261
|
and `cluster_code` (e.g. `infra-acme-eks-staging-mumbai-app-01`). They are
|
|
@@ -63,11 +63,11 @@ devlift eks create --name NAME --app APP --env ENV --region REGION --type api|wo
|
|
|
63
63
|
--repository owner/name --branch main [--branch ...]
|
|
64
64
|
--language Go|Python|Node.js|"Java Maven"|"Java Gradle" --version 1.24
|
|
65
65
|
[--set key=value ...] [--public] [-y]
|
|
66
|
-
devlift eks show <service> --env ENV
|
|
66
|
+
devlift eks show <service> --env ENV live config WITH any open draft over it; deployed_settings + pending{} give the real deployed values
|
|
67
67
|
devlift eks diff <service> --env ENV preview the pending request: Field | Deployed | Requested
|
|
68
68
|
devlift eks diff <service> --deployed instead: live config vs the last deployment
|
|
69
69
|
devlift eks settings <service> --env ENV the --set keys that apply to it, with current values
|
|
70
|
-
devlift clusters list [--type eks] [--app] [--env] the --cluster values
|
|
70
|
+
devlift clusters list [--type eks] [--app] [--env] the --cluster values; every row listed can take a service
|
|
71
71
|
devlift eks edit <service> --env ENV [--set key=value ...] [--repository owner/name] [--branch ...] [--language L --version V] [-y]
|
|
72
72
|
devlift eks deploy <service> --env ENV [--region R] [--confirm-name NAME] [--wait] [-y]
|
|
73
73
|
devlift eks status <service> --env ENV [--wait] [--no-argo]
|
|
@@ -59,7 +59,9 @@ the user it is a draft, nothing is deployed, and continue with workflow 6.
|
|
|
59
59
|
## 4. Change a running service's settings
|
|
60
60
|
|
|
61
61
|
```
|
|
62
|
-
devlift eks show orders --env stage --no-input -o json #
|
|
62
|
+
devlift eks show orders --env stage --no-input -o json # settings = live WITH the open draft over it.
|
|
63
|
+
# pending_request null -> settings IS the deployed config
|
|
64
|
+
# pending_request set -> use deployed_settings, or pending{key:{deployed,requested}}, or `eks diff`
|
|
63
65
|
devlift eks edit orders --env stage --set memory_limit=1 --no-input # Current | New, exit 5
|
|
64
66
|
devlift eks edit orders --env stage --set memory_limit=1 --no-input -y -o json
|
|
65
67
|
# → {changed, changes{field:{from,to}}, queue_code, queue_status}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|