fix: unjank the demo #85

Merged
taha merged 19 commits from fix_demo into master 2025-07-11 14:32:22 +00:00
20 changed files with 299 additions and 158 deletions

1
.gitignore vendored
View File

@ -2,3 +2,4 @@ target
private_repos private_repos
log/ log/
*.tgz *.tgz
.gitignore
Review

These would be better in the examples's own gitignore file examples/rust/.gitignore. Eventually we may have hundreds of examples, we don't want thousands of lines in this file.

But thinking a bit further, this points towards a problem on the way we manage these generated files. They should probably end up in a harmony specific directory that we can then ignore in a single line for all examples. Maybe harmony_generated or harmony_build ?

These would be better in the examples's own gitignore file `examples/rust/.gitignore`. Eventually we may have hundreds of examples, we don't want thousands of lines in this file. But thinking a bit further, this points towards a problem on the way we manage these generated files. They should probably end up in a harmony specific directory that we can then ignore in a single line for all examples. Maybe `harmony_generated` or `harmony_build` ?

38
Cargo.lock generated
View File

@ -1355,6 +1355,7 @@ dependencies = [
name = "example-rust" name = "example-rust"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"base64 0.22.1",
"env_logger", "env_logger",
"harmony", "harmony",
"harmony_cli", "harmony_cli",
@ -1427,6 +1428,18 @@ version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"
[[package]]
name = "filetime"
version = "0.2.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586"
dependencies = [
"cfg-if",
"libc",
"libredox",
"windows-sys 0.59.0",
]
[[package]] [[package]]
name = "flate2" name = "flate2"
version = "1.1.2" version = "1.1.2"
@ -1726,6 +1739,8 @@ name = "harmony"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"base64 0.22.1",
"bollard",
"chrono", "chrono",
"cidr", "cidr",
"convert_case", "convert_case",
@ -1765,6 +1780,7 @@ dependencies = [
"serde_yaml", "serde_yaml",
"similar", "similar",
"strum 0.27.1", "strum 0.27.1",
"tar",
"temp-dir", "temp-dir",
"temp-file", "temp-file",
"tempfile", "tempfile",
@ -2728,6 +2744,7 @@ checksum = "1580801010e535496706ba011c15f8532df6b42297d2e471fec38ceadd8c0638"
dependencies = [ dependencies = [
"bitflags 2.9.1", "bitflags 2.9.1",
"libc", "libc",
"redox_syscall",
] ]
[[package]] [[package]]
@ -4696,6 +4713,17 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
[[package]]
name = "tar"
version = "0.4.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a"
dependencies = [
"filetime",
"libc",
"xattr",
]
[[package]] [[package]]
name = "temp-dir" name = "temp-dir"
version = "0.1.16" version = "0.1.16"
@ -5741,6 +5769,16 @@ dependencies = [
"tap", "tap",
] ]
[[package]]
name = "xattr"
version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af3a19837351dc82ba89f8a125e22a3c475f05aba604acc023d62b2739ae2909"
dependencies = [
"libc",
"rustix 1.0.7",
]
[[package]] [[package]]
name = "xml-rs" name = "xml-rs"
version = "0.8.26" version = "0.8.26"

View File

@ -53,3 +53,6 @@ chrono = "0.4"
similar = "2" similar = "2"
uuid = { version = "1.11", features = ["v4", "fast-rng", "macro-diagnostics"] } uuid = { version = "1.11", features = ["v4", "fast-rng", "macro-diagnostics"] }
pretty_assertions = "1.4.1" pretty_assertions = "1.4.1"
bollard = "0.19.1"
base64 = "0.22.1"
tar = "0.4.44"

View File

@ -14,6 +14,7 @@ async fn main() {
maestro.register_all(vec![Box::new(NtfyScore { maestro.register_all(vec![Box::new(NtfyScore {
namespace: "monitoring".to_string(), namespace: "monitoring".to_string(),
host: "localhost".to_string(),
})]); })]);
harmony_cli::init(maestro, None).await.unwrap(); harmony_cli::init(maestro, None).await.unwrap();
} }

View File

@ -12,3 +12,4 @@ tokio = { workspace = true }
log = { workspace = true } log = { workspace = true }
env_logger = { workspace = true } env_logger = { workspace = true }
url = { workspace = true } url = { workspace = true }
base64.workspace = true

View File

