Merge branch 'master' into feat/monitoring_cluster_observability
Some checks failed
Run Check Script / check (pull_request) Failing after 20s

This commit is contained in:
Ian Letourneau 2025-09-08 14:05:02 +00:00
commit 0148d7890d
13 changed files with 371 additions and 113 deletions

3
.gitmodules vendored Normal file
View File

@ -0,0 +1,3 @@
[submodule "examples/try_rust_webapp/tryrust.org"]
path = examples/try_rust_webapp/tryrust.org
url = https://github.com/rust-dd/tryrust.org.git

44
Cargo.lock generated
View File

@ -1838,6 +1838,21 @@ dependencies = [
"url", "url",
] ]
[[package]]
name = "example-try-rust-webapp"
version = "0.1.0"
dependencies = [
"base64 0.22.1",
"env_logger",
"harmony",
"harmony_cli",
"harmony_macros",
"harmony_types",
"log",
"tokio",
"url",
]
[[package]] [[package]]
name = "example-tui" name = "example-tui"
version = "0.1.0" version = "0.1.0"
@ -2318,6 +2333,7 @@ dependencies = [
"tokio-util", "tokio-util",
"url", "url",
"uuid", "uuid",
"walkdir",
] ]
[[package]] [[package]]
@ -4970,6 +4986,15 @@ dependencies = [
"cipher", "cipher",
] ]
[[package]]
name = "same-file"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
dependencies = [
"winapi-util",
]
[[package]] [[package]]
name = "schannel" name = "schannel"
version = "0.1.27" version = "0.1.27"
@ -6513,6 +6538,16 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "walkdir"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
dependencies = [
"same-file",
"winapi-util",
]
[[package]] [[package]]
name = "want" name = "want"
version = "0.3.1" version = "0.3.1"
@ -6695,6 +6730,15 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-util"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0978bf7171b3d90bac376700cb56d606feb40f251a475a5d6634613564460b22"
dependencies = [
"windows-sys 0.60.2",
]
[[package]] [[package]]
name = "winapi-x86_64-pc-windows-gnu" name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0" version = "0.4.0"

View File

