1use std::collections::{BTreeMap, BTreeSet, HashMap};
6use std::fmt;
7use std::time::{Duration, Instant};
8
9use bollard::exec::{CreateExecOptions, StartExecResults};
10use std::pin::Pin;
11
12use bollard::models::{
13 ChangeType, ContainerConfig, ContainerCreateBody, CreateImageInfo, HostConfig, Mount,
14 NetworkCreateRequest, SystemInfo, VolumeCreateRequest,
15};
16use bollard::query_parameters::{
17 AttachContainerOptionsBuilder, CommitContainerOptionsBuilder, CreateContainerOptionsBuilder,
18 CreateImageOptionsBuilder, DataUsageOptionsBuilder, InspectContainerOptionsBuilder,
19 ListContainersOptionsBuilder, ListImagesOptionsBuilder, LogsOptionsBuilder,
20 RemoveContainerOptionsBuilder, RemoveImageOptionsBuilder, RemoveVolumeOptionsBuilder,
21 ResizeContainerTTYOptionsBuilder, UploadToContainerOptionsBuilder, WaitContainerOptionsBuilder,
22};
23use futures_util::stream::BoxStream;
24use futures_util::{StreamExt, TryStreamExt};
25use tracing::info;
26
27use crate::config::ByteSize;
28use crate::isolation::Isolation;
29
30pub use bollard::errors::Error;
31
32#[derive(Debug, Clone)]
34pub struct RunnerSpec {
35 pub name: String,
37 pub image: String,
39 pub profile: String,
41 pub slice: String,
43 pub cpus: u32,
45 pub memory: ByteSize,
47 pub env: Vec<String>,
49 pub mounts: Vec<Mount>,
51 pub network: Option<String>,
53 pub labels: BTreeMap<String, String>,
55 pub entrypoint: Option<Vec<String>>,
57 pub jit_config: Option<JitConfig>,
59}
60
61pub const JIT_CONFIG_PATH: &str = "/run/runnerd/jitconfig";
64
65#[derive(Clone)]
67pub struct JitConfig(pub String);
68
69impl fmt::Debug for JitConfig {
70 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
71 f.write_str("JitConfig(<redacted>)")
72 }
73}
74
75#[derive(Debug, Clone, PartialEq, Eq)]
77pub struct RunnerState {
78 pub running: bool,
80 pub exit_code: Option<i64>,
82 pub oom_killed: bool,
84 pub privileged: bool,
86 pub runtime: Option<String>,
88 pub pid: Option<u32>,
90 pub address: Option<String>,
92 pub started_at: Option<u64>,
94 pub memory: Option<u64>,
96 pub cpus: Option<u64>,
98}
99
100pub type Output = BoxStream<'static, Result<Vec<u8>, Error>>;
102
103#[derive(Debug, Clone, PartialEq, Eq)]
105pub struct Image {
106 pub reference: String,
108 pub labels: HashMap<String, String>,
110 pub bytes: u64,
112}
113
114#[derive(Debug, Clone, PartialEq, Eq)]
116pub struct TaggedImage {
117 pub id: String,
119 pub tags: Vec<String>,
121 pub digests: Vec<String>,
123}
124
125pub struct Tty {
127 pub container: String,
129 pub output: BoxStream<'static, Result<Vec<u8>, Error>>,
131 pub input: Pin<Box<dyn tokio::io::AsyncWrite + Send>>,
133}
134
135impl fmt::Debug for Tty {
136 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
137 f.debug_struct("Tty")
138 .field("container", &self.container)
139 .finish_non_exhaustive()
140 }
141}
142
143#[derive(Debug, Clone, Copy, PartialEq, Eq)]
145pub struct TtySize {
146 pub cols: u16,
148 pub rows: u16,
150}
151
152pub fn create_body(spec: &RunnerSpec, isolation: &dyn Isolation) -> ContainerCreateBody {
154 let memory = i64::try_from(spec.memory.0).unwrap_or(i64::MAX);
156 let mut host_config = HostConfig {
157 cgroup_parent: Some(spec.slice.clone()),
158 memory: Some(memory),
159 memory_swap: Some(memory),
161 nano_cpus: Some(i64::from(spec.cpus) * 1_000_000_000),
162 init: Some(true),
163 mounts: Some(spec.mounts.clone()),
164 network_mode: spec.network.clone(),
165 ..Default::default()
166 };
167 isolation.configure(&mut host_config);
168 let mut labels: HashMap<String, String> = spec.labels.clone().into_iter().collect();
169 labels.insert("runnerd.profile".to_owned(), spec.profile.clone());
170 labels.insert("runnerd.isolation".to_owned(), isolation.name().to_owned());
171 ContainerCreateBody {
172 image: Some(spec.image.clone()),
173 env: Some(spec.env.clone()),
174 entrypoint: spec.entrypoint.clone(),
175 labels: Some(labels),
176 host_config: Some(host_config),
177 ..Default::default()
178 }
179}
180
181#[derive(Debug, Clone, PartialEq, Eq)]
183pub struct Container {
184 pub id: String,
186 pub name: String,
188 pub running: bool,
190 pub labels: HashMap<String, String>,
192}
193
194pub fn is_not_found(error: &Error) -> bool {
196 matches!(
197 error,
198 Error::DockerResponseServerError {
199 status_code: 404,
200 ..
201 }
202 )
203}
204
205pub trait DockerApi: Send + Sync {
208 fn start(
210 &self,
211 spec: &RunnerSpec,
212 isolation: &dyn Isolation,
213 ) -> impl Future<Output = Result<String, Error>> + Send;
214 fn inspect(&self, container: &str) -> impl Future<Output = Result<RunnerState, Error>> + Send;
216 fn remove(&self, container: &str) -> impl Future<Output = Result<(), Error>> + Send;
218 fn run(
220 &self,
221 name: &str,
222 body: ContainerCreateBody,
223 ) -> impl Future<Output = Result<String, Error>> + Send;
224 fn list(&self, label: &str) -> impl Future<Output = Result<Vec<Container>, Error>> + Send;
226 fn wait(&self, container: &str) -> impl Future<Output = Result<i64, Error>> + Send;
228 fn logs(&self, container: &str) -> impl Future<Output = Result<String, Error>> + Send;
230 fn logs_since(
233 &self,
234 container: &str,
235 since: u64,
236 ) -> impl Future<Output = Result<String, Error>> + Send;
237 fn exec(
240 &self,
241 container: &str,
242 command: Vec<String>,
243 ) -> impl Future<Output = Result<Output, Error>> + Send;
244 fn image_present(&self, image: &str) -> impl Future<Output = Result<bool, Error>> + Send;
246 fn pull(&self, image: &str) -> impl Future<Output = Result<(), Error>> + Send;
248 fn ensure_network(&self, name: &str) -> impl Future<Output = Result<(), Error>> + Send;
250 fn ensure_volume(&self, name: &str) -> impl Future<Output = Result<(), Error>> + Send;
252 fn remove_volume(&self, name: &str) -> impl Future<Output = Result<(), Error>> + Send;
254 fn volume_sizes(&self) -> impl Future<Output = Result<BTreeMap<String, u64>, Error>> + Send;
256 fn changes(&self, container: &str) -> impl Future<Output = Result<Vec<String>, Error>> + Send;
258 fn written(&self, container: &str) -> impl Future<Output = Result<u64, Error>> + Send;
260 fn commit(
263 &self,
264 container: &str,
265 reference: &str,
266 labels: BTreeMap<String, String>,
267 ) -> impl Future<Output = Result<u64, Error>> + Send;
268 fn images(&self, label: &str) -> impl Future<Output = Result<Vec<Image>, Error>> + Send;
270 fn image_id(
272 &self,
273 reference: &str,
274 ) -> impl Future<Output = Result<Option<String>, Error>> + Send;
275 fn tagged_images(&self) -> impl Future<Output = Result<Vec<TaggedImage>, Error>> + Send;
277 fn container_images(&self) -> impl Future<Output = Result<BTreeSet<String>, Error>> + Send;
279 fn remove_image(&self, reference: &str) -> impl Future<Output = Result<(), Error>> + Send;
281 fn free_space(&self) -> impl Future<Output = Result<u64, Error>> + Send;
283 fn shell(
286 &self,
287 name: &str,
288 body: ContainerCreateBody,
289 size: TtySize,
290 ) -> impl Future<Output = Result<Tty, Error>> + Send;
291}
292
293#[derive(Debug)]
295pub struct Docker {
296 api: bollard::Docker,
297}
298
299impl Docker {
300 pub fn connect() -> Result<Docker, Error> {
302 connect_to(std::env::var("DOCKER_HOST").ok().as_deref())
303 }
304
305 pub async fn info(&self) -> Result<SystemInfo, Error> {
307 self.api.info().await
308 }
309
310 pub async fn image_labels(
312 &self,
313 image: &str,
314 ) -> Result<Option<HashMap<String, String>>, Error> {
315 match self.api.inspect_image(image).await {
316 Ok(inspect) => Ok(Some(
317 inspect
318 .config
319 .and_then(|config| config.labels)
320 .unwrap_or_default(),
321 )),
322 Err(error) if is_not_found(&error) => Ok(None),
323 Err(error) => Err(error),
324 }
325 }
326
327 pub async fn registry_platforms(&self, image: &str) -> Result<Vec<String>, Error> {
330 let inspect = self.api.inspect_registry_image(image, None).await?;
331 Ok(inspect
332 .platforms
333 .into_iter()
334 .filter_map(|platform| Some(format!("{}/{}", platform.os?, platform.architecture?)))
335 .filter(|platform| platform != "unknown/unknown")
337 .collect())
338 }
339}
340
341impl DockerApi for Docker {
342 async fn start(&self, spec: &RunnerSpec, isolation: &dyn Isolation) -> Result<String, Error> {
343 let options = CreateContainerOptionsBuilder::new()
344 .name(&spec.name)
345 .build();
346 let id = self
347 .api
348 .create_container(Some(options), create_body(spec, isolation))
349 .await?
350 .id;
351 let started = async {
352 if let Some(jit_config) = &spec.jit_config {
353 let options = UploadToContainerOptionsBuilder::new().path("/").build();
354 let tar = jit_config_tar(jit_config)?;
355 let tar = futures_util::stream::once(std::future::ready(tar.into()));
357 self.api
358 .upload_to_container(&id, Some(options), bollard::body_stream(tar))
359 .await?;
360 }
361 self.api.start_container(&id, None).await
362 };
363 if let Err(error) = started.await {
364 let _ = self.remove(&id).await;
366 return Err(error);
367 }
368 Ok(id)
369 }
370
371 async fn inspect(&self, container: &str) -> Result<RunnerState, Error> {
372 let response = self.api.inspect_container(container, None).await?;
373 let state = response.state.unwrap_or_default();
374 let host_config = response.host_config.unwrap_or_default();
375 let address = response
376 .network_settings
377 .and_then(|settings| settings.networks)
378 .and_then(|networks| {
379 networks
380 .into_values()
381 .find_map(|network| network.ip_address.filter(|address| !address.is_empty()))
382 });
383 Ok(RunnerState {
384 running: state.running.unwrap_or(false),
385 exit_code: state.exit_code,
386 oom_killed: state.oom_killed.unwrap_or(false),
387 privileged: host_config.privileged.unwrap_or(false),
388 runtime: host_config.runtime,
389 pid: state
390 .pid
391 .and_then(|pid| u32::try_from(pid).ok())
392 .filter(|pid| *pid > 0),
393 address,
394 started_at: state.started_at.as_deref().and_then(crate::time::parse_ms),
395 memory: host_config
396 .memory
397 .and_then(|memory| u64::try_from(memory).ok())
398 .filter(|memory| *memory > 0),
399 cpus: host_config
400 .nano_cpus
401 .and_then(|nano| u64::try_from(nano / 1_000_000).ok())
402 .filter(|cpus| *cpus > 0),
403 })
404 }
405
406 async fn remove(&self, container: &str) -> Result<(), Error> {
407 let options = RemoveContainerOptionsBuilder::new()
409 .force(true)
410 .v(true)
411 .build();
412 self.api.remove_container(container, Some(options)).await
413 }
414
415 async fn run(&self, name: &str, body: ContainerCreateBody) -> Result<String, Error> {
416 let options = CreateContainerOptionsBuilder::new().name(name).build();
417 let id = self.api.create_container(Some(options), body).await?.id;
418 if let Err(error) = self.api.start_container(&id, None).await {
419 let _ = self.remove(&id).await;
420 return Err(error);
421 }
422 Ok(id)
423 }
424
425 async fn list(&self, label: &str) -> Result<Vec<Container>, Error> {
426 let options = ListContainersOptionsBuilder::new()
427 .all(true)
428 .filters(&HashMap::from([("label", vec![label])]))
429 .build();
430 let containers = self.api.list_containers(Some(options)).await?;
431 Ok(containers
432 .into_iter()
433 .map(|container| Container {
434 id: container.id.unwrap_or_default(),
435 name: container
436 .names
437 .and_then(|names| names.into_iter().next())
438 .unwrap_or_default()
439 .trim_start_matches('/')
440 .to_owned(),
441 running: container.state
442 == Some(bollard::models::ContainerSummaryStateEnum::RUNNING),
443 labels: container.labels.unwrap_or_default(),
444 })
445 .collect())
446 }
447
448 async fn wait(&self, container: &str) -> Result<i64, Error> {
449 let options = WaitContainerOptionsBuilder::new()
450 .condition("not-running")
451 .build();
452 let mut responses = self.api.wait_container(container, Some(options));
453 match responses.next().await {
454 Some(Ok(response)) => Ok(response.status_code),
455 Some(Err(Error::DockerContainerWaitError { code, .. })) => Ok(code),
457 Some(Err(error)) => Err(error),
458 None => Ok(0),
459 }
460 }
461
462 async fn logs(&self, container: &str) -> Result<String, Error> {
463 let options = LogsOptionsBuilder::new().stdout(true).stderr(true).build();
464 let chunks: Vec<_> = self
465 .api
466 .logs(container, Some(options))
467 .try_collect()
468 .await?;
469 Ok(chunks.iter().map(ToString::to_string).collect())
470 }
471
472 async fn logs_since(&self, container: &str, since: u64) -> Result<String, Error> {
473 let options = LogsOptionsBuilder::new()
474 .stdout(true)
475 .stderr(true)
476 .timestamps(true)
477 .since(i32::try_from(since).unwrap_or(i32::MAX))
478 .build();
479 let chunks: Vec<_> = self
480 .api
481 .logs(container, Some(options))
482 .try_collect()
483 .await?;
484 Ok(chunks.iter().map(ToString::to_string).collect())
485 }
486
487 async fn exec(&self, container: &str, command: Vec<String>) -> Result<Output, Error> {
488 let options = CreateExecOptions {
489 cmd: Some(command),
490 user: Some("root".to_owned()),
491 attach_stdout: Some(true),
492 attach_stderr: Some(true),
493 ..Default::default()
494 };
495 let exec = self.api.create_exec(container, options).await?;
496 match self.api.start_exec(&exec.id, None).await? {
497 StartExecResults::Attached { output, .. } => Ok(output
498 .try_filter_map(|output| {
499 std::future::ready(Ok(match output {
500 bollard::container::LogOutput::StdOut { message } => Some(message.to_vec()),
501 _ => None,
502 }))
503 })
504 .boxed()),
505 StartExecResults::Detached => Ok(futures_util::stream::empty().boxed()),
506 }
507 }
508
509 async fn image_present(&self, image: &str) -> Result<bool, Error> {
510 Ok(self.image_labels(image).await?.is_some())
511 }
512
513 async fn pull(&self, image: &str) -> Result<(), Error> {
514 let options = CreateImageOptionsBuilder::new().from_image(image).build();
515 let mut stream = self.api.create_image(Some(options), None, None);
516 let mut progress = PullProgress::default();
517 let mut logged = Instant::now();
518 info!("pulling {image}");
519 while let Some(update) = stream.try_next().await? {
520 progress.update(&update);
521 if logged.elapsed() >= PULL_LOG_INTERVAL {
522 logged = Instant::now();
523 info!("pulling {image}: {progress}");
524 }
525 }
526 info!("pulled {image}");
527 Ok(())
528 }
529
530 async fn ensure_network(&self, name: &str) -> Result<(), Error> {
531 match self.api.inspect_network(name, None).await {
532 Err(error) if is_not_found(&error) => {
533 let request = NetworkCreateRequest {
534 name: name.to_owned(),
535 driver: Some("bridge".to_owned()),
536 ..Default::default()
537 };
538 self.api.create_network(request).await.map(|_| ())
539 }
540 other => other.map(|_| ()),
541 }
542 }
543
544 async fn ensure_volume(&self, name: &str) -> Result<(), Error> {
545 let request = VolumeCreateRequest {
547 name: Some(name.to_owned()),
548 ..Default::default()
549 };
550 self.api.create_volume(request).await.map(|_| ())
551 }
552
553 async fn remove_volume(&self, name: &str) -> Result<(), Error> {
554 let options = RemoveVolumeOptionsBuilder::new().build();
555 match self.api.remove_volume(name, Some(options)).await {
556 Err(error) if is_not_found(&error) => Ok(()),
557 other => other,
558 }
559 }
560
561 async fn volume_sizes(&self) -> Result<BTreeMap<String, u64>, Error> {
562 let options = DataUsageOptionsBuilder::new().verbose(true).build();
564 let usage = self.api.df(Some(options)).await?;
565 Ok(usage
567 .volume_usage
568 .and_then(|volumes| volumes.items)
569 .unwrap_or_default()
570 .into_iter()
571 .filter_map(|volume| {
572 let name = volume.get("Name")?.as_str()?.to_owned();
573 let size = volume.get("UsageData")?.get("Size")?.as_u64()?;
574 Some((name, size))
575 })
576 .collect())
577 }
578
579 async fn changes(&self, container: &str) -> Result<Vec<String>, Error> {
580 Ok(self
581 .api
582 .container_changes(container)
583 .await?
584 .unwrap_or_default()
585 .into_iter()
586 .filter(|change| change.kind != ChangeType::_2)
587 .map(|change| change.path)
588 .collect())
589 }
590
591 async fn written(&self, container: &str) -> Result<u64, Error> {
592 let options = InspectContainerOptionsBuilder::new().size(true).build();
593 let response = self.api.inspect_container(container, Some(options)).await?;
594 Ok(response
595 .size_rw
596 .and_then(|bytes| u64::try_from(bytes).ok())
597 .unwrap_or(0))
598 }
599
600 async fn commit(
601 &self,
602 container: &str,
603 reference: &str,
604 labels: BTreeMap<String, String>,
605 ) -> Result<u64, Error> {
606 let (repository, tag) = reference.rsplit_once(':').unwrap_or((reference, "latest"));
607 let options = CommitContainerOptionsBuilder::new()
608 .container(container)
609 .repo(repository)
610 .tag(tag)
611 .pause(false)
612 .build();
613 let config = ContainerConfig {
614 labels: Some(labels.into_iter().collect()),
615 ..Default::default()
616 };
617 let parent = match self.api.inspect_container(container, None).await?.image {
619 Some(parent) => self.api.inspect_image(&parent).await?.size.unwrap_or(0),
620 None => 0,
621 };
622 let id = self.api.commit_container(options, config).await?.id;
623 let size = self.api.inspect_image(&id).await?.size.unwrap_or(0);
624 Ok(u64::try_from(size - parent).unwrap_or(0))
625 }
626
627 async fn images(&self, label: &str) -> Result<Vec<Image>, Error> {
628 let options = ListImagesOptionsBuilder::new()
629 .filters(&HashMap::from([("label", vec![label])]))
630 .build();
631 let images = self.api.list_images(Some(options)).await?;
632 let mut parents: HashMap<String, i64> = HashMap::new();
633 let mut found = Vec::new();
634 for image in images {
635 let parent = if image.parent_id.is_empty() {
636 0
637 } else if let Some(size) = parents.get(&image.parent_id) {
638 *size
639 } else {
640 let size = self
641 .api
642 .inspect_image(&image.parent_id)
643 .await
644 .ok()
645 .and_then(|parent| parent.size)
646 .unwrap_or(0);
647 parents.insert(image.parent_id.clone(), size);
648 size
649 };
650 found.push(Image {
651 reference: image.repo_tags.into_iter().next().unwrap_or(image.id),
652 labels: image.labels,
653 bytes: u64::try_from(image.size - parent).unwrap_or(0),
654 });
655 }
656 Ok(found)
657 }
658
659 async fn image_id(&self, reference: &str) -> Result<Option<String>, Error> {
660 match self.api.inspect_image(reference).await {
661 Ok(inspect) => Ok(inspect.id),
662 Err(error) if is_not_found(&error) => Ok(None),
663 Err(error) => Err(error),
664 }
665 }
666
667 async fn tagged_images(&self) -> Result<Vec<TaggedImage>, Error> {
668 let images = self
669 .api
670 .list_images(Some(ListImagesOptionsBuilder::new().build()))
671 .await?;
672 Ok(images
673 .into_iter()
674 .map(|image| TaggedImage {
675 id: image.id,
676 tags: image
677 .repo_tags
678 .into_iter()
679 .filter(|tag| tag != "<none>:<none>")
680 .collect(),
681 digests: image
682 .repo_digests
683 .into_iter()
684 .filter(|digest| digest != "<none>@<none>")
685 .collect(),
686 })
687 .filter(|image| !image.tags.is_empty())
688 .collect())
689 }
690
691 async fn container_images(&self) -> Result<BTreeSet<String>, Error> {
692 let options = ListContainersOptionsBuilder::new().all(true).build();
693 let containers = self.api.list_containers(Some(options)).await?;
694 Ok(containers
695 .into_iter()
696 .filter_map(|container| container.image_id)
697 .collect())
698 }
699
700 async fn remove_image(&self, reference: &str) -> Result<(), Error> {
701 let options = RemoveImageOptionsBuilder::new().build();
702 match self.api.remove_image(reference, Some(options), None).await {
703 Err(error) if is_not_found(&error) => Ok(()),
704 other => other.map(|_| ()),
705 }
706 }
707
708 async fn free_space(&self) -> Result<u64, Error> {
709 let root = self.api.info().await?.docker_root_dir.unwrap_or_default();
710 let stat = rustix::fs::statvfs(root.as_str()).map_err(|error| Error::IOError {
712 err: std::io::Error::other(format!("cannot measure the disk of {root}: {error}")),
713 })?;
714 Ok(stat.f_bavail.saturating_mul(stat.f_frsize))
715 }
716
717 async fn shell(
718 &self,
719 name: &str,
720 body: ContainerCreateBody,
721 size: TtySize,
722 ) -> Result<Tty, Error> {
723 let options = CreateContainerOptionsBuilder::new().name(name).build();
724 let id = self.api.create_container(Some(options), body).await?.id;
725 let attached = async {
726 let options = AttachContainerOptionsBuilder::new()
727 .stream(true)
728 .stdin(true)
729 .stdout(true)
730 .stderr(true)
731 .build();
732 let attached = self.api.attach_container(&id, Some(options)).await?;
733 self.api.start_container(&id, None).await?;
734 let resize = ResizeContainerTTYOptionsBuilder::new()
735 .w(i32::from(size.cols))
736 .h(i32::from(size.rows))
737 .build();
738 self.api.resize_container_tty(&id, resize).await?;
739 Ok::<_, Error>(attached)
740 };
741 match attached.await {
742 Ok(attached) => Ok(Tty {
743 container: id,
744 output: attached
745 .output
746 .map_ok(|output| output.into_bytes().to_vec())
747 .boxed(),
748 input: attached.input,
749 }),
750 Err(error) => {
751 let _ = self.remove(&id).await;
752 Err(error)
753 }
754 }
755 }
756}
757
758const PULL_LOG_INTERVAL: Duration = Duration::from_secs(10);
760
761#[derive(Debug, Default)]
763struct PullProgress {
764 layers: BTreeMap<String, Layer>,
765}
766
767#[derive(Debug, Default)]
768struct Layer {
769 downloaded: u64,
770 size: u64,
771 done: bool,
772}
773
774impl PullProgress {
775 fn update(&mut self, update: &CreateImageInfo) {
776 let (Some(id), Some(status)) = (&update.id, &update.status) else {
777 return;
778 };
779 let bytes = |value: Option<i64>| value.and_then(|value| u64::try_from(value).ok());
780 let detail = update.progress_detail.as_ref();
781 match status.as_str() {
782 "Pulling fs layer" | "Waiting" => {
783 self.layers.entry(id.clone()).or_default();
784 }
785 "Downloading" => {
786 let layer = self.layers.entry(id.clone()).or_default();
787 layer.downloaded =
788 bytes(detail.and_then(|detail| detail.current)).unwrap_or(layer.downloaded);
789 layer.size = bytes(detail.and_then(|detail| detail.total)).unwrap_or(layer.size);
790 }
791 "Download complete" => {
792 if let Some(layer) = self.layers.get_mut(id) {
793 layer.downloaded = layer.size;
794 }
795 }
796 "Pull complete" | "Already exists" => {
797 let layer = self.layers.entry(id.clone()).or_default();
798 layer.downloaded = layer.size;
799 layer.done = true;
800 }
801 _ => {}
802 }
803 }
804}
805
806impl fmt::Display for PullProgress {
807 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
808 let done = self.layers.values().filter(|layer| layer.done).count();
809 let downloaded: u64 = self.layers.values().map(|layer| layer.downloaded).sum();
810 let size: u64 = self.layers.values().map(|layer| layer.size).sum();
811 write!(
812 f,
813 "{done} of {} layers done, {} of {} downloaded",
814 self.layers.len(),
815 ByteSize(downloaded),
816 ByteSize(size)
817 )
818 }
819}
820
821fn connect_to(docker_host: Option<&str>) -> Result<Docker, Error> {
822 let api = match docker_host.filter(|host| !host.is_empty()) {
823 None => bollard::Docker::connect_with_socket_defaults()?,
824 Some(host) if host.starts_with("unix://") => {
825 bollard::Docker::connect_with_socket(host, 120, bollard::API_DEFAULT_VERSION)?
826 }
827 Some(host) => {
829 return Err(Error::UnsupportedURISchemeError {
830 uri: host.to_owned(),
831 });
832 }
833 };
834 Ok(Docker { api })
835}
836
837fn jit_config_tar(jit_config: &JitConfig) -> std::io::Result<Vec<u8>> {
839 let mut tar = tar::Builder::new(Vec::new());
840 let path = JIT_CONFIG_PATH.trim_start_matches('/');
841 let mut dir = tar::Header::new_gnu();
842 dir.set_entry_type(tar::EntryType::Directory);
843 dir.set_mode(0o700);
844 dir.set_size(0);
845 tar.append_data(
846 &mut dir,
847 path.rsplit_once('/').map_or("", |(dir, _)| dir),
848 std::io::empty(),
849 )?;
850 let mut file = tar::Header::new_gnu();
851 file.set_mode(0o400);
852 file.set_size(jit_config.0.len() as u64);
853 tar.append_data(&mut file, path, jit_config.0.as_bytes())?;
854 tar.into_inner()
855}
856
857#[cfg(test)]
858mod tests;