Skip to content

Commit a351b4c

Browse files
authored
cli(on-hots): support empty proxy on config generation (#1834)
1 parent 3c4bdf0 commit a351b4c

File tree

5 files changed

+169
-33
lines changed

5 files changed

+169
-33
lines changed

agent-control/src/cli/on_host/config_gen.rs

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,15 @@ use std::path::PathBuf;
44

55
use tracing::info;
66

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},
7+
use crate::cli::{
8+
error::CliError,
9+
on_host::config_gen::{
10+
config::{
11+
AgentSet, AuthConfig, Config, FleetControl, ProxyConfig, Server, SignatureValidation,
1412
},
13+
identity::{Identity, provide_identity},
14+
region::{Region, region_parser},
1515
},
16-
http::config::ProxyConfig,
1716
};
1817

1918
pub mod config;
@@ -121,6 +120,11 @@ impl Args {
121120
));
122121
}
123122
}
123+
if let Some(proxy_config) = self.proxy_config.clone()
124+
&& let Err(err) = crate::http::config::ProxyConfig::try_from(proxy_config)
125+
{
126+
return Err(format!("invalid proxy configuration: {err}"));
127+
}
124128
Ok(())
125129
}
126130
}
@@ -226,7 +230,7 @@ mod tests {
226230
fn test_args_validation(#[case] args: fn() -> String) {
227231
let cmd = Args::command().no_binary_name(true);
228232
let matches = cmd
229-
.try_get_matches_from(args().split(" "))
233+
.try_get_matches_from(args().split_ascii_whitespace())
230234
.expect("arguments should be valid");
231235
let args = Args::from_arg_matches(&matches).expect("should create the struct back");
232236
assert_matches!(args.validate(), Ok(_));
@@ -254,10 +258,13 @@ mod tests {
254258
#[case::missing_auth_parent_client_id_with_secret(
255259
|| format!("--output-path /some/path --agent-set otel --region us --auth-private-key-path {} --auth-parent-client-secret SECRET --auth-parent-client-id id", pwd())
256260
)]
261+
#[case::invalid_proxy_config(
262+
|| String::from("--fleet-disabled --output-path /some/path --agent-set otel --region us --proxy-url https::/invalid")
263+
)]
257264
fn test_args_validation_errors(#[case] args: fn() -> String) {
258265
let cmd = Args::command().no_binary_name(true);
259266
let matches = cmd
260-
.try_get_matches_from(args().split(" "))
267+
.try_get_matches_from(args().split_ascii_whitespace())
261268
.expect("arguments should be valid");
262269
let args = Args::from_arg_matches(&matches).expect("should create the struct back");
263270

@@ -348,11 +355,12 @@ mod tests {
348355
}
349356

350357
fn some_proxy_config() -> Option<ProxyConfig> {
351-
let proxy_config: ProxyConfig = serde_yaml::from_str(
352-
r#"{"url": "https://some.proxy.url/", "ca_bundle_dir": "/test/bundle/dir",
353-
"ca_bundle_file": "/test/bundle/file", "ignore_system_proxy": true}"#,
354-
)
355-
.unwrap();
358+
let proxy_config = ProxyConfig {
359+
proxy_url: Some("https://some.proxy.url/".to_string()),
360+
proxy_ca_bundle_dir: Some("/test/bundle/dir".to_string()),
361+
proxy_ca_bundle_file: Some("/test/bundle/file".to_string()),
362+
ignore_system_proxy: true,
363+
};
356364
Some(proxy_config)
357365
}
358366

agent-control/src/cli/on_host/config_gen/config.rs

Lines changed: 92 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
//! Contains the definition of the configuration to be generated
22
3-
use std::collections::HashMap;
3+
use std::{collections::HashMap, convert::Infallible};
44

55
use serde::Serialize;
66

7-
use crate::http::config::ProxyConfig;
8-
97
/// Represents the set of agents to be included in the AC configuration.
108
#[derive(Debug, Copy, Clone, PartialEq, clap::ValueEnum)]
119
pub enum AgentSet {
@@ -36,6 +34,46 @@ impl From<AgentSet> for HashMap<String, Agent> {
3634
}
3735
}
3836