@ -30,6 +30,7 @@ async fn main() {
domain: Url::Url(url::Url::parse("https://rustapp.harmony.example.com").unwrap()), domain: Url::Url(url::Url::parse("https://rustapp.harmony.example.com").unwrap()),
project_root: PathBuf::from("./examples/rust/webapp"), project_root: PathBuf::from("./examples/rust/webapp"),
framework: Some(RustWebFramework::Leptos), framework: Some(RustWebFramework::Leptos),
service_port: 3000,
}); });
let webhook_receiver = WebhookReceiver { let webhook_receiver = WebhookReceiver {

View File

@ -20,8 +20,9 @@ async fn main() {
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()),
project_root: PathBuf::from("./webapp"), // Relative from 'harmony-path' param project_root: PathBuf::from("./webapp"),
framework: Some(RustWebFramework::Leptos), framework: Some(RustWebFramework::Leptos),
service_port: 3000,
}); });
let discord_receiver = DiscordWebhook { let discord_receiver = DiscordWebhook {

View File

@ -0,0 +1,17 @@
[package]
name = "example-try-rust-webapp"
edition = "2024"
version.workspace = true
readme.workspace = true
license.workspace = true
[dependencies]
harmony = { path = "../../harmony" }
harmony_cli = { path = "../../harmony_cli" }
harmony_types = { path = "../../harmony_types" }
harmony_macros = { path = "../../harmony_macros" }
tokio = { workspace = true }
log = { workspace = true }
env_logger = { workspace = true }
url = { workspace = true }
base64.workspace = true

View File

@ -0,0 +1,52 @@
use std::{path::PathBuf, sync::Arc};
use harmony::{
inventory::Inventory,
modules::{
application::{
ApplicationScore, RustWebFramework, RustWebapp,
features::{ContinuousDelivery, Monitoring},
},
monitoring::alert_channel::discord_alert_channel::DiscordWebhook,
},
topology::K8sAnywhereTopology,
};
use harmony_types::net::Url;
#[tokio::main]
async fn main() {
let application = Arc::new(RustWebapp {
name: "harmony-example-tryrust".to_string(),
domain: Url::Url(url::Url::parse("https://tryrust.harmony.example.com").unwrap()),
project_root: PathBuf::from("./tryrust.org"),
framework: Some(RustWebFramework::Leptos),
service_port: 8080,
});
let discord_receiver = DiscordWebhook {
name: "test-discord".to_string(),
url: Url::Url(url::Url::parse("https://discord.doesnt.exist.com").unwrap()),
};
let app = ApplicationScore {
features: vec![
Box::new(ContinuousDelivery {
application: application.clone(),
}),
Box::new(Monitoring {
application: application.clone(),
alert_receiver: vec![Box::new(discord_receiver)],
}),
],
application,
};
harmony_cli::run(
Inventory::autoload(),
K8sAnywhereTopology::from_env(),
vec![Box::new(app)],
None,
)
.await
.unwrap();
}

@ -0,0 +1 @@
Subproject commit 0f9ba145172867f467e5320b37d07a5bbb7dd438

View File

@ -66,6 +66,7 @@ tar.workspace = true
base64.workspace = true base64.workspace = true
thiserror.workspace = true thiserror.workspace = true
once_cell = "1.21.3" once_cell = "1.21.3"
walkdir = "2.5.0"
harmony_inventory_agent = { path = "../harmony_inventory_agent" } harmony_inventory_agent = { path = "../harmony_inventory_agent" }
harmony_secret_derive = { version = "0.1.0", path = "../harmony_secret_derive" } harmony_secret_derive = { version = "0.1.0", path = "../harmony_secret_derive" }
askama.workspace = true askama.workspace = true

View File

@ -17,7 +17,7 @@ use kube::{
}; };
use log::{debug, error, trace}; use log::{debug, error, trace};
use serde::{Serialize, de::DeserializeOwned}; use serde::{Serialize, de::DeserializeOwned};
use serde_json::json; use serde_json::{Value, json};
use similar::TextDiff; use similar::TextDiff;
use tokio::io::AsyncReadExt; use tokio::io::AsyncReadExt;
@ -53,6 +53,21 @@ impl K8sClient {
}) })
} }
pub async fn get_resource_json_value(
&self,
name: &str,
namespace: Option<&str>,
gvk: &GroupVersionKind,
) -> Result<DynamicObject, Error> {
let gvk = ApiResource::from_gvk(gvk);
let resource: Api<DynamicObject> = if let Some(ns) = namespace {
Api::namespaced_with(self.client.clone(), ns, &gvk)
} else {
Api::default_namespaced_with(self.client.clone(), &gvk)
};
Ok(resource.get(name).await?)
}
pub async fn get_deployment( pub async fn get_deployment(
&self, &self,
name: &str, name: &str,

View File

@ -176,18 +176,18 @@ impl<
} }
target => { target => {
info!("Deploying {} to target {target:?}", self.application.name()); info!("Deploying {} to target {target:?}", self.application.name());
let score = ArgoHelmScore { let score = ArgoHelmScore {
namespace: "harmony-example-rust-webapp".to_string(), namespace: format!("{}", self.application.name()),
openshift: true, openshift: true,
domain: "argo.harmonydemo.apps.ncd0.harmony.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 --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: "hub.nationtech.io/harmony".to_string(), helm_chart_repo_url: "hub.nationtech.io/harmony".to_string(),
helm_chart_name: "harmony-example-rust-webapp-chart".to_string(), helm_chart_name: format!("{}-chart", self.application.name()),
values_overrides: None, values_overrides: None,
name: "harmony-demo-rust-webapp".to_string(), name: format!("{}", self.application.name()),
namespace: "harmony-example-rust-webapp".to_string(), namespace: format!("{}", self.application.name()),
})], })],
}; };
score score

View File

