diff --git a/README.md b/README.md index ac53a06..6c5f1c9 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,16 @@ A Rust CLI tool for migrating Apache NiFi flow.json files between versions. - **Reason**: In NiFi 2.x, Jolt processors were moved to a separate bundle and properties were renamed - **Reference**: [NIFI-12554](https://issues.apache.org/jira/browse/NIFI-12554) +### Distributed Cache Controller Services + +All Distributed Cache services have been renamed to remove the "Distributed" prefix for clarity in NiFi 2.x. + +- **DistributedMapCacheClientService** → **MapCacheClientService** +- **DistributedSetCacheClientService** → **SetCacheClientService** +- **DistributedMapCacheServer** → **MapCacheServer** +- **DistributedSetCacheServer** → **SetCacheServer** +- **Reference**: [NIFI-13596](https://issues.apache.org/jira/browse/NIFI-13596) + ## Build ```bash diff --git a/src/migration.rs b/src/migration.rs index b1c433d..14289e2 100644 --- a/src/migration.rs +++ b/src/migration.rs @@ -4,7 +4,10 @@ mod rules; use anyhow::{Context, Result}; -use rules::{JoltTransformJsonMigration, JoltTransformRecordMigration, MigrationRule}; +use rules::{ + DistributedCacheServicesMigration, JoltTransformJsonMigration, JoltTransformRecordMigration, + MigrationRule, +}; use serde_json::Value; use std::fs; use std::path::Path; @@ -26,8 +29,11 @@ impl Default for Migrator { fn default() -> Self { Self { rules: vec![ + // Processor migrations Box::new(JoltTransformJsonMigration), Box::new(JoltTransformRecordMigration), + // Controller service migrations + Box::new(DistributedCacheServicesMigration), ], } } @@ -110,27 +116,22 @@ impl Migrator { fn process_value_with_context( &self, value: &mut Value, - parent_key: Option<&str>, + _parent_key: Option<&str>, changes: &mut Vec, ) { match value { Value::Object(map) => { - // Check if this object is a processor (but not a controller service) - // Controller services have the same structure as processors (type + bundle) - // but appear under "controllerServices" key instead of "processors" key - // This entire matching thing (as well as the migration rules) can be made smarter - // as needed. For now, we only have two rules and both are for processors so it's - // fine as is. - let is_processor = map.contains_key("type") - && map.contains_key("bundle") - && parent_key != Some("controllerServices"); - - if is_processor { - self.process_processor(value, changes); + // Check if this object is a processor or controller service. + // Both have the same structure (type + bundle fields). + let has_type_and_bundle = map.contains_key("type") && map.contains_key("bundle"); + + if has_type_and_bundle { + // Apply migration rules (works for both processors and controller services) + self.process_component(value, changes); } - // Recursively process all nested values - // Need to re-borrow to avoid double mutable borrow + // Recursively process all nested values. + // Need to re-borrow to avoid double mutable borrow. if let Value::Object(map) = value { for (key, val) in map.iter_mut() { self.process_value_with_context(val, Some(key), changes); @@ -139,33 +140,33 @@ impl Migrator { } Value::Array(arr) => { for item in arr.iter_mut() { - self.process_value_with_context(item, parent_key, changes); + self.process_value_with_context(item, _parent_key, changes); } } _ => {} } } - /// Process a single processor object. - fn process_processor(&self, processor: &mut Value, changes: &mut Vec) { + /// Process a single component (processor or controller service). + fn process_component(&self, component: &mut Value, changes: &mut Vec) { for rule in &self.rules { - if rule.applies(processor) && rule.apply(processor) { - let processor_id = processor + if rule.applies(component) && rule.apply(component) { + let component_id = component .get("identifier") - .or_else(|| processor.get("id")) + .or_else(|| component.get("id")) .and_then(|v| v.as_str()) .unwrap_or("unknown") .to_owned(); - let processor_name = processor + let component_name = component .get("name") .and_then(|v| v.as_str()) .unwrap_or("unnamed") .to_owned(); changes.push(MigrationChange { - processor_id, - processor_name, + processor_id: component_id, + processor_name: component_name, rule_description: rule.description(), }); } @@ -287,4 +288,91 @@ mod tests { // Verify jolt processor changed assert_eq!(flow["processors"][1]["bundle"]["artifact"], "nifi-jolt-nar"); } + + #[test] + fn test_controller_service_migration() { + let mut flow = json!({ + "flowContents": { + "controllerServices": [ + { + "identifier": "service-1", + "name": "MapCacheClient", + "type": "org.apache.nifi.distributed.cache.client.DistributedMapCacheClientService", + "bundle": { + "artifact": "nifi-distributed-cache-services-nar" + } + } + ] + } + }); + + let migrator = Migrator::default(); + let changes = migrator.migrate_flow(&mut flow); + + assert_eq!(changes.len(), 1); + assert_eq!(changes[0].processor_id, "service-1"); + assert_eq!(changes[0].processor_name, "MapCacheClient"); + + // Verify the controller service was migrated + assert_eq!( + flow["flowContents"]["controllerServices"][0]["type"], + "org.apache.nifi.distributed.cache.client.MapCacheClientService" + ); + } + + #[test] + fn test_mixed_processors_and_controller_services() { + let mut flow = json!({ + "processors": [ + { + "identifier": "proc-1", + "name": "JoltProc", + "type": "org.apache.nifi.processors.standard.JoltTransformJSON", + "bundle": { + "artifact": "nifi-standard-nar" + } + } + ], + "controllerServices": [ + { + "identifier": "service-1", + "name": "MapCache", + "type": "org.apache.nifi.distributed.cache.client.DistributedMapCacheClientService", + "bundle": { + "artifact": "nifi-distributed-cache-services-nar" + } + }, + { + "identifier": "service-2", + "name": "SetCache", + "type": "org.apache.nifi.distributed.cache.client.DistributedSetCacheClientService", + "bundle": { + "artifact": "nifi-distributed-cache-services-nar" + } + } + ] + }); + + let migrator = Migrator::default(); + let changes = migrator.migrate_flow(&mut flow); + + // Should migrate 1 processor + 2 controller services = 3 total + assert_eq!(changes.len(), 3); + + // Verify processor migration + assert_eq!( + flow["processors"][0]["type"], + "org.apache.nifi.processors.jolt.JoltTransformJSON" + ); + + // Verify controller service migrations + assert_eq!( + flow["controllerServices"][0]["type"], + "org.apache.nifi.distributed.cache.client.MapCacheClientService" + ); + assert_eq!( + flow["controllerServices"][1]["type"], + "org.apache.nifi.distributed.cache.client.SetCacheClientService" + ); + } } diff --git a/src/migration/rules.rs b/src/migration/rules.rs index ff59edc..7bde20c 100644 --- a/src/migration/rules.rs +++ b/src/migration/rules.rs @@ -1,21 +1,23 @@ // SPDX-FileCopyrightText: 2025 Stackable GmbH // SPDX-License-Identifier: Apache-2.0 +mod distributed_cache_services; mod jolt_transform_json; mod jolt_transform_record; +pub use distributed_cache_services::DistributedCacheServicesMigration; pub use jolt_transform_json::JoltTransformJsonMigration; pub use jolt_transform_record::JoltTransformRecordMigration; use serde_json::Value; -/// Represents a migration rule that can be applied to processors. +/// Represents a migration rule that can be applied to processors and controller services. pub trait MigrationRule { - /// Check if this rule applies to the given processor. - fn applies(&self, processor: &Value) -> bool; + /// Check if this rule applies to the given component. + fn applies(&self, component: &Value) -> bool; - /// Apply the migration to the processor, returning `true` if changes were made. - fn apply(&self, processor: &mut Value) -> bool; + /// Apply the migration to the component, returning `true` if changes were made. + fn apply(&self, component: &mut Value) -> bool; /// Get a description of what this rule does. fn description(&self) -> String; diff --git a/src/migration/rules/distributed_cache_services.rs b/src/migration/rules/distributed_cache_services.rs new file mode 100644 index 0000000..233e8f5 --- /dev/null +++ b/src/migration/rules/distributed_cache_services.rs @@ -0,0 +1,252 @@ +// SPDX-FileCopyrightText: 2025 Stackable GmbH +// SPDX-License-Identifier: Apache-2.0 + +use super::MigrationRule; +use serde_json::Value; + +/// Migration rule for Distributed Cache controller services. +/// +/// Migrates all Distributed Cache services by removing "Distributed" from their class names. +/// This includes both client and server services for Map and Set caches. +/// +/// Migrations: +/// - `DistributedMapCacheClientService` → `MapCacheClientService` +/// - `DistributedSetCacheClientService` → `SetCacheClientService` +/// - `DistributedMapCacheServer` → `MapCacheServer` +/// - `DistributedSetCacheServer` → `SetCacheServer` +/// +/// All services remain in the same bundle: `nifi-distributed-cache-services-nar`. +/// +/// Reference: +pub struct DistributedCacheServicesMigration; + +const CACHE_SERVICE_MIGRATIONS: [(&str, &str); 4] = [ + ( + "org.apache.nifi.distributed.cache.client.DistributedMapCacheClientService", + "org.apache.nifi.distributed.cache.client.MapCacheClientService", + ), + ( + "org.apache.nifi.distributed.cache.client.DistributedSetCacheClientService", + "org.apache.nifi.distributed.cache.client.SetCacheClientService", + ), + ( + "org.apache.nifi.distributed.cache.server.map.DistributedMapCacheServer", + "org.apache.nifi.distributed.cache.server.map.MapCacheServer", + ), + ( + "org.apache.nifi.distributed.cache.server.set.DistributedSetCacheServer", + "org.apache.nifi.distributed.cache.server.set.SetCacheServer", + ), +]; + +impl MigrationRule for DistributedCacheServicesMigration { + fn applies(&self, component: &Value) -> bool { + if let Some(type_str) = component.get("type").and_then(|t| t.as_str()) { + CACHE_SERVICE_MIGRATIONS + .iter() + .any(|(old_type, _)| type_str == *old_type) + } else { + false + } + } + + fn apply(&self, component: &mut Value) -> bool { + let mut changed = false; + + if let Some(type_field) = component.get_mut("type") { + if let Some(current_type) = type_field.as_str() { + for (old_type, new_type) in CACHE_SERVICE_MIGRATIONS { + if current_type == old_type { + *type_field = Value::String(new_type.to_string()); + changed = true; + break; + } + } + } + } + + changed + } + + fn description(&self) -> String { + "Migrate Distributed Cache services (remove 'Distributed' prefix)".to_owned() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use pretty_assertions::assert_eq; + use serde_json::json; + + #[test] + fn test_map_cache_client_migration() { + let mut service = json!({ + "identifier": "test-service-123", + "name": "MapCacheClientService", + "type": "org.apache.nifi.distributed.cache.client.DistributedMapCacheClientService", + "bundle": { + "group": "org.apache.nifi", + "artifact": "nifi-distributed-cache-services-nar", + "version": "1.27.0" + } + }); + + let rule = DistributedCacheServicesMigration; + assert!(rule.applies(&service)); + assert!(rule.apply(&mut service)); + + assert_eq!( + service.get("type").and_then(|v| v.as_str()), + Some("org.apache.nifi.distributed.cache.client.MapCacheClientService") + ); + } + + #[test] + fn test_set_cache_client_migration() { + let mut service = json!({ + "identifier": "test-service-456", + "name": "SetCacheClientService", + "type": "org.apache.nifi.distributed.cache.client.DistributedSetCacheClientService", + "bundle": { + "group": "org.apache.nifi", + "artifact": "nifi-distributed-cache-services-nar", + "version": "1.27.0" + } + }); + + let rule = DistributedCacheServicesMigration; + assert!(rule.applies(&service)); + assert!(rule.apply(&mut service)); + + assert_eq!( + service.get("type").and_then(|v| v.as_str()), + Some("org.apache.nifi.distributed.cache.client.SetCacheClientService") + ); + } + + #[test] + fn test_map_cache_server_migration() { + let mut service = json!({ + "identifier": "test-server-123", + "name": "MapCacheServer", + "type": "org.apache.nifi.distributed.cache.server.map.DistributedMapCacheServer", + "bundle": { + "group": "org.apache.nifi", + "artifact": "nifi-distributed-cache-services-nar", + "version": "1.27.0" + } + }); + + let rule = DistributedCacheServicesMigration; + assert!(rule.applies(&service)); + assert!(rule.apply(&mut service)); + + assert_eq!( + service.get("type").and_then(|v| v.as_str()), + Some("org.apache.nifi.distributed.cache.server.map.MapCacheServer") + ); + } + + #[test] + fn test_set_cache_server_migration() { + let mut service = json!({ + "identifier": "test-server-456", + "name": "SetCacheServer", + "type": "org.apache.nifi.distributed.cache.server.set.DistributedSetCacheServer", + "bundle": { + "group": "org.apache.nifi", + "artifact": "nifi-distributed-cache-services-nar", + "version": "1.27.0" + } + }); + + let rule = DistributedCacheServicesMigration; + assert!(rule.applies(&service)); + assert!(rule.apply(&mut service)); + + assert_eq!( + service.get("type").and_then(|v| v.as_str()), + Some("org.apache.nifi.distributed.cache.server.set.SetCacheServer") + ); + } + + #[test] + fn test_does_not_apply_to_other_services() { + let service = json!({ + "identifier": "other-service", + "name": "SomeOtherService", + "type": "org.apache.nifi.other.SomeService", + "bundle": { + "artifact": "nifi-standard-nar" + } + }); + + let rule = DistributedCacheServicesMigration; + assert!(!rule.applies(&service)); + } + + #[test] + fn test_with_full_controller_service_structure() { + let mut service = json!({ + "identifier": "d9a5b110-2bb0-3b18-866b-6660d12f0bc1", + "instanceIdentifier": "cfe54b26-0199-1000-ffff-ffffd8c8433f", + "name": "MapCacheClientService", + "comments": "", + "type": "org.apache.nifi.distributed.cache.client.DistributedMapCacheClientService", + "bundle": { + "group": "org.apache.nifi", + "artifact": "nifi-distributed-cache-services-nar", + "version": "1.27.0" + }, + "properties": { + "SSL Context Service": null, + "Server Port": "4557", + "Server Hostname": "localhost", + "Communications Timeout": "30 secs" + }, + "propertyDescriptors": { + "SSL Context Service": { + "name": "SSL Context Service", + "displayName": "SSL Context Service", + "identifiesControllerService": true, + "sensitive": false, + "dynamic": false + } + }, + "controllerServiceApis": [ + { + "type": "org.apache.nifi.distributed.cache.client.AtomicDistributedMapCacheClient", + "bundle": { + "group": "org.apache.nifi", + "artifact": "nifi-standard-services-api-nar", + "version": "1.27.0" + } + } + ], + "scheduledState": "DISABLED", + "bulletinLevel": "WARN", + "componentType": "CONTROLLER_SERVICE" + }); + + let rule = DistributedCacheServicesMigration; + assert!(rule.applies(&service)); + assert!(rule.apply(&mut service)); + + // Verify the type was changed + assert_eq!( + service.get("type").and_then(|v| v.as_str()), + Some("org.apache.nifi.distributed.cache.client.MapCacheClientService") + ); + + // Verify other fields remain unchanged + assert_eq!( + service.get("identifier").and_then(|v| v.as_str()), + Some("d9a5b110-2bb0-3b18-866b-6660d12f0bc1") + ); + assert_eq!( + service.get("componentType").and_then(|v| v.as_str()), + Some("CONTROLLER_SERVICE") + ); + } +}