Skip to content

Commit d86f9e7

Browse files
committed
feat: add config generator to host cli
1 parent 0fc98fb commit d86f9e7

File tree

10 files changed

+961
-33
lines changed

10 files changed

+961
-33
lines changed

Cargo.lock

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

agent-control/Cargo.toml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ chrono = { workspace = true }
3636
base64 = { version = "0.22.1" }
3737
# New Relic dependencies (private external repos)
3838
# IMPORTANT: GitHub deployment keys are used to access these repos on the CI/CD pipelines
39-
nr-auth = { git = "https://github.com/newrelic/newrelic-auth-rs.git", tag = "0.0.11" }
39+
nr-auth = { git = "https://github.com/newrelic/newrelic-auth-rs.git", tag = "0.1.1" }
4040
opamp-client = { git = "https://github.com/newrelic/newrelic-opamp-rs.git", tag = "0.0.34" }
4141
# local dependencies
4242
fs = { path = "../fs" }
@@ -91,7 +91,6 @@ fs = { path = "../fs", features = ["mocks"] }
9191
tokio = { version = "1.48.0", features = ["rt-multi-thread", "macros"] }
9292
fake = { version = "4.4.0", features = ["derive", "http"] }
9393
prost = "0.14.1"
94-
# Alpha version needed to test proxy the feature, it is safe because it is only used as dev-dependency
9594
httpmock = { version = "0.8.2", features = ["proxy"] }
9695
serial_test = "3.2.0"
9796
futures = "0.3.31"

agent-control/src/bin/main_agent_control_onhost_cli.rs

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,37 @@
11
use std::process::ExitCode;
22

33
use clap::Parser;
4-
use newrelic_agent_control::cli::{
5-
logs,
6-
on_host::config_gen::{ConfigInputs, generate_config},
7-
};
4+
use newrelic_agent_control::cli::{logs, on_host::config_gen};
85
use tracing::Level;
96

107
#[derive(Debug, clap::Parser)]
118
#[command()]
129
struct Cli {
1310
#[command(subcommand)]
1411
command: Commands,
12+
/// Log level for the cli command
1513
#[arg(long, global = true, default_value = "info")]
16-
log_level: Level,
14+
cli_log_level: Level,
1715
}
1816

1917
/// Commands supported by the cli
2018
#[derive(Debug, clap::Subcommand)]
2119
enum Commands {
2220
// Generate Agent Control configuration according to the provided configuration data.
23-
GenerateConfig(ConfigInputs),
21+
GenerateConfig(config_gen::Args),
2422
}
2523