@ -13,6 +13,12 @@ use harmony::{
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
env_logger::init(); env_logger::init();
let topology = K8sAnywhereTopology::from_env();
let mut maestro = Maestro::initialize(Inventory::autoload(), topology)
.await
.unwrap();
let application = Arc::new(RustWebapp { let application = Arc::new(RustWebapp {
name: "harmony-example-rust-webapp".to_string(), name: "harmony-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()),
@ -24,17 +30,14 @@ async fn main() {
features: vec![ features: vec![
Box::new(ContinuousDelivery { Box::new(ContinuousDelivery {

The Tenant should not be visible at the applcation scope. An application will auto detect the tenant it is supposed to be in from the environment (such as the content of the loaded kubeconfig).

For example, a client cannot provision its own tenant as it may not have permission on the host cluster to do so.

There may be exceptions and more complex use cases coming up soon, but for the sake of this example we want to keep it 100% focused on the application and especially how simple it it to deploy a fully featured infrastructure around an application with as close to zero config as possible.

The Tenant should not be visible at the applcation scope. An application will auto detect the tenant it is supposed to be in from the environment (such as the content of the loaded kubeconfig). For example, a client cannot provision its own tenant as it may not have permission on the host cluster to do so. There may be exceptions and more complex use cases coming up soon, but for the sake of this example we want to keep it 100% focused on the application and especially how **simple** it it to deploy a fully featured infrastructure around an application with as close to zero config as possible.
application: application.clone(), application: application.clone(),
}), // TODO add monitoring, backups, multisite ha, etc
Box::new(Monitoring {
application: application.clone(),
}), }),
Box::new(Monitoring {}),
// TODO add monitoring, backups, multisite ha, etc
], ],
application, application,
}; };
let topology = K8sAnywhereTopology::from_env();
let mut maestro = Maestro::initialize(Inventory::autoload(), topology)
.await
.unwrap();
maestro.register_all(vec![Box::new(app)]); maestro.register_all(vec![Box::new(app)]);
harmony_cli::init(maestro, None).await.unwrap(); harmony_cli::init(maestro, None).await.unwrap();
} }

View File

@ -59,6 +59,9 @@ tokio-util = "0.7.15"
strum = { version = "0.27.1", features = ["derive"] } strum = { version = "0.27.1", features = ["derive"] }
tempfile = "3.20.0" tempfile = "3.20.0"
serde_with = "3.14.0" serde_with = "3.14.0"
bollard.workspace = true
tar.workspace = true
base64.workspace = true
[dev-dependencies] [dev-dependencies]
pretty_assertions.workspace = true pretty_assertions.workspace = true

View File

@ -260,17 +260,33 @@ impl K8sClient {
) -> Result<(), Error> { ) -> Result<(), Error> {
let obj: DynamicObject = serde_yaml::from_value(yaml.clone()).expect("TODO do not unwrap"); let obj: DynamicObject = serde_yaml::from_value(yaml.clone()).expect("TODO do not unwrap");
let name = obj.metadata.name.as_ref().expect("YAML must have a name"); let name = obj.metadata.name.as_ref().expect("YAML must have a name");
let namespace = obj
.metadata
.namespace
.as_ref()
.expect("YAML must have a namespace");
// 4. Define the API resource type using the GVK from the object. let api_version = yaml
// The plural name 'applications' is taken from your CRD definition. .get("apiVersion")
error!("This only supports argocd application harcoded, very rrrong"); .expect("couldn't get apiVersion from YAML")
let gvk = GroupVersionKind::gvk("argoproj.io", "v1alpha1", "Application"); .as_str()
let api_resource = ApiResource::from_gvk_with_plural(&gvk, "applications"); .expect("couldn't get apiVersion as str");
let kind = yaml
.get("kind")
.expect("couldn't get kind from YAML")
.as_str()
.expect("couldn't get kind as str");
let split: Vec<&str> = api_version.splitn(2, "/").collect();
let g = split[0];
let v = split[1];
let gvk = GroupVersionKind::gvk(g, v, kind);
let api_resource = ApiResource::from_gvk(&gvk);
let namespace = match ns {
Some(n) => n,
None => obj
.metadata
.namespace
.as_ref()
.expect("YAML must have a namespace"),
};
// 5. Create a dynamic API client for this resource type. // 5. Create a dynamic API client for this resource type.
let api: Api<DynamicObject> = let api: Api<DynamicObject> =

View File

@ -1,11 +1,7 @@
use std::{backtrace, collections::HashMap};
use k8s_openapi::{Metadata, NamespaceResourceScope, Resource};
use log::debug; use log::debug;
use serde::Serialize; use serde::Serialize;
use serde_with::skip_serializing_none; use serde_with::skip_serializing_none;
use serde_yaml::Value; use serde_yaml::Value;
use url::Url;
use crate::modules::application::features::CDApplicationConfig; use crate::modules::application::features::CDApplicationConfig;
@ -33,11 +29,14 @@ pub struct Helm {
#[derive(Clone, Debug, Serialize)] #[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct Source { pub struct Source {
// Using string for this because URL enforces a URL scheme at the beginning but Helm, ArgoCD, etc do not, and it can be counterproductive,
// as the only way I've found to get OCI working isn't by using oci:// but rather no scheme at all
#[serde(rename = "repoURL")] #[serde(rename = "repoURL")]
pub repo_url: Url, pub repo_url: String,
pub target_revision: Option<String>, pub target_revision: Option<String>,
pub chart: String, pub chart: String,
pub helm: Helm, pub helm: Helm,
pub path: String,
} }
#[derive(Clone, Debug, Serialize)] #[derive(Clone, Debug, Serialize)]
@ -90,7 +89,7 @@ impl Default for ArgoApplication {
namespace: Default::default(), namespace: Default::default(),
project: Default::default(), project: Default::default(),
Outdated
Review

I would write some documentation for why you replaced url with string

I would write some documentation for why you replaced url with string
source: Source { source: Source {
repo_url: Url::parse("http://asdf").expect("Couldn't parse to URL"), repo_url: "http://asdf".to_string(),
target_revision: None, target_revision: None,
chart: "".to_string(), chart: "".to_string(),
helm: Helm { helm: Helm {
@ -109,6 +108,7 @@ impl Default for ArgoApplication {
api_versions: vec![], api_versions: vec![],
namespace: None, namespace: None,
}, },
path: "".to_string(),
}, },
sync_policy: SyncPolicy { sync_policy: SyncPolicy {
automated: Automated { automated: Automated {
@ -138,10 +138,10 @@ impl From<CDApplicationConfig> for ArgoApplication {
namespace: Some(value.namespace), namespace: Some(value.namespace),
project: "default".to_string(), project: "default".to_string(),
source: Source { source: Source {
repo_url: Url::parse(value.helm_chart_repo_url.to_string().as_str()) repo_url: value.helm_chart_repo_url,
.expect("couldn't convert to URL"),
target_revision: Some(value.version.to_string()), target_revision: Some(value.version.to_string()),
chart: value.helm_chart_name, chart: value.helm_chart_name.clone(),
path: value.helm_chart_name,
helm: Helm { helm: Helm {
pass_credentials: None, pass_credentials: None,
parameters: vec![], parameters: vec![],
@ -218,7 +218,7 @@ spec:
let mut yaml_value: Value = let mut yaml_value: Value =
serde_yaml::from_str(yaml_str.as_str()).expect("couldn't parse string to YAML"); serde_yaml::from_str(yaml_str.as_str()).expect("couldn't parse string to YAML");
let mut spec = yaml_value let spec = yaml_value
.get_mut("spec") .get_mut("spec")
.expect("couldn't get spec from yaml") .expect("couldn't get spec from yaml")
.as_mapping_mut() .as_mapping_mut()
@ -258,7 +258,6 @@ spec:
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
use url::Url;
use crate::modules::application::features::{ use crate::modules::application::features::{
ArgoApplication, Automated, Backoff, Helm, Retry, Source, SyncPolicy, ArgoApplication, Automated, Backoff, Helm, Retry, Source, SyncPolicy,
@ -271,7 +270,7 @@ mod tests {
namespace: Some("test-ns".to_string()), namespace: Some("test-ns".to_string()),
project: "test-project".to_string(), project: "test-project".to_string(),
source: Source { source: Source {
repo_url: Url::parse("http://test").unwrap(), repo_url: "http://test".to_string(),
target_revision: None, target_revision: None,
chart: "test-chart".to_string(), chart: "test-chart".to_string(),
helm: Helm { helm: Helm {
@ -290,6 +289,7 @@ mod tests {
api_versions: vec![], api_versions: vec![],
namespace: None, namespace: None,
}, },
path: "".to_string(),
}, },
sync_policy: SyncPolicy { sync_policy: SyncPolicy {
automated: Automated { automated: Automated {
@ -321,7 +321,7 @@ spec:
server: https://kubernetes.default.svc server: https://kubernetes.default.svc
namespace: test-ns namespace: test-ns
source: source:
repoURL: http://test/ repoURL: http://test
chart: test-chart chart: test-chart
helm: helm:
parameters: [] parameters: []
@ -329,6 +329,7 @@ spec:
releaseName: test-release-neame releaseName: test-release-neame
valueFiles: [] valueFiles: []
apiVersions: [] apiVersions: []
path: ''
syncPolicy: syncPolicy:
automated: automated:
prune: false prune: false

View File

@ -9,15 +9,12 @@ use crate::{
config::HARMONY_DATA_DIR, config::HARMONY_DATA_DIR,
data::Version, data::Version,
inventory::Inventory, inventory::Inventory,
modules::{ modules::application::{
application::{ Application, ApplicationFeature, HelmPackage, OCICompliant,
Application, ApplicationFeature, HelmPackage, OCICompliant, features::{ArgoApplication, ArgoHelmScore},
features::{ArgoApplication, ArgoHelmScore},
},
helm::chart::HelmChartScore,
}, },
score::Score, score::Score,
topology::{DeploymentTarget, HelmCommand, K8sclient, MultiTargetTopology, Topology, Url}, topology::{DeploymentTarget, HelmCommand, K8sclient, MultiTargetTopology, Topology},
}; };
/// ContinuousDelivery in Harmony provides this functionality : /// ContinuousDelivery in Harmony provides this functionality :
@ -162,8 +159,8 @@ impl<
info!("Pushed new helm chart {helm_chart}"); info!("Pushed new helm chart {helm_chart}");
error!("TODO Make building image configurable/skippable"); error!("TODO Make building image configurable/skippable");
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}");
info!("Installing ContinuousDelivery feature"); info!("Installing ContinuousDelivery feature");
// TODO this is a temporary hack for demo purposes, the deployment target should be driven // TODO this is a temporary hack for demo purposes, the deployment target should be driven
@ -188,12 +185,12 @@ impl<
info!("Deploying to target {target:?}"); info!("Deploying to target {target:?}");
let score = ArgoHelmScore { let score = ArgoHelmScore {
namespace: "harmonydemo-staging".to_string(), namespace: "harmonydemo-staging".to_string(),
openshift: true, openshift: false,
domain: "argo.harmonydemo.apps.st.mcd".to_string(), domain: "argo.harmonydemo.apps.st.mcd".to_string(),
argo_apps: vec![ArgoApplication::from(CDApplicationConfig { argo_apps: vec![ArgoApplication::from(CDApplicationConfig {
// helm pull oci://hub.nationtech.io/harmony/harmony-example-rust-webapp-chart/harmony-example-rust-webapp-chart --version 0.1.0 // helm pull oci://hub.nationtech.io/harmony/harmony-example-rust-webapp-chart --version 0.1.0
version: Version::from("0.1.0").unwrap(), version: Version::from("0.1.0").unwrap(),
helm_chart_repo_url: Url::Url(url::Url::parse("oci://hub.nationtech.io/harmony/harmony-example-rust-webapp-chart/harmony-example-rust-webapp-chart").unwrap()), helm_chart_repo_url: "hub.nationtech.io/harmony".to_string(),
helm_chart_name: "harmony-example-rust-webapp-chart".to_string(), helm_chart_name: "harmony-example-rust-webapp-chart".to_string(),
values_overrides: None, values_overrides: None,
name: "harmony-demo-rust-webapp".to_string(), name: "harmony-demo-rust-webapp".to_string(),
@ -207,14 +204,7 @@ impl<
.unwrap(); .unwrap();
} }
}; };
Ok(())
todo!("1. Create ArgoCD score that installs argo using helm chart, see if Taha's already done it
- [X] Package app (docker image, helm chart)
- [X] Push to registry
- [X] Push only if staging or prod
- [X] 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()
@ -225,7 +215,7 @@ impl<
/// more CD systems /// more CD systems
pub struct CDApplicationConfig { pub struct CDApplicationConfig {
pub version: Version, pub version: Version,
pub helm_chart_repo_url: Url, pub helm_chart_repo_url: String,
pub helm_chart_name: String, pub helm_chart_name: String,
pub values_overrides: Option<Value>, pub values_overrides: Option<Value>,
pub name: String, pub name: String,

View File

@ -1,5 +1,4 @@
use async_trait::async_trait; use async_trait::async_trait;
use k8s_openapi::Resource;
use log::error; use log::error;
use non_blank_string_rs::NonBlankString; use non_blank_string_rs::NonBlankString;
use serde::Serialize; use serde::Serialize;
@ -647,7 +646,7 @@ server:
# Argo CD server ingress configuration # Argo CD server ingress configuration
ingress: ingress:
# -- Enable an ingress resource for the Argo CD server # -- Enable an ingress resource for the Argo CD server
enabled: false enabled: true
# -- Specific implementation for ingress controller. One of `generic`, `aws` or `gke` # -- Specific implementation for ingress controller. One of `generic`, `aws` or `gke`
## Additional configuration might be required in related configuration sections ## Additional configuration might be required in related configuration sections
controller: generic controller: generic

View File

@ -1,36 +1,99 @@
use std::sync::Arc;
use async_trait::async_trait; use async_trait::async_trait;
use log::info; use base64::{Engine as _, engine::general_purpose};
use log::{debug, info};
use crate::{ use crate::{
inventory::Inventory, inventory::Inventory,
modules::{ modules::{
application::{Application, ApplicationFeature}, application::{ApplicationFeature, OCICompliant},
monitoring::{ monitoring::{
application_monitoring::k8s_application_monitoring_score::ApplicationPrometheusMonitoringScore, alert_channel::webhook_receiver::WebhookReceiver,
kube_prometheus::{ kube_prometheus::{
helm_prometheus_alert_score::HelmPrometheusAlertingScore, helm_prometheus_alert_score::HelmPrometheusAlertingScore,
types::{NamespaceSelector, ServiceMonitor}, types::{NamespaceSelector, ServiceMonitor},
}, },
ntfy::ntfy::NtfyScore,
}, },
}, },
score::Score, score::Score,
topology::{HelmCommand, Topology, tenant::TenantManager}, topology::{HelmCommand, K8sclient, Topology, Url, tenant::TenantManager},
}; };
#[derive(Debug, Default, Clone)] #[derive(Debug, Clone)]
taha marked this conversation as resolved Outdated
Outdated
Review

if we add pub application: Arc to monitoring we can use the application name as namespace so that it is consistent with the way continuous _delivery feature gets the namespace

if we add pub application: Arc<dyn Application> to monitoring we can use the application name as namespace so that it is consistent with the way continuous _delivery feature gets the namespace
pub struct Monitoring {} pub struct Monitoring {
pub application: Arc<dyn OCICompliant>,
}
#[async_trait] #[async_trait]
impl<T: Topology + HelmCommand + 'static + TenantManager> ApplicationFeature<T> for Monitoring { impl<T: Topology + HelmCommand + K8sclient + 'static + TenantManager> ApplicationFeature<T>
for Monitoring
{
async fn ensure_installed(&self, topology: &T) -> Result<(), String> { async fn ensure_installed(&self, topology: &T) -> Result<(), String> {
info!("Ensuring monitoring is available for application"); info!("Ensuring monitoring is available for application");
let ntfy = NtfyScore {
// namespace: topology
// .get_tenant_config()
// .await
taha marked this conversation as resolved
Review

rather than hardcoding namespace here

rather than hardcoding namespace here
// .expect("couldn't get tenant config")
// .name,
namespace: self.application.name(),
host: "localhost".to_string(),
};
ntfy.create_interpret()
.execute(&Inventory::empty(), topology)
.await
.expect("couldn't create interpret for ntfy");
let ntfy_default_auth_username = "harmony";
let ntfy_default_auth_password = "harmony";
let ntfy_default_auth_header = format!(
"Basic {}",
general_purpose::STANDARD.encode(format!(
"{ntfy_default_auth_username}:{ntfy_default_auth_password}"
))
);
debug!("ntfy_default_auth_header: {ntfy_default_auth_header}");
let ntfy_default_auth_param = general_purpose::STANDARD
.encode(ntfy_default_auth_header)
.replace("=", "");
debug!("ntfy_default_auth_param: {ntfy_default_auth_param}");
let ntfy_receiver = WebhookReceiver {
name: "ntfy-webhook".to_string(),
url: Url::Url(
url::Url::parse(
format!(
"http://ntfy.{}.svc.cluster.local/rust-web-app?auth={ntfy_default_auth_param}",
self.application.name()
)
.as_str(),
)
.unwrap(),
),
};
let mut service_monitor = ServiceMonitor::default(); let mut service_monitor = ServiceMonitor::default();
service_monitor.namespace_selector = Some(NamespaceSelector { service_monitor.namespace_selector = Some(NamespaceSelector {
any: true, any: true,
match_names: vec![], match_names: vec![],
}); });
let alerting_score = ApplicationPrometheusMonitoringScore {
receivers: vec![], service_monitor.name = "rust-webapp".to_string();
// let alerting_score = ApplicationPrometheusMonitoringScore {
// receivers: vec![Box::new(ntfy_receiver)],
// rules: vec![],
// service_monitors: vec![service_monitor],
// };
let alerting_score = HelmPrometheusAlertingScore {
receivers: vec![Box::new(ntfy_receiver)],
rules: vec![], rules: vec![],
service_monitors: vec![service_monitor], service_monitors: vec![service_monitor],
}; };

View File

@ -59,9 +59,7 @@ impl<A: Application, T: Topology + std::fmt::Debug> Interpret<T> for Application
} }
}; };
} }
todo!( Ok(Outcome::success("successfully created app".to_string()))
"Do I need to do anything more than this here?? I feel like the Application trait itself should expose something like ensure_ready but its becoming redundant. We'll see as this evolves."
)
} }
fn get_name(&self) -> InterpretName { fn get_name(&self) -> InterpretName {

View File

@ -4,11 +4,15 @@ use std::process;
use std::sync::Arc; use std::sync::Arc;
use async_trait::async_trait; use async_trait::async_trait;
use bollard::query_parameters::PushImageOptionsBuilder;
use bollard::{Docker, body_full};
wjro marked this conversation as resolved
Review

add a comment to justify using bollard vs docker via the cli

add a comment to justify using bollard vs docker via the cli
Review

The reason I used bollard was because I find it preferable to access the docker daemon programmatically over an API over using a (albeit relatively stable) CLI interface

The reason I used `bollard` was because I find it preferable to access the docker daemon programmatically over an API over using a (albeit relatively stable) CLI interface
use dockerfile_builder::Dockerfile; use dockerfile_builder::Dockerfile;
use dockerfile_builder::instruction::{CMD, COPY, ENV, EXPOSE, FROM, RUN, USER, WORKDIR}; use dockerfile_builder::instruction::{CMD, COPY, ENV, EXPOSE, FROM, RUN, USER, WORKDIR};
use dockerfile_builder::instruction_builder::CopyBuilder; use dockerfile_builder::instruction_builder::CopyBuilder;
use futures_util::StreamExt;
use log::{debug, error, info}; use log::{debug, error, info};
use serde::Serialize; use serde::Serialize;
use tar::Archive;
use crate::config::{REGISTRY_PROJECT, REGISTRY_URL}; use crate::config::{REGISTRY_PROJECT, REGISTRY_URL};
use crate::{ use crate::{
@ -108,6 +112,7 @@ impl OCICompliant for RustWebapp {
// 1. Build the local image by calling the synchronous helper function. // 1. Build the local image by calling the synchronous helper function.
let local_image_name = self.local_image_name(); let local_image_name = self.local_image_name();
self.build_docker_image(&local_image_name) self.build_docker_image(&local_image_name)
.await
.map_err(|e| format!("Failed to build Docker image: {}", e))?; .map_err(|e| format!("Failed to build Docker image: {}", e))?;
info!( info!(
"Successfully built local Docker image: {}", "Successfully built local Docker image: {}",
@ -117,6 +122,7 @@ impl OCICompliant for RustWebapp {
let remote_image_name = self.image_name(); let remote_image_name = self.image_name();
// 2. Push the image to the registry. // 2. Push the image to the registry.
self.push_docker_image(&local_image_name, &remote_image_name) self.push_docker_image(&local_image_name, &remote_image_name)
.await
.map_err(|e| format!("Failed to push Docker image: {}", e))?; .map_err(|e| format!("Failed to push Docker image: {}", e))?;
info!("Successfully pushed Docker image to: {}", remote_image_name); info!("Successfully pushed Docker image to: {}", remote_image_name);
@ -153,66 +159,68 @@ impl RustWebapp {
} }
/// Builds the Docker image using the generated Dockerfile. /// Builds the Docker image using the generated Dockerfile.
pub fn build_docker_image( pub async fn build_docker_image(
&self, &self,
image_name: &str, image_name: &str,
) -> Result<String, Box<dyn std::error::Error>> { ) -> Result<String, Box<dyn std::error::Error>> {
info!("Generating Dockerfile for '{}'", self.name); info!("Generating Dockerfile for '{}'", self.name);
let dockerfile_path = self.build_dockerfile()?; let _dockerfile_path = self.build_dockerfile()?;
info!( let docker = Docker::connect_with_socket_defaults().unwrap();
"Building Docker image with file {} from root {}",
dockerfile_path.to_string_lossy(), let build_image_options = bollard::query_parameters::BuildImageOptionsBuilder::default()
self.project_root.to_string_lossy() .dockerfile("Dockerfile.harmony")
.t(image_name)
.q(false)
.version(bollard::query_parameters::BuilderVersion::BuilderV1)
.platform("linux/x86_64");
let mut temp_tar_builder = tar::Builder::new(Vec::new());
let _ = temp_tar_builder
.append_dir_all("", self.project_root.clone())
.unwrap();
let archive = temp_tar_builder
.into_inner()
.expect("couldn't finish creating tar");
let archived_files = Archive::new(archive.as_slice())
.entries()
.unwrap()
.map(|entry| entry.unwrap().path().unwrap().into_owned())
.collect::<Vec<_>>();
debug!("files in docker tar: {:#?}", archived_files);
let mut image_build_stream = docker.build_image(
build_image_options.build(),
None,
Some(body_full(archive.into())),
); );
let output = process::Command::new("docker")
.args([
"build",
"--file",
dockerfile_path.to_str().unwrap(),
"-t",
&image_name,
self.project_root.to_str().unwrap(),
])
.spawn()?
.wait_with_output()?;
self.check_output(&output, "Failed to build Docker image")?; while let Some(msg) = image_build_stream.next().await {
println!("Message: {msg:?}");
}
Ok(image_name.to_string()) Ok(image_name.to_string())
} }
/// Tags and pushes a Docker image to the configured remote registry. /// Tags and pushes a Docker image to the configured remote registry.
fn push_docker_image( async fn push_docker_image(
&self, &self,
image_name: &str, image_name: &str,
full_tag: &str, full_tag: &str,
) -> Result<String, Box<dyn std::error::Error>> { ) -> Result<String, Box<dyn std::error::Error>> {
info!("Pushing docker image {full_tag}"); info!("Pushing docker image {full_tag}");
// Tag the image for the remote registry. let docker = Docker::connect_with_socket_defaults().unwrap();
let output = process::Command::new("docker")
.args(["tag", image_name, &full_tag])
.spawn()?
.wait_with_output()?;
self.check_output(&output, "Tagging docker image failed")?;
debug!(
"docker tag output: stdout: {}, stderr: {}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
// Push the image. // let push_options = PushImageOptionsBuilder::new().tag(tag);
let output = process::Command::new("docker")
.args(["push", &full_tag]) let mut push_image_stream =
.spawn()? docker.push_image(full_tag, Some(PushImageOptionsBuilder::new().build()), None);
.wait_with_output()?;
self.check_output(&output, "Pushing docker image failed")?; while let Some(msg) = push_image_stream.next().await {
debug!( println!("Message: {msg:?}");
"docker push output: stdout: {}, stderr: {}", }
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
Ok(full_tag.to_string()) Ok(full_tag.to_string())
} }
@ -349,7 +357,11 @@ impl RustWebapp {
image_url: &str, image_url: &str,
) -> Result<PathBuf, Box<dyn std::error::Error>> { ) -> Result<PathBuf, Box<dyn std::error::Error>> {
let chart_name = format!("{}-chart", self.name); let chart_name = format!("{}-chart", self.name);
let chart_dir = self.project_root.join("helm").join(&chart_name); let chart_dir = self
.project_root
.join(".harmony_generated")
.join("helm")
.join(&chart_name);
let templates_dir = chart_dir.join("templates"); let templates_dir = chart_dir.join("templates");
fs::create_dir_all(&templates_dir)?; fs::create_dir_all(&templates_dir)?;
@ -416,7 +428,7 @@ ingress:
Expand the name of the chart. Expand the name of the chart.
*/}} */}}
{{- define "chart.name" -}} {{- define "chart.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} {{- default .Chart.Name $.Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }} {{- end }}
{{/* {{/*
@ -424,7 +436,7 @@ Create a default fully qualified app name.
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
*/}} */}}
{{- define "chart.fullname" -}} {{- define "chart.fullname" -}}
{{- $name := default .Chart.Name .Values.nameOverride }} {{- $name := default .Chart.Name $.Values.nameOverride }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }} {{- end }}
"#; "#;
@ -437,12 +449,12 @@ kind: Service
metadata: metadata:
name: {{ include "chart.fullname" . }} name: {{ include "chart.fullname" . }}
spec: spec:

What does the $ do ?

What does the `$` do ?
Outdated
Review

$ is the global scope, for some reason .Values.service.type on its own wasn't working (different scope?), did some digging, found $ to specifically reference the global/top level scope and it then worked. I'm not an expert at helm templating though

`$` is the global scope, for some reason `.Values.service.type` on its own wasn't working (different scope?), did some digging, found `$` to specifically reference the global/top level scope and it then worked. I'm not an expert at helm templating though
type: {{ .Values.service.type }} type: {{ $.Values.service.type }}
ports: ports:
- port: {{ .Values.service.port }} - name: main
targetPort: 3000 port: {{ $.Values.service.port | default 3000 }}
Review

Thanks for fixing my demo hardcoding crap 😅

Thanks for fixing my demo hardcoding crap 😅
targetPort: {{ $.Values.service.port | default 3000 }}
protocol: TCP protocol: TCP
name: http
selector: selector:
app: {{ include "chart.name" . }} app: {{ include "chart.name" . }}
"#; "#;
@ -455,7 +467,7 @@ kind: Deployment
metadata: metadata:
name: {{ include "chart.fullname" . }} name: {{ include "chart.fullname" . }}
spec: spec:
replicas: {{ .Values.replicaCount }} replicas: {{ $.Values.replicaCount }}
selector: selector:
matchLabels: matchLabels:
app: {{ include "chart.name" . }} app: {{ include "chart.name" . }}
@ -466,28 +478,28 @@ spec:
spec: spec:
containers: containers:
- name: {{ .Chart.Name }} - name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" image: "{{ $.Values.image.repository }}:{{ $.Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }} imagePullPolicy: {{ $.Values.image.pullPolicy }}
ports: ports:
- name: http - name: main
containerPort: 3000 containerPort: {{ $.Values.service.port | default 3000 }}
protocol: TCP protocol: TCP
"#; "#;
fs::write(templates_dir.join("deployment.yaml"), deployment_yaml)?; fs::write(templates_dir.join("deployment.yaml"), deployment_yaml)?;
// Create templates/ingress.yaml // Create templates/ingress.yaml
let ingress_yaml = r#" let ingress_yaml = r#"
{{- if .Values.ingress.enabled -}} {{- if $.Values.ingress.enabled -}}
apiVersion: networking.k8s.io/v1 apiVersion: networking.k8s.io/v1
kind: Ingress kind: Ingress
metadata: metadata:
name: {{ include "chart.fullname" . }} name: {{ include "chart.fullname" . }}
annotations: annotations:
{{- toYaml .Values.ingress.annotations | nindent 4 }} {{- toYaml $.Values.ingress.annotations | nindent 4 }}
spec: spec:
{{- if .Values.ingress.tls }} {{- if $.Values.ingress.tls }}
tls: tls:
{{- range .Values.ingress.tls }} {{- range $.Values.ingress.tls }}
- hosts: - hosts:
{{- range .hosts }} {{- range .hosts }}
- {{ . | quote }} - {{ . | quote }}
@ -496,7 +508,7 @@ spec:
{{- end }} {{- end }}
{{- end }} {{- end }}
rules: rules:
{{- range .Values.ingress.hosts }} {{- range $.Values.ingress.hosts }}
- host: {{ .host | quote }} - host: {{ .host | quote }}
http: http:
paths: paths:
@ -507,7 +519,7 @@ spec:
service: service:
name: {{ include "chart.fullname" $ }} name: {{ include "chart.fullname" $ }}
port: port:
number: 3000 number: {{ $.Values.service.port | default 3000 }}
{{- end }} {{- end }}
{{- end }} {{- end }}
{{- end }} {{- end }}
@ -526,11 +538,15 @@ spec:
info!( info!(
"Launching `helm package {}` cli with CWD {}", "Launching `helm package {}` cli with CWD {}",
chart_dirname.to_string_lossy(), chart_dirname.to_string_lossy(),
&self.project_root.join("helm").to_string_lossy() &self
.project_root
.join(".harmony_generated")
.join("helm")
.to_string_lossy()
); );
let output = process::Command::new("helm") let output = process::Command::new("helm")
.args(["package", chart_dirname.to_str().unwrap()]) .args(["package", chart_dirname.to_str().unwrap()])
.current_dir(&self.project_root.join("helm")) // Run package from the parent dir .current_dir(&self.project_root.join(".harmony_generated").join("helm")) // Run package from the parent dir
.output()?; .output()?;
self.check_output(&output, "Failed to package Helm chart")?; self.check_output(&output, "Failed to package Helm chart")?;
@ -547,7 +563,11 @@ spec:
} }
// The output from helm is relative, so we join it with the execution directory. // The output from helm is relative, so we join it with the execution directory.
Ok(self.project_root.join("helm").join(tgz_name)) Ok(self
.project_root
.join(".harmony_generated")
.join("helm")
.join(tgz_name))
} }
/// Pushes a packaged Helm chart to an OCI registry. /// Pushes a packaged Helm chart to an OCI registry.

View File

@ -24,7 +24,10 @@ pub struct ApplicationPrometheusMonitoringScore {
impl<T: Topology + HelmCommand + TenantManager> Score<T> for ApplicationPrometheusMonitoringScore { impl<T: Topology + HelmCommand + TenantManager> Score<T> for ApplicationPrometheusMonitoringScore {
fn create_interpret(&self) -> Box<dyn crate::interpret::Interpret<T>> { fn create_interpret(&self) -> Box<dyn crate::interpret::Interpret<T>> {
let config = Arc::new(Mutex::new(PrometheusConfig::new())); let mut prom_config = PrometheusConfig::new();
prom_config.alert_manager = true;
let config = Arc::new(Mutex::new(prom_config));
config config
.try_lock() .try_lock()
.expect("couldn't lock config") .expect("couldn't lock config")

View File

@ -38,15 +38,15 @@ impl KubePrometheusConfig {
node_exporter: false, node_exporter: false,
prometheus: true, prometheus: true,
kubernetes_service_monitors: true, kubernetes_service_monitors: true,
kubernetes_api_server: false, kubernetes_api_server: true,
kubelet: true, kubelet: true,
kube_controller_manager: false, kube_controller_manager: true,
kube_etcd: false, kube_etcd: true,
kube_proxy: false, kube_proxy: true,
kube_state_metrics: true, kube_state_metrics: true,
prometheus_operator: true, prometheus_operator: true,
core_dns: false, core_dns: true,
kube_scheduler: false, kube_scheduler: true,
alert_receiver_configs: vec![], alert_receiver_configs: vec![],
alert_rules: vec![], alert_rules: vec![],
additional_service_monitors: vec![], additional_service_monitors: vec![],

View File

@ -70,12 +70,12 @@ pub fn kube_prometheus_helm_chart_score(
r#" r#"
global: global:
rbac: rbac:
create: false create: true
prometheus: prometheus:
enabled: {prometheus} enabled: {prometheus}
prometheusSpec: prometheusSpec:
resources: resources:
requests: requests:
cpu: 100m cpu: 100m
memory: 500Mi memory: 500Mi
limits: limits:
@ -121,7 +121,7 @@ defaultRules:
windowsMonitoring: windowsMonitoring:
enabled: {windows_monitoring} enabled: {windows_monitoring}
resources: resources:
requests: requests:
cpu: 100m cpu: 100m
memory: 150Mi memory: 150Mi
limits: limits:
@ -130,13 +130,13 @@ windowsMonitoring:
grafana: grafana:
enabled: {grafana} enabled: {grafana}
resources: resources:
requests: requests:
cpu: 100m cpu: 100m
memory: 150Mi memory: 150Mi
limits: limits:
cpu: 200m cpu: 200m
memory: 250Mi memory: 250Mi
initChownData: initChownData:
resources: resources:
requests: requests:
cpu: 10m cpu: 10m
@ -157,7 +157,7 @@ kubernetesServiceMonitors:
kubeApiServer: kubeApiServer:
enabled: {kubernetes_api_server} enabled: {kubernetes_api_server}
resources: resources:
requests: requests:
cpu: 100m cpu: 100m
memory: 150Mi memory: 150Mi
limits: limits:
@ -166,7 +166,7 @@ kubeApiServer:
kubelet: kubelet:
enabled: {kubelet} enabled: {kubelet}
resources: resources:
requests: requests:
cpu: 100m cpu: 100m
memory: 150Mi memory: 150Mi
limits: limits:
@ -175,7 +175,7 @@ kubelet:
kubeControllerManager: kubeControllerManager:
enabled: {kube_controller_manager} enabled: {kube_controller_manager}
resources: resources:
requests: requests:
cpu: 100m cpu: 100m
memory: 150Mi memory: 150Mi
limits: limits:
@ -184,7 +184,7 @@ kubeControllerManager:
coreDns: coreDns:
enabled: {core_dns} enabled: {core_dns}
resources: resources:
requests: requests:
cpu: 100m cpu: 100m
memory: 150Mi memory: 150Mi
limits: limits:
@ -193,7 +193,7 @@ coreDns:
kubeEtcd: kubeEtcd:
enabled: {kube_etcd} enabled: {kube_etcd}
resources: resources:
requests: requests:
cpu: 100m cpu: 100m
memory: 150Mi memory: 150Mi
limits: limits:
@ -202,7 +202,7 @@ kubeEtcd:
kubeScheduler: kubeScheduler:
enabled: {kube_scheduler} enabled: {kube_scheduler}
resources: resources:
requests: requests:
cpu: 100m cpu: 100m
memory: 150Mi memory: 150Mi
limits: limits:
@ -211,7 +211,7 @@ kubeScheduler:
kubeProxy: kubeProxy:
enabled: {kube_proxy} enabled: {kube_proxy}
resources: resources:
requests: requests:
cpu: 100m cpu: 100m
memory: 150Mi memory: 150Mi
limits: limits:
@ -221,7 +221,7 @@ kubeStateMetrics:
enabled: {kube_state_metrics} enabled: {kube_state_metrics}
kube-state-metrics: kube-state-metrics:
resources: resources:
requests: requests:
cpu: 100m cpu: 100m
memory: 150Mi memory: 150Mi
limits: limits:
@ -230,7 +230,7 @@ kube-state-metrics:
nodeExporter: nodeExporter:
enabled: {node_exporter} enabled: {node_exporter}
resources: resources:
requests: requests:
cpu: 100m cpu: 100m
memory: 150Mi memory: 150Mi
limits: limits:
@ -238,16 +238,16 @@ nodeExporter:
memory: 250Mi memory: 250Mi
prometheus-node-exporter: prometheus-node-exporter:
resources: resources:
requests: requests:
cpu: 100m cpu: 100m
memory: 150Mi memory: 150Mi
limits: limits:
cpu: 200m cpu: 200m
memory: 250Mi memory: 250Mi
prometheusOperator: prometheusOperator:
enabled: false enabled: true
resources: resources:
requests: requests:
cpu: 100m cpu: 100m
memory: 150Mi memory: 150Mi
limits: limits:
@ -255,7 +255,7 @@ prometheusOperator:
memory: 200Mi memory: 200Mi
prometheusConfigReloader: prometheusConfigReloader:
resources: resources:
requests: requests:
cpu: 100m cpu: 100m
memory: 150Mi memory: 150Mi
limits: limits:
@ -267,7 +267,7 @@ prometheusOperator:
limits: limits:
cpu: 10m cpu: 10m
memory: 100Mi memory: 100Mi
requests: requests:
cpu: 10m cpu: 10m
memory: 100Mi memory: 100Mi
patch: patch:
@ -275,7 +275,7 @@ prometheusOperator:
limits: limits:
cpu: 10m cpu: 10m
memory: 100Mi memory: 100Mi
requests: requests:
cpu: 10m cpu: 10m
memory: 100Mi memory: 100Mi
"#, "#,

View File

@ -28,7 +28,7 @@ impl<T: Topology + HelmCommand + TenantManager> Score<T> for HelmPrometheusAlert
.expect("couldn't lock config") .expect("couldn't lock config")
.additional_service_monitors = self.service_monitors.clone(); .additional_service_monitors = self.service_monitors.clone();
Box::new(AlertingInterpret { Box::new(AlertingInterpret {
sender: KubePrometheus::new(), sender: KubePrometheus { config },
receivers: self.receivers.clone(), receivers: self.receivers.clone(),
rules: self.rules.clone(), rules: self.rules.clone(),
}) })

View File

@ -3,7 +3,7 @@ use std::str::FromStr;
use crate::modules::helm::chart::{HelmChartScore, HelmRepository}; use crate::modules::helm::chart::{HelmChartScore, HelmRepository};
pub fn ntfy_helm_chart_score(namespace: String) -> HelmChartScore { pub fn ntfy_helm_chart_score(namespace: String, host: String) -> HelmChartScore {
let values = format!( let values = format!(
r#" r#"
replicaCount: 1 replicaCount: 1
@ -28,12 +28,12 @@ service:
port: 80 port: 80
ingress: ingress:
enabled: false enabled: true
# annotations: # annotations:
# kubernetes.io/ingress.class: nginx # kubernetes.io/ingress.class: nginx
# kubernetes.io/tls-acme: "true" # kubernetes.io/tls-acme: "true"
hosts: hosts:
- host: ntfy.host.com - host: {host}
paths: paths:
- path: / - path: /
pathType: ImplementationSpecific pathType: ImplementationSpecific

View File

@ -17,6 +17,7 @@ use crate::{
#[derive(Debug, Clone, Serialize)] #[derive(Debug, Clone, Serialize)]
pub struct NtfyScore { pub struct NtfyScore {
pub namespace: String, pub namespace: String,
pub host: String,
} }
impl<T: Topology + HelmCommand + K8sclient> Score<T> for NtfyScore { impl<T: Topology + HelmCommand + K8sclient> Score<T> for NtfyScore {
@ -126,7 +127,7 @@ impl<T: Topology + HelmCommand + K8sclient> Interpret<T> for NtfyInterpret {
inventory: &Inventory, inventory: &Inventory,
topology: &T, topology: &T,
) -> Result<Outcome, InterpretError> { ) -> Result<Outcome, InterpretError> {
ntfy_helm_chart_score(self.score.namespace.clone()) ntfy_helm_chart_score(self.score.namespace.clone(), self.score.host.clone())
.create_interpret() .create_interpret()
.execute(inventory, topology) .execute(inventory, topology)
.await?; .await?;