Skip to content

Commit 00c8a02

Browse files
authored
Merge pull request #714 from splitgraph/add-sgr-cloud-tunnel-CU-38f3r2e
Add sgr cloud tunnel command to start tunnel
2 parents 4bd6226 + f8739b8 commit 00c8a02

File tree

4 files changed

+233
-0
lines changed

4 files changed

+233
-0
lines changed

splitgraph/cloud/__init__.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@
6161
INGESTION_JOB_STATUS,
6262
JOB_LOGS,
6363
PROFILE_UPSERT,
64+
PROVISION_EPHEMERAL_TUNNEL,
65+
PROVISION_REPOSITORY_TUNNEL,
6466
REPO_CONDITIONS,
6567
REPO_PARAMS,
6668
START_EXPORT,
@@ -1104,3 +1106,37 @@ def load_all_repositories(self, limit_to: List[str] = None) -> List[Repository]:
11041106
parsed_external = ExternalResponse.from_response(external_r.json())
11051107

11061108
return make_repositories(parsed_metadata, parsed_external)
1109+
1110+
def provision_repository_tunnel(self, namespace: str, repository: str) -> Tuple[str, str, int]:
1111+
response = self._gql(
1112+
{
1113+
"query": PROVISION_REPOSITORY_TUNNEL,
1114+
"operationName": "ProvisionRepositoryTunnel",
1115+
"variables": {"namespace": namespace, "repository": repository},
1116+
},
1117+
handle_errors=True,
1118+
anonymous_ok=False,
1119+
)
1120+
return (
1121+
response.json()["data"]["provisionRepositoryTunnel"]["secretToken"],
1122+
response.json()["data"]["provisionRepositoryTunnel"]["tunnelConnectHost"],
1123+
response.json()["data"]["provisionRepositoryTunnel"]["tunnelConnectPort"],
1124+
)
1125+
1126+
def provision_ephemeral_tunnel(self) -> Tuple[str, str, int, str, int]:
1127+
response = self._gql(
1128+
{
1129+
"query": PROVISION_EPHEMERAL_TUNNEL,
1130+
"operationName": "ProvisionEphemeralTunnel",
1131+
"variables": {},
1132+
},
1133+
handle_errors=True,
1134+
anonymous_ok=False,
1135+
)
1136+
return (
1137+
response.json()["data"]["provisionEphemeralTunnel"]["secretToken"],
1138+
response.json()["data"]["provisionEphemeralTunnel"]["tunnelConnectHost"],
1139+
response.json()["data"]["provisionEphemeralTunnel"]["tunnelConnectPort"],
1140+
response.json()["data"]["provisionEphemeralTunnel"]["privateAddressHost"],
1141+
response.json()["data"]["provisionEphemeralTunnel"]["privateAddressPort"],
1142+
)

