You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
320 lines
9.7 KiB
Rust
320 lines
9.7 KiB
Rust
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<Mount<'a, PathBuf>>,
|
|
other_mounts: Vec<Mount<'a, PathBuf>>,
|
|
|
|
description: HashMap<String, String>,
|
|
caps: HashMap<String, bool>,
|
|
ignore_cache: bool,
|
|
}
|
|
|
|
impl<'a> Command<'a> {
|
|
pub fn run<S>(name: S) -> Self
|
|
where
|
|
S: Into<String>,
|
|
{
|
|
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<A, S>(mut self, args: A) -> Self
|
|
where
|
|
A: IntoIterator<Item = S>,
|
|
S: AsRef<str>,
|
|
{
|
|
self.context.args = args.into_iter().map(|item| item.as_ref().into()).collect();
|
|
self
|
|
}
|
|
|
|
pub fn env<S, Q>(mut self, name: S, value: Q) -> Self
|
|
where
|
|
S: AsRef<str>,
|
|
Q: AsRef<str>,
|
|
{
|
|
let env = format!("{}={}", name.as_ref(), value.as_ref());
|
|
|
|
self.context.env.push(env);
|
|
self
|
|
}
|
|
|
|
pub fn env_iter<I, S, Q>(mut self, iter: I) -> Self
|
|
where
|
|
I: IntoIterator<Item = (S, Q)>,
|
|
S: AsRef<str>,
|
|
Q: AsRef<str>,
|
|
{
|
|
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<P>(mut self, path: P) -> Self
|
|
where
|
|
P: Into<PathBuf>,
|
|
{
|
|
self.context.cwd = path.into();
|
|
self
|
|
}
|
|
|
|
pub fn user<S>(mut self, user: S) -> Self
|
|
where
|
|
S: Into<String>,
|
|
{
|
|
self.context.user = user.into();
|
|
self
|
|
}
|
|
|
|
pub fn mount<P>(mut self, mount: Mount<'a, P>) -> Self
|
|
where
|
|
P: AsRef<Path>,
|
|
{
|
|
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<Command<'a>> {
|
|
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<S>(mut self, name: S) -> Self
|
|
where
|
|
S: Into<String>,
|
|
{
|
|
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<Node> {
|
|
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))
|
|
}
|
|
}
|