devlift-cli 0.1.0__py3-none-any.whl

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 (56) hide show
  1. devlift_cli/MANUAL.md +1066 -0
  2. devlift_cli/__init__.py +3 -0
  3. devlift_cli/__main__.py +4 -0
  4. devlift_cli/api/__init__.py +0 -0
  5. devlift_cli/api/approvals.py +53 -0
  6. devlift_cli/api/catalog.py +96 -0
  7. devlift_cli/api/client.py +125 -0
  8. devlift_cli/api/context.py +21 -0
  9. devlift_cli/api/deployments.py +37 -0
  10. devlift_cli/api/infra.py +94 -0
  11. devlift_cli/api/infra_list.py +61 -0
  12. devlift_cli/api/kong.py +29 -0
  13. devlift_cli/api/services.py +106 -0
  14. devlift_cli/api/vpc.py +24 -0
  15. devlift_cli/app.py +163 -0
  16. devlift_cli/auth/__init__.py +0 -0
  17. devlift_cli/auth/oauth.py +270 -0
  18. devlift_cli/auth/session.py +64 -0
  19. devlift_cli/auth/storage.py +135 -0
  20. devlift_cli/commands/__init__.py +0 -0
  21. devlift_cli/commands/approval.py +51 -0
  22. devlift_cli/commands/auth.py +180 -0
  23. devlift_cli/commands/catalog.py +187 -0
  24. devlift_cli/commands/clusters.py +108 -0
  25. devlift_cli/commands/deployment.py +77 -0
  26. devlift_cli/commands/dynamodb.py +121 -0
  27. devlift_cli/commands/eks.py +326 -0
  28. devlift_cli/commands/kong.py +145 -0
  29. devlift_cli/commands/languages.py +40 -0
  30. devlift_cli/commands/manual.py +82 -0
  31. devlift_cli/commands/repositories.py +49 -0
  32. devlift_cli/commands/request.py +89 -0
  33. devlift_cli/commands/s3.py +198 -0
  34. devlift_cli/commands/sqs.py +229 -0
  35. devlift_cli/config.py +94 -0
  36. devlift_cli/context.py +97 -0
  37. devlift_cli/data/placement/vance.json +16 -0
  38. devlift_cli/errors.py +52 -0
  39. devlift_cli/ops/__init__.py +0 -0
  40. devlift_cli/ops/approvals.py +343 -0
  41. devlift_cli/ops/eks.py +877 -0
  42. devlift_cli/ops/kong.py +343 -0
  43. devlift_cli/ops/placement.py +128 -0
  44. devlift_cli/ops/resources.py +418 -0
  45. devlift_cli/ops/status.py +152 -0
  46. devlift_cli/ops/wait.py +82 -0
  47. devlift_cli/render/__init__.py +0 -0
  48. devlift_cli/render/output.py +75 -0
  49. devlift_cli/resolve/__init__.py +0 -0
  50. devlift_cli/resolve/allowlist.py +192 -0
  51. devlift_cli/resolve/names.py +179 -0
  52. devlift_cli-0.1.0.dist-info/METADATA +106 -0
  53. devlift_cli-0.1.0.dist-info/RECORD +56 -0
  54. devlift_cli-0.1.0.dist-info/WHEEL +5 -0
  55. devlift_cli-0.1.0.dist-info/entry_points.txt +3 -0
  56. devlift_cli-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,121 @@
