diff --git a/src/join.rs b/src/join.rs index 4577137..7e23f55 100644 --- a/src/join.rs +++ b/src/join.rs @@ -6,7 +6,8 @@ use crate::config_json::{ get_api_endpoint, get_root_certificate, merge_config_json, read_config_json, write_config_json, ConfigMap, }; -use crate::remote::{config_url, fetch_configuration, Configuration}; +use crate::migrate::migrate_config_json; +use crate::remote::{config_url, fetch_configuration, RemoteConfiguration}; use crate::schema::{read_os_config_schema, OsConfigSchema}; use crate::systemd; use anyhow::Result; @@ -24,10 +25,10 @@ pub fn join(args: &Args) -> Result<()> { unreachable!() }; - reconfigure(args, &config_json, true) + reconfigure(args, &mut config_json, true) } -pub fn reconfigure(args: &Args, config_json: &ConfigMap, joining: bool) -> Result<()> { +pub fn reconfigure(args: &Args, config_json: &mut ConfigMap, joining: bool) -> Result<()> { let schema = read_os_config_schema(&args.os_config_path)?; let api_endpoint = if let Some(api_endpoint) = get_api_endpoint(config_json)? { @@ -39,15 +40,18 @@ pub fn reconfigure(args: &Args, config_json: &ConfigMap, joining: bool) -> Resul let root_certificate = get_root_certificate(config_json)?; - let configuration = fetch_configuration( + let remote_config = fetch_configuration( &config_url(&api_endpoint, &args.config_route), root_certificate, !joining, )?; - let has_config_changes = has_config_changes(&schema, &configuration)?; + let has_service_config_changes = has_service_config_changes(&schema, &remote_config)?; - if !has_config_changes { + let has_config_json_migrations = + migrate_config_json(&schema, &remote_config.config, config_json); + + if !has_service_config_changes && !has_config_json_migrations { info!("No configuration changes"); if !joining { @@ -61,13 +65,15 @@ pub fn reconfigure(args: &Args, config_json: &ConfigMap, joining: bool) -> Resul systemd::await_service_exit(SUPERVISOR_SERVICE)?; } + let should_write_config_json = joining || has_config_json_migrations; + let result = reconfigure_core( args, config_json, &schema, - &configuration, - has_config_changes, - joining, + &remote_config, + has_service_config_changes, + should_write_config_json, ); if args.supervisor_exists { @@ -81,25 +87,28 @@ fn reconfigure_core( args: &Args, config_json: &ConfigMap, schema: &OsConfigSchema, - configuration: &Configuration, - has_config_changes: bool, - joining: bool, + remote_config: &RemoteConfiguration, + has_service_config_changes: bool, + should_write_config_json: bool, ) -> Result<()> { - if joining { + if should_write_config_json { write_config_json(&args.config_json_path, config_json)?; } - if has_config_changes { - configure_services(schema, configuration)?; + if has_service_config_changes { + configure_services(schema, remote_config)?; } Ok(()) } -fn has_config_changes(schema: &OsConfigSchema, configuration: &Configuration) -> Result { +fn has_service_config_changes( + schema: &OsConfigSchema, + remote_config: &RemoteConfiguration, +) -> Result { for service in &schema.services { for (name, config_file) in &service.files { - let future = configuration.get_config_contents(&service.id, name)?; + let future = remote_config.get_config_contents(&service.id, name)?; let current = get_config_contents(&config_file.path); if future != current { @@ -111,7 +120,7 @@ fn has_config_changes(schema: &OsConfigSchema, configuration: &Configuration) -> Ok(false) } -fn configure_services(schema: &OsConfigSchema, configuration: &Configuration) -> Result<()> { +fn configure_services(schema: &OsConfigSchema, remote_config: &RemoteConfiguration) -> Result<()> { for service in &schema.services { for systemd_service in &service.systemd_services { systemd::stop_service(systemd_service)?; @@ -126,7 +135,7 @@ fn configure_services(schema: &OsConfigSchema, configuration: &Configuration) -> names.sort(); for name in names { let config_file = &service.files[name as &str]; - let contents = configuration.get_config_contents(&service.id, name)?; + let contents = remote_config.get_config_contents(&service.id, name)?; let mode = fs::parse_mode(&config_file.perm)?; fs::write_file(Path::new(&config_file.path), contents, mode)?; info!("{} updated", &config_file.path); diff --git a/src/main.rs b/src/main.rs index e2a7b61..3a83238 100644 --- a/src/main.rs +++ b/src/main.rs @@ -33,6 +33,7 @@ mod generate; mod join; mod leave; mod logger; +mod migrate; mod random; mod remote; mod schema; diff --git a/src/migrate.rs b/src/migrate.rs new file mode 100644 index 0000000..8ee5042 --- /dev/null +++ b/src/migrate.rs @@ -0,0 +1,130 @@ +// Config.json migration module +// +// Provides methods for migrating config.json fields based on remote directives +// from /os/vX/config. Limits migrated fields based on os-config.json schema +// whitelist. + +use crate::config_json::ConfigMap; +use crate::remote::{ConfigMigrationInstructions, OverridesMap}; +use crate::schema::OsConfigSchema; + +pub fn migrate_config_json( + schema: &OsConfigSchema, + migration: &ConfigMigrationInstructions, + config_json: &mut ConfigMap, +) -> bool { + info!("Checking for config.json migrations..."); + + let overridden = handle_override_directives(schema, &migration.overrides, config_json); + + if overridden { + info!("Done config.json migrations"); + } + + overridden +} + +fn handle_override_directives( + schema: &OsConfigSchema, + overrides: &OverridesMap, + config_json: &mut ConfigMap, +) -> bool { + let mut overridden = false; + + // Sort overrides by key in order for tests to have predictable order + let mut items = overrides.iter().collect::>(); + items.sort_by_key(|pair| pair.0); + + for (key, new_value) in items { + if !schema.config.whitelist.contains(key) { + info!("Key `{}` not in whitelist, skipping", key); + continue; + } + + if let Some(existing_value) = config_json.get_mut(key) { + if new_value != existing_value { + info!( + "Key `{}` found with existing value `{}`, will override to `{}`", + key, existing_value, new_value + ); + *existing_value = new_value.clone(); + overridden = true; + } else { + debug!( + "Key `{}` found with existing value `{}` equal to override value `{}`, skipping", + key, existing_value, new_value + ); + } + } else { + info!("Key `{}` not found, will insert `{}`", key, new_value); + config_json.insert(key.to_string(), new_value.clone()); + overridden = true; + } + } + + overridden +} + +mod tests { + #[test] + fn test_generate_config_json_migration() { + let config_json = r#" + { + "deadbeef": 1, + "deadca1f": "2", + "deadca2f": true, + "deadca3f": "string1" + } + "# + .to_string(); + + let schema = r#" + { + "services": [ + ], + "keys": [], + "config": { + "whitelist": [ + "deadbeef", + "deadca1f", + "deadca2f", + "deadca3f", + "deadca4f" + ] + } + } + "# + .to_string(); + + let configuration = unindent::unindent( + r#" + { + "overrides": { + "deadbeef": 2, + "deadca1f": "3", + "deadca2f": false, + "deadca3f": "string0", + "deadca4f": "new_field", + "not_on_whitelist1": "not_on_whitelist" + } + } + "#, + ); + + let mut config = serde_json::from_str::(&config_json).unwrap(); + + let has_config_json_migrations = super::migrate_config_json( + &serde_json::from_str(&schema).unwrap(), + &serde_json::from_str(&configuration).unwrap(), + &mut config, + ); + + assert!(has_config_json_migrations); + assert_eq!(config.get("deadbeef").unwrap(), 2); + assert_eq!(config.get("deadca1f").unwrap(), "3"); + assert_eq!(config.get("deadca2f").unwrap(), false); + assert_eq!(config.get("deadca3f").unwrap(), "string0"); + assert_eq!(config.get("deadca4f").unwrap(), "new_field"); + assert!(config.get("not_on_whitelist1").is_none()); + } +} diff --git a/src/remote.rs b/src/remote.rs index f1f9378..cadc9f4 100644 --- a/src/remote.rs +++ b/src/remote.rs @@ -2,16 +2,22 @@ use std::collections::HashMap; use std::thread; use std::time::Duration; -use crate::schema::validate_schema_version; use anyhow::{anyhow, Context, Result}; +pub type OverridesMap = HashMap; + #[derive(Debug, Serialize, Deserialize, PartialEq)] -pub struct Configuration { +pub struct RemoteConfiguration { pub services: HashMap>, - pub schema_version: String, + pub config: ConfigMigrationInstructions, } -impl Configuration { +#[derive(Debug, Serialize, Deserialize, PartialEq)] +pub struct ConfigMigrationInstructions { + pub overrides: OverridesMap, +} + +impl RemoteConfiguration { pub fn get_config_contents<'a>( &'a self, service_id: &str, @@ -42,7 +48,7 @@ pub fn fetch_configuration( config_url: &str, root_certificate: Option, retry: bool, -) -> Result { +) -> Result { fetch_configuration_impl(config_url, root_certificate, retry) .context("Fetching configuration failed") } @@ -51,7 +57,7 @@ fn fetch_configuration_impl( config_url: &str, root_certificate: Option, retry: bool, -) -> Result { +) -> Result { let client = build_reqwest_client(root_certificate)?; let request_fn = if retry { @@ -66,8 +72,6 @@ fn fetch_configuration_impl( info!("Service configuration retrieved"); - validate_schema_version(&json_data)?; - Ok(serde_json::from_str(&json_data)?) } @@ -141,7 +145,6 @@ fn build_reqwest_client( mod tests { use super::*; - use crate::schema::SCHEMA_VERSION; const JSON_DATA: &str = r#"{ "services": { @@ -155,14 +158,18 @@ mod tests { "authorized_keys": "authorized keys here" } }, - "schema_version": "1.0.0" + "config": { + "overrides": { + "logsEndpoint": "https://logs.balenadev.io" + } + } }"#; #[test] - fn parse_configuration_v1() { - let parsed: Configuration = serde_json::from_str(JSON_DATA).unwrap(); + fn parse_configuration() { + let parsed: RemoteConfiguration = serde_json::from_str(JSON_DATA).unwrap(); - let expected = Configuration { + let expected = RemoteConfiguration { services: hashmap! { "openvpn".into() => hashmap!{ "config".into() => "main configuration here".into(), @@ -174,7 +181,11 @@ mod tests { "authorized_keys".into() => "authorized keys here".into() } }, - schema_version: SCHEMA_VERSION.into(), + config: ConfigMigrationInstructions { + overrides: hashmap! { + "logsEndpoint".into() => "https://logs.balenadev.io".into() + }, + }, }; assert_eq!(parsed, expected); diff --git a/src/schema.rs b/src/schema.rs index 0ed83fc..fba2651 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -1,18 +1,14 @@ +use crate::fs::read_file; +use anyhow::{Context, Result}; use std::collections::HashMap; use std::path::Path; -use serde_json::Value; - -use crate::fs::read_file; -use anyhow::{bail, Context, Result}; - -pub const SCHEMA_VERSION: &str = "1.0.0"; - #[derive(Debug, Serialize, Deserialize, PartialEq)] pub struct OsConfigSchema { pub services: Vec, + // Fields that should be removed from config.json when leaving a cloud env (`balena leave`) pub keys: Vec, - pub schema_version: String, + pub config: ConfigJsonSchema, } #[derive(Debug, Serialize, Deserialize, PartialEq)] @@ -28,6 +24,12 @@ pub struct ConfigFile { pub perm: String, } +#[derive(Debug, Serialize, Deserialize, PartialEq)] +pub struct ConfigJsonSchema { + // Fields that may be modified in config.json + pub whitelist: Vec, +} + pub fn read_os_config_schema(os_config_path: &Path) -> Result { read_os_config_schema_impl(os_config_path).context("Reading `os-config.json` schema failed") } @@ -35,33 +37,9 @@ pub fn read_os_config_schema(os_config_path: &Path) -> Result { fn read_os_config_schema_impl(os_config_path: &Path) -> Result { let json_data = read_file(os_config_path)?; - validate_schema_version(&json_data)?; - Ok(serde_json::from_str(&json_data)?) } -pub fn validate_schema_version(json_data: &str) -> Result<()> { - let parsed: Value = serde_json::from_str(json_data)?; - - match parsed.get("schema_version") { - Some(version_value) => match version_value.as_str() { - Some(schema_version) => { - if schema_version == SCHEMA_VERSION { - Ok(()) - } else { - bail!( - "Expected schema version {}, got {}", - SCHEMA_VERSION, - schema_version - ) - } - } - _ => bail!("`schema_version` should be a string"), - }, - _ => bail!("Missing `schema_version`"), - } -} - #[cfg(test)] mod tests { @@ -98,7 +76,9 @@ mod tests { } ], "keys": ["apiKey", "apiEndpoint", "vpnEndpoint"], - "schema_version": "1.0.0" + "config": { + "whitelist": ["logsEndpoint"] + } }"#; #[test] @@ -133,7 +113,9 @@ mod tests { }, ], keys: vec!["apiKey".into(), "apiEndpoint".into(), "vpnEndpoint".into()], - schema_version: SCHEMA_VERSION.into(), + config: ConfigJsonSchema { + whitelist: vec!["logsEndpoint".into()], + }, }; assert_eq!(parsed, expected); diff --git a/src/update.rs b/src/update.rs index 1f521f1..1e25002 100644 --- a/src/update.rs +++ b/src/update.rs @@ -6,7 +6,7 @@ use crate::config_json::read_config_json; use crate::join::reconfigure; pub fn update(args: &Args) -> Result<()> { - let config_json = read_config_json(&args.config_json_path)?; + let mut config_json = read_config_json(&args.config_json_path)?; - reconfigure(args, &config_json, false) + reconfigure(args, &mut config_json, false) } diff --git a/tests/integration.rs b/tests/integration.rs index d83a526..5af4995 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -77,7 +77,9 @@ fn join() { }} ], "keys": ["apiKey", "apiEndpoint", "vpnEndpoint"], - "schema_version": "1.0.0" + "config": {{ + "whitelist": ["logsEndpoint"] + }} }} "# ); @@ -99,7 +101,9 @@ fn join() { "mock-3": "MOCK-3-0123456789" } }, - "schema_version": "1.0.0" + "config": { + "overrides": {} + } } "#, ); @@ -135,6 +139,7 @@ fn join() { r#" Fetching service configuration from http://localhost:{port}/os/v1/config... Service configuration retrieved + Checking for config.json migrations... Stopping balena-supervisor.service... Awaiting balena-supervisor.service to exit... Writing {tmp_dir_path}/config.json @@ -257,7 +262,9 @@ fn join_flasher() { }} ], "keys": ["apiKey", "apiEndpoint", "vpnEndpoint"], - "schema_version": "1.0.0" + "config": {{ + "whitelist": ["logsEndpoint"] + }} }} "# ); @@ -272,7 +279,9 @@ fn join_flasher() { "mock-1": "MOCK-1-АБВГДЕЖЗИЙ" } }, - "schema_version": "1.0.0" + "config": { + "overrides": {} + } } "#, ); @@ -308,6 +317,7 @@ fn join_flasher() { r#" Fetching service configuration from http://localhost:{port}/os/v1/config... Service configuration retrieved + Checking for config.json migrations... Stopping balena-supervisor.service... Awaiting balena-supervisor.service to exit... Writing {tmp_dir_path}/config.json @@ -392,7 +402,9 @@ fn join_with_root_certificate() { "services": [ ], "keys": ["apiKey", "apiEndpoint", "vpnEndpoint"], - "schema_version": "1.0.0" + "config": { + "whitelist": ["logsEndpoint"] + } } "#; @@ -403,7 +415,9 @@ fn join_with_root_certificate() { { "services": { }, - "schema_version": "1.0.0" + "config": { + "overrides": {} + } } "#, ); @@ -441,6 +455,7 @@ fn join_with_root_certificate() { r#" Fetching service configuration from https://localhost:{port}/os/v1/config... Service configuration retrieved + Checking for config.json migrations... No configuration changes Stopping balena-supervisor.service... Awaiting balena-supervisor.service to exit... @@ -515,7 +530,9 @@ fn incompatible_device_types() { "services": [ ], "keys": ["apiKey", "apiEndpoint", "vpnEndpoint"], - "schema_version": "1.0.0" + "config": { + "whitelist": ["logsEndpoint"] + } } "#, ); @@ -608,7 +625,9 @@ fn reconfigure() { "services": [ ], "keys": ["apiKey", "apiEndpoint", "vpnEndpoint"], - "schema_version": "1.0.0" + "config": { + "whitelist": ["logsEndpoint"] + } } "#, ); @@ -620,7 +639,9 @@ fn reconfigure() { { "services": { }, - "schema_version": "1.0.0" + "config": { + "overrides": {} + } } "#, ); @@ -656,6 +677,7 @@ fn reconfigure() { r#" Fetching service configuration from http://localhost:{port}/os/v1/config... Service configuration retrieved + Checking for config.json migrations... No configuration changes Stopping balena-supervisor.service... Awaiting balena-supervisor.service to exit... @@ -751,7 +773,9 @@ fn reconfigure_stored() { "services": [ ], "keys": ["apiKey", "apiEndpoint", "vpnEndpoint"], - "schema_version": "1.0.0" + "config": { + "whitelist": ["logsEndpoint"] + } } "#, ); @@ -763,7 +787,9 @@ fn reconfigure_stored() { { "services": { }, - "schema_version": "1.0.0" + "config": { + "overrides": {} + } } "#, ); @@ -799,6 +825,7 @@ fn reconfigure_stored() { r#" Fetching service configuration from http://localhost:{port}/os/v1/config... Service configuration retrieved + Checking for config.json migrations... No configuration changes Stopping balena-supervisor.service... Awaiting balena-supervisor.service to exit... @@ -935,7 +962,9 @@ fn update() { }} ], "keys": ["apiKey", "apiEndpoint", "vpnEndpoint"], - "schema_version": "1.0.0" + "config": {{ + "whitelist": ["logsEndpoint"] + }} }} "# ); @@ -961,7 +990,9 @@ fn update() { "mock-3": "MOCK-3-0123456789" } }, - "schema_version": "1.0.0" + "config": { + "overrides": {} + } } "#, ); @@ -972,6 +1003,7 @@ fn update() { r#" Fetching service configuration from http://localhost:{port}/os/v1/config... Service configuration retrieved + Checking for config.json migrations... Stopping balena-supervisor.service... Awaiting balena-supervisor.service to exit... {tmp_dir_path}/not-a-service-1.conf updated @@ -1097,7 +1129,9 @@ fn update_no_config_changes() { }} ], "keys": ["apiKey", "apiEndpoint", "vpnEndpoint"], - "schema_version": "1.0.0" + "config": {{ + "whitelist": ["logsEndpoint"] + }} }} "# ); @@ -1122,7 +1156,9 @@ fn update_no_config_changes() { "mock-3": "MOCK-3-0123456789" } }, - "schema_version": "1.0.0" + "config": { + "overrides": {} + } } "#, ); @@ -1133,6 +1169,7 @@ fn update_no_config_changes() { r#" Fetching service configuration from http://localhost:{port}/os/v1/config... Service configuration retrieved + Checking for config.json migrations... No configuration changes "#, )); @@ -1211,7 +1248,9 @@ fn update_with_root_certificate() { "services": [ ], "keys": ["apiKey", "apiEndpoint", "vpnEndpoint"], - "schema_version": "1.0.0" + "config": { + "whitelist": ["logsEndpoint"] + } } "#; @@ -1222,7 +1261,9 @@ fn update_with_root_certificate() { { "services": { }, - "schema_version": "1.0.0" + "config": { + "overrides": {} + } } "#, ); @@ -1233,6 +1274,7 @@ fn update_with_root_certificate() { r#" Fetching service configuration from https://localhost:{port}/os/v1/config... Service configuration retrieved + Checking for config.json migrations... No configuration changes "#, )); @@ -1271,7 +1313,9 @@ fn update_unmanaged() { "services": [ ], "keys": ["apiKey", "apiEndpoint", "vpnEndpoint"], - "schema_version": "1.0.0" + "config": { + "whitelist": ["logsEndpoint"] + } } "#; @@ -1282,7 +1326,9 @@ fn update_unmanaged() { { "services": { }, - "schema_version": "1.0.0" + "config": { + "overrides": {} + } } "#, ); @@ -1361,7 +1407,9 @@ fn leave() { }} ], "keys": ["apiKey", "apiEndpoint", "vpnEndpoint", "vpnPort", "registryEndpoint", "deltaEndpoint"], - "schema_version": "1.0.0" + "config": {{ + "whitelist": ["logsEndpoint"] + }} }} "# ); @@ -1378,7 +1426,9 @@ fn leave() { "mock-3": "MOCK-3-0123456789" } }, - "schema_version": "1.0.0" + "config": { + "overrides": {} + } } "#, ); @@ -1461,7 +1511,9 @@ fn leave_unmanaged() { "services": [ ], "keys": ["apiKey", "apiEndpoint", "vpnEndpoint"], - "schema_version": "1.0.0" + "config": { + "whitelist": ["logsEndpoint"] + } } "#, ); @@ -1473,7 +1525,9 @@ fn leave_unmanaged() { { "services": { }, - "schema_version": "1.0.0" + "config": { + "overrides": {} + } } "#, ); @@ -1517,7 +1571,9 @@ fn generate_api_key_unmanaged() { "services": [ ], "keys": ["apiKey", "apiEndpoint", "vpnEndpoint"], - "schema_version": "1.0.0" + "config": { + "whitelist": ["logsEndpoint"] + } } "#, ); @@ -1576,7 +1632,9 @@ fn generate_api_key_already_generated() { "services": [ ], "keys": ["apiKey", "apiEndpoint", "vpnEndpoint"], - "schema_version": "1.0.0" + "config": { + "whitelist": ["logsEndpoint"] + } } "#, ); @@ -1635,7 +1693,9 @@ fn generate_api_key_reuse() { "services": [ ], "keys": ["apiKey", "apiEndpoint", "vpnEndpoint"], - "schema_version": "1.0.0" + "config": { + "whitelist": ["logsEndpoint"] + } } "#, ); @@ -1717,7 +1777,9 @@ fn generate_api_key_new() { "services": [ ], "keys": ["apiKey", "apiEndpoint", "vpnEndpoint"], - "schema_version": "1.0.0" + "config": { + "whitelist": ["logsEndpoint"] + } } "#, ); @@ -1764,6 +1826,234 @@ fn generate_api_key_new() { ); } +#[test] +#[timeout(10000)] +fn migrate_config_json() { + let port = 31014; + let tmp_dir = TempDir::new().unwrap(); + let tmp_dir_path = tmp_dir.path().to_str().unwrap().to_string(); + + let config_json = format!( + r#" + {{ + "deviceApiKey": "abcdef", + "deviceType": "intel-nuc", + "hostname": "balena", + "persistentLogging": false, + "applicationName": "aaaaaa", + "applicationId": 1234567, + "userId": 654321, + "appUpdatePollInterval": 900000, + "listenPort": 48484, + "vpnPort": 443, + "apiEndpoint": "http://{}", + "vpnEndpoint": "vpn.balenadev.io", + "registryEndpoint": "registry2.balenadev.io", + "deltaEndpoint": "https://delta.balenadev.io", + "apiKey": "12345678abcd1234efgh1234567890ab", + "version": "9.99.9+rev1", + "deadbeef": "12345678", + "mixpanelToken": "12345678abcd1234efgh1234567890ab" + }} + "#, + server_address(port) + ); + + let config_json_path = create_tmp_file(&tmp_dir, "config.json", &config_json, None); + + let schema = r#" + { + "services": [ + ], + "keys": ["apiKey", "apiEndpoint", "vpnEndpoint"], + "config": { + "whitelist": [ + "logsEndpoint", + "mixpanelToken", + "registryEndpoint", + "deltaEndpoint", + "applicationId", + "persistentLogging", + "deadbeef" + ] + } + } + "# + .to_string(); + + let os_config_path = create_tmp_file(&tmp_dir, "os-config.json", &schema, None); + + // - apiEndpoint: not on whitelist, should skip + // - logsEndpoint: value not present, should insert + // - registryEndpoint: value unchanged, should skip + // - deltaEndpoint: value changed, should update (String) + // - applicationId: value changed, should update (u64) + // - persistentLogging: value changed, should update (bool) + // - deadbeef: value changed, should update (stringified u64) + let configuration = unindent::unindent( + r#" + { + "services": {}, + "config": { + "overrides": { + "apiEndpoint": "http://api.balenadev.io", + "logsEndpoint": "https://logs.balenadev.io", + "registryEndpoint": "registry2.balenadev.io", + "deltaEndpoint": "https://delta2.balenadev.io", + "applicationId": 1234568, + "persistentLogging": true, + "deadbeef": "1234567890" + } + } + } + "#, + ); + + let mut serve = serve_config(configuration, false, port); + + let output = unindent::unindent(&format!( + r#" + Fetching service configuration from http://localhost:{port}/os/v1/config... + Service configuration retrieved + Checking for config.json migrations... + Key `apiEndpoint` not in whitelist, skipping + Key `applicationId` found with existing value `1234567`, will override to `1234568` + Key `deadbeef` found with existing value `"12345678"`, will override to `"1234567890"` + Key `deltaEndpoint` found with existing value `"https://delta.balenadev.io"`, will override to `"https://delta2.balenadev.io"` + Key `logsEndpoint` not found, will insert `"https://logs.balenadev.io"` + Key `persistentLogging` found with existing value `false`, will override to `true` + Done config.json migrations + Stopping balena-supervisor.service... + Awaiting balena-supervisor.service to exit... + Writing {tmp_dir_path}/config.json + Starting balena-supervisor.service... + "# + )); + + get_base_command() + .args(["update"]) + .timeout(Duration::from_secs(5)) + .envs(os_config_env(&os_config_path, &config_json_path)) + .assert() + .success() + .stdout(output); + + validate_json_file( + &config_json_path, + &format!( + r#" + {{ + "deviceApiKey": "abcdef", + "deviceType": "intel-nuc", + "hostname": "balena", + "persistentLogging": true, + "applicationName": "aaaaaa", + "applicationId": 1234568, + "userId": 654321, + "appUpdatePollInterval": 900000, + "listenPort": 48484, + "vpnPort": 443, + "apiEndpoint": "http://{}", + "vpnEndpoint": "vpn.balenadev.io", + "registryEndpoint": "registry2.balenadev.io", + "deltaEndpoint": "https://delta2.balenadev.io", + "apiKey": "12345678abcd1234efgh1234567890ab", + "version": "9.99.9+rev1", + "deadbeef": "1234567890", + "mixpanelToken": "12345678abcd1234efgh1234567890ab", + "logsEndpoint": "https://logs.balenadev.io" + }} + "#, + server_address(port) + ), + false, + ); + + serve.stop(); +} + +#[test] +#[timeout(10000)] +fn ignore_unknown_cloud_config_fields() { + let port = 31015; + let tmp_dir = TempDir::new().unwrap(); + + let config_json = format!( + r#" + {{ + "deviceApiKey": "abcdef", + "deviceType": "intel-nuc", + "hostname": "balena", + "persistentLogging": false, + "applicationName": "aaaaaa", + "applicationId": 1234567, + "userId": 654321, + "appUpdatePollInterval": 900000, + "listenPort": 48484, + "vpnPort": 443, + "apiEndpoint": "http://{}", + "vpnEndpoint": "vpn.balenadev.io", + "registryEndpoint": "registry2.balenadev.io", + "deltaEndpoint": "https://delta.balenadev.io", + "apiKey": "12345678abcd1234efgh1234567890ab", + "version": "9.99.9+rev1", + "mixpanelToken": "12345678abcd1234efgh1234567890ab" + }} + "#, + server_address(port) + ); + + let config_json_path = create_tmp_file(&tmp_dir, "config.json", &config_json, None); + + let schema = r#" + { + "services": [ + ], + "keys": ["apiKey", "apiEndpoint", "vpnEndpoint"], + "config": { + "whitelist": [] + } + } + "# + .to_string(); + + let os_config_path = create_tmp_file(&tmp_dir, "os-config.json", &schema, None); + + let configuration = unindent::unindent( + r#" + { + "services": {}, + "config": { + "overrides": {} + }, + "unknown_field": "unknown_value" + } + "#, + ); + + let mut serve = serve_config(configuration, false, port); + + let output = unindent::unindent(&format!( + r#" + Fetching service configuration from http://localhost:{port}/os/v1/config... + Service configuration retrieved + Checking for config.json migrations... + No configuration changes + "#, + )); + + get_base_command() + .args(["update"]) + .timeout(Duration::from_secs(5)) + .envs(os_config_env(&os_config_path, &config_json_path)) + .assert() + .success() + .stdout(output); + + validate_json_file(&config_json_path, &config_json, false); + + serve.stop(); +} /******************************************************************************* * os-config launch */