Skip to content

Commit c2407a9

Browse files
committed
Fix subsets inheriting service chaining req from whole app
Signed-off-by: itowlson <[email protected]>
1 parent 1abed0c commit c2407a9

File tree

3 files changed

+116
-32
lines changed

3 files changed

+116
-32
lines changed

crates/factor-outbound-networking/src/config.rs

+40-3
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ use std::ops::Range;
22

33
use anyhow::{bail, ensure, Context};
44
use spin_factors::{App, AppComponent};
5-
use spin_locked_app::MetadataKey;
5+
use spin_locked_app::{
6+
locked::{LockedApp, LockedComponent},
7+
MetadataKey,
8+
};
69

710
const ALLOWED_HOSTS_KEY: MetadataKey<Vec<String>> = MetadataKey::new("allowed_outbound_hosts");
811
const ALLOWED_HTTP_KEY: MetadataKey<Vec<String>> = MetadataKey::new("allowed_http_hosts");
@@ -14,8 +17,15 @@ pub const SERVICE_CHAINING_DOMAIN_SUFFIX: &str = ".spin.internal";
1417
///
1518
/// This has support for converting the old `allowed_http_hosts` key to the new `allowed_outbound_hosts` key.
1619
pub fn allowed_outbound_hosts(component: &AppComponent) -> anyhow::Result<Vec<String>> {
20+
allowed_outbound_hosts_locked(component.locked)
21+
}
22+
23+
fn allowed_outbound_hosts_locked(component: &LockedComponent) -> anyhow::Result<Vec<String>> {
24+
use spin_locked_app::MetadataExt;
25+
1726
let mut allowed_hosts = component
18-
.get_metadata(ALLOWED_HOSTS_KEY)
27+
.metadata
28+
.get_typed(ALLOWED_HOSTS_KEY)
1929
.with_context(|| {
2030
format!(
2131
"locked app metadata was malformed for key {}",
@@ -24,7 +34,8 @@ pub fn allowed_outbound_hosts(component: &AppComponent) -> anyhow::Result<Vec<St
2434
})?
2535
.unwrap_or_default();
2636
let allowed_http = component
27-
.get_metadata(ALLOWED_HTTP_KEY)
37+
.metadata
38+
.get_typed(ALLOWED_HTTP_KEY)
2839
.map(|h| h.unwrap_or_default())
2940
.unwrap_or_default();
3041
let converted =
@@ -74,6 +85,32 @@ pub fn validate_service_chaining_for_components(
7485
Ok(())
7586
}
7687

88+
/// If, after we have retained only a subset of components in an app, the remaining
89+
/// ones no longer require service chaining, remove the host requirement from the app.
90+
pub fn update_service_chaining_host_requirement(app: &mut LockedApp) {
91+
use spin_locked_app::locked::SERVICE_CHAINING_KEY;
92+
93+
if !app.host_requirements.contains_key(SERVICE_CHAINING_KEY) {
94+
// The app doesn't use service chaining so save checking.
95+
return;
96+
}
97+
98+
fn uses_service_chaining(c: &LockedComponent) -> bool {
99+
let Ok(allowed_outbound_hosts) = allowed_outbound_hosts_locked(c) else {
100+
return true; // err on the side of keeping the previously inferred requirement
101+
};
102+
allowed_outbound_hosts
103+
.iter()
104+
.filter_map(|h| h.parse().ok()) // don't consider templated hosts
105+
.any(|h| parse_service_chaining_target(&h).is_some())
106+
}
107+
108+
let app_uses_service_chaining = app.components.iter().any(uses_service_chaining);
109+
if !app_uses_service_chaining {
110+
app.host_requirements.remove(SERVICE_CHAINING_KEY);
111+
}
112+
}
113+
77114
/// An address is a url-like string that contains a host, a port, and an optional scheme
78115
#[derive(Eq, Debug, Clone)]
79116
pub struct AllowedHostConfig {

crates/factor-outbound-networking/src/lib.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ use std::{collections::HashMap, sync::Arc};
1616

1717
pub use config::{
1818
allowed_outbound_hosts, is_service_chaining_host, parse_service_chaining_target,
19-
validate_service_chaining_for_components, AllowedHostConfig, AllowedHostsConfig, HostConfig,
20-
OutboundUrl, SERVICE_CHAINING_DOMAIN_SUFFIX,
19+
update_service_chaining_host_requirement, validate_service_chaining_for_components,
20+
AllowedHostConfig, AllowedHostsConfig, HostConfig, OutboundUrl, SERVICE_CHAINING_DOMAIN_SUFFIX,
2121
};
2222

2323
pub use runtime_config::ComponentTlsConfigs;

src/commands/up.rs

+74-27
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ use clap::{CommandFactory, Parser};
1313
use reqwest::Url;
1414
use spin_app::locked::LockedApp;
1515
use spin_common::ui::quoted_path;
16-
use spin_factor_outbound_networking::validate_service_chaining_for_components;
16+
use spin_factor_outbound_networking::{
17+
update_service_chaining_host_requirement, validate_service_chaining_for_components,
18+
};
1719
use spin_loader::FilesMountStrategy;
1820
use spin_oci::OciLoader;
1921
use spin_trigger::cli::{LaunchMetadata, SPIN_LOCAL_APP_DIR, SPIN_LOCKED_URL, SPIN_WORKING_DIR};
@@ -184,7 +186,7 @@ impl UpCommand {
184186
return Ok(());
185187
}
186188
for cmd in trigger_cmds {
187-
let mut help_process = self.start_trigger(cmd.clone(), None, &[]).await?;
189+
let mut help_process = self.start_trigger(cmd, None, &[]).await?;
188190
_ = help_process.wait().await;
189191
}
190192
return Ok(());
@@ -213,6 +215,8 @@ impl UpCommand {
213215
)?;
214216
}
215217

218+
self.update_locked_app(&mut locked_app);
219+
216220
let trigger_types: HashSet<&str> = locked_app
217221
.triggers
218222
.iter()
@@ -225,13 +229,10 @@ impl UpCommand {
225229
.with_context(|| format!("Couldn't find trigger executor for {app_source}"))?;
226230
let is_multi = trigger_cmds.len() > 1;
227231

228-
self.update_locked_app(&mut locked_app);
229-
let locked_url = self.write_locked_app(&locked_app, &working_dir).await?;
230-
231232
let local_app_dir = app_source.local_app_dir().map(Into::into);
232233

233234
let run_opts = RunTriggerOpts {
234-
locked_url,
235+
locked_app: locked_app.clone(),
235236
working_dir,
236237
local_app_dir,
237238
};
@@ -284,26 +285,26 @@ impl UpCommand {
284285

285286
async fn get_trigger_launch_metas(
286287
&self,
287-
trigger_cmds: &[Vec<String>],
288+
trigger_cmds: &[TriggerCommand<'_>],
288289
) -> anyhow::Result<HashMap<Vec<String>, LaunchMetadata>> {
289290
let mut metas = HashMap::new();
290291

291-
for trigger_cmd in trigger_cmds {
292+
for t in trigger_cmds {
292293
let mut meta_cmd = tokio::process::Command::new(std::env::current_exe().unwrap());
293-
meta_cmd.args(trigger_cmd);
294+
meta_cmd.args(&t.cmd);
294295
meta_cmd.arg("--launch-metadata-only");
295296
meta_cmd.stdout(Stdio::piped()).stderr(Stdio::piped());
296297
let meta_out = meta_cmd.spawn()?.wait_with_output().await?;
297298
let meta = serde_json::from_slice::<LaunchMetadata>(&meta_out.stderr)?;
298-
metas.insert(trigger_cmd.clone(), meta);
299+
metas.insert(t.cmd.clone(), meta);
299300
}
300301

301302
Ok(metas)
302303
}
303304

304305
async fn start_trigger_processes(
305306
self,
306-
trigger_cmds: Vec<Vec<String>>,
307+
trigger_cmds: Vec<TriggerCommand<'_>>,
307308
run_opts: RunTriggerOpts,
308309
) -> anyhow::Result<Vec<tokio::process::Child>> {
309310
let is_multi = trigger_cmds.len() > 1;
@@ -333,13 +334,13 @@ impl UpCommand {
333334
let mut trigger_processes = Vec::with_capacity(trigger_cmds.len());
334335

335336
for cmd in trigger_cmds {
336-
let meta = trigger_metas.as_ref().and_then(|ms| ms.get(&cmd));
337+
let meta = trigger_metas.as_ref().and_then(|ms| ms.get(&cmd.cmd));
337338
let trigger_args = match meta {
338339
Some(m) => m.matches(&trigger_args),
339340
None => self.trigger_args.iter().collect(),
340341
};
341342
let child = self
342-
.start_trigger(cmd.clone(), Some(run_opts.clone()), &trigger_args)
343+
.start_trigger(cmd, Some(run_opts.clone()), &trigger_args)
343344
.await
344345
.context("Failed to start trigger process")?;
345346
trigger_processes.push(child);
@@ -357,21 +358,25 @@ impl UpCommand {
357358

358359
async fn start_trigger(
359360
&self,
360-
trigger_cmd: Vec<String>,
361+
trigger: TriggerCommand<'_>,
361362
opts: Option<RunTriggerOpts>,
362363
trigger_args: &[&OsString],
363364
) -> Result<tokio::process::Child, anyhow::Error> {
364365
// The docs for `current_exe` warn that this may be insecure because it could be executed
365366
// via hard-link. I think it should be fine as long as we aren't `setuid`ing this binary.
366367
let mut cmd = tokio::process::Command::new(std::env::current_exe().unwrap());
367-
cmd.args(&trigger_cmd);
368+
cmd.args(&trigger.cmd);
368369

369370
if let Some(RunTriggerOpts {
370-
locked_url,
371+
locked_app,
371372
working_dir,
372373
local_app_dir,
373374
}) = opts
374375
{
376+
let locked_url = self
377+
.write_locked_app_for_trigger(trigger.type_, &locked_app, &working_dir)
378+
.await?;
379+
375380
cmd.env(SPIN_LOCKED_URL, locked_url)
376381
.env(SPIN_WORKING_DIR, &working_dir)
377382
.args(trigger_args);
@@ -430,19 +435,33 @@ impl UpCommand {
430435
!self.trigger_args.is_empty() && !self.trigger_args[0].to_string_lossy().starts_with('-')
431436
}
432437

433-
async fn write_locked_app(
438+
async fn write_locked_app_for_trigger(
434439
&self,
440+
trigger_type: &str,
435441
locked_app: &LockedApp,
436442
working_dir: &Path,
437443
) -> Result<String, anyhow::Error> {
438-
let locked_path = working_dir.join("spin.lock");
444+
let locked_app_for_trigger = get_locked_app_for_trigger(trigger_type, locked_app)?;
445+
let locked_path = working_dir.join(format!("spin-{trigger_type}.lock"));
446+
Self::write_locked_app(&locked_app_for_trigger, &locked_path).await
447+
}
448+
449+
async fn write_locked_app(
450+
locked_app: &LockedApp,
451+
lock_file_path: &Path,
452+
) -> Result<String, anyhow::Error> {
439453
let locked_app_contents =
440454
serde_json::to_vec_pretty(&locked_app).context("failed to serialize locked app")?;
441-
tokio::fs::write(&locked_path, locked_app_contents)
455+
tokio::fs::write(&lock_file_path, locked_app_contents)
442456
.await
443-
.with_context(|| format!("failed to write {}", quoted_path(&locked_path)))?;
444-
let locked_url = Url::from_file_path(&locked_path)
445-
.map_err(|_| anyhow!("cannot convert to file URL: {}", quoted_path(&locked_path)))?
457+
.with_context(|| format!("failed to write {}", quoted_path(&lock_file_path)))?;
458+
let locked_url = Url::from_file_path(lock_file_path)
459+
.map_err(|_| {
460+
anyhow!(
461+
"cannot convert to file URL: {}",
462+
quoted_path(&lock_file_path)
463+
)
464+
})?
446465
.to_string();
447466

448467
Ok(locked_url)
@@ -550,6 +569,23 @@ impl UpCommand {
550569
}
551570
}
552571

572+
fn get_locked_app_for_trigger(
573+
trigger_type: &str,
574+
locked_app: &LockedApp,
575+
) -> Result<LockedApp, anyhow::Error> {
576+
let components = locked_app
577+
.triggers
578+
.iter()
579+
.filter(|t| t.trigger_type == trigger_type)
580+
.filter_map(|t| t.trigger_config.get("component"))
581+
.filter_map(|j| j.as_str())
582+
.collect::<Vec<_>>();
583+
let mut locked_app_for_trigger =
584+
spin_app::retain_components(locked_app.clone(), &components, &[])?;
585+
update_service_chaining_host_requirement(&mut locked_app_for_trigger);
586+
Ok(locked_app_for_trigger)
587+
}
588+
553589
fn is_flag_arg(arg: &OsString) -> bool {
554590
if let Some(s) = arg.to_str() {
555591
s.starts_with('-')
@@ -602,7 +638,7 @@ fn kill_child_processes(pids: &[nix::unistd::Pid]) {
602638

603639
#[derive(Clone)]
604640
struct RunTriggerOpts {
605-
locked_url: String,
641+
locked_app: LockedApp,
606642
working_dir: PathBuf,
607643
local_app_dir: Option<PathBuf>,
608644
}
@@ -663,18 +699,29 @@ fn resolve_trigger_plugin(trigger_type: &str) -> Result<String> {
663699
}
664700
}
665701

666-
fn trigger_command(trigger_type: &str) -> Vec<String> {
667-
vec!["trigger".to_owned(), trigger_type.to_owned()]
702+
struct TriggerCommand<'a> {
703+
type_: &'a str,
704+
cmd: Vec<String>,
705+
}
706+
707+
fn trigger_command(trigger_type: &str) -> TriggerCommand {
708+
TriggerCommand {
709+
type_: trigger_type,
710+
cmd: vec!["trigger".to_owned(), trigger_type.to_owned()],
711+
}
668712
}
669713

670-
fn trigger_commands_for_trigger_types(trigger_types: Vec<&str>) -> Result<Vec<Vec<String>>> {
714+
fn trigger_commands_for_trigger_types(trigger_types: Vec<&str>) -> Result<Vec<TriggerCommand>> {
671715
trigger_types
672716
.iter()
673717
.map(|&t| match t {
674718
"http" | "redis" => Ok(trigger_command(t)),
675719
_ => {
676720
let cmd = resolve_trigger_plugin(t)?;
677-
Ok(vec![cmd])
721+
Ok(TriggerCommand {
722+
type_: t,
723+
cmd: vec![cmd],
724+
})
678725
}
679726
})
680727
.collect()

0 commit comments

Comments
 (0)