37+
/// Holds the proxy configuration.
38+
/// Cannot use [crate::http::config::ProxyConfig] directly due lack of support for defaults in clap.
39+
/// See <https://github.com/clap-rs/clap/issues/4746> for details.
40+
#[derive(Debug, Default, Clone, PartialEq, Serialize, clap::Args)]
41+
pub struct ProxyConfig {
42+
#[serde(skip_serializing_if = "is_none_or_empty_string", rename = "url")]
43+
#[arg(long, required = false)]
44+
pub proxy_url: Option<String>,
45+
46+
#[serde(
47+
skip_serializing_if = "is_none_or_empty_string",
48+
rename = "ca_bundle_dir"
49+
)]
50+
#[arg(long, required = false)]
51+
pub proxy_ca_bundle_dir: Option<String>,
52+
53+
#[serde(
54+
skip_serializing_if = "is_none_or_empty_string",
55+
rename = "ca_bundle_file"
56+
)]
57+
#[arg(long, required = false)]
58+
pub proxy_ca_bundle_file: Option<String>,
59+
60+
#[arg(long, default_value_t = false, value_parser = ignore_system_proxy_parser, action = clap::ArgAction::Set)]
61+
pub ignore_system_proxy: bool,
62+
}
63+
64+
// Helper to avoid serializing empty values
65+
fn is_none_or_empty_string(v: &Option<String>) -> bool {
66+
v.as_ref().map(|s| s.is_empty()).unwrap_or(true)
67+
}
68+
69+
// Custom parser to allow empty values as false booleans
70+
fn ignore_system_proxy_parser(s: &str) -> Result<bool, Infallible> {
71+
match s.to_lowercase().as_str() {
72+
"" | "false" => Ok(false),
73+
_ => Ok(true),
74+
}
75+
}
76+
3977
/// Configuration to be written as result of the corresponding command.
4078
#[derive(Debug, PartialEq, Serialize)]
4179
pub struct Config {
@@ -84,6 +122,7 @@ pub struct Agent {
84122
#[cfg(test)]
85123
mod tests {
86124
use super::*;
125+
use clap::{Args, FromArgMatches};
87126
use rstest::rstest;
88127

89128
#[rstest]
@@ -109,4 +148,54 @@ mod tests {
109148

110149
assert_eq!(result, expected_map);
111150
}
151+
152+
#[test]
153+
fn test_serialize_proxy_config_all_empty_options() {
154+
let proxy_config = ProxyConfig {
155+
proxy_url: Some(String::new()),
156+
proxy_ca_bundle_dir: Some(String::new()),
157+
proxy_ca_bundle_file: Some(String::new()),
158+
ignore_system_proxy: false,
159+
};
160+
161+
let serialized = serde_yaml::to_string(&proxy_config).unwrap();
162+
// Only ignore_system_proxy should be present
163+
assert_eq!(serialized.trim(), "ignore_system_proxy: false");
164+
}
165+
166+
#[test]
167+
fn test_serialize_proxy_config_none_options() {
168+
let proxy_config = ProxyConfig {
169+
proxy_url: None,
170+
proxy_ca_bundle_dir: None,
171+
proxy_ca_bundle_file: None,
172+
ignore_system_proxy: true,
173+
};
174+
175+
let serialized = serde_yaml::to_string(&proxy_config).unwrap();
176+
// Only ignore_system_proxy should be present
177+
assert_eq!(serialized.trim(), "ignore_system_proxy: true");
178+
}
179+
180+
#[rstest]
181+
#[case("", ProxyConfig::default())]
182+
#[case(
183+
"--proxy-url https://proxy.url --proxy-ca-bundle-dir=/bundle/dir --proxy-ca-bundle-file=/bundle/file --ignore-system-proxy true",
184+
ProxyConfig{proxy_url: Some("https://proxy.url".into()), proxy_ca_bundle_dir: Some("/bundle/dir".into()), proxy_ca_bundle_file: Some("/bundle/file".into()), ignore_system_proxy: true},
185+
)]
186+
#[case("--proxy-url= --proxy-ca-bundle-dir= --proxy-ca-bundle-file= --ignore-system-proxy=", ProxyConfig{proxy_url: Some("".into()), proxy_ca_bundle_dir: Some("".into()), proxy_ca_bundle_file: Some("".into()), ignore_system_proxy: false})]
187+
#[case(" --ignore-system-proxy=", ProxyConfig{ignore_system_proxy: false, ..Default::default()})]
188+
#[case(" --ignore-system-proxy=false", ProxyConfig{ignore_system_proxy: false, ..Default::default()})]
189+
#[case(" --ignore-system-proxy=true", ProxyConfig{ignore_system_proxy: true, ..Default::default()})]
190+
#[case(" --ignore-system-proxy=False", ProxyConfig{ignore_system_proxy: false, ..Default::default()})]
191+
#[case(" --ignore-system-proxy=True", ProxyConfig{ignore_system_proxy: true, ..Default::default()})]
192+
fn test_proxy_args(#[case] args: &str, #[case] expected: ProxyConfig) {
193+
let cmd = clap::Command::new("test").no_binary_name(true);
194+
let cmd = ProxyConfig::augment_args(cmd);
195+
let matches = cmd
196+
.try_get_matches_from(args.split_ascii_whitespace())
197+
.expect("arguments should be valid");
198+
let value = ProxyConfig::from_arg_matches(&matches).expect("should create the struct back");
199+
assert_eq!(value, expected)
200+
}
112201
}

