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,3 @@
1
+ """DevLift command-line tool."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,4 @@
1
+ from devlift_cli.app import main
2
+
3
+ if __name__ == "__main__":
4
+ main()
File without changes
@@ -0,0 +1,53 @@
1
+ """The review lane: change requests on a service configuration.
2
+
3
+ GET /approvals requests you raised or can act on (status, resource_code filters)
4
+ GET /approvals/{queue_code} one request: frozen diff, snapshot, history, your flags
5
+ GET /approvals/services reviewer inbox, one row per service with pending/approved counts
6
+ GET /approvals/history?resource_code= every request ever raised against one configuration
7
+ GET /approvals/permissions?resource_code= what you may do on a configuration
8
+ POST /approvals/{queue_code}/<verb> submit | withdraw | discard | approve | request-changes | reject | revoke
9
+
10
+ POST /deployments/multiple-deploy ship the approved change set of one configuration
11
+ GET /deployments/multiple-deploy/status/{workflow_id}
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from devlift_cli.api.client import ApiClient
17
+
18
+ VERBS = ("submit", "withdraw", "discard", "approve", "request-changes", "reject", "revoke")
19
+
20
+
21
+ def list_requests(api: ApiClient, *, status: str | None = None, resource_code: str | None = None, limit: int = 200) -> list[dict]:
22
+ return api.get("/approvals", {"status": status, "resource_code": resource_code, "limit": limit}).get("approvals", [])
23
+
24
+
25
+ def get_request(api: ApiClient, queue_code: str) -> dict:
26
+ return api.get(f"/approvals/{queue_code}")
27
+
28
+
29
+ def services_awaiting(api: ApiClient) -> list[dict]:
30
+ return api.get("/approvals/services").get("services", [])
31
+
32
+
33
+ def history(api: ApiClient, resource_code: str, limit: int = 50) -> list[dict]:
34
+ return api.get("/approvals/history", {"resource_code": resource_code, "limit": limit}).get("approvals", [])
35
+
36
+
37
+ def permissions(api: ApiClient, resource_code: str) -> dict:
38
+ return api.get("/approvals/permissions", {"resource_code": resource_code})
39
+
40
+
41
+ def act(api: ApiClient, queue_code: str, verb: str, comment: str = "") -> dict:
42
+ if verb not in VERBS:
43
+ raise ValueError(verb)
44
+ body = {} if verb == "discard" else {"comment": comment or ""}
45
+ return api.post(f"/approvals/{queue_code}/{verb}", body)
46
+
47
+
48
+ def multiple_deploy(api: ApiClient, service_config_code: str) -> dict:
49
+ return api.post("/deployments/multiple-deploy", {"service_config_code": service_config_code})
50
+
51
+
52
+ def multiple_deploy_status(api: ApiClient, workflow_id: str) -> dict:
53
+ return api.get(f"/deployments/multiple-deploy/status/{workflow_id}")
@@ -0,0 +1,96 @@
1
+ """Read-only catalog calls: the things a user picks from.
2
+
3
+ Each function returns the server's list as-is (dicts), so `-o json` shows
4
+ exactly what the backend said. Filtering by name happens in resolve/names.py.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from devlift_cli.api.client import ApiClient
10
+
11
+
12
+ def list_applications(api: ApiClient, active: bool | None = None) -> list[dict]:
13
+ body = {"limit": 500}
14
+ if active is not None:
15
+ body["is_active"] = active
16
+ return api.post("/applications/get-all-applications", body).get("applications", [])
17
+
18
+
19
+ def list_services(
20
+ api: ApiClient,
21
+ application_code: str | None = None,
22
+ resource_group_code: str | None = None,
23
+ search: str | None = None,
24
+ active: bool | None = None,
25
+ ) -> list[dict]:
26
+ body: dict = {"limit": 500}
27
+ if application_code:
28
+ body["application_code"] = application_code
29
+ if resource_group_code:
30
+ body["resource_group_mst_code"] = resource_group_code
31
+ if search:
32
+ body["search_query"] = search
33
+ if active is not None:
34
+ body["is_active"] = active
35
+ return api.post("/services/get-all-services", body).get("services", [])
36
+
37
+
38
+ def get_service(api: ApiClient, service_code: str) -> dict:
39
+ return api.post("/services/get-service-detail", {"service_code": service_code})
40
+
41
+
42
+ def list_vendor_regions(api: ApiClient, vendor: str = "aws") -> list[dict]:
43
+ return api.get("/regions", {"vendor": vendor}).get("regions", [])
44
+
45
+
46
+ def list_infrastructure_types(api: ApiClient) -> list[dict]:
47
+ return api.get("/infrastructure-types/get-all-infrastructure-types").get("infrastructure_types", [])
48
+
49
+
50
+ def list_resource_groups(api: ApiClient, application_code: str | None = None, kind: str | None = None) -> list[dict]:
51
+ return api.get(
52
+ "/resource-groups/get-all-resource-groups",
53
+ {"application_code": application_code, "kind": kind, "limit": 500},
54
+ ).get("resource_groups", [])
55
+
56
+
57
+ def list_repositories(api: ApiClient) -> list[dict]:
58
+ """Repositories the tenant's GitHub App can see: {name, full_name,
59
+ default_branch, private, html_url, updated_at}."""
60
+ return api.get("/github/repositories").get("repositories", [])
61
+
62
+
63
+ def list_branches(api: ApiClient, full_name: str) -> list[dict]:
64
+ """Branches of one repository (owner/name): {name, protected}."""
65
+ owner, _, repo = full_name.partition("/")
66
+ return api.get(f"/github/repositories/{owner}/{repo}/branches").get("branches", [])
67
+
68
+
69
+ def placement_options(api: ApiClient, infra_type: str | None = None) -> list[dict]:
70
+ """The product → environment → region tree the tenant may place things in.
71
+
72
+ Each product: {product_code, product_name, applications_mst_code,
73
+ environments: [{environment_enum, environment_label,
74
+ geo_locations: [{geo_loc_mst_code, geo_loc_name}]}]}.
75
+ """
76
+ body = {"infra_type": infra_type} if infra_type else {}
77
+ return api.post("/placement-parameters/get-placement-options", body).get("products", [])
78
+
79
+
80
+ def placement_rows(products: list[dict]) -> list[dict]:
81
+ """Flatten the tree into one row per (product, environment, region)."""
82
+ rows = []
83
+ for product in products:
84
+ for env in product.get("environments", []):
85
+ for geo in env.get("geo_locations", []):
86
+ rows.append(
87
+ {
88
+ "product": product.get("product_name"),
89
+ "application_code": product.get("applications_mst_code") or product.get("product_code"),
90
+ "environment": env.get("environment_enum"),
91
+ "environment_label": env.get("environment_label"),
92
+ "region": geo.get("geo_loc_name"),
93
+ "geo_loc_mst_code": geo.get("geo_loc_mst_code"),
94
+ }
95
+ )
96
+ return rows
@@ -0,0 +1,125 @@
1
+ """HTTP client for obs_tool's REST API.
2
+
3
+ One instance per command invocation. Every non-2xx answer becomes a CliError
4
+ with the server's `detail` as the message and the HTTP status mapped to the
5
+ exit-code table in errors.py, so commands never inspect status codes
6
+ themselves. A 401 is retried once after a token refresh.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import sys
13
+ from typing import Any
14
+
15
+ import httpx
16
+
17
+ from devlift_cli import __version__
18
+ from devlift_cli.auth.session import TokenSource
19
+ from devlift_cli.config import Profile
20
+ from devlift_cli.errors import (
21
+ EXIT_CONFLICT,
22
+ EXIT_ERROR,
23
+ EXIT_INPUT,
24
+ EXIT_NOT_FOUND,
25
+ EXIT_PERMISSION,
26
+ AuthError,
27
+ CliError,
28
+ )
29
+
30
+ _TIMEOUT = httpx.Timeout(120.0, connect=10.0)
31
+
32
+ _STATUS_TO_EXIT = {
33
+ 400: EXIT_INPUT,
34
+ 403: EXIT_PERMISSION,
35
+ 404: EXIT_NOT_FOUND,
36
+ 409: EXIT_CONFLICT,
37
+ 422: EXIT_INPUT,
38
+ }
39
+
40
+
41
+ def _detail_text(resp: httpx.Response) -> str:
42
+ try:
43
+ body = resp.json()
44
+ except ValueError:
45
+ return resp.text[:500] or resp.reason_phrase
46
+ detail = body.get("detail", body) if isinstance(body, dict) else body
47
+ if isinstance(detail, dict):
48
+ return str(detail.get("message") or detail.get("detail") or json.dumps(detail))
49
+ if isinstance(detail, list):
50
+ # FastAPI validation errors: [{loc, msg, type}, …]
51
+ parts = []
52
+ for item in detail:
53
+ if isinstance(item, dict) and "msg" in item:
54
+ loc = ".".join(str(p) for p in item.get("loc", []) if p not in ("body", "query"))
55
+ parts.append(f"{loc}: {item['msg']}" if loc else item["msg"])
56
+ else:
57
+ parts.append(str(item))
58
+ return "; ".join(parts)
59
+ return str(detail)
60
+
61
+
62
+ class ApiClient:
63
+ def __init__(self, profile: Profile, tokens: TokenSource, debug: bool = False):
64
+ self.profile = profile
65
+ self.tokens = tokens
66
+ self.debug = debug
67
+ self._http = httpx.Client(
68
+ base_url=profile.api_url,
69
+ timeout=_TIMEOUT,
70
+ headers={"User-Agent": f"devlift-cli/{__version__}", "Accept": "application/json"},
71
+ )
72
+
73
+ def close(self) -> None:
74
+ self._http.close()
75
+
76
+ # ── verbs ────────────────────────────────────────────────────────────
77
+ def get(self, path: str, params: dict | None = None) -> Any:
78
+ return self.request("GET", path, params=params)
79
+
80
+ def post(self, path: str, json: Any = None, params: dict | None = None) -> Any:
81
+ return self.request("POST", path, json=json, params=params)
82
+
83
+ def put(self, path: str, json: Any = None) -> Any:
84
+ return self.request("PUT", path, json=json)
85
+
86
+ def delete(self, path: str) -> Any:
87
+ return self.request("DELETE", path)
88
+
89
+ def request(self, method: str, path: str, *, json: Any = None, params: dict | None = None) -> Any:
90
+ clean_params = {k: v for k, v in (params or {}).items() if v is not None}
91
+ token = self.tokens.current()
92
+ resp = self._send(method, path, token, json, clean_params)
93
+ if resp.status_code == 401:
94
+ token = self.tokens.force_refresh()
95
+ if token is None:
96
+ raise AuthError("Your session is not valid any more.")
97
+ resp = self._send(method, path, token, json, clean_params)
98
+ if resp.status_code == 401:
99
+ raise AuthError("Your session is not valid any more.")
100
+ if resp.status_code >= 300:
101
+ code = _STATUS_TO_EXIT.get(resp.status_code, EXIT_ERROR)
102
+ raise CliError(_detail_text(resp), code, http_status=resp.status_code)
103
+ if resp.status_code == 204 or not resp.content:
104
+ return None
105
+ try:
106
+ return resp.json()
107
+ except ValueError:
108
+ return resp.text
109
+
110
+ def _send(self, method: str, path: str, token: str, json: Any, params: dict) -> httpx.Response:
111
+ headers = {"Authorization": f"Bearer {token}"}
112
+ if self.debug:
113
+ print(f"> {method} {self.profile.api_url}{path} {params or ''}", file=sys.stderr)
114
+ try:
115
+ resp = self._http.request(method, path, json=json, params=params, headers=headers)
116
+ except httpx.ConnectError as exc:
117
+ raise CliError(
118
+ f"Could not connect to {self.profile.base_url}: {exc}",
119
+ hint="Is the DevLift backend running? Use --endpoint-url or `devlift configure`.",
120
+ ) from exc
121
+ except httpx.TimeoutException as exc:
122
+ raise CliError(f"{method} {path} timed out: {exc}") from exc
123
+ if self.debug:
124
+ print(f"< {resp.status_code} ({len(resp.content)} bytes)", file=sys.stderr)
125
+ return resp
@@ -0,0 +1,21 @@
1
+ """GET /cli/context — who am I, and where is everything."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pydantic import BaseModel
6
+
7
+ from devlift_cli.api.client import ApiClient
8
+
9
+
10
+ class CliContext(BaseModel):
11
+ user_code: str
12
+ email: str | None = None
13
+ tenant_code: str
14
+ tenant_name: str | None = None
15
+ secrets_api_base_url: str | None = None
16
+ frontend_base_url: str | None = None
17
+ features: dict[str, bool] = {}
18
+
19
+
20
+ def get_context(api: ApiClient) -> CliContext:
21
+ return CliContext.model_validate(api.get("/cli/context"))
@@ -0,0 +1,37 @@
1
+ """Deployment history and live application status — what the dashboard's
2
+ Deployments tab and the assistant's status tools read.
3
+
4
+ GET /deployments/history one row per workflow: status, PR, resources, who, when
5
+ GET /deployments/history/{workflow_id} the same row plus its pipeline stages
6
+ GET /deployments/application-status ArgoCD's view of one service configuration
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from devlift_cli.api.client import ApiClient
12
+
13
+ TERMINAL = {"COMPLETED", "FAILED", "TIMEOUT", "TIMEDOUT", "CANCELLED", "CANCELED"}
14
+
15
+
16
+ def history(
17
+ api: ApiClient,
18
+ *,
19
+ status: str | None = None,
20
+ application_code: str | None = None,
21
+ environment: str | None = None,
22
+ user_code: str | None = None,
23
+ limit: int = 20,
24
+ ) -> tuple[list[dict], int]:
25
+ data = api.get("/deployments/history", {
26
+ "status": status, "application_code": application_code, "environment": environment,
27
+ "user_code": user_code, "limit": limit,
28
+ })
29
+ return data.get("items", []), int(data.get("total") or 0)
30
+
31
+
32
+ def history_detail(api: ApiClient, workflow_id: str) -> dict:
33
+ return api.get(f"/deployments/history/{workflow_id}")
34
+
35
+
36
+ def application_status(api: ApiClient, service_config_code: str, workflow_id: str | None = None) -> dict:
37
+ return api.get("/deployments/application-status", {"service_config_code": service_config_code, "workflow_id": workflow_id})
@@ -0,0 +1,94 @@
1
+ """Infrastructure rows, the transaction queue, and deployments of queue items.
2
+
3
+ These are the calls behind `s3 / sqs / dynamodb create|update`, in the order
4
+ the web and the in-process provisioning path use them:
5
+
6
+ POST /infrastructures → the resource row (code)
7
+ POST /transaction-queue/add-to-queue → a queue item for it (id, code)
8
+ POST /transaction-queue/bulk-approve → mark it approved
9
+ POST /transaction-queue/deploy → start the deployment (workflow_id or PR)
10
+ GET /transaction-queue/deploy-status/{id} → poll the workflow
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from devlift_cli.api.client import ApiClient
16
+
17
+ TABLE_INFRASTRUCTURE = "INFRASTRUCTURE"
18
+
19
+ DUPLICATE_CHECKS = {
20
+ "s3_infrastructuretype_ref": ("/aws-ops/s3/validate-duplicate-bucket", "bucket_name"),
21
+ "sqs_infrastructuretype_ref": ("/aws-ops/sqs/validate-duplicate-queue", "queue_name"),
22
+ "dynamodb_infrastructuretype_ref": ("/aws-ops/dynamodb/validate-duplicate-table", "table_name"),
23
+ }
24
+
25
+
26
+ def validate_duplicate(api: ApiClient, infra_type: str, product_code: str, environment: str, geo_loc: str, name: str) -> dict | None:
27
+ """The server's own duplicate-name check, or None when the type has none."""
28
+ check = DUPLICATE_CHECKS.get(infra_type)
29
+ if not check:
30
+ return None
31
+ path, name_key = check
32
+ return api.post(path, {"product_code": product_code, "environment": environment, "geo_loc": geo_loc, name_key: name})
33
+
34
+
35
+ def create_infrastructure(
36
+ api: ApiClient,
37
+ *,
38
+ infra_type: str,
39
+ application_code: str,
40
+ environment: str,
41
+ geo_loc_mst_code: str,
42
+ type_specific_config: dict,
43
+ code: str | None = None,
44
+ service_mst_code: str | None = None,
45
+ created_by: str | None = None,
46
+ ) -> dict:
47
+ body = {
48
+ "infrastructuretype_ref_code": infra_type,
49
+ "application_code": application_code,
50
+ "environment": environment,
51
+ "geo_loc_mst_code": geo_loc_mst_code,
52
+ "type_specific_config": type_specific_config,
53
+ }
54
+ if code:
55
+ body["code"] = code
56
+ if service_mst_code:
57
+ body["service_mst_code"] = service_mst_code
58
+ if created_by:
59
+ body["created_by"] = created_by
60
+ return api.post("/infrastructures", body)
61
+
62
+
63
+ def add_to_queue(api: ApiClient, *, transaction_code: str, table_name: str, config_snapshot: dict, case_ref_code: str, queue_code: str | None = None) -> dict:
64
+ body = {
65
+ "transaction_code": transaction_code,
66
+ "table_name": table_name,
67
+ "config_snapshot": config_snapshot,
68
+ "case_ref_code": case_ref_code,
69
+ }
70
+ if queue_code:
71
+ body["queue_code"] = queue_code
72
+ return api.post("/transaction-queue/add-to-queue", body)
73
+
74
+
75
+ def bulk_approve(api: ApiClient, queue_ids: list[int]) -> dict:
76
+ return api.post("/transaction-queue/bulk-approve", {"queue_ids": queue_ids})
77
+
78
+
79
+ def deploy_queue_items(api: ApiClient, item_ids: list[int], environment: str | None = None) -> dict:
80
+ body: dict = {"item_ids": item_ids}
81
+ if environment:
82
+ body["environment"] = environment
83
+ return api.post("/transaction-queue/deploy", body)
84
+
85
+
86
+ def deploy_status(api: ApiClient, workflow_id: str) -> dict:
87
+ return api.get(f"/transaction-queue/deploy-status/{workflow_id}")
88
+
89
+
90
+ def list_queue(api: ApiClient, environment: str | None = None) -> list[dict]:
91
+ data = api.get("/transaction-queue", {"environment": environment})
92
+ if isinstance(data, dict):
93
+ return data.get("items") or data.get("queue_items") or data.get("queue") or []
94
+ return data or []
@@ -0,0 +1,61 @@
1
+ """Reading infrastructure rows: GET /infrastructure-mst/list and
2
+ POST /infrastructure-mst/get-detail."""
3
+
4
+ from __future__ import annotations
5
+
6
+ from devlift_cli.api.client import ApiClient
7
+
8
+
9
+ def list_infrastructures(
10
+ api: ApiClient,
11
+ *,
12
+ infra_type: str | None = None,
13
+ environment: str | None = None,
14
+ geo_loc_mst_code: str | None = None,
15
+ application_code: str | None = None,
16
+ ) -> list[dict]:
17
+ return api.get(
18
+ "/infrastructure-mst/list",
19
+ {
20
+ "infrastructuretype_ref_code": infra_type,
21
+ "environment": environment,
22
+ "geo_loc_mst_code": geo_loc_mst_code,
23
+ "applications_mst_code": application_code,
24
+ },
25
+ ).get("infrastructures", [])
26
+
27
+
28
+ def list_clusters(
29
+ api: ApiClient,
30
+ *,
31
+ infra_type: str | None = None,
32
+ environment: str | None = None,
33
+ geo_loc_mst_code: str | None = None,
34
+ search: str | None = None,
35
+ ) -> list[dict]:
36
+ """EKS and ECS-EC2 clusters, as the dashboard's Clusters screen lists them.
37
+
38
+ Unlike `/list`, this includes clusters that are not registered and ones
39
+ hidden from the canvas, reporting both flags — `is_registered` is the
40
+ "may take a new service" flag, so a cluster without it cannot be chosen
41
+ by `eks create`.
42
+ """
43
+ return api.get(
44
+ "/infrastructure-mst/clusters",
45
+ {
46
+ "infrastructuretype_ref_code": infra_type,
47
+ "environment": environment,
48
+ "geo_loc_mst_code": geo_loc_mst_code,
49
+ "search": search,
50
+ },
51
+ ).get("clusters", [])
52
+
53
+
54
+ def get_infrastructure(api: ApiClient, code: str) -> dict:
55
+ return api.post("/infrastructure-mst/get-detail", {"code": code})
56
+
57
+
58
+ def display_name(row: dict) -> str:
59
+ """The name the user typed: the locator's identifier, else the stored name."""
60
+ locator = row.get("locator") or {}
61
+ return str(locator.get("identifier") or row.get("resource_identifier") or row.get("name") or row.get("code"))
@@ -0,0 +1,29 @@
1
+ """Kong gateway routes of a service, the way the dashboard's Gateway tab
2
+ reads and writes them:
3
+
4
+ GET /service-configs/env-geo-options/{service_code} → the service's configurations (config_code per env/region)
5
+ GET /kong-route-configs/gateway/by-config/{config_code} → the gateway state: route groups, paths, pending edits
6
+ POST /transaction/kong-gateway/{config_code} → the change, parked as a DRAFT queue row (gateway half)
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from devlift_cli.api.client import ApiClient
12
+
13
+ GATEWAY_CASE_REF = "add_route"
14
+
15
+
16
+ def env_geo_options(api: ApiClient, service_code: str) -> list[dict]:
17
+ """[{environment, geo_loc_code, geo_loc_name, config_code, cluster_name, ...}]"""
18
+ return api.get(f"/service-configs/env-geo-options/{service_code}").get("options", [])
19
+
20
+
21
+ def gateway_state(api: ApiClient, config_code: str) -> dict:
22
+ """{service_name, environment, geo_loc_mst_code, groups: [{code, route_group_key,
23
+ http_method, plugins, regex_priority, updated_at, paths: [{code, route_path}],
24
+ pending: [...]}], ...}"""
25
+ return api.get(f"/kong-route-configs/gateway/by-config/{config_code}")
26
+
27
+
28
+ def save_gateway_draft(api: ApiClient, config_code: str, gateway_groups: list[dict]) -> dict:
29
+ return api.post(f"/transaction/kong-gateway/{config_code}", {"gateway_groups": gateway_groups})
@@ -0,0 +1,106 @@
1
+ """Services and their per-environment configuration.
2
+
3
+ The calls behind `devlift eks create`, in the order the web's "Create & Add"
4
+ and the Settings tab's Save use them:
5
+
6
+ POST /services/create-service → the service (service_code)
7
+ POST /service-configs → its baseline row for one placement (code)
8
+ POST /transaction/service-settings/{config_code} → the settings, parked as a DRAFT queue row
9
+
10
+ Plus the two read-only lists the command picks from:
11
+
12
+ GET /languages/language-versions/grouped → language → versions (language_ref_code)
13
+ GET /service-configs/eks/language-templates → per-language starting values
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from devlift_cli.api.client import ApiClient
19
+
20
+ EKS_INFRA_TYPE = "eks_infrastructuretype_ref"
21
+ SETTINGS_CASE_REF = "update_service"
22
+
23
+
24
+ def create_service(
25
+ api: ApiClient,
26
+ *,
27
+ application_code: str,
28
+ resource_group_code: str,
29
+ service_name: str,
30
+ service_type: str,
31
+ is_public_facing: bool = False,
32
+ ) -> dict:
33
+ return api.post(
34
+ "/services/create-service",
35
+ {
36
+ "application_code": application_code,
37
+ "resource_group_code": resource_group_code,
38
+ "service_name": service_name,
39
+ "service_type": service_type,
40
+ "is_active": True,
41
+ "is_public_facing": is_public_facing,
42
+ },
43
+ )
44
+
45
+
46
+ def create_service_config(
47
+ api: ApiClient,
48
+ *,
49
+ services_mst_code: str,
50
+ environment: str,
51
+ geo_loc_mst_code: str,
52
+ infrastructure_mst_code: str | None,
53
+ config: dict,
54
+ infra_type: str = EKS_INFRA_TYPE,
55
+ vendor: str = "aws",
56
+ ) -> dict:
57
+ body = {
58
+ "services_mst_code": services_mst_code,
59
+ "infrastructuretype_ref_code": infra_type,
60
+ "infra_vendor_enum": vendor,
61
+ "environment": environment,
62
+ "geo_loc_mst_code": geo_loc_mst_code,
63
+ "config": config,
64
+ }
65
+ if infrastructure_mst_code:
66
+ body["infrastructure_mst_code"] = infrastructure_mst_code
67
+ return api.post("/service-configs", body)
68
+
69
+
70
+ def save_settings_draft(
71
+ api: ApiClient,
72
+ *,
73
+ service_config_code: str,
74
+ config_snapshot: dict,
75
+ case_ref_code: str = SETTINGS_CASE_REF,
76
+ queue_code: str | None = None,
77
+ ) -> dict:
78
+ body: dict = {"config_snapshot": config_snapshot, "case_ref_code": case_ref_code}
79
+ if queue_code:
80
+ body["queue_code"] = queue_code
81
+ return api.post(f"/transaction/service-settings/{service_config_code}", body)
82
+
83
+
84
+ def get_service_config(api: ApiClient, config_code: str) -> dict:
85
+ """The live configuration row: config, language_ref_code, cluster, placement."""
86
+ return api.get(f"/service-configs/by-code/{config_code}")
87
+
88
+
89
+ def settings_diff(api: ApiClient, config_code: str) -> dict:
90
+ """{service_config_code, has_deployed_baseline, items: [{field, label, current_value, deployed_value}]}"""
91
+ return api.get(f"/service-configs/{config_code}/settings-diff")
92
+
93
+
94
+ def language_version(api: ApiClient, code: str) -> dict:
95
+ """{code, name ('Go 1.24'), version ('1.24'), ...}"""
96
+ return api.get(f"/languages/language-versions/{code}")
97
+
98
+
99
+ def language_versions_grouped(api: ApiClient) -> list[dict]:
100
+ """[{language_name, versions: [{code, name, version, ...}]}]"""
101
+ return api.get("/languages/language-versions/grouped").get("languages", [])
102
+
103
+
104
+ def eks_language_templates(api: ApiClient) -> dict:
105
+ """{language label: template block} — the values a new service starts from."""
106
+ return api.get("/service-configs/eks/language-templates").get("languages", {})
devlift_cli/api/vpc.py ADDED
@@ -0,0 +1,24 @@
1
+ """Canvas placement context: which AWS account and cloud region a resource
2
+ belongs to on the dashboard.
3
+
4
+ GET /vpc-discovery/fetch_vpc_and_resources?application_code=&environment=
5
+
6
+ The dashboard's canvas is built from this response. Its `geoLocations`,
7
+ `accounts` and `cloudRegions` are the three levels a resource node is nested
8
+ in, and a resource whose locator does not name an account and a cloud region
9
+ is skipped when the canvas is assembled. So every client that creates a
10
+ resource has to resolve and store that context: the web does it in its create
11
+ modal, the MCP in `_resolve_canvas_placement`, and this is the CLI's copy.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from devlift_cli.api.client import ApiClient
17
+
18
+
19
+ def placement_context(api: ApiClient, application_code: str, environment: str) -> dict:
20
+ """{geoLocations, accounts, cloudRegions, ...} for one product and environment."""
21
+ return api.get(
22
+ "/vpc-discovery/fetch_vpc_and_resources",
23
+ {"application_code": application_code, "environment": environment},
24
+ )