feat(example): added an example of packaging a rust app from github
All checks were successful
Run Check Script / check (pull_request) Successful in 1m0s
All checks were successful
Run Check Script / check (pull_request) Successful in 1m0s
This commit is contained in:
parent
3ca31179d0
commit
aebde3a382
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal 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
|
@ -29,6 +29,7 @@ async fn main() {
|
||||
domain: Url::Url(url::Url::parse("https://rustapp.harmony.example.com").unwrap()),
|
||||
project_root: PathBuf::from("./examples/rust/webapp"),
|
||||
framework: Some(RustWebFramework::Leptos),
|
||||
service_port: 3000,
|
||||
});
|
||||
|
||||
let webhook_receiver = WebhookReceiver {
|
||||
|
@ -19,8 +19,9 @@ async fn main() {
|
||||
let application = Arc::new(RustWebapp {
|
||||
name: "harmony-example-rust-webapp".to_string(),
|
||||
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),
|
||||
service_port: 3000,
|
||||
});
|
||||
|
||||
let discord_receiver = DiscordWebhook {
|
||||
|
12
examples/try_rust_webapp/Cargo.toml
Normal file
12
examples/try_rust_webapp/Cargo.toml
Normal file
@ -0,0 +1,12 @@
|
||||
[package]
|
||||
name = "example-try-rust-webapp"
|
||||
edition = "2024"
|
||||
version.workspace = true
|
||||
readme.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
harmony = { version = "0.1.0", path = "../../harmony" }
|
||||
harmony_cli = { version = "0.1.0", path = "../../harmony_cli" }
|
||||
tokio.workspace = true
|
||||
url.workspace = true
|
53
examples/try_rust_webapp/src/main.rs
Normal file
53
examples/try_rust_webapp/src/main.rs
Normal file
@ -0,0 +1,53 @@
|
||||
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, webhook_receiver::WebhookReceiver,
|
||||
},
|
||||
},
|
||||
topology::{K8sAnywhereTopology, 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();
|
||||
}
|
1
examples/try_rust_webapp/tryrust.org
Submodule
1
examples/try_rust_webapp/tryrust.org
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit 0f9ba145172867f467e5320b37d07a5bbb7dd438
|
@ -61,6 +61,7 @@ pub struct RustWebapp {
|
||||
pub domain: Url,
|
||||
/// The path to the root of the Rust project to be containerized.
|
||||
pub project_root: PathBuf,
|
||||
pub service_port: u32,
|
||||
pub framework: Option<RustWebFramework>,
|
||||
}
|
||||
|
||||
@ -160,45 +161,56 @@ impl RustWebapp {
|
||||
image_name: &str,
|
||||
) -> Result<String, Box<dyn std::error::Error>> {
|
||||
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 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()
|
||||
match dockerfile
|
||||
.unwrap()
|
||||
.map(|entry| entry.unwrap().path().unwrap().into_owned())
|
||||
.collect::<Vec<_>>();
|
||||
.file_name()
|
||||
.and_then(|os_str| os_str.to_str())
|
||||
{
|
||||
Some(path_str) => {
|
||||
debug!("Building from dockerfile {}", path_str);
|
||||
let build_image_options =
|
||||
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 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()
|
||||
.map(|entry| entry.unwrap().path().unwrap().into_owned())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
debug!("files in docker tar: {:#?}", archived_files);
|
||||
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 mut image_build_stream = docker.build_image(
|
||||
build_image_options.build(),
|
||||
None,
|
||||
Some(body_full(archive.into())),
|
||||
);
|
||||
|
||||
while let Some(msg) = image_build_stream.next().await {
|
||||
debug!("Message: {msg:?}");
|
||||
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())
|
||||
}
|
||||
|
||||
/// Tags and pushes a Docker image to the configured remote registry.
|
||||
@ -274,8 +286,11 @@ impl RustWebapp {
|
||||
"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(EXPOSE::from("3000/tcp"));
|
||||
dockerfile.push(ENV::from(format!(
|
||||
"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"));
|
||||
|
||||
// Copy static files
|
||||
@ -396,7 +411,7 @@ image:
|
||||
|
||||
service:
|
||||
type: ClusterIP
|
||||
port: 3000
|
||||
port: {}
|
||||
|
||||
ingress:
|
||||
enabled: true
|
||||
@ -416,112 +431,123 @@ ingress:
|
||||
- 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)?;
|
||||
|
||||
// Create templates/_helpers.tpl
|
||||
let helpers_tpl = r#"
|
||||
{{/*
|
||||
let helpers_tpl = format!(
|
||||
r#"
|
||||
{{{{/*
|
||||
Expand the name of the chart.
|
||||
*/}}
|
||||
{{- define "chart.name" -}}
|
||||
{{- default .Chart.Name $.Values.nameOverride | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
*/}}}}
|
||||
{{{{- define "chart.name" -}}}}
|
||||
{{{{- default .Chart.Name $.Values.nameOverride | trunc 63 | trimSuffix "-" }}}}
|
||||
{{{{- end }}}}
|
||||
|
||||
{{/*
|
||||
{{{{/*
|
||||
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).
|
||||
*/}}
|
||||
{{- define "chart.fullname" -}}
|
||||
{{- $name := default .Chart.Name $.Values.nameOverride }}
|
||||
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
"#;
|
||||
*/}}}}
|
||||
{{{{- define "chart.fullname" -}}}}
|
||||
{{{{- $name := default .Chart.Name $.Values.nameOverride }}}}
|
||||
{{{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}}}
|
||||
{{{{- end }}}}
|
||||
"#
|
||||
);
|
||||
fs::write(templates_dir.join("_helpers.tpl"), helpers_tpl)?;
|
||||
|
||||
// Create templates/service.yaml
|
||||
let service_yaml = r#"
|
||||
let service_yaml = format!(
|
||||
r#"
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "chart.fullname" . }}
|
||||
name: {{{{ include "chart.fullname" . }}}}
|
||||
spec:
|
||||
type: {{ $.Values.service.type }}
|
||||
type: {{{{ $.Values.service.type }}}}
|
||||
ports:
|
||||
- name: main
|
||||
port: {{ $.Values.service.port | default 3000 }}
|
||||
targetPort: {{ $.Values.service.port | default 3000 }}
|
||||
port: {{{{ $.Values.service.port | default {} }}}}
|
||||
targetPort: {{{{ $.Values.service.port | default {} }}}}
|
||||
protocol: TCP
|
||||
selector:
|
||||
app: {{ include "chart.name" . }}
|
||||
"#;
|
||||
app: {{{{ include "chart.name" . }}}}
|
||||
"#,
|
||||
self.service_port, self.service_port
|
||||
);
|
||||
fs::write(templates_dir.join("service.yaml"), service_yaml)?;
|
||||
|
||||
// Create templates/deployment.yaml
|
||||
let deployment_yaml = r#"
|
||||
let deployment_yaml = format!(
|
||||
r#"
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "chart.fullname" . }}
|
||||
name: {{{{ include "chart.fullname" . }}}}
|
||||
spec:
|
||||
replicas: {{ $.Values.replicaCount }}
|
||||
replicas: {{{{ $.Values.replicaCount }}}}
|
||||
selector:
|
||||
matchLabels:
|
||||
app: {{ include "chart.name" . }}
|
||||
app: {{{{ include "chart.name" . }}}}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: {{ include "chart.name" . }}
|
||||
app: {{{{ include "chart.name" . }}}}
|
||||
spec:
|
||||
containers:
|
||||
- name: {{ .Chart.Name }}
|
||||
image: "{{ $.Values.image.repository }}:{{ $.Values.image.tag | default .Chart.AppVersion }}"
|
||||
imagePullPolicy: {{ $.Values.image.pullPolicy }}
|
||||
- name: {{{{ .Chart.Name }}}}
|
||||
image: "{{{{ $.Values.image.repository }}}}:{{{{ $.Values.image.tag | default .Chart.AppVersion }}}}"
|
||||
imagePullPolicy: {{{{ $.Values.image.pullPolicy }}}}
|
||||
ports:
|
||||
- name: main
|
||||
containerPort: {{ $.Values.service.port | default 3000 }}
|
||||
containerPort: {{{{ $.Values.service.port | default {} }}}}
|
||||
protocol: TCP
|
||||
"#;
|
||||
"#,
|
||||
self.service_port
|
||||
);
|
||||
fs::write(templates_dir.join("deployment.yaml"), deployment_yaml)?;
|
||||
|
||||
// Create templates/ingress.yaml
|
||||
let ingress_yaml = r#"
|
||||
{{- if $.Values.ingress.enabled -}}
|
||||
let ingress_yaml = format!(
|
||||
r#"
|
||||
{{{{- if $.Values.ingress.enabled -}}}}
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: {{ include "chart.fullname" . }}
|
||||
name: {{{{ include "chart.fullname" . }}}}
|
||||
annotations:
|
||||
{{- toYaml $.Values.ingress.annotations | nindent 4 }}
|
||||
{{{{- toYaml $.Values.ingress.annotations | nindent 4 }}}}
|
||||
spec:
|
||||
{{- if $.Values.ingress.tls }}
|
||||
{{{{- if $.Values.ingress.tls }}}}
|
||||
tls:
|
||||
{{- range $.Values.ingress.tls }}
|
||||
{{{{- range $.Values.ingress.tls }}}}
|
||||
- hosts:
|
||||
{{- range .hosts }}
|
||||
- {{ . | quote }}
|
||||
{{- end }}
|
||||
secretName: {{ .secretName }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{{{- range .hosts }}}}
|
||||
- {{{{ . | quote }}}}
|
||||
{{{{- end }}}}
|
||||
secretName: {{{{ .secretName }}}}
|
||||
{{{{- end }}}}
|
||||
{{{{- end }}}}
|
||||
rules:
|
||||
{{- range $.Values.ingress.hosts }}
|
||||
- host: {{ .host | quote }}
|
||||
{{{{- range $.Values.ingress.hosts }}}}
|
||||
- host: {{{{ .host | quote }}}}
|
||||
http:
|
||||
paths:
|
||||
{{- range .paths }}
|
||||
- path: {{ .path }}
|
||||
pathType: {{ .pathType }}
|
||||
{{{{- range .paths }}}}
|
||||
- path: {{{{ .path }}}}
|
||||
pathType: {{{{ .pathType }}}}
|
||||
backend:
|
||||
service:
|
||||
name: {{ include "chart.fullname" $ }}
|
||||
name: {{{{ include "chart.fullname" $ }}}}
|
||||
port:
|
||||
number: {{ $.Values.service.port | default 3000 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
"#;
|
||||
number: {{{{ $.Values.service.port | default {} }}}}
|
||||
{{{{- end }}}}
|
||||
{{{{- end }}}}
|
||||
{{{{- end }}}}
|
||||
"#,
|
||||
self.service_port
|
||||
);
|
||||
fs::write(templates_dir.join("ingress.yaml"), ingress_yaml)?;
|
||||
|
||||
Ok(chart_dir)
|
||||
@ -573,7 +599,6 @@ spec:
|
||||
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_pull_url = format!("{oci_push_url}/{}-chart", self.name);
|
||||
|
||||
debug!(
|
||||
"Pushing Helm chart {} to {}",
|
||||
packaged_chart_path.to_string_lossy(),
|
||||
@ -592,4 +617,20 @@ spec:
|
||||
debug!("push url {oci_push_url}");
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user