feat/cd/localdeploymentdemo #79
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -1765,6 +1765,7 @@ dependencies = [
|
|||||||
"strum 0.27.1",
|
"strum 0.27.1",
|
||||||
"temp-dir",
|
"temp-dir",
|
||||||
"temp-file",
|
"temp-file",
|
||||||
|
"tempfile",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
"url",
|
"url",
|
||||||
|
|||||||
@ -17,6 +17,7 @@ async fn main() {
|
|||||||
project_root: PathBuf::from("./examples/rust/webapp"),
|
project_root: PathBuf::from("./examples/rust/webapp"),
|
||||||
framework: Some(RustWebFramework::Leptos),
|
framework: Some(RustWebFramework::Leptos),
|
||||||
};
|
};
|
||||||
|
// TODO RustWebappScore should simply take a RustWebApp as config
|
||||||
let app = RustWebappScore {
|
let app = RustWebappScore {
|
||||||
name: "Example Rust Webapp".to_string(),
|
name: "Example Rust Webapp".to_string(),
|
||||||
domain: Url::Url(url::Url::parse("https://rustapp.harmony.example.com").unwrap()),
|
domain: Url::Url(url::Url::parse("https://rustapp.harmony.example.com").unwrap()),
|
||||||
|
|||||||
@ -57,3 +57,4 @@ similar.workspace = true
|
|||||||
futures-util = "0.3.31"
|
futures-util = "0.3.31"
|
||||||
tokio-util = "0.7.15"
|
tokio-util = "0.7.15"
|
||||||
strum = { version = "0.27.1", features = ["derive"] }
|
strum = { version = "0.27.1", features = ["derive"] }
|
||||||
|
tempfile = "3.20.0"
|
||||||
|
|||||||
@ -2,7 +2,7 @@ use lazy_static::lazy_static;
|
|||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
pub static ref HARMONY_CONFIG_DIR: PathBuf = directories::BaseDirs::new()
|
pub static ref HARMONY_DATA_DIR: PathBuf = directories::BaseDirs::new()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.data_dir()
|
.data_dir()
|
||||||
.join("harmony");
|
.join("harmony");
|
||||||
|
|||||||
@ -16,7 +16,7 @@ use crate::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
HelmCommand, K8sclient, Topology,
|
DeploymentTarget, HelmCommand, K8sclient, MultiTargetTopology, Topology,
|
||||||
k8s::K8sClient,
|
k8s::K8sClient,
|
||||||
tenant::{TenantConfig, TenantManager, k8s::K8sTenantManager},
|
tenant::{TenantConfig, TenantManager, k8s::K8sTenantManager},
|
||||||
};
|
};
|
||||||
@ -246,6 +246,7 @@ pub struct K8sAnywhereConfig {
|
|||||||
///
|
///
|
||||||
/// default: true
|
/// default: true
|
||||||
pub use_local_k3d: bool,
|
pub use_local_k3d: bool,
|
||||||
|
harmony_profile: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl K8sAnywhereConfig {
|
impl K8sAnywhereConfig {
|
||||||
@ -256,6 +257,11 @@ impl K8sAnywhereConfig {
|
|||||||
.map_or_else(|_| false, |v| v.parse().ok().unwrap_or(false)),
|
.map_or_else(|_| false, |v| v.parse().ok().unwrap_or(false)),
|
||||||
autoinstall: std::env::var("HARMONY_AUTOINSTALL")
|
autoinstall: std::env::var("HARMONY_AUTOINSTALL")
|
||||||
.map_or_else(|_| false, |v| v.parse().ok().unwrap_or(false)),
|
.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")
|
use_local_k3d: std::env::var("HARMONY_USE_LOCAL_K3D")
|
||||||
.map_or_else(|_| true, |v| v.parse().ok().unwrap_or(true)),
|
.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 {}
|
impl HelmCommand for K8sAnywhereTopology {}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
|
|||||||
@ -62,6 +62,17 @@ pub trait Topology: Send + Sync {
|
|||||||
async fn ensure_ready(&self) -> Result<Outcome, InterpretError>;
|
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;
|
pub type IpAddress = IpAddr;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[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 async_trait::async_trait;
|
||||||
use log::{error, info};
|
use log::{error, info};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
use tempfile::NamedTempFile;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
config::HARMONY_DATA_DIR,
|
||||||
data::Version,
|
data::Version,
|
||||||
inventory::Inventory,
|
inventory::Inventory,
|
||||||
modules::{
|
modules::{
|
||||||
@ -12,7 +14,7 @@ use crate::{
|
|||||||
helm::chart::HelmChartScore,
|
helm::chart::HelmChartScore,
|
||||||
},
|
},
|
||||||
score::Score,
|
score::Score,
|
||||||
topology::{HelmCommand, Topology, Url},
|
topology::{DeploymentTarget, HelmCommand, MultiTargetTopology, Topology, Url},
|
||||||
};
|
};
|
||||||
|
|
||||||
/// ContinuousDelivery in Harmony provides this functionality :
|
/// ContinuousDelivery in Harmony provides this functionality :
|
||||||
@ -47,9 +49,98 @@ pub struct ContinuousDelivery<A: OCICompliant + HelmPackage> {
|
|||||||
pub application: Arc<A>,
|
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]
|
#[async_trait]
|
||||||
impl<A: OCICompliant + HelmPackage + Clone + 'static, T: Topology + HelmCommand + 'static>
|
impl<
|
||||||
ApplicationFeature<T> for ContinuousDelivery<A>
|
A: OCICompliant + HelmPackage + Clone + 'static,
|
||||||
|
T: Topology + HelmCommand + MultiTargetTopology + 'static,
|
||||||
|
> ApplicationFeature<T> for ContinuousDelivery<A>
|
||||||
{
|
{
|
||||||
async fn ensure_installed(&self, topology: &T) -> Result<(), String> {
|
async fn ensure_installed(&self, topology: &T) -> Result<(), String> {
|
||||||
let image = self.application.image_name();
|
let image = self.application.image_name();
|
||||||
@ -62,10 +153,31 @@ impl<A: OCICompliant + HelmPackage + Clone + 'static, T: Topology + HelmCommand
|
|||||||
let helm_chart = self.application.build_push_helm_package(&image).await?;
|
let helm_chart = self.application.build_push_helm_package(&image).await?;
|
||||||
info!("Pushed new helm chart {helm_chart}");
|
info!("Pushed new helm chart {helm_chart}");
|
||||||
|
|
||||||
let image = self.application.build_push_oci_image().await?;
|
// let image = self.application.build_push_oci_image().await?;
|
||||||
info!("Pushed new docker image {image}");
|
// info!("Pushed new docker image {image}");
|
||||||
|
error!("uncomment above");
|
||||||
|
|
||||||
info!("Installing ContinuousDelivery feature");
|
info!("Installing ContinuousDelivery feature");
|
||||||
|
// 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 {
|
let cd_server = HelmChartScore {
|
||||||
namespace: todo!(
|
namespace: todo!(
|
||||||
"ArgoCD Helm chart with proper understanding of Tenant, see how Will did it for Monitoring for now"
|
"ArgoCD Helm chart with proper understanding of Tenant, see how Will did it for Monitoring for now"
|
||||||
@ -81,12 +193,16 @@ impl<A: OCICompliant + HelmPackage + Clone + 'static, T: Topology + HelmCommand
|
|||||||
};
|
};
|
||||||
let interpret = cd_server.create_interpret();
|
let interpret = cd_server.create_interpret();
|
||||||
interpret.execute(&Inventory::empty(), topology);
|
interpret.execute(&Inventory::empty(), topology);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
todo!("1. Create ArgoCD score that installs argo using helm chart, see if Taha's already done it
|
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)
|
- [X] Package app (docker image, helm chart)
|
||||||
3. Push to registry if staging or prod
|
- [X] Push to registry
|
||||||
4. Poke Argo
|
- [ ] Push only if staging or prod
|
||||||
5. Ensure app is up")
|
- [ ] Deploy to local k3d when target is local
|
||||||
|
- [ ] Poke Argo
|
||||||
|
- [ ] Ensure app is up")
|
||||||
}
|
}
|
||||||
fn name(&self) -> String {
|
fn name(&self) -> String {
|
||||||
"ContinuousDelivery".to_string()
|
"ContinuousDelivery".to_string()
|
||||||
|
|||||||
@ -378,10 +378,10 @@ image:
|
|||||||
|
|
||||||
service:
|
service:
|
||||||
type: ClusterIP
|
type: ClusterIP
|
||||||
port: 80
|
port: 3000
|
||||||
|
|
||||||
ingress:
|
ingress:
|
||||||
enabled: false
|
enabled: true
|
||||||
# Annotations for cert-manager to handle SSL.
|
# Annotations for cert-manager to handle SSL.
|
||||||
annotations:
|
annotations:
|
||||||
cert-manager.io/cluster-issuer: "letsencrypt-prod"
|
cert-manager.io/cluster-issuer: "letsencrypt-prod"
|
||||||
@ -432,7 +432,7 @@ spec:
|
|||||||
type: {{ .Values.service.type }}
|
type: {{ .Values.service.type }}
|
||||||
ports:
|
ports:
|
||||||
- port: {{ .Values.service.port }}
|
- port: {{ .Values.service.port }}
|
||||||
targetPort: http
|
targetPort: 3000
|
||||||
protocol: TCP
|
protocol: TCP
|
||||||
name: http
|
name: http
|
||||||
selector:
|
selector:
|
||||||
@ -462,7 +462,7 @@ spec:
|
|||||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||||
ports:
|
ports:
|
||||||
- name: http
|
- name: http
|
||||||
containerPort: 8080 # Assuming the rust app listens on 8080
|
containerPort: 3000
|
||||||
protocol: TCP
|
protocol: TCP
|
||||||
"#;
|
"#;
|
||||||
fs::write(templates_dir.join("deployment.yaml"), deployment_yaml)?;
|
fs::write(templates_dir.join("deployment.yaml"), deployment_yaml)?;
|
||||||
@ -499,7 +499,7 @@ spec:
|
|||||||
service:
|
service:
|
||||||
name: {{ include "chart.fullname" $ }}
|
name: {{ include "chart.fullname" $ }}
|
||||||
port:
|
port:
|
||||||
name: http
|
number: 3000
|
||||||
{{- end }}
|
{{- end }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
@ -549,25 +549,25 @@ spec:
|
|||||||
) -> Result<String, Box<dyn std::error::Error>> {
|
) -> Result<String, Box<dyn std::error::Error>> {
|
||||||
// The chart name is the file stem of the .tgz file
|
// 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 chart_file_name = packaged_chart_path.file_stem().unwrap().to_str().unwrap();
|
||||||
let oci_url = format!(
|
let oci_push_url = format!("oci://{}/{}", *REGISTRY_URL, *REGISTRY_PROJECT);
|
||||||
"oci://{}/{}/{}-chart",
|
let oci_pull_url = format!("{oci_push_url}/{}-chart", self.name);
|
||||||
*REGISTRY_URL, *REGISTRY_PROJECT, self.name
|
|
||||||
);
|
|
||||||
|
|
||||||
info!(
|
info!(
|
||||||
"Pushing Helm chart {} to {}",
|
"Pushing Helm chart {} to {}",
|
||||||
packaged_chart_path.to_string_lossy(),
|
packaged_chart_path.to_string_lossy(),
|
||||||
oci_url
|
oci_push_url
|
||||||
);
|
);
|
||||||
|
|
||||||
let output = process::Command::new("helm")
|
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()?;
|
.output()?;
|
||||||
|
|
||||||
self.check_output(&output, "Pushing Helm chart failed")?;
|
self.check_output(&output, "Pushing Helm chart failed")?;
|
||||||
|
|
||||||
// The final URL includes the version tag, which is part of the file name
|
// The final URL includes the version tag, which is part of the file name
|
||||||
let version = chart_file_name.rsplit_once('-').unwrap().1;
|
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 serde::Serialize;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
config::HARMONY_CONFIG_DIR,
|
config::HARMONY_DATA_DIR,
|
||||||
data::{Id, Version},
|
data::{Id, Version},
|
||||||
interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome},
|
interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome},
|
||||||
inventory::Inventory,
|
inventory::Inventory,
|
||||||
@ -22,7 +22,7 @@ pub struct K3DInstallationScore {
|
|||||||
impl Default for K3DInstallationScore {
|
impl Default for K3DInstallationScore {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
installation_path: HARMONY_CONFIG_DIR.join("k3d"),
|
installation_path: HARMONY_DATA_DIR.join("k3d"),
|
||||||
cluster_name: "harmony".to_string(),
|
cluster_name: "harmony".to_string(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user