Skip to main content

mj_controller/targets/
bootstrap.rs

1use super::*;
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4pub enum ExecutionBoundary<'a> {
5    Direct,
6    Container {
7        engine: &'a str,
8        container_id: &'a str,
9    },
10    Ssh(&'a SshTarget),
11    SshContainer {
12        engine: &'a str,
13        ssh: &'a SshTarget,
14        container_id: &'a str,
15    },
16}
17
18#[derive(Debug, Clone, PartialEq, Eq)]
19pub struct HarnessProbe<'a> {
20    pub executable: &'a str,
21    pub version_args: &'a [&'a str],
22    pub bridge_executable: Option<&'a str>,
23}
24
25/// Compatibility is intentionally interpreted by the controller. A successful
26/// probe permits an image-baked tool to be reused; a missing/incompatible tool
27/// causes the controller to upload/install its release-owned copy.
28pub fn bootstrap_probe_plan(
29    boundary: ExecutionBoundary<'_>,
30    harness: HarnessProbe<'_>,
31) -> Result<CommandPlan> {
32    validate_executable(harness.executable)?;
33    let mut commands = vec![
34        at_boundary(
35            boundary,
36            std::iter::once(harness.executable)
37                .chain(harness.version_args.iter().copied())
38                .map(str::to_owned)
39                .collect(),
40        )
41        .purpose("probe harness version"),
42    ];
43    if let Some(bridge) = harness.bridge_executable {
44        validate_executable(bridge)?;
45        commands.push(
46            at_boundary(boundary, vec![bridge.to_owned(), "--version".to_owned()])
47                .purpose("probe ACP bridge version"),
48        );
49    }
50    commands.push(
51        at_boundary(boundary, vec!["git".to_owned(), "--version".to_owned()]).purpose("probe Git"),
52    );
53    Ok(CommandPlan {
54        description: "probe reusable target tools".to_owned(),
55        commands,
56    })
57}
58
59/// Thin Linux Git bootstrap. Managed containers also receive GitHub CLI and
60/// its HTTPS credential helper so an injected `GH_TOKEN` works before clone.
61pub fn install_git_plan(boundary: ExecutionBoundary<'_>) -> CommandPlan {
62    let managed_container = matches!(
63        boundary,
64        ExecutionBoundary::Container { .. } | ExecutionBoundary::SshContainer { .. }
65    );
66    let script = if managed_container {
67        "set -eu; if ! command -v git >/dev/null 2>&1 || ! command -v gh >/dev/null 2>&1; then SUDO=''; if [ \"$(id -u)\" != 0 ]; then command -v sudo >/dev/null 2>&1 && sudo -n true || { echo 'Git and GitHub CLI installation requires root or passwordless sudo' >&2; exit 1; }; SUDO='sudo -n'; fi; if command -v apt-get >/dev/null 2>&1; then $SUDO apt-get update; $SUDO apt-get install -y git gh ca-certificates curl; elif command -v dnf >/dev/null 2>&1; then $SUDO dnf install -y git gh ca-certificates curl; elif command -v yum >/dev/null 2>&1; then $SUDO yum install -y git gh ca-certificates curl; elif command -v apk >/dev/null 2>&1; then $SUDO apk add --no-cache git github-cli ca-certificates curl; else echo 'Unsupported package manager; install Git and GitHub CLI in the image' >&2; exit 1; fi; fi; git config --global credential.https://github.com.helper '!gh auth git-credential'; git config --global credential.https://gist.github.com.helper '!gh auth git-credential'"
68    } else {
69        "set -eu; if command -v git >/dev/null 2>&1; then exit 0; fi; SUDO=''; if [ \"$(id -u)\" != 0 ]; then command -v sudo >/dev/null 2>&1 && sudo -n true || { echo 'Git installation requires root or passwordless sudo' >&2; exit 1; }; SUDO='sudo -n'; fi; if command -v apt-get >/dev/null 2>&1; then $SUDO apt-get update; $SUDO apt-get install -y git ca-certificates curl; elif command -v dnf >/dev/null 2>&1; then $SUDO dnf install -y git ca-certificates curl; elif command -v yum >/dev/null 2>&1; then $SUDO yum install -y git ca-certificates curl; elif command -v apk >/dev/null 2>&1; then $SUDO apk add --no-cache git ca-certificates curl; else echo 'Unsupported package manager; install Git manually' >&2; exit 1; fi"
70    };
71    CommandPlan {
72        description: "install missing Git".to_owned(),
73        commands: vec![
74            at_boundary(
75                boundary,
76                vec!["sh".to_owned(), "-c".to_owned(), script.to_owned()],
77            )
78            .purpose("install Git")
79            .stage(ProvisionStage::Cloning),
80        ],
81    }
82}
83
84/// Shared [`CommandSpec::parallel_group`] marker for one bundle's per-repository
85/// clone/init commands. Every `clone_commands` call builds its own
86/// [`CommandPlan`], so a single fixed marker never mixes batches across plans.
87pub(super) const BUNDLE_REPOSITORIES_PARALLEL_GROUP: u32 = 1;
88
89pub(super) fn clone_commands(
90    bundle: &ProjectBundleSpec,
91    workspace: &str,
92    wrap: impl Fn(Vec<String>) -> CommandSpec,
93) -> Vec<CommandSpec> {
94    let mut commands = vec![
95        wrap(vec![
96            "mkdir".to_owned(),
97            "-p".to_owned(),
98            workspace.to_owned(),
99        ])
100        .purpose("create bundle workspace")
101        .stage(ProvisionStage::Cloning),
102    ];
103    for repository in &bundle.repositories {
104        let destination = format!("{workspace}/{}", repository.destination);
105        let url = repository
106            .url
107            .as_ref()
108            .expect("validated network repository");
109        let mut args = vec!["git".to_owned(), "clone".to_owned()];
110        for push_url in &repository.push_urls {
111            args.extend([
112                "--config".into(),
113                format!("remote.origin.pushurl={push_url}"),
114            ]);
115        }
116        if let Some(reference) = &repository.reference {
117            args.extend(["--reference-if-able".to_owned(), reference.clone()]);
118        }
119        args.push("--".to_owned());
120        args.push(url.clone());
121        args.push(destination);
122        commands.push(
123            wrap(args)
124                .purpose(format!("clone {}", repository.destination))
125                .stage(ProvisionStage::Cloning)
126                .parallel_group(BUNDLE_REPOSITORIES_PARALLEL_GROUP),
127        );
128    }
129    commands
130}