tinybird 0.0.1.dev101__py3-none-any.whl → 0.0.1.dev103__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.
Potentially problematic release.
This version of tinybird might be problematic. Click here for more details.
- tinybird/feedback_manager.py +6 -0
- tinybird/prompts.py +8 -11
- tinybird/tb/__cli__.py +2 -2
- tinybird/tb/modules/cicd.py +1 -1
- tinybird/tb/modules/cli.py +0 -41
- tinybird/tb/modules/common.py +51 -3
- tinybird/tb/modules/create.py +2 -2
- tinybird/tb/modules/datafile/common.py +2 -0
- tinybird/tb/modules/infra.py +463 -226
- tinybird/tb/modules/local.py +30 -0
- tinybird/tb/modules/mock.py +23 -7
- tinybird/tb/modules/shell.py +1 -1
- tinybird/tb/modules/workspace.py +4 -25
- {tinybird-0.0.1.dev101.dist-info → tinybird-0.0.1.dev103.dist-info}/METADATA +2 -19
- {tinybird-0.0.1.dev101.dist-info → tinybird-0.0.1.dev103.dist-info}/RECORD +18 -18
- {tinybird-0.0.1.dev101.dist-info → tinybird-0.0.1.dev103.dist-info}/WHEEL +0 -0
- {tinybird-0.0.1.dev101.dist-info → tinybird-0.0.1.dev103.dist-info}/entry_points.txt +0 -0
- {tinybird-0.0.1.dev101.dist-info → tinybird-0.0.1.dev103.dist-info}/top_level.txt +0 -0
tinybird/tb/modules/infra.py
CHANGED
|
@@ -3,28 +3,26 @@ import subprocess
|
|
|
3
3
|
import time
|
|
4
4
|
import uuid
|
|
5
5
|
from pathlib import Path
|
|
6
|
-
from typing import Optional
|
|
6
|
+
from typing import Any, Dict, List, Optional
|
|
7
7
|
|
|
8
8
|
import click
|
|
9
|
-
import pyperclip
|
|
10
9
|
import requests
|
|
11
10
|
from click import Context
|
|
12
11
|
|
|
13
12
|
from tinybird.client import TinyB
|
|
14
|
-
from tinybird.syncasync import async_to_sync
|
|
15
13
|
from tinybird.tb.modules.cli import CLIException, cli
|
|
16
14
|
from tinybird.tb.modules.common import coro, echo_safe_humanfriendly_tables_format_smart_table
|
|
17
15
|
from tinybird.tb.modules.config import CLIConfig
|
|
18
16
|
from tinybird.tb.modules.feedback_manager import FeedbackManager
|
|
19
17
|
|
|
20
|
-
from .common import CONTEXT_SETTINGS
|
|
18
|
+
from .common import CONTEXT_SETTINGS, ask_for_organization, get_organizations_by_user
|
|
21
19
|
|
|
22
20
|
K8S_YML = """
|
|
23
21
|
---
|
|
24
22
|
apiVersion: v1
|
|
25
23
|
kind: Namespace
|
|
26
24
|
metadata:
|
|
27
|
-
name: %(
|
|
25
|
+
name: %(kubernetes_namespace)s
|
|
28
26
|
labels:
|
|
29
27
|
name: tinybird
|
|
30
28
|
---
|
|
@@ -32,7 +30,7 @@ apiVersion: v1
|
|
|
32
30
|
kind: ServiceAccount
|
|
33
31
|
metadata:
|
|
34
32
|
name: tinybird
|
|
35
|
-
namespace: %(
|
|
33
|
+
namespace: %(kubernetes_namespace)s
|
|
36
34
|
labels:
|
|
37
35
|
name: tinybird
|
|
38
36
|
automountServiceAccountToken: true
|
|
@@ -41,7 +39,7 @@ apiVersion: v1
|
|
|
41
39
|
kind: Service
|
|
42
40
|
metadata:
|
|
43
41
|
name: tinybird
|
|
44
|
-
namespace: %(
|
|
42
|
+
namespace: %(kubernetes_namespace)s
|
|
45
43
|
labels:
|
|
46
44
|
name: tinybird
|
|
47
45
|
spec:
|
|
@@ -57,15 +55,16 @@ spec:
|
|
|
57
55
|
apiVersion: networking.k8s.io/v1
|
|
58
56
|
kind: Ingress
|
|
59
57
|
metadata:
|
|
60
|
-
namespace: %(
|
|
58
|
+
namespace: %(kubernetes_namespace)s
|
|
61
59
|
name: tinybird
|
|
62
60
|
annotations:
|
|
61
|
+
external-dns.alpha.kubernetes.io/aws-evaluate-target-health: "false"
|
|
63
62
|
alb.ingress.kubernetes.io/listen-ports: '[{"HTTPS":443}]'
|
|
64
63
|
alb.ingress.kubernetes.io/scheme: internet-facing
|
|
65
64
|
alb.ingress.kubernetes.io/ssl-policy: ELBSecurityPolicy-TLS13-1-2-2021-06
|
|
66
65
|
alb.ingress.kubernetes.io/ssl-redirect: '443'
|
|
67
66
|
alb.ingress.kubernetes.io/target-type: 'ip'
|
|
68
|
-
alb.ingress.kubernetes.io/load-balancer-name: %(
|
|
67
|
+
alb.ingress.kubernetes.io/load-balancer-name: %(kubernetes_namespace)s
|
|
69
68
|
alb.ingress.kubernetes.io/success-codes: '200,301,302'
|
|
70
69
|
spec:
|
|
71
70
|
ingressClassName: alb
|
|
@@ -87,7 +86,7 @@ apiVersion: apps/v1
|
|
|
87
86
|
kind: StatefulSet
|
|
88
87
|
metadata:
|
|
89
88
|
name: tinybird
|
|
90
|
-
namespace: %(
|
|
89
|
+
namespace: %(kubernetes_namespace)s
|
|
91
90
|
spec:
|
|
92
91
|
serviceName: "tinybird"
|
|
93
92
|
replicas: 1
|
|
@@ -120,6 +119,8 @@ spec:
|
|
|
120
119
|
volumeMounts:
|
|
121
120
|
- name: clickhouse-data
|
|
122
121
|
mountPath: /var/lib/clickhouse
|
|
122
|
+
- name: redis-data
|
|
123
|
+
mountPath: /redis-data
|
|
123
124
|
volumeClaimTemplates:
|
|
124
125
|
- metadata:
|
|
125
126
|
name: clickhouse-data
|
|
@@ -127,11 +128,17 @@ spec:
|
|
|
127
128
|
accessModes: [ "ReadWriteOnce" ]
|
|
128
129
|
resources:
|
|
129
130
|
requests:
|
|
130
|
-
storage: 100Gi
|
|
131
|
-
|
|
131
|
+
storage: 100Gi%(storage_class_line)s
|
|
132
|
+
- metadata:
|
|
133
|
+
name: redis-data
|
|
134
|
+
spec:
|
|
135
|
+
accessModes: [ "ReadWriteOnce" ]
|
|
136
|
+
resources:
|
|
137
|
+
requests:
|
|
138
|
+
storage: 50Gi%(storage_class_line)s
|
|
132
139
|
"""
|
|
133
140
|
|
|
134
|
-
|
|
141
|
+
TERRAFORM_TEMPLATE = """
|
|
135
142
|
terraform {
|
|
136
143
|
required_providers {
|
|
137
144
|
aws = {
|
|
@@ -189,7 +196,7 @@ resource "aws_acm_certificate_validation" "cert" {
|
|
|
189
196
|
TERRAFORM_SECOND_TEMPLATE = """
|
|
190
197
|
# Create Route 53 record for the load balancer
|
|
191
198
|
data "aws_alb" "tinybird" {
|
|
192
|
-
name = "%(
|
|
199
|
+
name = "%(kubernetes_namespace)s"
|
|
193
200
|
}
|
|
194
201
|
|
|
195
202
|
resource "aws_route53_record" "tinybird" {
|
|
@@ -210,50 +217,173 @@ output "tinybird_dns" {
|
|
|
210
217
|
"""
|
|
211
218
|
|
|
212
219
|
|
|
220
|
+
class Infrastructure:
|
|
221
|
+
"""Class to manage Tinybird infrastructure operations."""
|
|
222
|
+
|
|
223
|
+
def __init__(self, client: TinyB):
|
|
224
|
+
self.client = client
|
|
225
|
+
|
|
226
|
+
async def get_organizations_info(self, config: CLIConfig):
|
|
227
|
+
self.orgs = await get_organizations_by_user(config)
|
|
228
|
+
|
|
229
|
+
async def create_infra(
|
|
230
|
+
self, name: str, host: str, organization_id: str
|
|
231
|
+
) -> Dict[str, Any]:
|
|
232
|
+
"""Create a new infrastructure."""
|
|
233
|
+
infra = await self.client.infra_create(
|
|
234
|
+
organization_id=organization_id, name=name, host=host
|
|
235
|
+
)
|
|
236
|
+
return infra
|
|
237
|
+
|
|
238
|
+
async def list_infras(self) -> None:
|
|
239
|
+
"""List all self-managed regions for the admin organization."""
|
|
240
|
+
|
|
241
|
+
infras = await self.get_infra_list()
|
|
242
|
+
columns = [
|
|
243
|
+
"name",
|
|
244
|
+
"host",
|
|
245
|
+
"organization",
|
|
246
|
+
]
|
|
247
|
+
table_human_readable = []
|
|
248
|
+
table_machine_readable = []
|
|
249
|
+
|
|
250
|
+
for infra in infras:
|
|
251
|
+
name = infra["name"]
|
|
252
|
+
host = infra["host"]
|
|
253
|
+
organization = infra["organization"]
|
|
254
|
+
|
|
255
|
+
table_human_readable.append((name, host, organization))
|
|
256
|
+
table_machine_readable.append(
|
|
257
|
+
{"name": name, "host": host, "organization": organization}
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
click.echo(FeedbackManager.info(message="\n** Infras:"))
|
|
261
|
+
echo_safe_humanfriendly_tables_format_smart_table(table_human_readable, column_names=columns)
|
|
262
|
+
click.echo("\n")
|
|
263
|
+
|
|
264
|
+
async def remove_infra(self, name: str):
|
|
265
|
+
try:
|
|
266
|
+
click.echo(FeedbackManager.highlight(message=f"» Deleting infrastructure '{name}' from Tinybird..."))
|
|
267
|
+
infras = await self.get_infra_list()
|
|
268
|
+
infra = next((infra for infra in infras if infra["name"] == name), None)
|
|
269
|
+
if not infra:
|
|
270
|
+
raise CLIException(f"Infrastructure '{name}' not found")
|
|
271
|
+
await self.client.infra_delete(infra["id"], infra["organization_id"])
|
|
272
|
+
click.echo(
|
|
273
|
+
FeedbackManager.success(message=f"\n✓ Infrastructure '{name}' deleted")
|
|
274
|
+
)
|
|
275
|
+
except Exception as e:
|
|
276
|
+
click.echo(FeedbackManager.error(message=f"✗ Error: {e}"))
|
|
277
|
+
|
|
278
|
+
async def update(self, infra_name: str, name: str, host: str):
|
|
279
|
+
infras_list = await self.get_infra_list()
|
|
280
|
+
infra = next(
|
|
281
|
+
(infra for infra in infras_list if infra["name"] == infra_name), None
|
|
282
|
+
)
|
|
283
|
+
if not infra:
|
|
284
|
+
return None
|
|
285
|
+
await self.client.infra_update(
|
|
286
|
+
infra_id=infra["id"],
|
|
287
|
+
organization_id=infra["organization_id"],
|
|
288
|
+
name=name,
|
|
289
|
+
host=host,
|
|
290
|
+
)
|
|
291
|
+
return infra
|
|
292
|
+
|
|
293
|
+
async def get_infra_list(self) -> List[Dict[str, str]]:
|
|
294
|
+
"""Get a list of all infrastructures across organizations.
|
|
295
|
+
|
|
296
|
+
Returns:
|
|
297
|
+
List[Dict[str, Any]]: List of infrastructure objects, each containing:
|
|
298
|
+
- id: str
|
|
299
|
+
- name: str
|
|
300
|
+
- host: str
|
|
301
|
+
- organization: str
|
|
302
|
+
"""
|
|
303
|
+
try:
|
|
304
|
+
all_infras = []
|
|
305
|
+
for org in self.orgs:
|
|
306
|
+
org_id = org.get("id") or ""
|
|
307
|
+
org_name = org.get("name")
|
|
308
|
+
try:
|
|
309
|
+
infras = await self.client.infra_list(organization_id=org_id)
|
|
310
|
+
for infra in infras:
|
|
311
|
+
infra["organization"] = org_name
|
|
312
|
+
all_infras.append(infra)
|
|
313
|
+
except Exception as e:
|
|
314
|
+
click.echo(
|
|
315
|
+
FeedbackManager.warning(
|
|
316
|
+
message=f"Could not fetch infras for organization {org_name}: {str(e)}"
|
|
317
|
+
)
|
|
318
|
+
)
|
|
319
|
+
continue
|
|
320
|
+
return all_infras
|
|
321
|
+
except Exception as e:
|
|
322
|
+
click.echo(
|
|
323
|
+
FeedbackManager.error(
|
|
324
|
+
message=f"Failed to list infrastructures: {str(e)}"
|
|
325
|
+
)
|
|
326
|
+
)
|
|
327
|
+
return []
|
|
328
|
+
|
|
329
|
+
|
|
213
330
|
@cli.group(context_settings=CONTEXT_SETTINGS, hidden=True)
|
|
214
331
|
@click.pass_context
|
|
215
|
-
|
|
332
|
+
@coro
|
|
333
|
+
async def infra(ctx: Context) -> None:
|
|
216
334
|
"""Infra commands."""
|
|
335
|
+
client: TinyB = ctx.ensure_object(dict)["client"]
|
|
336
|
+
config = CLIConfig.get_project_config()
|
|
337
|
+
infra = Infrastructure(client)
|
|
338
|
+
await infra.get_organizations_info(config)
|
|
339
|
+
ctx.ensure_object(dict)["infra"] = infra
|
|
217
340
|
|
|
218
341
|
|
|
219
342
|
@infra.command(name="init")
|
|
220
|
-
@click.option("--name", type=str, help="Name for identifying the self-managed
|
|
221
|
-
@click.option("--provider", type=str, help="Infrastructure provider. Possible values are: aws, gcp, azure)")
|
|
222
|
-
@click.option("--region", type=str, help="AWS region, when using aws as the provider")
|
|
343
|
+
@click.option("--name", type=str, help="Name for identifying the self-managed region in Tinybird")
|
|
344
|
+
@click.option("--cloud-provider", type=str, help="Infrastructure provider. Possible values are: aws, gcp, azure)")
|
|
345
|
+
@click.option("--cloud-region", type=str, help="AWS region, when using aws as the provider")
|
|
223
346
|
@click.option("--dns-zone-name", type=str, help="DNS zone name")
|
|
224
|
-
@click.option("--
|
|
225
|
-
@click.option("--
|
|
226
|
-
@click.option("--storage-class", type=str, help="Storage class for the k8s StatefulSet")
|
|
347
|
+
@click.option("--dns-record", type=str, help="DNS record name to create, without domain. For example, 'tinybird'")
|
|
348
|
+
@click.option("--kubernetes-namespace", type=str, help="Kubernetes namespace for the deployment")
|
|
349
|
+
@click.option("--kubernetes-storage-class", type=str, help="Storage class for the k8s StatefulSet")
|
|
227
350
|
@click.option(
|
|
228
351
|
"--auto-apply", is_flag=True, help="Automatically apply Terraform and kubectl configuration without prompting"
|
|
229
352
|
)
|
|
230
353
|
@click.option("--skip-apply", is_flag=True, help="Skip Terraform and kubectl configuration and application")
|
|
354
|
+
@click.option("--organization-id", type=str, help="Organization ID for the self-managed region")
|
|
231
355
|
@click.pass_context
|
|
232
|
-
|
|
356
|
+
@coro
|
|
357
|
+
async def infra_init(
|
|
233
358
|
ctx: Context,
|
|
234
359
|
name: str,
|
|
235
|
-
|
|
236
|
-
|
|
360
|
+
cloud_provider: str,
|
|
361
|
+
cloud_region: Optional[str] = None,
|
|
237
362
|
dns_zone_name: Optional[str] = None,
|
|
238
|
-
|
|
363
|
+
kubernetes_namespace: Optional[str] = None,
|
|
239
364
|
dns_record: Optional[str] = None,
|
|
240
|
-
|
|
365
|
+
kubernetes_storage_class: Optional[str] = None,
|
|
241
366
|
auto_apply: bool = False,
|
|
242
367
|
skip_apply: bool = False,
|
|
368
|
+
organization_id: Optional[str] = None,
|
|
243
369
|
) -> None:
|
|
244
370
|
"""Init infra"""
|
|
245
371
|
# Check if provider is specified
|
|
246
|
-
if not
|
|
247
|
-
click.echo(
|
|
372
|
+
if not cloud_provider:
|
|
373
|
+
click.echo(
|
|
374
|
+
FeedbackManager.error(
|
|
375
|
+
message="✗ Error: --cloud-provider option is required. Specify a cloud provider. Possible values are: aws, gcp, azure."
|
|
376
|
+
)
|
|
377
|
+
)
|
|
248
378
|
return
|
|
249
379
|
|
|
250
380
|
# AWS-specific Terraform template creation
|
|
251
|
-
if
|
|
252
|
-
click.echo("Provider not supported yet.")
|
|
381
|
+
if cloud_provider.lower() != "aws":
|
|
382
|
+
click.echo(FeedbackManager.error(message="✗ Error: Provider not supported yet."))
|
|
253
383
|
return
|
|
254
384
|
|
|
255
385
|
# Create infra directory if it doesn't exist
|
|
256
|
-
infra_dir = Path(f"infra/{
|
|
386
|
+
infra_dir = Path(f"infra/{cloud_provider}")
|
|
257
387
|
infra_dir.mkdir(exist_ok=True)
|
|
258
388
|
yaml_path = infra_dir / "k8s.yaml"
|
|
259
389
|
tf_path = infra_dir / "main.tf"
|
|
@@ -263,145 +393,193 @@ def infra_init(
|
|
|
263
393
|
config = {}
|
|
264
394
|
if config_path.exists():
|
|
265
395
|
try:
|
|
266
|
-
with open(config_path, "r") as f:
|
|
396
|
+
with open(config_path, "r") as f: # noqa: ASYNC230
|
|
267
397
|
config = json.load(f)
|
|
268
|
-
click.echo("Loaded existing configuration from config.json")
|
|
398
|
+
click.echo(FeedbackManager.info(message="** Loaded existing configuration from config.json"))
|
|
269
399
|
except json.JSONDecodeError:
|
|
270
|
-
click.echo(
|
|
400
|
+
click.echo(
|
|
401
|
+
FeedbackManager.warning(
|
|
402
|
+
message="** Warning: Could not parse existing config.json. Creating a new file..."
|
|
403
|
+
)
|
|
404
|
+
)
|
|
271
405
|
|
|
272
406
|
# Generate a random ID for default values
|
|
273
407
|
random_id = str(uuid.uuid4())[:8]
|
|
274
408
|
|
|
275
409
|
# Get or prompt for configuration values
|
|
276
|
-
name = name or click.prompt("Enter the name for your self-managed region", type=str)
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
or config.get("namespace")
|
|
282
|
-
or click.prompt("Enter the Kubernetes namespace", default=f"tinybird-{random_id}", type=str)
|
|
410
|
+
name = name or config.get("name") or click.prompt("** Enter the name for your self-managed region", type=str)
|
|
411
|
+
cloud_region = (
|
|
412
|
+
cloud_region
|
|
413
|
+
or config.get("cloud_region")
|
|
414
|
+
or click.prompt("** Enter the AWS region", default="us-east-1", type=str)
|
|
283
415
|
)
|
|
416
|
+
dns_zone_name = dns_zone_name or config.get("dns_zone_name") or click.prompt("** Enter the DNS zone name", type=str)
|
|
284
417
|
dns_record = (
|
|
285
418
|
dns_record
|
|
286
419
|
or config.get("dns_record")
|
|
287
|
-
or click.prompt("Enter the DNS record name, without domain", default=f"tinybird-{random_id}", type=str)
|
|
420
|
+
or click.prompt("** Enter the DNS record name, without domain", default=f"tinybird-{random_id}", type=str)
|
|
288
421
|
)
|
|
289
|
-
|
|
290
|
-
|
|
422
|
+
kubernetes_namespace = (
|
|
423
|
+
kubernetes_namespace
|
|
424
|
+
or config.get("kubernetes_namespace")
|
|
425
|
+
or click.prompt("** Enter the Kubernetes namespace", default=f"tinybird-{random_id}", type=str)
|
|
291
426
|
)
|
|
292
427
|
|
|
428
|
+
# Special handling for kubernetes_storage_class to handle None values
|
|
429
|
+
if kubernetes_storage_class is not None:
|
|
430
|
+
# Use the provided value
|
|
431
|
+
pass
|
|
432
|
+
elif "kubernetes_storage_class" in config:
|
|
433
|
+
# Use the value from config, even if it's None
|
|
434
|
+
kubernetes_storage_class = config["kubernetes_storage_class"]
|
|
435
|
+
else:
|
|
436
|
+
# Prompt the user
|
|
437
|
+
kubernetes_storage_class = click.prompt(
|
|
438
|
+
"** Enter the Kubernetes storage class (leave empty for None)", default="", show_default=False, type=str
|
|
439
|
+
)
|
|
440
|
+
# Convert empty string to None
|
|
441
|
+
if kubernetes_storage_class == "":
|
|
442
|
+
kubernetes_storage_class = None
|
|
443
|
+
|
|
444
|
+
kubernetes_context = config.get("kubernetes_context")
|
|
445
|
+
|
|
293
446
|
# Save configuration
|
|
294
447
|
config = {
|
|
295
|
-
"
|
|
296
|
-
"
|
|
448
|
+
"name": name,
|
|
449
|
+
"cloud_provider": cloud_provider,
|
|
450
|
+
"cloud_region": cloud_region,
|
|
297
451
|
"dns_zone_name": dns_zone_name,
|
|
298
|
-
"namespace": namespace,
|
|
299
452
|
"dns_record": dns_record,
|
|
300
|
-
"
|
|
453
|
+
"kubernetes_namespace": kubernetes_namespace,
|
|
454
|
+
"kubernetes_storage_class": kubernetes_storage_class,
|
|
455
|
+
"kubernetes_context": kubernetes_context,
|
|
301
456
|
}
|
|
302
457
|
|
|
303
|
-
with open(config_path, "w") as f:
|
|
458
|
+
with open(config_path, "w") as f: # noqa: ASYNC230
|
|
304
459
|
json.dump(config, f, indent=2)
|
|
305
460
|
|
|
306
|
-
click.echo(f"Configuration saved to {config_path}")
|
|
461
|
+
click.echo(FeedbackManager.info(message=f"** Configuration saved to {config_path}"))
|
|
307
462
|
|
|
308
|
-
|
|
463
|
+
infra: Infrastructure = ctx.ensure_object(dict)["infra"]
|
|
309
464
|
cli_config = CLIConfig.get_project_config()
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
465
|
+
|
|
466
|
+
infras = await infra.get_infra_list()
|
|
467
|
+
infra_name = next((infra for infra in infras if infra["name"] == name), None)
|
|
468
|
+
if not infra_name:
|
|
469
|
+
# Handle organization if not provided
|
|
470
|
+
organization_id, organization_name = await ask_for_organization(infra.orgs, organization_id)
|
|
471
|
+
if not organization_id:
|
|
472
|
+
return
|
|
317
473
|
click.echo(FeedbackManager.highlight(message=f"\n» Creating infrastructure '{name}' in Tinybird..."))
|
|
318
474
|
host = f"https://{dns_record}.{dns_zone_name}"
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
475
|
+
infra_obj = await infra.create_infra(name, host, organization_id)
|
|
476
|
+
else:
|
|
477
|
+
click.echo(FeedbackManager.highlight(message=f"» Infrastructure '{name}' already exists."))
|
|
478
|
+
if infra_name["host"] != f"https://{dns_record}.{dns_zone_name}":
|
|
479
|
+
click.echo(
|
|
480
|
+
FeedbackManager.highlight(
|
|
481
|
+
message="» Infrastructure host is different in the current config than the one provisioned at Tinybird"
|
|
482
|
+
)
|
|
483
|
+
)
|
|
484
|
+
if click.confirm("Would you like to update the host in the infra provisioned at Tinybird?"):
|
|
485
|
+
await infra.update(infra_name=name, name=name, host=f"https://{dns_record}.{dns_zone_name}")
|
|
486
|
+
organization_name = infra_name["organization"]
|
|
487
|
+
infra_obj = infra_name
|
|
322
488
|
|
|
323
489
|
# Write the Terraform template
|
|
324
|
-
terraform_content =
|
|
325
|
-
"aws_region":
|
|
490
|
+
terraform_content = TERRAFORM_TEMPLATE % {
|
|
491
|
+
"aws_region": cloud_region,
|
|
326
492
|
"dns_zone_name": dns_zone_name,
|
|
327
493
|
"dns_record": dns_record,
|
|
328
494
|
}
|
|
329
495
|
|
|
330
|
-
with open(tf_path, "w") as f:
|
|
496
|
+
with open(tf_path, "w") as f: # noqa: ASYNC230
|
|
331
497
|
f.write(terraform_content.lstrip())
|
|
332
498
|
|
|
333
|
-
click.echo(f"Created Terraform configuration in {tf_path}")
|
|
499
|
+
click.echo(FeedbackManager.info(message=f"** Created Terraform configuration in {tf_path}"))
|
|
500
|
+
|
|
501
|
+
# Prepare the storage class line based on whether kubernetes_storage_class is None
|
|
502
|
+
storage_class_line = f"\n storageClassName: {kubernetes_storage_class}" if kubernetes_storage_class else ""
|
|
334
503
|
|
|
335
504
|
new_content = K8S_YML % {
|
|
336
|
-
"
|
|
337
|
-
"
|
|
505
|
+
"kubernetes_namespace": kubernetes_namespace,
|
|
506
|
+
"storage_class_line": storage_class_line,
|
|
338
507
|
"full_dns_name": f"{dns_record}.{dns_zone_name}",
|
|
339
|
-
"infra_token":
|
|
508
|
+
"infra_token": infra_obj["token"],
|
|
340
509
|
"infra_workspace": cli_config.get("name", ""),
|
|
341
|
-
"infra_organization":
|
|
510
|
+
"infra_organization": organization_name,
|
|
342
511
|
"infra_user": cli_config.get_user_email() or "",
|
|
343
512
|
}
|
|
344
513
|
|
|
345
|
-
with open(yaml_path, "w") as f:
|
|
514
|
+
with open(yaml_path, "w") as f: # noqa: ASYNC230
|
|
346
515
|
f.write(new_content.lstrip())
|
|
347
516
|
|
|
348
|
-
click.echo(f"Created Kubernetes configuration in {yaml_path}")
|
|
517
|
+
click.echo(FeedbackManager.info(message=f"** Created Kubernetes configuration in {yaml_path}"))
|
|
349
518
|
|
|
350
519
|
# Apply Terraform configuration if user confirms
|
|
351
520
|
if not skip_apply:
|
|
352
521
|
# Initialize Terraform
|
|
353
|
-
click.echo("Initializing Terraform...")
|
|
354
|
-
|
|
522
|
+
click.echo(FeedbackManager.info(message="** Initializing Terraform..."))
|
|
523
|
+
command = f"terraform -chdir={infra_dir} init"
|
|
524
|
+
click.echo(FeedbackManager.highlight(message=f"» Executing: {command}"))
|
|
525
|
+
|
|
526
|
+
init_result = subprocess.run(["terraform", f"-chdir={infra_dir}", "init"], capture_output=True, text=True) # noqa: ASYNC221
|
|
355
527
|
|
|
356
528
|
if init_result.returncode != 0:
|
|
357
|
-
click.echo("Terraform initialization failed:")
|
|
529
|
+
click.echo(FeedbackManager.error(message="✗ Error: Terraform initialization failed:"))
|
|
358
530
|
click.echo(init_result.stderr)
|
|
359
531
|
return
|
|
360
532
|
|
|
361
|
-
# Run terraform plan
|
|
362
|
-
click.echo("
|
|
363
|
-
|
|
533
|
+
# Run terraform plan
|
|
534
|
+
click.echo(FeedbackManager.info(message="** Running Terraform plan..."))
|
|
535
|
+
command = f"terraform -chdir={infra_dir} plan"
|
|
536
|
+
click.echo(FeedbackManager.highlight(message=f"» Executing: {command}"))
|
|
537
|
+
plan_result = subprocess.run(command.split(), capture_output=True, text=True) # noqa: ASYNC221
|
|
364
538
|
|
|
365
539
|
if plan_result.returncode != 0:
|
|
366
|
-
click.echo("Terraform plan failed:")
|
|
367
|
-
click.echo(plan_result.stderr)
|
|
540
|
+
click.echo(FeedbackManager.error(message="✗ Error: Terraform plan failed:"))
|
|
541
|
+
click.echo(format_terraform_error(plan_result.stderr, "TERRAFORM PLAN"))
|
|
368
542
|
return
|
|
369
543
|
|
|
370
|
-
|
|
544
|
+
# Display formatted plan output
|
|
545
|
+
click.echo(format_terraform_output(plan_result.stdout, "TERRAFORM PLAN"))
|
|
371
546
|
|
|
372
547
|
# Apply Terraform configuration if user confirms
|
|
373
548
|
if auto_apply or click.confirm("Would you like to apply the Terraform configuration now?"):
|
|
374
|
-
click.echo("
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
)
|
|
549
|
+
click.echo(FeedbackManager.info(message="** Applying Terraform configuration..."))
|
|
550
|
+
command = f"terraform -chdir={infra_dir} apply -auto-approve"
|
|
551
|
+
click.echo(FeedbackManager.highlight(message=f"» Executing: {command}"))
|
|
552
|
+
apply_result = subprocess.run(command.split(), capture_output=True, text=True) # noqa: ASYNC221
|
|
378
553
|
|
|
379
554
|
if apply_result.returncode != 0:
|
|
380
|
-
click.echo("Terraform apply failed:")
|
|
381
|
-
click.echo(apply_result.stderr)
|
|
555
|
+
click.echo(FeedbackManager.error(message="✗ Error: Terraform apply failed:"))
|
|
556
|
+
click.echo(format_terraform_error(apply_result.stderr, "TERRAFORM APPLY"))
|
|
382
557
|
return
|
|
383
558
|
|
|
384
|
-
|
|
559
|
+
# Display formatted apply output
|
|
560
|
+
click.echo(format_terraform_output(apply_result.stdout, "TERRAFORM APPLY"))
|
|
385
561
|
|
|
386
562
|
# Prompt to apply the k8s configuration
|
|
387
563
|
if not skip_apply and (
|
|
388
564
|
auto_apply or click.confirm("Would you like to apply the Kubernetes configuration now?")
|
|
389
565
|
):
|
|
390
566
|
# Get current kubectl context
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
)
|
|
567
|
+
command = "kubectl config current-context"
|
|
568
|
+
click.echo(FeedbackManager.highlight(message=f"» Executing: {command}"))
|
|
569
|
+
current_context_result = subprocess.run(command.split(), capture_output=True, text=True) # noqa: ASYNC221
|
|
394
570
|
|
|
395
571
|
current_context = (
|
|
396
572
|
current_context_result.stdout.strip() if current_context_result.returncode == 0 else "unknown"
|
|
397
573
|
)
|
|
574
|
+
|
|
398
575
|
# Get available contexts
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
576
|
+
command = "kubectl config get-contexts -o name"
|
|
577
|
+
click.echo(FeedbackManager.highlight(message=f"» Executing: {command}"))
|
|
578
|
+
|
|
579
|
+
contexts_result = subprocess.run(command.split(), capture_output=True, text=True) # noqa: ASYNC221
|
|
402
580
|
|
|
403
581
|
if contexts_result.returncode != 0:
|
|
404
|
-
click.echo("Failed to get kubectl contexts:")
|
|
582
|
+
click.echo(FeedbackManager.error(message="✗ Error: Failed to get kubectl contexts:"))
|
|
405
583
|
click.echo(contexts_result.stderr)
|
|
406
584
|
return
|
|
407
585
|
|
|
@@ -412,7 +590,10 @@ def infra_init(
|
|
|
412
590
|
return
|
|
413
591
|
|
|
414
592
|
# Prompt user to select a context
|
|
415
|
-
if
|
|
593
|
+
if config["kubernetes_context"]:
|
|
594
|
+
selected_context = config["kubernetes_context"]
|
|
595
|
+
click.echo(f"Using the kubectl context specified in the config: {selected_context}")
|
|
596
|
+
elif len(available_contexts) == 1:
|
|
416
597
|
selected_context = available_contexts[0]
|
|
417
598
|
click.echo(f"Using the only available kubectl context: {selected_context}")
|
|
418
599
|
else:
|
|
@@ -435,67 +616,83 @@ def infra_init(
|
|
|
435
616
|
selected_context = available_contexts[selected_index - 1]
|
|
436
617
|
click.echo(f"Selected context: {selected_context}")
|
|
437
618
|
|
|
619
|
+
# Update the config with the selected context
|
|
620
|
+
config["kubernetes_context"] = selected_context
|
|
621
|
+
with open(config_path, "w") as f: # noqa: ASYNC230
|
|
622
|
+
json.dump(config, f, indent=2)
|
|
623
|
+
click.echo(f"Updated configuration with selected Kubernetes context: {selected_context}")
|
|
624
|
+
|
|
438
625
|
# Apply the configuration to the selected context
|
|
439
626
|
click.echo(f"Applying Kubernetes configuration to context '{selected_context}'...")
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
627
|
+
|
|
628
|
+
# First show a diff of what will be applied
|
|
629
|
+
command = f"kubectl --context {selected_context} diff -f {str(yaml_path)}"
|
|
630
|
+
click.echo(FeedbackManager.highlight(message=f"» Executing: {command}"))
|
|
631
|
+
|
|
632
|
+
diff_result = subprocess.run(command.split(), capture_output=True, text=True) # noqa: ASYNC221
|
|
633
|
+
|
|
634
|
+
if diff_result.returncode not in [0, 1]: # kubectl diff returns 1 when there are differences
|
|
635
|
+
if (
|
|
636
|
+
"Error from server (NotFound): namespaces" in diff_result.stderr
|
|
637
|
+
or f'namespace "{kubernetes_namespace}" not found' in diff_result.stderr
|
|
638
|
+
):
|
|
639
|
+
click.echo("\nThis appears to be the first deployment - namespace doesn't exist yet.")
|
|
640
|
+
click.echo("No diff available for the initial deployment.")
|
|
641
|
+
else:
|
|
642
|
+
click.echo("Failed to get diff for Kubernetes configuration:")
|
|
643
|
+
click.echo(diff_result.stderr)
|
|
644
|
+
else:
|
|
645
|
+
if diff_result.stdout:
|
|
646
|
+
click.echo("\nChanges that will be applied:")
|
|
647
|
+
click.echo(diff_result.stdout)
|
|
648
|
+
else:
|
|
649
|
+
click.echo("\nNo changes detected or resources don't exist yet.")
|
|
650
|
+
|
|
651
|
+
# Now apply the configuration
|
|
652
|
+
command = f"kubectl --context {selected_context} apply -f {str(yaml_path)}"
|
|
653
|
+
click.echo(FeedbackManager.highlight(message=f"» Executing: {command}"))
|
|
654
|
+
apply_result = subprocess.run(command.split(), capture_output=True, text=True) # noqa: ASYNC221
|
|
445
655
|
|
|
446
656
|
if apply_result.returncode != 0:
|
|
447
|
-
click.echo("Failed to apply Kubernetes configuration:")
|
|
657
|
+
click.echo(FeedbackManager.error(message="✗ Error: Failed to apply Kubernetes configuration:"))
|
|
448
658
|
click.echo(apply_result.stderr)
|
|
449
659
|
else:
|
|
450
|
-
click.echo("Kubernetes configuration applied successfully:")
|
|
660
|
+
click.echo(FeedbackManager.success(message="✓ Kubernetes configuration applied successfully:"))
|
|
451
661
|
click.echo(apply_result.stdout)
|
|
452
662
|
|
|
453
|
-
|
|
454
|
-
namespace = None
|
|
455
|
-
with open(yaml_path, "r") as f:
|
|
456
|
-
for line in f:
|
|
457
|
-
if "namespace:" in line and not namespace:
|
|
458
|
-
namespace = line.split("namespace:")[1].strip()
|
|
459
|
-
break
|
|
460
|
-
|
|
461
|
-
if not namespace:
|
|
462
|
-
namespace = "tinybird" # Default namespace
|
|
463
|
-
|
|
464
|
-
click.echo("\nWaiting for load balancer and DNS to be provisioned...")
|
|
663
|
+
click.echo(FeedbackManager.info(message="Waiting for load balancer and DNS to be provisioned..."))
|
|
465
664
|
|
|
466
665
|
max_attempts = 30 # 30 attempts * 10 seconds = 5 minutes
|
|
467
666
|
endpoint_url = f"https://{dns_record}.{dns_zone_name}"
|
|
468
667
|
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
if response.status_code < 400: # Consider any non-error response as success
|
|
482
|
-
click.echo(click.style("\n✅ HTTPS endpoint is now accessible!", fg="green", bold=True))
|
|
483
|
-
break
|
|
484
|
-
except requests.RequestException:
|
|
485
|
-
pass
|
|
486
|
-
|
|
487
|
-
if attempt == max_attempts - 1:
|
|
488
|
-
click.echo(
|
|
489
|
-
click.style(
|
|
490
|
-
"\n⚠️ HTTPS endpoint not accessible after 5 minutes", fg="yellow", bold=True
|
|
491
|
-
)
|
|
492
|
-
)
|
|
668
|
+
click.echo(f"Checking endpoint availability: {endpoint_url}")
|
|
669
|
+
|
|
670
|
+
for attempt in range(max_attempts):
|
|
671
|
+
progress = "#" * (attempt + 1)
|
|
672
|
+
click.echo(
|
|
673
|
+
f"\rAttempt {attempt + 1}/{max_attempts}: Checking if endpoint is ready... [{progress}]",
|
|
674
|
+
nl=False,
|
|
675
|
+
)
|
|
676
|
+
|
|
677
|
+
try:
|
|
678
|
+
response = requests.get(endpoint_url, allow_redirects=False, timeout=5) # noqa: ASYNC210
|
|
679
|
+
if response.status_code < 400: # Consider any non-error response as success
|
|
493
680
|
click.echo(
|
|
494
|
-
"
|
|
681
|
+
"\n" + click.style("✅ HTTPS endpoint is now accessible!", fg="green", bold=True)
|
|
495
682
|
)
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
683
|
+
break
|
|
684
|
+
except (requests.RequestException, requests.Timeout):
|
|
685
|
+
pass
|
|
686
|
+
|
|
687
|
+
if attempt == max_attempts - 1:
|
|
688
|
+
click.echo(
|
|
689
|
+
"\n"
|
|
690
|
+
+ click.style("⚠️ HTTPS endpoint not accessible after 5 minutes", fg="yellow", bold=True)
|
|
691
|
+
)
|
|
692
|
+
click.echo(" This might be due to DNS propagation or the Load Balancer provisioning delays")
|
|
693
|
+
click.echo(f" Try accessing {endpoint_url} manually in a few minutes")
|
|
694
|
+
else:
|
|
695
|
+
time.sleep(10) # noqa: ASYNC251
|
|
499
696
|
|
|
500
697
|
if not skip_apply:
|
|
501
698
|
# Print a summary with the endpoint URL
|
|
@@ -515,110 +712,150 @@ def infra_init(
|
|
|
515
712
|
@click.argument("name")
|
|
516
713
|
@click.pass_context
|
|
517
714
|
@coro
|
|
518
|
-
async def
|
|
715
|
+
async def infra_remove(ctx: click.Context, name: str):
|
|
519
716
|
"""Delete an infrastructure from Tinybird"""
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
client: TinyB = ctx.ensure_object(dict)["client"]
|
|
523
|
-
user_workspaces = await client.user_workspaces_with_organization()
|
|
524
|
-
admin_org_id = user_workspaces.get("organization_id")
|
|
525
|
-
if not admin_org_id:
|
|
526
|
-
raise CLIException("No organization associated to this workspace")
|
|
527
|
-
infras = await client.infra_list(admin_org_id)
|
|
528
|
-
infra_id = next((infra["id"] for infra in infras if infra["name"] == name), None)
|
|
529
|
-
if not infra_id:
|
|
530
|
-
raise CLIException(f"Infrastructure '{name}' not found")
|
|
531
|
-
await client.infra_delete(infra_id, admin_org_id)
|
|
532
|
-
click.echo(FeedbackManager.success(message=f"\n✓ Infrastructure '{name}' deleted"))
|
|
533
|
-
except Exception as e:
|
|
534
|
-
click.echo(FeedbackManager.error(message=f"✗ Error: {e}"))
|
|
717
|
+
infra: Infrastructure = ctx.ensure_object(dict)["infra"]
|
|
718
|
+
await infra.remove_infra(name)
|
|
535
719
|
|
|
536
720
|
|
|
537
721
|
@infra.command(name="ls")
|
|
538
722
|
@click.pass_context
|
|
539
723
|
@coro
|
|
540
|
-
async def
|
|
724
|
+
async def infra_list(ctx: click.Context):
|
|
541
725
|
"""List self-managed infrastructures"""
|
|
726
|
+
infra: Infrastructure = ctx.ensure_object(dict)["infra"]
|
|
727
|
+
await infra.list_infras()
|
|
542
728
|
|
|
543
|
-
client: TinyB = ctx.ensure_object(dict)["client"]
|
|
544
|
-
config = CLIConfig.get_project_config()
|
|
545
|
-
user_client = config.get_client(token=config.get_user_token() or "")
|
|
546
|
-
user_workspaces = await user_client.user_workspaces_with_organization()
|
|
547
|
-
admin_org_id = user_workspaces.get("organization_id")
|
|
548
|
-
infras = await client.infra_list(organization_id=admin_org_id)
|
|
549
|
-
columns = [
|
|
550
|
-
"name",
|
|
551
|
-
"host",
|
|
552
|
-
]
|
|
553
|
-
table_human_readable = []
|
|
554
|
-
table_machine_readable = []
|
|
555
729
|
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
730
|
+
@infra.command(name="add")
|
|
731
|
+
@click.option("--name", type=str, help="Name for identifying the self-managed region in Tinybird")
|
|
732
|
+
@click.option("--host", type=str, help="Host for the self-managed region")
|
|
733
|
+
@click.option("--organization-id", type=str, help="Organization ID for the self-managed region")
|
|
734
|
+
@click.pass_context
|
|
735
|
+
@coro
|
|
736
|
+
async def infra_create(ctx: click.Context, name: str, host: str, organization_id: Optional[str] = None):
|
|
737
|
+
"""Creates a new self-managed region from an existing infrastructure URL."""
|
|
738
|
+
infra: Infrastructure = ctx.ensure_object(dict)["infra"]
|
|
559
739
|
|
|
560
|
-
|
|
561
|
-
|
|
740
|
+
organization_id, organization_name = await ask_for_organization(
|
|
741
|
+
infra.orgs, organization_id
|
|
742
|
+
)
|
|
743
|
+
if not organization_id:
|
|
744
|
+
return
|
|
562
745
|
|
|
563
|
-
click.
|
|
564
|
-
|
|
565
|
-
click.echo("\n")
|
|
746
|
+
name = name or click.prompt("Enter name", type=str)
|
|
747
|
+
host = host or click.prompt("Enter host", type=str)
|
|
566
748
|
|
|
749
|
+
click.echo(FeedbackManager.highlight(message=f"» Adding self-managed region '{name}' in Tinybird..."))
|
|
750
|
+
new_infra = await infra.create_infra(name, host, organization_id)
|
|
751
|
+
infra_token = new_infra["token"]
|
|
752
|
+
click.echo(FeedbackManager.success(message=f"\n✓ Self-managed region '{name}' added in '{organization_name}' Organization"))
|
|
567
753
|
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
"
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
user_workspaces = await client.user_workspaces_with_organization()
|
|
578
|
-
admin_org_id = user_workspaces.get("organization_id")
|
|
579
|
-
if not admin_org_id:
|
|
580
|
-
raise CLIException("No organization associated to this workspace")
|
|
581
|
-
name = name or click.prompt("Enter name", type=str)
|
|
582
|
-
host = host or click.prompt("Enter host", type=str)
|
|
583
|
-
click.echo(FeedbackManager.highlight(message=f"\n» Creating infrastructure '{name}' in Tinybird..."))
|
|
584
|
-
infra = await client.infra_create(organization_id=admin_org_id, name=name, host=host)
|
|
585
|
-
click.echo(FeedbackManager.success(message=f"\n✓ Infrastructure '{name}' created"))
|
|
586
|
-
pyperclip.copy(infra["token"])
|
|
587
|
-
click.echo(FeedbackManager.info(message="Access token has been copied to your clipboard."))
|
|
588
|
-
click.echo(
|
|
589
|
-
FeedbackManager.info(message="Pass it as an environment variable in your deployment as TB_INFRA_TOKEN")
|
|
590
|
-
)
|
|
591
|
-
except Exception as e:
|
|
592
|
-
click.echo(FeedbackManager.error(message=f"✗ Error: {str(e)}"))
|
|
754
|
+
# Get CLI config to access workspace and user information
|
|
755
|
+
cli_config = CLIConfig.get_project_config()
|
|
756
|
+
|
|
757
|
+
# Print environment variables needed for self-managed infrastructure
|
|
758
|
+
click.echo(FeedbackManager.highlight(message="» Required environment variables:"))
|
|
759
|
+
click.echo(f"TB_INFRA_TOKEN={infra_token}")
|
|
760
|
+
click.echo(f"TB_INFRA_WORKSPACE={cli_config.get('name', '')}")
|
|
761
|
+
click.echo(f"TB_INFRA_ORGANIZATION={organization_name}")
|
|
762
|
+
click.echo(f"TB_INFRA_USER={cli_config.get_user_email() or ''}")
|
|
593
763
|
|
|
594
764
|
|
|
595
765
|
@infra.command(name="update")
|
|
596
766
|
@click.argument("infra_name")
|
|
597
|
-
@click.option("--name", type=str, help="Name for identifying the self-managed
|
|
598
|
-
@click.option("--host", type=str, help="Host for the
|
|
767
|
+
@click.option("--name", type=str, help="Name for identifying the self-managed region in Tinybird")
|
|
768
|
+
@click.option("--host", type=str, help="Host for the self-managed region")
|
|
599
769
|
@click.pass_context
|
|
600
770
|
@coro
|
|
601
771
|
async def infra_update(ctx: click.Context, infra_name: str, name: str, host: str):
|
|
602
|
-
"""
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
if not name and not host:
|
|
772
|
+
"""Updates the URL of an existing self-managed region."""
|
|
773
|
+
infra: Infrastructure = ctx.ensure_object(dict)["infra"]
|
|
774
|
+
if not name and not host:
|
|
775
|
+
click.echo(FeedbackManager.warning(message="No name or host provided. Provide either a name or a host."))
|
|
776
|
+
return
|
|
777
|
+
|
|
778
|
+
if name or host:
|
|
779
|
+
try:
|
|
611
780
|
click.echo(
|
|
612
|
-
FeedbackManager.
|
|
781
|
+
FeedbackManager.highlight(message=f"» Updating self-managed region'{infra_name}' in Tinybird...")
|
|
613
782
|
)
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
if name or host:
|
|
617
|
-
infras = await client.infra_list(organization_id=admin_org_id)
|
|
618
|
-
infra_id = next((infra["id"] for infra in infras if infra["name"] == infra_name), None)
|
|
783
|
+
infra_id = await infra.update(infra_name, name, host)
|
|
619
784
|
if not infra_id:
|
|
620
|
-
raise CLIException(f"
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
785
|
+
raise CLIException(f"Self-managed region '{infra_name}' not found")
|
|
786
|
+
except Exception as e:
|
|
787
|
+
click.echo(FeedbackManager.error(message=f"✗ Error: {str(e)}"))
|
|
788
|
+
|
|
789
|
+
|
|
790
|
+
def format_terraform_output(output, command_name="Terraform"):
|
|
791
|
+
"""Format Terraform command output with colors and styling.
|
|
792
|
+
|
|
793
|
+
Args:
|
|
794
|
+
output: The stdout string from the Terraform command
|
|
795
|
+
command_name: The name of the command (e.g., "PLAN", "APPLY")
|
|
796
|
+
|
|
797
|
+
Returns:
|
|
798
|
+
Formatted output ready to be displayed
|
|
799
|
+
"""
|
|
800
|
+
if not output:
|
|
801
|
+
return FeedbackManager.info(message=f"No output from {command_name} command.")
|
|
802
|
+
|
|
803
|
+
# Add a header
|
|
804
|
+
formatted_lines = [
|
|
805
|
+
"\n" + "=" * 80,
|
|
806
|
+
FeedbackManager.highlight(message=f"{command_name.upper()} OUTPUT"),
|
|
807
|
+
"=" * 80 + "\n",
|
|
808
|
+
]
|
|
809
|
+
|
|
810
|
+
# Process and format the output
|
|
811
|
+
for line in output.splitlines():
|
|
812
|
+
# Highlight resource changes
|
|
813
|
+
if line.strip().startswith("+ "):
|
|
814
|
+
formatted_lines.append(click.style(line, fg="green"))
|
|
815
|
+
elif line.strip().startswith("- "):
|
|
816
|
+
formatted_lines.append(click.style(line, fg="red"))
|
|
817
|
+
elif line.strip().startswith("~ "):
|
|
818
|
+
formatted_lines.append(click.style(line, fg="yellow"))
|
|
819
|
+
# Highlight plan/apply summary
|
|
820
|
+
elif any(keyword in line for keyword in ["Plan:", "Apply complete", "Destroy complete"]):
|
|
821
|
+
formatted_lines.append("\n" + click.style(line, bold=True, fg="cyan"))
|
|
822
|
+
else:
|
|
823
|
+
formatted_lines.append(line)
|
|
824
|
+
|
|
825
|
+
# Add a footer
|
|
826
|
+
formatted_lines.append("\n" + "=" * 80)
|
|
827
|
+
|
|
828
|
+
return "\n".join(formatted_lines)
|
|
829
|
+
|
|
830
|
+
|
|
831
|
+
def format_terraform_error(error_output, command_name="Terraform"):
|
|
832
|
+
"""Format Terraform command error output with colors and styling.
|
|
833
|
+
|
|
834
|
+
Args:
|
|
835
|
+
error_output: The stderr string from the Terraform command
|
|
836
|
+
command_name: The name of the command (e.g., "PLAN", "APPLY")
|
|
837
|
+
|
|
838
|
+
Returns:
|
|
839
|
+
Formatted error output ready to be displayed
|
|
840
|
+
"""
|
|
841
|
+
if not error_output:
|
|
842
|
+
return FeedbackManager.error(message=f"Unknown error in {command_name} command.")
|
|
843
|
+
|
|
844
|
+
# Add a header
|
|
845
|
+
formatted_lines = ["\n" + "=" * 80, FeedbackManager.error(message=f"{command_name.upper()} ERROR"), "=" * 80 + "\n"]
|
|
846
|
+
|
|
847
|
+
# Process and format the error output
|
|
848
|
+
for line in error_output.splitlines():
|
|
849
|
+
# Highlight error messages
|
|
850
|
+
if any(keyword in line.lower() for keyword in ["error", "failed", "fatal"]):
|
|
851
|
+
formatted_lines.append(click.style(line, fg="red", bold=True))
|
|
852
|
+
# Highlight warnings
|
|
853
|
+
elif any(keyword in line.lower() for keyword in ["warning", "deprecated"]):
|
|
854
|
+
formatted_lines.append(click.style(line, fg="yellow"))
|
|
855
|
+
else:
|
|
856
|
+
formatted_lines.append(line)
|
|
857
|
+
|
|
858
|
+
# Add a footer
|
|
859
|
+
formatted_lines.append("\n" + "=" * 80)
|
|
860
|
+
|
|
861
|
+
return "\n".join(formatted_lines)
|