@ -1,7 +1,10 @@
use async_trait::async_trait; use async_trait::async_trait;
use kube::{Api, api::GroupVersionKind};
use log::{debug, warn};
use non_blank_string_rs::NonBlankString; use non_blank_string_rs::NonBlankString;
use serde::Serialize; use serde::Serialize;
use std::str::FromStr; use serde::de::DeserializeOwned;
use std::{process::Command, str::FromStr, sync::Arc};
use crate::{ use crate::{
data::Version, data::Version,
@ -9,7 +12,9 @@ use crate::{
inventory::Inventory, inventory::Inventory,
modules::helm::chart::{HelmChartScore, HelmRepository}, modules::helm::chart::{HelmChartScore, HelmRepository},
score::Score, score::Score,
topology::{HelmCommand, K8sclient, Topology}, topology::{
HelmCommand, K8sclient, PreparationError, PreparationOutcome, Topology, k8s::K8sClient,
},
}; };
use harmony_types::id::Id; use harmony_types::id::Id;
@ -19,15 +24,13 @@ use super::ArgoApplication;
pub struct ArgoHelmScore { pub struct ArgoHelmScore {
pub namespace: String, pub namespace: String,
pub openshift: bool, pub openshift: bool,
pub domain: String,
pub argo_apps: Vec<ArgoApplication>, pub argo_apps: Vec<ArgoApplication>,
} }
impl<T: Topology + HelmCommand + K8sclient> Score<T> for ArgoHelmScore { impl<T: Topology + HelmCommand + K8sclient> Score<T> for ArgoHelmScore {
fn create_interpret(&self) -> Box<dyn crate::interpret::Interpret<T>> { fn create_interpret(&self) -> Box<dyn crate::interpret::Interpret<T>> {
let helm_score = argo_helm_chart_score(&self.namespace, self.openshift, &self.domain);
Box::new(ArgoInterpret { Box::new(ArgoInterpret {
score: helm_score, score: self.clone(),
argo_apps: self.argo_apps.clone(), argo_apps: self.argo_apps.clone(),
}) })
} }
@ -39,7 +42,7 @@ impl<T: Topology + HelmCommand + K8sclient> Score<T> for ArgoHelmScore {
#[derive(Debug)] #[derive(Debug)]
pub struct ArgoInterpret { pub struct ArgoInterpret {
score: HelmChartScore, score: ArgoHelmScore,
argo_apps: Vec<ArgoApplication>, argo_apps: Vec<ArgoApplication>,
} }
@ -50,9 +53,16 @@ impl<T: Topology + K8sclient + HelmCommand> Interpret<T> for ArgoInterpret {
inventory: &Inventory, inventory: &Inventory,
topology: &T, topology: &T,
) -> Result<Outcome, InterpretError> { ) -> Result<Outcome, InterpretError> {
self.score.interpret(inventory, topology).await?;
let k8s_client = topology.k8s_client().await?; let k8s_client = topology.k8s_client().await?;
let domain = self
.get_host_domain(k8s_client.clone(), self.score.openshift)
.await?;
let domain = format!("argo.{domain}");
let helm_score =
argo_helm_chart_score(&self.score.namespace, self.score.openshift, &domain);
helm_score.interpret(inventory, topology).await?;
k8s_client k8s_client
.apply_yaml_many(&self.argo_apps.iter().map(|a| a.to_yaml()).collect(), None) .apply_yaml_many(&self.argo_apps.iter().map(|a| a.to_yaml()).collect(), None)
.await .await
@ -85,6 +95,38 @@ impl<T: Topology + K8sclient + HelmCommand> Interpret<T> for ArgoInterpret {
} }
} }
impl ArgoInterpret {
pub async fn get_host_domain(
&self,
client: Arc<K8sClient>,
openshift: bool,
) -> Result<String, InterpretError> {
//This should be the job of the topology to determine if we are in
//openshift, potentially we need on openshift topology the same way we create a
//localhosttopology
match openshift {
true => {
let gvk = GroupVersionKind {
group: "operator.openshift.io".into(),
version: "v1".into(),
kind: "IngressController".into(),
};
let ic = client
.get_resource_json_value("default", Some("openshift-ingress-operator"), &gvk)
.await?;
match ic.data["status"]["domain"].as_str() {
Some(domain) => return Ok(domain.to_string()),
None => return Err(InterpretError::new("Could not find domain".to_string())),
}
}
false => {
todo!()
}
};
}
}
pub fn argo_helm_chart_score(namespace: &str, openshift: bool, domain: &str) -> HelmChartScore { pub fn argo_helm_chart_score(namespace: &str, openshift: bool, domain: &str) -> HelmChartScore {
let values = format!( let values = format!(
r#" r#"
@ -660,7 +702,7 @@ server:
# nginx.ingress.kubernetes.io/ssl-passthrough: "true" # nginx.ingress.kubernetes.io/ssl-passthrough: "true"
# -- Defines which ingress controller will implement the resource # -- Defines which ingress controller will implement the resource
ingressClassName: "" ingressClassName: "openshift-default"
# -- Argo CD server hostname # -- Argo CD server hostname
# @default -- `""` (defaults to global.domain) # @default -- `""` (defaults to global.domain)

View File

@ -1,4 +1,5 @@
use std::fs; use std::fs::{self, File};
use std::io::Read;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::process; use std::process;
use std::sync::Arc; use std::sync::Arc;
@ -12,7 +13,8 @@ use dockerfile_builder::instruction_builder::CopyBuilder;
use futures_util::StreamExt; use futures_util::StreamExt;
use log::{debug, info, log_enabled}; use log::{debug, info, log_enabled};
use serde::Serialize; use serde::Serialize;
use tar::Archive; use tar::{Archive, Builder, Header};
use walkdir::WalkDir;
use crate::config::{REGISTRY_PROJECT, REGISTRY_URL}; use crate::config::{REGISTRY_PROJECT, REGISTRY_URL};
use crate::{score::Score, topology::Topology}; use crate::{score::Score, topology::Topology};
@ -59,6 +61,7 @@ pub struct RustWebapp {
pub domain: Url, pub domain: Url,
/// The path to the root of the Rust project to be containerized. /// The path to the root of the Rust project to be containerized.
pub project_root: PathBuf, pub project_root: PathBuf,
pub service_port: u32,
pub framework: Option<RustWebFramework>, pub framework: Option<RustWebFramework>,
} }
@ -158,45 +161,99 @@ impl RustWebapp {
image_name: &str, image_name: &str,
) -> Result<String, Box<dyn std::error::Error>> { ) -> Result<String, Box<dyn std::error::Error>> {
debug!("Generating Dockerfile for '{}'", self.name); debug!("Generating Dockerfile for '{}'", self.name);
let _dockerfile_path = self.build_dockerfile()?; let dockerfile = self.get_or_build_dockerfile();
let docker = Docker::connect_with_socket_defaults().unwrap();
let quiet = !log_enabled!(log::Level::Debug); let quiet = !log_enabled!(log::Level::Debug);
match dockerfile
let build_image_options = bollard::query_parameters::BuildImageOptionsBuilder::default()
.dockerfile("Dockerfile.harmony")
.t(image_name)
.q(quiet)
.version(bollard::query_parameters::BuilderVersion::BuilderV1)
.platform("linux/x86_64");
let mut temp_tar_builder = tar::Builder::new(Vec::new());
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() .unwrap()
.map(|entry| entry.unwrap().path().unwrap().into_owned()) .file_name()
.collect::<Vec<_>>(); .and_then(|os_str| os_str.to_str())
{
Some(path_str) => {
debug!("Building from dockerfile {}", path_str);
debug!("files in docker tar: {:#?}", archived_files); let tar_data = self
.create_deterministic_tar(&self.project_root.clone())
.await
.unwrap();
let mut image_build_stream = docker.build_image( let docker = Docker::connect_with_socket_defaults().unwrap();
build_image_options.build(),
None,
Some(body_full(archive.into())),
);
while let Some(msg) = image_build_stream.next().await { let build_image_options =
debug!("Message: {msg:?}"); bollard::query_parameters::BuildImageOptionsBuilder::default()
.dockerfile(path_str)
.t(image_name)
.q(quiet)
.version(bollard::query_parameters::BuilderVersion::BuilderV1)
.platform("linux/x86_64");
let mut image_build_stream = docker.build_image(
build_image_options.build(),
None,
Some(body_full(tar_data.into())),
);
while let Some(msg) = image_build_stream.next().await {
debug!("Message: {msg:?}");
}
Ok(image_name.to_string())
}
None => Err(Box::new(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"Path is not valid UTF-8",
))),
} }
}
Ok(image_name.to_string()) ///normalizes timestamp and ignores files that will bust the docker cach
async fn create_deterministic_tar(
&self,
project_root: &std::path::Path,
) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
debug!("building tar file from project root {:#?}", project_root);
let mut tar_data = Vec::new();
{
let mut builder = Builder::new(&mut tar_data);
let ignore_prefixes = [
"target",
".git",
".github",
".harmony_generated",
"node_modules",
];
let mut entries: Vec<_> = WalkDir::new(project_root)
.into_iter()
.filter_map(Result::ok)
.filter(|e| e.file_type().is_file())
.filter(|e| {
let rel_path = e.path().strip_prefix(project_root).unwrap();
!ignore_prefixes
.iter()
.any(|prefix| rel_path.starts_with(prefix))
})
.collect();
entries.sort_by_key(|e| e.path().to_owned());
for entry in entries {
let path = entry.path();
let rel_path = path.strip_prefix(project_root).unwrap();
let mut file = fs::File::open(path)?;
let mut header = Header::new_gnu();
header.set_size(entry.metadata()?.len());
header.set_mode(0o644);
header.set_mtime(0);
header.set_uid(0);
header.set_gid(0);
builder.append_data(&mut header, rel_path, &mut file)?;
}
builder.finish()?;
}
Ok(tar_data)
} }
/// Tags and pushes a Docker image to the configured remote registry. /// Tags and pushes a Docker image to the configured remote registry.
@ -272,8 +329,11 @@ impl RustWebapp {
"groupadd -r appgroup && useradd -r -s /bin/false -g appgroup appuser", "groupadd -r appgroup && useradd -r -s /bin/false -g appgroup appuser",
)); ));
dockerfile.push(ENV::from("LEPTOS_SITE_ADDR=0.0.0.0:3000")); dockerfile.push(ENV::from(format!(
dockerfile.push(EXPOSE::from("3000/tcp")); "LEPTOS_SITE_ADDR=0.0.0.0:{}",
self.service_port
)));
dockerfile.push(EXPOSE::from(format!("{}/tcp", self.service_port)));
dockerfile.push(WORKDIR::from("/home/appuser")); dockerfile.push(WORKDIR::from("/home/appuser"));
// Copy static files // Copy static files
@ -394,7 +454,7 @@ image:
service: service:
type: ClusterIP type: ClusterIP
port: 3000 port: {}
ingress: ingress:
enabled: true enabled: true
@ -414,112 +474,123 @@ ingress:
- chart-example.local - chart-example.local
"#, "#,
chart_name, image_repo, image_tag, self.name chart_name, image_repo, image_tag, self.service_port, self.name
); );
fs::write(chart_dir.join("values.yaml"), values_yaml)?; fs::write(chart_dir.join("values.yaml"), values_yaml)?;
// Create templates/_helpers.tpl // Create templates/_helpers.tpl
let helpers_tpl = r#" let helpers_tpl = format!(
{{/* r#"
{{{{/*
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 }}}}
{{/* {{{{/*
Create a default fully qualified app name. 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 }}}}
"#; "#
);
fs::write(templates_dir.join("_helpers.tpl"), helpers_tpl)?; fs::write(templates_dir.join("_helpers.tpl"), helpers_tpl)?;
// Create templates/service.yaml // Create templates/service.yaml
let service_yaml = r#" let service_yaml = format!(
r#"
apiVersion: v1 apiVersion: v1
kind: Service kind: Service
metadata: metadata:
name: {{ include "chart.fullname" . }} name: {{{{ include "chart.fullname" . }}}}
spec: spec:
type: {{ $.Values.service.type }} type: {{{{ $.Values.service.type }}}}
ports: ports:
- name: main - name: main
port: {{ $.Values.service.port | default 3000 }} port: {{{{ $.Values.service.port | default {} }}}}
targetPort: {{ $.Values.service.port | default 3000 }} targetPort: {{{{ $.Values.service.port | default {} }}}}
protocol: TCP protocol: TCP
selector: selector:
app: {{ include "chart.name" . }} app: {{{{ include "chart.name" . }}}}
"#; "#,
self.service_port, self.service_port
);
fs::write(templates_dir.join("service.yaml"), service_yaml)?; fs::write(templates_dir.join("service.yaml"), service_yaml)?;
// Create templates/deployment.yaml // Create templates/deployment.yaml
let deployment_yaml = r#" let deployment_yaml = format!(
r#"
apiVersion: apps/v1 apiVersion: apps/v1
kind: Deployment 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" . }}}}
template: template:
metadata: metadata:
labels: labels:
app: {{ include "chart.name" . }} app: {{{{ include "chart.name" . }}}}
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: main - name: main
containerPort: {{ $.Values.service.port | default 3000 }} containerPort: {{{{ $.Values.service.port | default {} }}}}
protocol: TCP protocol: TCP
"#; "#,
self.service_port
);
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 = format!(
{{- if $.Values.ingress.enabled -}} r#"
{{{{- 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 }}}}
{{- end }} {{{{- end }}}}
secretName: {{ .secretName }} secretName: {{{{ .secretName }}}}
{{- end }} {{{{- end }}}}
{{- end }} {{{{- end }}}}
rules: rules:
{{- range $.Values.ingress.hosts }} {{{{- range $.Values.ingress.hosts }}}}
- host: {{ .host | quote }} - host: {{{{ .host | quote }}}}
http: http:
paths: paths:
{{- range .paths }} {{{{- range .paths }}}}
- path: {{ .path }} - path: {{{{ .path }}}}
pathType: {{ .pathType }} pathType: {{{{ .pathType }}}}
backend: backend:
service: service:
name: {{ include "chart.fullname" $ }} name: {{{{ include "chart.fullname" $ }}}}
port: port:
number: {{ $.Values.service.port | default 3000 }} number: {{{{ $.Values.service.port | default {} }}}}
{{- end }} {{{{- end }}}}
{{- end }} {{{{- end }}}}
{{- end }} {{{{- end }}}}
"#; "#,
self.service_port
);
fs::write(templates_dir.join("ingress.yaml"), ingress_yaml)?; fs::write(templates_dir.join("ingress.yaml"), ingress_yaml)?;
Ok(chart_dir) Ok(chart_dir)
@ -571,7 +642,6 @@ spec:
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_push_url = format!("oci://{}/{}", *REGISTRY_URL, *REGISTRY_PROJECT); let oci_push_url = format!("oci://{}/{}", *REGISTRY_URL, *REGISTRY_PROJECT);
let oci_pull_url = format!("{oci_push_url}/{}-chart", self.name); let oci_pull_url = format!("{oci_push_url}/{}-chart", self.name);
debug!( debug!(
"Pushing Helm chart {} to {}", "Pushing Helm chart {} to {}",
packaged_chart_path.to_string_lossy(), packaged_chart_path.to_string_lossy(),
@ -590,4 +660,20 @@ spec:
debug!("push url {oci_push_url}"); debug!("push url {oci_push_url}");
Ok(format!("{}:{}", oci_pull_url, version)) Ok(format!("{}:{}", oci_pull_url, version))
} }
fn get_or_build_dockerfile(&self) -> Result<PathBuf, Box<dyn std::error::Error>> {
let existing_dockerfile = self.project_root.join("Dockerfile");
debug!("project_root = {:?}", self.project_root);
debug!("checking = {:?}", existing_dockerfile);
if existing_dockerfile.exists() {
debug!(
"Checking path {:#?} for existing Dockerfile",
self.project_root.clone()
);
return Ok(existing_dockerfile);
}
self.build_dockerfile()
}
} }

View File

@ -197,11 +197,6 @@ impl K8sPrometheusCRDAlertingInterpret {
} }
async fn ensure_grafana_operator(&self) -> Result<Outcome, InterpretError> { async fn ensure_grafana_operator(&self) -> Result<Outcome, InterpretError> {
if self.crd_exists("grafanas.grafana.integreatly.org").await {
debug!("grafana CRDs already exist — skipping install.");
return Ok(Outcome::success("Grafana CRDs already exist".to_string()));
}
let _ = Command::new("helm") let _ = Command::new("helm")
.args([ .args([
"repo", "repo",