Run non-interactive sessions through production coordination
This commit is contained in:
parent
308c5a1b56
commit
71b0266cf2
1 changed files with 3770 additions and 44 deletions
|
|
@ -1,16 +1,19 @@
|
|||
use std::collections::BTreeMap;
|
||||
use std::ffi::{OsStr, OsString};
|
||||
use std::fmt;
|
||||
use std::fs::{File, OpenOptions};
|
||||
use std::io::{self, Read, Seek, SeekFrom, Write};
|
||||
use std::os::fd::AsFd;
|
||||
use std::os::fd::{AsFd, AsRawFd};
|
||||
use std::os::unix::ffi::OsStringExt;
|
||||
use std::os::unix::fs::{FileTypeExt, MetadataExt, PermissionsExt};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::{Arc, Mutex, MutexGuard};
|
||||
|
||||
use ed25519_dalek::SigningKey;
|
||||
use lazaret_core::codec::{CodecError, decode, encode};
|
||||
use lazaret_core::configuration::ConfinedPathPlan;
|
||||
use lazaret_core::identity::{
|
||||
CertificateDigest, ClientId, SessionAnchorId, SessionId, TrustManifestDigest,
|
||||
CertificateDigest, ClientId, ContainerId, SessionAnchorId, SessionId, TrustManifestDigest,
|
||||
};
|
||||
use lazaret_core::protocol::{
|
||||
ProtocolValidationError, issue_protocol_credential, issue_session_capability,
|
||||
|
|
@ -18,10 +21,11 @@ use lazaret_core::protocol::{
|
|||
};
|
||||
use lazaret_core::records::{
|
||||
ClientEnrollmentRequest, ClientEnrollmentResponse, ControlRequest, ControlResponse,
|
||||
DurableDecision, GenerationIdentity, ProtocolCredential, ProtocolCredentialRole,
|
||||
DurableDecision, GenerationIdentity, NonInteractiveSessionMessage,
|
||||
NonInteractiveSessionRequest, ProtocolCredential, ProtocolCredentialRole,
|
||||
ProtocolPublicationIdentity, ProtocolSubjectId, ProtocolVersion, SandboxCredentialBundle,
|
||||
Session, SessionBinding, SessionPhase, SessionRegistrationRequest, SessionRegistrationResponse,
|
||||
TlsCredential, TlsRole, TlsSubjectId, TrustManifest,
|
||||
Session, SessionBinding, SessionExit, SessionPhase, SessionRegistrationRequest,
|
||||
SessionRegistrationResponse, TlsCredential, TlsRole, TlsSubjectId, TrustManifest,
|
||||
};
|
||||
use rcgen::{
|
||||
BasicConstraints, CertificateParams, CertificateSigningRequestParams, CertifiedIssuer,
|
||||
|
|
@ -42,14 +46,16 @@ use crate::domain_store::{DomainStoreError, DomainStoreMutation, HostCoordinator
|
|||
use crate::lifecycle::{
|
||||
HostCoordinatorLifecycle, LifecycleAction, SessionAdmissionIdentity, SessionLifecycleAdapter,
|
||||
};
|
||||
use lazaret_linux::process::{ProcessExit, TargetCommand, child_broker};
|
||||
use lazaret_runtime_supervisor::sandbox::RunscCommand;
|
||||
|
||||
pub const CONTROL_ALPN: &[u8] = b"lazaret-control/1";
|
||||
pub const CONTROL_SERVER_NAME: &str = "coordinator.lazaret.invalid";
|
||||
pub const MAX_ENROLLMENT_BYTES: usize = 8_192;
|
||||
const MAX_CONTROL_BYTES: usize = 65_536;
|
||||
const CONTROL_SOCKET_NAME: &str = "control.sock";
|
||||
const CONTROL_SOCKET_NAME: &str = "c.sock";
|
||||
const PUBLICATION_IDENTITY_NAME: &str = "protocol-identity.postcard";
|
||||
const STAGED_CONTROL_SOCKET_NAME: &str = ".control.sock.staged";
|
||||
const STAGED_CONTROL_SOCKET_NAME: &str = ".c.staged";
|
||||
const STAGED_PUBLICATION_IDENTITY_NAME: &str = ".protocol-identity.staged";
|
||||
|
||||
#[derive(Debug)]
|
||||
|
|
@ -824,6 +830,167 @@ pub struct PublishedControlFace {
|
|||
anchor: TrustAnchor,
|
||||
server_config: Arc<ServerConfig>,
|
||||
registry: Arc<SessionRegistry>,
|
||||
executor: Arc<dyn NonInteractiveSessionExecutor>,
|
||||
}
|
||||
|
||||
pub trait NonInteractiveSessionExecutor: Send + Sync {
|
||||
/// # Errors
|
||||
/// Returns a runtime, stream, or exact-session execution failure.
|
||||
fn execute(
|
||||
&self,
|
||||
request: &NonInteractiveSessionRequest,
|
||||
emit: &mut dyn FnMut(NonInteractiveSessionMessage) -> Result<(), ControlError>,
|
||||
) -> Result<SessionExit, ControlError>;
|
||||
}
|
||||
|
||||
struct ExecutionUnavailable;
|
||||
|
||||
impl NonInteractiveSessionExecutor for ExecutionUnavailable {
|
||||
fn execute(
|
||||
&self,
|
||||
_request: &NonInteractiveSessionRequest,
|
||||
_emit: &mut dyn FnMut(NonInteractiveSessionMessage) -> Result<(), ControlError>,
|
||||
) -> Result<SessionExit, ControlError> {
|
||||
Err(ControlError::InvalidRequest)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RunscNonInteractiveExecutor {
|
||||
program: OsString,
|
||||
global_arguments: Vec<OsString>,
|
||||
container: OsString,
|
||||
runner: Arc<dyn NonInteractiveCommandRunner>,
|
||||
}
|
||||
|
||||
trait NonInteractiveCommandRunner: Send + Sync {
|
||||
fn run(
|
||||
&self,
|
||||
target: &TargetCommand,
|
||||
session_id: SessionId,
|
||||
emit: &mut dyn FnMut(NonInteractiveSessionMessage) -> Result<(), ControlError>,
|
||||
) -> Result<ProcessExit, ControlError>;
|
||||
}
|
||||
|
||||
struct ChildBrokerCommandRunner;
|
||||
|
||||
impl NonInteractiveCommandRunner for ChildBrokerCommandRunner {
|
||||
fn run(
|
||||
&self,
|
||||
target: &TargetCommand,
|
||||
session_id: SessionId,
|
||||
emit: &mut dyn FnMut(NonInteractiveSessionMessage) -> Result<(), ControlError>,
|
||||
) -> Result<ProcessExit, ControlError> {
|
||||
let mut child = child_broker().run_transient_captured(target)?;
|
||||
let mut stdout_done = false;
|
||||
let mut stderr_done = false;
|
||||
while !(stdout_done && stderr_done && child.process().has_exited()?) {
|
||||
let (stdout, stderr) = child.streams();
|
||||
stdout_done |= emit_available(stdout, session_id, true, emit)?;
|
||||
stderr_done |= emit_available(stderr, session_id, false, emit)?;
|
||||
if !(stdout_done && stderr_done) {
|
||||
std::thread::yield_now();
|
||||
}
|
||||
}
|
||||
child_broker()
|
||||
.reap_status(child.into_process())
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
impl RunscNonInteractiveExecutor {
|
||||
/// Derives the exact `runsc exec` prefix from the generation's attached `runsc run` command.
|
||||
///
|
||||
/// # Errors
|
||||
/// Refuses a non-runsc command or a command without one exact run/container suffix.
|
||||
pub fn from_run_command(
|
||||
command: &RunscCommand,
|
||||
container_id: ContainerId,
|
||||
) -> Result<Self, ControlError> {
|
||||
if command.program() != OsStr::new("runsc") {
|
||||
return Err(ControlError::InvalidRequest);
|
||||
}
|
||||
let run = command
|
||||
.arguments()
|
||||
.iter()
|
||||
.position(|argument| argument == OsStr::new("run"))
|
||||
.ok_or(ControlError::InvalidRequest)?;
|
||||
if run + 3 != command.arguments().len()
|
||||
|| !command.arguments()[run + 1]
|
||||
.as_encoded_bytes()
|
||||
.starts_with(b"-bundle=")
|
||||
{
|
||||
return Err(ControlError::InvalidRequest);
|
||||
}
|
||||
let container = OsString::from(hex_lower(container_id.as_bytes()));
|
||||
if command.arguments()[run + 2] != container {
|
||||
return Err(ControlError::InvalidRequest);
|
||||
}
|
||||
Ok(Self {
|
||||
program: command.program().to_owned(),
|
||||
global_arguments: command.arguments()[..run].to_vec(),
|
||||
container,
|
||||
runner: Arc::new(ChildBrokerCommandRunner),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl NonInteractiveSessionExecutor for RunscNonInteractiveExecutor {
|
||||
fn execute(
|
||||
&self,
|
||||
request: &NonInteractiveSessionRequest,
|
||||
emit: &mut dyn FnMut(NonInteractiveSessionMessage) -> Result<(), ControlError>,
|
||||
) -> Result<SessionExit, ControlError> {
|
||||
let mut target = TargetCommand::new(self.program.clone());
|
||||
for argument in &self.global_arguments {
|
||||
target = target.argument(argument.clone());
|
||||
}
|
||||
target = target.argument("exec").argument(self.container.clone());
|
||||
for argument in &request.argv {
|
||||
target = target.argument(OsString::from_vec(argument.clone()));
|
||||
}
|
||||
let process_status = self.runner.run(&target, request.session_id, emit)?;
|
||||
match (process_status.exit_code, process_status.terminating_signal) {
|
||||
(Some(code), None) => Ok(SessionExit::exited(request.session_id, code)),
|
||||
(None, Some(_)) => Ok(SessionExit::terminated(request.session_id)),
|
||||
_ => Err(ControlError::InvalidRequest),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn emit_available(
|
||||
stream: &mut File,
|
||||
session_id: SessionId,
|
||||
stdout: bool,
|
||||
emit: &mut dyn FnMut(NonInteractiveSessionMessage) -> Result<(), ControlError>,
|
||||
) -> Result<bool, ControlError> {
|
||||
let mut bytes = [0_u8; 16 * 1024];
|
||||
loop {
|
||||
match stream.read(&mut bytes) {
|
||||
Ok(0) => return Ok(true),
|
||||
Ok(length) => {
|
||||
let output = lazaret_core::records::NonInteractiveOutput {
|
||||
session_id,
|
||||
bytes: bytes[..length].to_vec(),
|
||||
};
|
||||
emit(if stdout {
|
||||
NonInteractiveSessionMessage::Stdout(output)
|
||||
} else {
|
||||
NonInteractiveSessionMessage::Stderr(output)
|
||||
})?;
|
||||
}
|
||||
Err(error) if error.kind() == io::ErrorKind::WouldBlock => return Ok(false),
|
||||
Err(error) => return Err(error.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn hex_lower(bytes: &[u8]) -> String {
|
||||
use fmt::Write as _;
|
||||
|
||||
bytes.iter().fold(String::new(), |mut output, byte| {
|
||||
write!(output, "{byte:02x}").expect("writing to a String cannot fail");
|
||||
output
|
||||
})
|
||||
}
|
||||
|
||||
pub struct StagedControlFace {
|
||||
|
|
@ -838,6 +1005,7 @@ pub struct StagedControlFace {
|
|||
anchor: Option<TrustAnchor>,
|
||||
server_config: Arc<ServerConfig>,
|
||||
registry: Arc<SessionRegistry>,
|
||||
executor: Arc<dyn NonInteractiveSessionExecutor>,
|
||||
}
|
||||
|
||||
impl StagedControlFace {
|
||||
|
|
@ -850,6 +1018,26 @@ impl StagedControlFace {
|
|||
uid: u32,
|
||||
anchor: TrustAnchor,
|
||||
registry: Arc<SessionRegistry>,
|
||||
) -> Result<Self, ControlError> {
|
||||
Self::stage_with_executor(
|
||||
runtime_domain_root,
|
||||
uid,
|
||||
anchor,
|
||||
registry,
|
||||
Arc::new(ExecutionUnavailable),
|
||||
)
|
||||
}
|
||||
|
||||
/// Stages a control face with its generation-bound non-interactive executor.
|
||||
///
|
||||
/// # Errors
|
||||
/// Refuses the same invalid staging conditions as [`Self::stage`].
|
||||
pub fn stage_with_executor(
|
||||
runtime_domain_root: &Path,
|
||||
uid: u32,
|
||||
anchor: TrustAnchor,
|
||||
registry: Arc<SessionRegistry>,
|
||||
executor: Arc<dyn NonInteractiveSessionExecutor>,
|
||||
) -> Result<Self, ControlError> {
|
||||
if registry.generation_identity != anchor.generation_identity {
|
||||
return Err(ControlError::StaleGeneration);
|
||||
|
|
@ -902,6 +1090,7 @@ impl StagedControlFace {
|
|||
anchor: Some(anchor),
|
||||
server_config,
|
||||
registry,
|
||||
executor,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -959,6 +1148,7 @@ impl StagedControlFace {
|
|||
))?,
|
||||
server_config: Arc::clone(&self.server_config),
|
||||
registry: Arc::clone(&self.registry),
|
||||
executor: Arc::clone(&self.executor),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -971,6 +1161,128 @@ impl Drop for StagedControlFace {
|
|||
}
|
||||
|
||||
impl PublishedControlFace {
|
||||
/// Selects blocking or polling admission for the publication listener.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an operating-system failure changing the listener mode.
|
||||
pub fn set_nonblocking(&self, nonblocking: bool) -> Result<(), ControlError> {
|
||||
self.listener.set_nonblocking(nonblocking)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Discovers one publication through the retained runtime Domain authority.
|
||||
///
|
||||
/// # Errors
|
||||
/// Refuses changed root authority or absent, replaced, non-private, oversized, or malformed
|
||||
/// publication objects.
|
||||
pub fn discover_confined(
|
||||
runtime_domain_root: &ConfinedPathPlan,
|
||||
uid: u32,
|
||||
) -> Result<(File, PathBuf, ProtocolPublicationIdentity), ControlError> {
|
||||
let root = File::from(
|
||||
runtime_domain_root
|
||||
.open_authority()
|
||||
.map_err(|_| ControlError::InvalidPath("runtime Domain authority changed"))?,
|
||||
);
|
||||
let root_metadata = root.metadata()?;
|
||||
if !root_metadata.is_dir()
|
||||
|| root_metadata.uid() != uid
|
||||
|| root_metadata.mode() & 0o7777 != 0o700
|
||||
{
|
||||
return Err(ControlError::InvalidPath(
|
||||
"runtime Domain root is not invoking-user-owned mode 0700",
|
||||
));
|
||||
}
|
||||
let descriptor = openat(
|
||||
root.as_fd(),
|
||||
PUBLICATION_IDENTITY_NAME,
|
||||
OFlags::RDONLY | OFlags::CLOEXEC | OFlags::NOFOLLOW,
|
||||
Mode::empty(),
|
||||
)
|
||||
.map_err(io::Error::from)?;
|
||||
let mut identity = File::from(descriptor);
|
||||
let identity_metadata = identity.metadata()?;
|
||||
if !identity_metadata.file_type().is_file()
|
||||
|| identity_metadata.uid() != uid
|
||||
|| identity_metadata.mode() & 0o7777 != 0o600
|
||||
|| identity_metadata.nlink() != 1
|
||||
|| identity_metadata.len() > MAX_CONTROL_BYTES as u64
|
||||
{
|
||||
return Err(ControlError::InvalidPublication(
|
||||
"publication identity is not an exact private regular file",
|
||||
));
|
||||
}
|
||||
let socket = File::from(
|
||||
openat(
|
||||
root.as_fd(),
|
||||
CONTROL_SOCKET_NAME,
|
||||
OFlags::PATH | OFlags::CLOEXEC | OFlags::NOFOLLOW,
|
||||
Mode::empty(),
|
||||
)
|
||||
.map_err(io::Error::from)?,
|
||||
);
|
||||
let socket_metadata = socket.metadata()?;
|
||||
if !socket_metadata.file_type().is_socket()
|
||||
|| socket_metadata.uid() != uid
|
||||
|| socket_metadata.mode() & 0o7777 != 0o600
|
||||
{
|
||||
return Err(ControlError::InvalidPublication(
|
||||
"control socket is not an invoking-user-private socket",
|
||||
));
|
||||
}
|
||||
let mut bytes = Vec::new();
|
||||
identity.read_to_end(&mut bytes)?;
|
||||
let socket_path = PathBuf::from(format!(
|
||||
"/proc/self/fd/{}/{}",
|
||||
root.as_raw_fd(),
|
||||
CONTROL_SOCKET_NAME
|
||||
));
|
||||
Ok((root, socket_path, decode(&bytes)?))
|
||||
}
|
||||
|
||||
/// Discovers one exact current control publication beneath a validated runtime Domain root.
|
||||
///
|
||||
/// # Errors
|
||||
/// Refuses absent, replaced, non-private, oversized, or malformed publication objects.
|
||||
pub fn discover(
|
||||
runtime_domain_root: &Path,
|
||||
uid: u32,
|
||||
) -> Result<(PathBuf, ProtocolPublicationIdentity), ControlError> {
|
||||
let paths = ControlPaths::open(runtime_domain_root, uid)?;
|
||||
let descriptor = openat(
|
||||
paths.root_file.as_fd(),
|
||||
PUBLICATION_IDENTITY_NAME,
|
||||
OFlags::RDONLY | OFlags::CLOEXEC | OFlags::NOFOLLOW,
|
||||
Mode::empty(),
|
||||
)
|
||||
.map_err(io::Error::from)?;
|
||||
let mut identity = File::from(descriptor);
|
||||
let descriptor_metadata = identity.metadata()?;
|
||||
let path_metadata = std::fs::symlink_metadata(&paths.identity)?;
|
||||
let expected = FileIdentity::from_metadata(&descriptor_metadata);
|
||||
if FileIdentity::from_metadata(&path_metadata) != expected
|
||||
|| !descriptor_metadata.file_type().is_file()
|
||||
|| descriptor_metadata.uid() != uid
|
||||
|| descriptor_metadata.mode() & 0o7777 != 0o600
|
||||
|| descriptor_metadata.nlink() != 1
|
||||
|| descriptor_metadata.len() > MAX_CONTROL_BYTES as u64
|
||||
{
|
||||
return Err(ControlError::InvalidPublication(
|
||||
"publication identity is not an exact private regular file",
|
||||
));
|
||||
}
|
||||
let socket = std::fs::symlink_metadata(&paths.socket)?;
|
||||
if !socket.file_type().is_socket() || socket.uid() != uid || socket.mode() & 0o7777 != 0o600
|
||||
{
|
||||
return Err(ControlError::InvalidPublication(
|
||||
"control socket is not an invoking-user-private socket",
|
||||
));
|
||||
}
|
||||
let mut bytes = Vec::new();
|
||||
identity.read_to_end(&mut bytes)?;
|
||||
Ok((paths.socket, decode(&bytes)?))
|
||||
}
|
||||
|
||||
/// Publishes one generation control face in the fixed host-private runtime path.
|
||||
///
|
||||
/// # Errors
|
||||
|
|
@ -981,12 +1293,39 @@ impl PublishedControlFace {
|
|||
uid: u32,
|
||||
anchor: TrustAnchor,
|
||||
store: Arc<HostCoordinatorDomainStore>,
|
||||
) -> Result<Self, ControlError> {
|
||||
Self::publish_with_executor(
|
||||
runtime_domain_root,
|
||||
uid,
|
||||
anchor,
|
||||
store,
|
||||
Arc::new(ExecutionUnavailable),
|
||||
)
|
||||
}
|
||||
|
||||
/// Publishes one generation control face with non-interactive session execution.
|
||||
///
|
||||
/// # Errors
|
||||
/// Refuses the same invalid publication conditions as [`Self::publish`].
|
||||
pub fn publish_with_executor(
|
||||
runtime_domain_root: &Path,
|
||||
uid: u32,
|
||||
anchor: TrustAnchor,
|
||||
store: Arc<HostCoordinatorDomainStore>,
|
||||
executor: Arc<dyn NonInteractiveSessionExecutor>,
|
||||
) -> Result<Self, ControlError> {
|
||||
let registry = Arc::new(SessionRegistry::new(
|
||||
store,
|
||||
anchor.generation_identity.clone(),
|
||||
));
|
||||
StagedControlFace::stage(runtime_domain_root, uid, anchor, registry)?.expose()
|
||||
StagedControlFace::stage_with_executor(
|
||||
runtime_domain_root,
|
||||
uid,
|
||||
anchor,
|
||||
registry,
|
||||
executor,
|
||||
)?
|
||||
.expose()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
|
|
@ -1132,13 +1471,44 @@ impl PublishedControlFace {
|
|||
session_capability: capability,
|
||||
}),
|
||||
)?;
|
||||
let mut extra = [0_u8; 1];
|
||||
match stream.read(&mut extra) {
|
||||
Ok(0) => Ok(()),
|
||||
Ok(_) => Err(ControlError::InvalidRequest),
|
||||
Err(error) if error.kind() == io::ErrorKind::UnexpectedEof => Ok(()),
|
||||
Err(error) => Err(error.into()),
|
||||
let request: ControlRequest = match read_frame(stream, MAX_CONTROL_BYTES) {
|
||||
Ok(request) => request,
|
||||
Err(ControlError::Io(error))
|
||||
if matches!(
|
||||
error.kind(),
|
||||
io::ErrorKind::UnexpectedEof | io::ErrorKind::ConnectionReset
|
||||
) =>
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
let ControlRequest::Execute(request) = request else {
|
||||
return Err(ControlError::InvalidRequest);
|
||||
};
|
||||
if request.session_id != session_id
|
||||
|| request.argv.is_empty()
|
||||
|| request.argv[0].is_empty()
|
||||
|| request.argv.iter().any(|argument| argument.contains(&0))
|
||||
{
|
||||
return Err(ControlError::InvalidRequest);
|
||||
}
|
||||
let mut send_message = |message: NonInteractiveSessionMessage| {
|
||||
if matches!(message, NonInteractiveSessionMessage::Exit(_)) {
|
||||
return Err(ControlError::InvalidRequest);
|
||||
}
|
||||
require_non_interactive_session(&message, request.session_id)?;
|
||||
write_frame(stream, &message)
|
||||
};
|
||||
let outcome = self.executor.execute(&request, &mut send_message)?;
|
||||
if outcome.session_id() != request.session_id {
|
||||
return Err(ControlError::InvalidRequest);
|
||||
}
|
||||
require_non_interactive_session(
|
||||
&NonInteractiveSessionMessage::Exit(outcome),
|
||||
request.session_id,
|
||||
)?;
|
||||
write_frame(stream, &NonInteractiveSessionMessage::Exit(outcome))
|
||||
})();
|
||||
let disconnected = disconnect(session_id);
|
||||
result.and(disconnected)
|
||||
|
|
@ -1301,6 +1671,22 @@ fn remove_if_identity_matches(path: &Path, expected: FileIdentity) {
|
|||
}
|
||||
}
|
||||
|
||||
fn require_non_interactive_session(
|
||||
message: &NonInteractiveSessionMessage,
|
||||
expected: SessionId,
|
||||
) -> Result<(), ControlError> {
|
||||
let actual = match message {
|
||||
NonInteractiveSessionMessage::Stdout(output)
|
||||
| NonInteractiveSessionMessage::Stderr(output) => output.session_id,
|
||||
NonInteractiveSessionMessage::Exit(exit) => exit.session_id(),
|
||||
};
|
||||
if actual == expected {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(ControlError::InvalidRequest)
|
||||
}
|
||||
}
|
||||
|
||||
fn leaf_parameters(
|
||||
common_name: &str,
|
||||
usage: ExtendedKeyUsagePurpose,
|
||||
|
|
@ -1406,6 +1792,7 @@ fn write_frame<Record: serde::Serialize>(
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::ffi::OsString;
|
||||
use std::sync::Arc;
|
||||
use std::thread;
|
||||
|
||||
|
|
@ -1483,6 +1870,64 @@ mod tests {
|
|||
));
|
||||
}
|
||||
|
||||
struct RecordingRunner {
|
||||
observed: Mutex<Option<(OsString, Vec<OsString>)>>,
|
||||
exit: ProcessExit,
|
||||
}
|
||||
|
||||
impl NonInteractiveCommandRunner for RecordingRunner {
|
||||
fn run(
|
||||
&self,
|
||||
target: &TargetCommand,
|
||||
_session_id: SessionId,
|
||||
_emit: &mut dyn FnMut(NonInteractiveSessionMessage) -> Result<(), ControlError>,
|
||||
) -> Result<ProcessExit, ControlError> {
|
||||
*self.observed.lock().unwrap() =
|
||||
Some((target.program().to_owned(), target.arguments().to_vec()));
|
||||
Ok(self.exit)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runsc_exec_uses_the_command_as_the_first_post_container_argument() {
|
||||
let runner = Arc::new(RecordingRunner {
|
||||
observed: Mutex::new(None),
|
||||
exit: ProcessExit {
|
||||
exit_code: Some(23),
|
||||
terminating_signal: None,
|
||||
},
|
||||
});
|
||||
let executor = RunscNonInteractiveExecutor {
|
||||
program: "runsc".into(),
|
||||
global_arguments: vec!["--root=/runtime".into()],
|
||||
container: "container-id".into(),
|
||||
runner: runner.clone(),
|
||||
};
|
||||
let session_id = SessionId::new([9; 32]);
|
||||
let exit = executor
|
||||
.execute(
|
||||
&NonInteractiveSessionRequest {
|
||||
protocol_version: ProtocolVersion::V1,
|
||||
session_id,
|
||||
argv: vec![b"printf".to_vec(), b"hello".to_vec()],
|
||||
},
|
||||
&mut |_| Ok(()),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(exit.exit_code(), Some(23));
|
||||
assert_eq!(
|
||||
runner.observed.lock().unwrap().as_ref().unwrap(),
|
||||
&(
|
||||
OsString::from("runsc"),
|
||||
["--root=/runtime", "exec", "container-id", "printf", "hello"]
|
||||
.into_iter()
|
||||
.map(OsString::from)
|
||||
.collect()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rustls_refuses_client_leaf_with_server_role() {
|
||||
let anchor = TrustAnchor::create(generation()).unwrap();
|
||||
|
|
|
|||
Loading…
Reference in a new issue