Compare commits

...

9 Commits

Author SHA1 Message Date
tahahawa
28476af222 push using bollard
Some checks failed
Run Check Script / check (pull_request) Failing after -44s
2025-07-09 00:24:02 -04:00
tahahawa
de6969f824 build using bollard, not CLI 2025-07-09 00:13:51 -04:00
tahahawa
a9aa989b66 unjank argo app yaml 2025-07-09 00:13:42 -04:00
tahahawa
0faf85d850 uncomment docker build 2025-07-09 00:13:27 -04:00
tahahawa
cb18ba8e45 cargo lock 2025-07-09 00:13:11 -04:00
tahahawa
50870be2d3 add deps 2025-07-09 00:13:06 -04:00
tahahawa
226fa39f53 add deps 2025-07-09 00:12:42 -04:00
tahahawa
753c3eb9d5 add monitoring and ntfy 2025-07-09 00:12:39 -04:00
tahahawa
9fe586532f unjank the yaml func 2025-07-09 00:12:25 -04:00
9 changed files with 185 additions and 65 deletions

36
Cargo.lock generated
View File

@ -1355,6 +1355,7 @@ dependencies = [
name = "example-rust"
version = "0.1.0"
dependencies = [
"base64 0.22.1",
"env_logger",
"harmony",
"harmony_cli",
@ -1427,6 +1428,18 @@ version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "flate2"
version = "1.1.2"
@ -1766,6 +1779,7 @@ dependencies = [
"serde_yaml",
"similar",
"strum 0.27.1",
"tar",
"temp-dir",
"temp-file",
"tempfile",
@ -2729,6 +2743,7 @@ checksum = "1580801010e535496706ba011c15f8532df6b42297d2e471fec38ceadd8c0638"
dependencies = [
"bitflags 2.9.1",
"libc",
"redox_syscall",
]
[[package]]
@ -4697,6 +4712,17 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "temp-dir"
version = "0.1.16"
@ -5742,6 +5768,16 @@ dependencies = [
"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]]
name = "xml-rs"
version = "0.8.26"

View File

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

View File

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

View File

@ -1,5 +1,6 @@
use std::{path::PathBuf, sync::Arc};
use base64::{Engine as _, engine::general_purpose};
use harmony::{
data::Id,
inventory::Inventory,
@ -9,11 +10,17 @@ use harmony::{
ApplicationScore, RustWebFramework, RustWebapp,
features::{ContinuousDelivery, Monitoring},
},
monitoring::{
alert_channel::webhook_receiver::WebhookReceiver,
kube_prometheus::helm_prometheus_alert_score::HelmPrometheusAlertingScore,
ntfy::ntfy::NtfyScore,
},
tenant::TenantScore,
},
score::Score,
topology::{
K8sAnywhereTopology, Url,
tenant::{ResourceLimits, TenantConfig, TenantNetworkPolicy},
tenant::{ResourceLimits, TenantConfig, TenantManager, TenantNetworkPolicy},
},
};
@ -36,6 +43,17 @@ async fn main() {
},
};
let topology = K8sAnywhereTopology::from_env();
// topology
// .provision_tenant(&tenant.config)
// .await
// .expect("couldn't provision tenant");
let mut maestro = Maestro::initialize(Inventory::autoload(), topology)
.await
.unwrap();
let application = Arc::new(RustWebapp {
name: "harmony-example-rust-webapp".to_string(),
domain: Url::Url(url::Url::parse("https://rustapp.harmony.example.com").unwrap()),
@ -43,21 +61,59 @@ async fn main() {
framework: Some(RustWebFramework::Leptos),
});
let ntfy = NtfyScore {
namespace: tenant.clone().config.name,
};
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}"
))
);
let ntfy_default_auth_param = general_purpose::STANDARD
.encode(ntfy_default_auth_header)
.rsplit("=")
.collect::<Vec<&str>>()[0]
.to_string();
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}",
tenant.clone().config.name
)
.as_str(),
)
.unwrap(),
),
};
let alerting_score = HelmPrometheusAlertingScore {
receivers: vec![Box::new(ntfy_receiver)],
rules: vec![],
service_monitors: vec![],
};
let app = ApplicationScore {
features: vec![
Box::new(ContinuousDelivery {
application: application.clone(),
}),
Box::new(Monitoring {}),
// TODO add monitoring, backups, multisite ha, etc
}), // TODO add monitoring, backups, multisite ha, etc
],
application,
};
let topology = K8sAnywhereTopology::from_env();
let mut maestro = Maestro::initialize(Inventory::autoload(), topology)
.await
.unwrap();
maestro.register_all(vec![Box::new(tenant), Box::new(app)]);
maestro.register_all(vec![
Box::new(tenant),
Box::new(ntfy),
Box::new(alerting_score),
Box::new(app),
]);
harmony_cli::init(maestro, None).await.unwrap();
}

View File

@ -60,6 +60,7 @@ strum = { version = "0.27.1", features = ["derive"] }
tempfile = "3.20.0"
serde_with = "3.14.0"
bollard.workspace = true
tar.workspace = true
[dev-dependencies]
pretty_assertions.workspace = true

View File