splitgraph/cloud/queries.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,26 @@
280280
}
281281
"""
282282

283+
PROVISION_REPOSITORY_TUNNEL = """mutation ProvisionRepositoryTunnel($namespace: String!, $repository: String!) {
284+
provisionRepositoryTunnel(namespace:$namespace, repository:$repository) {
285+
secretToken,
286+
tunnelConnectHost,
287+
tunnelConnectPort
288+
}
289+
}
290+
"""
291+
292+
PROVISION_EPHEMERAL_TUNNEL = """mutation ProvisionEphemeralTunnel {
293+
provisionEphemeralTunnel {
294+
secretToken,
295+
tunnelConnectHost,
296+
tunnelConnectPort,
297+
privateAddressHost,
298+
privateAddressPort
299+
}
300+
}
301+
"""
302+
283303
EXPORT_JOB_STATUS = """query ExportJobStatus($taskId: UUID!) {
284304
exportJobStatus(taskId: $taskId) {
285305
taskId

splitgraph/cloud/tunnel_client.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import os
2+
import subprocess
3+
from os import path
4+
from typing import Optional
5+
6+
from click import echo
7+
8+
RATHOLE_CLIENT_CONFIG_FILENAME = "rathole-client.toml"
9+
10+
RATHOLE_CLIENT_CONFIG_TEMPLATE = """
11+
[client]
12+
remote_addr = "{tunnel_connect_address}"
13+
14+
[client.transport]
15+
type = "tls"
16+
17+
[client.transport.tls]
18+
{trusted_root_line}
19+
hostname = "{tls_hostname}"
20+
21+
[client.services."{section_id}"]
22+
local_addr = "{local_address}"
23+
token = "{secret_token}"
24+
25+
"""
26+
27+
28+
def get_rathole_client_config(
29+
tunnel_connect_address: str,
30+
tls_hostname: str,
31+
local_address: str,
32+
secret_token: str,
33+
section_id: str,
34+
trusted_root: Optional[str],
35+
) -> str:
36+
trusted_root_line = f'trusted_root = "{trusted_root}"' if trusted_root else ""
37+
return RATHOLE_CLIENT_CONFIG_TEMPLATE.format(
38+
tunnel_connect_address=tunnel_connect_address,
39+
tls_hostname=tls_hostname,
40+
local_address=local_address,
41+
secret_token=secret_token,
42+
section_id=section_id,
43+
trusted_root_line=trusted_root_line,
44+
)
45+
46+
47+
def get_config_dir():
48+
from splitgraph.config import CONFIG
49+
from splitgraph.config.config import get_singleton
50+
51+
return os.path.dirname(get_singleton(CONFIG, "SG_CONFIG_FILE"))
52+
53+
54+
def get_rathole_client_binary_path():
55+
return os.path.join(get_config_dir(), "rathole")
56+
57+
58+
def write_rathole_client_config(
59+
section_id: str,
60+
secret_token: str,
61+
tunnel_connect_host: str,
62+
tunnel_connect_port: int,
63+
local_address: str,
64+
tls_hostname: Optional[str],
65+
) -> str:
66+
# If a specific root CA file is used (eg: for self-signed hosts), reference
67+
# it in the rathole client config.
68+
trusted_root = os.environ.get("REQUESTS_CA_BUNDLE") or os.environ.get("SSL_CERT_FILE")
69+
rathole_client_config = get_rathole_client_config(
70+
tunnel_connect_address=f"{tunnel_connect_host}:{tunnel_connect_port}",
71+
tls_hostname=tls_hostname or tunnel_connect_host,
72+
local_address=local_address,
73+
secret_token=secret_token,
74+
section_id=section_id,
75+
trusted_root=trusted_root,
76+
)
77+
config_filename = path.join(get_config_dir(), RATHOLE_CLIENT_CONFIG_FILENAME)
78+
with open(config_filename, "w") as f:
79+
f.write(rathole_client_config)
80+
return config_filename
81+
82+
83+
def launch_rathole_client(rathole_client_config_path: str) -> None:
84+
echo("launching rathole client")
85+
command = [get_rathole_client_binary_path(), "--client", rathole_client_config_path]
86+
subprocess.check_call(command)

splitgraph/commandline/cloud.py

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@
1717

1818
from splitgraph.cloud.models import AddExternalRepositoryRequest, IntrospectionMode
1919
from splitgraph.cloud.project.models import Metadata, SplitgraphYAML
20+
from splitgraph.cloud.tunnel_client import (
21+
launch_rathole_client,
22+
write_rathole_client_config,
23+
)
2024
from splitgraph.commandline.common import (
2125
ImageType,
2226
RepositoryType,
@@ -1213,6 +1217,92 @@ def seed_c(remote, seed, github_repository, directory):
12131217
click.echo(f"Splitgraph project generated in {os.path.abspath(directory)}.")
12141218

12151219

1220+
def start_repository_tunnel(
1221+
remote: str, repository: "CoreRepository", external: "External"
1222+
) -> None:
1223+
if not external.tunnel:
1224+
raise click.UsageError(
1225+
f"Repository {repository.namespace}/{repository.repository} is not tunneled"
1226+
)
1227+
1228+
# TODO: Get current version of rathole client for architecture.
1229+
# user must manually download rathole for now
1230+
1231+
from splitgraph.cloud import GQLAPIClient
1232+
1233+
client = GQLAPIClient(remote)
1234+
(secret_token, tunnel_connect_host, tunnel_connect_port) = client.provision_repository_tunnel(
1235+
repository.namespace, repository.repository
1236+
)
1237+
1238+
section_id = f"{repository.namespace}/{repository.repository}"
1239+
local_address = f"{external.params['host']}:{external.params['port']}"
1240+
1241+
rathole_client_config_path = write_rathole_client_config(
1242+
section_id,
1243+
secret_token,
1244+
tunnel_connect_host,
1245+
tunnel_connect_port,
1246+
local_address,
1247+
tunnel_connect_host,
1248+
)
1249+
1250+
launch_rathole_client(rathole_client_config_path)
1251+
1252+
1253+
def start_ephemeral_tunnel(remote: str, local_address: str) -> None:
1254+
from splitgraph.cloud import GQLAPIClient
1255+
1256+
client = GQLAPIClient(remote)
1257+
(
1258+
secret_token,
1259+
tunnel_connect_host,
1260+
tunnel_connect_port,
1261+
private_address_host,
1262+
private_address_port,
1263+
) = client.provision_ephemeral_tunnel()
1264+
1265+
rathole_client_config_path = write_rathole_client_config(
1266+
private_address_host,
1267+
secret_token,
1268+
tunnel_connect_host,
1269+
tunnel_connect_port,
1270+
local_address,
1271+
tunnel_connect_host,
1272+
)
1273+
click.echo(
1274+
f"To connect to {local_address} from Splitgraph, use the following connection parameters:\nHost: {private_address_host}\nPort: {private_address_port}"
1275+
)
1276+
launch_rathole_client(rathole_client_config_path)
1277+
1278+
1279+
@click.command("tunnel")
1280+
@click.option("--remote", default="data.splitgraph.com", help="Name of the remote registry to use.")
1281+
@click.option(
1282+
"--repositories-file", "-f", default=["splitgraph.yml"], type=click.Path(), multiple=True
1283+
)
1284+
@click.argument("repository_or_local_address", type=str)
1285+
def tunnel_c(remote: str, repositories_file: List[Path], repository_or_local_address: str):
1286+
"""
1287+
Start the tunnel client to make tunneled external repo available.
1288+
1289+
This will load a splitgraph.yml file and tunnel the host:port address of the
1290+
external repository specified in the argument.
1291+
"""
1292+
1293+
if "/" in repository_or_local_address:
1294+
repository: "CoreRepository" = RepositoryType(exists=False).convert(
1295+
repository_or_local_address, None, None
1296+
)
1297+
external = _get_external_from_yaml(repositories_file, repository)[0]
1298+
start_repository_tunnel(remote, repository, external)
1299+
1300+
elif ":" in repository_or_local_address:
1301+
start_ephemeral_tunnel(remote, repository_or_local_address)
1302+
else:
1303+
raise click.UsageError("Argument should be of the form namespace/repository or host:port")
1304+
1305+
12161306
@click.group("cloud")
12171307
def cloud_c():
12181308
"""Run actions on Splitgraph Cloud."""
@@ -1240,3 +1330,4 @@ def cloud_c():
12401330
cloud_c.add_command(stub_c)
12411331
cloud_c.add_command(validate_c)
12421332
cloud_c.add_command(seed_c)
1333+
cloud_c.add_command(tunnel_c)

0 commit comments

Comments
 (0)