diff --git a/Cargo.lock b/Cargo.lock index 2af94a0..a5a62e9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1795,6 +1795,18 @@ dependencies = [ "url", ] +[[package]] +name = "example-openbao" +version = "0.1.0" +dependencies = [ + "harmony", + "harmony_cli", + "harmony_macros", + "harmony_types", + "tokio", + "url", +] + [[package]] name = "example-opnsense" version = "0.1.0" @@ -1810,6 +1822,18 @@ dependencies = [ "url", ] +[[package]] +name = "example-penpot" +version = "0.1.0" +dependencies = [ + "harmony", + "harmony_cli", + "harmony_macros", + "harmony_types", + "tokio", + "url", +] + [[package]] name = "example-pxe" version = "0.1.0" diff --git a/examples/openbao/src/main.rs b/examples/openbao/src/main.rs index 34d5973..52c5119 100644 --- a/examples/openbao/src/main.rs +++ b/examples/openbao/src/main.rs @@ -37,7 +37,8 @@ async fn main() { enabled: true size: 10Gi storageClass: null - accessMode: ReadWriteOnce"#.to_string(), + accessMode: ReadWriteOnce"# + .to_string(), ); let openbao = HelmChartScore { namespace: Some(NonBlankString::from_str("openbao").unwrap()), diff --git a/harmony/src/modules/application/features/helm_argocd_score.rs b/harmony/src/modules/application/features/helm_argocd_score.rs index ea53691..2e51a9e 100644 --- a/harmony/src/modules/application/features/helm_argocd_score.rs +++ b/harmony/src/modules/application/features/helm_argocd_score.rs @@ -1,4 +1,5 @@ use async_trait::async_trait; +use harmony_macros::hurl; use kube::{Api, api::GroupVersionKind}; use log::{debug, warn}; use non_blank_string_rs::NonBlankString; @@ -1051,7 +1052,7 @@ commitServer: install_only: false, repository: Some(HelmRepository::new( "argo".to_string(), - url::Url::parse("https://argoproj.github.io/argo-helm").unwrap(), + hurl!("https://argoproj.github.io/argo-helm"), true, )), } diff --git a/harmony/src/modules/cert_manager/helm.rs b/harmony/src/modules/cert_manager/helm.rs index eae0ed6..b0770f9 100644 --- a/harmony/src/modules/cert_manager/helm.rs +++ b/harmony/src/modules/cert_manager/helm.rs @@ -1,5 +1,6 @@ use std::{collections::HashMap, str::FromStr}; +use harmony_macros::hurl; use non_blank_string_rs::NonBlankString; use serde::Serialize; use url::Url; @@ -33,7 +34,7 @@ impl Score for CertManagerHelmScore { install_only: true, repository: Some(HelmRepository::new( "jetstack".to_string(), - Url::parse("https://charts.jetstack.io").unwrap(), + hurl!("https://charts.jetstack.io"), true, )), } diff --git a/harmony/src/modules/helm/chart.rs b/harmony/src/modules/helm/chart.rs index e2f8057..4b678f1 100644 --- a/harmony/src/modules/helm/chart.rs +++ b/harmony/src/modules/helm/chart.rs @@ -5,6 +5,7 @@ use crate::score::Score; use crate::topology::{HelmCommand, Topology}; use async_trait::async_trait; use harmony_types::id::Id; +use harmony_types::net::Url; use helm_wrapper_rs; use helm_wrapper_rs::blocking::{DefaultHelmExecutor, HelmExecutor}; use log::{debug, info, warn}; @@ -15,7 +16,6 @@ use std::path::Path; use std::process::{Command, Output, Stdio}; use std::str::FromStr; use temp_file::TempFile; -use url::Url; #[derive(Debug, Clone, Serialize)] pub struct HelmRepository { @@ -78,7 +78,8 @@ impl HelmChartInterpret { repo.name, repo.url, repo.force_update ); - let mut add_args = vec!["repo", "add", &repo.name, repo.url.as_str()]; + let repo_url = repo.url.to_string(); + let mut add_args = vec!["repo", "add", &repo.name, &repo_url]; if repo.force_update { add_args.push("--force-update"); } diff --git a/harmony/src/modules/helm/command.rs b/harmony/src/modules/helm/command.rs deleted file mode 100644 index c4d92c1..0000000 --- a/harmony/src/modules/helm/command.rs +++ /dev/null @@ -1,364 +0,0 @@ -use async_trait::async_trait; -use log::debug; -use serde::Serialize; -use std::collections::HashMap; -use std::io::ErrorKind; -use std::path::PathBuf; -use std::process::{Command, Output}; -use temp_dir::{self, TempDir}; -use temp_file::TempFile; - -use crate::data::Version; -use crate::interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}; -use crate::inventory::Inventory; -use crate::score::Score; -use crate::topology::{HelmCommand, K8sclient, Topology}; -use harmony_types::id::Id; - -#[derive(Clone)] -pub struct HelmCommandExecutor { - pub env: HashMap, - pub path: Option, - pub args: Vec, - pub api_versions: Option>, - pub kube_version: String, - pub debug: Option, - pub globals: HelmGlobals, - pub chart: HelmChart, -} - -#[derive(Clone)] -pub struct HelmGlobals { - pub chart_home: Option, - pub config_home: Option, -} - -#[derive(Debug, Clone, Serialize)] -pub struct HelmChart { - pub name: String, - pub version: Option, - pub repo: Option, - pub release_name: Option, - pub namespace: Option, - pub additional_values_files: Vec, - pub values_file: Option, - pub values_inline: Option, - pub include_crds: Option, - pub skip_hooks: Option, - pub api_versions: Option>, - pub kube_version: Option, - pub name_template: String, - pub skip_tests: Option, - pub debug: Option, -} - -impl HelmCommandExecutor { - pub fn generate(mut self) -> Result { - if self.globals.chart_home.is_none() { - self.globals.chart_home = Some(PathBuf::from("charts")); - } - - if self - .clone() - .chart - .clone() - .chart_exists_locally(self.clone().globals.chart_home.unwrap()) - .is_none() - { - if self.chart.repo.is_none() { - return Err(std::io::Error::new( - ErrorKind::Other, - "Chart doesn't exist locally and no repo specified", - )); - } - self.clone().run_command( - self.chart - .clone() - .pull_command(self.globals.chart_home.clone().unwrap()), - )?; - } - - let out = self.clone().run_command( - self.chart - .clone() - .helm_args(self.globals.chart_home.clone().unwrap()), - )?; - - // TODO: don't use unwrap here - let s = String::from_utf8(out.stdout).unwrap(); - debug!("helm stderr: {}", String::from_utf8(out.stderr).unwrap()); - debug!("helm status: {}", out.status); - debug!("helm output: {s}"); - - let clean = s.split_once("---").unwrap().1; - - Ok(clean.to_string()) - } - - pub fn version(self) -> Result { - let out = self.run_command(vec![ - "version".to_string(), - "-c".to_string(), - "--short".to_string(), - ])?; - - // TODO: don't use unwrap - Ok(String::from_utf8(out.stdout).unwrap()) - } - - pub fn run_command(mut self, mut args: Vec) -> Result { - if let Some(d) = self.debug { - if d { - args.push("--debug".to_string()); - } - } - - let path = if let Some(p) = self.path { - p - } else { - PathBuf::from("helm") - }; - - let config_home = match self.globals.config_home { - Some(p) => p, - None => PathBuf::from(TempDir::new()?.path()), - }; - - if let Some(yaml_str) = self.chart.values_inline { - let tf: TempFile = temp_file::with_contents(yaml_str.as_bytes()); - self.chart - .additional_values_files - .push(PathBuf::from(tf.path())); - }; - - self.env.insert( - "HELM_CONFIG_HOME".to_string(), - config_home.to_str().unwrap().to_string(), - ); - self.env.insert( - "HELM_CACHE_HOME".to_string(), - config_home.to_str().unwrap().to_string(), - ); - self.env.insert( - "HELM_DATA_HOME".to_string(), - config_home.to_str().unwrap().to_string(), - ); - - Command::new(path).envs(self.env).args(args).output() - } -} - -impl HelmChart { - pub fn chart_exists_locally(self, chart_home: PathBuf) -> Option { - let chart_path = - PathBuf::from(chart_home.to_str().unwrap().to_string() + "/" + &self.name.to_string()); - - if chart_path.exists() { - Some(chart_path) - } else { - None - } - } - - pub fn pull_command(self, chart_home: PathBuf) -> Vec { - let mut args = vec![ - "pull".to_string(), - "--untar".to_string(), - "--untardir".to_string(), - chart_home.to_str().unwrap().to_string(), - ]; - - match self.repo { - Some(r) => { - if r.starts_with("oci://") { - args.push( - r.trim_end_matches("/").to_string() + "/" + self.name.clone().as_str(), - ); - } else { - args.push("--repo".to_string()); - args.push(r.to_string()); - - args.push(self.name); - } - } - None => args.push(self.name), - }; - - if let Some(v) = self.version { - args.push("--version".to_string()); - args.push(v.to_string()); - } - - args - } - - pub fn helm_args(self, chart_home: PathBuf) -> Vec { - let mut args: Vec = vec!["template".to_string()]; - - match self.release_name { - Some(rn) => args.push(rn.to_string()), - None => args.push("--generate-name".to_string()), - } - - args.push( - PathBuf::from(chart_home.to_str().unwrap().to_string() + "/" + self.name.as_str()) - .to_str() - .unwrap() - .to_string(), - ); - - if let Some(n) = self.namespace { - args.push("--namespace".to_string()); - args.push(n.to_string()); - } - - if let Some(f) = self.values_file { - args.push("-f".to_string()); - args.push(f.to_str().unwrap().to_string()); - } - - for f in self.additional_values_files { - args.push("-f".to_string()); - args.push(f.to_str().unwrap().to_string()); - } - - if let Some(vv) = self.api_versions { - for v in vv { - args.push("--api-versions".to_string()); - args.push(v); - } - } - - if let Some(kv) = self.kube_version { - args.push("--kube-version".to_string()); - args.push(kv); - } - - if let Some(crd) = self.include_crds { - if crd { - args.push("--include-crds".to_string()); - } - } - - if let Some(st) = self.skip_tests { - if st { - args.push("--skip-tests".to_string()); - } - } - - if let Some(sh) = self.skip_hooks { - if sh { - args.push("--no-hooks".to_string()); - } - } - - if let Some(d) = self.debug { - if d { - args.push("--debug".to_string()); - } - } - - args - } -} - -#[derive(Debug, Clone, Serialize)] -pub struct HelmChartScoreV2 { - pub chart: HelmChart, -} - -impl Score for HelmChartScoreV2 { - fn create_interpret(&self) -> Box> { - Box::new(HelmChartInterpretV2 { - score: self.clone(), - }) - } - - fn name(&self) -> String { - format!( - "{} {} HelmChartScoreV2", - self.chart - .release_name - .clone() - .unwrap_or("Unknown".to_string()), - self.chart.name - ) - } -} - -#[derive(Debug, Serialize)] -pub struct HelmChartInterpretV2 { - pub score: HelmChartScoreV2, -} -impl HelmChartInterpretV2 {} - -#[async_trait] -impl Interpret for HelmChartInterpretV2 { - async fn execute( - &self, - _inventory: &Inventory, - _topology: &T, - ) -> Result { - let _ns = self - .score - .chart - .namespace - .as_ref() - .unwrap_or_else(|| todo!("Get namespace from active kubernetes cluster")); - - let helm_executor = HelmCommandExecutor { - env: HashMap::new(), - path: None, - args: vec![], - api_versions: None, - kube_version: "v1.33.0".to_string(), - debug: Some(false), - globals: HelmGlobals { - chart_home: None, - config_home: None, - }, - chart: self.score.chart.clone(), - }; - - // let mut helm_options = Vec::new(); - // if self.score.create_namespace { - // helm_options.push(NonBlankString::from_str("--create-namespace").unwrap()); - // } - - let res = helm_executor.generate(); - - let _output = match res { - Ok(output) => output, - Err(err) => return Err(InterpretError::new(err.to_string())), - }; - - // TODO: implement actually applying the YAML from the templating in the generate function to a k8s cluster, having trouble passing in straight YAML into the k8s client - - // let k8s_resource = k8s_openapi::serde_json::from_str(output.as_str()).unwrap(); - - // let client = topology - // .k8s_client() - // .await - // .expect("Environment should provide enough information to instanciate a client") - // .apply_namespaced(&vec![output], Some(ns.to_string().as_str())); - // match client.apply_yaml(output) { - // Ok(_) => return Ok(Outcome::success("Helm chart deployed".to_string())), - // Err(e) => return Err(InterpretError::new(e)), - // } - - Ok(Outcome::success("Helm chart deployed".to_string())) - } - - fn get_name(&self) -> InterpretName { - InterpretName::HelmCommand - } - fn get_version(&self) -> Version { - todo!() - } - fn get_status(&self) -> InterpretStatus { - todo!() - } - fn get_children(&self) -> Vec { - todo!() - } -} diff --git a/harmony/src/modules/helm/mod.rs b/harmony/src/modules/helm/mod.rs index de69381..831fbe5 100644 --- a/harmony/src/modules/helm/mod.rs +++ b/harmony/src/modules/helm/mod.rs @@ -1,2 +1 @@ pub mod chart; -pub mod command;