feat: add dry-run functionality and similar dependency #62
							
								
								
									
										7
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										7
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							| @ -1576,6 +1576,7 @@ dependencies = [ | |||||||
|  "serde-value", |  "serde-value", | ||||||
|  "serde_json", |  "serde_json", | ||||||
|  "serde_yaml", |  "serde_yaml", | ||||||
|  |  "similar", | ||||||
|  "temp-dir", |  "temp-dir", | ||||||
|  "temp-file", |  "temp-file", | ||||||
|  "tokio", |  "tokio", | ||||||
| @ -4090,6 +4091,12 @@ dependencies = [ | |||||||
|  "rand_core 0.6.4", |  "rand_core 0.6.4", | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "similar" | ||||||
|  | version = "2.7.0" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" | ||||||
|  | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "simple_asn1" | name = "simple_asn1" | ||||||
| version = "0.6.3" | version = "0.6.3" | ||||||
|  | |||||||
							
								
								
									
										47
									
								
								Cargo.toml
									
									
									
									
									
								
							
							
						
						
									
										47
									
								
								Cargo.toml
									
									
									
									
									
								
							| @ -20,34 +20,23 @@ readme = "README.md" | |||||||
| license = "GNU AGPL v3" | license = "GNU AGPL v3" | ||||||
| 
 | 
 | ||||||
| [workspace.dependencies] | [workspace.dependencies] | ||||||
| log = "0.4.22" | log = "0.4" | ||||||
| env_logger = "0.11.5" | env_logger = "0.11" | ||||||
| derive-new = "0.7.0" | derive-new = "0.7" | ||||||
| async-trait = "0.1.82" | async-trait = "0.1" | ||||||
| tokio = { version = "1.40.0", features = [ | tokio = { version = "1.40", features = ["io-std", "fs", "macros", "rt-multi-thread"] } | ||||||
|   "io-std", |  | ||||||
|   "fs", |  | ||||||
|   "macros", |  | ||||||
|   "rt-multi-thread", |  | ||||||
| ] } |  | ||||||
| cidr = { features = ["serde"], version = "0.2" } | cidr = { features = ["serde"], version = "0.2" } | ||||||
| russh = "0.45.0" | russh = "0.45" | ||||||
| russh-keys = "0.45.0" | russh-keys = "0.45" | ||||||
| rand = "0.8.5" | rand = "0.8" | ||||||
| url = "2.5.4" | url = "2.5" | ||||||
| kube = "0.98.0" | kube = "0.98" | ||||||
| k8s-openapi = { version = "0.24.0", features = ["v1_30"] } | k8s-openapi = { version = "0.24", features = ["v1_30"] } | ||||||
| serde_yaml = "0.9.34" | serde_yaml = "0.9" | ||||||
| serde-value = "0.7.0" | serde-value = "0.7" | ||||||
| http = "1.2.0" | http = "1.2" | ||||||
| inquire = "0.7.5" | inquire = "0.7" | ||||||
| convert_case = "0.8.0" | convert_case =  "0.8" | ||||||
| chrono = "0.4" | chrono = "0.4" | ||||||
| 
 | similar = "2" | ||||||
| [workspace.dependencies.uuid] | uuid = { version = "1.11", features = [ "v4", "fast-rng", "macro-diagnostics" ] } | ||||||
| version = "1.11.0" |  | ||||||
| features = [ |  | ||||||
|   "v4",                # Lets you generate random UUIDs |  | ||||||
|   "fast-rng",          # Use a faster (but still sufficiently random) RNG |  | ||||||
|   "macro-diagnostics", # Enable better diagnostics for compile-time UUIDs |  | ||||||
| ] |  | ||||||
|  | |||||||
| @ -53,3 +53,4 @@ fqdn = { version = "0.4.6", features = [ | |||||||
| ] } | ] } | ||||||
| temp-dir = "0.1.14" | temp-dir = "0.1.14" | ||||||
| dyn-clone = "1.0.19" | dyn-clone = "1.0.19" | ||||||
|  | similar.workspace = true | ||||||
|  | |||||||
| @ -10,4 +10,6 @@ lazy_static! { | |||||||
|         std::env::var("HARMONY_REGISTRY_URL").unwrap_or_else(|_| "hub.nationtech.io".to_string()); |         std::env::var("HARMONY_REGISTRY_URL").unwrap_or_else(|_| "hub.nationtech.io".to_string()); | ||||||
|     pub static ref REGISTRY_PROJECT: String = |     pub static ref REGISTRY_PROJECT: String = | ||||||
|         std::env::var("HARMONY_REGISTRY_PROJECT").unwrap_or_else(|_| "harmony".to_string()); |         std::env::var("HARMONY_REGISTRY_PROJECT").unwrap_or_else(|_| "harmony".to_string()); | ||||||
|  |     pub static ref DRY_RUN: bool = | ||||||
|  |         std::env::var("HARMONY_DRY_RUN").map_or(true, |value| value.parse().unwrap_or(true)); | ||||||
| } | } | ||||||
|  | |||||||
| @ -4,9 +4,11 @@ use kube::{ | |||||||
|     Api, Client, Config, Error, Resource, |     Api, Client, Config, Error, Resource, | ||||||
|     api::{Patch, PatchParams}, |     api::{Patch, PatchParams}, | ||||||
|     config::{KubeConfigOptions, Kubeconfig}, |     config::{KubeConfigOptions, Kubeconfig}, | ||||||
|  |     core::ErrorResponse, | ||||||
| }; | }; | ||||||
| use log::{debug, error, trace}; | use log::{debug, error, trace}; | ||||||
| use serde::de::DeserializeOwned; | use serde::de::DeserializeOwned; | ||||||
|  | use similar::TextDiff; | ||||||
| 
 | 
 | ||||||
