1use std::cell::RefCell;
4use std::collections::HashMap;
5use std::pin::Pin;
6use std::rc::Rc;
7
8use bollard::Docker;
9use bollard::models::{ContainerCreateBody, EndpointSettings, HostConfig, NetworkCreateRequest};
10use bollard::query_parameters::{
11 BuildImageOptions, CreateContainerOptions, InspectContainerOptions, KillContainerOptions,
12 RemoveContainerOptions, StartContainerOptions,
13};
14use bollard::secret::NetworkingConfig;
15use bytes::Bytes;
16use dfir_lang::graph::{AsCodeOptions, DfirGraph};
17use futures::{Sink, SinkExt, Stream, StreamExt};
18use http_body_util::Full;
19pub use hydro_deploy::LinuxCompileType;
21use hydro_deploy::RustCrate;
22use hydro_deploy::rust_crate::build::{BuildError, build_crate_memoized};
23use nanoid::nanoid;
24use proc_macro2::Span;
25use sinktools::lazy::LazySink;
26use stageleft::QuotedWithContext;
27use syn::parse_quote;
28use tar::{Builder, Header};
29use tokio::net::TcpStream;
30use tokio_util::codec::{FramedRead, FramedWrite, LengthDelimitedCodec};
31use tracing::{Instrument, instrument, trace, warn};
32
33use super::deploy_runtime_containerized::*;
34use crate::compile::builder::ExternalPortId;
35use crate::compile::deploy::DeployResult;
36use crate::compile::deploy_provider::{
37 ClusterSpec, Deploy, ExternalSpec, Node, ProcessSpec, RegisterPort,
38};
39use crate::compile::trybuild::generate::{LinkingMode, create_graph_trybuild};
40use crate::location::dynamic::LocationId;
41use crate::location::member_id::TaglessMemberId;
42use crate::location::{LocationKey, MembershipEvent, NetworkHint};
43
44#[derive(Clone, Debug)]
46pub struct DockerNetwork {
47 name: String,
48}
49
50impl DockerNetwork {
51 pub fn new(name: String) -> Self {
53 Self {
54 name: format!("{name}-{}", nanoid::nanoid!(6, &CONTAINER_ALPHABET)),
55 }
56 }
57}
58
59#[derive(Clone)]
61pub struct DockerDeployProcess {
62 key: LocationKey,
63 name: String,
64 next_port: Rc<RefCell<u16>>,
65 rust_crate: Rc<RefCell<Option<RustCrate>>>,
66
67 exposed_ports: Rc<RefCell<Vec<u16>>>,
68
69 docker_container_name: Rc<RefCell<Option<String>>>,
70
71 rustflags: Option<String>,
72
73 config: Vec<String>,
74
75 network: DockerNetwork,
76
77 base_image: Option<String>,
78
79 linux_compile_type: LinuxCompileType,
80
81 features: Vec<String>,
82}
83
84impl Node for DockerDeployProcess {
85 type Port = u16;
86 type Meta = ();
87 type InstantiateEnv = DockerDeploy;
88
89 #[instrument(level = "trace", skip_all, ret, fields(key = %self.key, name = self.name))]
90 fn next_port(&self) -> Self::Port {
91 let port = {
92 let mut borrow = self.next_port.borrow_mut();
93 let port = *borrow;
94 *borrow += 1;
95 port
96 };
97
98 port
99 }
100
101 #[instrument(level = "trace", skip_all, fields(key = %self.key, name = self.name))]
102 fn update_meta(&self, _meta: &Self::Meta) {}
103
104 #[instrument(level = "trace", skip_all, fields(key = %self.key, name = self.name, ?meta, extra_stmts = extra_stmts.len(), sidecars = sidecars.len()))]
105 fn instantiate(
106 &self,
107 _env: &mut Self::InstantiateEnv,
108 meta: &mut Self::Meta,
109 graph: DfirGraph,
110 extra_stmts: &[syn::Stmt],
111 sidecars: &[syn::Expr],
112 as_code_options: &AsCodeOptions,
113 ) {
114 let (bin_name, config) = create_graph_trybuild(
115 graph,
116 extra_stmts,
117 sidecars,
118 as_code_options,
119 Some(&self.name),
120 crate::compile::trybuild::generate::DeployMode::Containerized,
121 LinkingMode::Static,
122 );
123
124 let mut ret = RustCrate::new(&config.project_dir, &config.project_dir)
125 .target_dir(config.target_dir)
126 .example(bin_name)
127 .no_default_features();
128
129 ret = ret.display_name("test_display_name");
130
131 ret = ret.features(vec!["hydro___feature_docker_runtime".to_owned()]);
132
133 if let Some(features) = config.features {
134 ret = ret.features(features);
135 }
136
137 if !self.features.is_empty() {
138 ret = ret.features(self.features.clone());
139 }
140
141 ret = ret.build_env("STAGELEFT_TRYBUILD_BUILD_STAGED", "1");
142 ret = ret.config("build.incremental = false");
143
144 *self.rust_crate.borrow_mut() = Some(ret);
145 }
146}
147
148#[derive(Clone)]
150pub struct DockerDeployCluster {
151 key: LocationKey,
152 name: String,
153 next_port: Rc<RefCell<u16>>,
154 rust_crate: Rc<RefCell<Option<RustCrate>>>,
155
156 exposed_ports: Rc<RefCell<Vec<u16>>>,
157
158 docker_container_name: Rc<RefCell<Vec<String>>>,
159
160 rustflags: Option<String>,
161
162 config: Vec<String>,
163
164 count: usize,
165
166 base_image: Option<String>,
167
168 linux_compile_type: LinuxCompileType,
169
170 features: Vec<String>,
171}
172
173impl Node for DockerDeployCluster {
174 type Port = u16;
175 type Meta = ();
176 type InstantiateEnv = DockerDeploy;
177
178 #[instrument(level = "trace", skip_all, ret, fields(key = %self.key, name = self.name))]
179 fn next_port(&self) -> Self::Port {
180 let port = {
181 let mut borrow = self.next_port.borrow_mut();
182 let port = *borrow;
183 *borrow += 1;
184 port
185 };
186
187 port
188 }
189
190 #[instrument(level = "trace", skip_all, fields(key = %self.key, name = self.name))]
191 fn update_meta(&self, _meta: &Self::Meta) {}
192
193 #[instrument(level = "trace", skip_all, fields(key = %self.key, name = self.name, extra_stmts = extra_stmts.len()))]
194 fn instantiate(
195 &self,
196 _env: &mut Self::InstantiateEnv,
197 _meta: &mut Self::Meta,
198 graph: DfirGraph,
199 extra_stmts: &[syn::Stmt],
200 sidecars: &[syn::Expr],
201 as_code_options: &AsCodeOptions,
202 ) {
203 let (bin_name, config) = create_graph_trybuild(
204 graph,
205 extra_stmts,
206 sidecars,
207 as_code_options,
208 Some(&self.name),
209 crate::compile::trybuild::generate::DeployMode::Containerized,
210 LinkingMode::Static,
211 );
212
213 let mut ret = RustCrate::new(&config.project_dir, &config.project_dir)
214 .target_dir(config.target_dir)
215 .example(bin_name)
216 .no_default_features();
217
218 ret = ret.display_name("test_display_name");
219
220 ret = ret.features(vec!["hydro___feature_docker_runtime".to_owned()]);
221
222 if let Some(features) = config.features {
223 ret = ret.features(features);
224 }
225
226 if !self.features.is_empty() {
227 ret = ret.features(self.features.clone());
228 }
229
230 ret = ret.build_env("STAGELEFT_TRYBUILD_BUILD_STAGED", "1");
231 ret = ret.config("build.incremental = false");
232
233 *self.rust_crate.borrow_mut() = Some(ret);
234 }
235}
236
237#[derive(Clone, Debug)]
239#[expect(
240 dead_code,
241 reason = "fields used via Rc<RefCell> in RegisterPort impl and ExternalBytesPort construction"
242)]
243pub struct DockerDeployExternal {
244 pub(crate) key: LocationKey,
246 name: String,
247 next_port: Rc<RefCell<u16>>,
248
249 next_external_port_id: Rc<RefCell<crate::Counter<ExternalPortId>>>,
251
252 ports: Rc<RefCell<HashMap<ExternalPortId, u16>>>,
253
254 connection_info: Rc<RefCell<HashMap<u16, (Rc<RefCell<Option<String>>>, u16, DockerNetwork)>>>,
255}
256
257impl Node for DockerDeployExternal {
258 type Port = u16;
259 type Meta = ();
260 type InstantiateEnv = DockerDeploy;
261
262 #[instrument(level = "trace", skip_all, ret, fields(name = self.name))]
263 fn next_port(&self) -> Self::Port {
264 let port = {
265 let mut borrow = self.next_port.borrow_mut();
266 let port = *borrow;
267 *borrow += 1;
268 port
269 };
270
271 port
272 }
273
274 #[instrument(level = "trace", skip_all, fields(name = self.name))]
275 fn update_meta(&self, _meta: &Self::Meta) {}
276
277 #[instrument(level = "trace", skip_all, fields(name = self.name, ?meta, extra_stmts = extra_stmts.len(), sidecars = sidecars.len()))]
278 fn instantiate(
279 &self,
280 _env: &mut Self::InstantiateEnv,
281 meta: &mut Self::Meta,
282 graph: DfirGraph,
283 extra_stmts: &[syn::Stmt],
284 sidecars: &[syn::Expr],
285 _as_code_options: &AsCodeOptions,
286 ) {
287 trace!(name: "surface", surface = graph.surface_syntax_string());
288 }
289}
290
291impl DockerDeployProcess {
292 pub fn expose_port(&self, port: u16) {
298 self.exposed_ports.borrow_mut().push(port);
299 }
300
301 pub async fn get_tcp_endpoint(&self, container_port: u16) -> (String, u16) {
305 let name = self
306 .docker_container_name
307 .borrow()
308 .as_ref()
309 .expect("container not yet started")
310 .clone();
311 let host_port = find_dynamically_allocated_docker_port(&name, container_port).await;
312 ("localhost".to_owned(), host_port)
313 }
314}
315
316impl DockerDeployCluster {
317 pub fn expose_port(&self, port: u16) {
323 self.exposed_ports.borrow_mut().push(port);
324 }
325
326 pub async fn get_all_tcp_endpoints(&self, container_port: u16) -> Vec<(String, u16)> {
330 let names = self.docker_container_name.borrow().clone();
331 let mut endpoints = Vec::with_capacity(names.len());
332 for name in names {
333 let host_port = find_dynamically_allocated_docker_port(&name, container_port).await;
334 endpoints.push(("localhost".to_owned(), host_port));
335 }
336 endpoints
337 }
338}
339
340type DynSourceSink<Out, In, InErr> = (
341 Pin<Box<dyn Stream<Item = Out>>>,
342 Pin<Box<dyn Sink<In, Error = InErr>>>,
343);
344
345impl<'a> RegisterPort<'a, DockerDeploy> for DockerDeployExternal {
346 #[instrument(level = "trace", skip_all, fields(name = self.name, %external_port_id, %port))]
347 fn register(&self, external_port_id: ExternalPortId, port: Self::Port) {
348 self.ports.borrow_mut().insert(external_port_id, port);
349 }
350
351 fn as_bytes_bidi(
352 &self,
353 external_port_id: ExternalPortId,
354 ) -> impl Future<
355 Output = DynSourceSink<Result<bytes::BytesMut, std::io::Error>, Bytes, std::io::Error>,
356 > + 'a {
357 let guard =
358 tracing::trace_span!("as_bytes_bidi", name = %self.name, %external_port_id).entered();
359
360 let local_port = *self.ports.borrow().get(&external_port_id).unwrap();
361 let (docker_container_name, remote_port, _) = self
362 .connection_info
363 .borrow()
364 .get(&local_port)
365 .unwrap()
366 .clone();
367
368 let docker_container_name = docker_container_name.borrow().as_ref().unwrap().clone();
369
370 async move {
371 let local_port =
372 find_dynamically_allocated_docker_port(&docker_container_name, remote_port).await;
373 let remote_ip_address = "localhost";
374
375 trace!(name: "as_bytes_bidi_connecting", to = %remote_ip_address, to_port = %local_port);
376
377 let stream = TcpStream::connect(format!("{remote_ip_address}:{local_port}"))
378 .await
379 .unwrap();
380
381 trace!(name: "as_bytes_bidi_connected", to = %remote_ip_address, to_port = %local_port);
382
383 let (rx, tx) = stream.into_split();
384
385 let source = Box::pin(
386 FramedRead::new(rx, LengthDelimitedCodec::new()),
387 ) as Pin<Box<dyn Stream<Item = Result<bytes::BytesMut, std::io::Error>>>>;
388
389 let sink = Box::pin(FramedWrite::new(tx, LengthDelimitedCodec::new()))
390 as Pin<Box<dyn Sink<Bytes, Error = std::io::Error>>>;
391
392 (source, sink)
393 }
394 .instrument(guard.exit())
395 }
396
397 fn as_bincode_bidi<InT, OutT>(
398 &self,
399 external_port_id: ExternalPortId,
400 ) -> impl Future<Output = DynSourceSink<OutT, InT, std::io::Error>> + 'a
401 where
402 InT: serde::Serialize + 'static,
403 OutT: serde::de::DeserializeOwned + 'static,
404 {
405 let guard =
406 tracing::trace_span!("as_bincode_bidi", name = %self.name, %external_port_id).entered();
407
408 let local_port = *self.ports.borrow().get(&external_port_id).unwrap();
409 let (docker_container_name, remote_port, _) = self
410 .connection_info
411 .borrow()
412 .get(&local_port)
413 .unwrap()
414 .clone();
415
416 let docker_container_name = docker_container_name.borrow().as_ref().unwrap().clone();
417
418 async move {
419 let local_port =
420 find_dynamically_allocated_docker_port(&docker_container_name, remote_port).await;
421 let remote_ip_address = "localhost";
422
423 trace!(name: "as_bincode_bidi_connecting", to = %remote_ip_address, to_port = %local_port);
424
425 let stream = TcpStream::connect(format!("{remote_ip_address}:{local_port}"))
426 .await
427 .unwrap();
428
429 trace!(name: "as_bincode_bidi_connected", to = %remote_ip_address, to_port = %local_port);
430
431 let (rx, tx) = stream.into_split();
432
433 let source = Box::pin(
434 FramedRead::new(rx, LengthDelimitedCodec::new())
435 .map(|v| bincode::deserialize(&v.unwrap()).unwrap()),
436 ) as Pin<Box<dyn Stream<Item = OutT>>>;
437
438 let sink = Box::pin(
439 FramedWrite::new(tx, LengthDelimitedCodec::new()).with(move |v: InT| async move {
440 Ok::<_, std::io::Error>(Bytes::from(bincode::serialize(&v).unwrap()))
441 }),
442 ) as Pin<Box<dyn Sink<InT, Error = std::io::Error>>>;
443
444 (source, sink)
445 }
446 .instrument(guard.exit())
447 }
448
449 fn as_bincode_sink<T>(
450 &self,
451 external_port_id: ExternalPortId,
452 ) -> impl Future<Output = Pin<Box<dyn Sink<T, Error = std::io::Error>>>> + 'a
453 where
454 T: serde::Serialize + 'static,
455 {
456 let guard =
457 tracing::trace_span!("as_bincode_sink", name = %self.name, %external_port_id).entered();
458
459 let local_port = *self.ports.borrow().get(&external_port_id).unwrap();
460 let (docker_container_name, remote_port, _) = self
461 .connection_info
462 .borrow()
463 .get(&local_port)
464 .unwrap()
465 .clone();
466
467 let docker_container_name = docker_container_name.borrow().as_ref().unwrap().clone();
468
469 async move {
470 let local_port = find_dynamically_allocated_docker_port(&docker_container_name, remote_port).await;
471 let remote_ip_address = "localhost";
472
473 Box::pin(
474 LazySink::new(move || {
475 Box::pin(async move {
476 trace!(name: "as_bincode_sink_connecting", to = %remote_ip_address, to_port = %local_port);
477
478 let stream =
479 TcpStream::connect(format!("{remote_ip_address}:{local_port}"))
480 .await?;
481
482 trace!(name: "as_bincode_sink_connected", to = %remote_ip_address, to_port = %local_port);
483
484 Result::<_, std::io::Error>::Ok(FramedWrite::new(
485 stream,
486 LengthDelimitedCodec::new(),
487 ))
488 })
489 })
490 .with(move |v| async move {
491 Ok(Bytes::from(bincode::serialize(&v).unwrap()))
492 }),
493 ) as Pin<Box<dyn Sink<T, Error = std::io::Error>>>
494 }
495 .instrument(guard.exit())
496 }
497
498 fn as_bincode_source<T>(
499 &self,
500 external_port_id: ExternalPortId,
501 ) -> impl Future<Output = Pin<Box<dyn Stream<Item = T>>>> + 'a
502 where
503 T: serde::de::DeserializeOwned + 'static,
504 {
505 let guard =
506 tracing::trace_span!("as_bincode_sink", name = %self.name, %external_port_id).entered();
507
508 let local_port = *self.ports.borrow().get(&external_port_id).unwrap();
509 let (docker_container_name, remote_port, _) = self
510 .connection_info
511 .borrow()
512 .get(&local_port)
513 .unwrap()
514 .clone();
515
516 let docker_container_name = docker_container_name.borrow().as_ref().unwrap().clone();
517
518 async move {
519
520 let local_port = find_dynamically_allocated_docker_port(&docker_container_name, remote_port).await;
521 let remote_ip_address = "localhost";
522
523 trace!(name: "as_bincode_source_connecting", to = %remote_ip_address, to_port = %local_port);
524
525 let stream = TcpStream::connect(format!("{remote_ip_address}:{local_port}"))
526 .await
527 .unwrap();
528
529 trace!(name: "as_bincode_source_connected", to = %remote_ip_address, to_port = %local_port);
530
531 Box::pin(
532 FramedRead::new(stream, LengthDelimitedCodec::new())
533 .map(|v| bincode::deserialize(&v.unwrap()).unwrap()),
534 ) as Pin<Box<dyn Stream<Item = T>>>
535 }
536 .instrument(guard.exit())
537 }
538}
539
540#[instrument(level = "trace", skip_all, fields(%docker_container_name, %destination_port))]
541async fn find_dynamically_allocated_docker_port(
542 docker_container_name: &str,
543 destination_port: u16,
544) -> u16 {
545 let docker = Docker::connect_with_local_defaults().unwrap();
546
547 let container_info = docker
548 .inspect_container(docker_container_name, None::<InspectContainerOptions>)
549 .await
550 .unwrap();
551
552 trace!(name: "port struct", container_info = ?container_info.network_settings.as_ref().unwrap().ports.as_ref().unwrap());
553
554 let remote_port = container_info
556 .network_settings
557 .as_ref()
558 .unwrap()
559 .ports
560 .as_ref()
561 .unwrap()
562 .get(&format!("{destination_port}/tcp"))
563 .unwrap()
564 .as_ref()
565 .unwrap()
566 .iter()
567 .find(|v| v.host_ip == Some("0.0.0.0".to_owned()))
568 .unwrap()
569 .host_port
570 .as_ref()
571 .unwrap()
572 .parse()
573 .unwrap();
574
575 remote_port
576}
577
578pub struct DockerDeploy {
580 docker_processes: Vec<DockerDeployProcessSpec>,
581 docker_clusters: Vec<DockerDeployClusterSpec>,
582 network: DockerNetwork,
583 deployment_instance: String,
584}
585
586#[instrument(level = "trace", skip_all, fields(%image_name, %container_name, %network_name, %deployment_instance))]
587async fn create_and_start_container(
588 docker: &Docker,
589 container_name: &str,
590 image_name: &str,
591 network_name: &str,
592 deployment_instance: &str,
593) -> Result<(), anyhow::Error> {
594 let config = ContainerCreateBody {
595 image: Some(image_name.to_owned()),
596 hostname: Some(container_name.to_owned()),
597 host_config: Some(HostConfig {
598 binds: Some(vec!["/var/run/docker.sock:/var/run/docker.sock".to_owned()]),
599 publish_all_ports: Some(true),
600 port_bindings: Some(HashMap::new()), ..Default::default()
602 }),
603 env: Some(vec![
604 format!("CONTAINER_NAME={container_name}"),
605 format!("DEPLOYMENT_INSTANCE={deployment_instance}"),
606 "RUST_LOG=trace".to_owned(),
607 ]),
608 networking_config: Some(NetworkingConfig {
609 endpoints_config: Some(HashMap::from([(
610 network_name.to_owned(),
611 EndpointSettings {
612 ..Default::default()
613 },
614 )])),
615 }),
616 tty: Some(true),
617 ..Default::default()
618 };
619
620 let options = CreateContainerOptions {
621 name: Some(container_name.to_owned()),
622 ..Default::default()
623 };
624
625 tracing::error!("Config: {}", serde_json::to_string_pretty(&config).unwrap());
626 docker.create_container(Some(options), config).await?;
627 docker
628 .start_container(container_name, None::<StartContainerOptions>)
629 .await?;
630
631 Ok(())
632}
633
634#[instrument(level = "trace", skip_all, fields(%image_name))]
635async fn build_and_create_image(
636 rust_crate: &Rc<RefCell<Option<RustCrate>>>,
637 rustflags: Option<&str>,
638 config: &[String],
639 exposed_ports: &[u16],
640 image_name: &str,
641 base_image: Option<&str>,
642 linux_compile_type: LinuxCompileType,
643) -> Result<(), anyhow::Error> {
644 let mut rust_crate = rust_crate.borrow_mut().take().unwrap();
645
646 if let Some(rustflags) = rustflags {
647 rust_crate = rust_crate.rustflags(rustflags);
648 }
649
650 for cfg in config {
651 rust_crate = rust_crate.config(cfg);
652 }
653
654 let build_output = match build_crate_memoized(
655 rust_crate.get_build_params(hydro_deploy::HostTargetType::Linux(linux_compile_type)),
656 )
657 .await
658 {
659 Ok(build_output) => build_output,
660 Err(BuildError::FailedToBuildCrate {
661 exit_status,
662 diagnostics,
663 text_lines,
664 stderr_lines,
665 }) => {
666 let diagnostics = diagnostics
667 .into_iter()
668 .map(|d| d.rendered.unwrap())
669 .collect::<Vec<_>>()
670 .join("\n");
671 let text_lines = text_lines.join("\n");
672 let stderr_lines = stderr_lines.join("\n");
673
674 anyhow::bail!(
675 r#"
676Failed to build crate {exit_status:?}
677--- diagnostics
678---
679{diagnostics}
680---
681---
682---
683
684--- text_lines
685---
686---
687{text_lines}
688---
689---
690---
691
692--- stderr_lines
693---
694---
695{stderr_lines}
696---
697---
698---"#
699 );
700 }
701 Err(err) => {
702 anyhow::bail!("Failed to build crate {err:?}");
703 }
704 };
705
706 let docker = Docker::connect_with_local_defaults()?;
707
708 let mut tar_data = Vec::new();
709 {
710 let mut tar = Builder::new(&mut tar_data);
711
712 let exposed_ports = exposed_ports
713 .iter()
714 .map(|port| format!("EXPOSE {port}/tcp"))
715 .collect::<Vec<_>>()
716 .join("\n");
717
718 let from_image = base_image.unwrap_or("scratch");
719 let dockerfile_content = format!(
720 r#"
721 FROM {from_image}
722 {exposed_ports}
723 COPY app /app
724 CMD ["/app"]
725 "#,
726 );
727
728 trace!(name: "dockerfile", %dockerfile_content);
729
730 let mut header = Header::new_gnu();
731 header.set_path("Dockerfile")?;
732 header.set_size(dockerfile_content.len() as u64);
733 header.set_cksum();
734 tar.append(&header, dockerfile_content.as_bytes())?;
735
736 let mut header = Header::new_gnu();
737 header.set_path("app")?;
738 header.set_size(build_output.bin_data.len() as u64);
739 header.set_mode(0o755);
740 header.set_cksum();
741 tar.append(&header, &build_output.bin_data[..])?;
742
743 tar.finish()?;
744 }
745
746 let build_options = BuildImageOptions {
747 dockerfile: "Dockerfile".to_owned(),
748 t: Some(image_name.to_owned()),
749 rm: true,
750 ..Default::default()
751 };
752
753 use bollard::errors::Error;
754
755 let body = http_body_util::Either::Left(Full::new(Bytes::from(tar_data)));
756 let mut build_stream = docker.build_image(build_options, None, Some(body));
757 while let Some(msg) = build_stream.next().await {
758 match msg {
759 Ok(_) => {}
760 Err(e) => match e {
761 Error::DockerStreamError { error } => {
762 return Err(anyhow::anyhow!(
763 "Docker build failed: DockerStreamError: {{ error: {error} }}"
764 ));
765 }
766 _ => return Err(anyhow::anyhow!("Docker build failed: {}", e)),
767 },
768 }
769 }
770
771 Ok(())
772}
773
774impl DockerDeploy {
775 pub fn new(network: DockerNetwork) -> Self {
777 Self {
778 docker_processes: Vec::new(),
779 docker_clusters: Vec::new(),
780 network,
781 deployment_instance: nanoid!(6, &CONTAINER_ALPHABET),
782 }
783 }
784
785 pub fn add_localhost_docker(
787 &mut self,
788 rustflags: Option<String>,
789 config: Vec<String>,
790 ) -> DockerDeployProcessSpec {
791 let process = DockerDeployProcessSpec {
792 rustflags,
793 config,
794 network: self.network.clone(),
795 deployment_instance: self.deployment_instance.clone(),
796 base_image: None,
797 linux_compile_type: LinuxCompileType::Musl,
798 features: vec![],
799 };
800
801 self.docker_processes.push(process.clone());
802
803 process
804 }
805
806 pub fn add_localhost_docker_cluster(
808 &mut self,
809 rustflags: Option<String>,
810 config: Vec<String>,
811 count: usize,
812 ) -> DockerDeployClusterSpec {
813 let cluster = DockerDeployClusterSpec {
814 rustflags,
815 config,
816 count,
817 deployment_instance: self.deployment_instance.clone(),
818 base_image: None,
819 linux_compile_type: LinuxCompileType::Musl,
820 features: vec![],
821 };
822
823 self.docker_clusters.push(cluster.clone());
824
825 cluster
826 }
827
828 pub fn add_external(&self, name: String) -> DockerDeployExternalSpec {
830 DockerDeployExternalSpec { name }
831 }
832
833 pub fn get_deployment_instance(&self) -> String {
835 self.deployment_instance.clone()
836 }
837
838 #[instrument(level = "trace", skip_all)]
840 pub async fn provision(&self, nodes: &DeployResult<'_, Self>) -> Result<(), anyhow::Error> {
841 for (_, _, process) in nodes.get_all_processes() {
842 let exposed_ports = process.exposed_ports.borrow().clone();
843
844 build_and_create_image(
845 &process.rust_crate,
846 process.rustflags.as_deref(),
847 &process.config,
848 &exposed_ports,
849 &process.name,
850 process.base_image.as_deref(),
851 process.linux_compile_type,
852 )
853 .await?;
854 }
855
856 for (_, _, cluster) in nodes.get_all_clusters() {
857 let exposed_ports = cluster.exposed_ports.borrow().clone();
858 build_and_create_image(
859 &cluster.rust_crate,
860 cluster.rustflags.as_deref(),
861 &cluster.config,
862 &exposed_ports,
863 &cluster.name,
864 cluster.base_image.as_deref(),
865 cluster.linux_compile_type,
866 )
867 .await?;
868 }
869
870 Ok(())
871 }
872
873 #[instrument(level = "trace", skip_all)]
875 pub async fn start(&self, nodes: &DeployResult<'_, Self>) -> Result<(), anyhow::Error> {
876 let docker = Docker::connect_with_local_defaults()?;
877
878 match docker
879 .create_network(NetworkCreateRequest {
880 name: self.network.name.clone(),
881 driver: Some("bridge".to_owned()),
882 ..Default::default()
883 })
884 .await
885 {
886 Ok(v) => v.id,
887 Err(e) => {
888 panic!("Failed to create docker network: {e:?}");
889 }
890 };
891
892 for (_, _, process) in nodes.get_all_processes() {
893 let docker_container_name: String = get_docker_container_name(&process.name, None);
894 *process.docker_container_name.borrow_mut() = Some(docker_container_name.clone());
895
896 create_and_start_container(
897 &docker,
898 &docker_container_name,
899 &process.name,
900 &self.network.name,
901 &self.deployment_instance,
902 )
903 .await?;
904 }
905
906 for (_, _, cluster) in nodes.get_all_clusters() {
907 for num in 0..cluster.count {
908 let docker_container_name = get_docker_container_name(&cluster.name, Some(num));
909 cluster
910 .docker_container_name
911 .borrow_mut()
912 .push(docker_container_name.clone());
913
914 create_and_start_container(
915 &docker,
916 &docker_container_name,
917 &cluster.name,
918 &self.network.name,
919 &self.deployment_instance,
920 )
921 .await?;
922 }
923 }
924
925 Ok(())
926 }
927
928 #[instrument(level = "trace", skip_all)]
930 pub async fn stop(&mut self, nodes: &DeployResult<'_, Self>) -> Result<(), anyhow::Error> {
931 let docker = Docker::connect_with_local_defaults()?;
932
933 for (_, _, process) in nodes.get_all_processes() {
934 let docker_container_name: String = get_docker_container_name(&process.name, None);
935
936 docker
937 .kill_container(&docker_container_name, None::<KillContainerOptions>)
938 .await?;
939 }
940
941 for (_, _, cluster) in nodes.get_all_clusters() {
942 for num in 0..cluster.count {
943 let docker_container_name = get_docker_container_name(&cluster.name, Some(num));
944
945 docker
946 .kill_container(&docker_container_name, None::<KillContainerOptions>)
947 .await?;
948 }
949 }
950
951 Ok(())
952 }
953
954 #[instrument(level = "trace", skip_all)]
956 pub async fn cleanup(&mut self, nodes: &DeployResult<'_, Self>) -> Result<(), anyhow::Error> {
957 let docker = Docker::connect_with_local_defaults()?;
958
959 for (_, _, process) in nodes.get_all_processes() {
960 let docker_container_name: String = get_docker_container_name(&process.name, None);
961
962 docker
963 .remove_container(&docker_container_name, None::<RemoveContainerOptions>)
964 .await?;
965 }
966
967 for (_, _, cluster) in nodes.get_all_clusters() {
968 for num in 0..cluster.count {
969 let docker_container_name = get_docker_container_name(&cluster.name, Some(num));
970
971 docker
972 .remove_container(&docker_container_name, None::<RemoveContainerOptions>)
973 .await?;
974 }
975 }
976
977 docker
978 .remove_network(&self.network.name)
979 .await
980 .map_err(|e| anyhow::anyhow!("Failed to remove docker network: {e:?}"))?;
981
982 use bollard::query_parameters::RemoveImageOptions;
983
984 for (_, _, process) in nodes.get_all_processes() {
985 docker
986 .remove_image(&process.name, None::<RemoveImageOptions>, None)
987 .await?;
988 }
989
990 for (_, _, cluster) in nodes.get_all_clusters() {
991 docker
992 .remove_image(&cluster.name, None::<RemoveImageOptions>, None)
993 .await?;
994 }
995
996 Ok(())
997 }
998}
999
1000impl<'a> Deploy<'a> for DockerDeploy {
1001 type Meta = ();
1002 type InstantiateEnv = Self;
1003
1004 type Process = DockerDeployProcess;
1005 type Cluster = DockerDeployCluster;
1006 type External = DockerDeployExternal;
1007
1008 #[instrument(level = "trace", skip_all, fields(p1 = p1.name, %p1_port, p2 = p2.name, %p2_port))]
1009 fn o2o_sink_source(
1010 _env: &mut Self::InstantiateEnv,
1011 p1: &Self::Process,
1012 p1_port: &<Self::Process as Node>::Port,
1013 p2: &Self::Process,
1014 p2_port: &<Self::Process as Node>::Port,
1015 name: Option<&str>,
1016 networking_info: &crate::networking::NetworkingInfo,
1017 _external_types: Option<(&syn::Type, &syn::Type)>,
1018 ) -> (syn::Expr, syn::Expr) {
1019 match networking_info {
1020 crate::networking::NetworkingInfo::Tcp {
1021 fault: crate::networking::TcpFault::FailStop,
1022 } => {}
1023 _ => panic!("Unsupported networking info: {:?}", networking_info),
1024 }
1025
1026 deploy_containerized_o2o(
1027 &p2.name,
1028 name.expect("channel name is required for containerized deployment"),
1029 )
1030 }
1031
1032 #[instrument(level = "trace", skip_all, fields(p1 = p1.name, %p1_port, p2 = p2.name, %p2_port))]
1033 fn o2o_connect(
1034 p1: &Self::Process,
1035 p1_port: &<Self::Process as Node>::Port,
1036 p2: &Self::Process,
1037 p2_port: &<Self::Process as Node>::Port,
1038 ) -> Box<dyn FnOnce()> {
1039 let serialized = format!("o2o_connect {}:{p1_port} -> {}:{p2_port}", p1.name, p2.name);
1040
1041 Box::new(move || {
1042 trace!(name: "o2o_connect thunk", %serialized);
1043 })
1044 }
1045
1046 #[instrument(level = "trace", skip_all, fields(p1 = p1.name, %p1_port, c2 = c2.name, %c2_port))]
1047 fn o2m_sink_source(
1048 _env: &mut Self::InstantiateEnv,
1049 p1: &Self::Process,
1050 p1_port: &<Self::Process as Node>::Port,
1051 c2: &Self::Cluster,
1052 c2_port: &<Self::Cluster as Node>::Port,
1053 name: Option<&str>,
1054 networking_info: &crate::networking::NetworkingInfo,
1055 _external_types: Option<(&syn::Type, &syn::Type)>,
1056 ) -> (syn::Expr, syn::Expr) {
1057 match networking_info {
1058 crate::networking::NetworkingInfo::Tcp {
1059 fault: crate::networking::TcpFault::FailStop,
1060 } => {}
1061 _ => panic!("Unsupported networking info: {:?}", networking_info),
1062 }
1063
1064 deploy_containerized_o2m(
1065 name.expect("channel name is required for containerized deployment"),
1066 )
1067 }
1068
1069 #[instrument(level = "trace", skip_all, fields(p1 = p1.name, %p1_port, c2 = c2.name, %c2_port))]
1070 fn o2m_connect(
1071 p1: &Self::Process,
1072 p1_port: &<Self::Process as Node>::Port,
1073 c2: &Self::Cluster,
1074 c2_port: &<Self::Cluster as Node>::Port,
1075 ) -> Box<dyn FnOnce()> {
1076 let serialized = format!("o2m_connect {}:{p1_port} -> {}:{c2_port}", p1.name, c2.name);
1077
1078 Box::new(move || {
1079 trace!(name: "o2m_connect thunk", %serialized);
1080 })
1081 }
1082
1083 #[instrument(level = "trace", skip_all, fields(c1 = c1.name, %c1_port, p2 = p2.name, %p2_port))]
1084 fn m2o_sink_source(
1085 _env: &mut Self::InstantiateEnv,
1086 c1: &Self::Cluster,
1087 c1_port: &<Self::Cluster as Node>::Port,
1088 p2: &Self::Process,
1089 p2_port: &<Self::Process as Node>::Port,
1090 name: Option<&str>,
1091 networking_info: &crate::networking::NetworkingInfo,
1092 _external_types: Option<(&syn::Type, &syn::Type)>,
1093 ) -> (syn::Expr, syn::Expr) {
1094 match networking_info {
1095 crate::networking::NetworkingInfo::Tcp {
1096 fault: crate::networking::TcpFault::FailStop,
1097 } => {}
1098 _ => panic!("Unsupported networking info: {:?}", networking_info),
1099 }
1100
1101 deploy_containerized_m2o(
1102 &p2.name,
1103 name.expect("channel name is required for containerized deployment"),
1104 )
1105 }
1106
1107 #[instrument(level = "trace", skip_all, fields(c1 = c1.name, %c1_port, p2 = p2.name, %p2_port))]
1108 fn m2o_connect(
1109 c1: &Self::Cluster,
1110 c1_port: &<Self::Cluster as Node>::Port,
1111 p2: &Self::Process,
1112 p2_port: &<Self::Process as Node>::Port,
1113 ) -> Box<dyn FnOnce()> {
1114 let serialized = format!("o2m_connect {}:{c1_port} -> {}:{p2_port}", c1.name, p2.name);
1115
1116 Box::new(move || {
1117 trace!(name: "m2o_connect thunk", %serialized);
1118 })
1119 }
1120
1121 #[instrument(level = "trace", skip_all, fields(c1 = c1.name, %c1_port, c2 = c2.name, %c2_port))]
1122 fn m2m_sink_source(
1123 _env: &mut Self::InstantiateEnv,
1124 c1: &Self::Cluster,
1125 c1_port: &<Self::Cluster as Node>::Port,
1126 c2: &Self::Cluster,
1127 c2_port: &<Self::Cluster as Node>::Port,
1128 name: Option<&str>,
1129 networking_info: &crate::networking::NetworkingInfo,
1130 _external_types: Option<(&syn::Type, &syn::Type)>,
1131 ) -> (syn::Expr, syn::Expr) {
1132 match networking_info {
1133 crate::networking::NetworkingInfo::Tcp {
1134 fault: crate::networking::TcpFault::FailStop,
1135 } => {}
1136 _ => panic!("Unsupported networking info: {:?}", networking_info),
1137 }
1138
1139 deploy_containerized_m2m(
1140 name.expect("channel name is required for containerized deployment"),
1141 )
1142 }
1143
1144 #[instrument(level = "trace", skip_all, fields(c1 = c1.name, %c1_port, c2 = c2.name, %c2_port))]
1145 fn m2m_connect(
1146 c1: &Self::Cluster,
1147 c1_port: &<Self::Cluster as Node>::Port,
1148 c2: &Self::Cluster,
1149 c2_port: &<Self::Cluster as Node>::Port,
1150 ) -> Box<dyn FnOnce()> {
1151 let serialized = format!("m2m_connect {}:{c1_port} -> {}:{c2_port}", c1.name, c2.name);
1152
1153 Box::new(move || {
1154 trace!(name: "m2m_connect thunk", %serialized);
1155 })
1156 }
1157
1158 #[instrument(level = "trace", skip_all, fields(p2 = p2.name, %p2_port, %shared_handle, extra_stmts = extra_stmts.len()))]
1159 fn e2o_many_source(
1160 extra_stmts: &mut Vec<syn::Stmt>,
1161 p2: &Self::Process,
1162 p2_port: &<Self::Process as Node>::Port,
1163 codec_type: &syn::Type,
1164 shared_handle: String,
1165 ) -> syn::Expr {
1166 p2.exposed_ports.borrow_mut().push(*p2_port);
1167
1168 let socket_ident = syn::Ident::new(
1169 &format!("__hydro_deploy_many_{}_socket", shared_handle),
1170 Span::call_site(),
1171 );
1172
1173 let source_ident = syn::Ident::new(
1174 &format!("__hydro_deploy_many_{}_source", shared_handle),
1175 Span::call_site(),
1176 );
1177
1178 let sink_ident = syn::Ident::new(
1179 &format!("__hydro_deploy_many_{}_sink", shared_handle),
1180 Span::call_site(),
1181 );
1182
1183 let membership_ident = syn::Ident::new(
1184 &format!("__hydro_deploy_many_{}_membership", shared_handle),
1185 Span::call_site(),
1186 );
1187
1188 let bind_addr = format!("0.0.0.0:{}", p2_port);
1189
1190 extra_stmts.push(syn::parse_quote! {
1191 let #socket_ident = tokio::net::TcpListener::bind(#bind_addr).await.unwrap();
1192 });
1193
1194 let root = crate::staging_util::get_this_crate();
1195
1196 extra_stmts.push(syn::parse_quote! {
1197 let (#source_ident, #sink_ident, #membership_ident) = #root::runtime_support::hydro_deploy_integration::multi_connection::tcp_multi_connection::<_, #codec_type>(#socket_ident);
1198 });
1199
1200 parse_quote!(#source_ident)
1201 }
1202
1203 #[instrument(level = "trace", skip_all, fields(%shared_handle))]
1204 fn e2o_many_sink(shared_handle: String) -> syn::Expr {
1205 let sink_ident = syn::Ident::new(
1206 &format!("__hydro_deploy_many_{}_sink", shared_handle),
1207 Span::call_site(),
1208 );
1209 parse_quote!(#sink_ident)
1210 }
1211
1212 #[instrument(level = "trace", skip_all, fields(p1 = p1.name, %p1_port, p2 = p2.name, %p2_port, %shared_handle))]
1213 fn e2o_source(
1214 extra_stmts: &mut Vec<syn::Stmt>,
1215 p1: &Self::External,
1216 p1_port: &<Self::External as Node>::Port,
1217 p2: &Self::Process,
1218 p2_port: &<Self::Process as Node>::Port,
1219 _codec_type: &syn::Type,
1220 shared_handle: String,
1221 ) -> syn::Expr {
1222 p1.connection_info.borrow_mut().insert(
1223 *p1_port,
1224 (
1225 p2.docker_container_name.clone(),
1226 *p2_port,
1227 p2.network.clone(),
1228 ),
1229 );
1230
1231 p2.exposed_ports.borrow_mut().push(*p2_port);
1232
1233 let socket_ident = syn::Ident::new(
1234 &format!("__hydro_deploy_{}_socket", shared_handle),
1235 Span::call_site(),
1236 );
1237
1238 let source_ident = syn::Ident::new(
1239 &format!("__hydro_deploy_{}_source", shared_handle),
1240 Span::call_site(),
1241 );
1242
1243 let sink_ident = syn::Ident::new(
1244 &format!("__hydro_deploy_{}_sink", shared_handle),
1245 Span::call_site(),
1246 );
1247
1248 let bind_addr = format!("0.0.0.0:{}", p2_port);
1249
1250 extra_stmts.push(syn::parse_quote! {
1251 let #socket_ident = tokio::net::TcpListener::bind(#bind_addr).await.unwrap();
1252 });
1253
1254 let create_expr = deploy_containerized_external_sink_source_ident(socket_ident);
1255
1256 extra_stmts.push(syn::parse_quote! {
1257 let (#sink_ident, #source_ident) = (#create_expr).split();
1258 });
1259
1260 parse_quote!(#source_ident)
1261 }
1262
1263 #[instrument(level = "trace", skip_all, fields(p1 = p1.name, %p1_port, p2 = p2.name, %p2_port, ?many, ?server_hint))]
1264 fn e2o_connect(
1265 p1: &Self::External,
1266 p1_port: &<Self::External as Node>::Port,
1267 p2: &Self::Process,
1268 p2_port: &<Self::Process as Node>::Port,
1269 many: bool,
1270 server_hint: NetworkHint,
1271 ) -> Box<dyn FnOnce()> {
1272 if server_hint != NetworkHint::Auto {
1273 panic!(
1274 "Docker deployment only supports NetworkHint::Auto, got {:?}",
1275 server_hint
1276 );
1277 }
1278
1279 if many {
1281 p1.connection_info.borrow_mut().insert(
1282 *p1_port,
1283 (
1284 p2.docker_container_name.clone(),
1285 *p2_port,
1286 p2.network.clone(),
1287 ),
1288 );
1289 }
1290
1291 let serialized = format!("e2o_connect {}:{p1_port} -> {}:{p2_port}", p1.name, p2.name);
1292
1293 Box::new(move || {
1294 trace!(name: "e2o_connect thunk", %serialized);
1295 })
1296 }
1297
1298 #[instrument(level = "trace", skip_all, fields(p1 = p1.name, %p1_port, p2 = p2.name, %p2_port, %shared_handle))]
1299 fn o2e_sink(
1300 p1: &Self::Process,
1301 p1_port: &<Self::Process as Node>::Port,
1302 p2: &Self::External,
1303 p2_port: &<Self::External as Node>::Port,
1304 shared_handle: String,
1305 ) -> syn::Expr {
1306 let sink_ident = syn::Ident::new(
1307 &format!("__hydro_deploy_{}_sink", shared_handle),
1308 Span::call_site(),
1309 );
1310 parse_quote!(#sink_ident)
1311 }
1312
1313 #[instrument(level = "trace", skip_all, fields(%of_cluster))]
1314 fn cluster_ids(
1315 of_cluster: LocationKey,
1316 ) -> impl QuotedWithContext<'a, &'a [TaglessMemberId], ()> + Clone + 'a {
1317 cluster_ids()
1318 }
1319
1320 #[instrument(level = "trace", skip_all)]
1321 fn cluster_self_id() -> impl QuotedWithContext<'a, TaglessMemberId, ()> + Clone + 'a {
1322 cluster_self_id()
1323 }
1324
1325 #[instrument(level = "trace", skip_all, fields(?location_id))]
1326 fn cluster_membership_stream(
1327 _env: &mut Self::InstantiateEnv,
1328 _at_location: &LocationId,
1329 location_id: &LocationId,
1330 ) -> impl QuotedWithContext<'a, Box<dyn Stream<Item = (TaglessMemberId, MembershipEvent)> + Unpin>, ()>
1331 {
1332 cluster_membership_stream(location_id)
1333 }
1334}
1335
1336const CONTAINER_ALPHABET: [char; 36] = [
1337 '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i',
1338 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
1339];
1340
1341fn is_valid_docker_image_name(name: &str) -> bool {
1342 regex::Regex::new(r"^[a-z0-9]+([._-][a-z0-9]+)*$")
1343 .unwrap()
1344 .is_match(name)
1345}
1346
1347#[instrument(level = "trace", skip_all, ret, fields(%name_hint, %location_key, %deployment_instance))]
1348fn get_docker_image_name(
1349 name_hint: &str,
1350 location_key: LocationKey,
1351 deployment_instance: &str,
1352) -> String {
1353 let name_hint: String = name_hint
1354 .split("::")
1355 .last()
1356 .unwrap()
1357 .to_ascii_lowercase()
1358 .split(['.', '_', '-'])
1359 .filter(|s| !s.is_empty())
1360 .collect::<Vec<_>>()
1361 .join("-");
1362
1363 let image_name = format!("hy-{name_hint}-{deployment_instance}-{location_key}");
1364
1365 if !is_valid_docker_image_name(&image_name) {
1366 panic!(
1367 "Generated Docker image name '{image_name}' is not a valid Docker image name. \
1368 Docker image names may only contain lowercase alphanumeric characters \
1369 separated by single '.', '_', or '-' characters, and must start and end \
1370 with an alphanumeric character. The most likely cause is your location \
1371 struct name '{name_hint}'"
1372 );
1373 }
1374
1375 image_name
1376}
1377
1378#[instrument(level = "trace", skip_all, ret, fields(%image_name, ?instance))]
1379fn get_docker_container_name(image_name: &str, instance: Option<usize>) -> String {
1380 if let Some(instance) = instance {
1381 format!("{image_name}-{instance}")
1382 } else {
1383 image_name.to_owned()
1384 }
1385}
1386#[derive(Clone)]
1388pub struct DockerDeployProcessSpec {
1389 rustflags: Option<String>,
1390 config: Vec<String>,
1391 network: DockerNetwork,
1392 deployment_instance: String,
1393 base_image: Option<String>,
1394 linux_compile_type: LinuxCompileType,
1395 features: Vec<String>,
1396}
1397
1398impl<'a> ProcessSpec<'a, DockerDeploy> for DockerDeployProcessSpec {
1399 #[instrument(level = "trace", skip_all, fields(%key, %name_hint))]
1400 fn build(self, key: LocationKey, name_hint: &'_ str) -> <DockerDeploy as Deploy<'a>>::Process {
1401 DockerDeployProcess {
1402 key,
1403 name: get_docker_image_name(name_hint, key, &self.deployment_instance),
1404
1405 next_port: Rc::new(RefCell::new(1000)),
1406 rust_crate: Rc::new(RefCell::new(None)),
1407
1408 exposed_ports: Rc::new(RefCell::new(Vec::new())),
1409
1410 docker_container_name: Rc::new(RefCell::new(None)),
1411
1412 rustflags: self.rustflags,
1413 config: self.config,
1414
1415 network: self.network.clone(),
1416
1417 base_image: self.base_image,
1418 linux_compile_type: self.linux_compile_type,
1419 features: self.features,
1420 }
1421 }
1422}
1423
1424#[derive(Clone)]
1426pub struct DockerDeployClusterSpec {
1427 rustflags: Option<String>,
1428 config: Vec<String>,
1429 count: usize,
1430 deployment_instance: String,
1431 base_image: Option<String>,
1432 linux_compile_type: LinuxCompileType,
1433 features: Vec<String>,
1434}
1435
1436impl<'a> ClusterSpec<'a, DockerDeploy> for DockerDeployClusterSpec {
1437 #[instrument(level = "trace", skip_all, fields(%key, %name_hint))]
1438 fn build(self, key: LocationKey, name_hint: &str) -> <DockerDeploy as Deploy<'a>>::Cluster {
1439 DockerDeployCluster {
1440 key,
1441 name: get_docker_image_name(name_hint, key, &self.deployment_instance),
1442
1443 next_port: Rc::new(RefCell::new(1000)),
1444 rust_crate: Rc::new(RefCell::new(None)),
1445
1446 exposed_ports: Rc::new(RefCell::new(Vec::new())),
1447
1448 docker_container_name: Rc::new(RefCell::new(Vec::new())),
1449
1450 rustflags: self.rustflags,
1451 config: self.config,
1452
1453 count: self.count,
1454
1455 base_image: self.base_image,
1456 linux_compile_type: self.linux_compile_type,
1457 features: self.features,
1458 }
1459 }
1460}
1461
1462impl DockerDeployProcessSpec {
1463 pub fn base_image(mut self, image: impl Into<String>) -> Self {
1466 self.base_image = Some(image.into());
1467 self
1468 }
1469
1470 pub fn linux_compile_type(mut self, compile_type: LinuxCompileType) -> Self {
1473 self.linux_compile_type = compile_type;
1474 self
1475 }
1476
1477 pub fn features(mut self, features: impl IntoIterator<Item = impl Into<String>>) -> Self {
1479 self.features.extend(features.into_iter().map(Into::into));
1480 self
1481 }
1482
1483 pub fn feature(mut self, feature: impl Into<String>) -> Self {
1485 self.features.push(feature.into());
1486 self
1487 }
1488}
1489
1490impl DockerDeployClusterSpec {
1491 pub fn base_image(mut self, image: impl Into<String>) -> Self {
1494 self.base_image = Some(image.into());
1495 self
1496 }
1497
1498 pub fn linux_compile_type(mut self, compile_type: LinuxCompileType) -> Self {
1501 self.linux_compile_type = compile_type;
1502 self
1503 }
1504
1505 pub fn features(mut self, features: impl IntoIterator<Item = impl Into<String>>) -> Self {
1507 self.features.extend(features.into_iter().map(Into::into));
1508 self
1509 }
1510
1511 pub fn feature(mut self, feature: impl Into<String>) -> Self {
1513 self.features.push(feature.into());
1514 self
1515 }
1516}
1517
1518pub struct DockerDeployExternalSpec {
1520 name: String,
1521}
1522
1523impl<'a> ExternalSpec<'a, DockerDeploy> for DockerDeployExternalSpec {
1524 #[instrument(level = "trace", skip_all, fields(%key, %name_hint))]
1525 fn build(self, key: LocationKey, name_hint: &str) -> <DockerDeploy as Deploy<'a>>::External {
1526 DockerDeployExternal {
1527 key,
1528 name: self.name,
1529 next_port: Rc::new(RefCell::new(10000)),
1530 next_external_port_id: Rc::new(RefCell::new(crate::Counter::default())),
1531 ports: Rc::new(RefCell::new(HashMap::new())),
1532 connection_info: Rc::new(RefCell::new(HashMap::new())),
1533 }
1534 }
1535}