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.
Files changed (66) hide show
  1. {devlift_cli-0.1.4 → devlift_cli-0.1.6}/PKG-INFO +1 -1
  2. {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli/MANUAL.md +36 -15
  3. {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli/__init__.py +1 -1
  4. {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli/commands/catalog.py +28 -21
  5. {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli/commands/clusters.py +16 -22
  6. {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli/commands/eks.py +28 -4
  7. {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli/ops/resources.py +65 -3
  8. {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli/skill/SKILL.md +17 -0
  9. {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli/skill/reference/commands.md +2 -2
  10. {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli/skill/reference/workflows.md +3 -1
  11. {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli.egg-info/PKG-INFO +1 -1
  12. {devlift_cli-0.1.4 → devlift_cli-0.1.6}/README.md +0 -0
  13. {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli/__main__.py +0 -0
  14. {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli/api/__init__.py +0 -0
  15. {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli/api/approvals.py +0 -0
  16. {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli/api/catalog.py +0 -0
  17. {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli/api/client.py +0 -0
  18. {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli/api/context.py +0 -0
  19. {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli/api/deployments.py +0 -0
  20. {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli/api/infra.py +0 -0
  21. {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli/api/infra_list.py +0 -0
  22. {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli/api/kong.py +0 -0
  23. {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli/api/services.py +0 -0
  24. {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli/api/vpc.py +0 -0
  25. {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli/app.py +0 -0
  26. {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli/auth/__init__.py +0 -0
  27. {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli/auth/oauth.py +0 -0
  28. {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli/auth/session.py +0 -0
  29. {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli/auth/storage.py +0 -0
  30. {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli/commands/__init__.py +0 -0
  31. {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli/commands/approval.py +0 -0
  32. {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli/commands/auth.py +0 -0
  33. {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli/commands/deployment.py +0 -0
  34. {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli/commands/dynamodb.py +0 -0
  35. {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli/commands/kong.py +0 -0
  36. {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli/commands/languages.py +0 -0
  37. {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli/commands/manual.py +0 -0
  38. {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli/commands/repositories.py +0 -0
  39. {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli/commands/request.py +0 -0
  40. {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli/commands/s3.py +0 -0
  41. {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli/commands/skill.py +0 -0
  42. {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli/commands/sqs.py +0 -0
  43. {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli/config.py +0 -0
  44. {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli/context.py +0 -0
  45. {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli/data/placement/vance.json +0 -0
  46. {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli/errors.py +0 -0
  47. {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli/ops/__init__.py +0 -0
  48. {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli/ops/approvals.py +0 -0
  49. {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli/ops/eks.py +0 -0
  50. {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli/ops/kong.py +0 -0
  51. {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli/ops/placement.py +0 -0
  52. {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli/ops/status.py +0 -0
  53. {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli/ops/wait.py +0 -0
  54. {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli/render/__init__.py +0 -0
  55. {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli/render/output.py +0 -0
  56. {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli/resolve/__init__.py +0 -0
  57. {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli/resolve/allowlist.py +0 -0
  58. {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli/resolve/names.py +0 -0
  59. {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli/skill/reference/exit-codes.md +0 -0
  60. {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli.egg-info/SOURCES.txt +0 -0
  61. {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli.egg-info/dependency_links.txt +0 -0
  62. {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli.egg-info/entry_points.txt +0 -0
  63. {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli.egg-info/requires.txt +0 -0
  64. {devlift_cli-0.1.4 → devlift_cli-0.1.6}/devlift_cli.egg-info/top_level.txt +0 -0
  65. {devlift_cli-0.1.4 → devlift_cli-0.1.6}/pyproject.toml +0 -0
  66. {devlift_cli-0.1.4 → devlift_cli-0.1.6}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devlift-cli
3
- Version: 0.1.4
3
+ Version: 0.1.6
4
4
  Summary: DevLift command-line tool: create, change, review and deploy DevLift resources from the terminal.
5
5
  License: Proprietary
6
6
  Keywords: devlift,devops,deployment,infrastructure,cli
@@ -136,17 +136,33 @@ error: s3 is not offered in 'canada' for core/stage.
136
136
  Available there: Mumbai.
137
137
  ```
138
138
 
139
- `applications list`, `environments list` and `regions list` are filtered by
140
- it too, so what you are offered is what you can actually create in. They say
141
- how many rows they hid, and `--all` shows everything the backend reports:
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 (falcon has no placement)
146
+ devlift applications list core
145
147
  devlift environments list core/stage: Mumbai
146
148
  devlift regions list --for s3 Mumbai
147
- devlift regions list --all Canada, London, Mumbai
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] [--all]
189
- devlift environments list [--app NAME] [--all] Environments and their regions per application.
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] [--all]
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. `--all` drops all three and shows
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 this deployment offers; --all shows everything.
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 as it stands, with pending values marked.
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
- `eks show` marks each value that a pending request would change.
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
 
@@ -1,3 +1,3 @@
1
1
  """DevLift command-line tool."""
2
2
 
3
- __version__ = "0.1.4"
3
+ __version__ = "0.1.6"
@@ -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 by the placement allowlist; --all shows everything.")
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."), show_all: bool = _ALL, no_cache: bool = _NO_CACHE):
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
- if not show_all:
45
- before = len(apps)
46
- apps = allowlist.filter_applications(apps, res.placements(), res.tenant_code())
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."), show_all: bool = _ALL, no_cache: bool = _NO_CACHE):
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
- if not show_all:
62
- before = len(rows)
63
- rows = allowlist.filter_placements(rows, res.tenant_code())
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. `--all` skips the filter.
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
- if not show_all:
101
- before = len(rows)
102
- rows = allowlist.filter_placements(rows, res.tenant_code(), for_service.strip().lower() if for_service else None)
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."), show_all: bool = _ALL, no_cache: bool = _NO_CACHE):
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
- elif not show_all:
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
- if not show_all:
99
- before = len(rows)
100
- rows = _placeable(res, rows)
101
- placement_hidden = before - len(rows)
102
-
103
- # Unregistered clusters are not offerable, so listing them is worse than
104
- # useless: a service cannot be created on one, and naming it invites a
105
- # comparison against the registered cluster that reads as a discrepancy.
106
- # `--all` still shows them, for someone checking what exists.
107
- registered = [r for r in rows if r.get("is_registered")]
108
- unregistered_hidden = len(rows) - len(registered)
109
- rows = registered
110
-
111
- if placement_hidden:
112
- output.info(f"{placement_hidden} of {before} hidden: not a placement this deployment offers; --all shows everything.")
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
- "settings": eff.settings, "pending_request": {"code": request["code"], "status": request["status"]} if request else None,
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
- return f"pending ({d['pending_request']['status']})" if hit and d["pending_request"] else ""
149
- return rows_table(["Setting", "Value", ""], [(LABELS.get(k, k), _show(v), mark(k)) for k, v in d["settings"].items()], title="Configuration")
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 list_resources(inv: Invocation, res: Resolver, kind: ResourceKind, *, app: str | None, env: str | None, region: str | None) -> list[dict]:
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
- return infra_list.list_infrastructures(inv.api, infra_type=kind.infra_type, environment=environment, geo_loc_mst_code=geo, application_code=application_code)
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 configuration as it stands; pending values marked
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 (Registered = may take a service)
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 # current values; pending_request + pending_keys if a draft exists
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}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devlift-cli
3
- Version: 0.1.4
3
+ Version: 0.1.6
4
4
  Summary: DevLift command-line tool: create, change, review and deploy DevLift resources from the terminal.
5
5
  License: Proprietary
6
6
  Keywords: devlift,devops,deployment,infrastructure,cli
File without changes
File without changes
File without changes