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.
This commit is contained in:
Jean-Gabriel Gill-Couture 2025-04-25 14:29:03 -04:00
parent 065e3904b8
commit 16a665241e
6 changed files with 226 additions and 11 deletions

23
Cargo.lock generated
View File

@ -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",

View File

@ -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"

View File

@ -57,8 +57,10 @@ impl Topology for HAClusterTopology {
#[async_trait]
impl K8sclient for HAClusterTopology {
async fn k8s_client(&self) -> Result<Arc<K8sClient>, kube::Error> {
Ok(Arc::new(K8sClient::try_default().await?))
async fn k8s_client(&self) -> Result<Arc<K8sClient>, String> {
Ok(Arc::new(
K8sClient::try_default().await.map_err(|e| e.to_string())?,
))
}
}

View File

@ -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<K8sClient>,
_source: K8sSource,
message: String,
}
@ -29,6 +29,23 @@ pub struct K8sAnywhereTopology {
k8s_state: OnceCell<Option<K8sState>>,
}
#[async_trait]
impl K8sclient for K8sAnywhereTopology {
async fn k8s_client(&self) -> Result<Arc<K8sClient>, 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(),
},

View File

@ -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<Arc<K8sClient>, kube::Error>;
pub trait K8sclient: Send + Sync {
async fn k8s_client(&self) -> Result<Arc<K8sClient>, String>;
}
#[async_trait]

View File

@ -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<T: Topology> Score<T> for LAMPScore {
impl<T: Topology + K8sclient> Score<T> for LAMPScore {
fn create_interpret(&self) -> Box<dyn Interpret<T>> {
todo!()
Box::new(LAMPInterpret {
score: self.clone(),
})
}
fn name(&self) -> String {
@ -57,11 +60,23 @@ impl<T: Topology + K8sclient> Interpret<T> for LAMPInterpret {
inventory: &Inventory,
topology: &T,
) -> Result<Outcome, InterpretError> {
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: <LAMPScore as Score<T>>::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<T: Topology + K8sclient> Interpret<T> 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<PathBuf, Box<dyn std::error::Error>> {
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::<Vec<&str>>()
.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<String, Box<dyn std::error::Error>> {
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)
}
}