2624
fn main() -> ExitCode {
2725
let cli = Cli::parse();
2826

29-
let tracer = logs::init(cli.log_level);
27+
let tracer = logs::init(cli.cli_log_level);
3028
if let Err(err) = tracer {
3129
eprintln!("Failed to initialize tracing: {err}");
3230
return err.into();
3331
}
3432

3533
let result = match cli.command {
36-
Commands::GenerateConfig(inputs) => generate_config(inputs),
34+
Commands::GenerateConfig(inputs) => config_gen::generate_config(inputs),
3735
};
3836

3937
if let Err(err) = result {
Lines changed: 297 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,305 @@
1-
use crate::cli::error::CliError;
1+
//! Implementation of the generate-config command for the on-host cli.
22
3+
use std::path::PathBuf;
4+
5+
use tracing::info;
6+
7+
use crate::{
8+
cli::{
9+
error::CliError,
10+
on_host::config_gen::{
11+
config::{AgentSet, AuthConfig, Config, FleetControl, Server, SignatureValidation},
12+
identity::{Identity, provide_identity},
13+
region::{Region, region_parser},
14+
},
15+
},
16+
http::config::ProxyConfig,
17+
};
18+
19+
pub mod config;
20+
pub mod identity;
21+
pub mod region;
22+
23+
/// Generates the Agent Control configuration for host environments.
324
#[derive(Debug, clap::Parser)]
4-
pub struct ConfigInputs {
25+
pub struct Args {
26+
/// Sets where the generated configuration should be written to.
27+
#[arg(long)]
28+
output_path: PathBuf,
29+
30+
/// Defines if Fleet Control is enabled
31+
#[arg(long, default_value = "true")]
32+
fleet_enabled: bool,
33+
34+
/// New Relic region
35+
#[arg(long, value_parser = region_parser())]
36+
region: Region,
37+
538
/// Fleet identifier
639
#[arg(long)]
7-
fleet_id: Option<String>,
8-
// TODO: add remaining inputs
40+
fleet_id: String,
41+
42+
/// Organization identifier
43+
#[arg(long)]
44+
organization_id: String,
45+
46+
/// Set of agents to be used as local configuration.
47+
#[arg(long)]
48+
agent_set: AgentSet,
49+
50+
/// Client ID corresponding to the parent system identity (requires `auth_client_secret`).
51+
#[arg(long)]
52+
auth_parent_client_id: String,
53+
54+
/// Client Secret corresponding to the parent system identity (requires `auth_client_id`).
55+
#[arg(long)]
56+
auth_parent_client_secret: String,
57+
58+
/// Auth token corresponding to the parent system identity.
59+
#[arg(long)]
60+
auth_parent_token: String,
61+
62+
/// When (`auth_token` or `auth_client_id` + `auth_client_secret`) are set, this path is used
63+
/// to store the identity key. Otherwise, the path is expected to contain the already provided
64+
/// private key was already provided.
65+
#[arg(long)]
66+
auth_private_key_path: PathBuf,
67+
68+
/// Client identifier corresponding to an already provisioned identity. No identity creation is performed,
69+
/// therefore setting this up also requires an existing private key pointed in `auth_private_key_path`.
70+
#[arg(long)]
71+
auth_client_id: String,
72+
73+
/// Proxy configuration
74+
#[command(flatten)]
75+
proxy_config: Option<ProxyConfig>,
976
}
1077

1178
/// Generates the Agent Control configuration and any requisite according to the provided inputs.
12-
pub fn generate_config(_inputs: ConfigInputs) -> Result<(), CliError> {
13-
unimplemented!("TODO: implement config generation")
79+
pub fn generate_config(args: Args) -> Result<(), CliError> {
80+
info!("Generating Agent Control configuration");
81+
let yaml = gen_config(&args, provide_identity)?;
82+
83+
std::fs::write(&args.output_path, yaml).map_err(|err| {
84+
CliError::Command(format!(
85+
"error writing the configuration file to '{}': {}",
86+
args.output_path.to_string_lossy(),
87+
err
88+
))
89+
})?;
90+
info!(config_path=%args.output_path.to_string_lossy(), "Agent Control configuration generated successfully");
91+
Ok(())
92+
}
93+
94+
/// Generates the configuration according to args using the provided function to generate the identity.
95+
fn gen_config<F>(args: &Args, provide_identity_fn: F) -> Result<String, CliError>
96+
where
97+
F: Fn(&Args) -> Result<Identity, CliError>,
98+
{
99+
let fleet_control = if !args.fleet_enabled {
100+
None
101+
} else {
102+
let Identity {
103+
client_id,
104+
private_key_path,
105+
} = provide_identity_fn(args)?;
106+
107+
Some(FleetControl {
108+
endpoint: args.region.opamp_endpoint().to_string(),
109+
signature_validation: SignatureValidation {
110+
public_key_server_url: args.region.public_key_endpoint().to_string(),
111+
},
112+
fleet_id: args.fleet_id.to_string(),
113+
auth_config: AuthConfig {
114+
token_url: args.region.token_renewal_endpoint().to_string(),
115+
client_id,
116+
provider: "local".to_string(),
117+
private_key_path: private_key_path.to_string_lossy().to_string(),
118+
},
119+
})
120+
};
121+
122+
let config = Config {
123+
fleet_control,
124+
server: Server { enabled: true },
125+
proxy: args.proxy_config.clone(),
126+
agents: args.agent_set.into(),
127+
};
128+
129+
serde_yaml::to_string(&config)
130+
.map_err(|err| CliError::Command(format!("failed to serialize configuration: {err}")))
131+
}
132+
133+
#[cfg(test)]
134+
mod tests {
135+
use super::*;
136+
use rstest::rstest;
137+
138+
#[rstest]
139+
#[case(true, Region::US, AgentSet::InfraAgent, None, EXPECTED_INFRA_US)]
140+
#[case(true, Region::EU, AgentSet::Otel, None, EXPECTED_OTEL_EU)]
141+
#[case(true, Region::STAGING, AgentSet::None, None, EXPECTED_NONE_STAGING)]
142+
#[case(
143+
false,
144+
Region::US,
145+
AgentSet::InfraAgent,
146+
None,
147+
EXPECTED_FLEET_DISABLED_INFRA
148+
)]
149+
#[case(
150+
true,
151+
Region::US,
152+
AgentSet::InfraAgent,
153+
some_proxy_config(),
154+
EXPECTED_INFRA_US_PROXY
155+
)]
156+
fn test_gen_config_with_fleet_enabled(
157+
#[case] fleet_enabled: bool,
158+
#[case] region: Region,
159+
#[case] agent_set: AgentSet,
160+
#[case] proxy_config: Option<ProxyConfig>,
161+
#[case] expected: &str,
162+
) {
163+
let args = create_test_args(fleet_enabled, region, agent_set, proxy_config);
164+
165+
let yaml = gen_config(&args, identity_provider_mock).expect("result expected to be OK");
166+
167+
let parsed: serde_yaml::Value =
168+
serde_yaml::from_str(&yaml).expect("Invalid generated YAML");
169+
let expected_parsed: serde_yaml::Value =
170+
serde_yaml::from_str(expected).expect("Invalid expectation");
171+
172+
assert_eq!(parsed, expected_parsed);
173+
}
174+
175+
fn identity_provider_mock(_: &Args) -> Result<Identity, CliError> {
176+
Ok(Identity {
177+
client_id: "test-client-id".to_string(),
178+
private_key_path: PathBuf::from("/path/to/private/key"),
179+
})
180+
}
181+
182+
fn create_test_args(
183+
fleet_enabled: bool,
184+
region: Region,
185+
agent_set: AgentSet,
186+
proxy_config: Option<ProxyConfig>,
187+
) -> Args {
188+
Args {
189+
output_path: PathBuf::from("/tmp/config.yaml"),
190+
fleet_enabled,
191+
region,
192+
fleet_id: "test-fleet-id".to_string(),
193+
organization_id: "test-org-id".to_string(),
194+
agent_set,
195+
auth_parent_client_id: "parent-client-id".to_string(),
196+
auth_parent_client_secret: "parent-client-secret".to_string(),
197+
auth_parent_token: "parent-token".to_string(),
198+
auth_private_key_path: PathBuf::from("/path/to/key"),
199+
auth_client_id: "client-id".to_string(),
200+
proxy_config,
201+
}
202+
}
203+
204+
fn some_proxy_config() -> Option<ProxyConfig> {
205+
let proxy_config: ProxyConfig = serde_yaml::from_str(
206+
r#"{"url": "https://some.proxy.url/", "ca_bundle_dir": "/test/bundle/dir",
207+
"ca_bundle_file": "/test/bundle/file", "ignore_system_proxy": true}"#,
208+
)
209+
.unwrap();
210+
Some(proxy_config)
211+
}
212+
213+
const EXPECTED_INFRA_US: &str = r#"
214+
fleet_control:
215+
endpoint: https://opamp.service.newrelic.com/v1/opamp
216+
signature_validation:
217+
public_key_server_url: https://publickeys.newrelic.com/r/blob-management/global/agentconfiguration/jwks.json
218+
fleet_id: test-fleet-id
219+
auth_config:
220+
token_url: https://system-identity-oauth.service.newrelic.com/oauth2/token
221+
client_id: test-client-id
222+
provider: local
223+
private_key_path: /path/to/private/key
224+
225+
server:
226+
enabled: true
227+
228+
agents:
229+
nr-infra:
230+
agent_type: "newrelic/com.newrelic.infrastructure:0.1.0"
231+
"#;
232+
233+
const EXPECTED_OTEL_EU: &str = r#"
234+
fleet_control:
235+
endpoint: https://opamp.service.eu.newrelic.com/v1/opamp
236+
signature_validation:
237+
public_key_server_url: https://publickeys.eu.newrelic.com/r/blob-management/global/agentconfiguration/jwks.json
238+
fleet_id: test-fleet-id
239+
auth_config:
240+
token_url: https://system-identity-oauth.service.newrelic.com/oauth2/token
241+
client_id: test-client-id
242+
provider: local
243+
private_key_path: /path/to/private/key
244+
245+
server:
246+
enabled: true
247+
248+
agents:
249+
nrdot:
250+
agent_type: "newrelic/com.newrelic.opentelemetry.collector:0.1.0"
251+
"#;
252+
253+
const EXPECTED_NONE_STAGING: &str = r#"
254+
fleet_control:
255+
endpoint: https://staging-service.newrelic.com/v1/opamp
256+
signature_validation:
257+
public_key_server_url: https://staging-publickeys.newrelic.com/r/blob-management/global/agentconfiguration/jwks.json
258+
fleet_id: test-fleet-id
259+
auth_config:
260+
token_url: https://system-identity-oauth.staging-service.newrelic.com/oauth2/token
261+
client_id: test-client-id
262+
provider: local
263+
private_key_path: /path/to/private/key
264+
265+
server:
266+
enabled: true
267+
268+
agents: {}
269+
"#;
270+
271+
const EXPECTED_FLEET_DISABLED_INFRA: &str = r#"
272+
server:
273+
enabled: true
274+
275+
agents:
276+
nr-infra:
277+
agent_type: "newrelic/com.newrelic.infrastructure:0.1.0"
278+
"#;
279+
280+
const EXPECTED_INFRA_US_PROXY: &str = r#"
281+
fleet_control:
282+
endpoint: https://opamp.service.newrelic.com/v1/opamp
283+
signature_validation:
284+
public_key_server_url: https://publickeys.newrelic.com/r/blob-management/global/agentconfiguration/jwks.json
285+
fleet_id: test-fleet-id
286+
auth_config:
287+
token_url: https://system-identity-oauth.service.newrelic.com/oauth2/token
288+
client_id: test-client-id
289+
provider: local
290+
private_key_path: /path/to/private/key
291+
292+
server:
293+
enabled: true
294+
295+
proxy:
296+
url: https://some.proxy.url/
297+
ca_bundle_dir: /test/bundle/dir
298+
ca_bundle_file: /test/bundle/file
299+
ignore_system_proxy: true
300+
301+
agents:
302+
nr-infra:
303+
agent_type: "newrelic/com.newrelic.infrastructure:0.1.0"
304+
"#;
14305
}

0 commit comments

Comments
 (0)