feat: add actual render

Signed-off-by: kjuulh <contact@kjuulh.io>
This commit is contained in:
Kasper Juul Hermansen 2024-05-23 21:25:53 +02:00
parent fc2430adda
commit 50d85ed383
Signed by: kjuulh
GPG Key ID: 9AA7BC13CE474394
7 changed files with 302 additions and 14 deletions

View File

@ -1,2 +1,129 @@
pub mod cluster_vars; pub mod cluster_vars;
pub mod cuddle_vars; pub mod cuddle_vars;
pub mod vault_secret {
use minijinja::{value::Object, Value};
use crate::Component;
#[derive(Debug)]
pub struct VaultSecretValues {
name: String,
secrets: VaultSecretsLookup,
}
#[derive(Debug, Clone)]
pub struct VaultSecretsLookup {
secrets: Vec<String>,
}
impl From<Vec<String>> for VaultSecretsLookup {
fn from(value: Vec<String>) -> Self {
Self { secrets: value }
}
}
#[derive(Default, Clone)]
pub struct VaultSecret {}
impl Component for VaultSecret {
fn name(&self) -> String {
"vault/secret".into()
}
fn validate(&self, value: &serde_yaml::Value) -> anyhow::Result<()> {
Ok(())
}
fn render_value(
&self,
environment: &str,
value: &serde_yaml::Value,
) -> Option<anyhow::Result<minijinja::Value>> {
if let Some(values) = value
.as_mapping()
.and_then(|map| map.get("env"))
.and_then(|v| v.as_mapping())
.map(|v| {
v.iter()
.filter_map(|(k, v)| {
if v.as_mapping()
.map(|m| m.get("vault").filter(|v| v.as_bool() == Some(true)))
.is_some()
{
Some(k)
} else {
None
}
})
.filter_map(|k| k.as_str())
.collect::<Vec<_>>()
})
{
let vault_values = VaultSecretValues {
name: self.name().replace("/", "-"),
secrets: values
.into_iter()
.map(|i| i.into())
.collect::<Vec<_>>()
.into(),
};
return Some(Ok(minijinja::Value::from_object(vault_values)));
}
None
}
fn render(
&self,
_environment: &str,
_value: &serde_yaml::Value,
) -> Option<anyhow::Result<(String, String)>> {
Some(Ok((
format!("{}.yaml", self.name().replace("/", "_")),
r#"apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultStaticSecret
metadata:
name: {{ vars.cuddle_vars.service }}-{{ vars.vault_secret.name }}
namespace: {{ vars.cluster_vars.namespace }}
spec:
destination:
create: true
name: {{ vars.cuddle_vars.service }}-{{ vars.vault_secret.name }}
mount: kvv2
path: {{ vars.cuddle_vars.service }}/{{ environment }}
refreshAfter: 30s
type: kv-v2
"#
.into(),
)))
}
}
impl Object for VaultSecretValues {
fn get_value(
self: &std::sync::Arc<Self>,
key: &minijinja::Value,
) -> Option<minijinja::Value> {
let obj = match key.as_str()? {
"name" => Value::from_safe_string(self.name.clone()),
"secrets" => Value::from_object(self.secrets.clone()),
_ => return None,
};
Some(obj)
}
}
impl Object for VaultSecretsLookup {
fn get_value(self: &std::sync::Arc<Self>, key: &Value) -> Option<Value> {
let idx = key.as_usize()?;
self.secrets.get(idx).cloned().map(Value::from_safe_string)
}
fn enumerate(self: &std::sync::Arc<Self>) -> minijinja::value::Enumerator {
minijinja::value::Enumerator::Seq(self.secrets.len())
}
}
}

View File

@ -10,7 +10,7 @@ use minijinja::context;
use tokio::io::AsyncWriteExt; use tokio::io::AsyncWriteExt;
use tokio_stream::{wrappers::ReadDirStream, StreamExt}; use tokio_stream::{wrappers::ReadDirStream, StreamExt};
use crate::components::{Component, ConcreteComponent, IntoComponent}; use crate::components::{ConcreteComponent, IntoComponent};
pub async fn process() -> anyhow::Result<()> { pub async fn process() -> anyhow::Result<()> {
process_opts(Vec::<ConcreteComponent>::new(), ProcessOpts::default()).await process_opts(Vec::<ConcreteComponent>::new(), ProcessOpts::default()).await
@ -210,6 +210,28 @@ async fn process_cluster(
process_template_file(components, value, environment, template_file, dest).await?; process_template_file(components, value, environment, template_file, dest).await?;
} }
for (template_file_name, template_content) in components
.iter()
.filter_map(|c| c.render(environment, value))
.flat_map(|v| match v {
Ok(v) => Ok(v),
Err(e) => {
tracing::warn!("failed to render value for template");
Err(e)
}
})
{
process_render_template(
components,
value,
environment,
&template_file_name,
&template_content,
dest,
)
.await?;
}
for (_, raw_file) in &template_files.raw { for (_, raw_file) in &template_files.raw {
process_raw_file(environment, raw_file, dest).await?; process_raw_file(environment, raw_file, dest).await?;
} }
@ -224,27 +246,43 @@ async fn process_template_file(
template_file: &PathBuf, template_file: &PathBuf,
dest: &Path, dest: &Path,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
// TODO: use mini jinja
let file = tokio::fs::read_to_string(template_file) let file = tokio::fs::read_to_string(template_file)
.await .await
.context(format!("failed to find file: {}", template_file.display()))?; .context(format!("failed to find file: {}", template_file.display()))?;
if !dest.exists() {
tokio::fs::create_dir_all(dest).await?;
}
let file_name = template_file let file_name = template_file
.file_stem() .file_stem()
.ok_or(anyhow::anyhow!("file didn't have a jinja2 format"))?; .ok_or(anyhow::anyhow!("file didn't have a jinja2 format"))?;
process_render_template(
components,
value,
environment,
&file_name.to_string_lossy(),
&file,
dest,
)
.await?;
Ok(())
}
async fn process_render_template(
components: &Vec<ConcreteComponent>,
value: &serde_yaml::Value,
environment: &str,
file_name: &str,
file_content: &str,
dest: &Path,
) -> anyhow::Result<()> {
if !dest.exists() {
tokio::fs::create_dir_all(dest).await?;
}
let mut dest_file = tokio::fs::File::create(dest.join(file_name)).await?; let mut dest_file = tokio::fs::File::create(dest.join(file_name)).await?;
let mut env = minijinja::Environment::new(); let mut env = minijinja::Environment::new();
env.add_template(file_name.to_str().unwrap_or_default(), &file) env.add_template(file_name, &file_content)
.context(format!( .context(format!("failed to load template at: {}", file_name))?;
"failed to load template at: {}",
template_file.display()
))?;
env.add_global("environment", environment); env.add_global("environment", environment);
let mut variables = HashMap::new(); let mut variables = HashMap::new();
@ -258,7 +296,7 @@ async fn process_template_file(
} }
} }
let tmpl = env.get_template(file_name.to_str().unwrap_or_default())?; let tmpl = env.get_template(file_name)?;
let rendered = tmpl.render(context! { let rendered = tmpl.render(context! {
vars => variables vars => variables
})?; })?;

View File

@ -4,7 +4,7 @@ mod can_run_for_env;
mod cuddle_vars; mod cuddle_vars;
use cuddle_clusters::{ use cuddle_clusters::{
catalog::{cluster_vars::ClusterVars, cuddle_vars::CuddleVars}, catalog::{cluster_vars::ClusterVars, cuddle_vars::CuddleVars, vault_secret::VaultSecret},
IntoComponent, IntoComponent,
}; };
@ -79,3 +79,20 @@ async fn with_actual_deployment() -> anyhow::Result<()> {
Ok(()) Ok(())
} }
#[tokio::test]
async fn with_vault_secrets() -> anyhow::Result<()> {
let current_dir = std::env::current_dir()?.join("tests/with_vault_secrets");
run_test_with_components(
"with_vault_secrets",
vec![
CuddleVars::new(&current_dir).await?.into_component(),
ClusterVars::default().into_component(),
VaultSecret::default().into_component(),
],
)
.await?;
Ok(())
}

View File

@ -0,0 +1,9 @@
vars:
service: service
cuddle/clusters:
dev:
env:
some.key:
vault: true
some.other.key: some_value

View File

@ -0,0 +1,39 @@
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: service
name: service
spec:
replicas: 3
selector:
matchLabels:
app: service
template:
metadata:
labels:
app: service
spec:
containers:
- args:
- serve
command:
- service
image: kasperhermansen/service:main-1715336504
name: service
envFrom:
- configMapRef:
name: service-config
env:
- name: SOME_KEY
valueFrom:
secretKeyRef:
name: service-vault-secret
key: some.key
ports:
- containerPort: 3000
name: external-http
- containerPort: 3001
name: internal-http
- containerPort: 3002
name: internal-grpc

View File

@ -0,0 +1,13 @@
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultStaticSecret
metadata:
name: service-vault-secret
namespace: dev
spec:
destination:
create: true
name: service-vault-secret
mount: kvv2
path: service/dev
refreshAfter: 30s
type: kv-v2

View File

@ -0,0 +1,45 @@
{%- set service_name = vars.cuddle_vars.service -%}
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: {{ service_name }}
name: {{ service_name }}
spec:
replicas: 3
selector:
matchLabels:
app: {{ service_name }}
template:
metadata:
labels:
app: {{ service_name }}
spec:
containers:
- args:
- serve
command:
- {{ service_name }}
image: kasperhermansen/{{ service_name }}:main-1715336504
name: {{ service_name }}
envFrom:
- configMapRef:
name: {{service_name}}-config
{%- if vars.vault_secret is defined and (vars.vault_secret.secrets | length) > 0 %}
env:
{%- for secret in vars.vault_secret.secrets %}
- name: {{secret | upper | replace(".", "_") | replace("-", "_") }}
valueFrom:
secretKeyRef:
name: {{ service_name }}-{{ vars.vault_secret.name }}
key: {{ secret }}
{%- endfor %}
{%- endif %}
ports:
- containerPort: 3000
name: external-http
- containerPort: 3001
name: internal-http
- containerPort: 3002
name: internal-grpc