@ -2,7 +2,10 @@ use derive_new::new;
use futures_util::StreamExt;
use k8s_openapi::{
ClusterResourceScope, NamespaceResourceScope,
api::{apps::v1::Deployment, core::v1::Pod},
api::{
apps::v1::Deployment,
core::v1::{ObjectReference, Pod},
},
};
use kube::{
Client, Config, Error, Resource,
@ -244,37 +247,39 @@ impl K8sClient {
pub async fn apply_yaml_many(
&self,
api_resource: &ApiResource,
yaml: &Vec<serde_yaml::Value>,
ns: Option<&str>,
) -> Result<(), Error> {
for y in yaml.iter() {
self.apply_yaml(y, ns).await?;
self.apply_yaml(api_resource, y, ns).await?;
}
Ok(())
}
pub async fn apply_yaml(
&self,
api_resource: &ApiResource,
yaml: &serde_yaml::Value,
ns: Option<&str>,
) -> Result<(), Error> {
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 namespace = obj
.metadata
.namespace
.as_ref()
.expect("YAML must have a namespace");
// 4. Define the API resource type using the GVK from the object.
// The plural name 'applications' is taken from your CRD definition.
error!("This only supports argocd application harcoded, very rrrong");
let gvk = GroupVersionKind::gvk("argoproj.io", "v1alpha1", "Application");
let api_resource = ApiResource::from_gvk_with_plural(&gvk, "applications");
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.
let api: Api<DynamicObject> =
Api::namespaced_with(self.client.clone(), namespace, &api_resource);
Api::namespaced_with(self.client.clone(), namespace, api_resource);
// 6. Apply the object to the cluster using Server-Side Apply.
// This will create the resource if it doesn't exist, or update it if it does.

View File

@ -159,7 +159,7 @@ impl<
info!("Pushed new helm chart {helm_chart}");
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!("Installing ContinuousDelivery feature");

View File

@ -1,4 +1,5 @@
use async_trait::async_trait;
use kube::api::{ApiResource, GroupVersionKind};
use log::error;
use non_blank_string_rs::NonBlankString;
use serde::Serialize;
@ -56,9 +57,16 @@ impl<T: Topology + K8sclient + HelmCommand> Interpret<T> for ArgoInterpret {
.execute(inventory, topology)
.await?;
let gvk = GroupVersionKind::gvk("argoproj.io", "v1alpha1", "Application");
let api_resource = ApiResource::from_gvk_with_plural(&gvk, "applications");
let k8s_client = topology.k8s_client().await?;
k8s_client
.apply_yaml_many(&self.argo_apps.iter().map(|a| a.to_yaml()).collect(), None)
.apply_yaml_many(
&api_resource,
&self.argo_apps.iter().map(|a| a.to_yaml()).collect(),
None,
)
.await
.unwrap();
Ok(Outcome::success(format!(

View File

@ -1,14 +1,21 @@
use std::fs;
use std::io::Read;
use std::path::PathBuf;
use std::process;
use std::sync::Arc;
use async_trait::async_trait;
use bollard::image::PushImageOptions;
use bollard::query_parameters::PushImageOptionsBuilder;
use bollard::{Docker, body_full};
use dockerfile_builder::Dockerfile;
use dockerfile_builder::instruction::{CMD, COPY, ENV, EXPOSE, FROM, RUN, USER, WORKDIR};
use dockerfile_builder::instruction_builder::CopyBuilder;
use futures_util::StreamExt;
use log::{debug, error, info};
use serde::Serialize;
use tar::Archive;
use tempfile::tempfile;
use crate::config::{REGISTRY_PROJECT, REGISTRY_URL};
use crate::{
@ -108,6 +115,7 @@ impl OCICompliant for RustWebapp {
// 1. Build the local image by calling the synchronous helper function.
let local_image_name = self.local_image_name();
self.build_docker_image(&local_image_name)
.await
.map_err(|e| format!("Failed to build Docker image: {}", e))?;
info!(
"Successfully built local Docker image: {}",
@ -117,6 +125,7 @@ impl OCICompliant for RustWebapp {
let remote_image_name = self.image_name();
// 2. Push the image to the registry.
self.push_docker_image(&local_image_name, &remote_image_name)
.await
.map_err(|e| format!("Failed to push Docker image: {}", e))?;
info!("Successfully pushed Docker image to: {}", remote_image_name);
@ -153,66 +162,68 @@ impl RustWebapp {
}
/// Builds the Docker image using the generated Dockerfile.
pub fn build_docker_image(
pub async fn build_docker_image(
&self,
image_name: &str,
) -> Result<String, Box<dyn std::error::Error>> {
info!("Generating Dockerfile for '{}'", self.name);
let dockerfile_path = self.build_dockerfile()?;
let _dockerfile_path = self.build_dockerfile()?;
info!(
"Building Docker image with file {} from root {}",
dockerfile_path.to_string_lossy(),
self.project_root.to_string_lossy()
let docker = Docker::connect_with_socket_defaults().unwrap();
let build_image_options = bollard::query_parameters::BuildImageOptionsBuilder::default()
.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())
}
/// Tags and pushes a Docker image to the configured remote registry.
fn push_docker_image(
async fn push_docker_image(
&self,
image_name: &str,
full_tag: &str,
) -> Result<String, Box<dyn std::error::Error>> {
info!("Pushing docker image {full_tag}");
// Tag the image for the remote registry.
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)
);
let docker = Docker::connect_with_socket_defaults().unwrap();
// Push the image.
let output = process::Command::new("docker")
.args(["push", &full_tag])
.spawn()?
.wait_with_output()?;
self.check_output(&output, "Pushing docker image failed")?;
debug!(
"docker push output: stdout: {}, stderr: {}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
// let push_options = PushImageOptionsBuilder::new().tag(tag);
let mut push_image_stream =
docker.push_image(full_tag, Some(PushImageOptionsBuilder::new().build()), None);
while let Some(msg) = push_image_stream.next().await {
println!("Message: {msg:?}");
}
Ok(full_tag.to_string())
}