Compare commits
9 Commits
6de889aa0f
...
28476af222
Author | SHA1 | Date | |
---|---|---|---|
|
28476af222 | ||
|
de6969f824 | ||
|
a9aa989b66 | ||
|
0faf85d850 | ||
|
cb18ba8e45 | ||
|
50870be2d3 | ||
|
226fa39f53 | ||
|
753c3eb9d5 | ||
|
9fe586532f |
36
Cargo.lock
generated
36
Cargo.lock
generated
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -12,3 +12,4 @@ tokio = { workspace = true }
|
||||
log = { workspace = true }
|
||||
env_logger = { workspace = true }
|
||||
url = { workspace = true }
|
||||
base64.workspace = true
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
@ -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");
|
||||
|
@ -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!(
|
||||
|
@ -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())
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user