Skip to content

Commit 838e551

Browse files
committed
fix: tighten project-scoped service targeting
1 parent 65364f4 commit 838e551

2 files changed

Lines changed: 303 additions & 127 deletions

File tree

src/bin/main.rs

Lines changed: 100 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ const FETCH_SPINNER_DELAY: Duration = Duration::from_millis(120);
6060
const FETCH_SPINNER_TICK: Duration = Duration::from_millis(80);
6161
const FETCH_SPINNER_FRAMES: [&str; 4] = ["⠋", "⠙", "⠹", "⠸"];
6262
const RESTART_DAEMON_ACK_TIMEOUT: Duration = Duration::from_millis(250);
63+
const DEFAULT_CONFIG_PATH: &str = "systemg.yaml";
6364

6465
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
6566
enum InspectStreamAction {
@@ -321,6 +322,11 @@ fn main() -> Result<(), Box<dyn Error>> {
321322

322323
let start_target =
323324
resolve_start_target(&config, service, name.as_deref(), command)?;
325+
let command_project = resolve_command_project(
326+
&config,
327+
project.clone(),
328+
start_target.service.as_deref(),
329+
)?;
324330

325331
if daemonize {
326332
if supervisor_running() {
@@ -341,11 +347,11 @@ fn main() -> Result<(), Box<dyn Error>> {
341347
start_target.config_path.display()
342348
);
343349
} else if let Some(service_name) = start_target.service {
344-
let target_project =
345-
project.clone().or(start_target.project_id.clone());
346350
let command = ControlCommand::Start {
347351
service: Some(service_name.clone()),
348-
project: target_project,
352+
project: command_project
353+
.clone()
354+
.or(start_target.project_id.clone()),
349355
};
350356
send_control_command(command)?;
351357
info!(
@@ -384,11 +390,9 @@ fn main() -> Result<(), Box<dyn Error>> {
384390
if supervisor_running() {
385391
match start_target.service {
386392
Some(service_name) => {
387-
let target_project =
388-
project.clone().or(start_target.project_id);
389393
let command = ControlCommand::Start {
390394
service: Some(service_name.clone()),
391-
project: target_project,
395+
project: command_project.or(start_target.project_id),
392396
};
393397
send_control_command(command)?;
394398
info!(
@@ -415,15 +419,17 @@ fn main() -> Result<(), Box<dyn Error>> {
415419
} => {
416420
let service_name = service.clone();
417421
if supervisor_running() {
422+
let target_project =
423+
resolve_command_project(&config, project, service_name.as_deref())?;
418424
let command = if let Some(name) = service_name.clone() {
419425
ControlCommand::Stop {
420426
service: Some(name),
421-
project,
427+
project: target_project,
422428
}
423-
} else if project.is_some() {
429+
} else if target_project.is_some() {
424430
ControlCommand::Stop {
425431
service: None,
426-
project,
432+
project: target_project,
427433
}
428434
} else {
429435
ControlCommand::Shutdown
@@ -481,7 +487,14 @@ fn main() -> Result<(), Box<dyn Error>> {
481487
"--drop-privileges is managed by the running supervisor and has no effect for this restart request"
482488
);
483489
}
484-
let config_override = if config.is_empty() {
490+
let target_project = resolve_command_project(
491+
&config,
492+
project.clone(),
493+
service.as_deref(),
494+
)?;
495+
let config_override = if config.is_empty()
496+
|| (config == DEFAULT_CONFIG_PATH && project.is_some())
497+
{
485498
None
486499
} else {
487500
Some(resolve_config_path(&config)?.display().to_string())
@@ -490,7 +503,7 @@ fn main() -> Result<(), Box<dyn Error>> {
490503
let command = ControlCommand::Restart {
491504
config: config_override.clone(),
492505
service: service.clone(),
493-
project: project.clone(),
506+
project: target_project,
494507
};
495508
if daemonize {
496509
let recycle_config_path = config_override
@@ -537,14 +550,19 @@ Use --daemonize in deployment scripts to ensure daemonized supervision is restor
537550
{
538551
effective_config = hint.to_string_lossy().to_string();
539552
}
553+
let target_project = resolve_command_project(
554+
&effective_config,
555+
project.clone(),
556+
service.as_deref(),
557+
)?;
540558

541559
let render_opts = StatusRenderOptions {
542560
json,
543561
no_color,
544562
full_cmd,
545563
include_orphans: all,
546564
service_filter: service.as_deref(),
547-
project_filter: project.as_deref(),
565+
project_filter: target_project.as_deref(),
548566
};
549567

550568
if let Some(stream_interval) = stream {
@@ -616,6 +634,11 @@ Use --daemonize in deployment scripts to ensure daemonized supervision is restor
616634
{
617635
effective_config = hint.to_string_lossy().to_string();
618636
}
637+
let target_project = resolve_command_project(
638+
&effective_config,
639+
project.clone(),
640+
Some(&service),
641+
)?;
619642

620643
let stream_seconds = match stream.as_deref() {
621644
Some(value) => match parse_stream_duration(value) {
@@ -659,7 +682,7 @@ Use --daemonize in deployment scripts to ensure daemonized supervision is restor
659682
let payload = fetch_inspect(
660683
&effective_config,
661684
&service,
662-
project.as_deref(),
685+
target_project.as_deref(),
663686
samples_limit,
664687
live,
665688
)?;
@@ -693,7 +716,7 @@ Use --daemonize in deployment scripts to ensure daemonized supervision is restor
693716
let payload = fetch_inspect(
694717
&effective_config,
695718
&service,
696-
project.as_deref(),
719+
target_project.as_deref(),
697720
samples_limit,
698721
live,
699722
)?;
@@ -711,7 +734,7 @@ Use --daemonize in deployment scripts to ensure daemonized supervision is restor
711734
fetch_inspect(
712735
&effective_config,
713736
&service,
714-
project.as_deref(),
737+
target_project.as_deref(),
715738
samples_limit,
716739
live,
717740
)
@@ -749,12 +772,17 @@ Use --daemonize in deployment scripts to ensure daemonized supervision is restor
749772
}
750773
}
751774
};
775+
let target_project = resolve_command_project(
776+
&effective_config,
777+
project.clone(),
778+
service.as_deref(),
779+
)?;
752780

753781
let pid = Arc::new(Mutex::new(PidFile::load().unwrap_or_default()));
754782
let manager = LogManager::new(pid.clone());
755783

756784
if purge {
757-
if project.is_some() {
785+
if target_project.is_some() {
758786
let snapshot = with_progress_spinner("Purging logs", || {
759787
fetch_status_snapshot(&effective_config, false)
760788
})?;
@@ -765,7 +793,7 @@ Use --daemonize in deployment scripts to ensure daemonized supervision is restor
765793
status_unit_matches_selector(
766794
unit,
767795
service.as_deref(),
768-
project.as_deref(),
796+
target_project.as_deref(),
769797
)
770798
})
771799
.collect();
@@ -796,7 +824,7 @@ Use --daemonize in deployment scripts to ensure daemonized supervision is restor
796824
|follow: bool| -> Result<(), Box<dyn Error>> {
797825
let command = ControlCommand::Logs {
798826
service: service.clone(),
799-
project: project.clone(),
827+
project: target_project.clone(),
800828
lines,
801829
kind: kind.as_ref().map(|kind| kind.as_str().to_string()),
802830
follow,
@@ -817,7 +845,7 @@ Use --daemonize in deployment scripts to ensure daemonized supervision is restor
817845
&manager,
818846
&snapshot,
819847
service_name,
820-
project.as_deref(),
848+
target_project.as_deref(),
821849
lines,
822850
kind.as_ref().map(|kind| kind.as_str()),
823851
snapshot_mode,
@@ -828,7 +856,7 @@ Use --daemonize in deployment scripts to ensure daemonized supervision is restor
828856
render_all_logs_from_snapshot(
829857
&manager,
830858
&snapshot,
831-
project.as_deref(),
859+
target_project.as_deref(),
832860
lines,
833861
kind.as_ref().map(|kind| kind.as_str()),
834862
snapshot_mode,
@@ -865,7 +893,7 @@ Use --daemonize in deployment scripts to ensure daemonized supervision is restor
865893
loop {
866894
let command = ControlCommand::Logs {
867895
service: service.clone(),
868-
project: project.clone(),
896+
project: target_project.clone(),
869897
lines,
870898
kind: kind.as_ref().map(|kind| kind.as_str().to_string()),
871899
follow: false,
@@ -3200,6 +3228,57 @@ fn yaml_single_quoted(value: &str) -> String {
32003228
format!("'{}'", value.replace('\'', "''"))
32013229
}
32023230

3231+
/// Strips an optional `project/service` selector prefix.
3232+
fn service_selector_name(selector: &str) -> &str {
3233+
selector
3234+
.split_once('/')
3235+
.map(|(_, service)| service)
3236+
.unwrap_or(selector)
3237+
}
3238+
3239+
/// Resolves the project a command should target from an explicit project flag and config.
3240+
fn resolve_command_project(
3241+
config_arg: &str,
3242+
explicit_project: Option<String>,
3243+
service: Option<&str>,
3244+
) -> Result<Option<String>, Box<dyn Error>> {
3245+
let config_path = resolve_config_path(config_arg)?;
3246+
let config_value = load_config(Some(config_path.to_string_lossy().as_ref())).ok();
3247+
3248+
if let Some(project) = explicit_project {
3249+
if config_arg != DEFAULT_CONFIG_PATH
3250+
&& let Some(config) = config_value.as_ref()
3251+
&& config.project.id != project
3252+
{
3253+
return Err(io::Error::new(
3254+
io::ErrorKind::InvalidInput,
3255+
format!(
3256+
"project '{}' does not match config project '{}'",
3257+
project, config.project.id
3258+
),
3259+
)
3260+
.into());
3261+
}
3262+
return Ok(Some(project));
3263+
}
3264+
3265+
let Some(config) = config_value else {
3266+
return Ok(None);
3267+
};
3268+
3269+
if config_arg != DEFAULT_CONFIG_PATH {
3270+
return Ok(Some(config.project.id));
3271+
}
3272+
3273+
if let Some(service) = service
3274+
&& config.services.contains_key(service_selector_name(service))
3275+
{
3276+
return Ok(Some(config.project.id));
3277+
}
3278+
3279+
Ok(None)
3280+
}
3281+
32033282
/// Resolves config path.
32043283
fn resolve_config_path(path: &str) -> Result<PathBuf, Box<dyn Error>> {
32053284
let candidate = PathBuf::from(path);

0 commit comments

Comments
 (0)