feat: harmony now defaults to using local k3d cluster. Also created OCICompliant: Application trait to make building images cleaner
#76
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,3 +1,4 @@
|
|||||||
target
|
target
|
||||||
private_repos
|
private_repos
|
||||||
log/
|
log/
|
||||||
|
*.tgz
|
||||||
|
|||||||
@ -1,19 +0,0 @@
|
|||||||
apiVersion: v1
|
|
||||||
clusters:
|
|
||||||
- cluster:
|
|
||||||
certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJlRENDQVIyZ0F3SUJBZ0lCQURBS0JnZ3Foa2pPUFFRREFqQWpNU0V3SHdZRFZRUUREQmhyTTNNdGMyVnkKZG1WeUxXTmhRREUzTkRZM01qYzROell3SGhjTk1qVXdOVEE0TVRneE1URTJXaGNOTXpVd05UQTJNVGd4TVRFMgpXakFqTVNFd0h3WURWUVFEREJock0zTXRjMlZ5ZG1WeUxXTmhRREUzTkRZM01qYzROell3V1RBVEJnY3Foa2pPClBRSUJCZ2dxaGtqT1BRTUJCd05DQUFUdm8rYjhqbmZmeHpTWlBvdWt0MUdWQStBcE9nRTRsd3pXd0tLVU1LdTMKemdLYUJnTDJrdmkxRnZEZGlMZ0RhcUJENmYzYTVQWWd4QWViZXA2Nk5odmRvMEl3UURBT0JnTlZIUThCQWY4RQpCQU1DQXFRd0R3WURWUjBUQVFIL0JBVXdBd0VCL3pBZEJnTlZIUTRFRmdRVWZSS1ByVlZmZ3VrQmhQQWJZMmEwCkNiYkFnenN3Q2dZSUtvWkl6ajBFQXdJRFNRQXdSZ0loQU5yeFFXaWowektuOTRJeXpjMnRPNTQ5Wnk0YlpSU3kKQllNeVRWT3I1QWREQWlFQWhrWW8zdDFiMFhwLzg4Tkt0cVRCY0V4NGtrZ24za0FBWXEweTRUTXU5QW89Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K
|
|
||||||
server: https://0.0.0.0:40437
|
|
||||||
name: k3d-harmony
|
|
||||||
contexts:
|
|
||||||
- context:
|
|
||||||
cluster: k3d-harmony
|
|
||||||
user: admin@k3d-harmony
|
|
||||||
name: k3d-harmony
|
|
||||||
current-context: k3d-harmony
|
|
||||||
kind: Config
|
|
||||||
preferences: {}
|
|
||||||
users:
|
|
||||||
- name: admin@k3d-harmony
|
|
||||||
user:
|
|
||||||
client-certificate-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJrRENDQVRlZ0F3SUJBZ0lJRVc5bnVqeDdDV2N3Q2dZSUtvWkl6ajBFQXdJd0l6RWhNQjhHQTFVRUF3d1kKYXpOekxXTnNhV1Z1ZEMxallVQXhOelEyTnpJM09EYzJNQjRYRFRJMU1EVXdPREU0TVRFeE5sb1hEVEkyTURVdwpPREU0TVRFeE5sb3dNREVYTUJVR0ExVUVDaE1PYzNsemRHVnRPbTFoYzNSbGNuTXhGVEFUQmdOVkJBTVRESE41CmMzUmxiVHBoWkcxcGJqQlpNQk1HQnlxR1NNNDlBZ0VHQ0NxR1NNNDlBd0VIQTBJQUJEUW5CM2FOZU5CU2FySjUKV1VpRjd1TFMwVmpWT3A4R3FxV1JjMUhNb0s3eVluUlFEWm0veFgwMkZ5Vkh6cjBvNmJtN1lRTkQvVTYwMVo1YwprTVhqOTNLalNEQkdNQTRHQTFVZER3RUIvd1FFQXdJRm9EQVRCZ05WSFNVRUREQUtCZ2dyQmdFRkJRY0RBakFmCkJnTlZIU01FR0RBV2dCUTVJWjFCMkhKQWYxOEMwTFJ0NE5EVkQxdmFOekFLQmdncWhrak9QUVFEQWdOSEFEQkUKQWlCUGMzQ1doRlJSQUFmUDhBU0NtaWMxaFRXQ1FnbjVuUUpNNjBEbm9xWkZVQUlnVXdDWlpmK2p1enlTcGhCSApqNUFpS0psaUJZSklUZ1pETnFWS2VIZ0l3VG89Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0KLS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJkekNDQVIyZ0F3SUJBZ0lCQURBS0JnZ3Foa2pPUFFRREFqQWpNU0V3SHdZRFZRUUREQmhyTTNNdFkyeHAKWlc1MExXTmhRREUzTkRZM01qYzROell3SGhjTk1qVXdOVEE0TVRneE1URTJXaGNOTXpVd05UQTJNVGd4TVRFMgpXakFqTVNFd0h3WURWUVFEREJock0zTXRZMnhwWlc1MExXTmhRREUzTkRZM01qYzROell3V1RBVEJnY3Foa2pPClBRSUJCZ2dxaGtqT1BRTUJCd05DQUFRQVQ0VXkvbm5YVjhmN2xtSTEwTHk1NjNmOStBL0VOeUYyWGVlVnFKNVQKVENCaVNncERIZ09ncE82MEZrMVdhRkRJWmZYcU9RTWI0Q1hjT2wrSVJyWS9vMEl3UURBT0JnTlZIUThCQWY4RQpCQU1DQXFRd0R3WURWUjBUQVFIL0JBVXdBd0VCL3pBZEJnTlZIUTRFRmdRVU9TR2RRZGh5UUg5ZkF0QzBiZURRCjFROWIyamN3Q2dZSUtvWkl6ajBFQXdJRFNBQXdSUUlnZEVYS0RBcjNlT0QzYWI0ZXZHMzgvbHplMEpoTXJIOFoKR25EUTRob2NncnNDSVFEejZFbGZtNWYvL0x1akdUUEFQT3BpR293SFFoMEI4Mk9kbFlMcDN3SEt3QT09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K
|
|
||||||
client-key-data: LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCk1IY0NBUUVFSU1zVUt1MXVBZ0k5VWR1ek9Jc2VvRjRFNGwvMlMzYnJhRlVvTGtuWXpwbGZvQW9HQ0NxR1NNNDkKQXdFSG9VUURRZ0FFTkNjSGRvMTQwRkpxc25sWlNJWHU0dExSV05VNm53YXFwWkZ6VWN5Z3J2SmlkRkFObWIvRgpmVFlYSlVmT3ZTanB1YnRoQTBQOVRyVFZubHlReGVQM2NnPT0KLS0tLS1FTkQgRUMgUFJJVkFURSBLRVktLS0tLQo=
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
FROM php:8.4-apache
|
|
||||||
ENV PHP_MEMORY_LIMIT=256M
|
|
||||||
ENV PHP_MAX_EXECUTION_TIME=30
|
|
||||||
ENV PHP_ERROR_REPORTING="E_ERROR | E_WARNING | E_PARSE"
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends libfreetype6-dev libjpeg62-turbo-dev libpng-dev libzip-dev unzip && apt-get clean && rm -rf /var/lib/apt/lists/*
|
|
||||||
RUN docker-php-ext-configure gd --with-freetype --with-jpeg && docker-php-ext-install -j$(nproc) gd mysqli pdo_mysql zip opcache
|
|
||||||
RUN sed -i 's/VirtualHost \*:80/VirtualHost *:8080/' /etc/apache2/sites-available/000-default.conf && \
|
|
||||||
sed -i 's/^Listen 80$/Listen 8080/' /etc/apache2/ports.conf
|
|
||||||
RUN mkdir -p /usr/local/etc/php/conf.d/
|
|
||||||
COPY docker-php.ini /usr/local/etc/php/conf.d/docker-php.ini
|
|
||||||
RUN a2enmod headers && a2enmod rewrite && sed -i 's/ServerTokens OS/ServerTokens Prod/' /etc/apache2/conf-enabled/security.conf && sed -i 's/ServerSignature On/ServerSignature Off/' /etc/apache2/conf-enabled/security.conf
|
|
||||||
RUN echo 'PassEnv MYSQL_PASSWORD' >> /etc/apache2/sites-available/000-default.conf && echo 'PassEnv MYSQL_USER' >> /etc/apache2/sites-available/000-default.conf && echo 'PassEnv MYSQL_HOST' >> /etc/apache2/sites-available/000-default.conf
|
|
||||||
RUN groupadd -g 1000 appuser && useradd -u 1000 -g appuser -m -s /bin/bash appuser && chown -R appuser:appuser /var/www/html
|
|
||||||
WORKDIR /var/www/html
|
|
||||||
COPY . /var/www/html
|
|
||||||
RUN chown -R appuser:appuser /var/www/html
|
|
||||||
EXPOSE 8080/tcp
|
|
||||||
CMD apache2-foreground
|
|
||||||
@ -1,16 +0,0 @@
|
|||||||
|
|
||||||
memory_limit = ${PHP_MEMORY_LIMIT}
|
|
||||||
max_execution_time = ${PHP_MAX_EXECUTION_TIME}
|
|
||||||
error_reporting = ${PHP_ERROR_REPORTING}
|
|
||||||
display_errors = Off
|
|
||||||
log_errors = On
|
|
||||||
error_log = /dev/stderr
|
|
||||||
date.timezone = UTC
|
|
||||||
|
|
||||||
; Opcache configuration for production
|
|
||||||
opcache.enable=1
|
|
||||||
opcache.memory_consumption=128
|
|
||||||
opcache.interned_strings_buffer=8
|
|
||||||
opcache.max_accelerated_files=4000
|
|
||||||
opcache.revalidate_freq=2
|
|
||||||
opcache.fast_shutdown=1
|
|
||||||
@ -3,7 +3,9 @@ use std::{path::PathBuf, sync::Arc};
|
|||||||
use harmony::{
|
use harmony::{
|
||||||
inventory::Inventory,
|
inventory::Inventory,
|
||||||
maestro::Maestro,
|
maestro::Maestro,
|
||||||
modules::application::{RustWebapp, RustWebappScore, features::ContinuousDelivery},
|
modules::application::{
|
||||||
|
RustWebFramework, RustWebapp, RustWebappScore, features::ContinuousDelivery,
|
||||||
|
},
|
||||||
topology::{K8sAnywhereTopology, Url},
|
topology::{K8sAnywhereTopology, Url},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -13,6 +15,7 @@ async fn main() {
|
|||||||
let application = RustWebapp {
|
let application = RustWebapp {
|
||||||
name: "harmony-example-rust-webapp".to_string(),
|
name: "harmony-example-rust-webapp".to_string(),
|
||||||
project_root: PathBuf::from("./examples/rust/webapp"),
|
project_root: PathBuf::from("./examples/rust/webapp"),
|
||||||
|
framework: Some(RustWebFramework::Leptos),
|
||||||
};
|
};
|
||||||
let app = RustWebappScore {
|
let app = RustWebappScore {
|
||||||
name: "Example Rust Webapp".to_string(),
|
name: "Example Rust Webapp".to_string(),
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "webapp"
|
name = "harmony-example-rust-webapp"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
@ -41,7 +41,7 @@ panic = "abort"
|
|||||||
|
|
||||||
[package.metadata.leptos]
|
[package.metadata.leptos]
|
||||||
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
|
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
|
||||||
output-name = "webapp"
|
output-name = "harmony-example-rust-webapp"
|
||||||
# The site root folder is where cargo-leptos generate all output. WARNING: all content of this folder will be erased on a rebuild. Use it in your server setup.
|
# The site root folder is where cargo-leptos generate all output. WARNING: all content of this folder will be erased on a rebuild. Use it in your server setup.
|
||||||
site-root = "target/site"
|
site-root = "target/site"
|
||||||
# The site-root relative folder where all compiled output (JS, WASM and CSS) is written
|
# The site-root relative folder where all compiled output (JS, WASM and CSS) is written
|
||||||
@ -55,7 +55,7 @@ style-file = "style/main.scss"
|
|||||||
# Optional. Env: LEPTOS_ASSETS_DIR.
|
# Optional. Env: LEPTOS_ASSETS_DIR.
|
||||||
assets-dir = "assets"
|
assets-dir = "assets"
|
||||||
# The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup.
|
# The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup.
|
||||||
site-addr = "127.0.0.1:3000"
|
site-addr = "0.0.0.0:3000"
|
||||||
# The port to use for automatic reload monitoring
|
# The port to use for automatic reload monitoring
|
||||||
reload-port = 3001
|
reload-port = 3001
|
||||||
# [Optional] Command to use when running end2end tests. It will run in the end2end dir.
|
# [Optional] Command to use when running end2end tests. It will run in the end2end dir.
|
||||||
|
|||||||
@ -1,10 +1,16 @@
|
|||||||
FROM rust:latest as builder
|
FROM rust:bookworm as builder
|
||||||
|
RUN 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/*
|
||||||
|
RUN cargo binstall cargo-leptos -y
|
||||||
RUN rustup target add wasm32-unknown-unknown
|
RUN rustup target add wasm32-unknown-unknown
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN cargo build --release --locked
|
RUN cargo leptos build --release -vv
|
||||||
FROM debian:bullseye-slim
|
FROM debian:bookworm-slim
|
||||||
RUN groupadd -r appgroup && useradd -r -s /bin/false -g appgroup appuser
|
RUN groupadd -r appgroup && useradd -r -s /bin/false -g appgroup appuser
|
||||||
COPY --from=builder /app/target/release/harmony-example-rust-webapp /usr/local/bin/harmony-example-rust-webapp
|
ENV LEPTOS_SITE_ADDR=0.0.0.0:3000
|
||||||
|
EXPOSE 3000/tcp
|
||||||
|
WORKDIR /home/appuser
|
||||||
|
COPY --from=builder /app/target/site/pkg /home/appuser/pkg
|
||||||
|
COPY --from=builder /app/target/release/harmony-example-rust-webapp /home/appuser/harmony-example-rust-webapp
|
||||||
USER appuser
|
USER appuser
|
||||||
CMD /usr/local/bin/harmony-example-rust-webapp
|
CMD /home/appuser/harmony-example-rust-webapp
|
||||||
@ -13,7 +13,7 @@ pub fn App() -> impl IntoView {
|
|||||||
view! {
|
view! {
|
||||||
// injects a stylesheet into the document <head>
|
// injects a stylesheet into the document <head>
|
||||||
// id=leptos means cargo-leptos will hot-reload this stylesheet
|
// id=leptos means cargo-leptos will hot-reload this stylesheet
|
||||||
<Stylesheet id="leptos" href="/pkg/webapp.css"/>
|
<Stylesheet id="leptos" href="/pkg/harmony-example-rust-webapp.css"/>
|
||||||
|
|
||||||
// sets the document title
|
// sets the document title
|
||||||
<Title text="Welcome to Leptos"/>
|
<Title text="Welcome to Leptos"/>
|
||||||
|
|||||||
@ -7,7 +7,7 @@ async fn main() -> std::io::Result<()> {
|
|||||||
use leptos::config::get_configuration;
|
use leptos::config::get_configuration;
|
||||||
use leptos_meta::MetaTags;
|
use leptos_meta::MetaTags;
|
||||||
use leptos_actix::{generate_route_list, LeptosRoutes};
|
use leptos_actix::{generate_route_list, LeptosRoutes};
|
||||||
use webapp::app::*;
|
use harmony_example_rust_webapp::app::*;
|
||||||
|
|
||||||
let conf = get_configuration(None).unwrap();
|
let conf = get_configuration(None).unwrap();
|
||||||
let addr = conf.leptos_options.site_addr;
|
let addr = conf.leptos_options.site_addr;
|
||||||
@ -80,7 +80,7 @@ pub fn main() {
|
|||||||
// a client-side main function is required for using `trunk serve`
|
// a client-side main function is required for using `trunk serve`
|
||||||
// prefer using `cargo leptos serve` instead
|
// prefer using `cargo leptos serve` instead
|
||||||
// to run: `trunk serve --open --features csr`
|
// to run: `trunk serve --open --features csr`
|
||||||
use webapp::app::*;
|
use harmony_example_rust_webapp::app::*;
|
||||||
|
|
||||||
console_error_panic_hook::set_once();
|
console_error_panic_hook::set_once();
|
||||||
|
|
||||||
|
|||||||
@ -1,14 +1,14 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use log::info;
|
use log::{error, info};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
data::Version,
|
data::Version,
|
||||||
inventory::Inventory,
|
inventory::Inventory,
|
||||||
modules::{
|
modules::{
|
||||||
application::{Application, ApplicationFeature, OCICompliant},
|
application::{Application, ApplicationFeature, HelmPackage, OCICompliant},
|
||||||
helm::chart::HelmChartScore,
|
helm::chart::HelmChartScore,
|
||||||
},
|
},
|
||||||
score::Score,
|
score::Score,
|
||||||
@ -43,16 +43,27 @@ use crate::{
|
|||||||
/// - ArgoCD to install/upgrade/rollback/inspect k8s resources
|
/// - ArgoCD to install/upgrade/rollback/inspect k8s resources
|
||||||
/// - Kubernetes for runtime orchestration
|
/// - Kubernetes for runtime orchestration
|
||||||
#[derive(Debug, Default, Clone)]
|
#[derive(Debug, Default, Clone)]
|
||||||
pub struct ContinuousDelivery<A: OCICompliant> {
|
pub struct ContinuousDelivery<A: OCICompliant + HelmPackage> {
|
||||||
pub application: Arc<A>,
|
pub application: Arc<A>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl<A: OCICompliant + Clone + 'static, T: Topology + HelmCommand + 'static> ApplicationFeature<T>
|
impl<A: OCICompliant + HelmPackage + Clone + 'static, T: Topology + HelmCommand + 'static>
|
||||||
for ContinuousDelivery<A>
|
ApplicationFeature<T> for ContinuousDelivery<A>
|
||||||
{
|
{
|
||||||
async fn ensure_installed(&self, topology: &T) -> Result<(), String> {
|
async fn ensure_installed(&self, topology: &T) -> Result<(), String> {
|
||||||
|
let image = self.application.image_name();
|
||||||
|
|
||||||
|
// TODO
|
||||||
|
error!(
|
||||||
|
"TODO reverse helm chart packaging and docker image build. I put helm package first for faster iterations"
|
||||||
|
);
|
||||||
|
|
||||||
|
let helm_chart = self.application.build_push_helm_package(&image).await?;
|
||||||
|
info!("Pushed new helm chart {helm_chart}");
|
||||||
|
|
||||||
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");
|
info!("Installing ContinuousDelivery feature");
|
||||||
let cd_server = HelmChartScore {
|
let cd_server = HelmChartScore {
|
||||||
|
|||||||
@ -5,4 +5,17 @@ use super::Application;
|
|||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait OCICompliant: Application {
|
pub trait OCICompliant: Application {
|
||||||
async fn build_push_oci_image(&self) -> Result<String, String>; // TODO consider using oci-spec and friends crates here
|
async fn build_push_oci_image(&self) -> Result<String, String>; // TODO consider using oci-spec and friends crates here
|
||||||
|
|
||||||
|
fn image_name(&self) -> String;
|
||||||
|
|
||||||
|
fn local_image_name(&self) -> String;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait HelmPackage: Application {
|
||||||
|
/// Generates, packages, and pushes a Helm chart for the web application to an OCI registry.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `image_url` - The full URL of the OCI container image to be used in the Deployment.
|
||||||
|
async fn build_push_helm_package(&self, image_url: &str) -> Result<String, String>;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,9 +5,9 @@ use std::sync::Arc;
|
|||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use dockerfile_builder::Dockerfile;
|
use dockerfile_builder::Dockerfile;
|
||||||
use dockerfile_builder::instruction::{CMD, COPY, FROM, RUN, USER, WORKDIR};
|
use dockerfile_builder::instruction::{CMD, COPY, ENV, EXPOSE, FROM, RUN, USER, WORKDIR};
|
||||||
use dockerfile_builder::instruction_builder::CopyBuilder;
|
use dockerfile_builder::instruction_builder::CopyBuilder;
|
||||||
use log::{debug, info};
|
use log::{debug, error, info};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
use crate::config::{REGISTRY_PROJECT, REGISTRY_URL};
|
use crate::config::{REGISTRY_PROJECT, REGISTRY_URL};
|
||||||
@ -16,7 +16,7 @@ use crate::{
|
|||||||
topology::{Topology, Url},
|
topology::{Topology, Url},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{Application, ApplicationFeature, ApplicationInterpret, OCICompliant};
|
use super::{Application, ApplicationFeature, ApplicationInterpret, HelmPackage, OCICompliant};
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Clone)]
|
#[derive(Debug, Serialize, Clone)]
|
||||||
pub struct RustWebappScore<T: Topology + Clone + Serialize> {
|
pub struct RustWebappScore<T: Topology + Clone + Serialize> {
|
||||||
@ -58,6 +58,36 @@ impl Application for RustWebapp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl HelmPackage for RustWebapp {
|
||||||
|
async fn build_push_helm_package(&self, image_url: &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)
|
||||||
|
|
|||||||
|
.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]
|
#[async_trait]
|
||||||
impl OCICompliant for RustWebapp {
|
impl OCICompliant for RustWebapp {
|
||||||
/// Builds a Docker image for the Rust web application using a multi-stage build,
|
/// Builds a Docker image for the Rust web application using a multi-stage build,
|
||||||
@ -68,22 +98,35 @@ impl OCICompliant for RustWebapp {
|
|||||||
info!("Starting OCI image build and push for '{}'", self.name);
|
info!("Starting OCI image build and push for '{}'", self.name);
|
||||||
|
|
||||||
// 1. Build the local image by calling the synchronous helper function.
|
// 1. Build the local image by calling the synchronous helper function.
|
||||||
let local_image_name = self
|
let local_image_name = self.local_image_name();
|
||||||
.build_docker_image()
|
self.build_docker_image(&local_image_name)
|
||||||
.map_err(|e| format!("Failed to build Docker image: {}", e))?;
|
.map_err(|e| format!("Failed to build Docker image: {}", e))?;
|
||||||
info!(
|
info!(
|
||||||
"Successfully built local Docker image: {}",
|
"Successfully built local Docker image: {}",
|
||||||
local_image_name
|
local_image_name
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let remote_image_name = self.image_name();
|
||||||
// 2. Push the image to the registry.
|
// 2. Push the image to the registry.
|
||||||
let remote_image_name = self
|
self.push_docker_image(&local_image_name, &remote_image_name)
|
||||||
.push_docker_image(&local_image_name)
|
|
||||||
.map_err(|e| format!("Failed to push Docker image: {}", e))?;
|
.map_err(|e| format!("Failed to push Docker image: {}", e))?;
|
||||||
info!("Successfully pushed Docker image to: {}", remote_image_name);
|
info!("Successfully pushed Docker image to: {}", remote_image_name);
|
||||||
|
|
||||||
Ok(remote_image_name)
|
Ok(remote_image_name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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.
|
/// Implementation of helper methods for building and pushing the Docker image.
|
||||||
@ -94,33 +137,6 @@ impl RustWebapp {
|
|||||||
|
|
||||||
self.build_builder_image(&mut dockerfile);
|
self.build_builder_image(&mut dockerfile);
|
||||||
|
|
||||||
// --- Stage 2: Final Image ---
|
|
||||||
// Use a minimal, non-Alpine base image for the final container.
|
|
||||||
dockerfile.push(FROM::from("debian:bullseye-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.
|
|
||||||
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));
|
|
||||||
|
|
||||||
// Save the Dockerfile to a uniquely named file in the project root to avoid conflicts.
|
// Save the Dockerfile to a uniquely named file in the project root to avoid conflicts.
|
||||||
let dockerfile_path = self.project_root.join("Dockerfile.harmony");
|
let dockerfile_path = self.project_root.join("Dockerfile.harmony");
|
||||||
fs::write(&dockerfile_path, dockerfile.to_string())?;
|
fs::write(&dockerfile_path, dockerfile.to_string())?;
|
||||||
@ -129,7 +145,10 @@ impl RustWebapp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Builds the Docker image using the generated Dockerfile.
|
/// Builds the Docker image using the generated Dockerfile.
|
||||||
pub fn build_docker_image(&self) -> Result<String, Box<dyn std::error::Error>> {
|
pub fn build_docker_image(
|
||||||
|
&self,
|
||||||
|
image_name: &str,
|
||||||
|
) -> Result<String, Box<dyn std::error::Error>> {
|
||||||
info!("Generating Dockerfile for '{}'", self.name);
|
info!("Generating Dockerfile for '{}'", self.name);
|
||||||
let dockerfile_path = self.build_dockerfile()?;
|
let dockerfile_path = self.build_dockerfile()?;
|
||||||
|
|
||||||
@ -138,8 +157,6 @@ impl RustWebapp {
|
|||||||
dockerfile_path.to_string_lossy(),
|
dockerfile_path.to_string_lossy(),
|
||||||
self.project_root.to_string_lossy()
|
self.project_root.to_string_lossy()
|
||||||
);
|
);
|
||||||
let image_name = format!("{}-webapp", self.name);
|
|
||||||
|
|
||||||
let output = process::Command::new("docker")
|
let output = process::Command::new("docker")
|
||||||
.args([
|
.args([
|
||||||
"build",
|
"build",
|
||||||
@ -154,30 +171,34 @@ impl RustWebapp {
|
|||||||
|
|
||||||
self.check_output(&output, "Failed to build Docker image")?;
|
self.check_output(&output, "Failed to build Docker image")?;
|
||||||
|
|
||||||
Ok(image_name)
|
Ok(image_name.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Tags and pushes a Docker image to the configured remote registry.
|
/// Tags and pushes a Docker image to the configured remote registry.
|
||||||
fn push_docker_image(&self, image_name: &str) -> Result<String, Box<dyn std::error::Error>> {
|
fn push_docker_image(
|
||||||
let full_tag = format!("{}/{}/{}", *REGISTRY_URL, *REGISTRY_PROJECT, &image_name);
|
&self,
|
||||||
|
image_name: &str,
|
||||||
|
full_tag: &str,
|
||||||
|
) -> Result<String, Box<dyn std::error::Error>> {
|
||||||
info!("Pushing docker image {full_tag}");
|
info!("Pushing docker image {full_tag}");
|
||||||
|
|
||||||
// Tag the image for the remote registry.
|
// Tag the image for the remote registry.
|
||||||
let output = process::Command::new("docker")
|
let output = process::Command::new("docker")
|
||||||
.args(["tag", image_name, &full_tag])
|
.args(["tag", image_name, &full_tag])
|
||||||
.output()?;
|
.spawn()?
|
||||||
|
.wait_with_output()?;
|
||||||
self.check_output(&output, "Tagging docker image failed")?;
|
self.check_output(&output, "Tagging docker image failed")?;
|
||||||
debug!(
|
debug!(
|
||||||
"docker tag output: stdout: {}, stderr: {}",
|
"docker tag output: stdout: {}, stderr: {}",
|
||||||
String::from_utf8_lossy(&output.stdout),
|
String::from_utf8_lossy(&output.stdout),
|
||||||
String::from_utf8_lossy(&output.stderr)
|
String::from_utf8_lossy(&output.stderr)
|
||||||
);
|
);
|
||||||
todo!("Are we good?");
|
|
||||||
|
|
||||||
// Push the image.
|
// Push the image.
|
||||||
let output = process::Command::new("docker")
|
let output = process::Command::new("docker")
|
||||||
.args(["push", &full_tag])
|
.args(["push", &full_tag])
|
||||||
.output()?;
|
.spawn()?
|
||||||
|
.wait_with_output()?;
|
||||||
self.check_output(&output, "Pushing docker image failed")?;
|
self.check_output(&output, "Pushing docker image failed")?;
|
||||||
debug!(
|
debug!(
|
||||||
"docker push output: stdout: {}, stderr: {}",
|
"docker push output: stdout: {}, stderr: {}",
|
||||||
@ -185,7 +206,7 @@ impl RustWebapp {
|
|||||||
String::from_utf8_lossy(&output.stderr)
|
String::from_utf8_lossy(&output.stderr)
|
||||||
);
|
);
|
||||||
|
|
||||||
Ok(full_tag)
|
Ok(full_tag.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Checks the output of a process command for success.
|
/// Checks the output of a process command for success.
|
||||||
@ -203,40 +224,76 @@ impl RustWebapp {
|
|||||||
|
|
||||||
fn build_builder_image(&self, dockerfile: &mut Dockerfile) {
|
fn build_builder_image(&self, dockerfile: &mut Dockerfile) {
|
||||||
match self.framework {
|
match self.framework {
|
||||||
Some(RustWebFramework::Leptos) => {todo!(r#"
|
Some(RustWebFramework::Leptos) => {
|
||||||
# Get started with a build env with Rust nightly
|
// --- Stage 1: Builder for Leptos ---
|
||||||
FROM rustlang/rust:nightly-bookworm as builder
|
dockerfile.push(FROM::from("rust:bookworm as builder"));
|
||||||
|
|
||||||
# If you’re using stable, use this instead
|
// Install dependencies, cargo-binstall, and clean up in one layer
|
||||||
# FROM rust:1.86-bullseye as builder
|
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-binstall, which makes it easier to install other
|
// Install cargo-leptos
|
||||||
# cargo extensions like cargo-leptos
|
dockerfile.push(RUN::from("cargo binstall cargo-leptos -y"));
|
||||||
RUN wget https://github.com/cargo-bins/cargo-binstall/releases/latest/download/cargo-binstall-x86_64-unknown-linux-musl.tgz
|
|
||||||
RUN tar -xvf cargo-binstall-x86_64-unknown-linux-musl.tgz
|
|
||||||
RUN cp cargo-binstall /usr/local/cargo/bin
|
|
||||||
|
|
||||||
# Install required tools
|
// Add the WASM target
|
||||||
RUN apt-get update -y \
|
dockerfile.push(RUN::from("rustup target add wasm32-unknown-unknown"));
|
||||||
&& apt-get install -y --no-install-recommends clang
|
|
||||||
|
|
||||||
# Install cargo-leptos
|
// Set up workdir, copy source, and build
|
||||||
RUN cargo binstall cargo-leptos -y
|
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"));
|
||||||
|
|
||||||
# Add the WASM target
|
// Create a non-root user for security.
|
||||||
RUN rustup target add wasm32-unknown-unknown
|
dockerfile.push(RUN::from(
|
||||||
|
"groupadd -r appgroup && useradd -r -s /bin/false -g appgroup appuser",
|
||||||
|
));
|
||||||
|
|
||||||
# Make an /app dir, which everything will eventually live in
|
dockerfile.push(ENV::from("LEPTOS_SITE_ADDR=0.0.0.0:3000"));
|
||||||
RUN mkdir -p /app
|
dockerfile.push(EXPOSE::from("3000/tcp"));
|
||||||
WORKDIR /app
|
dockerfile.push(WORKDIR::from("/home/appuser"));
|
||||||
COPY . .
|
|
||||||
|
|
||||||
# Build the app
|
// Copy static files
|
||||||
RUN cargo leptos build --release -vv
|
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.
|
||||||
|
error!(
|
||||||
|
"FIXME Should not be using score name here, instead should use name from Cargo.toml"
|
||||||
|
);
|
||||||
|
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 => {
|
None => {
|
||||||
// --- Stage 1: Builder ---
|
// --- Stage 1: Builder for a generic Rust app ---
|
||||||
// Use the official Rust image as the build environment.
|
|
||||||
dockerfile.push(FROM::from("rust:latest as builder"));
|
dockerfile.push(FROM::from("rust:latest as builder"));
|
||||||
|
|
||||||
// Install the wasm32 target as required.
|
// Install the wasm32 target as required.
|
||||||
@ -246,7 +303,271 @@ RUN cargo leptos build --release -vv
|
|||||||
// Copy the source code and build the application.
|
// Copy the source code and build the application.
|
||||||
dockerfile.push(COPY::from(". ."));
|
dockerfile.push(COPY::from(". ."));
|
||||||
dockerfile.push(RUN::from("cargo build --release --locked"));
|
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.
|
||||||
|
error!(
|
||||||
|
"FIXME Should not be using score name here, instead should use name from Cargo.toml"
|
||||||
|
);
|
||||||
|
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.
|
||||||
|
fn create_helm_chart_files(
|
||||||
|
&self,
|
||||||
|
image_url: &str,
|
||||||
|
) -> Result<PathBuf, Box<dyn std::error::Error>> {
|
||||||
|
let chart_name = format!("{}-chart", self.name);
|
||||||
|
let chart_dir = self.project_root.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: 80
|
||||||
|
|
||||||
|
ingress:
|
||||||
|
enabled: false
|
||||||
|
# Annotations for cert-manager to handle SSL.
|
||||||
|
annotations:
|
||||||
|
cert-manager.io/cluster-issuer: "letsencrypt-prod"
|
||||||
|
# Add other annotations like nginx ingress class if needed
|
||||||
|
# kubernetes.io/ingress.class: nginx
|
||||||
|
hosts:
|
||||||
|
- host: chart-example.local
|
||||||
|
paths:
|
||||||
|
- path: /
|
||||||
|
pathType: ImplementationSpecific
|
||||||
|
tls:
|
||||||
|
- secretName: {}-tls
|
||||||
|
hosts:
|
||||||
|
- chart-example.local
|
||||||
|
|
||||||
|
"#,
|
||||||
|
chart_name, image_repo, image_tag, self.name
|
||||||
|
);
|
||||||
|
fs::write(chart_dir.join("values.yaml"), values_yaml)?;
|
||||||
|
|
||||||
|
// Create templates/_helpers.tpl
|
||||||
|
let helpers_tpl = 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 = r#"
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: {{ include "chart.fullname" . }}
|
||||||
|
spec:
|
||||||
|
type: {{ .Values.service.type }}
|
||||||
|
ports:
|
||||||
|
- port: {{ .Values.service.port }}
|
||||||
|
targetPort: http
|
||||||
|
protocol: TCP
|
||||||
|
name: http
|
||||||
|
selector:
|
||||||
|
app: {{ include "chart.name" . }}
|
||||||
|
"#;
|
||||||
|
fs::write(templates_dir.join("service.yaml"), service_yaml)?;
|
||||||
|
|
||||||
|
// Create templates/deployment.yaml
|
||||||
|
let deployment_yaml = 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: http
|
||||||
|
containerPort: 8080 # Assuming the rust app listens on 8080
|
||||||
|
protocol: TCP
|
||||||
|
"#;
|
||||||
|
fs::write(templates_dir.join("deployment.yaml"), deployment_yaml)?;
|
||||||
|
|
||||||
|
// Create templates/ingress.yaml
|
||||||
|
let ingress_yaml = 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:
|
||||||
|
name: http
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
"#;
|
||||||
|
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: &PathBuf,
|
||||||
|
) -> Result<PathBuf, Box<dyn std::error::Error>> {
|
||||||
|
let chart_dirname = chart_dir.file_name().expect("Should find a chart dirname");
|
||||||
|
info!(
|
||||||
|
"Launching `helm package {}` cli with CWD {}",
|
||||||
|
chart_dirname.to_string_lossy(),
|
||||||
|
&self.project_root.join("helm").to_string_lossy()
|
||||||
|
);
|
||||||
|
let output = process::Command::new("helm")
|
||||||
|
.args(["package", chart_dirname.to_str().unwrap()])
|
||||||
|
.current_dir(&self.project_root.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)?
|
||||||
|
.trim()
|
||||||
|
.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("helm").join(tgz_name))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pushes a packaged Helm chart to an OCI registry.
|
||||||
|
fn push_helm_chart(
|
||||||
|
&self,
|
||||||
|
packaged_chart_path: &PathBuf,
|
||||||
|
) -> 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_url = format!(
|
||||||
|
"oci://{}/{}/{}-chart",
|
||||||
|
*REGISTRY_URL, *REGISTRY_PROJECT, self.name
|
||||||
|
);
|
||||||
|
|
||||||
|
info!(
|
||||||
|
"Pushing Helm chart {} to {}",
|
||||||
|
packaged_chart_path.to_string_lossy(),
|
||||||
|
oci_url
|
||||||
|
);
|
||||||
|
|
||||||
|
let output = process::Command::new("helm")
|
||||||
|
.args(["push", packaged_chart_path.to_str().unwrap(), &oci_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;
|
||||||
|
Ok(format!("{}:{}", oci_url, version))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user
For OCI, we get the (local) image name from the trait directly (
self.image_name()&self.local_image_name()). Why is it different here?Also, could (should?) it be generated automatically from the application name instead? It doesn't seem like we actually need it from outside.
This is actually the container image url. The chart uses it to reference the image correctly.
But yes, this is a common pain with image urls, helm chart names, application names, etc. Perhaps we could easily create an ApplicationIdentifier or ApplicationLocator concept that is used instead of a String that provides a robust way to get container image location, helm chart location, app name, etc, etc.