flowstash-cli 0.2.13__tar.gz → 0.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {flowstash_cli-0.2.13 → flowstash_cli-0.4.0}/PKG-INFO +2 -2
- {flowstash_cli-0.2.13 → flowstash_cli-0.4.0}/pyproject.toml +2 -2
- flowstash_cli-0.4.0/src/flowstash/cli/commands/apikey.py +273 -0
- {flowstash_cli-0.2.13 → flowstash_cli-0.4.0}/src/flowstash/cli/commands/build.py +44 -20
- flowstash_cli-0.4.0/src/flowstash/cli/commands/client.py +479 -0
- {flowstash_cli-0.2.13 → flowstash_cli-0.4.0}/src/flowstash/cli/commands/project.py +26 -217
- {flowstash_cli-0.2.13 → flowstash_cli-0.4.0}/src/flowstash/cli/main.py +22 -1
- flowstash_cli-0.4.0/src/flowstash/cli/templates/_config/shared/backend.yaml +80 -0
- {flowstash_cli-0.2.13 → flowstash_cli-0.4.0}/src/flowstash/cli/templates/_config/shared/clients/demoClient.yaml +16 -1
- flowstash_cli-0.2.13/src/flowstash/cli/templates/_config/[env]/.env +0 -3
- flowstash_cli-0.2.13/src/flowstash/cli/templates/_config/shared/.env +0 -2
- flowstash_cli-0.2.13/src/flowstash/cli/templates/_config/shared/backend.yaml +0 -7
- flowstash_cli-0.2.13/src/flowstash/cli/templates/_deployment/shared/.env +0 -5
- {flowstash_cli-0.2.13 → flowstash_cli-0.4.0}/src/flowstash/cli/__init__.py +0 -0
- {flowstash_cli-0.2.13 → flowstash_cli-0.4.0}/src/flowstash/cli/commands/__init__.py +0 -0
- {flowstash_cli-0.2.13 → flowstash_cli-0.4.0}/src/flowstash/cli/commands/auth.py +0 -0
- {flowstash_cli-0.2.13 → flowstash_cli-0.4.0}/src/flowstash/cli/commands/deploy.py +0 -0
- {flowstash_cli-0.2.13 → flowstash_cli-0.4.0}/src/flowstash/cli/commands/run.py +0 -0
- {flowstash_cli-0.2.13 → flowstash_cli-0.4.0}/src/flowstash/cli/commands/webhook.py +0 -0
- {flowstash_cli-0.2.13 → flowstash_cli-0.4.0}/src/flowstash/cli/core/__init__.py +0 -0
- {flowstash_cli-0.2.13 → flowstash_cli-0.4.0}/src/flowstash/cli/core/api_client.py +0 -0
- {flowstash_cli-0.2.13 → flowstash_cli-0.4.0}/src/flowstash/cli/core/auth_server.py +0 -0
- {flowstash_cli-0.2.13 → flowstash_cli-0.4.0}/src/flowstash/cli/core/builder.py +0 -0
- {flowstash_cli-0.2.13 → flowstash_cli-0.4.0}/src/flowstash/cli/core/config.py +0 -0
- {flowstash_cli-0.2.13 → flowstash_cli-0.4.0}/src/flowstash/cli/core/docker_utils.py +0 -0
- {flowstash_cli-0.2.13 → flowstash_cli-0.4.0}/src/flowstash/cli/core/patcher.py +0 -0
- {flowstash_cli-0.2.13 → flowstash_cli-0.4.0}/src/flowstash/cli/templates/AGENTS.md +0 -0
- {flowstash_cli-0.2.13 → flowstash_cli-0.4.0}/src/flowstash/cli/templates/README.md +0 -0
- {flowstash_cli-0.2.13 → flowstash_cli-0.4.0}/src/flowstash/cli/templates/_.dockerignore +0 -0
- {flowstash_cli-0.2.13 → flowstash_cli-0.4.0}/src/flowstash/cli/templates/_.flowstash +0 -0
- {flowstash_cli-0.2.13 → flowstash_cli-0.4.0}/src/flowstash/cli/templates/_api_main.py +0 -0
- {flowstash_cli-0.2.13 → flowstash_cli-0.4.0}/src/flowstash/cli/templates/_config/[env]/(backend-asyncio)/backend.yaml +0 -0
- {flowstash_cli-0.2.13 → flowstash_cli-0.4.0}/src/flowstash/cli/templates/_config/[env]/(backend-dramatiq)/backend.yaml +0 -0
- {flowstash_cli-0.2.13 → flowstash_cli-0.4.0}/src/flowstash/cli/templates/_config/[env]/(backend-managed)/backend.yaml +0 -0
- {flowstash_cli-0.2.13 → flowstash_cli-0.4.0}/src/flowstash/cli/templates/_config/[env]/(observability-logfile)/observability.yaml +0 -0
- {flowstash_cli-0.2.13 → flowstash_cli-0.4.0}/src/flowstash/cli/templates/_config/[env]/(observability-managed)/observability.yaml +0 -0
- {flowstash_cli-0.2.13 → flowstash_cli-0.4.0}/src/flowstash/cli/templates/_config/[env]/_backend.yaml +0 -0
- {flowstash_cli-0.2.13 → flowstash_cli-0.4.0}/src/flowstash/cli/templates/_config/shared/clients.yaml +0 -0
- {flowstash_cli-0.2.13 → flowstash_cli-0.4.0}/src/flowstash/cli/templates/_deployment/[env]/(backend-asyncio)/docker-compose.yaml +0 -0
- {flowstash_cli-0.2.13 → flowstash_cli-0.4.0}/src/flowstash/cli/templates/_deployment/[env]/(backend-dramatiq)/docker-compose.yaml +0 -0
- {flowstash_cli-0.2.13 → flowstash_cli-0.4.0}/src/flowstash/cli/templates/_deployment/shared/api.Dockerfile +0 -0
- {flowstash_cli-0.2.13 → flowstash_cli-0.4.0}/src/flowstash/cli/templates/_deployment/shared/worker.Dockerfile +0 -0
- {flowstash_cli-0.2.13 → flowstash_cli-0.4.0}/src/flowstash/cli/templates/_pyproject.toml +0 -0
- {flowstash_cli-0.2.13 → flowstash_cli-0.4.0}/src/flowstash/cli/templates/_src/_api/__init__.py +0 -0
- {flowstash_cli-0.2.13 → flowstash_cli-0.4.0}/src/flowstash/cli/templates/_src/_api/_routes/webhooks.py +0 -0
- {flowstash_cli-0.2.13 → flowstash_cli-0.4.0}/src/flowstash/cli/templates/_src/_shared/__init__.py +0 -0
- {flowstash_cli-0.2.13 → flowstash_cli-0.4.0}/src/flowstash/cli/templates/_src/_shared/clients/client.py +0 -0
- {flowstash_cli-0.2.13 → flowstash_cli-0.4.0}/src/flowstash/cli/templates/_src/_shared/models/models.py +0 -0
- {flowstash_cli-0.2.13 → flowstash_cli-0.4.0}/src/flowstash/cli/templates/_src/_shared/tasks/sharedTasks.py +0 -0
- {flowstash_cli-0.2.13 → flowstash_cli-0.4.0}/src/flowstash/cli/templates/_src/_worker/__init__.py +0 -0
- {flowstash_cli-0.2.13 → flowstash_cli-0.4.0}/src/flowstash/cli/templates/_src/_worker/tasks/tasks.py +0 -0
- {flowstash_cli-0.2.13 → flowstash_cli-0.4.0}/src/flowstash/cli/templates/_worker_main.py +0 -0
- {flowstash_cli-0.2.13 → flowstash_cli-0.4.0}/src/flowstash/cli/ui/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: flowstash-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: CLI for the flowstash Managed Platform
|
|
5
5
|
Author: juraj.bezdek@gmail.com
|
|
6
6
|
Author-email: juraj.bezdek@gmail.com
|
|
@@ -9,7 +9,7 @@ Classifier: Programming Language :: Python :: 3
|
|
|
9
9
|
Classifier: Programming Language :: Python :: 3.11
|
|
10
10
|
Classifier: Programming Language :: Python :: 3.12
|
|
11
11
|
Classifier: Programming Language :: Python :: 3.13
|
|
12
|
-
Requires-Dist: flowstash-runtime (>=0.
|
|
12
|
+
Requires-Dist: flowstash-runtime (>=0.4.0,<0.5.0)
|
|
13
13
|
Requires-Dist: httpx (>=0.27.0)
|
|
14
14
|
Requires-Dist: keyring (>=25.0.0)
|
|
15
15
|
Requires-Dist: libcst (>=1.1.0)
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "flowstash-cli"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.4.0"
|
|
4
4
|
description = "CLI for the flowstash Managed Platform"
|
|
5
5
|
authors = [{name = "juraj.bezdek@gmail.com", email = "juraj.bezdek@gmail.com"}]
|
|
6
6
|
requires-python = ">=3.11"
|
|
7
7
|
dependencies = [
|
|
8
|
-
"flowstash-runtime>=0.
|
|
8
|
+
"flowstash-runtime>=0.4.0,<0.5.0",
|
|
9
9
|
"typer[all]>=0.12.0",
|
|
10
10
|
"httpx>=0.27.0",
|
|
11
11
|
"pyyaml>=6.0.1",
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
"""
|
|
2
|
+
API key management commands.
|
|
3
|
+
|
|
4
|
+
Available as both:
|
|
5
|
+
flowstash api-keys <cmd> (top-level shortcut)
|
|
6
|
+
flowstash project apikey <cmd> (legacy / project-scoped path)
|
|
7
|
+
|
|
8
|
+
Commands:
|
|
9
|
+
new — Create a new API key
|
|
10
|
+
list — List all active API keys
|
|
11
|
+
revoke — Revoke an API key
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from typing import Optional
|
|
15
|
+
from datetime import datetime
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
import asyncio
|
|
18
|
+
import re
|
|
19
|
+
|
|
20
|
+
import typer
|
|
21
|
+
import questionary
|
|
22
|
+
from rich.console import Console
|
|
23
|
+
from rich.prompt import Prompt
|
|
24
|
+
from rich.table import Table
|
|
25
|
+
|
|
26
|
+
from ..core.api_client import APIClient
|
|
27
|
+
from ..core.config import get_access_token
|
|
28
|
+
|
|
29
|
+
app = typer.Typer(
|
|
30
|
+
name="api-keys",
|
|
31
|
+
help="Manage observability API keys",
|
|
32
|
+
no_args_is_help=True,
|
|
33
|
+
)
|
|
34
|
+
console = Console()
|
|
35
|
+
|
|
36
|
+
_VALID_SCOPES = {"observability:ingest", "admin"}
|
|
37
|
+
_SCOPE_DESCRIPTIONS = {
|
|
38
|
+
"observability:ingest": "Write-only ingestion (SDK / workers)",
|
|
39
|
+
"admin": "Full management access (CI / automation)",
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _format_ts(ts) -> str:
|
|
44
|
+
if not ts:
|
|
45
|
+
return "—"
|
|
46
|
+
try:
|
|
47
|
+
return datetime.utcfromtimestamp(float(ts)).strftime("%Y-%m-%d %H:%M UTC")
|
|
48
|
+
except Exception:
|
|
49
|
+
return str(ts)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _find_project_root(start_path: Path = Path.cwd()) -> Optional[Path]:
|
|
53
|
+
for parent in [start_path] + list(start_path.parents):
|
|
54
|
+
if (parent / ".flowstash").exists() or (parent / "pyproject.toml").exists():
|
|
55
|
+
return parent
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _write_key_to_env(env_name: str, raw_key: str) -> None:
|
|
60
|
+
"""Write FLOWSTASH_API_KEY to the environment's .env file."""
|
|
61
|
+
root = _find_project_root()
|
|
62
|
+
if not root:
|
|
63
|
+
console.print(
|
|
64
|
+
"[yellow]Could not find project root — skipping .env update.[/yellow]"
|
|
65
|
+
)
|
|
66
|
+
return
|
|
67
|
+
|
|
68
|
+
candidates = [
|
|
69
|
+
root / "_config" / env_name / ".env",
|
|
70
|
+
root / env_name / ".env",
|
|
71
|
+
]
|
|
72
|
+
env_file = next((p for p in candidates if p.exists()), None)
|
|
73
|
+
|
|
74
|
+
if not env_file:
|
|
75
|
+
env_dir = root / env_name
|
|
76
|
+
if env_dir.is_dir():
|
|
77
|
+
found = list(env_dir.glob("**/.env"))
|
|
78
|
+
if found:
|
|
79
|
+
env_file = found[0]
|
|
80
|
+
|
|
81
|
+
if not env_file:
|
|
82
|
+
console.print(
|
|
83
|
+
f"[yellow]Could not find .env for environment '{env_name}'. "
|
|
84
|
+
"Set FLOWSTASH_API_KEY manually.[/yellow]"
|
|
85
|
+
)
|
|
86
|
+
return
|
|
87
|
+
|
|
88
|
+
content = env_file.read_text()
|
|
89
|
+
new_line = f"FLOWSTASH_API_KEY={raw_key}"
|
|
90
|
+
|
|
91
|
+
if "FLOWSTASH_API_KEY=" in content:
|
|
92
|
+
content = re.sub(r"#?\s*FLOWSTASH_API_KEY=.*", new_line, content)
|
|
93
|
+
else:
|
|
94
|
+
content = content.rstrip("\n") + f"\n{new_line}\n"
|
|
95
|
+
|
|
96
|
+
env_file.write_text(content)
|
|
97
|
+
console.print(
|
|
98
|
+
f"[green]Written FLOWSTASH_API_KEY to {env_file.relative_to(root)}[/green]"
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _create_key(
|
|
103
|
+
label: Optional[str],
|
|
104
|
+
scope: Optional[str],
|
|
105
|
+
env: Optional[str],
|
|
106
|
+
):
|
|
107
|
+
"""Shared implementation for 'new' and 'create'."""
|
|
108
|
+
token = get_access_token()
|
|
109
|
+
if not token:
|
|
110
|
+
console.print("[red]Not logged in. Run 'flowstash login' first.[/red]")
|
|
111
|
+
raise typer.Exit(code=1)
|
|
112
|
+
|
|
113
|
+
if not label:
|
|
114
|
+
label = Prompt.ask("Key label", default="observability key")
|
|
115
|
+
|
|
116
|
+
if not scope:
|
|
117
|
+
scope = questionary.select(
|
|
118
|
+
"Key scope:",
|
|
119
|
+
choices=[
|
|
120
|
+
questionary.Choice(f"{s} — {_SCOPE_DESCRIPTIONS[s]}", s)
|
|
121
|
+
for s in _VALID_SCOPES
|
|
122
|
+
],
|
|
123
|
+
default="observability:ingest",
|
|
124
|
+
).ask()
|
|
125
|
+
if not scope:
|
|
126
|
+
raise typer.Exit(code=1)
|
|
127
|
+
|
|
128
|
+
if scope not in _VALID_SCOPES:
|
|
129
|
+
console.print(
|
|
130
|
+
f"[red]Invalid scope '{scope}'. Valid: {', '.join(_VALID_SCOPES)}[/red]"
|
|
131
|
+
)
|
|
132
|
+
raise typer.Exit(code=1)
|
|
133
|
+
|
|
134
|
+
if scope == "admin" and env:
|
|
135
|
+
console.print(
|
|
136
|
+
"[yellow]Warning: writing an 'admin' key to .env is not recommended. "
|
|
137
|
+
"Admin keys grant full management access — use them in secure CI environments only.[/yellow]"
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
async def _create():
|
|
141
|
+
api = APIClient()
|
|
142
|
+
return await api.post("/v1/api-keys", json={"label": label, "scopes": [scope]})
|
|
143
|
+
|
|
144
|
+
try:
|
|
145
|
+
result = asyncio.run(_create())
|
|
146
|
+
except Exception as e:
|
|
147
|
+
console.print(f"[red]Failed to create API key: {e}[/red]")
|
|
148
|
+
raise typer.Exit(code=1)
|
|
149
|
+
|
|
150
|
+
raw_key = result["api_key"]
|
|
151
|
+
|
|
152
|
+
console.print()
|
|
153
|
+
console.print("[green]✅ API key created![/green]")
|
|
154
|
+
console.print(f" Key ID : [bold]{result['key_id']}[/bold]")
|
|
155
|
+
console.print(f" Label : {label}")
|
|
156
|
+
console.print(f" Scope : {scope}")
|
|
157
|
+
console.print()
|
|
158
|
+
console.print(
|
|
159
|
+
"[bold yellow]⚠ Save this key — it will NOT be shown again:[/bold yellow]"
|
|
160
|
+
)
|
|
161
|
+
console.print(f"\n [bold cyan]{raw_key}[/bold cyan]\n")
|
|
162
|
+
|
|
163
|
+
if env:
|
|
164
|
+
_write_key_to_env(env, raw_key)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
@app.command("new")
|
|
168
|
+
def apikey_new(
|
|
169
|
+
label: Optional[str] = typer.Option(
|
|
170
|
+
None, "--label", "-l", help="Human-readable label for the key"
|
|
171
|
+
),
|
|
172
|
+
scope: Optional[str] = typer.Option(
|
|
173
|
+
None, "--scope", "-s", help="Scope: observability:ingest | admin"
|
|
174
|
+
),
|
|
175
|
+
env: Optional[str] = typer.Option(
|
|
176
|
+
None, "--env", "-e", help="Write key to this environment's .env file"
|
|
177
|
+
),
|
|
178
|
+
):
|
|
179
|
+
"""Create a new API key and optionally write it to an environment .env file."""
|
|
180
|
+
_create_key(label=label, scope=scope, env=env)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
@app.command("create")
|
|
184
|
+
def apikey_create(
|
|
185
|
+
label: Optional[str] = typer.Option(
|
|
186
|
+
None, "--label", "-l", help="Human-readable label for the key"
|
|
187
|
+
),
|
|
188
|
+
scope: Optional[str] = typer.Option(
|
|
189
|
+
None, "--scope", "-s", help="Scope: observability:ingest | admin"
|
|
190
|
+
),
|
|
191
|
+
env: Optional[str] = typer.Option(
|
|
192
|
+
None, "--env", "-e", help="Write key to this environment's .env file"
|
|
193
|
+
),
|
|
194
|
+
):
|
|
195
|
+
"""Create a new API key (alias for 'new')."""
|
|
196
|
+
_create_key(label=label, scope=scope, env=env)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
@app.command("list")
|
|
200
|
+
def apikey_list():
|
|
201
|
+
"""List all active API keys for the current tenant."""
|
|
202
|
+
token = get_access_token()
|
|
203
|
+
if not token:
|
|
204
|
+
console.print("[red]Not logged in. Run 'flowstash login' first.[/red]")
|
|
205
|
+
raise typer.Exit(code=1)
|
|
206
|
+
|
|
207
|
+
async def _list():
|
|
208
|
+
api = APIClient()
|
|
209
|
+
return await api.get("/v1/api-keys")
|
|
210
|
+
|
|
211
|
+
try:
|
|
212
|
+
result = asyncio.run(_list())
|
|
213
|
+
except Exception as e:
|
|
214
|
+
console.print(f"[red]Failed to list API keys: {e}[/red]")
|
|
215
|
+
raise typer.Exit(code=1)
|
|
216
|
+
|
|
217
|
+
keys = result.get("api_keys", [])
|
|
218
|
+
if not keys:
|
|
219
|
+
console.print("[dim]No active API keys found.[/dim]")
|
|
220
|
+
return
|
|
221
|
+
|
|
222
|
+
table = Table(title="API Keys", show_lines=True)
|
|
223
|
+
table.add_column("Key ID", style="bold")
|
|
224
|
+
table.add_column("Label")
|
|
225
|
+
table.add_column("Prefix")
|
|
226
|
+
table.add_column("Scopes", style="cyan")
|
|
227
|
+
table.add_column("Created")
|
|
228
|
+
table.add_column("Last Used")
|
|
229
|
+
|
|
230
|
+
for k in keys:
|
|
231
|
+
table.add_row(
|
|
232
|
+
k["key_id"],
|
|
233
|
+
k["label"],
|
|
234
|
+
k["key_prefix"],
|
|
235
|
+
", ".join(k.get("scopes", [])),
|
|
236
|
+
_format_ts(k.get("created_at")),
|
|
237
|
+
_format_ts(k.get("last_used_at")),
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
console.print(table)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
@app.command("revoke")
|
|
244
|
+
def apikey_revoke(
|
|
245
|
+
key_id: str = typer.Argument(..., help="Key ID to revoke (e.g. key-a3b2c1d4)"),
|
|
246
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompt"),
|
|
247
|
+
):
|
|
248
|
+
"""Revoke an API key immediately."""
|
|
249
|
+
token = get_access_token()
|
|
250
|
+
if not token:
|
|
251
|
+
console.print("[red]Not logged in. Run 'flowstash login' first.[/red]")
|
|
252
|
+
raise typer.Exit(code=1)
|
|
253
|
+
|
|
254
|
+
if not yes:
|
|
255
|
+
from rich.prompt import Confirm
|
|
256
|
+
|
|
257
|
+
if not Confirm.ask(
|
|
258
|
+
f"Revoke key [bold red]{key_id}[/bold red]? This cannot be undone."
|
|
259
|
+
):
|
|
260
|
+
console.print("Cancelled.")
|
|
261
|
+
return
|
|
262
|
+
|
|
263
|
+
async def _revoke():
|
|
264
|
+
api = APIClient()
|
|
265
|
+
return await api.delete(f"/v1/api-keys/{key_id}")
|
|
266
|
+
|
|
267
|
+
try:
|
|
268
|
+
asyncio.run(_revoke())
|
|
269
|
+
except Exception as e:
|
|
270
|
+
console.print(f"[red]Failed to revoke API key: {e}[/red]")
|
|
271
|
+
raise typer.Exit(code=1)
|
|
272
|
+
|
|
273
|
+
console.print(f"[green]Key {key_id} revoked successfully.[/green]")
|
|
@@ -14,26 +14,34 @@ app = typer.Typer()
|
|
|
14
14
|
console = Console()
|
|
15
15
|
|
|
16
16
|
from .project import find_project_root
|
|
17
|
-
from ..core.docker_utils import
|
|
17
|
+
from ..core.docker_utils import (
|
|
18
|
+
check_docker_binary,
|
|
19
|
+
check_docker_daemon,
|
|
20
|
+
get_docker_compose_cmd,
|
|
21
|
+
)
|
|
22
|
+
|
|
18
23
|
|
|
19
24
|
async def run_managed_build(tag: str = "latest"):
|
|
20
25
|
project_config = load_project_config()
|
|
21
26
|
if not project_config or not project_config.project_id:
|
|
22
|
-
console.print(
|
|
27
|
+
console.print(
|
|
28
|
+
"[red]Project not linked. Run 'flowstash init' to link to a managed project.[/red]"
|
|
29
|
+
)
|
|
23
30
|
raise typer.Exit(code=1)
|
|
24
|
-
|
|
31
|
+
|
|
25
32
|
api = APIClient()
|
|
26
33
|
# ... rest of existing managed build logic ...
|
|
27
34
|
# (I'll keep the existing implementation but wrap it)
|
|
28
35
|
|
|
36
|
+
|
|
29
37
|
@app.command()
|
|
30
38
|
def build(
|
|
31
39
|
env: str = typer.Argument(..., help="Environment to build"),
|
|
32
|
-
tag: str = typer.Option("latest", "--tag", "-t", help="Tag for the image")
|
|
40
|
+
tag: str = typer.Option("latest", "--tag", "-t", help="Tag for the image"),
|
|
33
41
|
):
|
|
34
42
|
"""Build project artifacts/images for the specified environment."""
|
|
35
43
|
project_config = load_project_config()
|
|
36
|
-
|
|
44
|
+
|
|
37
45
|
# Check if env is managed
|
|
38
46
|
is_managed = False
|
|
39
47
|
if project_config:
|
|
@@ -41,7 +49,7 @@ def build(
|
|
|
41
49
|
if em.name == env:
|
|
42
50
|
is_managed = em.managed
|
|
43
51
|
break
|
|
44
|
-
|
|
52
|
+
|
|
45
53
|
if is_managed:
|
|
46
54
|
result = asyncio.run(run_build_flow(tag))
|
|
47
55
|
console.print(f"[green]Managed build completed successfully![/green]")
|
|
@@ -59,7 +67,9 @@ def build(
|
|
|
59
67
|
|
|
60
68
|
compose_file = root / "deployment" / env / "docker-compose.yaml"
|
|
61
69
|
if not compose_file.exists():
|
|
62
|
-
console.print(
|
|
70
|
+
console.print(
|
|
71
|
+
f"[red]No docker-compose.yaml found for env '{env}' at {compose_file}[/red]"
|
|
72
|
+
)
|
|
63
73
|
raise typer.Exit(code=1)
|
|
64
74
|
|
|
65
75
|
cmd = get_docker_compose_cmd() + ["-f", str(compose_file), "build"]
|
|
@@ -71,12 +81,13 @@ def build(
|
|
|
71
81
|
console.print("[red]Local build failed.[/red]")
|
|
72
82
|
raise typer.Exit(code=1)
|
|
73
83
|
|
|
84
|
+
|
|
74
85
|
async def run_build_flow(tag: str = "latest"):
|
|
75
86
|
# (Moved existing run_build_flow logic here for completeness in the file)
|
|
76
87
|
project_config = load_project_config()
|
|
77
88
|
project_id = project_config.project_id
|
|
78
89
|
api = APIClient()
|
|
79
|
-
|
|
90
|
+
|
|
80
91
|
try:
|
|
81
92
|
with Progress(
|
|
82
93
|
SpinnerColumn(),
|
|
@@ -85,26 +96,35 @@ async def run_build_flow(tag: str = "latest"):
|
|
|
85
96
|
task = progress.add_task(description="Bundling source code...", total=None)
|
|
86
97
|
tar_path = bundle_source(Path.cwd())
|
|
87
98
|
progress.update(task, description="Source bundled.")
|
|
88
|
-
|
|
89
|
-
task = progress.add_task(
|
|
90
|
-
|
|
99
|
+
|
|
100
|
+
task = progress.add_task(
|
|
101
|
+
description="Requesting upload path...", total=None
|
|
102
|
+
)
|
|
103
|
+
upload_data = await api.get(
|
|
104
|
+
"/v1/builds/upload-path?project_id=" + project_id
|
|
105
|
+
)
|
|
91
106
|
build_id = upload_data["build_id"]
|
|
92
107
|
upload_url = upload_data["upload_url"]
|
|
93
108
|
progress.update(task, description="Upload path received.")
|
|
94
|
-
|
|
109
|
+
|
|
95
110
|
task = progress.add_task(description="Uploading source...", total=None)
|
|
96
111
|
with open(tar_path, "rb") as f:
|
|
97
112
|
data = f.read()
|
|
98
|
-
await api.put_binary(
|
|
113
|
+
await api.put_binary(
|
|
114
|
+
upload_url, data, headers={"Content-Type": "application/gzip"}
|
|
115
|
+
)
|
|
99
116
|
progress.update(task, description="Source uploaded.")
|
|
100
|
-
|
|
117
|
+
|
|
101
118
|
task = progress.add_task(description="Triggering build...", total=None)
|
|
102
|
-
trigger_resp = await api.post(
|
|
103
|
-
|
|
119
|
+
trigger_resp = await api.post(
|
|
120
|
+
f"/v1/builds/{build_id}/trigger",
|
|
121
|
+
json={"image_tag": tag, "project_id": project_id},
|
|
122
|
+
)
|
|
123
|
+
|
|
104
124
|
# update build_id to the triggered true GCP build ID
|
|
105
125
|
build_id = trigger_resp["build_id"]
|
|
106
126
|
progress.update(task, description=f"Build triggered (ID: {build_id}).")
|
|
107
|
-
|
|
127
|
+
|
|
108
128
|
task = progress.add_task(description="Building...", total=None)
|
|
109
129
|
while True:
|
|
110
130
|
status_data = await api.get(f"/v1/builds/{build_id}/status")
|
|
@@ -113,11 +133,15 @@ async def run_build_flow(tag: str = "latest"):
|
|
|
113
133
|
progress.update(task, description="Build successful!")
|
|
114
134
|
return status_data
|
|
115
135
|
elif status in ["FAILURE", "INTERNAL_ERROR", "TIMEOUT", "CANCELLED"]:
|
|
116
|
-
progress.update(
|
|
136
|
+
progress.update(
|
|
137
|
+
task, description=f"[red]Build failed: {status}[/red]"
|
|
138
|
+
)
|
|
117
139
|
console.print(f"[red]Build ended with status: {status}[/red]")
|
|
118
|
-
log_url = status_data.get(
|
|
140
|
+
log_url = status_data.get("log_url")
|
|
119
141
|
if log_url:
|
|
120
|
-
console.print(
|
|
142
|
+
console.print(
|
|
143
|
+
f"Check logs here: [link={log_url}]{log_url}[/link]"
|
|
144
|
+
)
|
|
121
145
|
raise typer.Exit(code=1)
|
|
122
146
|
await asyncio.sleep(5)
|
|
123
147
|
except Exception as e:
|