Merge pull request 'feat: push docker image to registry and deploy with full tag' (#27) from feat/lampDatabase into master

Reviewed-on: https://git.nationtech.io/NationTech/harmony/pulls/27
Reviewed-by: wjro <wrolleman@nationtech.io>
This commit is contained in:
johnride 2025-05-01 17:39:23 +00:00
commit 90b80b24bc
9 changed files with 139 additions and 30 deletions

10
Cargo.lock generated
View File

@ -524,6 +524,15 @@ dependencies = [
"tiny-keccak", "tiny-keccak",
] ]
[[package]]
name = "convert_case"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baaaa0ecca5b51987b9423ccdc971514dd8b0bb7b4060b983d3664dad3f1f89f"
dependencies = [
"unicode-segmentation",
]
[[package]] [[package]]
name = "core-foundation" name = "core-foundation"
version = "0.9.4" version = "0.9.4"
@ -1383,6 +1392,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"cidr", "cidr",
"convert_case",
"derive-new", "derive-new",
"directories", "directories",
"dockerfile_builder", "dockerfile_builder",

View File

@ -35,6 +35,7 @@ serde_yaml = "0.9.34"
serde-value = "0.7.0" serde-value = "0.7.0"
http = "1.2.0" http = "1.2.0"
inquire = "0.7.5" inquire = "0.7.5"
convert_case = "0.8.0"
[workspace.dependencies.uuid] [workspace.dependencies.uuid]
version = "1.11.0" version = "1.11.0"

View File

