use std::collections::HashMap; use std::iter::{empty, once}; use std::path::{Path, PathBuf}; use std::sync::Arc; use buildkit_proto::pb::{ self, op::Op, ExecOp, Input, MountType, NetMode, OpMetadata, SecurityMode, }; use either::Either; use super::context::Context; use super::mount::Mount; use crate::ops::{MultiBorrowedOutput, MultiOwnedOutput, OperationBuilder}; use crate::serialization::{Context as SerializationCtx, Node, Operation, OperationId, Result}; use crate::utils::{OperationOutput, OutputIdx}; /// Command execution operation. This is what a Dockerfile's `RUN` directive is translated to. #[derive(Debug, Clone)] pub struct Command<'a> { id: OperationId, context: Context, root_mount: Option>, other_mounts: Vec>, description: HashMap, caps: HashMap, ignore_cache: bool, } impl<'a> Command<'a> { pub fn run(name: S) -> Self where S: Into, { Self { id: OperationId::default(), context: Context::new(name), root_mount: None, other_mounts: vec![], description: Default::default(), caps: Default::default(), ignore_cache: false, } } pub fn args(mut self, args: A) -> Self where A: IntoIterator, S: AsRef, { self.context.args = args.into_iter().map(|item| item.as_ref().into()).collect(); self } pub fn env(mut self, name: S, value: Q) -> Self where S: AsRef, Q: AsRef, { let env = format!("{}={}", name.as_ref(), value.as_ref()); self.context.env.push(env); self } pub fn env_iter(mut self, iter: I) -> Self where I: IntoIterator, S: AsRef, Q: AsRef, { for (name, value) in iter.into_iter() { let env = format!("{}={}", name.as_ref(), value.as_ref()); self.context.env.push(env); } self } pub fn cwd

(mut self, path: P) -> Self where P: Into, { self.context.cwd = path.into(); self } pub fn user(mut self, user: S) -> Self where S: Into, { self.context.user = user.into(); self } pub fn mount

(mut self, mount: Mount<'a, P>) -> Self where P: AsRef, { match mount { Mount::Layer(..) | Mount::ReadOnlyLayer(..) | Mount::Scratch(..) => { self.caps.insert("exec.mount.bind".into(), true); } Mount::ReadOnlySelector(..) => { self.caps.insert("exec.mount.bind".into(), true); self.caps.insert("exec.mount.selector".into(), true); } Mount::SharedCache(..) => { self.caps.insert("exec.mount.cache".into(), true); self.caps.insert("exec.mount.cache.sharing".into(), true); } Mount::OptionalSshAgent(..) => { self.caps.insert("exec.mount.ssh".into(), true); } } if mount.is_root() { self.root_mount = Some(mount.into_owned()); } else { self.other_mounts.push(mount.into_owned()); } self } } impl<'a, 'b: 'a> MultiBorrowedOutput<'b> for Command<'b> { fn output(&'b self, index: u32) -> OperationOutput<'b> { // TODO: check if the requested index available. OperationOutput::borrowed(self, OutputIdx(index)) } } impl<'a> MultiOwnedOutput<'a> for Arc> { fn output(&self, index: u32) -> OperationOutput<'a> { // TODO: check if the requested index available. OperationOutput::owned(self.clone(), OutputIdx(index)) } } impl<'a> OperationBuilder<'a> for Command<'a> { fn custom_name(mut self, name: S) -> Self where S: Into, { self.description .insert("llb.customname".into(), name.into()); self } fn ignore_cache(mut self, ignore: bool) -> Self { self.ignore_cache = ignore; self } } impl<'a> Operation for Command<'a> { fn id(&self) -> &OperationId { &self.id } fn serialize(&self, cx: &mut SerializationCtx) -> Result { let (inputs, mounts): (Vec<_>, Vec<_>) = { let mut last_input_index = 0; self.root_mount .as_ref() .into_iter() .chain(self.other_mounts.iter()) .map(|mount| { let inner_mount = match mount { Mount::ReadOnlyLayer(_, destination) => pb::Mount { input: last_input_index, dest: destination.to_string_lossy().into(), output: -1, readonly: true, mount_type: MountType::Bind as i32, ..Default::default() }, Mount::ReadOnlySelector(_, destination, source) => pb::Mount { input: last_input_index, dest: destination.to_string_lossy().into(), output: -1, readonly: true, selector: source.to_string_lossy().into(), mount_type: MountType::Bind as i32, ..Default::default() }, Mount::Layer(output, _, path) => pb::Mount { input: last_input_index, dest: path.to_string_lossy().into(), output: output.into(), mount_type: MountType::Bind as i32, ..Default::default() }, Mount::Scratch(output, path) => { let mount = pb::Mount { input: -1, dest: path.to_string_lossy().into(), output: output.into(), mount_type: MountType::Bind as i32, ..Default::default() }; return (Either::Right(empty()), mount); } Mount::SharedCache(path) => { use buildkit_proto::pb::{CacheOpt, CacheSharingOpt}; let mount = pb::Mount { input: -1, dest: path.to_string_lossy().into(), output: -1, mount_type: MountType::Cache as i32, cache_opt: Some(CacheOpt { id: path.display().to_string(), sharing: CacheSharingOpt::Shared as i32, }), ..Default::default() }; return (Either::Right(empty()), mount); } Mount::OptionalSshAgent(path) => { use buildkit_proto::pb::SshOpt; let mount = pb::Mount { input: -1, dest: path.to_string_lossy().into(), output: -1, mount_type: MountType::Ssh as i32, ssh_opt: Some(SshOpt { mode: 0o600, optional: true, ..Default::default() }), ..Default::default() }; return (Either::Right(empty()), mount); } }; let input = match mount { Mount::ReadOnlyLayer(input, ..) => input, Mount::ReadOnlySelector(input, ..) => input, Mount::Layer(_, input, ..) => input, Mount::SharedCache(..) => { unreachable!(); } Mount::Scratch(..) => { unreachable!(); } Mount::OptionalSshAgent(..) => { unreachable!(); } }; let serialized = cx.register(input.operation()).unwrap(); let input = Input { digest: serialized.digest.clone(), index: input.output().into(), }; last_input_index += 1; (Either::Left(once(input)), inner_mount) }) .unzip() }; let head = pb::Op { op: Some(Op::Exec(ExecOp { mounts, network: NetMode::Unset.into(), security: SecurityMode::Sandbox.into(), meta: Some(self.context.clone().into()), })), inputs: inputs.into_iter().flatten().collect(), ..Default::default() }; let metadata = OpMetadata { description: self.description.clone(), caps: self.caps.clone(), ignore_cache: self.ignore_cache, ..Default::default() }; Ok(Node::new(head, metadata)) } }