agent-control/src/cli/on_host/config_gen/identity.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ use nr_auth::{
2323
};
2424
use tracing::info;
2525

26-
use crate::{cli::error::CliError, http::config::ProxyConfig};
26+
use crate::cli::{error::CliError, on_host::config_gen::config::ProxyConfig};
2727

2828
use super::Args;
2929

@@ -142,13 +142,13 @@ fn build_nr_auth_proxy_config(
142142
ac_cfg: ProxyConfig,
143143
) -> Result<nr_auth::http::config::ProxyConfig, CliError> {
144144
let auth_cfg = nr_auth::http::config::ProxyConfig::new(
145-
ac_cfg.url_as_string(),
146-
ac_cfg.ca_bundle_dir().to_owned(),
147-
ac_cfg.ca_bundle_file().to_owned(),
145+
ac_cfg.proxy_url.unwrap_or_default(),
146+
PathBuf::from(ac_cfg.proxy_ca_bundle_dir.unwrap_or_default()),
147+
PathBuf::from(ac_cfg.proxy_ca_bundle_file.unwrap_or_default()),
148148
)
149149
.map_err(|err| CliError::Command(format!("invalid proxy configuration: {err}")))?;
150150

151-
if ac_cfg.ignore_system_proxy() {
151+
if ac_cfg.ignore_system_proxy {
152152
Ok(auth_cfg)
153153
} else {
154154
auth_cfg

agent-control/src/http/config.rs

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -124,26 +124,21 @@ impl ProxyUrl {
124124
/// // The url will contain the value corresponding to the standard environment variables.
125125
/// let proxy_config = ProxyConfig::default().try_with_url_from_env().unwrap();
126126
/// ```
127-
#[derive(Debug, Deserialize, Serialize, PartialEq, Clone, Default, clap::Args)]
128-
#[group()]
127+
#[derive(Debug, Deserialize, Serialize, PartialEq, Clone, Default)]
129128
pub struct ProxyConfig {
130129
/// Proxy URL proxy:
131130
/// <protocol>://<user>:<password>@<host>:<port>
132131
/// (All parts except host are optional)
133132
#[serde(default)]
134-
#[arg(long="proxy-url", value_parser = clap::builder::ValueParser::new(|s: &str| ProxyUrl::try_from(s)), required=false)]
135133
url: ProxyUrl,
136134
/// System path with the CA certificates in PEM format. All `.pem` files in the directory are read.
137135
#[serde(default)]
138-
#[arg(long = "proxy-ca-bundle-dir", required = false)]
139136
ca_bundle_dir: PathBuf,
140137
/// System path with the CA certificate in PEM format.
141138
#[serde(default)]
142-
#[arg(long = "proxy-ca-bundle-file", required = false)]
143139
ca_bundle_file: PathBuf,
144140
/// When set to true, the HTTPS_PROXY and HTTP_PROXY environment variables are ignored, defaults to false.
145141
#[serde(default)]
146-
#[arg(long, required = false)]
147142
ignore_system_proxy: bool,
148143
}
149144

@@ -191,6 +186,22 @@ impl ProxyConfig {
191186
}
192187
}
193188

189+
impl TryFrom<crate::cli::on_host::config_gen::config::ProxyConfig> for ProxyConfig {
190+
type Error = ProxyError;
191+
192+
fn try_from(
193+
value: crate::cli::on_host::config_gen::config::ProxyConfig,
194+
) -> Result<Self, Self::Error> {
195+
let url = value.proxy_url.unwrap_or_default();
196+
Ok(Self {
197+
url: ProxyUrl::try_from(url.as_str())?,
198+
ca_bundle_dir: PathBuf::from(value.proxy_ca_bundle_dir.unwrap_or_default()),
199+
ca_bundle_file: PathBuf::from(value.proxy_ca_bundle_file.unwrap_or_default()),
200+
ignore_system_proxy: value.ignore_system_proxy,
201+
})
202+
}
203+
}
204+
194205
#[cfg(test)]
195206
pub(crate) mod tests {
196207
use super::ProxyError;

agent-control/tests/on_host/agent_control_cli.rs

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ fn test_config_generator_fleet_disabled_proxy() {
1010

1111
let mut cmd = cargo_bin_cmd!("newrelic-agent-control-cli");
1212
let args = format!(
13-
"generate-config --fleet-disabled --agent-set infra-agent --region us --proxy-url https://some.proxy.url/ --proxy-ca-bundle-dir /test/bundle/dir --proxy-ca-bundle-file /test/bundle/file --ignore-system-proxy --output-path {path}",
13+
"generate-config --fleet-disabled --agent-set infra-agent --region us --proxy-url https://some.proxy.url/ --proxy-ca-bundle-dir /test/bundle/dir --ignore-system-proxy true --output-path {path}",
1414
);
15-
cmd.args(args.split(" "));
15+
cmd.args(args.split_ascii_whitespace());
1616
cmd.assert().success();
1717

1818
let expected_value: serde_yaml::Value = serde_yaml::from_str(
@@ -25,7 +25,6 @@ agents:
2525
proxy:
2626
url: https://some.proxy.url/
2727
ca_bundle_dir: /test/bundle/dir
28-
ca_bundle_file: /test/bundle/file
2928
ignore_system_proxy: true
3029
"#,
3130
)
@@ -35,6 +34,35 @@ proxy:
3534
assert_eq!(actual_value, expected_value);
3635
}
3736

37+
#[test]
38+
fn test_config_generator_fleet_disabled_proxy_empty_fields() {
39+
let tmp = TempDir::new().unwrap();
40+
let path = tmp.path().join("output.yaml").to_string_lossy().to_string();
41+
42+
let mut cmd = cargo_bin_cmd!("newrelic-agent-control-cli");
43+
let args = format!(
44+
"generate-config --fleet-disabled --agent-set infra-agent --region us --proxy-url= --proxy-ca-bundle-dir= --proxy-ca-bundle-file= --ignore-system-proxy= --output-path {path}",
45+
);
46+
cmd.args(args.split_ascii_whitespace());
47+
cmd.assert().success();
48+
49+
let expected_value: serde_yaml::Value = serde_yaml::from_str(
50+
r#"
51+
server:
52+
enabled: true
53+
agents:
54+
nr-infra:
55+
agent_type: "newrelic/com.newrelic.infrastructure:0.1.0"
56+
proxy:
57+
ignore_system_proxy: false
58+
"#,
59+
)
60+
.unwrap();
61+
let actual_content = std::fs::read_to_string(&path).unwrap();
62+
let actual_value: serde_yaml::Value = serde_yaml::from_str(&actual_content).unwrap();
63+
assert_eq!(actual_value, expected_value);
64+
}
65+
3866
#[test]
3967
fn test_config_generator_fleet_enabled_identity_provisioned() {
4068
let tmp = TempDir::new().unwrap();
@@ -47,7 +75,7 @@ fn test_config_generator_fleet_enabled_identity_provisioned() {
4775
let args = format!(
4876
"generate-config --agent-set infra-agent --region us --fleet-id FLEET-ID --auth-client-id CLIENT-ID --auth-private-key-path {key_path} --output-path {path}",
4977
);
50-
cmd.args(args.split(" "));
78+
cmd.args(args.split_ascii_whitespace());
5179
cmd.assert().success();
5280

5381
let expected_value: serde_yaml::Value = serde_yaml::from_str(

0 commit comments

Comments
 (0)