712 lines
25 KiB
Rust
712 lines
25 KiB
Rust
use std::fs::{self};
|
|
use std::path::{Path, PathBuf};
|
|
use std::process;
|
|
use std::sync::Arc;
|
|
|
|
use async_trait::async_trait;
|
|
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, log_enabled, trace, warn};
|
|
use serde::Serialize;
|
|
use tar::{Builder, Header};
|
|
use walkdir::WalkDir;
|
|
|
|
use crate::config::{REGISTRY_PROJECT, REGISTRY_URL};
|
|
use crate::{score::Score, topology::Topology};
|
|
|
|
use super::{Application, ApplicationFeature, ApplicationInterpret, HelmPackage, OCICompliant};
|
|
|
|
#[derive(Debug, Serialize, Clone)]
|
|
pub struct ApplicationScore<A: Application + Serialize, T: Topology + Clone + Serialize>
|
|
where
|
|
Arc<A>: Serialize + Clone,
|
|
{
|
|
pub features: Vec<Box<dyn ApplicationFeature<T>>>,
|
|
pub application: Arc<A>,
|
|
}
|
|
|
|
impl<
|
|
A: Application + Serialize + Clone + 'static,
|
|
T: Topology + std::fmt::Debug + Clone + Serialize + 'static,
|
|
> Score<T> for ApplicationScore<A, T>
|
|
where
|
|
Arc<A>: Serialize,
|
|
{
|
|
fn create_interpret(&self) -> Box<dyn crate::interpret::Interpret<T>> {
|
|
Box::new(ApplicationInterpret {
|
|
features: self.features.clone(),
|
|
application: self.application.clone(),
|
|
})
|
|
}
|
|
|
|
fn name(&self) -> String {
|
|
format!("{} [ApplicationScore]", self.application.name())
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize)]
|
|
pub enum RustWebFramework {
|
|
Leptos,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize)]
|
|
pub struct RustWebapp {
|
|
pub name: String,
|
|
/// The path to the root of the Rust project to be containerized.
|
|
pub project_root: PathBuf,
|
|
pub service_port: u32,
|
|
pub framework: Option<RustWebFramework>,
|
|
}
|
|
|
|
impl Application for RustWebapp {
|
|
fn name(&self) -> String {
|
|
self.name.clone()
|
|
}
|
|
}
|
|
|
|
#[async_trait]
|
|
impl HelmPackage for RustWebapp {
|
|
async fn build_push_helm_package(
|
|
&self,
|
|
image_url: &str,
|
|
domain: &str,
|
|
) -> Result<String, String> {
|
|
info!("Starting Helm chart build and push for '{}'", self.name);
|
|
|
|
// 1. Create the Helm chart files on disk.
|
|
let chart_dir = self
|
|
.create_helm_chart_files(image_url, domain)
|
|
.await
|
|
.map_err(|e| format!("Failed to create Helm chart files: {}", e))?;
|
|
info!("Successfully created Helm chart files in {:?}", chart_dir);
|
|
|
|
// 2. Package the chart into a .tgz archive.
|
|
let packaged_chart_path = self
|
|
.package_helm_chart(&chart_dir)
|
|
.map_err(|e| format!("Failed to package Helm chart: {}", e))?;
|
|
info!(
|
|
"Successfully packaged Helm chart: {}",
|
|
packaged_chart_path.to_string_lossy()
|
|
);
|
|
|
|
// 3. Push the packaged chart to the OCI registry.
|
|
let oci_chart_url = self
|
|
.push_helm_chart(&packaged_chart_path)
|
|
.map_err(|e| format!("Failed to push Helm chart: {}", e))?;
|
|
info!("Successfully pushed Helm chart to: {}", oci_chart_url);
|
|
|
|
Ok(oci_chart_url)
|
|
}
|
|
}
|
|
|
|
#[async_trait]
|
|
impl OCICompliant for RustWebapp {
|
|
/// Builds a Docker image for the Rust web application using a multi-stage build,
|
|
/// pushes it to the configured OCI registry, and returns the full image tag.
|
|
async fn build_push_oci_image(&self) -> Result<String, String> {
|
|
// This function orchestrates the build and push process.
|
|
// It's async to match the trait definition, though the underlying docker commands are blocking.
|
|
info!("Starting OCI image build and push for '{}'", self.name);
|
|
|
|
// 1. Build the image by calling the synchronous helper function.
|
|
let image_tag = self.image_name();
|
|
self.build_docker_image(&image_tag)
|
|
.await
|
|
.map_err(|e| format!("Failed to build Docker image: {}", e))?;
|
|
info!("Successfully built Docker image: {}", image_tag);
|
|
|
|
// 2. Push the image to the registry.
|
|
self.push_docker_image(&image_tag)
|
|
.await
|
|
.map_err(|e| format!("Failed to push Docker image: {}", e))?;
|
|
info!("Successfully pushed Docker image to: {}", image_tag);
|
|
|
|
Ok(image_tag)
|
|
}
|
|
|
|
fn local_image_name(&self) -> String {
|
|
self.name.clone()
|
|
}
|
|
|
|
fn image_name(&self) -> String {
|
|
format!(
|
|
"{}/{}/{}",
|
|
*REGISTRY_URL,
|
|
*REGISTRY_PROJECT,
|
|
&self.local_image_name()
|
|
)
|
|
}
|
|
}
|
|
|
|
/// Implementation of helper methods for building and pushing the Docker image.
|
|
impl RustWebapp {
|
|
/// Generates a multi-stage Dockerfile for a Rust application.
|
|
fn build_dockerfile(&self) -> Result<PathBuf, Box<dyn std::error::Error>> {
|
|
let mut dockerfile = Dockerfile::new();
|
|
|
|
self.build_builder_image(&mut dockerfile);
|
|
|
|
// Save the Dockerfile to a uniquely named file in the project root to avoid conflicts.
|
|
let dockerfile_path = self.project_root.join("Dockerfile.harmony");
|
|
fs::write(&dockerfile_path, dockerfile.to_string())?;
|
|
|
|
Ok(dockerfile_path)
|
|
}
|
|
|
|
/// Builds the Docker image using the generated Dockerfile.
|
|
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 = self.get_or_build_dockerfile();
|
|
let quiet = !log_enabled!(log::Level::Debug);
|
|
match dockerfile
|
|
.unwrap()
|
|
.file_name()
|
|
.and_then(|os_str| os_str.to_str())
|
|
{
|
|
Some(path_str) => {
|
|
debug!("Building from dockerfile {}", path_str);
|
|
|
|
let tar_data = self
|
|
.create_deterministic_tar(&self.project_root.clone())
|
|
.await
|
|
.unwrap();
|
|
|
|
let docker = Docker::connect_with_socket_defaults().unwrap();
|
|
|
|
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 image_build_stream = docker.build_image(
|
|
build_image_options.build(),
|
|
None,
|
|
Some(body_full(tar_data.into())),
|
|
);
|
|
|
|
while let Some(mut msg) = image_build_stream.next().await {
|
|
trace!("Got bollard msg {msg:?}");
|
|
match msg {
|
|
Ok(mut msg) => {
|
|
if let Some(progress) = msg.progress_detail {
|
|
info!(
|
|
"Build progress {}/{}",
|
|
progress.current.unwrap_or(0),
|
|
progress.total.unwrap_or(0)
|
|
);
|
|
}
|
|
|
|
if let Some(mut log) = msg.stream {
|
|
if log.ends_with('\n') {
|
|
log.pop();
|
|
if log.ends_with('\r') {
|
|
log.pop();
|
|
}
|
|
}
|
|
info!("{log}");
|
|
}
|
|
|
|
if let Some(error) = msg.error {
|
|
warn!("Build error : {error:?}");
|
|
}
|
|
|
|
if let Some(error) = msg.error_detail {
|
|
warn!("Build error : {error:?}");
|
|
}
|
|
}
|
|
Err(e) => {
|
|
error!("Build failed : {e}");
|
|
return Err(format!("Build failed : {e}").into());
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(image_name.to_string())
|
|
}
|
|
|
|
None => Err(Box::new(std::io::Error::new(
|
|
std::io::ErrorKind::InvalidData,
|
|
"Path is not valid UTF-8",
|
|
))),
|
|
}
|
|
}
|
|
|
|
///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",
|
|
"harmony",
|
|
"node_modules",
|
|
"Dockerfile.harmony",
|
|
];
|
|
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.
|
|
async fn push_docker_image(
|
|
&self,
|
|
image_tag: &str,
|
|
) -> Result<String, Box<dyn std::error::Error>> {
|
|
debug!("Pushing docker image {image_tag}");
|
|
|
|
let docker = Docker::connect_with_socket_defaults().unwrap();
|
|
|
|
let mut push_image_stream = docker.push_image(
|
|
image_tag,
|
|
Some(PushImageOptionsBuilder::new().build()),
|
|
None,
|
|
);
|
|
|
|
while let Some(msg) = push_image_stream.next().await {
|
|
// let msg = msg?;
|
|
// TODO this fails silently, for some reason bollard cannot push to hub.nationtech.io
|
|
debug!("Message: {msg:?}");
|
|
}
|
|
|
|
Ok(image_tag.to_string())
|
|
}
|
|
|
|
/// Checks the output of a process command for success.
|
|
fn check_output(
|
|
&self,
|
|
output: &process::Output,
|
|
msg: &str,
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
if !output.status.success() {
|
|
let error_message = format!("{}: {}", msg, String::from_utf8_lossy(&output.stderr));
|
|
return Err(error_message.into());
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn build_builder_image(&self, dockerfile: &mut Dockerfile) {
|
|
match self.framework {
|
|
Some(RustWebFramework::Leptos) => {
|
|
// --- Stage 1: Builder for Leptos ---
|
|
dockerfile.push(FROM::from("rust:bookworm as builder"));
|
|
|
|
// Install dependencies, cargo-binstall, and clean up in one layer
|
|
dockerfile.push(RUN::from(
|
|
"apt-get update && \
|
|
apt-get install -y --no-install-recommends clang wget && \
|
|
wget https://github.com/cargo-bins/cargo-binstall/releases/latest/download/cargo-binstall-x86_64-unknown-linux-musl.tgz && \
|
|
tar -xvf cargo-binstall-x86_64-unknown-linux-musl.tgz && \
|
|
cp cargo-binstall /usr/local/cargo/bin && \
|
|
rm cargo-binstall-x86_64-unknown-linux-musl.tgz cargo-binstall && \
|
|
apt-get clean && \
|
|
rm -rf /var/lib/apt/lists/*"
|
|
));
|
|
|
|
// Install cargo-leptos
|
|
dockerfile.push(RUN::from("cargo binstall cargo-leptos -y"));
|
|
|
|
// Add the WASM target
|
|
dockerfile.push(RUN::from("rustup target add wasm32-unknown-unknown"));
|
|
|
|
// Set up workdir, copy source, and build
|
|
dockerfile.push(WORKDIR::from("/app"));
|
|
dockerfile.push(COPY::from(". ."));
|
|
dockerfile.push(RUN::from("cargo leptos build --release -vv"));
|
|
// --- Stage 2: Final Image ---
|
|
dockerfile.push(FROM::from("debian:bookworm-slim"));
|
|
|
|
// Create a non-root user for security.
|
|
dockerfile.push(RUN::from(
|
|
"groupadd -r appgroup && useradd -r -s /bin/false -g appgroup appuser",
|
|
));
|
|
|
|
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
|
|
dockerfile.push(
|
|
CopyBuilder::builder()
|
|
.from("builder")
|
|
.src("/app/target/site/pkg")
|
|
.dest("/home/appuser/pkg")
|
|
.build()
|
|
.unwrap(),
|
|
);
|
|
// Copy the compiled binary from the builder stage.
|
|
// TODO: Should not be using score name here, instead should use name from Cargo.toml
|
|
// https://git.nationtech.io/NationTech/harmony/issues/105
|
|
let binary_path_in_builder = format!("/app/target/release/{}", self.name);
|
|
let binary_path_in_final = format!("/home/appuser/{}", self.name);
|
|
dockerfile.push(
|
|
CopyBuilder::builder()
|
|
.from("builder")
|
|
.src(binary_path_in_builder)
|
|
.dest(&binary_path_in_final)
|
|
.build()
|
|
.unwrap(),
|
|
);
|
|
|
|
// Run as the non-root user.
|
|
dockerfile.push(USER::from("appuser"));
|
|
|
|
// Set the command to run the application.
|
|
dockerfile.push(CMD::from(binary_path_in_final));
|
|
}
|
|
None => {
|
|
// --- Stage 1: Builder for a generic Rust app ---
|
|
dockerfile.push(FROM::from("rust:latest as builder"));
|
|
|
|
// Install the wasm32 target as required.
|
|
dockerfile.push(RUN::from("rustup target add wasm32-unknown-unknown"));
|
|
dockerfile.push(WORKDIR::from("/app"));
|
|
|
|
// Copy the source code and build the application.
|
|
dockerfile.push(COPY::from(". ."));
|
|
dockerfile.push(RUN::from("cargo build --release --locked"));
|
|
// --- Stage 2: Final Image ---
|
|
dockerfile.push(FROM::from("debian:bookworm-slim"));
|
|
|
|
// Create a non-root user for security.
|
|
dockerfile.push(RUN::from(
|
|
"groupadd -r appgroup && useradd -r -s /bin/false -g appgroup appuser",
|
|
));
|
|
|
|
// Copy only the compiled binary from the builder stage.
|
|
// TODO: Should not be using score name here, instead should use name from Cargo.toml
|
|
// https://git.nationtech.io/NationTech/harmony/issues/105
|
|
let binary_path_in_builder = format!("/app/target/release/{}", self.name);
|
|
let binary_path_in_final = format!("/usr/local/bin/{}", self.name);
|
|
dockerfile.push(
|
|
CopyBuilder::builder()
|
|
.from("builder")
|
|
.src(binary_path_in_builder)
|
|
.dest(&binary_path_in_final)
|
|
.build()
|
|
.unwrap(),
|
|
);
|
|
|
|
// Run as the non-root user.
|
|
dockerfile.push(USER::from("appuser"));
|
|
|
|
// Set the command to run the application.
|
|
dockerfile.push(CMD::from(binary_path_in_final));
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Creates all necessary files for a basic Helm chart.
|
|
async fn create_helm_chart_files(
|
|
&self,
|
|
image_url: &str,
|
|
domain: &str,
|
|
) -> Result<PathBuf, Box<dyn std::error::Error>> {
|
|
let chart_name = format!("{}-chart", self.name);
|
|
let chart_dir = self
|
|
.project_root
|
|
.join(".harmony_generated")
|
|
.join("helm")
|
|
.join(&chart_name);
|
|
let templates_dir = chart_dir.join("templates");
|
|
fs::create_dir_all(&templates_dir)?;
|
|
|
|
let (image_repo, image_tag) = image_url.rsplit_once(':').unwrap_or((image_url, "latest"));
|
|
|
|
// Create Chart.yaml
|
|
let chart_yaml = format!(
|
|
r#"
|
|
apiVersion: v2
|
|
name: {}
|
|
description: A Helm chart for the {} web application.
|
|
type: application
|
|
version: 0.1.0
|
|
appVersion: "{}"
|
|
"#,
|
|
chart_name, self.name, image_tag
|
|
);
|
|
fs::write(chart_dir.join("Chart.yaml"), chart_yaml)?;
|
|
|
|
// Create values.yaml
|
|
let values_yaml = format!(
|
|
r#"
|
|
# Default values for {}.
|
|
# This is a YAML-formatted file.
|
|
# Declare variables to be passed into your templates.
|
|
|
|
replicaCount: 1
|
|
|
|
image:
|
|
repository: {}
|
|
pullPolicy: IfNotPresent
|
|
# Overridden by the chart's appVersion
|
|
tag: "{}"
|
|
|
|
service:
|
|
type: ClusterIP
|
|
port: {}
|
|
|
|
ingress:
|
|
enabled: true
|
|
# Annotations for cert-manager to handle SSL.
|
|
annotations:
|
|
# Add other annotations like nginx ingress class if needed
|
|
# kubernetes.io/ingress.class: nginx
|
|
hosts:
|
|
- host: {}
|
|
paths:
|
|
- path: /
|
|
pathType: ImplementationSpecific
|
|
"#,
|
|
chart_name, image_repo, image_tag, self.service_port, domain,
|
|
);
|
|
fs::write(chart_dir.join("values.yaml"), values_yaml)?;
|
|
|
|
// Create templates/_helpers.tpl
|
|
let helpers_tpl = format!(
|
|
r#"
|
|
{{{{/*
|
|
Expand the name of the chart.
|
|
*/}}}}
|
|
{{{{- 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 }}}}
|
|
"#
|
|
);
|
|
fs::write(templates_dir.join("_helpers.tpl"), helpers_tpl)?;
|
|
|
|
// Create templates/service.yaml
|
|
let service_yaml = format!(
|
|
r#"
|
|
apiVersion: v1
|
|
kind: Service
|
|
metadata:
|
|
name: {{{{ include "chart.fullname" . }}}}
|
|
spec:
|
|
type: {{{{ $.Values.service.type }}}}
|
|
ports:
|
|
- name: main
|
|
port: {{{{ $.Values.service.port | default {} }}}}
|
|
targetPort: {{{{ $.Values.service.port | default {} }}}}
|
|
protocol: TCP
|
|
selector:
|
|
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 = format!(
|
|
r#"
|
|
apiVersion: apps/v1
|
|
kind: Deployment
|
|
metadata:
|
|
name: {{{{ include "chart.fullname" . }}}}
|
|
spec:
|
|
replicas: {{{{ $.Values.replicaCount }}}}
|
|
selector:
|
|
matchLabels:
|
|
app: {{{{ include "chart.name" . }}}}
|
|
template:
|
|
metadata:
|
|
labels:
|
|
app: {{{{ include "chart.name" . }}}}
|
|
spec:
|
|
containers:
|
|
- 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 {} }}}}
|
|
protocol: TCP
|
|
"#,
|
|
self.service_port
|
|
);
|
|
fs::write(templates_dir.join("deployment.yaml"), deployment_yaml)?;
|
|
|
|
// Create templates/ingress.yaml
|
|
let ingress_yaml = format!(
|
|
r#"
|
|
{{{{- if $.Values.ingress.enabled -}}}}
|
|
apiVersion: networking.k8s.io/v1
|
|
kind: Ingress
|
|
metadata:
|
|
name: {{{{ include "chart.fullname" . }}}}
|
|
annotations:
|
|
{{{{- toYaml $.Values.ingress.annotations | nindent 4 }}}}
|
|
spec:
|
|
{{{{- if $.Values.ingress.tls }}}}
|
|
tls:
|
|
{{{{- range $.Values.ingress.tls }}}}
|
|
- hosts:
|
|
{{{{- range .hosts }}}}
|
|
- {{{{ . | quote }}}}
|
|
{{{{- end }}}}
|
|
secretName: {{{{ .secretName }}}}
|
|
{{{{- end }}}}
|
|
{{{{- end }}}}
|
|
rules:
|
|
{{{{- range $.Values.ingress.hosts }}}}
|
|
- host: {{{{ .host | quote }}}}
|
|
http:
|
|
paths:
|
|
{{{{- range .paths }}}}
|
|
- path: {{{{ .path }}}}
|
|
pathType: {{{{ .pathType }}}}
|
|
backend:
|
|
service:
|
|
name: {{{{ include "chart.fullname" $ }}}}
|
|
port:
|
|
number: {{{{ $.Values.service.port | default {} }}}}
|
|
{{{{- end }}}}
|
|
{{{{- end }}}}
|
|
{{{{- end }}}}
|
|
"#,
|
|
self.service_port
|
|
);
|
|
fs::write(templates_dir.join("ingress.yaml"), ingress_yaml)?;
|
|
|
|
Ok(chart_dir)
|
|
}
|
|
|
|
/// Packages a Helm chart directory into a .tgz file.
|
|
fn package_helm_chart(&self, chart_dir: &Path) -> Result<PathBuf, Box<dyn std::error::Error>> {
|
|
let chart_dirname = chart_dir.file_name().expect("Should find a chart dirname");
|
|
debug!(
|
|
"Launching `helm package {}` cli with CWD {}",
|
|
chart_dirname.to_string_lossy(),
|
|
&self
|
|
.project_root
|
|
.join(".harmony_generated")
|
|
.join("helm")
|
|
.to_string_lossy()
|
|
);
|
|
let output = process::Command::new("helm")
|
|
.args(["package", chart_dirname.to_str().unwrap()])
|
|
.current_dir(self.project_root.join(".harmony_generated").join("helm")) // Run package from the parent dir
|
|
.output()?;
|
|
|
|
self.check_output(&output, "Failed to package Helm chart")?;
|
|
|
|
// Helm prints the path of the created chart to stdout.
|
|
let tgz_name = String::from_utf8(output.stdout)?
|
|
.split_whitespace()
|
|
.last()
|
|
.unwrap_or_default()
|
|
.to_string();
|
|
if tgz_name.is_empty() {
|
|
return Err("Could not determine packaged chart filename.".into());
|
|
}
|
|
|
|
// The output from helm is relative, so we join it with the execution directory.
|
|
Ok(self
|
|
.project_root
|
|
.join(".harmony_generated")
|
|
.join("helm")
|
|
.join(tgz_name))
|
|
}
|
|
|
|
/// Pushes a packaged Helm chart to an OCI registry.
|
|
fn push_helm_chart(
|
|
&self,
|
|
packaged_chart_path: &Path,
|
|
) -> Result<String, Box<dyn std::error::Error>> {
|
|
// The chart name is the file stem of the .tgz file
|
|
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(),
|
|
oci_push_url
|
|
);
|
|
|
|
let output = process::Command::new("helm")
|
|
.args(["push", packaged_chart_path.to_str().unwrap(), &oci_push_url])
|
|
.output()?;
|
|
|
|
self.check_output(&output, "Pushing Helm chart failed")?;
|
|
|
|
// The final URL includes the version tag, which is part of the file name
|
|
let version = chart_file_name.rsplit_once('-').unwrap().1;
|
|
debug!("pull url {oci_pull_url}");
|
|
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()
|
|
}
|
|
}
|