Compare commits
	
		
			3 Commits
		
	
	
		
			5a89495c61
			...
			e6612245a5
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| e6612245a5 | |||
| d317c0ba76 | |||
| 539b8299ae | 
							
								
								
									
										1
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							@ -1765,6 +1765,7 @@ dependencies = [
 | 
			
		||||
 "strum 0.27.1",
 | 
			
		||||
 "temp-dir",
 | 
			
		||||
 "temp-file",
 | 
			
		||||
 "tempfile",
 | 
			
		||||
 "tokio",
 | 
			
		||||
 "tokio-util",
 | 
			
		||||
 "url",
 | 
			
		||||
 | 
			
		||||
@ -17,6 +17,7 @@ async fn main() {
 | 
			
		||||
        project_root: PathBuf::from("./examples/rust/webapp"),
 | 
			
		||||
        framework: Some(RustWebFramework::Leptos),
 | 
			
		||||
    };
 | 
			
		||||
    // TODO RustWebappScore should simply take a RustWebApp as config
 | 
			
		||||
    let app = RustWebappScore {
 | 
			
		||||
        name: "Example Rust Webapp".to_string(),
 | 
			
		||||
        domain: Url::Url(url::Url::parse("https://rustapp.harmony.example.com").unwrap()),
 | 
			
		||||
 | 
			
		||||
@ -57,3 +57,4 @@ similar.workspace = true
 | 
			
		||||
futures-util = "0.3.31"
 | 
			
		||||
tokio-util = "0.7.15"
 | 
			
		||||
strum = { version = "0.27.1", features = ["derive"] }
 | 
			
		||||
tempfile = "3.20.0"
 | 
			
		||||
 | 
			
		||||
@ -2,7 +2,7 @@ use lazy_static::lazy_static;
 | 
			
		||||
use std::path::PathBuf;
 | 
			
		||||
 | 
			
		||||
lazy_static! {
 | 
			
		||||
    pub static ref HARMONY_CONFIG_DIR: PathBuf = directories::BaseDirs::new()
 | 
			
		||||
    pub static ref HARMONY_DATA_DIR: PathBuf = directories::BaseDirs::new()
 | 
			
		||||
        .unwrap()
 | 
			
		||||
        .data_dir()
 | 
			
		||||
        .join("harmony");
 | 
			
		||||
 | 
			
		||||
@ -16,7 +16,7 @@ use crate::{
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
use super::{
 | 
			
		||||
    HelmCommand, K8sclient, Topology,
 | 
			
		||||
    DeploymentTarget, HelmCommand, K8sclient, MultiTargetTopology, Topology,
 | 
			
		||||
    k8s::K8sClient,
 | 
			
		||||
    tenant::{TenantConfig, TenantManager, k8s::K8sTenantManager},
 | 
			
		||||
};
 | 
			
		||||
@ -246,6 +246,7 @@ pub struct K8sAnywhereConfig {
 | 
			
		||||
    ///
 | 
			
		||||
    /// default: true
 | 
			
		||||
    pub use_local_k3d: bool,
 | 
			
		||||
    harmony_profile: String,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl K8sAnywhereConfig {
 | 
			
		||||
@ -256,6 +257,11 @@ impl K8sAnywhereConfig {
 | 
			
		||||
                .map_or_else(|_| false, |v| v.parse().ok().unwrap_or(false)),
 | 
			
		||||
            autoinstall: std::env::var("HARMONY_AUTOINSTALL")
 | 
			
		||||
                .map_or_else(|_| false, |v| v.parse().ok().unwrap_or(false)),
 | 
			
		||||
            // TODO harmony_profile should be managed at a more core level than this
 | 
			
		||||
            harmony_profile: std::env::var("HARMONY_PROFILE").map_or_else(
 | 
			
		||||
                |_| "dev".to_string(),
 | 
			
		||||
                |v| v.parse().ok().unwrap_or("dev".to_string()),
 | 
			
		||||
            ),
 | 
			
		||||
            use_local_k3d: std::env::var("HARMONY_USE_LOCAL_K3D")
 | 
			
		||||
                .map_or_else(|_| true, |v| v.parse().ok().unwrap_or(true)),
 | 
			
		||||
        }
 | 
			
		||||
@ -292,6 +298,20 @@ impl Topology for K8sAnywhereTopology {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl MultiTargetTopology for K8sAnywhereTopology {
 | 
			
		||||
    fn current_target(&self) -> DeploymentTarget {
 | 
			
		||||
        if self.config.use_local_k3d {
 | 
			
		||||
            return DeploymentTarget::LocalDev;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        match self.config.harmony_profile.to_lowercase().as_str() {
 | 
			
		||||
            "staging" => DeploymentTarget::Staging,
 | 
			
		||||
            "production" => DeploymentTarget::Production,
 | 
			
		||||
            _ => todo!("HARMONY_PROFILE must be set when use_local_k3d is not set"),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl HelmCommand for K8sAnywhereTopology {}
 | 
			
		||||
 | 
			
		||||
#[async_trait]
 | 
			
		||||
 | 
			
		||||
@ -62,6 +62,17 @@ pub trait Topology: Send + Sync {
 | 
			
		||||
    async fn ensure_ready(&self) -> Result<Outcome, InterpretError>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug)]
 | 
			
		||||
pub enum DeploymentTarget {
 | 
			
		||||
    LocalDev,
 | 
			
		||||
    Staging,
 | 
			
		||||
    Production,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub trait MultiTargetTopology: Topology {
 | 
			
		||||
    fn current_target(&self) -> DeploymentTarget;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub type IpAddress = IpAddr;
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Clone)]
 | 
			
		||||
 | 
			
		||||
@ -1,10 +1,12 @@
 | 
			
		||||
use std::sync::Arc;
 | 
			
		||||
use std::{io::Write, process::Command, sync::Arc};
 | 
			
		||||
 | 
			
		||||
use async_trait::async_trait;
 | 
			
		||||
use log::{error, info};
 | 
			
		||||
use serde_json::Value;
 | 
			
		||||
use tempfile::NamedTempFile;
 | 
			
		||||
 | 
			
		||||
use crate::{
 | 
			
		||||
    config::HARMONY_DATA_DIR,
 | 
			
		||||
    data::Version,
 | 
			
		||||
    inventory::Inventory,
 | 
			
		||||
    modules::{
 | 
			
		||||
@ -12,7 +14,7 @@ use crate::{
 | 
			
		||||
        helm::chart::HelmChartScore,
 | 
			
		||||
    },
 | 
			
		||||
    score::Score,
 | 
			
		||||
    topology::{HelmCommand, Topology, Url},
 | 
			
		||||
    topology::{DeploymentTarget, HelmCommand, MultiTargetTopology, Topology, Url},
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/// ContinuousDelivery in Harmony provides this functionality :
 | 
			
		||||
@ -47,9 +49,98 @@ pub struct ContinuousDelivery<A: OCICompliant + HelmPackage> {
 | 
			
		||||
    pub application: Arc<A>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl<A: OCICompliant + HelmPackage> ContinuousDelivery<A> {
 | 
			
		||||
    async fn deploy_to_local_k3d(
 | 
			
		||||
        &self,
 | 
			
		||||
        app_name: String,
 | 
			
		||||
        chart_url: String,
 | 
			
		||||
        image_name: String,
 | 
			
		||||
    ) -> Result<(), String> {
 | 
			
		||||
        error!(
 | 
			
		||||
            "FIXME This works only with local k3d installations, which is fine only for current demo purposes. We assume usage of K8sAnywhereTopology"
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        error!("TODO hardcoded k3d bin path is wrong");
 | 
			
		||||
        let k3d_bin_path = (*HARMONY_DATA_DIR).join("k3d").join("k3d");
 | 
			
		||||
        // --- 1. Import the container image into the k3d cluster ---
 | 
			
		||||
        info!(
 | 
			
		||||
            "Importing image '{}' into k3d cluster 'harmony'",
 | 
			
		||||
            image_name
 | 
			
		||||
        );
 | 
			
		||||
        let import_output = Command::new(&k3d_bin_path)
 | 
			
		||||
            .args(["image", "import", &image_name, "--cluster", "harmony"])
 | 
			
		||||
            .output()
 | 
			
		||||
            .map_err(|e| format!("Failed to execute k3d image import: {}", e))?;
 | 
			
		||||
 | 
			
		||||
        if !import_output.status.success() {
 | 
			
		||||
            return Err(format!(
 | 
			
		||||
                "Failed to import image to k3d: {}",
 | 
			
		||||
                String::from_utf8_lossy(&import_output.stderr)
 | 
			
		||||
            ));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // --- 2. Get the kubeconfig for the k3d cluster and write it to a temp file ---
 | 
			
		||||
        info!("Retrieving kubeconfig for k3d cluster 'harmony'");
 | 
			
		||||
        let kubeconfig_output = Command::new(&k3d_bin_path)
 | 
			
		||||
            .args(["kubeconfig", "get", "harmony"])
 | 
			
		||||
            .output()
 | 
			
		||||
            .map_err(|e| format!("Failed to execute k3d kubeconfig get: {}", e))?;
 | 
			
		||||
 | 
			
		||||
        if !kubeconfig_output.status.success() {
 | 
			
		||||
            return Err(format!(
 | 
			
		||||
                "Failed to get kubeconfig from k3d: {}",
 | 
			
		||||
                String::from_utf8_lossy(&kubeconfig_output.stderr)
 | 
			
		||||
            ));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let mut temp_kubeconfig = NamedTempFile::new()
 | 
			
		||||
            .map_err(|e| format!("Failed to create temp file for kubeconfig: {}", e))?;
 | 
			
		||||
        temp_kubeconfig
 | 
			
		||||
            .write_all(&kubeconfig_output.stdout)
 | 
			
		||||
            .map_err(|e| format!("Failed to write to temp kubeconfig file: {}", e))?;
 | 
			
		||||
        let kubeconfig_path = temp_kubeconfig.path().to_str().unwrap();
 | 
			
		||||
 | 
			
		||||
        // --- 3. Install or upgrade the Helm chart in the cluster ---
 | 
			
		||||
        info!(
 | 
			
		||||
            "Deploying Helm chart '{}' to namespace '{}'",
 | 
			
		||||
            chart_url, app_name
 | 
			
		||||
        );
 | 
			
		||||
        let release_name = app_name.to_lowercase(); // Helm release names are often lowercase
 | 
			
		||||
        let helm_output = Command::new("helm")
 | 
			
		||||
            .args([
 | 
			
		||||
                "upgrade",
 | 
			
		||||
                "--install",
 | 
			
		||||
                &release_name,
 | 
			
		||||
                &chart_url,
 | 
			
		||||
                "--namespace",
 | 
			
		||||
                &app_name,
 | 
			
		||||
                "--create-namespace",
 | 
			
		||||
                "--wait", // Wait for the deployment to be ready
 | 
			
		||||
                "--kubeconfig",
 | 
			
		||||
                kubeconfig_path,
 | 
			
		||||
            ])
 | 
			
		||||
            .spawn()
 | 
			
		||||
            .map_err(|e| format!("Failed to execute helm upgrade: {}", e))?
 | 
			
		||||
            .wait_with_output()
 | 
			
		||||
            .map_err(|e| format!("Failed to execute helm upgrade: {}", e))?;
 | 
			
		||||
 | 
			
		||||
        if !helm_output.status.success() {
 | 
			
		||||
            return Err(format!(
 | 
			
		||||
                "Failed to deploy Helm chart: {}",
 | 
			
		||||
                String::from_utf8_lossy(&helm_output.stderr)
 | 
			
		||||
            ));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        info!("Successfully deployed '{}' to local k3d cluster.", app_name);
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[async_trait]
 | 
			
		||||
impl<A: OCICompliant + HelmPackage + Clone + 'static, T: Topology + HelmCommand + 'static>
 | 
			
		||||
    ApplicationFeature<T> for ContinuousDelivery<A>
 | 
			
		||||
impl<
 | 
			
		||||
    A: OCICompliant + HelmPackage + Clone + 'static,
 | 
			
		||||
    T: Topology + HelmCommand + MultiTargetTopology + 'static,
 | 
			
		||||
> ApplicationFeature<T> for ContinuousDelivery<A>
 | 
			
		||||
{
 | 
			
		||||
    async fn ensure_installed(&self, topology: &T) -> Result<(), String> {
 | 
			
		||||
        let image = self.application.image_name();
 | 
			
		||||
@ -62,31 +153,56 @@ impl<A: OCICompliant + HelmPackage + Clone + 'static, T: Topology + HelmCommand
 | 
			
		||||
        let helm_chart = self.application.build_push_helm_package(&image).await?;
 | 
			
		||||
        info!("Pushed new helm chart {helm_chart}");
 | 
			
		||||
 | 
			
		||||
        let image = self.application.build_push_oci_image().await?;
 | 
			
		||||
        info!("Pushed new docker image {image}");
 | 
			
		||||
        // let image = self.application.build_push_oci_image().await?;
 | 
			
		||||
        // info!("Pushed new docker image {image}");
 | 
			
		||||
        error!("uncomment above");
 | 
			
		||||
 | 
			
		||||
        info!("Installing ContinuousDelivery feature");
 | 
			
		||||
        let cd_server = HelmChartScore {
 | 
			
		||||
            namespace: todo!(
 | 
			
		||||
                "ArgoCD Helm chart with proper understanding of Tenant, see how Will did it for Monitoring for now"
 | 
			
		||||
            ),
 | 
			
		||||
            release_name: todo!("argocd helm chart whatever"),
 | 
			
		||||
            chart_name: todo!(),
 | 
			
		||||
            chart_version: todo!(),
 | 
			
		||||
            values_overrides: todo!(),
 | 
			
		||||
            values_yaml: todo!(),
 | 
			
		||||
            create_namespace: todo!(),
 | 
			
		||||
            install_only: todo!(),
 | 
			
		||||
            repository: todo!(),
 | 
			
		||||
        // TODO this is a temporary hack for demo purposes, the deployment target should be driven
 | 
			
		||||
        // by the topology only and we should not have to know how to perform tasks like this for
 | 
			
		||||
        // which the topology should be responsible.
 | 
			
		||||
        //
 | 
			
		||||
        // That said, this will require some careful architectural decisions, since the concept of
 | 
			
		||||
        // deployment targets / profiles is probably a layer of complexity that we won't be
 | 
			
		||||
        // completely able to avoid
 | 
			
		||||
        //
 | 
			
		||||
        // I'll try something for now that must be thought through after : att a deployment_profile
 | 
			
		||||
        // function to the topology trait that returns a profile, then anybody who needs it can
 | 
			
		||||
        // access it. This forces every Topology to understand the concept of targets though... So
 | 
			
		||||
        // instead I'll create a new Capability which is MultiTargetTopology and we'll see how it
 | 
			
		||||
        // goes. It still does not feel right though.
 | 
			
		||||
        match topology.current_target() {
 | 
			
		||||
            DeploymentTarget::LocalDev => {
 | 
			
		||||
                self.deploy_to_local_k3d(self.application.name(), helm_chart, image)
 | 
			
		||||
                    .await?;
 | 
			
		||||
            }
 | 
			
		||||
            target => {
 | 
			
		||||
                info!("Deploying to target {target:?}");
 | 
			
		||||
                let cd_server = HelmChartScore {
 | 
			
		||||
                    namespace: todo!(
 | 
			
		||||
                        "ArgoCD Helm chart with proper understanding of Tenant, see how Will did it for Monitoring for now"
 | 
			
		||||
                    ),
 | 
			
		||||
                    release_name: todo!("argocd helm chart whatever"),
 | 
			
		||||
                    chart_name: todo!(),
 | 
			
		||||
                    chart_version: todo!(),
 | 
			
		||||
                    values_overrides: todo!(),
 | 
			
		||||
                    values_yaml: todo!(),
 | 
			
		||||
                    create_namespace: todo!(),
 | 
			
		||||
                    install_only: todo!(),
 | 
			
		||||
                    repository: todo!(),
 | 
			
		||||
                };
 | 
			
		||||
                let interpret = cd_server.create_interpret();
 | 
			
		||||
                interpret.execute(&Inventory::empty(), topology);
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
        let interpret = cd_server.create_interpret();
 | 
			
		||||
        interpret.execute(&Inventory::empty(), topology);
 | 
			
		||||
 | 
			
		||||
        todo!("1. Create ArgoCD score that installs argo using helm chart, see if Taha's already done it
 | 
			
		||||
            2. Package app (docker image, helm chart)
 | 
			
		||||
            3. Push to registry if staging or prod
 | 
			
		||||
            4. Poke Argo
 | 
			
		||||
            5. Ensure app is up")
 | 
			
		||||
            - [X] Package app (docker image, helm chart)
 | 
			
		||||
            - [X] Push to registry
 | 
			
		||||
            - [ ] Push only if staging or prod
 | 
			
		||||
            - [ ] Deploy to local k3d when target is local
 | 
			
		||||
            - [ ] Poke Argo
 | 
			
		||||
            - [ ] Ensure app is up")
 | 
			
		||||
    }
 | 
			
		||||
    fn name(&self) -> String {
 | 
			
		||||
        "ContinuousDelivery".to_string()
 | 
			
		||||
 | 
			
		||||
@ -378,10 +378,10 @@ image:
 | 
			
		||||
 | 
			
		||||
service:
 | 
			
		||||
  type: ClusterIP
 | 
			
		||||
  port: 80
 | 
			
		||||
  port: 3000
 | 
			
		||||
 | 
			
		||||
ingress:
 | 
			
		||||
  enabled: false
 | 
			
		||||
  enabled: true
 | 
			
		||||
  # Annotations for cert-manager to handle SSL.
 | 
			
		||||
  annotations:
 | 
			
		||||
    cert-manager.io/cluster-issuer: "letsencrypt-prod"
 | 
			
		||||
@ -432,7 +432,7 @@ spec:
 | 
			
		||||
  type: {{ .Values.service.type }}
 | 
			
		||||
  ports:
 | 
			
		||||
    - port: {{ .Values.service.port }}
 | 
			
		||||
      targetPort: http
 | 
			
		||||
      targetPort: 3000
 | 
			
		||||
      protocol: TCP
 | 
			
		||||
      name: http
 | 
			
		||||
  selector:
 | 
			
		||||
@ -462,7 +462,7 @@ spec:
 | 
			
		||||
          imagePullPolicy: {{ .Values.image.pullPolicy }}
 | 
			
		||||
          ports:
 | 
			
		||||
            - name: http
 | 
			
		||||
              containerPort: 8080 # Assuming the rust app listens on 8080
 | 
			
		||||
              containerPort: 3000
 | 
			
		||||
              protocol: TCP
 | 
			
		||||
"#;
 | 
			
		||||
        fs::write(templates_dir.join("deployment.yaml"), deployment_yaml)?;
 | 
			
		||||
@ -499,7 +499,7 @@ spec:
 | 
			
		||||
              service:
 | 
			
		||||
                name: {{ include "chart.fullname" $ }}
 | 
			
		||||
                port:
 | 
			
		||||
                  name: http
 | 
			
		||||
                  number: 3000
 | 
			
		||||
          {{- end }}
 | 
			
		||||
    {{- end }}
 | 
			
		||||
{{- end }}
 | 
			
		||||
@ -549,25 +549,25 @@ spec:
 | 
			
		||||
    ) -> Result<String, Box<dyn std::error::Error>> {
 | 
			
		||||
        // The chart name is the file stem of the .tgz file
 | 
			
		||||
        let chart_file_name = packaged_chart_path.file_stem().unwrap().to_str().unwrap();
 | 
			
		||||
        let oci_url = format!(
 | 
			
		||||
            "oci://{}/{}/{}-chart",
 | 
			
		||||
            *REGISTRY_URL, *REGISTRY_PROJECT, self.name
 | 
			
		||||
        );
 | 
			
		||||
        let oci_push_url = format!("oci://{}/{}", *REGISTRY_URL, *REGISTRY_PROJECT);
 | 
			
		||||
        let oci_pull_url = format!("{oci_push_url}/{}-chart", self.name);
 | 
			
		||||
 | 
			
		||||
        info!(
 | 
			
		||||
            "Pushing Helm chart {} to {}",
 | 
			
		||||
            packaged_chart_path.to_string_lossy(),
 | 
			
		||||
            oci_url
 | 
			
		||||
            oci_push_url
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        let output = process::Command::new("helm")
 | 
			
		||||
            .args(["push", packaged_chart_path.to_str().unwrap(), &oci_url])
 | 
			
		||||
            .args(["push", packaged_chart_path.to_str().unwrap(), &oci_push_url])
 | 
			
		||||
            .output()?;
 | 
			
		||||
 | 
			
		||||
        self.check_output(&output, "Pushing Helm chart failed")?;
 | 
			
		||||
 | 
			
		||||
        // The final URL includes the version tag, which is part of the file name
 | 
			
		||||
        let version = chart_file_name.rsplit_once('-').unwrap().1;
 | 
			
		||||
        Ok(format!("{}:{}", oci_url, version))
 | 
			
		||||
        debug!("pull url {oci_pull_url}");
 | 
			
		||||
        debug!("push url {oci_push_url}");
 | 
			
		||||
        Ok(format!("{}:{}", oci_pull_url, version))
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -5,7 +5,7 @@ use log::info;
 | 
			
		||||
use serde::Serialize;
 | 
			
		||||
 | 
			
		||||
use crate::{
 | 
			
		||||
    config::HARMONY_CONFIG_DIR,
 | 
			
		||||
    config::HARMONY_DATA_DIR,
 | 
			
		||||
    data::{Id, Version},
 | 
			
		||||
    interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome},
 | 
			
		||||
    inventory::Inventory,
 | 
			
		||||
@ -22,7 +22,7 @@ pub struct K3DInstallationScore {
 | 
			
		||||
impl Default for K3DInstallationScore {
 | 
			
		||||
    fn default() -> Self {
 | 
			
		||||
        Self {
 | 
			
		||||
            installation_path: HARMONY_CONFIG_DIR.join("k3d"),
 | 
			
		||||
            installation_path: HARMONY_DATA_DIR.join("k3d"),
 | 
			
		||||
            cluster_name: "harmony".to_string(),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user