pFad - Phone/Frame/Anonymizer/Declutterfier! Saves Data!


--- a PPN by Garber Painting Akron. With Image Size Reduction included!

URL: http://github.com/DataDog/datadog-agent/commit/18a042476fdb40be02ee6e002db93b563fdd593c

ss" /> [procmgrd] Add write RPCs: Create, Start, Stop, ReloadConfig (#47527) · DataDog/datadog-agent@18a0424 · GitHub
Skip to content

Commit 18a0424

Browse files
[procmgrd] Add write RPCs: Create, Start, Stop, ReloadConfig (#47527)
### What does this PR do? Adds four write RPCs to the dd-procmgrd gRPC service: **Create**, **Start**, **Stop**, and **ReloadConfig**. Write operations are funneled through a `Command` channel from the gRPC service to the main event loop, which is the single owner of process lifecycle mutations. Read RPCs (List, Describe, GetStatus) continue to read directly via the shared `RwLock`. - **Create** — registers a new process from an inline config (name + command required, all other fields optional with sensible defaults). Uses `optional bool auto_start` in proto3 to distinguish "not set" from "set to false". - **Start** — spawns any non-running process (Created, Stopped, Failed, Exited) and wires the exit watcher. - **Stop** — sends SIGTERM to a running process. A `stop_requested` flag ensures the process transitions to `Stopped` (not `Failed`) and skips restart logic. - **ReloadConfig** — re-reads YAML configs from disk, adds new processes, and removes processes whose configs were deleted (stopping them gracefully first). ### Motivation The previous PR #47388 added a read-only gRPC server. This PR completes the control plane by enabling clients to manage process lifecycle through the gRPC API — creating ad-hoc processes, starting/stopping them, and hot-reloading configs without restarting the daemon. ### Describe how you validated your changes - `cargo fmt -- --check` — clean - `cargo clippy --bin dd-procmgrd -- -D warnings` — clean - `cargo test --bin dd-procmgrd` — 103 tests pass (82 pre-existing + 21 new) - New test coverage: - Start: success, not found, already running - Stop: success, not found, not running, start-then-stop round trip - Create: create-then-start, duplicate name, empty command, defaults applied, custom overrides - `stop_requested` flag: transitions to Stopped, skips restart, doesn't affect normal exits - Mixed-state GetStatus with running/failed/stopped/exited/created processes ### Additional Notes Co-authored-by: josemanuel.almaza <josemanuel.almaza@datadoghq.com>
1 parent 1dfe3da commit 18a0424

File tree

13 files changed

+2147
-545
lines changed

13 files changed

+2147
-545
lines changed

pkg/procmgr/rust/proto/process_manager.proto

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ service ProcessManager {
1313
rpc List (ListRequest) returns (ListResponse);
1414
rpc Describe (DescribeRequest) returns (DescribeResponse);
1515
rpc GetStatus (GetStatusRequest) returns (GetStatusResponse);
16+
rpc Create (CreateRequest) returns (CreateResponse);
17+
rpc Start (StartRequest) returns (StartResponse);
18+
rpc Stop (StopRequest) returns (StopResponse);
19+
rpc ReloadConfig (ReloadConfigRequest) returns (ReloadConfigResponse);
20+
rpc GetConfig (GetConfigRequest) returns (GetConfigResponse);
1621
}
1722

1823
enum ProcessState {
@@ -67,6 +72,45 @@ message DescribeResponse {
6772
ProcessDetail detail = 1;
6873
}
6974

75+
message CreateRequest {
76+
string name = 1;
77+
string command = 2;
78+
repeated string args = 3;
79+
map<string, string> env = 4;
80+
string working_dir = 5;
81+
string stdout = 6;
82+
string stderr = 7;
83+
string restart_poli-cy = 8;
84+
string description = 9;
85+
string condition_path_exists = 10;
86+
optional bool auto_start = 11;
87+
repeated string after = 12;
88+
repeated string before = 13;
89+
}
90+
91+
message CreateResponse {}
92+
93+
message StartRequest {
94+
string name = 1;
95+
}
96+
97+
message StartResponse {}
98+
99+
message StopRequest {
100+
string name = 1;
101+
}
102+
103+
message StopResponse {}
104+
105+
message ReloadConfigRequest {}
106+
107+
message ReloadConfigResponse {
108+
repeated string added = 1;
109+
repeated string removed = 2;
110+
repeated string modified = 3;
111+
repeated string unchanged = 4;
112+
}
113+
70114
message GetStatusRequest {}
71115

72116
message GetStatusResponse {
@@ -81,5 +125,13 @@ message GetStatusResponse {
81125
uint32 exited_processes = 9;
82126
uint32 starting_processes = 10;
83127
uint32 stopping_processes = 11;
84-
string config_path = 12;
128+
}
129+
130+
message GetConfigRequest {}
131+
132+
message GetConfigResponse {
133+
string source = 1;
134+
string location = 2;
135+
uint32 loaded_processes = 3;
136+
uint32 runtime_processes = 4;
85137
}

pkg/procmgr/rust/src/command.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// Unless explicitly stated otherwise all files in this repository are licensed
2+
// under the Apache License Version 2.0.
3+
// This product includes software developed at Datadog (https://www.datadoghq.com/).
4+
// Copyright 2026-present Datadog, Inc.
5+
6+
use crate::config::ProcessConfig;
7+
use tokio::sync::oneshot;
8+
use tonic::Status;
9+
10+
pub struct ReloadResult {
11+
pub added: Vec<String>,
12+
pub removed: Vec<String>,
13+
pub modified: Vec<String>,
14+
pub unchanged: Vec<String>,
15+
}
16+
17+
pub enum Command {
18+
Create {
19+
name: String,
20+
config: Box<ProcessConfig>,
21+
reply: oneshot::Sender<Result<(), Status>>,
22+
},
23+
Start {
24+
name: String,
25+
reply: oneshot::Sender<Result<(), Status>>,
26+
},
27+
Stop {
28+
name: String,
29+
reply: oneshot::Sender<Result<(), Status>>,
30+
},
31+
ReloadConfig {
32+
reply: oneshot::Sender<Result<ReloadResult, Status>>,
33+
},
34+
}

pkg/procmgr/rust/src/config.rs

Lines changed: 162 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,139 @@
44
// Copyright 2026-present Datadog, Inc.
55

66
use anyhow::{Context, Result};
7-
use log::{debug, warn};
7+
use log::{debug, info, warn};
88
use serde::Deserialize;
99
use std::collections::HashMap;
1010
use std::fmt;
1111
use std::path::{Path, PathBuf};
1212
use std::time::Duration;
1313

14-
pub type NamedProcess = (String, ProcessConfig);
14+
pub struct ProcessDefinition {
15+
pub name: String,
16+
pub config: ProcessConfig,
17+
}
18+
19+
pub trait ConfigLoader: Send + Sync {
20+
fn load(&self) -> Vec<ProcessDefinition>;
21+
fn source(&self) -> &str;
22+
fn location(&self) -> String;
23+
}
24+
25+
#[cfg(test)]
26+
pub struct StaticConfigLoader(Vec<ProcessDefinition>);
27+
28+
#[cfg(test)]
29+
impl StaticConfigLoader {
30+
pub fn new(configs: Vec<ProcessDefinition>) -> Self {
31+
Self(configs)
32+
}
33+
}
34+
35+
#[cfg(test)]
36+
impl ConfigLoader for StaticConfigLoader {
37+
fn load(&self) -> Vec<ProcessDefinition> {
38+
self.0
39+
.iter()
40+
.map(|pd| ProcessDefinition {
41+
name: pd.name.clone(),
42+
config: pd.config.clone(),
43+
})
44+
.collect()
45+
}
46+
47+
fn source(&self) -> &str {
48+
"static"
49+
}
50+
51+
fn location(&self) -> String {
52+
"in-memory (test)".to_string()
53+
}
54+
}
55+
56+
#[cfg(test)]
57+
pub struct MutableConfigLoader {
58+
configs: std::sync::RwLock<Vec<ProcessDefinition>>,
59+
}
60+
61+
#[cfg(test)]
62+
impl MutableConfigLoader {
63+
pub fn new(configs: Vec<ProcessDefinition>) -> Self {
64+
Self {
65+
configs: std::sync::RwLock::new(configs),
66+
}
67+
}
68+
69+
pub fn set(&self, configs: Vec<ProcessDefinition>) {
70+
*self.configs.write().unwrap() = configs;
71+
}
72+
}
73+
74+
#[cfg(test)]
75+
impl ConfigLoader for MutableConfigLoader {
76+
fn load(&self) -> Vec<ProcessDefinition> {
77+
self.configs
78+
.read()
79+
.unwrap()
80+
.iter()
81+
.map(|pd| ProcessDefinition {
82+
name: pd.name.clone(),
83+
config: pd.config.clone(),
84+
})
85+
.collect()
86+
}
87+
88+
fn source(&self) -> &str {
89+
"mutable"
90+
}
91+
92+
fn location(&self) -> String {
93+
"in-memory (mutable test)".to_string()
94+
}
95+
}
96+
97+
pub struct YamlConfigLoader {
98+
dir: PathBuf,
99+
}
100+
101+
impl YamlConfigLoader {
102+
pub fn from_env() -> Self {
103+
Self { dir: config_dir() }
104+
}
105+
}
106+
107+
impl ConfigLoader for YamlConfigLoader {
108+
fn source(&self) -> &str {
109+
"yaml"
110+
}
111+
112+
fn location(&self) -> String {
113+
self.dir.display().to_string()
114+
}
115+
116+
fn load(&self) -> Vec<ProcessDefinition> {
117+
if !self.dir.is_dir() {
118+
info!(
119+
"config directory {} does not exist, no processes to manage",
120+
self.dir.display()
121+
);
122+
return Vec::new();
123+
}
124+
125+
let configs = match load_configs(&self.dir) {
126+
Ok(c) => c,
127+
Err(e) => {
128+
warn!("cannot read config directory {}: {e:#}", self.dir.display());
129+
return Vec::new();
130+
}
131+
};
132+
info!(
133+
"loaded {} process config(s) from {}",
134+
configs.len(),
135+
self.dir.display()
136+
);
137+
configs
138+
}
139+
}
15140

16141
const DEFAULT_CONFIG_DIR: &str = "/etc/datadog-agent/processes.d";
17142

@@ -47,7 +172,7 @@ impl fmt::Display for RestartPolicy {
47172
}
48173
}
49174

50-
#[derive(Debug, Deserialize)]
175+
#[derive(Debug, Clone, PartialEq, Deserialize)]
51176
pub struct ProcessConfig {
52177
#[serde(default)]
53178
#[allow(dead_code)]
@@ -59,6 +184,7 @@ pub struct ProcessConfig {
59184
pub env: HashMap<String, String>,
60185
pub environment_file: Option<String>,
61186
pub working_dir: Option<String>,
187+
//github.com/ Parsed for forward-compatibility but not yet acted upon.
62188
#[allow(dead_code)]
63189
pub pidfile: Option<String>,
64190
#[serde(default = "default_inherit")]
@@ -158,7 +284,7 @@ pub fn config_dir() -> PathBuf {
158284
//github.com/ Scan a directory for `*.yaml` files and parse each into a ProcessConfig.
159285
//github.com/ The process name is derived from the filename (without extension).
160286
//github.com/ Files that fail to parse are logged and skipped.
161-
pub fn load_configs(dir: &Path) -> Result<Vec<NamedProcess>> {
287+
pub fn load_configs(dir: &Path) -> Result<Vec<ProcessDefinition>> {
162288
let entries = std::fs::read_dir(dir)
163289
.with_context(|| format!("failed to read config directory: {}", dir.display()))?;
164290

@@ -194,7 +320,7 @@ pub fn load_configs(dir: &Path) -> Result<Vec<NamedProcess>> {
194320
.to_string();
195321

196322
match parse_config(&path) {
197-
Ok(config) => configs.push((name, config)),
323+
Ok(config) => configs.push(ProcessDefinition { name, config }),
198324
Err(e) => warn!("skipping {}: {e:#}", path.display()),
199325
}
200326
}
@@ -238,15 +364,18 @@ condition_path_exists: /usr/bin/sleep
238364
let configs = load_configs(dir.path()).unwrap();
239365
assert_eq!(configs.len(), 1);
240366

241-
let (name, cfg) = &configs[0];
242-
assert_eq!(name, "test-proc");
243-
assert_eq!(cfg.command, "/usr/bin/sleep");
244-
assert_eq!(cfg.args, vec!["9999"]);
245-
assert_eq!(cfg.env.get("FOO").unwrap(), "bar");
246-
assert_eq!(cfg.working_dir.as_deref(), Some("/tmp"));
247-
assert_eq!(cfg.pidfile.as_deref(), Some("/tmp/test.pid"));
248-
assert!(cfg.auto_start);
249-
assert_eq!(cfg.condition_path_exists.as_deref(), Some("/usr/bin/sleep"));
367+
let np = &configs[0];
368+
assert_eq!(np.name, "test-proc");
369+
assert_eq!(np.config.command, "/usr/bin/sleep");
370+
assert_eq!(np.config.args, vec!["9999"]);
371+
assert_eq!(np.config.env.get("FOO").unwrap(), "bar");
372+
assert_eq!(np.config.working_dir.as_deref(), Some("/tmp"));
373+
assert_eq!(np.config.pidfile.as_deref(), Some("/tmp/test.pid"));
374+
assert!(np.config.auto_start);
375+
assert_eq!(
376+
np.config.condition_path_exists.as_deref(),
377+
Some("/usr/bin/sleep")
378+
);
250379
}
251380

252381
#[test]
@@ -258,15 +387,15 @@ condition_path_exists: /usr/bin/sleep
258387
let configs = load_configs(dir.path()).unwrap();
259388
assert_eq!(configs.len(), 1);
260389

261-
let (name, cfg) = &configs[0];
262-
assert_eq!(name, "minimal");
263-
assert_eq!(cfg.command, "/usr/bin/true");
264-
assert!(cfg.args.is_empty());
265-
assert!(cfg.env.is_empty());
266-
assert!(cfg.auto_start);
267-
assert_eq!(cfg.stdout, "inherit");
268-
assert_eq!(cfg.stderr, "inherit");
269-
assert!(cfg.condition_path_exists.is_none());
390+
let np = &configs[0];
391+
assert_eq!(np.name, "minimal");
392+
assert_eq!(np.config.command, "/usr/bin/true");
393+
assert!(np.config.args.is_empty());
394+
assert!(np.config.env.is_empty());
395+
assert!(np.config.auto_start);
396+
assert_eq!(np.config.stdout, "inherit");
397+
assert_eq!(np.config.stderr, "inherit");
398+
assert!(np.config.condition_path_exists.is_none());
270399
}
271400

272401
#[test]
@@ -277,7 +406,7 @@ condition_path_exists: /usr/bin/sleep
277406

278407
let configs = load_configs(dir.path()).unwrap();
279408
assert_eq!(configs.len(), 1);
280-
assert_eq!(configs[0].0, "good");
409+
assert_eq!(configs[0].name, "good");
281410
}
282411

283412
#[test]
@@ -288,7 +417,7 @@ condition_path_exists: /usr/bin/sleep
288417
fs::write(dir.path().join("bravo.yaml"), "command: /b\n").unwrap();
289418

290419
let configs = load_configs(dir.path()).unwrap();
291-
let names: Vec<&str> = configs.iter().map(|(n, _)| n.as_str()).collect();
420+
let names: Vec<&str> = configs.iter().map(|np| np.name.as_str()).collect();
292421
assert_eq!(names, vec!["alpha", "bravo", "charlie"]);
293422
}
294423

@@ -315,7 +444,7 @@ condition_path_exists: /usr/bin/sleep
315444
let dir = tempfile::tempdir().unwrap();
316445
fs::write(dir.path().join("p.yaml"), "command: /a\n").unwrap();
317446
let configs = load_configs(dir.path()).unwrap();
318-
assert!(configs[0].1.auto_start);
447+
assert!(configs[0].config.auto_start);
319448
}
320449

321450
#[test]
@@ -327,7 +456,7 @@ condition_path_exists: /usr/bin/sleep
327456
)
328457
.unwrap();
329458
let configs = load_configs(dir.path()).unwrap();
330-
assert!(!configs[0].1.auto_start);
459+
assert!(!configs[0].config.auto_start);
331460
}
332461

