feat: Add sample leptos webapp as example
This commit is contained in:
@@ -1,8 +1,16 @@
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use std::process;
|
||||
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_builder::CopyBuilder;
|
||||
use log::{debug, info};
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::config::{REGISTRY_PROJECT, REGISTRY_URL};
|
||||
use crate::{
|
||||
score::Score,
|
||||
topology::{Topology, Url},
|
||||
@@ -31,9 +39,17 @@ impl<T: Topology + std::fmt::Debug + Clone + Serialize + 'static> Score<T> for R
|
||||
}
|
||||
}
|
||||
|
||||
#[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 framework: Option<RustWebFramework>,
|
||||
}
|
||||
|
||||
impl Application for RustWebapp {
|
||||
@@ -44,7 +60,193 @@ impl Application for RustWebapp {
|
||||
|
||||
#[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> {
|
||||
todo!()
|
||||
// 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 local image by calling the synchronous helper function.
|
||||
let local_image_name = self
|
||||
.build_docker_image()
|
||||
.map_err(|e| format!("Failed to build Docker image: {}", e))?;
|
||||
info!(
|
||||
"Successfully built local Docker image: {}",
|
||||
local_image_name
|
||||
);
|
||||
|
||||
// 2. Push the image to the registry.
|
||||
let remote_image_name = self
|
||||
.push_docker_image(&local_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)
|
||||
}
|
||||
}
|
||||
|
||||
/// 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);
|
||||
|
||||
// --- 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())?;
|
||||
|
||||
Ok(dockerfile_path)
|
||||
}
|
||||
|
||||
/// Builds the Docker image using the generated Dockerfile.
|
||||
pub fn build_docker_image(&self) -> Result<String, Box<dyn std::error::Error>> {
|
||||
info!("Generating Dockerfile for '{}'", self.name);
|
||||
let dockerfile_path = self.build_dockerfile()?;
|
||||
|
||||
info!(
|
||||
"Building Docker image with file {} from root {}",
|
||||
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",
|
||||
"--file",
|
||||
dockerfile_path.to_str().unwrap(),
|
||||
"-t",
|
||||
&image_name,
|
||||
self.project_root.to_str().unwrap(),
|
||||
])
|
||||
.spawn()?
|
||||
.wait_with_output()?;
|
||||
|
||||
self.check_output(&output, "Failed to build Docker image")?;
|
||||
|
||||
Ok(image_name)
|
||||
}
|
||||
|
||||
/// 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);
|
||||
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()?;
|
||||
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()?;
|
||||
self.check_output(&output, "Pushing docker image failed")?;
|
||||
debug!(
|
||||
"docker push output: stdout: {}, stderr: {}",
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
|
||||
Ok(full_tag)
|
||||
}
|
||||
|
||||
/// 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) => {todo!(r#"
|
||||
# Get started with a build env with Rust nightly
|
||||
FROM rustlang/rust:nightly-bookworm as builder
|
||||
|
||||
# If you’re using stable, use this instead
|
||||
# FROM rust:1.86-bullseye as builder
|
||||
|
||||
# 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 required tools
|
||||
RUN apt-get update -y \
|
||||
&& apt-get install -y --no-install-recommends clang
|
||||
|
||||
# Install cargo-leptos
|
||||
RUN cargo binstall cargo-leptos -y
|
||||
|
||||
# Add the WASM target
|
||||
RUN rustup target add wasm32-unknown-unknown
|
||||
|
||||
# Make an /app dir, which everything will eventually live in
|
||||
RUN mkdir -p /app
|
||||
WORKDIR /app
|
||||
COPY . .
|
||||
|
||||
# Build the app
|
||||
RUN cargo leptos build --release -vv
|
||||
"#)}
|
||||
None => {
|
||||
// --- Stage 1: Builder ---
|
||||
// Use the official Rust image as the build environment.
|
||||
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"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user