Skip to content

Commit fbb950e

Browse files
committed
Add docker module for managing singlestoredb-dev-image
1 parent 298a773 commit fbb950e

File tree

1 file changed

+210
-0
lines changed

1 file changed

+210
-0
lines changed

singlestoredb/docker.py

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
#!/usr/bin/env python
2+
"""Utilities for running singlestoredb-dev-image."""
3+
import atexit
4+
import os
5+
import platform
6+
import secrets
7+
import urllib.parse
8+
from types import TracebackType
9+
from typing import Any
10+
from typing import Dict
11+
from typing import List
12+
from typing import Optional
13+
from typing import Type
14+
15+
import docker
16+
17+
from . import connect
18+
from .connection import Connection
19+
20+
try:
21+
import pymongo
22+
has_pymongo = True
23+
except ImportError:
24+
has_pymongo = False
25+
26+
27+
class SingleStoreDB:
28+
29+
name: Optional[str]
30+
hostname: Optional[str]
31+
root_password: str
32+
license: str
33+
kai_enabled: bool
34+
server_port: int
35+
studio_port: int
36+
data_api_port: int
37+
kai_port: int
38+
data_dir: Optional[str]
39+
logs_dir: Optional[str]
40+
server_dir: Optional[str]
41+
global_vars: Dict[str, Any]
42+
43+
def __init__(
44+
self,
45+
name: Optional[str] = None,
46+
root_password: Optional[str] = None,
47+
license: Optional[str] = None,
48+
enable_kai: bool = False,
49+
server_port: int = 3306,
50+
studio_port: int = 8080,
51+
data_api_port: int = 9000,
52+
kai_port: int = 27017,
53+
hostname: Optional[str] = None,
54+
data_dir: Optional[str] = None,
55+
logs_dir: Optional[str] = None,
56+
server_dir: Optional[str] = None,
57+
global_vars: Optional[Dict[str, Any]] = None,
58+
init_sql: Optional[str] = None,
59+
image: str = 'ghcr.io/singlestore-labs/singlestoredb-dev:latest',
60+
):
61+
self.kai_enabled = enable_kai
62+
self.server_port = server_port
63+
self.studio_port = studio_port
64+
self.data_api_port = data_api_port
65+
self.kai_port = kai_port
66+
self.data_dir = data_dir
67+
self.logs_dir = logs_dir
68+
self.server_dir = server_dir
69+
self.hostname = hostname
70+
71+
# Setup container ports
72+
ports = {
73+
'3306/tcp': server_port,
74+
'8080/tcp': studio_port,
75+
'9000/tcp': data_api_port,
76+
}
77+
78+
if enable_kai:
79+
ports['27017/tcp'] = kai_port
80+
81+
# Setup root password
82+
self.root_password = root_password or secrets.token_urlsafe(10)
83+
84+
# Setup license value
85+
if license is None:
86+
try:
87+
self.license = os.environ['SINGLESTORE_LICENSE']
88+
except KeyError:
89+
raise ValueError('a SingleStore license must be supplied')
90+
else:
91+
self.license = license
92+
93+
env = {
94+
'ROOT_PASSWORD': self.root_password,
95+
'SINGLESTORE_LICENSE': self.license,
96+
}
97+
98+
if enable_kai:
99+
env['ENABLE_KAI'] = '1'
100+
101+
# Construct Docker arguments
102+
kwargs = {
103+
'environment': env,
104+
'ports': ports,
105+
'detach': True,
106+
'auto_remove': True,
107+
'remove': True,
108+
}
109+
110+
if 'macOS' in platform.platform():
111+
kwargs['platform'] = 'linux/amd64'
112+
113+
for pname, pvalue in [('name', name), ('hostname', hostname)]:
114+
if pvalue is not None:
115+
kwargs[pname] = pvalue
116+
117+
# Setup volumes
118+
volumes: Dict[str, Dict[str, str]] = {}
119+
if data_dir:
120+
{data_dir: {'bind': '/data', 'mode': 'rw'}}
121+
if logs_dir:
122+
{logs_dir: {'bind': '/logs', 'mode': 'ro'}}
123+
if server_dir:
124+
{server_dir: {'bind': '/server', 'mode': 'ro'}}
125+
if init_sql:
126+
{init_sql: {'bind': '/init.sql', 'mode': 'ro'}}
127+
if volumes:
128+
kwargs['volumes'] = volumes
129+
130+
# Setup global vars
131+
self.global_vars = global_vars or {}
132+
for k, v in self.global_vars.items():
133+
env['SINGLESTORE_SET_GLOBAL_' + k.upper()] = str(v)
134+
135+
docker_client = docker.from_env()
136+
137+
self.container = docker_client.containers.run(
138+
image,
139+
environmen=env,
140+
ports=ports,
141+
detach=True,
142+
**kwargs,
143+
)
144+
145+
atexit.register(self.stop)
146+
147+
def logs(self) -> List[str]:
148+
return self.container.logs().encode('utf8').split('\n')
149+
150+
@property
151+
def connection_url(self) -> str:
152+
root_password = urllib.parse.quote_plus(self.root_password)
153+
return f'singlestoredb://root:{root_password}@' + \
154+
f'localhost:{self.server_port}'
155+
156+
@property
157+
def http_connection_url(self) -> str:
158+
root_password = urllib.parse.quote_plus(self.root_password)
159+
return f'singlestoredb+http://root:{root_password}@' + \
160+
f'localhost:{self.data_api_port}'
161+
162+
def connect(
163+
self,
164+
use_data_api: bool = False,
165+
**kwargs: Any,
166+
) -> Connection:
167+
if use_data_api:
168+
return connect(self.http_connection_url, **kwargs)
169+
return connect(self.connection_url, **kwargs)
170+
171+
@property
172+
def kai_url(self) -> Optional[str]:
173+
if not self.kai_enabled:
174+
return None
175+
root_password = urllib.parse.quote_plus(self.root_password)
176+
return f'mongodb://root:{root_password}@' + \
177+
f'localhost:{self.kai_port}/?authMechanism=PLAIN&loadBalanced=true'
178+
179+
def connect_kai(self) -> pymongo.MongoClient:
180+
if not self.kai_enabled:
181+
raise RuntimeError('kai is not enabled')
182+
if not has_pymongo:
183+
raise RuntimeError('pymongo is not installed')
184+
return pymongo.MongoClient(self.kai_url)
185+
186+
@property
187+
def studio_url(self) -> str:
188+
root_password = urllib.parse.quote_plus(self.root_password)
189+
return f'http://root:{root_password}@localhost:{self.studio_port}'
190+
191+
def connect_studio(self) -> None:
192+
import webbrowser
193+
webbrowser.open(self.studio_url)
194+
195+
def __enter__(self) -> None:
196+
pass
197+
198+
def __exit__(
199+
self,
200+
exc_type: Optional[Type[BaseException]],
201+
exc_val: Optional[BaseException],
202+
exc_tb: Optional[TracebackType],
203+
) -> Optional[bool]:
204+
self.stop()
205+
return None
206+
207+
def stop(self) -> None:
208+
if self.container is not None:
209+
self.container.stop()
210+
self.container = None

0 commit comments

Comments
 (0)