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 assert!(
1273 server_hint == NetworkHint::Auto,
1274 "Docker deployment only supports NetworkHint::Auto, got {:?}",
1275 server_hint
1276 );
1277
1278 if many {
1280 p1.connection_info.borrow_mut().insert(
1281 *p1_port,
1282 (
1283 p2.docker_container_name.clone(),
1284 *p2_port,
1285 p2.network.clone(),
1286 ),
1287 );
1288 }
1289
1290 let serialized = format!("e2o_connect {}:{p1_port} -> {}:{p2_port}", p1.name, p2.name);
1291
1292 Box::new(move || {
1293 trace!(name: "e2o_connect thunk", %serialized);
1294 })
1295 }
1296
1297 #[instrument(level = "trace", skip_all, fields(p1 = p1.name, %p1_port, p2 = p2.name, %p2_port, %shared_handle))]
1298 fn o2e_sink(
1299 p1: &Self::Process,
1300 p1_port: &<Self::Process as Node>::Port,
1301 p2: &Self::External,
1302 p2_port: &<Self::External as Node>::Port,
1303 shared_handle: String,
1304 ) -> syn::Expr {
1305 let sink_ident = syn::Ident::new(
1306 &format!("__hydro_deploy_{}_sink", shared_handle),
1307 Span::call_site(),
1308 );
1309 parse_quote!(#sink_ident)
1310 }
1311
1312 #[instrument(level = "trace", skip_all, fields(%of_cluster))]
1313 fn cluster_ids(
1314 of_cluster: LocationKey,
1315 ) -> impl QuotedWithContext<'a, &'a [TaglessMemberId], ()> + Clone + 'a {
1316 cluster_ids()
1317 }
1318
1319 #[instrument(level = "trace", skip_all)]
1320 fn cluster_self_id() -> impl QuotedWithContext<'a, TaglessMemberId, ()> + Clone + 'a {
1321 cluster_self_id()
1322 }
1323
1324 #[instrument(level = "trace", skip_all, fields(?location_id))]
1325 fn cluster_membership_stream(
1326 _env: &mut Self::InstantiateEnv,
1327 _at_location: &LocationId,
1328 location_id: &LocationId,
1329 ) -> impl QuotedWithContext<'a, Box<dyn Stream<Item = (TaglessMemberId, MembershipEvent)> + Unpin>, ()>
1330 {
1331 cluster_membership_stream(location_id)
1332 }
1333}
1334
1335const CONTAINER_ALPHABET: [char; 36] = [
1336 '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i',
1337 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
1338];
1339
1340fn is_valid_docker_image_name(name: &str) -> bool {
1341 regex::Regex::new(r"^[a-z0-9]+([._-][a-z0-9]+)*$")
1342 .unwrap()
1343 .is_match(name)
1344}
1345
1346#[instrument(level = "trace", skip_all, ret, fields(%name_hint, %location_key, %deployment_instance))]
1347fn get_docker_image_name(
1348 name_hint: &str,
1349 location_key: LocationKey,
1350 deployment_instance: &str,
1351) -> String {
1352 let name_hint: String = name_hint
1353 .split("::")
1354 .last()
1355 .unwrap()
1356 .to_ascii_lowercase()
1357 .split(['.', '_', '-'])
1358 .filter(|s| !s.is_empty())
1359 .collect::<Vec<_>>()
1360 .join("-");
1361
1362 let image_name = format!("hy-{name_hint}-{deployment_instance}-{location_key}");
1363
1364 assert!(
1365 is_valid_docker_image_name(&image_name),
1366 "Generated Docker image name '{image_name}' is not a valid Docker image name. \
1367 Docker image names may only contain lowercase alphanumeric characters \
1368 separated by single '.', '_', or '-' characters, and must start and end \
1369 with an alphanumeric character. The most likely cause is your location \
1370 struct name '{name_hint}'"
1371 );
1372
1373 image_name
1374}
1375
1376#[instrument(level = "trace", skip_all, ret, fields(%image_name, ?instance))]
1377fn get_docker_container_name(image_name: &str, instance: Option<usize>) -> String {
1378 if let Some(instance) = instance {
1379 format!("{image_name}-{instance}")
1380 } else {
1381 image_name.to_owned()
1382 }
1383}
1384#[derive(Clone)]
1386pub struct DockerDeployProcessSpec {
1387 rustflags: Option<String>,
1388 config: Vec<String>,
1389 network: DockerNetwork,
1390 deployment_instance: String,
1391 base_image: Option<String>,
1392 linux_compile_type: LinuxCompileType,
1393 features: Vec<String>,
1394}
1395
1396impl<'a> ProcessSpec<'a, DockerDeploy> for DockerDeployProcessSpec {
1397 #[instrument(level = "trace", skip_all, fields(%key, %name_hint))]
1398 fn build(self, key: LocationKey, name_hint: &'_ str) -> <DockerDeploy as Deploy<'a>>::Process {
1399 DockerDeployProcess {
1400 key,
1401 name: get_docker_image_name(name_hint, key, &self.deployment_instance),
1402
1403 next_port: Rc::new(RefCell::new(1000)),
1404 rust_crate: Rc::new(RefCell::new(None)),
1405
1406 exposed_ports: Rc::new(RefCell::new(Vec::new())),
1407
1408 docker_container_name: Rc::new(RefCell::new(None)),
1409
1410 rustflags: self.rustflags,
1411 config: self.config,
1412
1413 network: self.network.clone(),
1414
1415 base_image: self.base_image,
1416 linux_compile_type: self.linux_compile_type,
1417 features: self.features,
1418 }
1419 }
1420}
1421
1422#[derive(Clone)]
1424pub struct DockerDeployClusterSpec {
1425 rustflags: Option<String>,
1426 config: Vec<String>,
1427 count: usize,
1428 deployment_instance: String,
1429 base_image: Option<String>,
1430 linux_compile_type: LinuxCompileType,
1431 features: Vec<String>,
1432}
1433
1434impl<'a> ClusterSpec<'a, DockerDeploy> for DockerDeployClusterSpec {
1435 #[instrument(level = "trace", skip_all, fields(%key, %name_hint))]
1436 fn build(self, key: LocationKey, name_hint: &str) -> <DockerDeploy as Deploy<'a>>::Cluster {
1437 DockerDeployCluster {
1438 key,
1439 name: get_docker_image_name(name_hint, key, &self.deployment_instance),
1440
1441 next_port: Rc::new(RefCell::new(1000)),
1442 rust_crate: Rc::new(RefCell::new(None)),
1443
1444 exposed_ports: Rc::new(RefCell::new(Vec::new())),
1445
1446 docker_container_name: Rc::new(RefCell::new(Vec::new())),
1447
1448 rustflags: self.rustflags,
1449 config: self.config,
1450
1451 count: self.count,
1452
1453 base_image: self.base_image,
1454 linux_compile_type: self.linux_compile_type,
1455 features: self.features,
1456 }
1457 }
1458}
1459
1460impl DockerDeployProcessSpec {
1461 pub fn base_image(mut self, image: impl Into<String>) -> Self {
1464 self.base_image = Some(image.into());
1465 self
1466 }
1467
1468 pub fn linux_compile_type(mut self, compile_type: LinuxCompileType) -> Self {
1471 self.linux_compile_type = compile_type;
1472 self
1473 }
1474
1475 pub fn features(mut self, features: impl IntoIterator<Item = impl Into<String>>) -> Self {
1477 self.features.extend(features.into_iter().map(Into::into));
1478 self
1479 }
1480
1481 pub fn feature(mut self, feature: impl Into<String>) -> Self {
1483 self.features.push(feature.into());
1484 self
1485 }
1486}
1487
1488impl DockerDeployClusterSpec {
1489 pub fn base_image(mut self, image: impl Into<String>) -> Self {
1492 self.base_image = Some(image.into());
1493 self
1494 }
1495
1496 pub fn linux_compile_type(mut self, compile_type: LinuxCompileType) -> Self {
1499 self.linux_compile_type = compile_type;
1500 self
1501 }
1502
1503 pub fn features(mut self, features: impl IntoIterator<Item = impl Into<String>>) -> Self {
1505 self.features.extend(features.into_iter().map(Into::into));
1506 self
1507 }
1508
1509 pub fn feature(mut self, feature: impl Into<String>) -> Self {
1511 self.features.push(feature.into());
1512 self
1513 }
1514}
1515
1516pub struct DockerDeployExternalSpec {
1518 name: String,
1519}
1520
1521impl<'a> ExternalSpec<'a, DockerDeploy> for DockerDeployExternalSpec {
1522 #[instrument(level = "trace", skip_all, fields(%key, %name_hint))]
1523 fn build(self, key: LocationKey, name_hint: &str) -> <DockerDeploy as Deploy<'a>>::External {
1524 DockerDeployExternal {
1525 key,
1526 name: self.name,
1527 next_port: Rc::new(RefCell::new(10000)),
1528 next_external_port_id: Rc::new(RefCell::new(crate::Counter::default())),
1529 ports: Rc::new(RefCell::new(HashMap::new())),
1530 connection_info: Rc::new(RefCell::new(HashMap::new())),
1531 }
1532 }
1533}