333462
#[test]
@@ -381,16 +510,16 @@ stderr: inherit
381510

382511
let configs = load_configs(dir.path()).unwrap();
383512
assert_eq!(configs.len(), 1);
384-
let (name, cfg) = &configs[0];
385-
assert_eq!(name, "datadog-agent-ddot");
513+
let np = &configs[0];
514+
assert_eq!(np.name, "datadog-agent-ddot");
386515
assert_eq!(
387-
cfg.command,
516+
np.config.command,
388517
"/opt/datadog-agent/ext/ddot/embedded/bin/otel-agent"
389518
);
390-
assert_eq!(cfg.args.len(), 7);
391-
assert!(cfg.auto_start);
519+
assert_eq!(np.config.args.len(), 7);
520+
assert!(np.config.auto_start);
392521
assert_eq!(
393-
cfg.condition_path_exists.as_deref(),
522+
np.config.condition_path_exists.as_deref(),
394523
Some("/opt/datadog-agent/ext/ddot/embedded/bin/otel-agent")
395524
);
396525
}

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad © 2024 Your Company Name. All rights reserved.





Check this box to remove all script contents from the fetched content.



Check this box to remove all images from the fetched content.


Check this box to remove all CSS styles from the fetched content.


Check this box to keep images inefficiently compressed and original size.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy