feat(example): added an example of packaging a rust app from github #124
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()),
|
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 {
|
||||||
|
|||||||
@ -19,8 +19,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 {
|
||||||
|
|||||||
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,
|
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>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -160,19 +161,23 @@ 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 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()
|
.unwrap()
|
||||||
.dockerfile("Dockerfile.harmony")
|
.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)
|
.t(image_name)
|
||||||
.q(quiet)
|
.q(quiet)
|
||||||
.version(bollard::query_parameters::BuilderVersion::BuilderV1)
|
.version(bollard::query_parameters::BuilderVersion::BuilderV1)
|
||||||
.platform("linux/x86_64");
|
.platform("linux/x86_64");
|
||||||
|
|
||||||
let mut temp_tar_builder = tar::Builder::new(Vec::new());
|
let mut temp_tar_builder = tar::Builder::new(Vec::new());
|
||||||
temp_tar_builder
|
temp_tar_builder
|
||||||
.append_dir_all("", self.project_root.clone())
|
.append_dir_all("", self.project_root.clone())
|
||||||
@ -201,6 +206,13 @@ impl RustWebapp {
|
|||||||
Ok(image_name.to_string())
|
Ok(image_name.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
None => Err(Box::new(std::io::Error::new(
|
||||||
|
std::io::ErrorKind::InvalidData,
|
||||||
|
"Path is not valid UTF-8",
|
||||||
|
))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Tags and pushes a Docker image to the configured remote registry.
|
/// Tags and pushes a Docker image to the configured remote registry.
|
||||||
async fn push_docker_image(
|
async fn push_docker_image(
|
||||||
&self,
|
&self,
|
||||||
@ -274,8 +286,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
|
||||||
@ -396,7 +411,7 @@ image:
|
|||||||
|
|
||||||
service:
|
service:
|
||||||
type: ClusterIP
|
type: ClusterIP
|
||||||
port: 3000
|
port: {}
|
||||||
|
|
||||||
ingress:
|
ingress:
|
||||||
enabled: true
|
enabled: true
|
||||||
@ -416,112 +431,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)
|
||||||
@ -573,7 +599,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(),
|
||||||
@ -592,4 +617,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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user