tinybird 0.0.1.dev99__py3-none-any.whl → 0.0.1.dev101__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/client.py +10 -0
- tinybird/sql.py +2 -2
- tinybird/sql_toolset.py +16 -3
- tinybird/tb/__cli__.py +2 -2
- tinybird/tb/modules/datafile/common.py +21 -1
- tinybird/tb/modules/infra.py +282 -351
- {tinybird-0.0.1.dev99.dist-info → tinybird-0.0.1.dev101.dist-info}/METADATA +1 -1
- {tinybird-0.0.1.dev99.dist-info → tinybird-0.0.1.dev101.dist-info}/RECORD +11 -11
- {tinybird-0.0.1.dev99.dist-info → tinybird-0.0.1.dev101.dist-info}/WHEEL +0 -0
- {tinybird-0.0.1.dev99.dist-info → tinybird-0.0.1.dev101.dist-info}/entry_points.txt +0 -0
- {tinybird-0.0.1.dev99.dist-info → tinybird-0.0.1.dev101.dist-info}/top_level.txt +0 -0
tinybird/client.py
CHANGED
|
@@ -915,6 +915,16 @@ class TinyB:
|
|
|
915
915
|
}
|
|
916
916
|
return await self._req(f"/v1/infra?{urlencode(params)}", method="POST")
|
|
917
917
|
|
|
918
|
+
async def infra_update(self, infra_id: str, organization_id: str, name: str, host: str) -> Dict[str, Any]:
|
|
919
|
+
params = {
|
|
920
|
+
"organization_id": organization_id,
|
|
921
|
+
}
|
|
922
|
+
if name:
|
|
923
|
+
params["name"] = name
|
|
924
|
+
if host:
|
|
925
|
+
params["host"] = host
|
|
926
|
+
return await self._req(f"/v1/infra/{infra_id}?{urlencode(params)}", method="PUT")
|
|
927
|
+
|
|
918
928
|
async def infra_list(self, organization_id: str) -> List[Dict[str, Any]]:
|
|
919
929
|
data = await self._req(f"/v1/infra?organization_id={organization_id}")
|
|
920
930
|
return data.get("infras", [])
|
tinybird/sql.py
CHANGED
|
@@ -174,7 +174,7 @@ def try_to_fix_nullable_in_simple_aggregating_function(t: str) -> Optional[str]:
|
|
|
174
174
|
return result
|
|
175
175
|
|
|
176
176
|
|
|
177
|
-
def schema_to_sql_columns(schema: List[Dict[str, Any]]) -> List[str]:
|
|
177
|
+
def schema_to_sql_columns(schema: List[Dict[str, Any]], skip_jsonpaths: bool = False) -> List[str]:
|
|
178
178
|
"""return an array with each column in SQL
|
|
179
179
|
>>> schema_to_sql_columns([{'name': 'temperature', 'type': 'Float32', 'codec': None, 'default_value': None, 'nullable': False, 'normalized_name': 'temperature'}, {'name': 'temperature_delta', 'type': 'Float32', 'codec': 'CODEC(Delta(4), LZ4))', 'default_value': 'MATERIALIZED temperature', 'nullable': False, 'normalized_name': 'temperature_delta'}])
|
|
180
180
|
['`temperature` Float32', '`temperature_delta` Float32 MATERIALIZED temperature CODEC(Delta(4), LZ4))']
|
|
@@ -198,7 +198,7 @@ def schema_to_sql_columns(schema: List[Dict[str, Any]]) -> List[str]:
|
|
|
198
198
|
else:
|
|
199
199
|
_type = x["type"]
|
|
200
200
|
parts = [col_name(name, backquotes=True), _type]
|
|
201
|
-
if x.get("jsonpath", None):
|
|
201
|
+
if x.get("jsonpath", None) and not skip_jsonpaths:
|
|
202
202
|
parts.append(f"`json:{x['jsonpath']}`")
|
|
203
203
|
if "default_value" in x and x["default_value"] not in ("", None):
|
|
204
204
|
parts.append(x["default_value"])
|
tinybird/sql_toolset.py
CHANGED
|
@@ -10,6 +10,9 @@ from toposort import toposort
|
|
|
10
10
|
|
|
11
11
|
from tinybird.ch_utils.constants import COPY_ENABLED_TABLE_FUNCTIONS, ENABLED_TABLE_FUNCTIONS
|
|
12
12
|
|
|
13
|
+
# VALID_REMOTE is used to explicitly vet queries sent to sql_toolset. In this module, when a table used in a query has
|
|
14
|
+
# VALID_REMOTE in the database portion (as in (VALID_REMOTE, "select * from cluster(tinybird, public.t_blabla)"), the
|
|
15
|
+
# query is not blocked, even if the rhs (the "select * ..." bit) is not found in the various collections of allowed tables.
|
|
13
16
|
VALID_REMOTE = "VALID_REMOTE"
|
|
14
17
|
|
|
15
18
|
|
|
@@ -351,14 +354,24 @@ def is_invalid_resource(
|
|
|
351
354
|
valid_tables: Optional[Set[Tuple[str, str]]] = None,
|
|
352
355
|
) -> bool:
|
|
353
356
|
return is_invalid_resource_from_other_workspace(
|
|
354
|
-
r, database, default_database, _replaced_with
|
|
357
|
+
r, database, default_database, _replaced_with, valid_tables
|
|
355
358
|
) or is_invalid_resource_from_current_workspace(r, database, default_database, _replaced_with, valid_tables)
|
|
356
359
|
|
|
357
360
|
|
|
358
361
|
def is_invalid_resource_from_other_workspace(
|
|
359
|
-
r: Tuple[str, str],
|
|
362
|
+
r: Tuple[str, str],
|
|
363
|
+
database: str,
|
|
364
|
+
default_database: str,
|
|
365
|
+
_replaced_with: Set[Tuple[str, str]],
|
|
366
|
+
valid_tables: Optional[Set[Tuple[str, str]]],
|
|
360
367
|
) -> bool:
|
|
361
|
-
return database not in [default_database, "tinybird", VALID_REMOTE] and r not in _replaced_with
|
|
368
|
+
# return database not in [default_database, "tinybird", VALID_REMOTE] and r not in _replaced_with
|
|
369
|
+
return bool(
|
|
370
|
+
database not in [default_database, "tinybird", VALID_REMOTE]
|
|
371
|
+
and valid_tables
|
|
372
|
+
and r not in valid_tables
|
|
373
|
+
and r not in _replaced_with
|
|
374
|
+
)
|
|
362
375
|
|
|
363
376
|
|
|
364
377
|
def is_invalid_resource_from_current_workspace(
|
tinybird/tb/__cli__.py
CHANGED
|
@@ -4,5 +4,5 @@ __description__ = 'Tinybird Command Line Tool'
|
|
|
4
4
|
__url__ = 'https://www.tinybird.co/docs/cli/introduction.html'
|
|
5
5
|
__author__ = 'Tinybird'
|
|
6
6
|
__author_email__ = 'support@tinybird.co'
|
|
7
|
-
__version__ = '0.0.1.
|
|
8
|
-
__revision__ = '
|
|
7
|
+
__version__ = '0.0.1.dev101'
|
|
8
|
+
__revision__ = '5dc0d4b'
|
|
@@ -183,6 +183,15 @@ class DatafileKind(Enum):
|
|
|
183
183
|
return extension_map[extension]
|
|
184
184
|
|
|
185
185
|
|
|
186
|
+
KAFKA_PARAMS = {
|
|
187
|
+
"kafka_connection_name",
|
|
188
|
+
"kafka_topic",
|
|
189
|
+
"kafka_group_id",
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
REQUIRED_KAFKA_PARAMS = KAFKA_PARAMS
|
|
193
|
+
|
|
194
|
+
|
|
186
195
|
class Datafile:
|
|
187
196
|
def __init__(self) -> None:
|
|
188
197
|
self.maintainer: Optional[str] = None
|
|
@@ -248,9 +257,10 @@ class Datafile:
|
|
|
248
257
|
f"Invalid permission {token['permission']} for token {token['token_name']}. Only READ is allowed for pipes"
|
|
249
258
|
)
|
|
250
259
|
elif self.kind == DatafileKind.datasource:
|
|
251
|
-
# TODO(eclbg):
|
|
252
260
|
# [x] Just one node
|
|
253
261
|
# [x] Engine is present
|
|
262
|
+
# [x] Token permissions are valid
|
|
263
|
+
# [x] If it's a kafka datasource, all required kafka params are present
|
|
254
264
|
# [ ] ...
|
|
255
265
|
if len(self.nodes) > 1:
|
|
256
266
|
# Our users are not aware of data source data files being a single-node data file, hence this error
|
|
@@ -259,11 +269,20 @@ class Datafile:
|
|
|
259
269
|
node = self.nodes[0]
|
|
260
270
|
if "schema" not in node:
|
|
261
271
|
raise DatafileValidationError("SCHEMA is mandatory")
|
|
272
|
+
# Validate token permissions
|
|
262
273
|
for token in self.tokens:
|
|
263
274
|
if token["permission"].upper() not in {"READ", "APPEND"}:
|
|
264
275
|
raise DatafileValidationError(
|
|
265
276
|
f"Invalid permission {token['permission']} for token {token['token_name']}. Only READ and APPEND are allowed for datasources"
|
|
266
277
|
)
|
|
278
|
+
# Validate Kafka params
|
|
279
|
+
if any(param in node for param in KAFKA_PARAMS) and not all(
|
|
280
|
+
param in node for param in REQUIRED_KAFKA_PARAMS
|
|
281
|
+
):
|
|
282
|
+
missing = [param for param in REQUIRED_KAFKA_PARAMS if param not in node]
|
|
283
|
+
raise DatafileValidationError(
|
|
284
|
+
f"Some Kafka params have been provided, but the following required ones are missing: {missing}"
|
|
285
|
+
)
|
|
267
286
|
else:
|
|
268
287
|
# We cannot validate a datafile whose kind is unknown
|
|
269
288
|
pass
|
|
@@ -1427,6 +1446,7 @@ def parse(
|
|
|
1427
1446
|
"include": include,
|
|
1428
1447
|
"sql": sql("sql"),
|
|
1429
1448
|
"version": version,
|
|
1449
|
+
# TODO(eclbg): We should decide on a single place to define the kafka params. Definitely not here.
|
|
1430
1450
|
"kafka_connection_name": assign_var("kafka_connection_name"),
|
|
1431
1451
|
"kafka_topic": assign_var("kafka_topic"),
|
|
1432
1452
|
"kafka_group_id": assign_var("kafka_group_id"),
|
tinybird/tb/modules/infra.py
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import json
|
|
2
|
+
import subprocess
|
|
3
|
+
import time
|
|
2
4
|
import uuid
|
|
3
5
|
from pathlib import Path
|
|
4
6
|
from typing import Optional
|
|
5
7
|
|
|
6
8
|
import click
|
|
9
|
+
import pyperclip
|
|
10
|
+
import requests
|
|
7
11
|
from click import Context
|
|
8
12
|
|
|
9
13
|
from tinybird.client import TinyB
|
|
@@ -40,28 +44,50 @@ metadata:
|
|
|
40
44
|
namespace: %(namespace)s
|
|
41
45
|
labels:
|
|
42
46
|
name: tinybird
|
|
43
|
-
annotations:
|
|
44
|
-
service.beta.kubernetes.io/aws-load-balancer-type: 'external'
|
|
45
|
-
service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: 'ip'
|
|
46
|
-
service.beta.kubernetes.io/aws-load-balancer-backend-protocol: 'tcp'
|
|
47
|
-
service.beta.kubernetes.io/aws-load-balancer-scheme: 'internet-facing'
|
|
48
|
-
service.beta.kubernetes.io/aws-load-balancer-ssl-negotiation-policy: 'ELBSecurityPolicy-TLS13-1-2-2021-06'
|
|
49
|
-
service.beta.kubernetes.io/aws-load-balancer-ssl-cert: '%(cert_arn)s'
|
|
50
47
|
spec:
|
|
51
|
-
type:
|
|
48
|
+
type: ClusterIP
|
|
52
49
|
ports:
|
|
53
|
-
- port:
|
|
50
|
+
- port: 80
|
|
54
51
|
targetPort: http
|
|
55
52
|
protocol: TCP
|
|
56
|
-
name:
|
|
53
|
+
name: http
|
|
57
54
|
selector:
|
|
58
55
|
name: tinybird
|
|
59
56
|
---
|
|
57
|
+
apiVersion: networking.k8s.io/v1
|
|
58
|
+
kind: Ingress
|
|
59
|
+
metadata:
|
|
60
|
+
namespace: %(namespace)s
|
|
61
|
+
name: tinybird
|
|
62
|
+
annotations:
|
|
63
|
+
alb.ingress.kubernetes.io/listen-ports: '[{"HTTPS":443}]'
|
|
64
|
+
alb.ingress.kubernetes.io/scheme: internet-facing
|
|
65
|
+
alb.ingress.kubernetes.io/ssl-policy: ELBSecurityPolicy-TLS13-1-2-2021-06
|
|
66
|
+
alb.ingress.kubernetes.io/ssl-redirect: '443'
|
|
67
|
+
alb.ingress.kubernetes.io/target-type: 'ip'
|
|
68
|
+
alb.ingress.kubernetes.io/load-balancer-name: %(namespace)s
|
|
69
|
+
alb.ingress.kubernetes.io/success-codes: '200,301,302'
|
|
70
|
+
spec:
|
|
71
|
+
ingressClassName: alb
|
|
72
|
+
tls:
|
|
73
|
+
- hosts:
|
|
74
|
+
- %(full_dns_name)s
|
|
75
|
+
rules:
|
|
76
|
+
- http:
|
|
77
|
+
paths:
|
|
78
|
+
- path: /
|
|
79
|
+
pathType: Prefix
|
|
80
|
+
backend:
|
|
81
|
+
service:
|
|
82
|
+
name: tinybird
|
|
83
|
+
port:
|
|
84
|
+
name: http
|
|
85
|
+
---
|
|
60
86
|
apiVersion: apps/v1
|
|
61
87
|
kind: StatefulSet
|
|
62
88
|
metadata:
|
|
63
89
|
name: tinybird
|
|
64
|
-
namespace:
|
|
90
|
+
namespace: %(namespace)s
|
|
65
91
|
spec:
|
|
66
92
|
serviceName: "tinybird"
|
|
67
93
|
replicas: 1
|
|
@@ -85,6 +111,12 @@ spec:
|
|
|
85
111
|
env:
|
|
86
112
|
- name: TB_INFRA_TOKEN
|
|
87
113
|
value: "%(infra_token)s"
|
|
114
|
+
- name: TB_INFRA_WORKSPACE
|
|
115
|
+
value: "%(infra_workspace)s"
|
|
116
|
+
- name: TB_INFRA_ORGANIZATION
|
|
117
|
+
value: "%(infra_organization)s"
|
|
118
|
+
- name: TB_INFRA_USER
|
|
119
|
+
value: "%(infra_user)s"
|
|
88
120
|
volumeMounts:
|
|
89
121
|
- name: clickhouse-data
|
|
90
122
|
mountPath: /var/lib/clickhouse
|
|
@@ -115,12 +147,12 @@ provider "aws" {
|
|
|
115
147
|
|
|
116
148
|
# Get the hosted zone data
|
|
117
149
|
data "aws_route53_zone" "selected" {
|
|
118
|
-
name = "%(
|
|
150
|
+
name = "%(dns_zone_name)s"
|
|
119
151
|
}
|
|
120
152
|
|
|
121
153
|
# Create ACM certificate
|
|
122
154
|
resource "aws_acm_certificate" "cert" {
|
|
123
|
-
domain_name = "
|
|
155
|
+
domain_name = "%(dns_record)s.${data.aws_route53_zone.selected.name}"
|
|
124
156
|
validation_method = "DNS"
|
|
125
157
|
subject_alternative_names = [data.aws_route53_zone.selected.name]
|
|
126
158
|
|
|
@@ -152,21 +184,23 @@ resource "aws_acm_certificate_validation" "cert" {
|
|
|
152
184
|
certificate_arn = aws_acm_certificate.cert.arn
|
|
153
185
|
validation_record_fqdns = [for record in aws_route53_record.cert_validation : record.fqdn]
|
|
154
186
|
}
|
|
155
|
-
|
|
156
|
-
output "certificate_arn" {
|
|
157
|
-
description = "The ARN of the ACM certificate"
|
|
158
|
-
value = aws_acm_certificate.cert.arn
|
|
159
|
-
}
|
|
160
187
|
"""
|
|
161
188
|
|
|
162
189
|
TERRAFORM_SECOND_TEMPLATE = """
|
|
163
190
|
# Create Route 53 record for the load balancer
|
|
191
|
+
data "aws_alb" "tinybird" {
|
|
192
|
+
name = "%(namespace)s"
|
|
193
|
+
}
|
|
194
|
+
|
|
164
195
|
resource "aws_route53_record" "tinybird" {
|
|
165
196
|
zone_id = data.aws_route53_zone.selected.zone_id
|
|
166
197
|
name = "%(full_dns_name)s"
|
|
167
|
-
type = "
|
|
168
|
-
|
|
169
|
-
|
|
198
|
+
type = "A"
|
|
199
|
+
alias {
|
|
200
|
+
name = data.aws_alb.tinybird.dns_name
|
|
201
|
+
zone_id = data.aws_alb.tinybird.zone_id
|
|
202
|
+
evaluate_target_health = true
|
|
203
|
+
}
|
|
170
204
|
}
|
|
171
205
|
|
|
172
206
|
output "tinybird_dns" {
|
|
@@ -184,46 +218,42 @@ def infra(ctx: Context) -> None:
|
|
|
184
218
|
|
|
185
219
|
@infra.command(name="init")
|
|
186
220
|
@click.option("--name", type=str, help="Name for identifying the self-managed infrastructure in Tinybird")
|
|
187
|
-
@click.option("--provider",
|
|
188
|
-
@click.option("--region", type=str, help="AWS region
|
|
189
|
-
@click.option("--
|
|
190
|
-
@click.option("--namespace", type=str, help="Kubernetes namespace for deployment")
|
|
191
|
-
@click.option("--dns-record", type=str, help="DNS record name to create
|
|
192
|
-
@click.option(
|
|
193
|
-
"--auto-apply-terraform", is_flag=True, help="Automatically apply Terraform configuration without prompting"
|
|
194
|
-
)
|
|
195
|
-
@click.option("--auto-apply-dns", is_flag=True, help="Automatically apply DNS configuration without prompting")
|
|
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")
|
|
223
|
+
@click.option("--dns-zone-name", type=str, help="DNS zone name")
|
|
224
|
+
@click.option("--namespace", type=str, help="Kubernetes namespace for the deployment")
|
|
225
|
+
@click.option("--dns-record", type=str, help="DNS record name to create, without domain. For example, 'tinybird')")
|
|
226
|
+
@click.option("--storage-class", type=str, help="Storage class for the k8s StatefulSet")
|
|
196
227
|
@click.option(
|
|
197
|
-
"--auto-apply
|
|
228
|
+
"--auto-apply", is_flag=True, help="Automatically apply Terraform and kubectl configuration without prompting"
|
|
198
229
|
)
|
|
199
|
-
@click.option("--skip-
|
|
200
|
-
@click.option("--skip-kubectl", is_flag=True, help="Skip Kubernetes configuration and application")
|
|
201
|
-
@click.option("--skip-dns", is_flag=True, help="Skip DNS configuration and application")
|
|
230
|
+
@click.option("--skip-apply", is_flag=True, help="Skip Terraform and kubectl configuration and application")
|
|
202
231
|
@click.pass_context
|
|
203
232
|
def infra_init(
|
|
204
233
|
ctx: Context,
|
|
205
234
|
name: str,
|
|
206
235
|
provider: str,
|
|
207
236
|
region: Optional[str] = None,
|
|
208
|
-
|
|
237
|
+
dns_zone_name: Optional[str] = None,
|
|
209
238
|
namespace: Optional[str] = None,
|
|
210
239
|
dns_record: Optional[str] = None,
|
|
211
240
|
storage_class: Optional[str] = None,
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
auto_apply_kubectl: bool = False,
|
|
215
|
-
skip_terraform: bool = False,
|
|
216
|
-
skip_kubectl: bool = False,
|
|
217
|
-
skip_dns: bool = False,
|
|
241
|
+
auto_apply: bool = False,
|
|
242
|
+
skip_apply: bool = False,
|
|
218
243
|
) -> None:
|
|
219
244
|
"""Init infra"""
|
|
245
|
+
# Check if provider is specified
|
|
246
|
+
if not provider:
|
|
247
|
+
click.echo("Error: --provider option is required. Specify a provider. Possible values are: aws, gcp, azure.")
|
|
248
|
+
return
|
|
249
|
+
|
|
220
250
|
# AWS-specific Terraform template creation
|
|
221
251
|
if provider.lower() != "aws":
|
|
222
252
|
click.echo("Provider not supported yet.")
|
|
223
253
|
return
|
|
224
254
|
|
|
225
255
|
# Create infra directory if it doesn't exist
|
|
226
|
-
infra_dir = Path("infra")
|
|
256
|
+
infra_dir = Path(f"infra/{provider}")
|
|
227
257
|
infra_dir.mkdir(exist_ok=True)
|
|
228
258
|
yaml_path = infra_dir / "k8s.yaml"
|
|
229
259
|
tf_path = infra_dir / "main.tf"
|
|
@@ -237,34 +267,34 @@ def infra_init(
|
|
|
237
267
|
config = json.load(f)
|
|
238
268
|
click.echo("Loaded existing configuration from config.json")
|
|
239
269
|
except json.JSONDecodeError:
|
|
240
|
-
click.echo("Warning: Could not parse existing config.json
|
|
270
|
+
click.echo("Warning: Could not parse existing config.json. Creating a new file...")
|
|
241
271
|
|
|
242
272
|
# Generate a random ID for default values
|
|
243
273
|
random_id = str(uuid.uuid4())[:8]
|
|
244
274
|
|
|
245
275
|
# Get or prompt for configuration values
|
|
246
|
-
name = name or click.prompt("Enter name", type=str)
|
|
247
|
-
region = region or config.get("region") or click.prompt("Enter
|
|
248
|
-
|
|
276
|
+
name = name or click.prompt("Enter the name for your self-managed region", type=str)
|
|
277
|
+
region = region or config.get("region") or click.prompt("Enter the AWS region", default="us-east-1", type=str)
|
|
278
|
+
dns_zone_name = dns_zone_name or config.get("dns_zone_name") or click.prompt("Enter the DNS zone name", type=str)
|
|
249
279
|
namespace = (
|
|
250
280
|
namespace
|
|
251
281
|
or config.get("namespace")
|
|
252
|
-
or click.prompt("Enter namespace
|
|
282
|
+
or click.prompt("Enter the Kubernetes namespace", default=f"tinybird-{random_id}", type=str)
|
|
253
283
|
)
|
|
254
284
|
dns_record = (
|
|
255
285
|
dns_record
|
|
256
286
|
or config.get("dns_record")
|
|
257
|
-
or click.prompt("Enter DNS record name
|
|
287
|
+
or click.prompt("Enter the DNS record name, without domain", default=f"tinybird-{random_id}", type=str)
|
|
258
288
|
)
|
|
259
289
|
storage_class = config.get("storage_class") or click.prompt(
|
|
260
|
-
"Enter storage class", default="gp3-encrypted", type=str
|
|
290
|
+
"Enter the Kubernetes storage class", default="gp3-encrypted", type=str
|
|
261
291
|
)
|
|
262
292
|
|
|
263
293
|
# Save configuration
|
|
264
294
|
config = {
|
|
265
295
|
"provider": provider,
|
|
266
296
|
"region": region,
|
|
267
|
-
"
|
|
297
|
+
"dns_zone_name": dns_zone_name,
|
|
268
298
|
"namespace": namespace,
|
|
269
299
|
"dns_record": dns_record,
|
|
270
300
|
"storage_class": storage_class,
|
|
@@ -280,30 +310,48 @@ def infra_init(
|
|
|
280
310
|
user_client = cli_config.get_client(token=cli_config.get_user_token() or "")
|
|
281
311
|
user_workspaces = async_to_sync(user_client.user_workspaces_with_organization)()
|
|
282
312
|
admin_org_id = user_workspaces.get("organization_id")
|
|
313
|
+
admin_org_name = user_workspaces.get("organization_name", "")
|
|
283
314
|
infras = async_to_sync(client.infra_list)(organization_id=admin_org_id)
|
|
284
315
|
infra = next((infra for infra in infras if infra["name"] == name), None)
|
|
285
316
|
if not infra:
|
|
286
317
|
click.echo(FeedbackManager.highlight(message=f"\n» Creating infrastructure '{name}' in Tinybird..."))
|
|
287
|
-
host = f"https://{dns_record}.{
|
|
318
|
+
host = f"https://{dns_record}.{dns_zone_name}"
|
|
288
319
|
infra = async_to_sync(client.infra_create)(organization_id=admin_org_id, name=name, host=host)
|
|
289
320
|
|
|
290
321
|
infra_token = infra["token"]
|
|
291
322
|
|
|
292
323
|
# Write the Terraform template
|
|
293
|
-
terraform_content = TERRAFORM_FIRST_TEMPLATE % {
|
|
324
|
+
terraform_content = TERRAFORM_FIRST_TEMPLATE % {
|
|
325
|
+
"aws_region": region,
|
|
326
|
+
"dns_zone_name": dns_zone_name,
|
|
327
|
+
"dns_record": dns_record,
|
|
328
|
+
}
|
|
294
329
|
|
|
295
330
|
with open(tf_path, "w") as f:
|
|
296
331
|
f.write(terraform_content.lstrip())
|
|
297
332
|
|
|
298
|
-
click.echo(f"
|
|
333
|
+
click.echo(f"Created Terraform configuration in {tf_path}")
|
|
299
334
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
335
|
+
new_content = K8S_YML % {
|
|
336
|
+
"namespace": namespace,
|
|
337
|
+
"storage_class": storage_class,
|
|
338
|
+
"full_dns_name": f"{dns_record}.{dns_zone_name}",
|
|
339
|
+
"infra_token": infra_token,
|
|
340
|
+
"infra_workspace": cli_config.get("name", ""),
|
|
341
|
+
"infra_organization": admin_org_name,
|
|
342
|
+
"infra_user": cli_config.get_user_email() or "",
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
with open(yaml_path, "w") as f:
|
|
346
|
+
f.write(new_content.lstrip())
|
|
347
|
+
|
|
348
|
+
click.echo(f"Created Kubernetes configuration in {yaml_path}")
|
|
303
349
|
|
|
350
|
+
# Apply Terraform configuration if user confirms
|
|
351
|
+
if not skip_apply:
|
|
304
352
|
# Initialize Terraform
|
|
305
353
|
click.echo("Initializing Terraform...")
|
|
306
|
-
init_result = subprocess.run(["terraform", "-chdir=
|
|
354
|
+
init_result = subprocess.run(["terraform", f"-chdir={infra_dir}", "init"], capture_output=True, text=True)
|
|
307
355
|
|
|
308
356
|
if init_result.returncode != 0:
|
|
309
357
|
click.echo("Terraform initialization failed:")
|
|
@@ -312,7 +360,7 @@ def infra_init(
|
|
|
312
360
|
|
|
313
361
|
# Run terraform plan first
|
|
314
362
|
click.echo("\nRunning Terraform plan...\n")
|
|
315
|
-
plan_result = subprocess.run(["terraform", "-chdir=
|
|
363
|
+
plan_result = subprocess.run(["terraform", f"-chdir={infra_dir}", "plan"], capture_output=True, text=True)
|
|
316
364
|
|
|
317
365
|
if plan_result.returncode != 0:
|
|
318
366
|
click.echo("Terraform plan failed:")
|
|
@@ -322,10 +370,10 @@ def infra_init(
|
|
|
322
370
|
click.echo(plan_result.stdout)
|
|
323
371
|
|
|
324
372
|
# Apply Terraform configuration if user confirms
|
|
325
|
-
if
|
|
373
|
+
if auto_apply or click.confirm("Would you like to apply the Terraform configuration now?"):
|
|
326
374
|
click.echo("\nApplying Terraform configuration...\n")
|
|
327
375
|
apply_result = subprocess.run(
|
|
328
|
-
["terraform", "-chdir=
|
|
376
|
+
["terraform", f"-chdir={infra_dir}", "apply", "-auto-approve"], capture_output=True, text=True
|
|
329
377
|
)
|
|
330
378
|
|
|
331
379
|
if apply_result.returncode != 0:
|
|
@@ -335,309 +383,133 @@ def infra_init(
|
|
|
335
383
|
|
|
336
384
|
click.echo(apply_result.stdout)
|
|
337
385
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
386
|
+
# Prompt to apply the k8s configuration
|
|
387
|
+
if not skip_apply and (
|
|
388
|
+
auto_apply or click.confirm("Would you like to apply the Kubernetes configuration now?")
|
|
389
|
+
):
|
|
390
|
+
# Get current kubectl context
|
|
391
|
+
current_context_result = subprocess.run(
|
|
392
|
+
["kubectl", "config", "current-context"], capture_output=True, text=True
|
|
341
393
|
)
|
|
342
394
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
"infra_token": infra_token,
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
with open(yaml_path, "w") as f:
|
|
354
|
-
f.write(new_content.lstrip())
|
|
355
|
-
|
|
356
|
-
click.echo(f"Created Kubernetes configuration with certificate ARN in {yaml_path}")
|
|
357
|
-
|
|
358
|
-
# Prompt to apply the k8s configuration
|
|
359
|
-
if not skip_kubectl and (
|
|
360
|
-
auto_apply_kubectl or click.confirm("Would you like to apply the Kubernetes configuration now?")
|
|
361
|
-
):
|
|
362
|
-
import subprocess
|
|
363
|
-
|
|
364
|
-
# Get current kubectl context
|
|
365
|
-
current_context_result = subprocess.run(
|
|
366
|
-
["kubectl", "config", "current-context"], capture_output=True, text=True
|
|
367
|
-
)
|
|
368
|
-
|
|
369
|
-
current_context = (
|
|
370
|
-
current_context_result.stdout.strip() if current_context_result.returncode == 0 else "unknown"
|
|
371
|
-
)
|
|
372
|
-
|
|
373
|
-
# Get available contexts
|
|
374
|
-
contexts_result = subprocess.run(
|
|
375
|
-
["kubectl", "config", "get-contexts", "-o", "name"], capture_output=True, text=True
|
|
376
|
-
)
|
|
377
|
-
|
|
378
|
-
if contexts_result.returncode != 0:
|
|
379
|
-
click.echo("Failed to get kubectl contexts:")
|
|
380
|
-
click.echo(contexts_result.stderr)
|
|
381
|
-
return
|
|
382
|
-
|
|
383
|
-
available_contexts = [
|
|
384
|
-
context.strip() for context in contexts_result.stdout.splitlines() if context.strip()
|
|
385
|
-
]
|
|
386
|
-
|
|
387
|
-
if not available_contexts:
|
|
388
|
-
click.echo("No kubectl contexts found. Please configure kubectl first.")
|
|
389
|
-
return
|
|
390
|
-
|
|
391
|
-
# Prompt user to select a context
|
|
392
|
-
if len(available_contexts) == 1:
|
|
393
|
-
selected_context = available_contexts[0]
|
|
394
|
-
click.echo(f"Using the only available kubectl context: {selected_context}")
|
|
395
|
-
else:
|
|
396
|
-
click.echo("\nAvailable kubectl contexts:")
|
|
397
|
-
for i, context in enumerate(available_contexts):
|
|
398
|
-
marker = " (current)" if context == current_context else ""
|
|
399
|
-
click.echo(f" {i + 1}. {context}{marker}")
|
|
400
|
-
|
|
401
|
-
click.echo("")
|
|
402
|
-
default_index = (
|
|
403
|
-
available_contexts.index(current_context) + 1
|
|
404
|
-
if current_context in available_contexts
|
|
405
|
-
else 1
|
|
406
|
-
)
|
|
407
|
-
|
|
408
|
-
selected_index = click.prompt(
|
|
409
|
-
"Select kubectl context number to apply configuration",
|
|
410
|
-
type=click.IntRange(1, len(available_contexts)),
|
|
411
|
-
default=default_index,
|
|
412
|
-
)
|
|
413
|
-
|
|
414
|
-
selected_context = available_contexts[selected_index - 1]
|
|
415
|
-
click.echo(f"Selected context: {selected_context}")
|
|
416
|
-
|
|
417
|
-
# Apply the configuration to the selected context
|
|
418
|
-
click.echo(f"Applying Kubernetes configuration to context '{selected_context}'...")
|
|
419
|
-
apply_result = subprocess.run(
|
|
420
|
-
["kubectl", "--context", selected_context, "apply", "-f", str(yaml_path)],
|
|
421
|
-
capture_output=True,
|
|
422
|
-
text=True,
|
|
423
|
-
)
|
|
424
|
-
|
|
425
|
-
if apply_result.returncode != 0:
|
|
426
|
-
click.echo("Failed to apply Kubernetes configuration:")
|
|
427
|
-
click.echo(apply_result.stderr)
|
|
428
|
-
else:
|
|
429
|
-
click.echo("Kubernetes configuration applied successfully:")
|
|
430
|
-
click.echo(apply_result.stdout)
|
|
431
|
-
|
|
432
|
-
# Get the namespace from the applied configuration
|
|
433
|
-
namespace = None
|
|
434
|
-
with open(yaml_path, "r") as f:
|
|
435
|
-
for line in f:
|
|
436
|
-
if "namespace:" in line and not namespace:
|
|
437
|
-
namespace = line.split("namespace:")[1].strip()
|
|
438
|
-
break
|
|
439
|
-
|
|
440
|
-
if not namespace:
|
|
441
|
-
namespace = "tinybird" # Default namespace
|
|
442
|
-
|
|
443
|
-
click.echo(f"\nWaiting for load balancer to be provisioned in namespace '{namespace}'...")
|
|
444
|
-
|
|
445
|
-
# Wait for the load balancer to get an external IP
|
|
446
|
-
max_attempts = 30
|
|
447
|
-
attempt = 0
|
|
448
|
-
external_ip = None
|
|
449
|
-
|
|
450
|
-
while attempt < max_attempts and not external_ip:
|
|
451
|
-
attempt += 1
|
|
452
|
-
|
|
453
|
-
# Get the service details
|
|
454
|
-
get_service_result = subprocess.run(
|
|
455
|
-
[
|
|
456
|
-
"kubectl",
|
|
457
|
-
"--context",
|
|
458
|
-
selected_context,
|
|
459
|
-
"-n",
|
|
460
|
-
namespace,
|
|
461
|
-
"get",
|
|
462
|
-
"service",
|
|
463
|
-
"tinybird",
|
|
464
|
-
"-o",
|
|
465
|
-
"jsonpath='{.status.loadBalancer.ingress[0].hostname}'",
|
|
466
|
-
],
|
|
467
|
-
capture_output=True,
|
|
468
|
-
text=True,
|
|
469
|
-
)
|
|
470
|
-
|
|
471
|
-
if get_service_result.returncode == 0:
|
|
472
|
-
potential_ip = get_service_result.stdout.strip().replace("'", "")
|
|
473
|
-
if potential_ip and potential_ip != "":
|
|
474
|
-
external_ip = potential_ip
|
|
475
|
-
break
|
|
476
|
-
|
|
477
|
-
click.echo(
|
|
478
|
-
f"Attempt {attempt}/{max_attempts}: Load balancer not ready yet, waiting 10 seconds..."
|
|
479
|
-
)
|
|
480
|
-
import time
|
|
481
|
-
|
|
482
|
-
time.sleep(10)
|
|
483
|
-
|
|
484
|
-
if external_ip:
|
|
485
|
-
click.echo("\nLoad balancer provisioned successfully.")
|
|
486
|
-
|
|
487
|
-
# Update the Terraform configuration with the load balancer DNS
|
|
488
|
-
if not skip_dns and domain and tf_path.exists():
|
|
489
|
-
click.echo("\nUpdating Terraform configuration with load balancer DNS...")
|
|
490
|
-
|
|
491
|
-
with open(tf_path, "r") as f:
|
|
492
|
-
tf_content = f.read()
|
|
493
|
-
|
|
494
|
-
# Check if the Route 53 record already exists in the file
|
|
495
|
-
if 'resource "aws_route53_record" "tinybird"' not in tf_content:
|
|
496
|
-
# Create the full DNS name
|
|
497
|
-
full_dns_name = f"{dns_record}.{domain}"
|
|
498
|
-
|
|
499
|
-
# Use in the Terraform template
|
|
500
|
-
route53_record = TERRAFORM_SECOND_TEMPLATE % {
|
|
501
|
-
"external_ip": external_ip,
|
|
502
|
-
"full_dns_name": full_dns_name,
|
|
503
|
-
}
|
|
395
|
+
current_context = (
|
|
396
|
+
current_context_result.stdout.strip() if current_context_result.returncode == 0 else "unknown"
|
|
397
|
+
)
|
|
398
|
+
# Get available contexts
|
|
399
|
+
contexts_result = subprocess.run(
|
|
400
|
+
["kubectl", "config", "get-contexts", "-o", "name"], capture_output=True, text=True
|
|
401
|
+
)
|
|
504
402
|
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
403
|
+
if contexts_result.returncode != 0:
|
|
404
|
+
click.echo("Failed to get kubectl contexts:")
|
|
405
|
+
click.echo(contexts_result.stderr)
|
|
406
|
+
return
|
|
508
407
|
|
|
509
|
-
|
|
510
|
-
else:
|
|
511
|
-
# Update the existing Route 53 record
|
|
512
|
-
updated_tf = tf_content.replace(
|
|
513
|
-
'records = ["LOAD_BALANCER_DNS_PLACEHOLDER"]', f'records = ["{external_ip}"]'
|
|
514
|
-
)
|
|
408
|
+
available_contexts = [context.strip() for context in contexts_result.stdout.splitlines() if context.strip()]
|
|
515
409
|
|
|
516
|
-
|
|
517
|
-
|
|
410
|
+
if not available_contexts:
|
|
411
|
+
click.echo("No kubectl contexts found. Configure kubectl first.")
|
|
412
|
+
return
|
|
518
413
|
|
|
519
|
-
|
|
520
|
-
|
|
414
|
+
# Prompt user to select a context
|
|
415
|
+
if len(available_contexts) == 1:
|
|
416
|
+
selected_context = available_contexts[0]
|
|
417
|
+
click.echo(f"Using the only available kubectl context: {selected_context}")
|
|
418
|
+
else:
|
|
419
|
+
click.echo("\nAvailable kubectl contexts:")
|
|
420
|
+
for i, context in enumerate(available_contexts):
|
|
421
|
+
marker = " (current)" if context == current_context else ""
|
|
422
|
+
click.echo(f" {i + 1}. {context}{marker}")
|
|
423
|
+
|
|
424
|
+
click.echo("")
|
|
425
|
+
default_index = (
|
|
426
|
+
available_contexts.index(current_context) + 1 if current_context in available_contexts else 1
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
selected_index = click.prompt(
|
|
430
|
+
"Select kubectl context number to apply configuration",
|
|
431
|
+
type=click.IntRange(1, len(available_contexts)),
|
|
432
|
+
default=default_index,
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
selected_context = available_contexts[selected_index - 1]
|
|
436
|
+
click.echo(f"Selected context: {selected_context}")
|
|
437
|
+
|
|
438
|
+
# Apply the configuration to the selected context
|
|
439
|
+
click.echo(f"Applying Kubernetes configuration to context '{selected_context}'...")
|
|
440
|
+
apply_result = subprocess.run(
|
|
441
|
+
["kubectl", "--context", selected_context, "apply", "-f", str(yaml_path)],
|
|
442
|
+
capture_output=True,
|
|
443
|
+
text=True,
|
|
444
|
+
)
|
|
521
445
|
|
|
522
|
-
|
|
523
|
-
|
|
446
|
+
if apply_result.returncode != 0:
|
|
447
|
+
click.echo("Failed to apply Kubernetes configuration:")
|
|
448
|
+
click.echo(apply_result.stderr)
|
|
449
|
+
else:
|
|
450
|
+
click.echo("Kubernetes configuration applied successfully:")
|
|
451
|
+
click.echo(apply_result.stdout)
|
|
452
|
+
|
|
453
|
+
# Get the namespace from the applied configuration
|
|
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...")
|
|
465
|
+
|
|
466
|
+
max_attempts = 30 # 30 attempts * 10 seconds = 5 minutes
|
|
467
|
+
endpoint_url = f"https://{dns_record}.{dns_zone_name}"
|
|
468
|
+
|
|
469
|
+
with click.progressbar(
|
|
470
|
+
range(max_attempts),
|
|
471
|
+
label=f"Checking endpoint availability: {endpoint_url}",
|
|
472
|
+
length=max_attempts,
|
|
473
|
+
show_eta=True,
|
|
474
|
+
show_percent=True,
|
|
475
|
+
fill_char="█",
|
|
476
|
+
empty_char="░",
|
|
477
|
+
) as bar:
|
|
478
|
+
for attempt in bar:
|
|
479
|
+
try:
|
|
480
|
+
response = requests.get(endpoint_url, allow_redirects=False, timeout=5)
|
|
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
|
+
)
|
|
493
|
+
click.echo(
|
|
494
|
+
" This might be due to DNS propagation or the Load Balancer provisioning delays"
|
|
495
|
+
)
|
|
496
|
+
click.echo(f" Please try accessing {endpoint_url} manually in a few minutes")
|
|
497
|
+
else:
|
|
498
|
+
time.sleep(10)
|
|
524
499
|
|
|
525
|
-
|
|
500
|
+
if not skip_apply:
|
|
501
|
+
# Print a summary with the endpoint URL
|
|
502
|
+
click.echo("\n" + "=" * 60)
|
|
503
|
+
click.echo("DEPLOYMENT SUMMARY".center(60))
|
|
504
|
+
click.echo("=" * 60)
|
|
505
|
+
click.echo("✅ Load balancer provisioned")
|
|
526
506
|
|
|
527
|
-
|
|
528
|
-
click.echo("\nRunning Terraform plan for DNS changes...\n")
|
|
529
|
-
plan_result = subprocess.run(
|
|
530
|
-
["terraform", "-chdir=infra", "plan"], capture_output=True, text=True
|
|
531
|
-
)
|
|
507
|
+
click.echo(f"\n🔗 Tinybird is available at: https://{dns_record}.{dns_zone_name}")
|
|
532
508
|
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
click.echo(plan_result.stderr)
|
|
536
|
-
else:
|
|
537
|
-
click.echo(plan_result.stdout)
|
|
538
|
-
|
|
539
|
-
# Apply the updated Terraform configuration
|
|
540
|
-
if not skip_dns and (
|
|
541
|
-
auto_apply_dns
|
|
542
|
-
or click.confirm("Would you like to create the DNS record in Route 53 now?")
|
|
543
|
-
):
|
|
544
|
-
click.echo("Applying updated Terraform configuration...")
|
|
545
|
-
apply_result = subprocess.run(
|
|
546
|
-
["terraform", "-chdir=infra", "apply", "-auto-approve"],
|
|
547
|
-
capture_output=True,
|
|
548
|
-
text=True,
|
|
549
|
-
)
|
|
550
|
-
|
|
551
|
-
if apply_result.returncode != 0:
|
|
552
|
-
click.echo("Failed to create DNS record:")
|
|
553
|
-
click.echo(apply_result.stderr)
|
|
554
|
-
else:
|
|
555
|
-
click.echo(apply_result.stdout)
|
|
556
|
-
click.echo("DNS record created successfully!")
|
|
557
|
-
|
|
558
|
-
# Get the DNS name from Terraform output
|
|
559
|
-
dns_output = subprocess.run(
|
|
560
|
-
["terraform", "-chdir=infra", "output", "tinybird_dns"],
|
|
561
|
-
capture_output=True,
|
|
562
|
-
text=True,
|
|
563
|
-
)
|
|
564
|
-
|
|
565
|
-
if dns_output.returncode == 0:
|
|
566
|
-
dns_name = dns_output.stdout.strip().replace('"', "")
|
|
567
|
-
click.echo("\nDNS record created successfully!")
|
|
568
|
-
click.echo(
|
|
569
|
-
"\nWaiting up to 5 minutes for HTTPS endpoint to become available..."
|
|
570
|
-
)
|
|
571
|
-
|
|
572
|
-
import time
|
|
573
|
-
|
|
574
|
-
import requests
|
|
575
|
-
|
|
576
|
-
max_attempts = 30 # 30 attempts * 10 seconds = 5 minutes
|
|
577
|
-
attempt = 0
|
|
578
|
-
while attempt < max_attempts:
|
|
579
|
-
attempt += 1
|
|
580
|
-
try:
|
|
581
|
-
response = requests.get(
|
|
582
|
-
f"https://{dns_name}", allow_redirects=False, timeout=5
|
|
583
|
-
)
|
|
584
|
-
response.raise_for_status()
|
|
585
|
-
click.echo("\n✅ HTTPS endpoint is now accessible!")
|
|
586
|
-
break
|
|
587
|
-
except requests.RequestException:
|
|
588
|
-
if attempt == max_attempts:
|
|
589
|
-
click.echo(
|
|
590
|
-
"\n⚠️ HTTPS endpoint not accessible after 5 minutes"
|
|
591
|
-
)
|
|
592
|
-
click.echo(
|
|
593
|
-
" This might be due to DNS propagation or the Load Balancer provisioning delays"
|
|
594
|
-
)
|
|
595
|
-
click.echo(
|
|
596
|
-
" Please try accessing the URL manually in a few minutes"
|
|
597
|
-
)
|
|
598
|
-
else:
|
|
599
|
-
click.echo(
|
|
600
|
-
f"Attempt {attempt}/{max_attempts}: Not ready yet, waiting 10 seconds..."
|
|
601
|
-
)
|
|
602
|
-
time.sleep(10)
|
|
603
|
-
else:
|
|
604
|
-
click.echo(
|
|
605
|
-
f"\nYour Tinybird instance should be available at: https://tinybird.{domain}"
|
|
606
|
-
)
|
|
607
|
-
|
|
608
|
-
# Print a summary with the endpoint URL
|
|
609
|
-
click.echo("\n" + "=" * 60)
|
|
610
|
-
click.echo("DEPLOYMENT SUMMARY".center(60))
|
|
611
|
-
click.echo("=" * 60)
|
|
612
|
-
|
|
613
|
-
if not skip_kubectl and external_ip:
|
|
614
|
-
click.echo(f"✅ Load balancer provisioned: {external_ip}")
|
|
615
|
-
|
|
616
|
-
if not skip_dns and not skip_terraform and domain:
|
|
617
|
-
# Try to get the DNS name from Terraform output
|
|
618
|
-
dns_output = subprocess.run(
|
|
619
|
-
["terraform", "-chdir=infra", "output", "tinybird_dns"], capture_output=True, text=True
|
|
509
|
+
click.echo(
|
|
510
|
+
"\n📌 Note: It may take a few minutes for DNS to propagate and the HTTPS certificate to be fully provisioned."
|
|
620
511
|
)
|
|
621
512
|
|
|
622
|
-
if dns_output.returncode == 0:
|
|
623
|
-
dns_name = dns_output.stdout.strip().replace('"', "")
|
|
624
|
-
click.echo(f"✅ DNS record created: {dns_name}")
|
|
625
|
-
click.echo(f"\n🔗 Tinybird is available at: https://{dns_name}")
|
|
626
|
-
else:
|
|
627
|
-
# Fallback to constructed DNS name
|
|
628
|
-
full_dns_name = f"{dns_record}.{domain}"
|
|
629
|
-
click.echo(f"✅ DNS record created: {full_dns_name}")
|
|
630
|
-
click.echo(f"\n🔗 Tinybird is available at: https://{full_dns_name}")
|
|
631
|
-
elif not skip_kubectl and external_ip:
|
|
632
|
-
click.echo(f"\n🔗 Tinybird is available at: https://{external_ip}")
|
|
633
|
-
if domain:
|
|
634
|
-
click.echo(f"\n📝 Consider creating a DNS record: {dns_record}.{domain} → {external_ip}")
|
|
635
|
-
|
|
636
|
-
click.echo(
|
|
637
|
-
"\n📌 Note: It may take a few minutes for DNS to propagate and the HTTPS certificate to be fully provisioned."
|
|
638
|
-
)
|
|
639
|
-
click.echo("=" * 60)
|
|
640
|
-
|
|
641
513
|
|
|
642
514
|
@infra.command(name="rm")
|
|
643
515
|
@click.argument("name")
|
|
@@ -691,3 +563,62 @@ async def infra_ls(ctx: click.Context):
|
|
|
691
563
|
click.echo(FeedbackManager.info(message="\n** Infras:"))
|
|
692
564
|
echo_safe_humanfriendly_tables_format_smart_table(table_human_readable, column_names=columns)
|
|
693
565
|
click.echo("\n")
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
@infra.command(name="create")
|
|
569
|
+
@click.option("--name", type=str, help="Name for identifying the self-managed infrastructure in Tinybird")
|
|
570
|
+
@click.option("--host", type=str, help="Host for the infrastructure")
|
|
571
|
+
@click.pass_context
|
|
572
|
+
@coro
|
|
573
|
+
async def infra_create(ctx: click.Context, name: str, host: str):
|
|
574
|
+
"""Create a self-managed infrastructure"""
|
|
575
|
+
try:
|
|
576
|
+
client: TinyB = ctx.ensure_object(dict)["client"]
|
|
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)}"))
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
@infra.command(name="update")
|
|
596
|
+
@click.argument("infra_name")
|
|
597
|
+
@click.option("--name", type=str, help="Name for identifying the self-managed infrastructure in Tinybird")
|
|
598
|
+
@click.option("--host", type=str, help="Host for the infrastructure")
|
|
599
|
+
@click.pass_context
|
|
600
|
+
@coro
|
|
601
|
+
async def infra_update(ctx: click.Context, infra_name: str, name: str, host: str):
|
|
602
|
+
"""Update a self-managed infrastructure"""
|
|
603
|
+
try:
|
|
604
|
+
client: TinyB = ctx.ensure_object(dict)["client"]
|
|
605
|
+
user_workspaces = await client.user_workspaces_with_organization()
|
|
606
|
+
admin_org_id = user_workspaces.get("organization_id")
|
|
607
|
+
if not admin_org_id:
|
|
608
|
+
raise CLIException("No organization associated to this workspace")
|
|
609
|
+
|
|
610
|
+
if not name and not host:
|
|
611
|
+
click.echo(
|
|
612
|
+
FeedbackManager.warning(message="No name or host provided. Please provide either a name or a host.")
|
|
613
|
+
)
|
|
614
|
+
return
|
|
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)
|
|
619
|
+
if not infra_id:
|
|
620
|
+
raise CLIException(f"Infrastructure '{infra_name}' not found")
|
|
621
|
+
click.echo(FeedbackManager.highlight(message=f"\n» Updating infrastructure '{infra_name}' in Tinybird..."))
|
|
622
|
+
await client.infra_update(infra_id=infra_id, organization_id=admin_org_id, name=name, host=host)
|
|
623
|
+
except Exception as e:
|
|
624
|
+
click.echo(FeedbackManager.error(message=f"✗ Error: {str(e)}"))
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
tinybird/__cli__.py,sha256=esPl5QDTzuQgHe5FuxWLm-fURFigGGwjnYLh9GuWUw4,232
|
|
2
|
-
tinybird/client.py,sha256=
|
|
2
|
+
tinybird/client.py,sha256=yc2BP2HiQhaUjU-t7nBuZEiJsb2nQMH3vBfOhKCciyU,55468
|
|
3
3
|
tinybird/config.py,sha256=5UP_UZ2Qtlm5aOH5W7SbtN8r7X-8u3-r853joKqU5zs,6072
|
|
4
4
|
tinybird/connectors.py,sha256=7Gjms7b5MAaBFGi3xytsJurCylprONpFcYrzp4Fw2Rc,15241
|
|
5
5
|
tinybird/context.py,sha256=FfqYfrGX_I7PKGTQo93utaKPDNVYWelg4Hsp3evX5wM,1291
|
|
@@ -7,15 +7,15 @@ tinybird/datatypes.py,sha256=XNypumfqNjsvLJ5iNXnbVHRvAJe0aQwI3lS6Cxox-e0,10979
|
|
|
7
7
|
tinybird/feedback_manager.py,sha256=YSjtFDJvc8y66j2J0iIkb3SVzDdYAJbzFL-JPQ26pak,68761
|
|
8
8
|
tinybird/git_settings.py,sha256=Sw_8rGmribEFJ4Z_6idrVytxpFYk7ez8ei0qHULzs3E,3934
|
|
9
9
|
tinybird/prompts.py,sha256=syxUvbdkzxIrIK8pkS-Y7nzBx7DMS2Z9HaiXx7AjvnQ,33912
|
|
10
|
-
tinybird/sql.py,sha256=
|
|
10
|
+
tinybird/sql.py,sha256=J35bhdpuu84HW2tiLp-cs_nzkRwPhiy1yPcFhcWMCR4,46248
|
|
11
11
|
tinybird/sql_template.py,sha256=VH8io4n5eP2z6TEw111d8hcA9FKQerFjprKKCW2MODw,99127
|
|
12
12
|
tinybird/sql_template_fmt.py,sha256=KUHdj5rYCYm_rKKdXYSJAE9vIyXUQLB0YSZnUXHeBlY,10196
|
|
13
|
-
tinybird/sql_toolset.py,sha256=
|
|
13
|
+
tinybird/sql_toolset.py,sha256=KORVbNAUTfW1qo3U9oe7Z59xQ0QMsFhB0ji3HzY2JVo,15324
|
|
14
14
|
tinybird/syncasync.py,sha256=IPnOx6lMbf9SNddN1eBtssg8vCLHMt76SuZ6YNYm-Yk,27761
|
|
15
15
|
tinybird/tornado_template.py,sha256=jjNVDMnkYFWXflmT8KU_Ssbo5vR8KQq3EJMk5vYgXRw,41959
|
|
16
16
|
tinybird/ch_utils/constants.py,sha256=aYvg2C_WxYWsnqPdZB1ZFoIr8ZY-XjUXYyHKE9Ansj0,3890
|
|
17
17
|
tinybird/ch_utils/engine.py,sha256=BZuPM7MFS7vaEKK5tOMR2bwSAgJudPrJt27uVEwZmTY,40512
|
|
18
|
-
tinybird/tb/__cli__.py,sha256=
|
|
18
|
+
tinybird/tb/__cli__.py,sha256=9fN3V2hgGADGGvafwXcIrRciQG80C2cmQs_OlsP8u-A,252
|
|
19
19
|
tinybird/tb/cli.py,sha256=H_HaZhkimKgkryYXpBjHfY9Qtg-ZORiONU3psDNpzDk,1135
|
|
20
20
|
tinybird/tb/modules/auth.py,sha256=L1IatO2arRSzys3t8px8xVt8uPWUL5EVD0sFzAV_uVU,9022
|
|
21
21
|
tinybird/tb/modules/build.py,sha256=h5drdmDFX8NHts9dA2Zepao7KSgMAl3DZGyFufVZP78,11085
|
|
@@ -32,7 +32,7 @@ tinybird/tb/modules/endpoint.py,sha256=EhVoGAXsFz-83Fiwj1gI-I73iRRvL49d0W81un7hv
|
|
|
32
32
|
tinybird/tb/modules/exceptions.py,sha256=4A2sSjCEqKUMqpP3WI00zouCWW4uLaghXXLZBSw04mY,3363
|
|
33
33
|
tinybird/tb/modules/feedback_manager.py,sha256=7nNiOx7OMebiheLED1r0d75SbuXCNxyBmF4e20rCBNc,69511
|
|
34
34
|
tinybird/tb/modules/fmt.py,sha256=qpf9APqKTKL2uphNgdbj4OMVyLkAxZn6dn4eHF99L5g,3553
|
|
35
|
-
tinybird/tb/modules/infra.py,sha256=
|
|
35
|
+
tinybird/tb/modules/infra.py,sha256=vb6WmVHFuU1ZhtQMh2wOzRd2x8g7CLt_2gR3EL_L2E4,22867
|
|
36
36
|
tinybird/tb/modules/job.py,sha256=956Pj8BEEsiD2GZsV9RKKVM3I_CveOLgS82lykO5ukk,2963
|
|
37
37
|
tinybird/tb/modules/llm.py,sha256=AC0VSphTOM2t-v1_3NLvNN_FIbgMo4dTyMqIv5nniPo,835
|
|
38
38
|
tinybird/tb/modules/llm_utils.py,sha256=nS9r4FAElJw8yXtmdYrx-rtI2zXR8qXfi1QqUDCfxvg,3469
|
|
@@ -61,7 +61,7 @@ tinybird/tb/modules/datafile/build.py,sha256=jhfIJ2xt0N13XsLPe3iMQIyCPApHS13_Df2
|
|
|
61
61
|
tinybird/tb/modules/datafile/build_common.py,sha256=rT7VJ5mnQ68R_8US91DAtkusfvjWuG_NObOzNgtN_ko,4562
|
|
62
62
|
tinybird/tb/modules/datafile/build_datasource.py,sha256=CCU3eQ8Rax9RgHHfbAXDRL6rQ49N35h_GDQnGrUUUzA,17379
|
|
63
63
|
tinybird/tb/modules/datafile/build_pipe.py,sha256=w-Wd08gZYAEcak9FdBijVfIU2_Wn_PPdgAZddPpoGTo,11382
|
|
64
|
-
tinybird/tb/modules/datafile/common.py,sha256=
|
|
64
|
+
tinybird/tb/modules/datafile/common.py,sha256=ZRRx-9EXeHMOz6Lu9Y1TSWELF0ftfUHLLQAANjJZ334,82241
|
|
65
65
|
tinybird/tb/modules/datafile/diff.py,sha256=-0J7PsBO64T7LOZSkZ4ZFHHCPvT7cKItnJkbz2PkndU,6754
|
|
66
66
|
tinybird/tb/modules/datafile/exceptions.py,sha256=8rw2umdZjtby85QbuRKFO5ETz_eRHwUY5l7eHsy1wnI,556
|
|
67
67
|
tinybird/tb/modules/datafile/fixture.py,sha256=si-9LB-LdKQSWDtVW82xDrHtFfko5bgBG1cvjqqrcPU,1064
|
|
@@ -81,8 +81,8 @@ tinybird/tb_cli_modules/config.py,sha256=IsgdtFRnUrkY8-Zo32lmk6O7u3bHie1QCxLwgp4
|
|
|
81
81
|
tinybird/tb_cli_modules/exceptions.py,sha256=pmucP4kTF4irIt7dXiG-FcnI-o3mvDusPmch1L8RCWk,3367
|
|
82
82
|
tinybird/tb_cli_modules/regions.py,sha256=QjsL5H6Kg-qr0aYVLrvb1STeJ5Sx_sjvbOYO0LrEGMk,166
|
|
83
83
|
tinybird/tb_cli_modules/telemetry.py,sha256=Hh2Io8ZPROSunbOLuMvuIFU4TqwWPmQTqal4WS09K1A,10449
|
|
84
|
-
tinybird-0.0.1.
|
|
85
|
-
tinybird-0.0.1.
|
|
86
|
-
tinybird-0.0.1.
|
|
87
|
-
tinybird-0.0.1.
|
|
88
|
-
tinybird-0.0.1.
|
|
84
|
+
tinybird-0.0.1.dev101.dist-info/METADATA,sha256=73sj_cIfixFVZVrtmkn_MwEYvtkMLO4GYeJCjJQ1KA0,2586
|
|
85
|
+
tinybird-0.0.1.dev101.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
|
86
|
+
tinybird-0.0.1.dev101.dist-info/entry_points.txt,sha256=LwdHU6TfKx4Qs7BqqtaczEZbImgU7Abe9Lp920zb_fo,43
|
|
87
|
+
tinybird-0.0.1.dev101.dist-info/top_level.txt,sha256=VqqqEmkAy7UNaD8-V51FCoMMWXjLUlR0IstvK7tJYVY,54
|
|
88
|
+
tinybird-0.0.1.dev101.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|