Skip to content

Commit 28dc0d9

Browse files
committed
init
Signed-off-by: Ayush Kamat <ayush@latch.bio>
1 parent bff1a93 commit 28dc0d9

5 files changed

Lines changed: 477 additions & 202 deletions

File tree

src/latch_cli/services/docker/__init__.py

Whitespace-only changes.
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
import base64
2+
import os
3+
from dataclasses import asdict, dataclass
4+
from json import JSONDecodeError
5+
from pathlib import Path
6+
from typing import TYPE_CHECKING
7+
from urllib.parse import urljoin
8+
9+
import boto3.session
10+
import click
11+
import docker
12+
import docker.auth
13+
import docker.errors
14+
import paramiko
15+
from docker.transport import SSHHTTPAdapter
16+
17+
from latch.utils import current_workspace
18+
from latch_cli import tinyrequests
19+
from latch_sdk_config.latch import NUCLEUS_URL, config
20+
21+
from ...utils import TemporarySSHCredentials, get_auth_header
22+
from ..register.register import print_and_write_build_logs, print_upload_logs
23+
24+
if TYPE_CHECKING:
25+
from collections.abc import Iterable
26+
27+
from ..register.utils import DockerBuildLogItem
28+
29+
30+
@dataclass
31+
class DockerCredentials:
32+
username: str
33+
password: str
34+
35+
36+
def get_credentials(image: str) -> DockerCredentials:
37+
response = tinyrequests.post(
38+
urljoin(NUCLEUS_URL, "/sdk/initiate-image-upload"),
39+
headers={"Authorization": get_auth_header()},
40+
json={"pkg_name": image, "ws_account_id": current_workspace()},
41+
)
42+
43+
try:
44+
data = response.json()
45+
46+
# todo(ayush): compute the authorization token in the endpoint and send it directly
47+
access_key = data["tmp_access_key"]
48+
secret_key = data["tmp_secret_key"]
49+
session_token = data["tmp_session_token"]
50+
except (JSONDecodeError, KeyError) as err:
51+
raise ValueError(
52+
f"malformed response on image upload: {response.content}"
53+
) from err
54+
55+
ecr = boto3.session.Session(
56+
aws_access_key_id=access_key,
57+
aws_secret_access_key=secret_key,
58+
aws_session_token=session_token,
59+
region_name="us-west-2",
60+
).client("ecr")
61+
62+
token = ecr.get_authorization_token()["authorizationData"][0]["authorizationToken"]
63+
username, password = base64.b64decode(token).decode("utf-8").split(":")
64+
65+
return DockerCredentials(username=username, password=password)
66+
67+
68+
def get_local_docker_client() -> docker.APIClient:
69+
try:
70+
host = os.environ.get("DOCKER_HOST")
71+
72+
if host is None or host == "":
73+
return docker.APIClient(base_url="unix://var/run/docker.sock")
74+
75+
cert_path = os.environ.get("DOCKER_CERT_PATH")
76+
if cert_path == "":
77+
cert_path = None
78+
79+
tls_verify = os.environ.get("DOCKER_TLS_VERIFY") != ""
80+
enable_tls = tls_verify or cert_path is not None
81+
82+
if not enable_tls:
83+
return docker.APIClient(host)
84+
85+
if cert_path is None:
86+
cert_path = Path.home() / ".docker"
87+
else:
88+
cert_path = Path(cert_path)
89+
90+
return docker.APIClient(
91+
host,
92+
tls=docker.tls.TLSConfig(
93+
client_cert=(str(cert_path / "cert.pem"), str(cert_path / "key.pem")),
94+
ca_cert=str(cert_path / "ca.pem"),
95+
verify=tls_verify,
96+
),
97+
)
98+
except docker.errors.DockerException as de:
99+
click.secho(
100+
"Docker is not running. Make sure that Docker is running before attempting to register a workflow.",
101+
fg="red",
102+
)
103+
raise click.exceptions.Exit(1) from de
104+
105+
106+
def dbnp(
107+
client: docker.APIClient,
108+
pkg_root: Path,
109+
image: str,
110+
version: str,
111+
dockerfile: Path,
112+
*,
113+
progress_plain: bool,
114+
):
115+
credentials = get_credentials(image)
116+
client._auth_configs = docker.auth.AuthConfig({ # noqa: SLF001
117+
"auths": {config.dkr_repo: asdict(credentials)}
118+
})
119+
120+
build_logs: Iterable[DockerBuildLogItem] = client.build(
121+
path=str(pkg_root),
122+
tag=f"{config.dkr_repo}/{image}:{version}",
123+
dockerfile=str(dockerfile),
124+
buildargs={"tag": f"{config.dkr_repo}/{image}:{version}"},
125+
decode=True,
126+
)
127+
128+
print_and_write_build_logs(
129+
build_logs, image, pkg_root, progress_plain=progress_plain
130+
)
131+
132+
upload_logs = client.push(
133+
repository=f"{config.dkr_repo}/{image}",
134+
tag=version,
135+
stream=True,
136+
decode=True,
137+
auth_config=asdict(credentials),
138+
)
139+
140+
print_upload_logs(upload_logs, image)
141+
142+
143+
def remote_dbnp(
144+
pkg_root: Path, image: str, version: str, dockerfile: Path, *, progress_plain: bool
145+
):
146+
key_path = pkg_root / ".latch" / "ssh_key"
147+
148+
with TemporarySSHCredentials(key_path) as keys:
149+
response = tinyrequests.post(
150+
urljoin(NUCLEUS_URL, "/sdk/provision-centromere"),
151+
headers={"Authorization": get_auth_header()},
152+
json={"public_key": keys.public_key},
153+
)
154+
155+
resp = response.json()
156+
try:
157+
hostname = resp["ip"]
158+
username = resp["username"]
159+
except KeyError as e:
160+
raise ValueError(
161+
f"Malformed response from request to provision centromere {resp}"
162+
) from e
163+
164+
ssh = paramiko.SSHClient()
165+
ssh.load_system_host_keys()
166+
ssh.set_missing_host_key_policy(paramiko.MissingHostKeyPolicy)
167+
168+
pkey = paramiko.PKey.from_path(key_path)
169+
ssh.connect(hostname, username=username, pkey=pkey)
170+
171+
transport = ssh.get_transport()
172+
assert transport is not None
173+
174+
transport.set_keepalive(30)
175+
176+
def _patched_connect(self: SSHHTTPAdapter): ...
177+
178+
def _patched_create_paramiko_client(self: SSHHTTPAdapter, base_url: str):
179+
self.ssh_client = ssh
180+
181+
SSHHTTPAdapter._create_paramiko_client = _patched_create_paramiko_client
182+
SSHHTTPAdapter._connect = _patched_connect
183+
184+
# todo(ayush): drop pydocker and connect to the socket directly
185+
client = docker.APIClient("ssh://fake", version="1.41")
186+
187+
dbnp(
188+
client, pkg_root, image, version, dockerfile, progress_plain=progress_plain
189+
)

0 commit comments

Comments
 (0)