1use std::io::Read;
16use std::net::SocketAddr;
17use std::path::{Path, PathBuf};
18use std::process::Command;
19use std::time::Duration;
20
21use sha2::{Digest, Sha256};
22use tokio::io::AsyncWriteExt;
23use tokio::net::TcpSocket;
24use tokio::time;
25use tokio_stream::StreamExt;
26
27const PORT_CHECK_INTERVAL: Duration = Duration::from_millis(100);
29
30pub const PROGRAM: &str = "greptime";
31
32fn http_proxy() -> Option<String> {
33 for proxy in ["http_proxy", "HTTP_PROXY", "all_proxy", "ALL_PROXY"] {
34 if let Ok(proxy_addr) = std::env::var(proxy) {
35 println!("Getting Proxy from env var: {}={}", proxy, proxy_addr);
36 return Some(proxy_addr);
37 }
38 }
39 None
40}
41
42fn https_proxy() -> Option<String> {
43 for proxy in ["https_proxy", "HTTPS_PROXY", "all_proxy", "ALL_PROXY"] {
44 if let Ok(proxy_addr) = std::env::var(proxy) {
45 println!("Getting Proxy from env var: {}={}", proxy, proxy_addr);
46 return Some(proxy_addr);
47 }
48 }
49 None
50}
51
52async fn download_files(url: &str, path: &str) {
53 let proxy = if url.starts_with("http://") {
54 http_proxy().map(|proxy| reqwest::Proxy::http(proxy).unwrap())
55 } else if url.starts_with("https://") {
56 https_proxy().map(|proxy| reqwest::Proxy::https(proxy).unwrap())
57 } else {
58 None
59 };
60
61 let client = proxy
62 .map(|proxy| {
63 reqwest::Client::builder()
64 .proxy(proxy)
65 .build()
66 .expect("Failed to build client")
67 })
68 .unwrap_or(reqwest::Client::new());
69
70 let mut file = tokio::fs::File::create(path)
71 .await
72 .unwrap_or_else(|_| panic!("Failed to create file in {path}"));
73 println!("Downloading {}...", url);
74
75 let resp = client
76 .get(url)
77 .send()
78 .await
79 .expect("Failed to send download request")
80 .error_for_status()
81 .unwrap_or_else(|e| panic!("Failed to download {url}: {e}"));
82 let len = resp.content_length();
83 let mut stream = resp.bytes_stream();
84 let mut size_downloaded = 0;
85
86 while let Some(chunk_result) = stream.next().await {
87 let chunk = chunk_result.unwrap();
88 size_downloaded += chunk.len();
89 if let Some(len) = len {
90 print!("\rDownloading {}/{} bytes", size_downloaded, len);
91 } else {
92 print!("\rDownloaded {} bytes", size_downloaded);
93 }
94
95 file.write_all(&chunk).await.unwrap();
96 }
97
98 file.flush().await.unwrap();
99
100 println!("\nDownloaded {}", url);
101}
102
103fn decompress(archive: &str, dest: &str) {
104 let tar = std::fs::File::open(archive).unwrap();
105 let dec = flate2::read::GzDecoder::new(tar);
106 let mut a = tar::Archive::new(dec);
107 a.unpack(dest).unwrap();
108}
109
110pub async fn pull_binary(version: &str) {
116 let os = match std::env::consts::OS {
118 "macos" => "darwin",
119 os => os,
120 };
121 let arch = match std::env::consts::ARCH {
122 "x86_64" => "amd64",
123 "aarch64" => "arm64",
124 _ => panic!("Unsupported arch: {}", std::env::consts::ARCH),
125 };
126 let triple = format!("greptime-{}-{}-{}", os, arch, version);
127 let filename = format!("{triple}.tar.gz");
128
129 let url = format!(
130 "https://github.com/GreptimeTeam/greptimedb/releases/download/{version}/{filename}"
131 );
132 println!("Downloading {version} binary from {}", url);
133
134 let _ = std::fs::create_dir(version);
136
137 let archive = Path::new(version).join(filename);
138 let folder_path = Path::new(version);
139
140 download_files(&url, &archive.to_string_lossy()).await;
142
143 let checksum_file = format!("{triple}.sha256sum");
144 let checksum_url = format!(
145 "https://github.com/GreptimeTeam/greptimedb/releases/download/{version}/{checksum_file}"
146 );
147 download_files(
148 &checksum_url,
149 &PathBuf::from_iter([version, &checksum_file]).to_string_lossy(),
150 )
151 .await;
152
153 let mut file = std::fs::File::open(&archive).unwrap();
155 let mut sha256 = Sha256::new();
156 std::io::copy(&mut file, &mut sha256).unwrap();
157 let checksum: Vec<u8> = sha256.finalize().to_vec();
158
159 let mut expected_checksum =
160 std::fs::File::open(PathBuf::from_iter([version, &checksum_file])).unwrap();
161 let mut buf = String::new();
162 expected_checksum.read_to_string(&mut buf).unwrap();
163 let expected_checksum = hex::decode(buf.lines().next().unwrap()).unwrap();
164
165 assert_eq!(
166 checksum, expected_checksum,
167 "Checksum mismatched, downloaded file is corrupted"
168 );
169
170 decompress(&archive.to_string_lossy(), &folder_path.to_string_lossy());
171 println!("Downloaded and extracted {version} binary to {folder_path:?}");
172
173 std::fs::rename(
175 PathBuf::from_iter([version, &triple, "greptime"]),
176 PathBuf::from_iter([version, "greptime"]),
177 )
178 .unwrap();
179
180 std::fs::remove_file(&archive).unwrap();
182 std::fs::remove_dir(PathBuf::from_iter([version, &triple])).unwrap();
183}
184
185pub async fn maybe_pull_binary(version: &str, pull_version_on_need: bool) {
187 let exist = Path::new(version).join(PROGRAM).is_file();
188 match (exist, pull_version_on_need) {
189 (true, _) => println!("Binary {version} exists"),
190 (false, false) => panic!(
191 "Binary {version} does not exist, please run with --pull-version-on-need or manually download it"
192 ),
193 (false, true) => {
194 pull_binary(version).await;
195 }
196 }
197}
198
199pub fn setup_etcd(client_ports: Vec<u16>, peer_port: Option<u16>, etcd_version: Option<&str>) {
201 if std::process::Command::new("docker")
202 .args(["-v"])
203 .status()
204 .is_err()
205 {
206 panic!("Docker is not installed");
207 }
208 let peer_port = peer_port.unwrap_or(2380);
209 let exposed_port: Vec<_> = client_ports.iter().chain(Some(&peer_port)).collect();
210 let exposed_port_str = exposed_port
211 .iter()
212 .flat_map(|p| ["-p".to_string(), format!("{p}:{p}")])
213 .collect::<Vec<_>>();
214 let etcd_version = etcd_version.unwrap_or("v3.5.17");
215 let etcd_image = format!("quay.io/coreos/etcd:{etcd_version}");
216 let peer_url = format!("http://0.0.0.0:{peer_port}");
217 let my_local_ip = local_ip_address::local_ip().unwrap();
218
219 let my_local_ip_str = my_local_ip.to_string();
220
221 let mut arg_list = vec![];
222 arg_list.extend([
223 "run",
224 "-d",
225 "-v",
226 "/usr/share/ca-certificates/:/etc/ssl/certs",
227 ]);
228 arg_list.extend(exposed_port_str.iter().map(std::ops::Deref::deref));
229 arg_list.extend([
230 "--name",
231 "etcd",
232 &etcd_image,
233 "etcd",
234 "-name",
235 "etcd0",
236 "-advertise-client-urls",
237 ]);
238
239 let adv_client_urls = client_ports
240 .iter()
241 .map(|p| format!("http://{my_local_ip_str}:{p}"))
242 .collect::<Vec<_>>()
243 .join(",");
244
245 arg_list.push(&adv_client_urls);
246
247 arg_list.extend(["-listen-client-urls"]);
248
249 let client_ports_fmt = client_ports
250 .iter()
251 .map(|p| format!("http://0.0.0.0:{p}"))
252 .collect::<Vec<_>>()
253 .join(",");
254
255 arg_list.push(&client_ports_fmt);
256
257 arg_list.push("-initial-advertise-peer-urls");
258 let advertise_peer_url = format!("http://{my_local_ip_str}:{peer_port}");
259 arg_list.push(&advertise_peer_url);
260
261 arg_list.extend(["-listen-peer-urls", &peer_url]);
262
263 arg_list.extend(["-initial-cluster-token", "etcd-cluster-1"]);
264
265 arg_list.push("-initial-cluster");
266
267 let init_cluster_url = format!("etcd0=http://{my_local_ip_str}:{peer_port}");
268
269 arg_list.push(&init_cluster_url);
270
271 arg_list.extend(["-initial-cluster-state", "new"]);
272
273 let mut cmd = std::process::Command::new("docker");
274
275 cmd.args(arg_list);
276
277 println!("Starting etcd with command: {:?}", cmd);
278
279 let status = cmd.status();
280 if status.is_err() {
281 panic!("Failed to start etcd: {:?}", status);
282 } else if let Ok(status) = status {
283 if status.success() {
284 println!(
285 "Started etcd with client ports {:?} and peer port {}, statues:{status:?}",
286 client_ports, peer_port
287 );
288 } else {
289 panic!("Failed to start etcd: {:?}", status);
290 }
291 }
292}
293
294pub fn stop_rm_etcd_checked() -> Result<(), String> {
296 let status = std::process::Command::new("docker")
297 .args(["container", "rm", "--force", "etcd"])
298 .status()
299 .map_err(|error| format!("Failed to run Docker while removing etcd: {error}"))?;
300 if status.success() {
301 println!("Removed etcd container");
302 return Ok(());
303 }
304
305 let listed = std::process::Command::new("docker")
306 .args([
307 "container",
308 "ls",
309 "--all",
310 "--filter",
311 "name=^/etcd$",
312 "--format",
313 "{{.ID}}",
314 ])
315 .output()
316 .map_err(|error| {
317 format!(
318 "Docker failed to remove etcd ({status}) and could not verify its absence: {error}"
319 )
320 })?;
321 if !listed.status.success() {
322 return Err(format!(
323 "Docker failed to remove etcd ({status}) and listing containers failed: {}",
324 String::from_utf8_lossy(&listed.stderr).trim()
325 ));
326 }
327 if String::from_utf8_lossy(&listed.stdout).trim().is_empty() {
328 println!("Etcd container is already absent");
329 return Ok(());
330 }
331
332 Err(format!(
333 "Docker failed to remove etcd container (status {status}); the container still exists"
334 ))
335}
336
337pub fn stop_rm_etcd() {
342 stop_rm_etcd_checked().unwrap_or_else(|error| panic!("{error}"));
343}
344
345pub fn setup_pg(pg_port: u16, pg_version: Option<&str>) {
347 if std::process::Command::new("docker")
348 .args(["-v"])
349 .status()
350 .is_err()
351 {
352 panic!("Docker is not installed");
353 }
354
355 let pg_image = if let Some(pg_version) = pg_version {
356 format!("postgres:{pg_version}")
357 } else {
358 "postgres:latest".to_string()
359 };
360 let pg_password = "admin";
361 let pg_user = "greptimedb";
362
363 let mut arg_list = vec![];
364 arg_list.extend(["run", "-d"]);
365
366 let pg_password_env = format!("POSTGRES_PASSWORD={pg_password}");
367 let pg_user_env = format!("POSTGRES_USER={pg_user}");
368 let pg_port_forward = format!("{pg_port}:5432");
369 arg_list.extend(["-e", &pg_password_env, "-e", &pg_user_env]);
370 arg_list.extend(["-p", &pg_port_forward]);
371
372 arg_list.extend(["--name", "greptimedb_pg", &pg_image]);
373
374 let mut cmd = std::process::Command::new("docker");
375
376 cmd.args(arg_list);
377
378 println!("Starting PostgreSQL with command: {:?}", cmd);
379
380 let status = cmd.status();
381 if status.is_err() {
382 panic!("Failed to start PostgreSQL: {:?}", status);
383 } else if let Ok(status) = status {
384 if status.success() {
385 println!("Started PostgreSQL with port {}", pg_port);
386 } else {
387 panic!("Failed to start PostgreSQL: {:?}", status);
388 }
389 }
390}
391
392pub fn setup_mysql(mysql_port: u16, mysql_version: Option<&str>) {
394 if std::process::Command::new("docker")
395 .args(["-v"])
396 .status()
397 .is_err()
398 {
399 panic!("Docker is not installed");
400 }
401
402 let mysql_image = if let Some(mysql_version) = mysql_version {
403 format!("greptime/mysql:{mysql_version}")
404 } else {
405 "greptime/mysql:5.7".to_string()
406 };
407 let mysql_password = "admin";
408 let mysql_user = "greptimedb";
409
410 let mut arg_list = vec![];
411 arg_list.extend(["run", "-d"]);
412
413 let mysql_password_env = format!("MYSQL_PASSWORD={mysql_password}");
414 let mysql_user_env = format!("MYSQL_USER={mysql_user}");
415 let mysql_root_password_env = format!("MYSQL_ROOT_PASSWORD={mysql_password}");
416 let mysql_port_forward = format!("{mysql_port}:3306");
417 arg_list.extend([
418 "-e",
419 &mysql_password_env,
420 "-e",
421 &mysql_user_env,
422 "-e",
423 &mysql_root_password_env,
424 "-e",
425 "MYSQL_DATABASE=mysql",
426 ]);
427 arg_list.extend(["-p", &mysql_port_forward]);
428
429 arg_list.extend(["--name", "greptimedb_mysql", &mysql_image]);
430
431 let mut cmd = std::process::Command::new("docker");
432
433 cmd.args(arg_list);
434
435 println!("Starting MySQL with command: {:?}", cmd);
436
437 let status = cmd.status();
438 if status.is_err() {
439 panic!("Failed to start MySQL: {:?}", status);
440 } else if let Ok(status) = status {
441 if status.success() {
442 println!("Started MySQL with port {}", mysql_port);
443 } else {
444 panic!("Failed to start MySQL: {:?}", status);
445 }
446 }
447}
448
449pub fn get_case_dir(case_dir: Option<PathBuf>) -> String {
452 let runner_path = match case_dir {
453 Some(path) => path,
454 None => {
455 let mut runner_crate_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
457 let _ = runner_crate_path.pop();
459 runner_crate_path.push("cases");
460 runner_crate_path
461 }
462 };
463
464 runner_path.into_os_string().into_string().unwrap()
465}
466
467pub fn get_workspace_root() -> String {
469 let mut runner_crate_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
471
472 let _ = runner_crate_path.pop();
474 let _ = runner_crate_path.pop();
475
476 runner_crate_path.into_os_string().into_string().unwrap()
477}
478
479pub fn get_binary_dir(mode: &str) -> PathBuf {
480 let mut workspace_root = PathBuf::from(get_workspace_root());
482
483 workspace_root.push("target");
485 workspace_root.push(mode);
486
487 workspace_root
488}
489
490pub async fn check_port(ip_addr: SocketAddr, timeout: Duration) -> bool {
493 let check_task = async {
494 loop {
495 let socket = TcpSocket::new_v4().expect("Cannot create v4 socket");
496 match socket.connect(ip_addr).await {
497 Ok(mut stream) => {
498 let _ = stream.shutdown().await;
499 break;
500 }
501 Err(_) => time::sleep(PORT_CHECK_INTERVAL).await,
502 }
503 }
504 };
505
506 tokio::time::timeout(timeout, check_task).await.is_ok()
507}
508
509pub fn sqlness_conf_path() -> PathBuf {
511 let mut sqlness_root_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
512 sqlness_root_path.pop();
513 sqlness_root_path.push("conf");
514 sqlness_root_path
515}
516
517pub fn setup_wal() {
523 let mut sqlness_root_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
524 sqlness_root_path.pop();
525 sqlness_root_path.push("conf");
526
527 Command::new("docker")
528 .current_dir(sqlness_conf_path())
529 .args([
530 "compose",
531 "-f",
532 "kafka-cluster.yml",
533 "up",
534 "kafka",
535 "-d",
536 "--wait",
537 ])
538 .output()
539 .expect("Failed to start kafka cluster");
540
541 println!("kafka cluster is up");
542}
543
544pub fn teardown_wal() {
550 let mut sqlness_root_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
551 sqlness_root_path.pop();
552 sqlness_root_path.push("conf");
553
554 Command::new("docker")
555 .current_dir(sqlness_conf_path())
556 .args(["compose", "-f", "kafka-cluster.yml", "down", "kafka"])
557 .output()
558 .expect("Failed to stop kafka cluster");
559
560 println!("kafka cluster is down");
561}
562
563pub fn get_random_port() -> u16 {
565 use std::net::TcpListener;
566 let listener = TcpListener::bind("127.0.0.1:0").expect("Failed to bind to random port");
567 listener
568 .local_addr()
569 .expect("Failed to get local address")
570 .port()
571}
572
573pub async fn retry_with_backoff<T, E, Fut, F>(
575 mut fut: F,
576 max_retry: usize,
577 init_backoff: Duration,
578) -> Result<T, E>
579where
580 F: FnMut() -> Fut,
581 Fut: Future<Output = Result<T, E>>,
582{
583 let mut backoff = init_backoff;
584 for attempt in 0..max_retry {
585 match fut().await {
586 Ok(res) => return Ok(res),
587 Err(err) if attempt + 1 == max_retry => return Err(err),
588 Err(_) => {
589 tokio::time::sleep(backoff).await;
590 backoff *= 2;
591 }
592 }
593 }
594
595 unreachable!("loop should have returned before here")
596}