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.
- devlift_cli/MANUAL.md +1066 -0
- devlift_cli/__init__.py +3 -0
- devlift_cli/__main__.py +4 -0
- devlift_cli/api/__init__.py +0 -0
- devlift_cli/api/approvals.py +53 -0
- devlift_cli/api/catalog.py +96 -0
- devlift_cli/api/client.py +125 -0
- devlift_cli/api/context.py +21 -0
- devlift_cli/api/deployments.py +37 -0
- devlift_cli/api/infra.py +94 -0
- devlift_cli/api/infra_list.py +61 -0
- devlift_cli/api/kong.py +29 -0
- devlift_cli/api/services.py +106 -0
- devlift_cli/api/vpc.py +24 -0
- devlift_cli/app.py +163 -0
- devlift_cli/auth/__init__.py +0 -0
- devlift_cli/auth/oauth.py +270 -0
- devlift_cli/auth/session.py +64 -0
- devlift_cli/auth/storage.py +135 -0
- devlift_cli/commands/__init__.py +0 -0
- devlift_cli/commands/approval.py +51 -0
- devlift_cli/commands/auth.py +180 -0
- devlift_cli/commands/catalog.py +187 -0
- devlift_cli/commands/clusters.py +108 -0
- devlift_cli/commands/deployment.py +77 -0
- devlift_cli/commands/dynamodb.py +121 -0
- devlift_cli/commands/eks.py +326 -0
- devlift_cli/commands/kong.py +145 -0
- devlift_cli/commands/languages.py +40 -0
- devlift_cli/commands/manual.py +82 -0
- devlift_cli/commands/repositories.py +49 -0
- devlift_cli/commands/request.py +89 -0
- devlift_cli/commands/s3.py +198 -0
- devlift_cli/commands/sqs.py +229 -0
- devlift_cli/config.py +94 -0
- devlift_cli/context.py +97 -0
- devlift_cli/data/placement/vance.json +16 -0
- devlift_cli/errors.py +52 -0
- devlift_cli/ops/__init__.py +0 -0
- devlift_cli/ops/approvals.py +343 -0
- devlift_cli/ops/eks.py +877 -0
- devlift_cli/ops/kong.py +343 -0
- devlift_cli/ops/placement.py +128 -0
- devlift_cli/ops/resources.py +418 -0
- devlift_cli/ops/status.py +152 -0
- devlift_cli/ops/wait.py +82 -0
- devlift_cli/render/__init__.py +0 -0
- devlift_cli/render/output.py +75 -0
- devlift_cli/resolve/__init__.py +0 -0
- devlift_cli/resolve/allowlist.py +192 -0
- devlift_cli/resolve/names.py +179 -0
- devlift_cli-0.1.0.dist-info/METADATA +106 -0
- devlift_cli-0.1.0.dist-info/RECORD +56 -0
- devlift_cli-0.1.0.dist-info/WHEEL +5 -0
- devlift_cli-0.1.0.dist-info/entry_points.txt +3 -0
- devlift_cli-0.1.0.dist-info/top_level.txt +1 -0
devlift_cli/__init__.py
ADDED
devlift_cli/__main__.py
ADDED
|
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})
|
devlift_cli/api/infra.py
ADDED
|
@@ -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"))
|
devlift_cli/api/kong.py
ADDED
|
@@ -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
|
+
)
|