| 19 |
19 |
|
use crate::state::ExecutorMap;
|
| 20 |
20 |
|
use anyhow::{Context as _, Result};
|
| 21 |
21 |
|
use ops_core::live_log::LiveLog;
|
| 22 |
|
- |
use ops_core::remote::RemoteHost;
|
| 23 |
|
- |
use ops_exec::{Action, ObserveKind, Step as OpStep};
|
|
22 |
+ |
use ops_exec::{Action, Executor, ObserveKind, Step as OpStep, SyncOpts};
|
| 24 |
23 |
|
use rhai::{Engine, EvalAltResult, Map};
|
| 25 |
24 |
|
use sqlx::SqlitePool;
|
| 26 |
|
- |
use std::collections::HashMap;
|
| 27 |
25 |
|
use std::path::{Path, PathBuf};
|
| 28 |
26 |
|
use std::sync::atomic::{AtomicBool, Ordering};
|
| 29 |
27 |
|
use std::sync::{Arc, Mutex};
|
| 80 |
78 |
|
/// through these — the transport (local / ssh / in-session agent) and the
|
| 81 |
79 |
|
/// capability gate are the executor's, not the engine's.
|
| 82 |
80 |
|
pub execs: Arc<ExecutorMap>,
|
| 83 |
|
- |
/// Host name -> ssh string, used only by `collect` to name the remote scp
|
| 84 |
|
- |
/// source for a daemon-local artifact pull. Not an execution path.
|
| 85 |
|
- |
pub host_ssh: HashMap<String, String>,
|
|
81 |
+ |
/// Sync transport per build host, used only by `collect` to pull artifacts
|
|
82 |
+ |
/// back. Never the agent, even for an agent host — see `state::build_sync`.
|
|
83 |
+ |
/// Not an execution path.
|
|
84 |
+ |
pub syncs: Arc<ExecutorMap>,
|
| 86 |
85 |
|
pub pool: SqlitePool,
|
| 87 |
86 |
|
pub events: EventTx,
|
| 88 |
87 |
|
pub cfg: Arc<Config>,
|
| 114 |
113 |
|
repo: String,
|
| 115 |
114 |
|
target_run_id: i64,
|
| 116 |
115 |
|
execs: Arc<ExecutorMap>,
|
| 117 |
|
- |
host_ssh: HashMap<String, String>,
|
|
116 |
+ |
syncs: Arc<ExecutorMap>,
|
| 118 |
117 |
|
pool: SqlitePool,
|
| 119 |
118 |
|
events: EventTx,
|
| 120 |
119 |
|
cfg: Arc<Config>,
|
| 130 |
129 |
|
repo,
|
| 131 |
130 |
|
target_run_id,
|
| 132 |
131 |
|
execs,
|
| 133 |
|
- |
host_ssh,
|
|
132 |
+ |
syncs,
|
| 134 |
133 |
|
pool,
|
| 135 |
134 |
|
events,
|
| 136 |
135 |
|
cfg,
|
| 310 |
309 |
|
}
|
| 311 |
310 |
|
|
| 312 |
311 |
|
/// The ssh string for `name` (for `collect`'s remote scp source).
|
| 313 |
|
- |
fn host_ssh(&self, name: &str) -> Result<String> {
|
| 314 |
|
- |
self.host_ssh
|
|
312 |
+ |
/// The transport that moves artifacts off `name`. Distinct from
|
|
313 |
+ |
/// [`RecipeCtx::exec_for`]'s executor: an agent host signs over `AgentRpc`
|
|
314 |
+ |
/// but is collected from over ssh (`state::build_sync`).
|
|
315 |
+ |
fn host_sync(&self, name: &str) -> Result<Arc<dyn Executor>> {
|
|
316 |
+ |
self.syncs
|
| 315 |
317 |
|
.get(name)
|
| 316 |
318 |
|
.cloned()
|
| 317 |
319 |
|
.ok_or_else(|| anyhow::anyhow!("unknown build host `{name}` (not in topology)"))
|
| 610 |
612 |
|
fn collect(self: &Arc<Self>, host: &str, glob: &str, app: &str, version: &str) -> Result<()> {
|
| 611 |
613 |
|
let dest = self.cfg.dist_root.join(app).join(version);
|
| 612 |
614 |
|
let dest_s = dest.to_string_lossy().into_owned();
|
| 613 |
|
- |
let ssh = self.host_ssh(host)?;
|
| 614 |
|
- |
let is_local = ssh == "local" || ssh.is_empty();
|
| 615 |
|
- |
// Both branches run the transfer through `sh -c`, and both need the
|
| 616 |
|
- |
// wildcard (`*?[]`) to reach the shell live — so the glob is left
|
| 617 |
|
- |
// UNQUOTED in both and validated against shell metacharacters instead.
|
| 618 |
|
- |
// (Quoting it on the local side, as before, neutered the wildcard so
|
| 619 |
|
- |
// `cp '*.dmg'` matched nothing.) Path/wildcard chars are allowed; command
|
| 620 |
|
- |
// metacharacters are not.
|
|
615 |
+ |
// The glob reaches a remote login shell intact (that's what expands it),
|
|
616 |
+ |
// so command metacharacters stay barred. Path/wildcard chars are fine.
|
|
617 |
+ |
// Not a privilege boundary — a recipe already runs arbitrary shell via
|
|
618 |
+ |
// `sh_ok` — but it keeps a malformed pattern from becoming a command.
|
| 621 |
619 |
|
anyhow::ensure!(
|
| 622 |
620 |
|
!glob.chars().any(|c| matches!(c, ';' | '&' | '|' | '$' | '`' | '\'' | '"' | '\\' | ' ' | '\n' | '(' | ')' | '<' | '>')),
|
| 623 |
621 |
|
"collect glob `{glob}` contains shell metacharacters"
|
| 624 |
622 |
|
);
|
| 625 |
|
- |
let d = ops_core::remote::sh_quote(&dest_s);
|
| 626 |
|
- |
let cmd = if is_local {
|
| 627 |
|
- |
format!("mkdir -p {d} && cp -vR {glob} {d}/")
|
| 628 |
|
- |
} else {
|
| 629 |
|
- |
format!(
|
| 630 |
|
- |
"mkdir -p {d} && scp -r {flags} {tgt}:{glob} {d}/",
|
| 631 |
|
- |
flags = ops_core::remote::SSH_FLAGS.join(" "),
|
| 632 |
|
- |
tgt = ops_core::remote::sh_quote(&ssh),
|
| 633 |
|
- |
)
|
| 634 |
|
- |
};
|
| 635 |
|
- |
// The daemon always runs the transfer itself (local cp or local scp),
|
| 636 |
|
- |
// regardless of which host built the artifact.
|
| 637 |
|
- |
let sink = self.ensure_step()?;
|
| 638 |
|
- |
let local = RemoteHost::new("local");
|
| 639 |
|
- |
let out = self.rt.block_on(async move { local.run_streaming(&cmd, sink).await })?;
|
| 640 |
|
- |
if !out.success() {
|
| 641 |
|
- |
anyhow::bail!("collect failed (exit {:?})", out.status.code());
|
| 642 |
|
- |
}
|
|
623 |
+ |
std::fs::create_dir_all(&dest)
|
|
624 |
+ |
.with_context(|| format!("creating collect dest {dest_s}"))?;
|
|
625 |
+ |
// The SYNC transport, not the host's exec executor: artifacts move over
|
|
626 |
+ |
// ssh/rsync even from an agent host, whose `/pull` is confined to a
|
|
627 |
+ |
// narrow `pull_root` that deliberately excludes the repo checkout these
|
|
628 |
+ |
// artifacts are built in (see `state::build_sync`). The daemon still
|
|
629 |
+ |
// runs the transfer itself, as it always has.
|
|
630 |
+ |
let sync = self.host_sync(host)?;
|
|
631 |
+ |
let opts = SyncOpts::precompressed();
|
|
632 |
+ |
self.rt
|
|
633 |
+ |
.block_on(async { sync.pull_glob(glob, &dest, &opts).await })
|
|
634 |
+ |
.with_context(|| format!("collect {glob} from `{host}`"))?;
|
| 643 |
635 |
|
// Best-effort size accounting for the event.
|
| 644 |
636 |
|
events::emit(
|
| 645 |
637 |
|
&self.events,
|
| 963 |
955 |
|
"fw13".into(),
|
| 964 |
956 |
|
"/tmp".into(),
|
| 965 |
957 |
|
1,
|
| 966 |
|
- |
Arc::new(HashMap::new()),
|
| 967 |
|
- |
HashMap::new(),
|
|
958 |
+ |
Arc::new(std::collections::HashMap::new()),
|
|
959 |
+ |
Arc::new(std::collections::HashMap::new()),
|
| 968 |
960 |
|
pool,
|
| 969 |
961 |
|
crate::events::channel(),
|
| 970 |
962 |
|
cfg,
|