1
+ """devlift dynamodb …"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+
7
+ import typer
8
+
9
+ from devlift_cli.commands.manual import section_command
10
+ from devlift_cli.api.infra_list import display_name
11
+ from devlift_cli.context import Invocation
12
+ from devlift_cli.errors import InputError
13
+ from devlift_cli.ops.resources import ResourceKind, create_resource, describe_resource, list_resources
14
+ from devlift_cli.render.output import kv_table, rows_table
15
+ from devlift_cli.resolve import allowlist
16
+ from devlift_cli.resolve.names import Resolver
17
+
18
+ dynamodb_app = typer.Typer(help="DynamoDB tables.", no_args_is_help=True)
19
+
20
+ _TABLE_RE = re.compile(r"^[a-zA-Z0-9._-]+$")
21
+ _ATTRIBUTE_RE = re.compile(r"^[a-zA-Z0-9._-]{1,255}$")
22
+ _KEY_TYPES = ("S", "N", "B")
23
+
24
+
25
+ def validate_table_name(raw: str) -> str | None:
26
+ name = raw.strip()
27
+ if not name:
28
+ return "required."
29
+ if len(name) < 3:
30
+ return f"must be at least 3 characters (currently {len(name)})."
31
+ if len(name) > 200:
32
+ return f"must be 200 characters or fewer (currently {len(name)})."
33
+ if not _TABLE_RE.match(name):
34
+ return "use only letters, numbers, dots (.), underscores (_) and hyphens (-)."
35
+ return None
36
+
37
+
38
+ DYNAMODB = ResourceKind(
39
+ noun="table",
40
+ infra_type="dynamodb_infrastructuretype_ref",
41
+ case_ref_code="table_management",
42
+ service=allowlist.SERVICE_DYNAMODB,
43
+ validate_name=validate_table_name,
44
+ labels={"partition_key": "Partition key", "partition_key_type": "Partition key type"},
45
+ )
46
+
47
+
48
+ @dynamodb_app.command("list")
49
+ def dynamodb_list(
50
+ ctx: typer.Context,
51
+ app: str | None = typer.Option(None, "--app", "--product", help="Only this application."),
52
+ env: str | None = typer.Option(None, "--env", help="Only this environment."),
53
+ region: str | None = typer.Option(None, "--region", help="Only this region."),
54
+ ):
55
+ """List DynamoDB tables registered in DevLift."""
56
+ inv: Invocation = ctx.obj
57
+ rows = list_resources(inv, Resolver(inv.api, inv.profile.name), DYNAMODB, app=app, env=env, region=region)
58
+ inv.emit(rows, lambda d: rows_table(
59
+ ["Table", "Environment", "Region", "Partition key", "Status", "Code"],
60
+ [(display_name(r), r.get("environment"), r.get("geo_loc_mst_code"), f"{(r.get('locator') or {}).get('partition_key') or ''} ({(r.get('locator') or {}).get('partition_key_type') or ''})", r.get("status"), r.get("code")) for r in d],
61
+ ))
62
+
63
+
64
+ @dynamodb_app.command("describe")
65
+ def dynamodb_describe(ctx: typer.Context, name: str = typer.Argument(..., help="Table name or resource code."), env: str | None = typer.Option(None, "--env")):
66
+ """Show one table."""
67
+ inv: Invocation = ctx.obj
68
+ detail = describe_resource(inv, Resolver(inv.api, inv.profile.name), DYNAMODB, name, env=env)
69
+ loc = detail.get("locator") or {}
70
+ inv.emit(detail, lambda d: kv_table([
71
+ ("Table", display_name(d)),
72
+ ("Full name", loc.get("table_name") or loc.get("name")),
73
+ ("ARN", loc.get("table_arn")),
74
+ ("Code", d.get("code")),
75
+ ("Environment", d.get("environment")),
76
+ ("Region", f"{loc.get('region') or ''} ({d.get('geo_loc_mst_code')})"),
77
+ ("Partition key", f"{loc.get('partition_key') or ''} ({loc.get('partition_key_type') or ''})"),
78
+ ("Status", d.get("infra_status")),
79
+ ], title=display_name(d)))
80
+
81
+
82
+ @dynamodb_app.command("create")
83
+ def dynamodb_create(
84
+ ctx: typer.Context,
85
+ name: str | None = typer.Option(None, "--name", help="Table name."),
86
+ app: str | None = typer.Option(None, "--app", "--product", help="Application name or code."),
87
+ env: str | None = typer.Option(None, "--env", help="dev | stage | qa | prod"),
88
+ region: str | None = typer.Option(None, "--region", help="Region name or code."),
89
+ partition_key: str | None = typer.Option(None, "--partition-key", help="Partition (hash) key attribute name."),
90
+ partition_key_type: str | None = typer.Option(None, "--partition-key-type", help="S (string) | N (number) | B (binary). Default S."),
91
+ do_wait: bool = typer.Option(False, "--wait", help="Follow the deployment until it finishes."),
92
+ ):
93
+ """Create a DynamoDB table. Any flag you omit is asked for on a terminal.
94
+
95
+ Example: devlift dynamodb create --name orders --app core --env stage --region mumbai --partition-key order_id -y
96
+ """
97
+ inv: Invocation = ctx.obj
98
+ res = Resolver(inv.api, inv.profile.name)
99
+ if name is None:
100
+ name = inv.ask("Table name", flag="--name")
101
+ if partition_key is None:
102
+ partition_key = inv.ask("Partition key attribute", flag="--partition-key")
103
+ if not _ATTRIBUTE_RE.match(partition_key.strip()):
104
+ raise InputError("--partition-key: 1-255 characters, letters, numbers, dots, underscores and hyphens.")
105
+ key_type = (partition_key_type or "S").strip().upper()
106
+ if key_type not in _KEY_TYPES:
107
+ raise InputError(f"--partition-key-type must be one of {', '.join(_KEY_TYPES)}.")
108
+ attributes = {"partition_key": partition_key.strip(), "partition_key_type": key_type}
109
+ result = create_resource(inv, res, DYNAMODB, name=name, app=app, env=env, region=region, attributes=attributes, do_wait=do_wait)
110
+ data = {"table": name, **result.to_dict()}
111
+ inv.emit(data, lambda d: kv_table([
112
+ ("Table", d["table"]),
113
+ ("Resource code", d["resource_code"]),
114
+ ("Queue item", d["queue_code"]),
115
+ ("Status", d.get("status")),
116
+ ("Workflow", d.get("workflow_id")),
117
+ ("Pull request", d.get("pr_url")),
118
+ ]))
119
+
120
+
121
+ dynamodb_app.command("manual")(section_command("dynamodb"))
@@ -0,0 +1,326 @@
1
+ """devlift eks …"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import typer
6
+
7
+ from devlift_cli.commands.manual import section_command
8
+ from devlift_cli.context import Invocation
9
+ from devlift_cli.ops.eks import create_eks_service, describe_settings, parse_set
10
+ from devlift_cli.render.output import kv_table, rows_table
11
+ from devlift_cli.resolve.names import Resolver
12
+
13
+ eks_app = typer.Typer(help="EKS services.", no_args_is_help=True)
14
+
15
+
16
+ @eks_app.command("create")
17
+ def eks_create(
18
+ ctx: typer.Context,
19
+ name: str | None = typer.Option(None, "--name", help="Service name."),
20
+ app: str | None = typer.Option(None, "--app", "--product", help="Application name or code."),
21
+ env: str | None = typer.Option(None, "--env", help="dev | stage | qa | prod"),
22
+ region: str | None = typer.Option(None, "--region", help="Region name or code."),
23
+ resource_group: str | None = typer.Option(None, "--resource-group", help="Resource group name or code (asked when the application has several)."),
24
+ service_type: str | None = typer.Option(None, "--type", help="api (behind the shared ALB) | worker (no load balancer)."),
25
+ repository: str | None = typer.Option(None, "--repository", "--repo", help="Git repository, owner/name (see `devlift repositories list`)."),
26
+ branches: list[str] = typer.Option([], "--branch", help="Branch that triggers a build (repeatable; see `devlift repositories branches`). Required."),
27
+ language: str | None = typer.Option(None, "--language", help="Language name (see `devlift languages list`)."),
28
+ version: str | None = typer.Option(None, "--version", help="Language version, e.g. 1.24 or 3.12 (see `devlift languages list`)."),
29
+ cluster: str | None = typer.Option(None, "--cluster", help="EKS cluster name or code (asked when several are available)."),
30
+ sets: list[str] = typer.Option([], "--set", help="Override a setting: --set port=9000 (repeatable). `devlift eks settings` lists the keys."),
31
+ public: bool = typer.Option(False, "--public", help="Mark the service as publicly reachable."),
32
+ ):
33
+ """Create an EKS service and save its configuration as a draft for review.
34
+
35
+ Registers the service, creates its configuration row for the chosen
36
+ environment and region, and parks the settings as a DRAFT queue item —
37
+ the same three steps as the dashboard's "Create & Add" plus Settings Save.
38
+ Nothing is deployed until the draft is submitted, approved and deployed.
39
+
40
+ You supply what no template can know: repository, branches, language and
41
+ version. Every other setting starts from the platform's template for that
42
+ language and is shown before you confirm; --set changes any of them.
43
+
44
+ Example: devlift eks create --name orders --app core --env stage --region mumbai
45
+ --type api --repo example-org/orders --branch main --language Go --version 1.24 -y
46
+ """
47
+ inv: Invocation = ctx.obj
48
+ res = Resolver(inv.api, inv.profile.name)
49
+ result = create_eks_service(
50
+ inv, res,
51
+ name=name, app=app, env=env, region=region, resource_group=resource_group, service_type=service_type,
52
+ repository=repository, branches=[b for b in branches if b.strip()], language=language, version=version,
53
+ cluster=cluster, overrides=parse_set(sets), public=public,
54
+ )
55
+ inv.emit(result.to_dict(), lambda d: kv_table([
56
+ ("Service", d["service"]),
57
+ ("Service code", d["service_code"]),
58
+ ("Configuration", d["service_config_code"]),
59
+ ("Environment", f"{d['environment']} / {d['region']}"),
60
+ ("Cluster", d["cluster"]),
61
+ ("Queue item", d.get("queue_code")),
62
+ ("Status", d.get("queue_status")),
63
+ ], title="Draft saved"))
64
+
65
+
66
+ @eks_app.command("edit")
67
+ def eks_edit(
68
+ ctx: typer.Context,
69
+ service: str = typer.Argument(..., help="Service name or code."),
70
+ env: str | None = typer.Option(None, "--env", help="Environment (asked when the service is configured in several)."),
71
+ region: str | None = typer.Option(None, "--region", help="Region, when the service runs in several."),
72
+ sets: list[str] = typer.Option([], "--set", help="Change a setting: --set cpu_limit=1 (repeatable). `devlift eks settings <service>` lists the keys and current values."),
73
+ repository: str | None = typer.Option(None, "--repository", "--repo", help="Change the repository (owner/name)."),
74
+ branches: list[str] = typer.Option([], "--branch", help="Replace the build branches (repeatable)."),
75
+ language: str | None = typer.Option(None, "--language", help="Change the language."),
76
+ version: str | None = typer.Option(None, "--version", help="Change the language version."),
77
+ ):
78
+ """Change an existing service's settings and save them as a draft for review.
79
+
80
+ Starts from the configuration as it stands — live values with your open
81
+ draft, if any, laid over them — applies your changes, shows Current / New,
82
+ and saves the whole result as a settings draft. An open draft of yours is
83
+ updated in place. A request under review blocks editing until it is
84
+ deployed, withdrawn or revoked.
85
+
86
+ Example: devlift eks edit orders --env stage --set cpu_limit=1 --set memory_limit=2 -y
87
+ """
88
+ from devlift_cli.ops.eks import edit_eks_service
89
+
90
+ inv: Invocation = ctx.obj
91
+ overrides = parse_set(sets)
92
+ if not overrides and not repository and not branches and not language and not version:
93
+ from devlift_cli.errors import InputError
94
+ raise InputError("Nothing to change.", hint="Pass --set key=value, --repository, --branch, --language or --version.")
95
+ result = edit_eks_service(inv, Resolver(inv.api, inv.profile.name), service, env=env, region=region, overrides=overrides,
96
+ repository=repository, branches=[b for b in branches if b.strip()], language=language, version=version)
97
+ inv.emit(result.to_dict(), lambda d: kv_table([
98
+ ("Service", f"{d['service']} ({d['environment']})"),
99
+ ("Changed", "yes" if d["changed"] else "no"),
100
+ ("Queue item", d.get("queue_code")),
101
+ ("Status", d.get("queue_status")),
102
+ ]))
103
+
104
+
105
+ @eks_app.command("show")
106
+ def eks_show(
107
+ ctx: typer.Context,
108
+ service: str = typer.Argument(..., help="Service name or code."),
109
+ env: str | None = typer.Option(None, "--env", help="Environment (asked when the service is configured in several)."),
110
+ region: str | None = typer.Option(None, "--region", help="Region, when the service runs in several."),
111
+ ):
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
114
+ from devlift_cli.render import output
115
+
116
+ inv: Invocation = ctx.obj
117
+ eff = load_effective(inv, Resolver(inv.api, inv.profile.name), service, env, region, for_edit=False)
118
+ live = eff.live
119
+ request = eff.draft
120
+ data = {
121
+ "service": eff.target.service_name, "service_config_code": eff.target.config_code,
122
+ "environment": live.get("environment"), "region": eff.target.region_name, "cluster": live.get("infrastructure_mst_code"),
123
+ "type": eff.service.get("service_type"), "repository": eff.config.get("repository"),
124
+ "branches": eff.config.get("branches") or eff.config.get("selected_branches"),
125
+ "language": f"{eff.language_label or ''} {eff.language_version or ''}".strip() or None,
126
+ "settings": eff.settings, "pending_request": {"code": request["code"], "status": request["status"]} if request else None,
127
+ "pending_keys": sorted(eff.pending_keys),
128
+ }
129
+
130
+ def table(d):
131
+ output.err_console.print(kv_table([
132
+ ("Service", d["service"]), ("Type", d["type"]), ("Environment", f"{d['environment']} / {d['region']}"),
133
+ ("Cluster", d["cluster"]), ("Repository", d["repository"] or "–"),
134
+ ("Branches", ", ".join(d["branches"] or []) or "–"), ("Language", d["language"] or "–"),
135
+ ("Pending request", f"{d['pending_request']['code']} ({d['pending_request']['status']})" if d["pending_request"] else "none"),
136
+ ], title=d["service"]))
137
+ pend = set(d["pending_keys"])
138
+ def mark(k):
139
+ hit = k in pend or (k in ("hpa_enabled", "min_replicas", "max_replicas") and "hpa" in pend)
140
+ return f"pending ({d['pending_request']['status']})" if hit and d["pending_request"] else ""
141
+ return rows_table(["Setting", "Value", ""], [(LABELS.get(k, k), _show(v), mark(k)) for k, v in d["settings"].items()], title="Configuration")
142
+ inv.emit(data, table)
143
+
144
+
145
+ @eks_app.command("diff")
146
+ def eks_diff(
147
+ ctx: typer.Context,
148
+ service: str = typer.Argument(..., help="Service name or code."),
149
+ env: str | None = typer.Option(None, "--env", help="Environment (asked when the service is configured in several)."),
150
+ region: str | None = typer.Option(None, "--region", help="Environment's region, when the service runs in several."),
151
+ deployed: bool = typer.Option(False, "--deployed", help="Instead: what the live configuration holds versus what was last deployed."),
152
+ ):
153
+ """Preview the pending change: what a request would alter, Deployed to Requested.
154
+
155
+ The same table the dashboard's Preview tab and the assistant show, built
156
+ from the request DevLift computed against the live configuration. With no
157
+ request pending, the service has nothing to preview, and `--deployed`
158
+ compares the live configuration against the last deployment instead.
159
+ """
160
+ from devlift_cli.api import approvals as approvals_api
161
+ from devlift_cli.api import services as services_api
162
+ from devlift_cli.ops.approvals import diff_rows, kind_of
163
+ from devlift_cli.ops.kong import resolve_target
164
+ from devlift_cli.render import output
165
+
166
+ inv: Invocation = ctx.obj
167
+ target = resolve_target(inv, Resolver(inv.api, inv.profile.name), service, env, region)
168
+ where = f"{target.service_name} ({target.environment} / {target.region_name})"
169
+
170
+ if deployed:
171
+ diff = services_api.settings_diff(inv.api, target.config_code)
172
+
173
+ def drift(d):
174
+ if not d.get("has_deployed_baseline"):
175
+ output.info("Never deployed, so everything below is pending its first deploy." if d.get("items")
176
+ else "No settings have been saved for this configuration yet.")
177
+ return rows_table(["Setting", "Deployed", "Live"],
178
+ [(i.get("label") or i.get("field"), i.get("deployed_value") or "–", i.get("current_value") or "–") for i in d.get("items", [])],
179
+ title=where)
180
+ inv.emit(diff, drift)
181
+ return
182
+
183
+ open_rows = [r for r in approvals_api.list_requests(inv.api, resource_code=target.config_code)
184
+ if r.get("status") in ("draft", "submit", "approved")]
185
+ if not open_rows:
186
+ output.info(f"Nothing pending on {where}.")
187
+ output.info("`devlift eks diff --deployed` compares the live configuration against the last deployment.")
188
+ full = [approvals_api.get_request(inv.api, r["code"]) for r in open_rows]
189
+ data = [{"code": r["code"], "kind": kind_of(r), "status": r["status"],
190
+ "requested_by": r.get("requested_by_name") or r.get("requested_by"),
191
+ "changes": [{"field": f, "deployed": a, "requested": b} for f, a, b in diff_rows(r)]} for r in full]
192
+
193
+ def table(d):
194
+ for row in d:
195
+ output.err_console.print(rows_table(
196
+ ["Field", "Deployed", "Requested"],
197
+ [(c["field"], c["deployed"], c["requested"]) for c in row["changes"]] or [("–", "–", "nothing yet")],
198
+ title=f"{where} · {row['kind']} · {row['status']} ({row['code']}, by {row['requested_by']})",
199
+ ))
200
+ return kv_table([])
201
+ inv.emit(data, table)
202
+
203
+
204
+ @eks_app.command("deploy")
205
+ def eks_deploy(
206
+ ctx: typer.Context,
207
+ service: str = typer.Argument(..., help="Service name or code."),
208
+ env: str | None = typer.Option(None, "--env", help="Environment (asked when the service is configured in several)."),
209
+ region: str | None = typer.Option(None, "--region", help="Region, when the service runs in several."),
210
+ confirm_name: str | None = typer.Option(None, "--confirm-name", help="Production only: the service name typed back."),
211
+ do_wait: bool = typer.Option(False, "--wait", help="Follow the deployment until it finishes."),
212
+ ):
213
+ """Deploy the approved change of a service (settings, routes and variables together).
214
+
215
+ One call, as the dashboard makes it: the backend resolves what is approved
216
+ on the configuration, checks it is still exactly what was approved, and
217
+ starts one deployment. Production asks for the service name typed back.
218
+ """
219
+ from devlift_cli.ops.approvals import deploy
220
+
221
+ inv: Invocation = ctx.obj
222
+ result = deploy(inv, Resolver(inv.api, inv.profile.name), service, env=env, region=region, confirm_name=confirm_name, do_wait=do_wait)
223
+ inv.emit(result.to_dict(), lambda d: kv_table([
224
+ ("Service", f"{d['service']} ({d['environment']})"),
225
+ ("Deployed requests", ", ".join(f"{r['code']} ({r['kind']})" for r in d["requests"])),
226
+ ("Workflow", d.get("workflow_id")),
227
+ ("Status", d.get("status")),
228
+ ], title="Deployment"))
229
+
230
+
231
+ @eks_app.command("status")
232
+ def eks_status(
233
+ ctx: typer.Context,
234
+ service: str = typer.Argument(..., help="Service name or code."),
235
+ env: str | None = typer.Option(None, "--env", help="Environment (asked when the service is configured in several)."),
236
+ region: str | None = typer.Option(None, "--region", help="Region, when the service runs in several."),
237
+ do_wait: bool = typer.Option(False, "--wait", help="Follow the deployment to its end, then ArgoCD until the app settles."),
238
+ no_argo: bool = typer.Option(False, "--no-argo", help="Deployment pipeline only; skip the ArgoCD check."),
239
+ ):
240
+ """Where a service stands: its latest deployment, and whether ArgoCD reports it running.
241
+
242
+ Two answers, because they differ: DevLift's pipeline is done when the
243
+ manifests are merged; ArgoCD then still has to roll the pods, and for new
244
+ code the image is built outside DevLift. The ArgoCD reading is compared
245
+ against the deploy's finish time so the previous revision's "Healthy" is
246
+ not reported as the new one.
247
+ """
248
+ from devlift_cli.ops.approvals import resolve_target
249
+ from devlift_cli.ops.status import application_status, deployment_detail, latest_deployment_for, print_application, print_deployment
250
+ from devlift_cli.render import output
251
+
252
+ inv: Invocation = ctx.obj
253
+ res = Resolver(inv.api, inv.profile.name)
254
+ target = resolve_target(inv, res, service, env, region)
255
+ application_code = res.service(service)["application_code"]
256
+ latest = latest_deployment_for(inv, application_code=application_code, environment=target.environment or "", config_code=target.config_code)
257
+ detail = deployment_detail(inv, latest["workflow_id"], do_wait=do_wait) if latest else None
258
+ argo = None if no_argo else application_status(inv, target.config_code, (latest or {}).get("workflow_id"), do_wait=do_wait)
259
+
260
+ data = {"service": target.service_name, "service_config_code": target.config_code, "environment": target.environment,
261
+ "deployment": detail, "application": argo}
262
+
263
+ def table(d):
264
+ if d["deployment"]:
265
+ print_deployment(d["deployment"])
266
+ else:
267
+ output.info(f"No deployment recorded yet for {d['service']} in {d['environment']}.")
268
+ if d["application"]:
269
+ print_application(d["application"])
270
+ return kv_table([])
271
+ inv.emit(data, table)
272
+
273
+
274
+ @eks_app.command("settings")
275
+ def eks_settings(
276
+ ctx: typer.Context,
277
+ service: str | None = typer.Argument(None, help="Narrow to one service: only the keys that apply to it, with its current values."),
278
+ env: str | None = typer.Option(None, "--env", help="Environment, when the service is configured in several."),
279
+ region: str | None = typer.Option(None, "--region", help="Region, when the service runs in several."),
280
+ language: str | None = typer.Option(None, "--language", help="Narrow to a language without naming a service (Go, Python, …)."),
281
+ service_type: str | None = typer.Option(None, "--type", help="Narrow to api or worker without naming a service."),
282
+ ):
283
+ """The keys `--set` accepts on `eks create` and `eks edit`.
284
+
285
+ With no arguments this is every key, with the rule for when each applies.
286
+ Name a service and it narrows to the keys that apply to that service,
287
+ each with the value it holds today, which is the list to work from when
288
+ editing it.
289
+ """
290
+ from devlift_cli.ops.eks import SERVICE_TYPES, _show, applies, load_effective
291
+
292
+ inv: Invocation = ctx.obj
293
+ rows = describe_settings()
294
+ current: dict = {}
295
+ shown = None
296
+
297
+ if service:
298
+ eff = load_effective(inv, Resolver(inv.api, inv.profile.name), service, env, region, for_edit=False)
299
+ language = language or eff.language_label
300
+ service_type = service_type or eff.service.get("service_type")
301
+ current = eff.settings
302
+ shown = f"{eff.target.service_name} ({eff.target.environment})"
303
+ if service_type:
304
+ service_type = SERVICE_TYPES.get(service_type.strip().lower(), service_type.strip().upper())
305
+
306
+ if language or service_type:
307
+ rows = [r for r in rows
308
+ if applies(r["key"], language=language or "", service_type=service_type or "API", settings=current)[0]]
309
+ for r in rows:
310
+ r["current"] = current.get(r["key"])
311
+ inv.emit(rows, lambda d: rows_table(
312
+ ["Key", "Setting", "Values", "Current", "Required"],
313
+ [(r["key"], r["label"], r["values"],
314
+ _show(r.get("current")) if r.get("current") is not None else "–",
315
+ "yes" if r["required"] else "") for r in d],
316
+ title=shown or " ".join(x for x in (language, service_type) if x),
317
+ ))
318
+ return
319
+
320
+ inv.emit(rows, lambda d: rows_table(
321
+ ["Key", "Setting", "Values", "Applies", "Required"],
322
+ [(r["key"], r["label"], r["values"], r["applies"], "yes" if r["required"] else "") for r in d],
323
+ ))
324
+
325
+
326
+ eks_app.command("manual")(section_command("eks"))
@@ -0,0 +1,145 @@
1
+ """devlift kong …"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import typer
6
+
7
+ from devlift_cli.commands.manual import section_command
8
+ from devlift_cli.api import kong as kong_api
9
+ from devlift_cli.context import Invocation
10
+ from devlift_cli.errors import InputError
11
+ from devlift_cli.ops.kong import (
12
+ AUTH, METHODS, RouteChange, apply_change, normalise_plugin, resolve_target, route_cards, routes_table,
13
+ validate_path, validate_tag,
14
+ )
15
+ from devlift_cli.render.output import kv_table
16
+ from devlift_cli.resolve.names import Resolver
17
+
18
+ kong_app = typer.Typer(help="Kong gateway routes of a service.", no_args_is_help=True)
19
+ routes_app = typer.Typer(help="Read the gateway.", no_args_is_help=True)
20
+ route_app = typer.Typer(help="Change one route group (add / remove / rename paths).", no_args_is_help=True)
21
+ plugin_app = typer.Typer(help="Plugins on a route group.", no_args_is_help=True)
22
+ kong_app.add_typer(routes_app, name="routes")
23
+ kong_app.add_typer(route_app, name="route")
24
+ kong_app.add_typer(plugin_app, name="plugin")
25
+
26
+ _SERVICE = typer.Argument(..., help="Service name or code.")
27
+ _ENV = typer.Option(None, "--env", help="dev | stage | qa | prod (asked when the service has several).")
28
+ _REGION = typer.Option(None, "--region", help="Region name or code (only when the service runs in several).")
29
+ _METHOD = typer.Option(..., "--method", help="GET | POST | PUT | PATCH | DELETE | OPTIONS")
30
+ _TAG_REQ = typer.Option(..., "--tag", help="Route group (tag); see `devlift kong routes list`.")
31
+
32
+
33
+ def _method(value: str) -> str:
34
+ m = value.strip().upper()
35
+ if m not in METHODS:
36
+ raise InputError(f"--method must be one of {', '.join(METHODS)}.")
37
+ return m
38
+
39
+
40
+ def _emit(inv: Invocation, result) -> None:
41
+ inv.emit(result.to_dict(), lambda d: kv_table([
42
+ ("Service", d["service"]),
43
+ ("Configuration", d["service_config_code"]),
44
+ ("Route group", f"{d['change']['tag']} · {d['change']['method']}"),
45
+ ("Changed", "yes" if d["changed"] else "no"),
46
+ ("Queue item", d.get("queue_code")),
47
+ ("Status", d.get("queue_status")),
48
+ ]))
49
+
50
+
51
+ @routes_app.command("list")
52
+ def routes_list(ctx: typer.Context, service: str = _SERVICE, env: str | None = _ENV, region: str | None = _REGION):
53
+ """List a service's route groups: method, auth, tag, paths, pending edits."""
54
+ inv: Invocation = ctx.obj
55
+ target = resolve_target(inv, Resolver(inv.api, inv.profile.name), service, env, region)
56
+ cards = route_cards(kong_api.gateway_state(inv.api, target.config_code))
57
+ inv.emit(cards, lambda d: routes_table(d, f"{target.service_name} — {target.environment} / {target.region_name}"))
58
+
59
+
60
+ @route_app.command("add")
61
+ def route_add(
62
+ ctx: typer.Context,
63
+ service: str = _SERVICE,
64
+ env: str | None = _ENV,
65
+ region: str | None = _REGION,
66
+ method: str = _METHOD,
67
+ auth: str = typer.Option(..., "--auth", help="jwt (token required) | public"),
68
+ paths: list[str] = typer.Option(..., "--path", help="Kong regex path, e.g. '~/api/v1/orders$' (repeatable)."),
69
+ tag: str | None = typer.Option(None, "--tag", help="Route group to add to. Default: the service name (public: '<service>-open')."),
70
+ priority: int | None = typer.Option(None, "--priority", help="Regex priority 0-10000, only when two route regexes overlap."),
71
+ plugins: list[str] = typer.Option([], "--plugin", help="Extra plugin (repeatable). JWT comes from --auth."),
72
+ ):
73
+ """Add paths to a route group, creating the group if needed. Saved as a draft.
74
+
75
+ Example: devlift kong route add orders --env stage --method GET --auth jwt --path '~/api/v1/orders$' -y
76
+ """
77
+ inv: Invocation = ctx.obj
78
+ if auth.strip().lower() not in AUTH:
79
+ raise InputError("--auth must be jwt or public.")
80
+ if priority is not None and not 0 <= priority <= 10000:
81
+ raise InputError("--priority must be between 0 and 10000.")
82
+ change = RouteChange(
83
+ method=_method(method), secured=AUTH[auth.strip().lower()], tag=validate_tag(tag) if tag else None,
84
+ add=[validate_path(p) for p in paths], plugins=[normalise_plugin(p) for p in plugins], priority=priority,
85
+ )
86
+ target = resolve_target(inv, Resolver(inv.api, inv.profile.name), service, env, region)
87
+ _emit(inv, apply_change(inv, target, change))
88
+
89
+
90
+ @route_app.command("remove")
91
+ def route_remove(
92
+ ctx: typer.Context,
93
+ service: str = _SERVICE,
94
+ env: str | None = _ENV,
95
+ region: str | None = _REGION,
96
+ method: str = _METHOD,
97
+ tag: str = _TAG_REQ,
98
+ paths: list[str] = typer.Option(..., "--path", help="Path to remove, exactly as listed (repeatable)."),
99
+ ):
100
+ """Remove paths from a route group. Saved as a draft."""
101
+ inv: Invocation = ctx.obj
102
+ change = RouteChange(method=_method(method), tag=validate_tag(tag), remove=[p.strip() for p in paths])
103
+ target = resolve_target(inv, Resolver(inv.api, inv.profile.name), service, env, region)
104
+ _emit(inv, apply_change(inv, target, change))
105
+
106
+
107
+ @route_app.command("rename")
108
+ def route_rename(
109
+ ctx: typer.Context,
110
+ service: str = _SERVICE,
111
+ env: str | None = _ENV,
112
+ region: str | None = _REGION,
113
+ method: str = _METHOD,
114
+ tag: str = _TAG_REQ,
115
+ old: str = typer.Option(..., "--from", help="The path as it is now."),
116
+ new: str = typer.Option(..., "--to", help="The new path."),
117
+ ):
118
+ """Rename one path on a route group (keeps its identity). Saved as a draft."""
119
+ inv: Invocation = ctx.obj
120
+ change = RouteChange(method=_method(method), tag=validate_tag(tag), rename=[(old.strip(), validate_path(new))])
121
+ target = resolve_target(inv, Resolver(inv.api, inv.profile.name), service, env, region)
122
+ _emit(inv, apply_change(inv, target, change))
123
+
124
+
125
+ @plugin_app.command("add")
126
+ def plugin_add(
127
+ ctx: typer.Context,
128
+ service: str = _SERVICE,
129
+ env: str | None = _ENV,
130
+ region: str | None = _REGION,
131
+ method: str = _METHOD,
132
+ tag: str = _TAG_REQ,
133
+ plugins: list[str] = typer.Option(..., "--plugin", help="Plugin to add (repeatable), e.g. 'User ID Injection'."),
134
+ priority: int | None = typer.Option(None, "--priority", help="Also set the regex priority (0-10000)."),
135
+ ):
136
+ """Add plugins (and optionally set the priority) on an existing route group. Saved as a draft."""
137
+ inv: Invocation = ctx.obj
138
+ if priority is not None and not 0 <= priority <= 10000:
139
+ raise InputError("--priority must be between 0 and 10000.")
140
+ change = RouteChange(method=_method(method), tag=validate_tag(tag), plugins=[normalise_plugin(p) for p in plugins], priority=priority)
141
+ target = resolve_target(inv, Resolver(inv.api, inv.profile.name), service, env, region)
142
+ _emit(inv, apply_change(inv, target, change))
143
+
144
+
145
+ kong_app.command("manual")(section_command("kong"))
@@ -0,0 +1,40 @@
1
+ """devlift languages … — the languages and versions `eks create` accepts."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import typer
6
+
7
+ from devlift_cli.commands.manual import section_command
8
+ from devlift_cli.api import services as services_api
9
+ from devlift_cli.context import Invocation
10
+ from devlift_cli.render.output import rows_table
11
+
12
+ languages_app = typer.Typer(help="Languages and versions a service can be built with.", no_args_is_help=True)
13
+
14
+
15
+ @languages_app.command("list")
16
+ def languages_list(ctx: typer.Context):
17
+ """List languages and their versions (the --language / --version values).
18
+
19
+ "Template" says whether the platform has starting values for that
20
+ language; without one every setting must be given with --set.
21
+ """
22
+ inv: Invocation = ctx.obj
23
+ groups = services_api.language_versions_grouped(inv.api)
24
+ templated = {name.lower() for name in services_api.eks_language_templates(inv.api)}
25
+ data = [
26
+ {
27
+ "language": g["language_name"],
28
+ "versions": [v.get("version") for v in g.get("versions", [])],
29
+ "codes": [v.get("code") for v in g.get("versions", [])],
30
+ "template": g["language_name"].lower() in templated,
31
+ }
32
+ for g in groups
33
+ ]
34
+ inv.emit(data, lambda d: rows_table(
35
+ ["Language", "Versions", "Template"],
36
+ [(r["language"], ", ".join(str(v) for v in r["versions"]), "yes" if r["template"] else "no") for r in d],
37
+ ))
38
+
39
+
40
+ languages_app.command("manual")(section_command("languages"))