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:
parent
065e3904b8
commit
16a665241e
23
Cargo.lock
generated
23
Cargo.lock
generated
@ -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",
|
||||
|
@ -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"
|
||||
|
@ -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())?,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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(),
|
||||
},
|
||||
|
@ -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]
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user