forked from NationTech/harmony
This includes Id, IpAddress, Url and some other heavily used types
419 lines
13 KiB
Rust
419 lines
13 KiB
Rust
use convert_case::{Case, Casing};
|
|
use dockerfile_builder::instruction::{CMD, COPY, ENV, EXPOSE, FROM, RUN, WORKDIR};
|
|
use dockerfile_builder::{Dockerfile, instruction_builder::EnvBuilder};
|
|
use fqdn::fqdn;
|
|
use harmony_macros::ingress_path;
|
|
use harmony_types::net::Url;
|
|
use non_blank_string_rs::NonBlankString;
|
|
use serde_json::json;
|
|
use std::collections::HashMap;
|
|
use std::fs;
|
|
use std::path::{Path, PathBuf};
|
|
use std::str::FromStr;
|
|
|
|
use async_trait::async_trait;
|
|
use log::{debug, info};
|
|
use serde::Serialize;
|
|
|
|
use crate::config::{REGISTRY_PROJECT, REGISTRY_URL};
|
|
use crate::modules::k8s::ingress::K8sIngressScore;
|
|
use crate::topology::HelmCommand;
|
|
use crate::{
|
|
data::Version,
|
|
interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome},
|
|
inventory::Inventory,
|
|
modules::k8s::deployment::K8sDeploymentScore,
|
|
score::Score,
|
|
topology::{K8sclient, Topology},
|
|
};
|
|
use harmony_types::id::Id;
|
|
|
|
use super::helm::chart::HelmChartScore;
|
|
|
|
#[derive(Debug, Clone, Serialize)]
|
|
pub struct LAMPScore {
|
|
pub name: String,
|
|
pub domain: Url,
|
|
pub config: LAMPConfig,
|
|
pub php_version: Version,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize)]
|
|
pub struct LAMPConfig {
|
|
pub project_root: PathBuf,
|
|
pub ssl_enabled: bool,
|
|
pub database_size: Option<String>,
|
|
pub namespace: String,
|
|
}
|
|
|
|
impl Default for LAMPConfig {
|
|
fn default() -> Self {
|
|
LAMPConfig {
|
|
project_root: Path::new("./src").to_path_buf(),
|
|
ssl_enabled: true,
|
|
database_size: None,
|
|
namespace: "harmony-lamp".to_string(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<T: Topology + K8sclient + HelmCommand> Score<T> for LAMPScore {
|
|
fn create_interpret(&self) -> Box<dyn Interpret<T>> {
|
|
Box::new(LAMPInterpret {
|
|
score: self.clone(),
|
|
})
|
|
}
|
|
|
|
fn name(&self) -> String {
|
|
"LampScore".to_string()
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct LAMPInterpret {
|
|
score: LAMPScore,
|
|
}
|
|
|
|
#[async_trait]
|
|
impl<T: Topology + K8sclient + HelmCommand> Interpret<T> for LAMPInterpret {
|
|
async fn execute(
|
|
&self,
|
|
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 remote_name = match self.push_docker_image(&image_name) {
|
|
Ok(remote_name) => remote_name,
|
|
Err(e) => {
|
|
return Err(InterpretError::new(format!(
|
|
"Could not push docker image {e}"
|
|
)));
|
|
}
|
|
};
|
|
info!("LAMP docker image pushed to {remote_name}");
|
|
|
|
info!("Deploying database");
|
|
self.deploy_database(inventory, topology).await?;
|
|
|
|
let base_name = self.score.name.to_case(Case::Kebab);
|
|
let secret_name = format!("{}-database-mariadb", base_name);
|
|
|
|
let deployment_score = K8sDeploymentScore {
|
|
name: <LAMPScore as Score<T>>::name(&self.score).to_case(Case::Kebab),
|
|
image: remote_name,
|
|
namespace: self.get_namespace().map(|nbs| nbs.to_string()),
|
|
env_vars: json!([
|
|
{
|
|
"name": "MYSQL_PASSWORD",
|
|
"valueFrom": {
|
|
"secretKeyRef": {
|
|
"name": secret_name,
|
|
"key": "mariadb-root-password"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"name": "MYSQL_HOST",
|
|
"value": secret_name
|
|
},
|
|
]),
|
|
};
|
|
|
|
info!("Deploying score {deployment_score:#?}");
|
|
|
|
deployment_score.interpret(inventory, topology).await?;
|
|
|
|
info!("LAMP deployment_score {deployment_score:?}");
|
|
|
|
let ingress_path = ingress_path!("/");
|
|
|
|
let lamp_ingress = K8sIngressScore {
|
|
name: fqdn!("lamp-ingress"),
|
|
host: fqdn!("test"),
|
|
backend_service: fqdn!(
|
|
<LAMPScore as Score<T>>::name(&self.score)
|
|
.to_case(Case::Kebab)
|
|
.as_str()
|
|
),
|
|
port: 8080,
|
|
path: Some(ingress_path),
|
|
path_type: None,
|
|
namespace: self
|
|
.get_namespace()
|
|
.map(|nbs| fqdn!(nbs.to_string().as_str())),
|
|
};
|
|
|
|
lamp_ingress.interpret(inventory, topology).await?;
|
|
|
|
info!("LAMP lamp_ingress {lamp_ingress:?}");
|
|
|
|
Ok(Outcome::success(
|
|
"Successfully deployed LAMP Stack!".to_string(),
|
|
))
|
|
}
|
|
|
|
fn get_name(&self) -> InterpretName {
|
|
InterpretName::Lamp
|
|
}
|
|
|
|
fn get_version(&self) -> Version {
|
|
todo!()
|
|
}
|
|
|
|
fn get_status(&self) -> InterpretStatus {
|
|
todo!()
|
|
}
|
|
|
|
fn get_children(&self) -> Vec<Id> {
|
|
todo!()
|
|
}
|
|
}
|
|
|
|
impl LAMPInterpret {
|
|
async fn deploy_database<T: Topology + K8sclient + HelmCommand>(
|
|
&self,
|
|
inventory: &Inventory,
|
|
topology: &T,
|
|
) -> Result<Outcome, InterpretError> {
|
|
let mut values_overrides = HashMap::new();
|
|
if let Some(database_size) = self.score.config.database_size.clone() {
|
|
values_overrides.insert(
|
|
NonBlankString::from_str("primary.persistence.size").unwrap(),
|
|
database_size,
|
|
);
|
|
values_overrides.insert(
|
|
NonBlankString::from_str("auth.rootPassword").unwrap(),
|
|
"mariadb-changethis".to_string(),
|
|
);
|
|
}
|
|
let score = HelmChartScore {
|
|
namespace: self.get_namespace(),
|
|
release_name: NonBlankString::from_str(&format!("{}-database", self.score.name))
|
|
.unwrap(),
|
|
chart_name: NonBlankString::from_str(
|
|
"oci://registry-1.docker.io/bitnamicharts/mariadb",
|
|
)
|
|
.unwrap(),
|
|
chart_version: None,
|
|
values_overrides: Some(values_overrides),
|
|
create_namespace: true,
|
|
install_only: false,
|
|
values_yaml: None,
|
|
repository: None,
|
|
};
|
|
|
|
score.interpret(inventory, topology).await
|
|
}
|
|
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",
|
|
));
|
|
|
|
dockerfile.push(RUN::from(r#"sed -i 's/VirtualHost \*:80/VirtualHost *:8080/' /etc/apache2/sites-available/000-default.conf && \
|
|
sed -i 's/^Listen 80$/Listen 8080/' /etc/apache2/ports.conf"#));
|
|
|
|
// 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"
|
|
));
|
|
|
|
// Set env vars
|
|
dockerfile.push(RUN::from(
|
|
"echo 'PassEnv MYSQL_PASSWORD' >> /etc/apache2/sites-available/000-default.conf \
|
|
&& echo 'PassEnv MYSQL_USER' >> /etc/apache2/sites-available/000-default.conf \
|
|
&& echo 'PassEnv MYSQL_HOST' >> /etc/apache2/sites-available/000-default.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("8080/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)
|
|
}
|
|
|
|
fn check_output(
|
|
&self,
|
|
output: &std::process::Output,
|
|
msg: &str,
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
if !output.status.success() {
|
|
return Err(format!("{msg}: {}", String::from_utf8_lossy(&output.stderr)).into());
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
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);
|
|
let output = std::process::Command::new("docker")
|
|
.args(["tag", image_name, &full_tag])
|
|
.output()?;
|
|
self.check_output(&output, "Tagging docker image failed")?;
|
|
|
|
debug!(
|
|
"docker tag output {} {}",
|
|
String::from_utf8_lossy(&output.stdout),
|
|
String::from_utf8_lossy(&output.stderr)
|
|
);
|
|
|
|
let output = std::process::Command::new("docker")
|
|
.args(["push", &full_tag])
|
|
.output()?;
|
|
self.check_output(&output, "Pushing docker image failed")?;
|
|
debug!(
|
|
"docker push output {} {}",
|
|
String::from_utf8_lossy(&output.stdout),
|
|
String::from_utf8_lossy(&output.stderr)
|
|
);
|
|
|
|
Ok(full_tag)
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
fn get_namespace(&self) -> Option<NonBlankString> {
|
|
Some(NonBlankString::from_str(&self.score.config.namespace).unwrap())
|
|
}
|
|
}
|