@ -8,17 +8,30 @@ use harmony::{
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
// let _ = env_logger::Builder::from_default_env().filter_level(log::LevelFilter::Info).try_init(); // This here is the whole configuration to
// - setup a local K3D cluster
// - Build a docker image with the PHP project builtin and production grade settings
// - Deploy a mariadb database using a production grade helm chart
// - Deploy the new container using a kubernetes deployment
// - Configure networking between the PHP container and the database
// - Provision a public route and an SSL certificate automatically on production environments
//
// Enjoy :)
let lamp_stack = LAMPScore { let lamp_stack = LAMPScore {
name: "harmony-lamp-demo".to_string(), name: "harmony-lamp-demo".to_string(),
domain: Url::Url(url::Url::parse("https://lampdemo.harmony.nationtech.io").unwrap()), domain: Url::Url(url::Url::parse("https://lampdemo.harmony.nationtech.io").unwrap()),
php_version: Version::from("8.4.4").unwrap(), php_version: Version::from("8.4.4").unwrap(),
// This config can be extended as needed for more complicated configurations
config: LAMPConfig { config: LAMPConfig {
project_root: "./php".into(), project_root: "./php".into(),
..Default::default() ..Default::default()
}, },
}; };
// You can choose the type of Topology you want, we suggest starting with the
// K8sAnywhereTopology as it is the most automatic one that enables you to easily deploy
// locally, to development environment from a CI, to staging, and to production with settings
// that automatically adapt to each environment grade.
let mut maestro = Maestro::<K8sAnywhereTopology>::initialize( let mut maestro = Maestro::<K8sAnywhereTopology>::initialize(
Inventory::autoload(), Inventory::autoload(),
K8sAnywhereTopology::new(), K8sAnywhereTopology::new(),
@ -26,5 +39,7 @@ async fn main() {
.await .await
.unwrap(); .unwrap();
maestro.register_all(vec![Box::new(lamp_stack)]); maestro.register_all(vec![Box::new(lamp_stack)]);
// Here we bootstrap the CLI, this gives some nice features if you need them
harmony_cli::init(maestro, None).await.unwrap(); harmony_cli::init(maestro, None).await.unwrap();
} }
// That's it, end of the infra as code.

View File

@ -13,23 +13,23 @@ rust-ipmi = "0.1.1"
semver = "1.0.23" semver = "1.0.23"
serde = { version = "1.0.209", features = ["derive"] } serde = { version = "1.0.209", features = ["derive"] }
serde_json = "1.0.127" serde_json = "1.0.127"
tokio = { workspace = true } tokio.workspace = true
derive-new = { workspace = true } derive-new.workspace = true
log = { workspace = true } log.workspace = true
env_logger = { workspace = true } env_logger.workspace = true
async-trait = { workspace = true } async-trait.workspace = true
cidr = { workspace = true } cidr.workspace = true
opnsense-config = { path = "../opnsense-config" } opnsense-config = { path = "../opnsense-config" }
opnsense-config-xml = { path = "../opnsense-config-xml" } opnsense-config-xml = { path = "../opnsense-config-xml" }
harmony_macros = { path = "../harmony_macros" } harmony_macros = { path = "../harmony_macros" }
harmony_types = { path = "../harmony_types" } harmony_types = { path = "../harmony_types" }
uuid = { workspace = true } uuid.workspace = true
url = { workspace = true } url.workspace = true
kube = { workspace = true } kube.workspace = true
k8s-openapi = { workspace = true } k8s-openapi.workspace = true
serde_yaml = { workspace = true } serde_yaml.workspace = true
http = { workspace = true } http.workspace = true
serde-value = { workspace = true } serde-value.workspace = true
inquire.workspace = true inquire.workspace = true
helm-wrapper-rs = "0.4.0" helm-wrapper-rs = "0.4.0"
non-blank-string-rs = "1.0.4" non-blank-string-rs = "1.0.4"
@ -38,3 +38,4 @@ directories = "6.0.0"
lazy_static = "1.5.0" lazy_static = "1.5.0"
dockerfile_builder = "0.1.5" dockerfile_builder = "0.1.5"
temp-file = "0.1.9" temp-file = "0.1.9"
convert_case.workspace = true

View File

@ -6,4 +6,8 @@ lazy_static! {
.unwrap() .unwrap()
.data_dir() .data_dir()
.join("harmony"); .join("harmony");
pub static ref REGISTRY_URL: String = std::env::var("HARMONY_REGISTRY_URL")
.unwrap_or_else(|_| "hub.nationtech.io".to_string());
pub static ref REGISTRY_PROJECT: String =
std::env::var("HARMONY_REGISTRY_PROJECT").unwrap_or_else(|_| "harmony".to_string());
} }

View File

@ -38,7 +38,7 @@ impl K8sClient {
Ok(result) Ok(result)
} }
pub async fn apply_namespaced<K>(&self, resource: &Vec<K>) -> Result<K, Error> pub async fn apply_namespaced<K>(&self, resource: &Vec<K>, ns: Option<&str>) -> Result<K, Error>
where where
K: Resource<Scope = NamespaceResourceScope> K: Resource<Scope = NamespaceResourceScope>
+ Clone + Clone
@ -49,7 +49,10 @@ impl K8sClient {
<K as kube::Resource>::DynamicType: Default, <K as kube::Resource>::DynamicType: Default,
{ {
for r in resource.iter() { for r in resource.iter() {
let api: Api<K> = Api::default_namespaced(self.client.clone()); let api: Api<K> = match ns {
Some(ns) => Api::namespaced(self.client.clone(), ns),
None => Api::default_namespaced(self.client.clone()),
};
api.create(&PostParams::default(), &r).await?; api.create(&PostParams::default(), &r).await?;
} }
todo!("") todo!("")

View File

@ -1,4 +1,5 @@
use k8s_openapi::api::apps::v1::Deployment; use k8s_openapi::{DeepMerge, api::apps::v1::Deployment};
use log::debug;
use serde::Serialize; use serde::Serialize;
use serde_json::json; use serde_json::json;
@ -14,11 +15,13 @@ use super::resource::{K8sResourceInterpret, K8sResourceScore};
pub struct K8sDeploymentScore { pub struct K8sDeploymentScore {
pub name: String, pub name: String,
pub image: String, pub image: String,
pub namespace: Option<String>,
pub env_vars: serde_json::Value,
} }
impl<T: Topology + K8sclient> Score<T> for K8sDeploymentScore { impl<T: Topology + K8sclient> Score<T> for K8sDeploymentScore {
fn create_interpret(&self) -> Box<dyn Interpret<T>> { fn create_interpret(&self) -> Box<dyn Interpret<T>> {
let deployment: Deployment = serde_json::from_value(json!( let deployment = json!(
{ {
"metadata": { "metadata": {
"name": self.name "name": self.name
@ -38,18 +41,21 @@ impl<T: Topology + K8sclient> Score<T> for K8sDeploymentScore {
"spec": { "spec": {
"containers": [ "containers": [
{ {
"image": self.image, "image": self.image,
"name": self.image "name": self.name,
"imagePullPolicy": "IfNotPresent",
"env": self.env_vars,
} }
] ]
} }
} }
} }
} }
)) );
.unwrap();
let deployment: Deployment = serde_json::from_value(deployment).unwrap();
Box::new(K8sResourceInterpret { Box::new(K8sResourceInterpret {
score: K8sResourceScore::single(deployment.clone()), score: K8sResourceScore::single(deployment.clone(), self.namespace.clone()),
}) })
} }

View File

@ -14,12 +14,14 @@ use crate::{
#[derive(Debug, Clone, Serialize)] #[derive(Debug, Clone, Serialize)]
pub struct K8sResourceScore<K: Resource + std::fmt::Debug> { pub struct K8sResourceScore<K: Resource + std::fmt::Debug> {
pub resource: Vec<K>, pub resource: Vec<K>,
pub namespace: Option<String>,
} }
impl<K: Resource + std::fmt::Debug> K8sResourceScore<K> { impl<K: Resource + std::fmt::Debug> K8sResourceScore<K> {
pub fn single(resource: K) -> Self { pub fn single(resource: K, namespace: Option<String>) -> Self {
Self { Self {
resource: vec![resource], resource: vec![resource],
namespace,
} }
} }
} }
@ -77,7 +79,7 @@ where
.k8s_client() .k8s_client()
.await .await
.expect("Environment should provide enough information to instanciate a client") .expect("Environment should provide enough information to instanciate a client")
.apply_namespaced(&self.score.resource) .apply_namespaced(&self.score.resource, self.score.namespace.as_deref())
.await?; .await?;
Ok(Outcome::success( Ok(Outcome::success(

View File

@ -1,14 +1,17 @@
use convert_case::{Case, Casing};
use dockerfile_builder::instruction::{CMD, COPY, ENV, EXPOSE, FROM, RUN, WORKDIR}; use dockerfile_builder::instruction::{CMD, COPY, ENV, EXPOSE, FROM, RUN, WORKDIR};
use dockerfile_builder::{Dockerfile, instruction_builder::EnvBuilder}; use dockerfile_builder::{Dockerfile, instruction_builder::EnvBuilder};
use non_blank_string_rs::NonBlankString; use non_blank_string_rs::NonBlankString;
use serde_json::json;
use std::fs; use std::fs;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::str::FromStr; use std::str::FromStr;
use async_trait::async_trait; use async_trait::async_trait;
use log::info; use log::{debug, info};
use serde::Serialize; use serde::Serialize;
use crate::config::{REGISTRY_PROJECT, REGISTRY_URL};
use crate::topology::HelmCommand; use crate::topology::HelmCommand;
use crate::{ use crate::{
data::{Id, Version}, data::{Id, Version},
@ -80,22 +83,49 @@ impl<T: Topology + K8sclient + HelmCommand> Interpret<T> for LAMPInterpret {
}; };
info!("LAMP docker image built {image_name}"); 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"); info!("Deploying database");
self.deploy_database(inventory, topology).await?; 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 { let deployment_score = K8sDeploymentScore {
name: <LAMPScore as Score<T>>::name(&self.score), name: <LAMPScore as Score<T>>::name(&self.score).to_case(Case::Kebab),
image: image_name, 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"
}
}
},
]),
}; };
info!("Deploying score {deployment_score:#?}");
deployment_score deployment_score
.create_interpret() .create_interpret()
.execute(inventory, topology) .execute(inventory, topology)
.await?; .await?;
info!("LAMP deployment_score {deployment_score:?}"); info!("LAMP deployment_score {deployment_score:?}");
todo!("1. Use HelmChartScore to deploy mariadb todo!("1. [x] Use HelmChartScore to deploy mariadb
2. Use deploymentScore to deploy lamp docker container 2. [x] Use deploymentScore to deploy lamp docker container
3. for remote clusters, push the image to some registry (use nationtech's for demos? push to the cluster's registry?)"); 3. for remote clusters, push the image to some registry (use nationtech's for demos? push to the cluster's registry?)");
} }
@ -258,6 +288,43 @@ opcache.fast_shutdown=1
Ok(dockerfile_path) 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>> { pub fn build_docker_image(&self) -> Result<String, Box<dyn std::error::Error>> {
info!("Generating Dockerfile"); info!("Generating Dockerfile");
let dockerfile = self.build_dockerfile(&self.score)?; let dockerfile = self.build_dockerfile(&self.score)?;