osdk_core/container/
runtime.rs1use std::io;
4use std::process::ExitStatus;
5
6use super::redact::{CommandPurpose, NativeProgram, RedactedCommand};
7use super::report::{DiagnosticReport, RuntimeKind};
8use crate::process::{CaptureLimits, CommandOutcome, CommandRunner, CommandSpec};
9
10pub trait RuntimeAdapter: Send + Sync {
13 fn kind(&self) -> RuntimeKind;
14
15 fn diagnose(&self, runner: &dyn CommandRunner, limits: CaptureLimits) -> DiagnosticReport;
16}
17
18#[derive(Debug)]
24pub struct ProbeCommand {
25 command: CommandSpec,
26 evidence: RedactedCommand,
27}
28
29impl ProbeCommand {
30 pub fn new(program: NativeProgram, purpose: CommandPurpose, command: CommandSpec) -> Self {
31 let evidence = RedactedCommand::from_spec(program, purpose, &command);
32 Self { command, evidence }
33 }
34
35 pub fn evidence(&self) -> &RedactedCommand {
36 &self.evidence
37 }
38
39 pub fn execute(&self, runner: &dyn CommandRunner, limits: CaptureLimits) -> CommandOutcome {
40 runner.run_captured(&self.command, limits)
41 }
42}
43
44#[derive(Debug)]
49pub struct ForegroundCommand {
50 command: CommandSpec,
51 evidence: RedactedCommand,
52}
53
54impl ForegroundCommand {
55 pub fn new(program: NativeProgram, purpose: CommandPurpose, command: CommandSpec) -> Self {
56 let evidence = RedactedCommand::from_spec(program, purpose, &command);
57 Self { command, evidence }
58 }
59
60 pub fn evidence(&self) -> &RedactedCommand {
61 &self.evidence
62 }
63
64 pub fn execute(self, runner: &dyn CommandRunner) -> io::Result<ExitStatus> {
65 runner.run_foreground(&self.command)
66 }
67}
68
69#[cfg(test)]
70mod tests {
71 use std::sync::atomic::{AtomicUsize, Ordering};
72
73 use super::*;
74 use crate::process::CapturedOutput;
75
76 #[derive(Default)]
77 struct FakeRunner {
78 captured_calls: AtomicUsize,
79 foreground_calls: AtomicUsize,
80 }
81
82 impl CommandRunner for FakeRunner {
83 fn run_captured(&self, _command: &CommandSpec, _limits: CaptureLimits) -> CommandOutcome {
84 self.captured_calls.fetch_add(1, Ordering::Relaxed);
85 CommandOutcome::TimedOut {
86 output: CapturedOutput::default(),
87 termination: crate::process::TerminationStatus::Requested,
88 }
89 }
90
91 fn run_foreground(&self, _command: &CommandSpec) -> io::Result<ExitStatus> {
92 self.foreground_calls.fetch_add(1, Ordering::Relaxed);
93 Err(io::Error::other("synthetic foreground failure"))
94 }
95 }
96
97 #[test]
98 fn probe_uses_injected_runner_and_exposes_only_redacted_evidence() {
99 let runner = FakeRunner::default();
100 let probe = ProbeCommand::new(
101 NativeProgram::Docker,
102 CommandPurpose::ContextInspect,
103 CommandSpec::new("docker").args(["context", "inspect", "secret-context"]),
104 );
105
106 assert!(matches!(
107 probe.execute(&runner, CaptureLimits::default()),
108 CommandOutcome::TimedOut { .. }
109 ));
110 assert_eq!(runner.captured_calls.load(Ordering::Relaxed), 1);
111 let json = serde_json::to_string(probe.evidence()).unwrap();
112 assert!(!json.contains("secret-context"));
113 assert!(json.contains("context-inspect"));
114 }
115
116 #[test]
117 fn foreground_delegation_invokes_the_runner_exactly_once() {
118 let runner = FakeRunner::default();
119 let operation = ForegroundCommand::new(
120 NativeProgram::Docker,
121 CommandPurpose::Pull,
122 CommandSpec::new("docker").args(["pull", "registry.example/secret"]),
123 );
124
125 assert!(operation.execute(&runner).is_err());
126 assert_eq!(runner.foreground_calls.load(Ordering::Relaxed), 1);
127 }
128}