devlift-cli 0.1.5__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.5 → devlift_cli-0.1.6}/PKG-INFO +1 -1
- {devlift_cli-0.1.5 → devlift_cli-0.1.6}/devlift_cli/MANUAL.md +26 -13
- {devlift_cli-0.1.5 → devlift_cli-0.1.6}/devlift_cli/__init__.py +1 -1
- {devlift_cli-0.1.5 → devlift_cli-0.1.6}/devlift_cli/commands/catalog.py +28 -21
- {devlift_cli-0.1.5 → devlift_cli-0.1.6}/devlift_cli/commands/clusters.py +16 -22
- {devlift_cli-0.1.5 → devlift_cli-0.1.6}/devlift_cli/ops/resources.py +65 -3
- {devlift_cli-0.1.5 → devlift_cli-0.1.6}/devlift_cli/skill/SKILL.md +7 -0
- {devlift_cli-0.1.5 → devlift_cli-0.1.6}/devlift_cli/skill/reference/commands.md +1 -1
- {devlift_cli-0.1.5 → devlift_cli-0.1.6}/devlift_cli.egg-info/PKG-INFO +1 -1
- {devlift_cli-0.1.5 → devlift_cli-0.1.6}/README.md +0 -0
- {devlift_cli-0.1.5 → devlift_cli-0.1.6}/devlift_cli/__main__.py +0 -0
- {devlift_cli-0.1.5 → devlift_cli-0.1.6}/devlift_cli/api/__init__.py +0 -0
- {devlift_cli-0.1.5 → devlift_cli-0.1.6}/devlift_cli/api/approvals.py +0 -0
- {devlift_cli-0.1.5 → devlift_cli-0.1.6}/devlift_cli/api/catalog.py +0 -0
- {devlift_cli-0.1.5 → devlift_cli-0.1.6}/devlift_cli/api/client.py +0 -0
- {devlift_cli-0.1.5 → devlift_cli-0.1.6}/devlift_cli/api/context.py +0 -0
- {devlift_cli-0.1.5 → devlift_cli-0.1.6}/devlift_cli/api/deployments.py +0 -0
- {devlift_cli-0.1.5 → devlift_cli-0.1.6}/devlift_cli/api/infra.py +0 -0
- {devlift_cli-0.1.5 → devlift_cli-0.1.6}/devlift_cli/api/infra_list.py +0 -0
- {devlift_cli-0.1.5 → devlift_cli-0.1.6}/devlift_cli/api/kong.py +0 -0
- {devlift_cli-0.1.5 → devlift_cli-0.1.6}/devlift_cli/api/services.py +0 -0
- {devlift_cli-0.1.5 → devlift_cli-0.1.6}/devlift_cli/api/vpc.py +0 -0
- {devlift_cli-0.1.5 → devlift_cli-0.1.6}/devlift_cli/app.py +0 -0
- {devlift_cli-0.1.5 → devlift_cli-0.1.6}/devlift_cli/auth/__init__.py +0 -0
- {devlift_cli-0.1.5 → devlift_cli-0.1.6}/devlift_cli/auth/oauth.py +0 -0
- {devlift_cli-0.1.5 → devlift_cli-0.1.6}/devlift_cli/auth/session.py +0 -0
- {devlift_cli-0.1.5 → devlift_cli-0.1.6}/devlift_cli/auth/storage.py +0 -0
- {devlift_cli-0.1.5 → devlift_cli-0.1.6}/devlift_cli/commands/__init__.py +0 -0
- {devlift_cli-0.1.5 → devlift_cli-0.1.6}/devlift_cli/commands/approval.py +0 -0
- {devlift_cli-0.1.5 → devlift_cli-0.1.6}/devlift_cli/commands/auth.py +0 -0
- {devlift_cli-0.1.5 → devlift_cli-0.1.6}/devlift_cli/commands/deployment.py +0 -0
- {devlift_cli-0.1.5 → devlift_cli-0.1.6}/devlift_cli/commands/dynamodb.py +0 -0
- {devlift_cli-0.1.5 → devlift_cli-0.1.6}/devlift_cli/commands/eks.py +0 -0
- {devlift_cli-0.1.5 → devlift_cli-0.1.6}/devlift_cli/commands/kong.py +0 -0
- {devlift_cli-0.1.5 → devlift_cli-0.1.6}/devlift_cli/commands/languages.py +0 -0
- {devlift_cli-0.1.5 → devlift_cli-0.1.6}/devlift_cli/commands/manual.py +0 -0
- {devlift_cli-0.1.5 → devlift_cli-0.1.6}/devlift_cli/commands/repositories.py +0 -0
- {devlift_cli-0.1.5 → devlift_cli-0.1.6}/devlift_cli/commands/request.py +0 -0
- {devlift_cli-0.1.5 → devlift_cli-0.1.6}/devlift_cli/commands/s3.py +0 -0
- {devlift_cli-0.1.5 → devlift_cli-0.1.6}/devlift_cli/commands/skill.py +0 -0
- {devlift_cli-0.1.5 → devlift_cli-0.1.6}/devlift_cli/commands/sqs.py +0 -0
- {devlift_cli-0.1.5 → devlift_cli-0.1.6}/devlift_cli/config.py +0 -0
- {devlift_cli-0.1.5 → devlift_cli-0.1.6}/devlift_cli/context.py +0 -0
- {devlift_cli-0.1.5 → devlift_cli-0.1.6}/devlift_cli/data/placement/vance.json +0 -0
- {devlift_cli-0.1.5 → devlift_cli-0.1.6}/devlift_cli/errors.py +0 -0
- {devlift_cli-0.1.5 → devlift_cli-0.1.6}/devlift_cli/ops/__init__.py +0 -0
- {devlift_cli-0.1.5 → devlift_cli-0.1.6}/devlift_cli/ops/approvals.py +0 -0
- {devlift_cli-0.1.5 → devlift_cli-0.1.6}/devlift_cli/ops/eks.py +0 -0
- {devlift_cli-0.1.5 → devlift_cli-0.1.6}/devlift_cli/ops/kong.py +0 -0
- {devlift_cli-0.1.5 → devlift_cli-0.1.6}/devlift_cli/ops/placement.py +0 -0
- {devlift_cli-0.1.5 → devlift_cli-0.1.6}/devlift_cli/ops/status.py +0 -0
- {devlift_cli-0.1.5 → devlift_cli-0.1.6}/devlift_cli/ops/wait.py +0 -0
- {devlift_cli-0.1.5 → devlift_cli-0.1.6}/devlift_cli/render/__init__.py +0 -0
- {devlift_cli-0.1.5 → devlift_cli-0.1.6}/devlift_cli/render/output.py +0 -0
- {devlift_cli-0.1.5 → devlift_cli-0.1.6}/devlift_cli/resolve/__init__.py +0 -0
- {devlift_cli-0.1.5 → devlift_cli-0.1.6}/devlift_cli/resolve/allowlist.py +0 -0
- {devlift_cli-0.1.5 → devlift_cli-0.1.6}/devlift_cli/resolve/names.py +0 -0
- {devlift_cli-0.1.5 → devlift_cli-0.1.6}/devlift_cli/skill/reference/exit-codes.md +0 -0
- {devlift_cli-0.1.5 → devlift_cli-0.1.6}/devlift_cli/skill/reference/workflows.md +0 -0
- {devlift_cli-0.1.5 → devlift_cli-0.1.6}/devlift_cli.egg-info/SOURCES.txt +0 -0
- {devlift_cli-0.1.5 → devlift_cli-0.1.6}/devlift_cli.egg-info/dependency_links.txt +0 -0
- {devlift_cli-0.1.5 → devlift_cli-0.1.6}/devlift_cli.egg-info/entry_points.txt +0 -0
- {devlift_cli-0.1.5 → devlift_cli-0.1.6}/devlift_cli.egg-info/requires.txt +0 -0
- {devlift_cli-0.1.5 → devlift_cli-0.1.6}/devlift_cli.egg-info/top_level.txt +0 -0
- {devlift_cli-0.1.5 → devlift_cli-0.1.6}/pyproject.toml +0 -0
- {devlift_cli-0.1.5 → 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
|
|
@@ -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"),
|
|
@@ -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 ·
|
|
@@ -67,7 +67,7 @@ devlift eks show <service> --env ENV live config WITH any open draft
|
|
|
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]
|
|
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
|
|
File without changes
|
|
File without changes
|