specification and initial launching
This commit is contained in:
parent
565c1567c4
commit
969bf3a9ee
115
Cargo.lock
generated
115
Cargo.lock
generated
@ -46,14 +46,34 @@ version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "3.0.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b63edc3f163b3c71ec8aa23f9bd6070f77edbf3d1d198b164afa90ff00e4ec62"
|
||||
dependencies = [
|
||||
"atty",
|
||||
"bitflags",
|
||||
"indexmap",
|
||||
"os_str_bytes",
|
||||
"strsim",
|
||||
"termcolor",
|
||||
"textwrap",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clone-shim"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"env_logger",
|
||||
"exitcode",
|
||||
"ipnetwork",
|
||||
"libc",
|
||||
"log",
|
||||
"nix",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
@ -70,6 +90,18 @@ dependencies = [
|
||||
"termcolor",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "exitcode"
|
||||
version = "1.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "de853764b47027c2e862a995c34978ffa63c1501f2e15f987ba11bd4f9bba193"
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.11.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e"
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.1.19"
|
||||
@ -85,6 +117,31 @@ version = "2.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "1.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "282a6247722caba404c065016bbfa522806e51714c34f5dfc3e4a3a46fcb4223"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ipnetwork"
|
||||
version = "0.18.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4088d739b183546b239688ddbc79891831df421773df95e236daf7867866d355"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.117"
|
||||
@ -128,6 +185,15 @@ dependencies = [
|
||||
"memoffset",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "os_str_bytes"
|
||||
version = "6.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e22443d1643a904602595ba1cd8f7d896afe56d26712531c5ff73a15b2fbf64"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.36"
|
||||
@ -163,6 +229,49 @@ version = "0.6.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b"
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.136"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce31e24b01e1e524df96f1c2fdd054405f8d7376249a5110886fb4b658484789"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.136"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08597e7152fcd306f41838ed3e37be9eaeed2b61c42e2117266a554fab4662f9"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.79"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e8d9fa5c3b304765ce1fd9c4c8a3de2c8db365a5b91be52f186efc675681d95"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"ryu",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.86"
|
||||
@ -183,6 +292,12 @@ dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "textwrap"
|
||||
version = "0.14.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0066c8d12af8b5acd21e00547c3797fde4e8677254a7ee429176ccebbe93dd80"
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.30"
|
||||
|
@ -9,6 +9,12 @@ edition = "2021"
|
||||
log = "0.4"
|
||||
env_logger = "0.9"
|
||||
thiserror = "1"
|
||||
clap = "3"
|
||||
exitcode = "1"
|
||||
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
ipnetwork = "0.18"
|
||||
|
||||
libc = "0.2.117"
|
||||
nix = "0.23.1"
|
||||
|
18
build.rs
Normal file
18
build.rs
Normal file
@ -0,0 +1,18 @@
|
||||
use std::process::Command;
|
||||
|
||||
fn main() {
|
||||
let output = Command::new("git")
|
||||
.args(&["rev-parse", "HEAD"])
|
||||
.output()
|
||||
.unwrap();
|
||||
let mut git_hash = String::from_utf8(output.stdout).unwrap();
|
||||
git_hash.truncate(16);
|
||||
let clean_status = Command::new("git")
|
||||
.args(&["diff", "--exit-code"])
|
||||
.status()
|
||||
.unwrap();
|
||||
if !clean_status.success() {
|
||||
git_hash.push_str("-dirty");
|
||||
}
|
||||
println!("cargo:rustc-env=GIT_HASH={}", git_hash);
|
||||
}
|
20
src/error.rs
20
src/error.rs
@ -1,7 +1,27 @@
|
||||
use std::io;
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum Error {
|
||||
#[error("{msg}: {src}")]
|
||||
Nix { msg: &'static str, src: nix::Error },
|
||||
|
||||
#[error("io: {0}")]
|
||||
Io(#[from] io::Error),
|
||||
|
||||
#[error("json: {0}")]
|
||||
Json(#[from] serde_json::Error),
|
||||
|
||||
#[error("bad specification type: only .json files are supported")]
|
||||
BadSpecType,
|
||||
|
||||
#[error("too many pipes: a pipe must have one reader and one writer: {0}")]
|
||||
TooManyPipes(String),
|
||||
|
||||
#[error("read only pipe: a pipe must have one reader and one writer: {0}")]
|
||||
ReadOnlyPipe(String),
|
||||
|
||||
#[error("write only pipe: a pipe must have one reader and one writer: {0}")]
|
||||
WriteOnlyPipe(String),
|
||||
}
|
||||
|
140
src/main.rs
140
src/main.rs
@ -1,28 +1,142 @@
|
||||
use log::info;
|
||||
use log::{debug, error, info};
|
||||
|
||||
mod clone;
|
||||
mod error;
|
||||
mod specification;
|
||||
|
||||
use clone::{clone3, CloneArgs, CloneFlags};
|
||||
use error::Error;
|
||||
use specification::{Arg, Entrypoint, Pipe, Specification, Trigger};
|
||||
|
||||
use nix::unistd::Pid;
|
||||
use std::collections::HashMap;
|
||||
use std::ffi::CString;
|
||||
use std::fs::File;
|
||||
use std::os::unix::io::{AsRawFd, FromRawFd};
|
||||
|
||||
fn main() -> Result<(), Error> {
|
||||
let env = env_logger::Env::new().filter_or("LOG", "info");
|
||||
use clap::{App, AppSettings};
|
||||
use nix::unistd::{self, Pid};
|
||||
|
||||
fn main() {
|
||||
std::process::exit(match run() {
|
||||
Ok(_) => {
|
||||
info!("launched successfully");
|
||||
exitcode::OK
|
||||
}
|
||||
Err(e) => {
|
||||
error!("error: {}", e);
|
||||
-1
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn run() -> Result<(), Error> {
|
||||
// process arguments
|
||||
let matches = App::new("clone-shim")
|
||||
.version(env!("GIT_HASH"))
|
||||
.author("Jake Hillion <jake@hillion.co.uk>")
|
||||
.about("Launch a multi entrypoint app, cloning as requested by an external specification or the ELF.")
|
||||
.arg(clap::Arg::new("spec").long("specification").short('s').help("Provide the specification as an external JSON file.").takes_value(true))
|
||||
.setting(AppSettings::TrailingVarArg)
|
||||
.arg(clap::Arg::new("verbose").long("verbose").short('v').help("Use verbose logging.").takes_value(false))
|
||||
.arg(clap::Arg::new("binary").index(1).help("Binary and arguments to launch with the shim").required(true).multiple_values(true))
|
||||
.get_matches();
|
||||
|
||||
let (binary, trailing) = {
|
||||
let mut argv = matches.values_of("binary").unwrap();
|
||||
|
||||
let binary = argv.next().unwrap();
|
||||
let trailing: Vec<&str> = argv.collect();
|
||||
|
||||
(binary, trailing)
|
||||
};
|
||||
|
||||
// setup logging
|
||||
let env = env_logger::Env::new().filter_or(
|
||||
"LOG",
|
||||
if matches.is_present("verbose") {
|
||||
"debug"
|
||||
} else {
|
||||
"warn"
|
||||
},
|
||||
);
|
||||
env_logger::init_from_env(env);
|
||||
|
||||
info!("getting started");
|
||||
|
||||
if clone3(CloneArgs::new(CloneFlags::empty())).map_err(|e| Error::Nix {
|
||||
msg: "clone3",
|
||||
src: e,
|
||||
})? != Pid::from_raw(0)
|
||||
{
|
||||
info!("hello from the child");
|
||||
// parse the specification
|
||||
let spec: Specification = if let Some(m) = matches.value_of("spec") {
|
||||
if m.ends_with(".json") {
|
||||
let f = std::fs::File::open(m)?;
|
||||
Ok(serde_json::from_reader(f)?)
|
||||
} else {
|
||||
Err(Error::BadSpecType)
|
||||
}
|
||||
} else {
|
||||
info!("hello from the parent");
|
||||
unimplemented!("reading spec from the elf is unimplemented")
|
||||
}?;
|
||||
|
||||
debug!("specification read: {:?}", &spec);
|
||||
spec.validate()?;
|
||||
|
||||
// create all the pipes
|
||||
let (pipes, _) = spec.pipes();
|
||||
let mut read_pipes = HashMap::new();
|
||||
let mut write_pipes = HashMap::new();
|
||||
|
||||
for pipe in pipes {
|
||||
info!("creating pipe pair `{}`", pipe);
|
||||
|
||||
let (read, write) = unistd::pipe().map_err(|e| Error::Nix {
|
||||
msg: "pipe",
|
||||
src: e,
|
||||
})?;
|
||||
|
||||
// safe to create files given the successful return of pipe(2)
|
||||
read_pipes.insert(pipe.to_string(), unsafe { File::from_raw_fd(read) });
|
||||
write_pipes.insert(pipe.to_string(), unsafe { File::from_raw_fd(write) });
|
||||
}
|
||||
|
||||
// spawn all processes
|
||||
for (name, entry) in &spec.entrypoints {
|
||||
info!("spawning entrypoint `{}`", name.as_str());
|
||||
|
||||
match &entry.trigger {
|
||||
Trigger::Startup => {
|
||||
if clone3(CloneArgs::new(CloneFlags::empty())).map_err(|e| Error::Nix {
|
||||
msg: "clone3",
|
||||
src: e,
|
||||
})? == Pid::from_raw(0)
|
||||
{
|
||||
let mut args = Vec::new();
|
||||
for arg in &entry.args {
|
||||
match arg {
|
||||
Arg::BinaryName => args.push(CString::new(binary).unwrap()),
|
||||
Arg::Entrypoint => args.push(CString::new(name.as_str()).unwrap()),
|
||||
Arg::Pipe(p) => args.push(match p {
|
||||
Pipe::Rx(s) => {
|
||||
CString::new(read_pipes[s].as_raw_fd().to_string()).unwrap()
|
||||
}
|
||||
Pipe::Tx(s) => {
|
||||
CString::new(write_pipes[s].as_raw_fd().to_string()).unwrap()
|
||||
}
|
||||
}),
|
||||
Arg::Trailing => {
|
||||
args.extend(trailing.iter().map(|s| CString::new(*s).unwrap()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unistd::execv(&CString::new(binary).unwrap(), &args).map_err(|e| {
|
||||
Error::Nix {
|
||||
msg: "execv",
|
||||
src: e,
|
||||
}
|
||||
})?;
|
||||
}
|
||||
}
|
||||
Trigger::Pipe(s) => {}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn pipe_trigger(pipe: File, entry: &Entrypoint) {}
|
||||
|
129
src/specification.rs
Normal file
129
src/specification.rs
Normal file
@ -0,0 +1,129 @@
|
||||
use crate::Error;
|
||||
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::path::PathBuf;
|
||||
|
||||
use ipnetwork::{Ipv4Network, Ipv6Network};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct Specification {
|
||||
pub entrypoints: HashMap<String, Entrypoint>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct Entrypoint {
|
||||
pub trigger: Trigger,
|
||||
pub args: Vec<Arg>,
|
||||
pub permissions: HashSet<Permissions>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum Trigger {
|
||||
Startup,
|
||||
Pipe(String),
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum Arg {
|
||||
/// The binary name, or argv[0], of the original program start
|
||||
BinaryName,
|
||||
|
||||
/// The name of this entrypoint
|
||||
Entrypoint,
|
||||
|
||||
/// A chosen end of a named pipe
|
||||
Pipe(Pipe),
|
||||
|
||||
/// The rest of argv[1..], 0 or more arguments
|
||||
Trailing,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum Pipe {
|
||||
Rx(String),
|
||||
Tx(String),
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, PartialEq, Eq, Hash, Debug)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum Permissions {
|
||||
Filesystem {
|
||||
host_path: PathBuf,
|
||||
final_path: PathBuf,
|
||||
},
|
||||
Network {
|
||||
network: Network,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, PartialEq, Eq, Hash, Debug)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum Network {
|
||||
InternetV4,
|
||||
InternetV6,
|
||||
PrivateV4(Ipv4Network),
|
||||
PrivateV6(Ipv6Network),
|
||||
}
|
||||
|
||||
impl Specification {
|
||||
pub fn pipes(&self) -> (Vec<&str>, Vec<&str>) {
|
||||
let mut read = Vec::new();
|
||||
let mut write = Vec::new();
|
||||
|
||||
for (_, entry) in &self.entrypoints {
|
||||
match &entry.trigger {
|
||||
Trigger::Startup => {}
|
||||
Trigger::Pipe(s) => read.push(s.as_str()),
|
||||
}
|
||||
|
||||
for arg in &entry.args {
|
||||
match arg {
|
||||
Arg::BinaryName => {}
|
||||
Arg::Entrypoint => {}
|
||||
Arg::Pipe(p) => match p {
|
||||
Pipe::Rx(s) => read.push(s.as_str()),
|
||||
Pipe::Tx(s) => write.push(s.as_str()),
|
||||
},
|
||||
Arg::Trailing => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(read, write)
|
||||
}
|
||||
|
||||
pub fn validate(&self) -> Result<(), Error> {
|
||||
// validate pipes match
|
||||
let (read, write) = self.pipes();
|
||||
let mut read_set = HashSet::with_capacity(read.len());
|
||||
|
||||
for pipe in read {
|
||||
if read_set.insert(pipe) {
|
||||
return Err(Error::TooManyPipes(pipe.to_string()));
|
||||
}
|
||||
}
|
||||
|
||||
let mut write_set = HashSet::with_capacity(write.len());
|
||||
for pipe in write {
|
||||
if write_set.insert(pipe) {
|
||||
return Err(Error::TooManyPipes(pipe.to_string()));
|
||||
}
|
||||
}
|
||||
|
||||
for pipe in read_set {
|
||||
if !write_set.remove(pipe) {
|
||||
return Err(Error::ReadOnlyPipe(pipe.to_string()));
|
||||
}
|
||||
}
|
||||
|
||||
for pipe in write_set {
|
||||
return Err(Error::WriteOnlyPipe(pipe.to_string()));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user