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
|
@@ -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"))
|