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
|
||||
private_repos
|
||||
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::{
|
||||
inventory::Inventory,
|
||||
maestro::Maestro,
|
||||
modules::application::{RustWebapp, RustWebappScore, features::ContinuousDelivery},
|
||||
modules::application::{
|
||||
RustWebFramework, RustWebapp, RustWebappScore, features::ContinuousDelivery,
|
||||
},
|
||||
topology::{K8sAnywhereTopology, Url},
|
||||
};
|
||||
|
||||
@ -13,6 +15,7 @@ async fn main() {
|
||||
let application = RustWebapp {
|
||||
name: "harmony-example-rust-webapp".to_string(),
|
||||
project_root: PathBuf::from("./examples/rust/webapp"),
|
||||
framework: Some(RustWebFramework::Leptos),
|
||||
};
|
||||
let app = RustWebappScore {
|
||||
name: "Example Rust Webapp".to_string(),
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
[package]
|
||||
name = "webapp"
|
||||
name = "harmony-example-rust-webapp"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
@ -41,7 +41,7 @@ panic = "abort"
|
||||
|
||||
[package.metadata.leptos]
|
||||
# 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.
|
||||
site-root = "target/site"
|
||||
# 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.
|
||||
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.
|
||||
site-addr = "127.0.0.1:3000"
|
||||
site-addr = "0.0.0.0:3000"
|
||||
# The port to use for automatic reload monitoring
|
||||
reload-port = 3001
|
||||
# [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
|
||||
WORKDIR /app
|
||||
COPY . .
|
||||
RUN cargo build --release --locked
|
||||
FROM debian:bullseye-slim
|
||||
RUN cargo leptos build --release -vv
|
||||
FROM debian:bookworm-slim
|
||||
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
|
||||
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! {
|
||||
// injects a stylesheet into the document <head>
|
||||
// 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
|
||||
<Title text="Welcome to Leptos"/>
|
||||
|
||||
@ -7,7 +7,7 @@ async fn main() -> std::io::Result<()> {
|
||||
use leptos::config::get_configuration;
|
||||
use leptos_meta::MetaTags;
|
||||
use leptos_actix::{generate_route_list, LeptosRoutes};
|
||||
use webapp::app::*;
|
||||
use harmony_example_rust_webapp::app::*;
|
||||
|
||||
let conf = get_configuration(None).unwrap();
|
||||
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`
|
||||
// prefer using `cargo leptos serve` instead
|
||||
// to run: `trunk serve --open --features csr`
|
||||
use webapp::app::*;
|
||||
use harmony_example_rust_webapp::app::*;
|
||||
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
|
||||
@ -1,14 +1,14 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use log::info;
|
||||
use log::{error, info};
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::{
|
||||
data::Version,
|
||||
inventory::Inventory,
|
||||
modules::{
|
||||
application::{Application, ApplicationFeature, OCICompliant},
|
||||
application::{Application, ApplicationFeature, HelmPackage, OCICompliant},
|
||||
helm::chart::HelmChartScore,
|
||||
},
|
||||
score::Score,
|
||||
@ -43,16 +43,27 @@ use crate::{
|
||||
/// - ArgoCD to install/upgrade/rollback/inspect k8s resources
|
||||
/// - Kubernetes for runtime orchestration
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct ContinuousDelivery<A: OCICompliant> {
|
||||
pub struct ContinuousDelivery<A: OCICompliant + HelmPackage> {
|
||||
pub application: Arc<A>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<A: OCICompliant + Clone + 'static, T: Topology + HelmCommand + 'static> ApplicationFeature<T>
|
||||
for ContinuousDelivery<A>
|
||||
impl<A: OCICompliant + HelmPackage + Clone + 'static, T: Topology + HelmCommand + 'static>
|
||||
ApplicationFeature<T> for ContinuousDelivery<A>
|
||||
{
|
||||
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?;
|
||||
info!("Pushed new docker image {image}");
|
||||
|
||||
info!("Installing ContinuousDelivery feature");
|
||||
let cd_server = HelmChartScore {
|
||||
|
||||
@ -5,4 +5,17 @@ use super::Application;
|
||||
#[async_trait]
|
||||
pub trait OCICompliant: Application {
|
||||
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 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 log::{debug, info};
|
||||
use log::{debug, error, info};
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::config::{REGISTRY_PROJECT, REGISTRY_URL};
|
||||
@ -16,7 +16,7 @@ use crate::{
|
||||
topology::{Topology, Url},
|
||||
};
|
||||
|
||||
use super::{Application, ApplicationFeature, ApplicationInterpret, OCICompliant};
|
||||
use super::{Application, ApplicationFeature, ApplicationInterpret, HelmPackage, OCICompliant};
|
||||
|
||||
#[derive(Debug, Serialize, Clone)]
|
||||
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]
|
||||
impl OCICompliant for RustWebapp {
|
||||
/// 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);
|
||||
|
||||
// 1. Build the local image by calling the synchronous helper function.
|
||||
let local_image_name = self
|
||||
.build_docker_image()
|
||||
let local_image_name = self.local_image_name();
|
||||
self.build_docker_image(&local_image_name)
|
||||
.map_err(|e| format!("Failed to build Docker image: {}", e))?;
|
||||
info!(
|
||||
"Successfully built local Docker image: {}",
|
||||
local_image_name
|
||||
);
|
||||
|
||||
let remote_image_name = self.image_name();
|
||||
// 2. Push the image to the registry.
|
||||
let remote_image_name = self
|
||||
.push_docker_image(&local_image_name)
|
||||
self.push_docker_image(&local_image_name, &remote_image_name)
|
||||
.map_err(|e| format!("Failed to push Docker image: {}", e))?;
|
||||
info!("Successfully pushed Docker image to: {}", 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.
|
||||
@ -94,33 +137,6 @@ impl RustWebapp {
|
||||
|
||||
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.
|
||||
let dockerfile_path = self.project_root.join("Dockerfile.harmony");
|
||||
fs::write(&dockerfile_path, dockerfile.to_string())?;
|
||||
@ -129,7 +145,10 @@ impl RustWebapp {
|
||||
}
|
||||
|
||||
/// 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);
|
||||
let dockerfile_path = self.build_dockerfile()?;
|
||||
|
||||
@ -138,8 +157,6 @@ impl RustWebapp {
|
||||
dockerfile_path.to_string_lossy(),
|
||||
self.project_root.to_string_lossy()
|
||||
);
|
||||
let image_name = format!("{}-webapp", self.name);
|
||||
|
||||
let output = process::Command::new("docker")
|
||||
.args([
|
||||
"build",
|
||||
@ -154,30 +171,34 @@ impl RustWebapp {
|
||||
|
||||
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.
|
||||
fn push_docker_image(&self, image_name: &str) -> Result<String, Box<dyn std::error::Error>> {
|
||||
let full_tag = format!("{}/{}/{}", *REGISTRY_URL, *REGISTRY_PROJECT, &image_name);
|
||||
fn push_docker_image(
|
||||
&self,
|
||||
image_name: &str,
|
||||
full_tag: &str,
|
||||
) -> Result<String, Box<dyn std::error::Error>> {
|
||||
info!("Pushing docker image {full_tag}");
|
||||
|
||||
// Tag the image for the remote registry.
|
||||
let output = process::Command::new("docker")
|
||||
.args(["tag", image_name, &full_tag])
|
||||
.output()?;
|
||||
.spawn()?
|
||||
.wait_with_output()?;
|
||||
self.check_output(&output, "Tagging docker image failed")?;
|
||||
debug!(
|
||||
"docker tag output: stdout: {}, stderr: {}",
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
todo!("Are we good?");
|
||||
|
||||
// Push the image.
|
||||
let output = process::Command::new("docker")
|
||||
.args(["push", &full_tag])
|
||||
.output()?;
|
||||
.spawn()?
|
||||
.wait_with_output()?;
|
||||
self.check_output(&output, "Pushing docker image failed")?;
|
||||
debug!(
|
||||
"docker push output: stdout: {}, stderr: {}",
|
||||
@ -185,7 +206,7 @@ impl RustWebapp {
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
|
||||
Ok(full_tag)
|
||||
Ok(full_tag.to_string())
|
||||
}
|
||||
|
||||
/// Checks the output of a process command for success.
|
||||
@ -203,40 +224,76 @@ impl RustWebapp {
|
||||
|
||||
fn build_builder_image(&self, dockerfile: &mut Dockerfile) {
|
||||
match self.framework {
|
||||
Some(RustWebFramework::Leptos) => {todo!(r#"
|
||||
# Get started with a build env with Rust nightly
|
||||
FROM rustlang/rust:nightly-bookworm as builder
|
||||
Some(RustWebFramework::Leptos) => {
|
||||
// --- Stage 1: Builder for Leptos ---
|
||||
dockerfile.push(FROM::from("rust:bookworm as builder"));
|
||||
|
||||
# If you’re using stable, use this instead
|
||||
# FROM rust:1.86-bullseye 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-binstall, which makes it easier to install other
|
||||
# cargo extensions like cargo-leptos
|
||||
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 cargo-leptos
|
||||
dockerfile.push(RUN::from("cargo binstall cargo-leptos -y"));
|
||||
|
||||
# Install required tools
|
||||
RUN apt-get update -y \
|
||||
&& apt-get install -y --no-install-recommends clang
|
||||
// Add the WASM target
|
||||
dockerfile.push(RUN::from("rustup target add wasm32-unknown-unknown"));
|
||||
|
||||
# Install cargo-leptos
|
||||
RUN cargo binstall cargo-leptos -y
|
||||
// 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"));
|
||||
|
||||
# Add the WASM target
|
||||
RUN rustup target add wasm32-unknown-unknown
|
||||
// Create a non-root user for security.
|
||||
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
|
||||
RUN mkdir -p /app
|
||||
WORKDIR /app
|
||||
COPY . .
|
||||
dockerfile.push(ENV::from("LEPTOS_SITE_ADDR=0.0.0.0:3000"));
|
||||
dockerfile.push(EXPOSE::from("3000/tcp"));
|
||||
dockerfile.push(WORKDIR::from("/home/appuser"));
|
||||
|
||||
# Build the app
|
||||
RUN cargo leptos build --release -vv
|
||||
"#)}
|
||||
// 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.
|
||||
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 => {
|
||||
// --- Stage 1: Builder ---
|
||||
// Use the official Rust image as the build environment.
|
||||
// --- Stage 1: Builder for a generic Rust app ---
|
||||
dockerfile.push(FROM::from("rust:latest as builder"));
|
||||
|
||||
// Install the wasm32 target as required.
|
||||
@ -246,7 +303,271 @@ RUN cargo leptos build --release -vv
|
||||
// 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.
|
||||
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.