Merge pull request 'feat: LampScore implement dockerfile generation and image building' (#22) from feat/lampDocker into master
Reviewed-on: https://git.nationtech.io/NationTech/harmony/pulls/22 Reviewed-by: wjro <wrolleman@nationtech.io>
This commit is contained in:
		
						commit
						76c0cacc1b
					
				
							
								
								
									
										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" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" | 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]] | [[package]] | ||||||
| name = "dyn-clone" | name = "dyn-clone" | ||||||
| version = "1.0.19" | version = "1.0.19" | ||||||
| @ -1363,6 +1385,7 @@ dependencies = [ | |||||||
|  "cidr", |  "cidr", | ||||||
|  "derive-new", |  "derive-new", | ||||||
|  "directories", |  "directories", | ||||||
|  |  "dockerfile_builder", | ||||||
|  "env_logger", |  "env_logger", | ||||||
|  "harmony_macros", |  "harmony_macros", | ||||||
|  "harmony_types", |  "harmony_types", | ||||||
|  | |||||||
| @ -36,3 +36,4 @@ non-blank-string-rs = "1.0.4" | |||||||
| k3d-rs = { path = "../k3d" } | k3d-rs = { path = "../k3d" } | ||||||
| directories = "6.0.0" | directories = "6.0.0" | ||||||
| lazy_static = "1.5.0" | lazy_static = "1.5.0" | ||||||
|  | dockerfile_builder = "0.1.5" | ||||||
|  | |||||||
| @ -57,8 +57,10 @@ impl Topology for HAClusterTopology { | |||||||
| 
 | 
 | ||||||
| #[async_trait] | #[async_trait] | ||||||
| impl K8sclient for HAClusterTopology { | impl K8sclient for HAClusterTopology { | ||||||
|     async fn k8s_client(&self) -> Result<Arc<K8sClient>, kube::Error> { |     async fn k8s_client(&self) -> Result<Arc<K8sClient>, String> { | ||||||
|         Ok(Arc::new(K8sClient::try_default().await?)) |         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 async_trait::async_trait; | ||||||
| use inquire::Confirm; | use inquire::Confirm; | ||||||
| @ -13,10 +13,10 @@ use crate::{ | |||||||
|     topology::LocalhostTopology, |     topology::LocalhostTopology, | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| use super::{HelmCommand, Topology, k8s::K8sClient}; | use super::{HelmCommand, K8sclient, Topology, k8s::K8sClient}; | ||||||
| 
 | 
 | ||||||
| struct K8sState { | struct K8sState { | ||||||
|     _client: K8sClient, |     client: Arc<K8sClient>, | ||||||
|     _source: K8sSource, |     _source: K8sSource, | ||||||
|     message: String, |     message: String, | ||||||
| } | } | ||||||
| @ -29,6 +29,23 @@ pub struct K8sAnywhereTopology { | |||||||
|     k8s_state: OnceCell<Option<K8sState>>, |     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 { | impl K8sAnywhereTopology { | ||||||
|     pub fn new() -> Self { |     pub fn new() -> Self { | ||||||
|         Self { |         Self { | ||||||
| @ -124,7 +141,7 @@ impl K8sAnywhereTopology { | |||||||
|         let k3d = k3d_rs::K3d::new(k3d_score.installation_path, Some(k3d_score.cluster_name)); |         let k3d = k3d_rs::K3d::new(k3d_score.installation_path, Some(k3d_score.cluster_name)); | ||||||
|         let state = match k3d.get_client().await { |         let state = match k3d.get_client().await { | ||||||
|             Ok(client) => K8sState { |             Ok(client) => K8sState { | ||||||
|                 _client: K8sClient::new(client), |                 client: Arc::new(K8sClient::new(client)), | ||||||
|                 _source: K8sSource::LocalK3d, |                 _source: K8sSource::LocalK3d, | ||||||
|                 message: "Successfully installed K3D cluster and acquired client".to_string(), |                 message: "Successfully installed K3D cluster and acquired client".to_string(), | ||||||
|             }, |             }, | ||||||
|  | |||||||
| @ -42,8 +42,8 @@ pub struct NetworkDomain { | |||||||
|     pub name: String, |     pub name: String, | ||||||
| } | } | ||||||
| #[async_trait] | #[async_trait] | ||||||
| pub trait K8sclient: Send + Sync + std::fmt::Debug { | pub trait K8sclient: Send + Sync { | ||||||
|     async fn k8s_client(&self) -> Result<Arc<K8sClient>, kube::Error>; |     async fn k8s_client(&self) -> Result<Arc<K8sClient>, String>; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| #[async_trait] | #[async_trait] | ||||||
|  | |||||||
| @ -1,6 +1,7 @@ | |||||||
| use std::path::{Path, PathBuf}; | use std::path::{Path, PathBuf}; | ||||||
| 
 | 
 | ||||||
| use async_trait::async_trait; | use async_trait::async_trait; | ||||||
|  | use log::info; | ||||||
| use serde::Serialize; | use serde::Serialize; | ||||||
| 
 | 
 | ||||||
| use crate::{ | 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>> { |     fn create_interpret(&self) -> Box<dyn Interpret<T>> { | ||||||
|         todo!() |         Box::new(LAMPInterpret { | ||||||
|  |             score: self.clone(), | ||||||
|  |         }) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     fn name(&self) -> String { |     fn name(&self) -> String { | ||||||
| @ -57,11 +60,23 @@ impl<T: Topology + K8sclient> Interpret<T> for LAMPInterpret { | |||||||
|         inventory: &Inventory, |         inventory: &Inventory, | ||||||
|         topology: &T, |         topology: &T, | ||||||
|     ) -> Result<Outcome, InterpretError> { |     ) -> 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 { |         let deployment_score = K8sDeploymentScore { | ||||||
|             name: <LAMPScore as Score<T>>::name(&self.score), |             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 |         deployment_score | ||||||
|             .create_interpret() |             .create_interpret() | ||||||
|             .execute(inventory, topology) |             .execute(inventory, topology) | ||||||
| @ -85,3 +100,164 @@ impl<T: Topology + K8sclient> Interpret<T> for LAMPInterpret { | |||||||
|         todo!() |         todo!() | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | use dockerfile_builder::instruction::{CMD, COPY, ENV, EXPOSE, FROM, RUN, WORKDIR}; | ||||||
|  | use dockerfile_builder::{Dockerfile, instruction_builder::EnvBuilder}; | ||||||
|  | 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( | ||||||
|  |             EnvBuilder::builder() | ||||||
|  |                 .key("PHP_ERROR_REPORTING") | ||||||
|  |                 .value("\"E_ERROR | E_WARNING | E_PARSE\"") | ||||||
|  |                 .build() | ||||||
|  |                 .unwrap(), | ||||||
|  |         ); | ||||||
|  | 
 | ||||||
|  |         // 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