|
1 | | -use crate::cli::error::CliError; |
| 1 | +//! Implementation of the generate-config command for the on-host cli. |
2 | 2 |
|
| 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. |
3 | 24 | #[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 | + |
5 | 38 | /// Fleet identifier |
6 | 39 | #[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>, |
9 | 76 | } |
10 | 77 |
|
11 | 78 | /// 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 | +"#; |
14 | 305 | } |
0 commit comments