| #[derive(new)] | #[derive(new)] | ||||||
| pub struct K8sClient { | pub struct K8sClient { | ||||||
| @ -48,8 +50,79 @@ impl K8sClient { | |||||||
|             .name |             .name | ||||||
|             .as_ref() |             .as_ref() | ||||||
|             .expect("K8s Resource should have a name"); |             .expect("K8s Resource should have a name"); | ||||||
|         api.patch(name, &patch_params, &Patch::Apply(resource)) | 
 | ||||||
|             .await |         if *crate::config::DRY_RUN { | ||||||
|  |             match api.get(name).await { | ||||||
|  |                 Ok(current) => { | ||||||
|  |                     trace!("Received current value {current:#?}"); | ||||||
|  |                     // The resource exists, so we calculate and display a diff.
 | ||||||
|  |                     println!("\nPerforming dry-run for resource: '{}'", name); | ||||||
|  |                     let mut current_yaml = serde_yaml::to_value(¤t) | ||||||
|  |                         .expect(&format!("Could not serialize current value : {current:#?}")); | ||||||
|  |                     if current_yaml.is_mapping() && current_yaml.get("status").is_some() { | ||||||
|  |                         let map = current_yaml.as_mapping_mut().unwrap(); | ||||||
|  |                         let removed = map.remove_entry("status"); | ||||||
|  |                         trace!("Removed status {:?}", removed); | ||||||
|  |                     } else { | ||||||
|  |                         trace!( | ||||||
|  |                             "Did not find status entry for current object {}/{}", | ||||||
|  |                             current.meta().namespace.as_ref().unwrap_or(&"".to_string()), | ||||||
|  |                             current.meta().name.as_ref().unwrap_or(&"".to_string()) | ||||||
|  |                         ); | ||||||
|  |                     } | ||||||
|  |                     let current_yaml = serde_yaml::to_string(¤t_yaml) | ||||||
|  |                         .unwrap_or_else(|_| "Failed to serialize current resource".to_string()); | ||||||
|  |                     let new_yaml = serde_yaml::to_string(resource) | ||||||
|  |                         .unwrap_or_else(|_| "Failed to serialize new resource".to_string()); | ||||||
|  | 
 | ||||||
|  |                     if current_yaml == new_yaml { | ||||||
|  |                         println!("No changes detected."); | ||||||
|  |                         // Return the current resource state as there are no changes.
 | ||||||
|  |                         return Ok(current); | ||||||
|  |                     } | ||||||
|  | 
 | ||||||
|  |                     println!("Changes detected:"); | ||||||
|  |                     let diff = TextDiff::from_lines(¤t_yaml, &new_yaml); | ||||||
|  | 
 | ||||||
|  |                     // Iterate over the changes and print them in a git-like diff format.
 | ||||||
|  |                     for change in diff.iter_all_changes() { | ||||||
|  |                         let sign = match change.tag() { | ||||||
|  |                             similar::ChangeTag::Delete => "-", | ||||||
|  |                             similar::ChangeTag::Insert => "+", | ||||||
|  |                             similar::ChangeTag::Equal => " ", | ||||||
|  |                         }; | ||||||
|  |                         print!("{}{}", sign, change); | ||||||
|  |                     } | ||||||
|  |                     // In a dry run, we return the new resource state that would have been applied.
 | ||||||
|  |                     Ok(resource.clone()) | ||||||
|  |                 } | ||||||
|  |                 Err(Error::Api(ErrorResponse { code: 404, .. })) => { | ||||||
|  |                     // The resource does not exist, so the "diff" is the entire new resource.
 | ||||||
|  |                     println!("\nPerforming dry-run for new resource: '{}'", name); | ||||||
|  |                     println!( | ||||||
|  |                         "Resource does not exist. It would be created with the following content:" | ||||||
|  |                     ); | ||||||
|  |                     let new_yaml = serde_yaml::to_string(resource) | ||||||
|  |                         .unwrap_or_else(|_| "Failed to serialize new resource".to_string()); | ||||||
|  | 
 | ||||||
|  |                     // Print each line of the new resource with a '+' prefix.
 | ||||||
|  |                     for line in new_yaml.lines() { | ||||||
|  |                         println!("+{}", line); | ||||||
|  |                     } | ||||||
|  |                     // In a dry run, we return the new resource state that would have been created.
 | ||||||
|  |                     Ok(resource.clone()) | ||||||
|  |                 } | ||||||
|  |                 Err(e) => { | ||||||
|  |                     // Another API error occurred.
 | ||||||
|  |                     error!("Failed to get resource '{}': {}", name, e); | ||||||
|  |                     Err(e) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } else { | ||||||
|  |             return api | ||||||
|  |                 .patch(name, &patch_params, &Patch::Apply(resource)) | ||||||
|  |                 .await; | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     pub async fn apply_many<K>(&self, resource: &Vec<K>, ns: Option<&str>) -> Result<Vec<K>, Error> |     pub async fn apply_many<K>(&self, resource: &Vec<K>, ns: Option<&str>) -> Result<Vec<K>, Error> | ||||||
|  | |||||||
| @ -138,6 +138,7 @@ impl K8sTenantManager { | |||||||
|           "kind": "NetworkPolicy", |           "kind": "NetworkPolicy", | ||||||
|           "metadata": { |           "metadata": { | ||||||
|               "name": format!("{}-network-policy", config.name), |               "name": format!("{}-network-policy", config.name), | ||||||
|  |               "namespace": self.get_namespace_name(config), | ||||||
|           }, |           }, | ||||||
|           "spec": { |           "spec": { | ||||||
|             "podSelector": {}, |             "podSelector": {}, | ||||||
| @ -219,8 +220,29 @@ impl K8sTenantManager { | |||||||
|                             }) |                             }) | ||||||
|                         }) |                         }) | ||||||
|                         .collect(); |                         .collect(); | ||||||
|  |                 let ports: Option<Vec<NetworkPolicyPort>> = | ||||||
|  |                     c.1.as_ref().map(|spec| match &spec.data { | ||||||
|  |                         super::PortSpecData::SinglePort(port) => vec![NetworkPolicyPort { | ||||||
|  |                             port: Some(IntOrString::Int(port.clone().into())), | ||||||
|  |                             ..Default::default() | ||||||
|  |                         }], | ||||||
|  |                         super::PortSpecData::PortRange(start, end) => vec![NetworkPolicyPort { | ||||||
|  |                             port: Some(IntOrString::Int(start.clone().into())), | ||||||
|  |                             end_port: Some(end.clone().into()), | ||||||
|  |                             protocol: None, // Not currently supported by Harmony
 | ||||||
|  |                         }], | ||||||
|  | 
 | ||||||
|  |                         super::PortSpecData::ListOfPorts(items) => items | ||||||
|  |                             .iter() | ||||||
|  |                             .map(|i| NetworkPolicyPort { | ||||||
|  |                                 port: Some(IntOrString::Int(i.clone().into())), | ||||||
|  |                                 ..Default::default() | ||||||
|  |                             }) | ||||||
|  |                             .collect(), | ||||||
|  |                     }); | ||||||
|                 let rule = serde_json::from_value::<NetworkPolicyIngressRule>(json!({ |                 let rule = serde_json::from_value::<NetworkPolicyIngressRule>(json!({ | ||||||
|                     "from": cidr_list |                     "from": cidr_list, | ||||||
|  |                     "ports": ports, | ||||||
|                 })) |                 })) | ||||||
|                 .map_err(|e| { |                 .map_err(|e| { | ||||||
|                     ExecutorError::ConfigurationError(format!( |                     ExecutorError::ConfigurationError(format!( | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user