From 16a665241e6e98cd6a5046f42748c97a0b0f068e Mon Sep 17 00:00:00 2001 From: Jean-Gabriel Gill-Couture Date: Fri, 25 Apr 2025 14:29:03 -0400 Subject: [PATCH] feat: LampScore implement dockerfile generation and image building - Added `build_dockerfile` function to generate a Dockerfile based on the LAMP stack for the given project. - Implemented `build_docker_image` to execute the docker build command and create the image. - Configured user and permissions for apache. - Included necessary apache configuration for security. - Added error handling for docker build failures. - Exposed port 80 for external access. - Added basic serialization to Config struct. --- Cargo.lock | 23 +++ harmony/Cargo.toml | 1 + harmony/src/domain/topology/ha_cluster.rs | 6 +- harmony/src/domain/topology/k8s_anywhere.rs | 25 ++- harmony/src/domain/topology/network.rs | 4 +- harmony/src/modules/lamp.rs | 178 +++++++++++++++++++- 6 files changed, 226 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5d6c373..35c5d85 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -833,6 +833,28 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" +[[package]] +name = "dockerfile_builder" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ac372e31c7dd054d0fc69ca96ca36ee8d1cf79881683ad6f783c47aba3dc6e2" +dependencies = [ + "dockerfile_builder_macros", + "eyre", +] + +[[package]] +name = "dockerfile_builder_macros" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b627d9019ce257916c7ada6f233cf22e1e5246b6d9426b20610218afb7fd3ec9" +dependencies = [ + "eyre", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "dyn-clone" version = "1.0.19" @@ -1363,6 +1385,7 @@ dependencies = [ "cidr", "derive-new", "directories", + "dockerfile_builder", "env_logger", "harmony_macros", "harmony_types", diff --git a/harmony/Cargo.toml b/harmony/Cargo.toml index 5c36335..a140a8b 100644 --- a/harmony/Cargo.toml +++ b/harmony/Cargo.toml @@ -36,3 +36,4 @@ non-blank-string-rs = "1.0.4" k3d-rs = { path = "../k3d" } directories = "6.0.0" lazy_static = "1.5.0" +dockerfile_builder = "0.1.5" diff --git a/harmony/src/domain/topology/ha_cluster.rs b/harmony/src/domain/topology/ha_cluster.rs index 766c93c..4e94f49 100644 --- a/harmony/src/domain/topology/ha_cluster.rs +++ b/harmony/src/domain/topology/ha_cluster.rs @@ -57,8 +57,10 @@ impl Topology for HAClusterTopology { #[async_trait] impl K8sclient for HAClusterTopology { - async fn k8s_client(&self) -> Result, kube::Error> { - Ok(Arc::new(K8sClient::try_default().await?)) + async fn k8s_client(&self) -> Result, String> { + Ok(Arc::new( + K8sClient::try_default().await.map_err(|e| e.to_string())?, + )) } } diff --git a/harmony/src/domain/topology/k8s_anywhere.rs b/harmony/src/domain/topology/k8s_anywhere.rs index 5325915..f363524 100644 --- a/harmony/src/domain/topology/k8s_anywhere.rs +++ b/harmony/src/domain/topology/k8s_anywhere.rs @@ -1,4 +1,4 @@ -use std::process::Command; +use std::{process::Command, sync::Arc}; use async_trait::async_trait; use inquire::Confirm; @@ -13,10 +13,10 @@ use crate::{ topology::LocalhostTopology, }; -use super::{HelmCommand, Topology, k8s::K8sClient}; +use super::{HelmCommand, K8sclient, Topology, k8s::K8sClient}; struct K8sState { - _client: K8sClient, + client: Arc, _source: K8sSource, message: String, } @@ -29,6 +29,23 @@ pub struct K8sAnywhereTopology { k8s_state: OnceCell>, } +#[async_trait] +impl K8sclient for K8sAnywhereTopology { + async fn k8s_client(&self) -> Result, String> { + let state = match self.k8s_state.get() { + Some(state) => state, + None => return Err("K8s state not initialized yet".to_string()), + }; + + let state = match state { + Some(state) => state, + None => return Err("K8s client initialized but empty".to_string()), + }; + + Ok(state.client.clone()) + } +} + impl K8sAnywhereTopology { pub fn new() -> Self { Self { @@ -124,7 +141,7 @@ impl K8sAnywhereTopology { let k3d = k3d_rs::K3d::new(k3d_score.installation_path, Some(k3d_score.cluster_name)); let state = match k3d.get_client().await { Ok(client) => K8sState { - _client: K8sClient::new(client), + client: Arc::new(K8sClient::new(client)), _source: K8sSource::LocalK3d, message: "Successfully installed K3D cluster and acquired client".to_string(), }, diff --git a/harmony/src/domain/topology/network.rs b/harmony/src/domain/topology/network.rs index d4463ae..ce6ec1e 100644 --- a/harmony/src/domain/topology/network.rs +++ b/harmony/src/domain/topology/network.rs @@ -42,8 +42,8 @@ pub struct NetworkDomain { pub name: String, } #[async_trait] -pub trait K8sclient: Send + Sync + std::fmt::Debug { - async fn k8s_client(&self) -> Result, kube::Error>; +pub trait K8sclient: Send + Sync { + async fn k8s_client(&self) -> Result, String>; } #[async_trait] diff --git a/harmony/src/modules/lamp.rs b/harmony/src/modules/lamp.rs index 55eefdd..928e8b7 100644 --- a/harmony/src/modules/lamp.rs +++ b/harmony/src/modules/lamp.rs @@ -1,6 +1,7 @@ use std::path::{Path, PathBuf}; use async_trait::async_trait; +use log::info; use serde::Serialize; use crate::{ @@ -35,9 +36,11 @@ impl Default for LAMPConfig { } } -impl Score for LAMPScore { +impl Score for LAMPScore { fn create_interpret(&self) -> Box> { - todo!() + Box::new(LAMPInterpret { + score: self.clone(), + }) } fn name(&self) -> String { @@ -57,11 +60,23 @@ impl Interpret for LAMPInterpret { inventory: &Inventory, topology: &T, ) -> Result { + let image_name = match self.build_docker_image() { + Ok(name) => name, + Err(e) => { + return Err(InterpretError::new(format!( + "Could not build LAMP docker image {e}" + ))); + } + }; + info!("LAMP docker image built {image_name}"); + let deployment_score = K8sDeploymentScore { name: >::name(&self.score), - image: "local_image".to_string(), + image: image_name, }; + info!("LAMP deployment_score {deployment_score:?}"); + todo!(); deployment_score .create_interpret() .execute(inventory, topology) @@ -85,3 +100,160 @@ impl Interpret for LAMPInterpret { todo!() } } + +use dockerfile_builder::Dockerfile; +use dockerfile_builder::instruction::{CMD, COPY, ENV, EXPOSE, FROM, RUN, WORKDIR}; +use std::fs; + +impl LAMPInterpret { + pub fn build_dockerfile( + &self, + score: &LAMPScore, + ) -> Result> { + let mut dockerfile = Dockerfile::new(); + + // Use the PHP version from the score to determine the base image + let php_version = score.php_version.to_string(); + let php_major_minor = php_version + .split('.') + .take(2) + .collect::>() + .join("."); + + // Base image selection - using official PHP image with Apache + dockerfile.push(FROM::from(format!("php:{}-apache", php_major_minor))); + + // Set environment variables for PHP configuration + dockerfile.push(ENV::from("PHP_MEMORY_LIMIT=256M")); + dockerfile.push(ENV::from("PHP_MAX_EXECUTION_TIME=30")); + dockerfile.push(ENV::from( + "PHP_ERROR_REPORTING=E_ERROR | E_WARNING | E_PARSE", + )); + + // Install necessary PHP extensions and dependencies + dockerfile.push(RUN::from( + "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/*", + )); + + dockerfile.push(RUN::from( + "docker-php-ext-configure gd --with-freetype --with-jpeg && \ + docker-php-ext-install -j$(nproc) \ + gd \ + mysqli \ + pdo_mysql \ + zip \ + opcache", + )); + + // Copy PHP configuration + dockerfile.push(RUN::from("mkdir -p /usr/local/etc/php/conf.d/")); + + // Create and copy a custom PHP configuration + let php_config = r#" +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 +"#; + + // Save this configuration to a temporary file within the project root + let config_path = Path::new(&score.config.project_root).join("docker-php.ini"); + fs::write(&config_path, php_config)?; + + // Reference the file within the Docker context (where the build runs) + dockerfile.push(COPY::from( + "docker-php.ini /usr/local/etc/php/conf.d/docker-php.ini", + )); + + // Security hardening + dockerfile.push(RUN::from( + "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" + )); + + // Create a dedicated user for running Apache + dockerfile.push(RUN::from( + "groupadd -g 1000 appuser && \ + useradd -u 1000 -g appuser -m -s /bin/bash appuser && \ + chown -R appuser:appuser /var/www/html", + )); + + // Set the working directory + dockerfile.push(WORKDIR::from("/var/www/html")); + + // Copy application code from the project root to the container + // Note: In Dockerfile, the COPY context is relative to the build context + // We'll handle the actual context in the build_docker_image method + dockerfile.push(COPY::from(". /var/www/html")); + + // Fix permissions + dockerfile.push(RUN::from("chown -R appuser:appuser /var/www/html")); + + // Expose Apache port + dockerfile.push(EXPOSE::from("80/tcp")); + + // Set the default command + dockerfile.push(CMD::from("apache2-foreground")); + + // Save the Dockerfile to disk in the project root + let dockerfile_path = Path::new(&score.config.project_root).join("Dockerfile"); + fs::write(&dockerfile_path, dockerfile.to_string())?; + + Ok(dockerfile_path) + } + + pub fn build_docker_image(&self) -> Result> { + info!("Generating Dockerfile"); + let dockerfile = self.build_dockerfile(&self.score)?; + + info!( + "Building Docker image with file {} from root {}", + dockerfile.to_string_lossy(), + self.score.config.project_root.to_string_lossy() + ); + let image_name = format!("{}-php-apache", self.score.name); + let project_root = &self.score.config.project_root; + + let output = std::process::Command::new("docker") + .args([ + "build", + "--file", + dockerfile.to_str().unwrap(), + "-t", + &image_name, + project_root.to_str().unwrap(), + ]) + .output()?; + + if !output.status.success() { + return Err(format!( + "Failed to build Docker image: {}", + String::from_utf8_lossy(&output.stderr) + ) + .into()); + } + + Ok(image_name) + } +}