feat: add actual render
Signed-off-by: kjuulh <contact@kjuulh.io>
This commit is contained in:
parent
fc2430adda
commit
50d85ed383
@ -1,2 +1,129 @@
|
||||
pub mod cluster_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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -10,7 +10,7 @@ use minijinja::context;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use tokio_stream::{wrappers::ReadDirStream, StreamExt};
|
||||
|
||||
use crate::components::{Component, ConcreteComponent, IntoComponent};
|
||||
use crate::components::{ConcreteComponent, IntoComponent};
|
||||
|
||||
pub async fn process() -> anyhow::Result<()> {
|
||||
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?;
|
||||
}
|
||||
|
||||
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 {
|
||||
process_raw_file(environment, raw_file, dest).await?;
|
||||
}
|
||||
@ -224,27 +246,43 @@ async fn process_template_file(
|
||||
template_file: &PathBuf,
|
||||
dest: &Path,
|
||||
) -> anyhow::Result<()> {
|
||||
// TODO: use mini jinja
|
||||
let file = tokio::fs::read_to_string(template_file)
|
||||
.await
|
||||
.context(format!("failed to find file: {}", template_file.display()))?;
|
||||
|
||||
if !dest.exists() {
|
||||
tokio::fs::create_dir_all(dest).await?;
|
||||
}
|
||||
|
||||
let file_name = template_file
|
||||
.file_stem()
|
||||
.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 env = minijinja::Environment::new();
|
||||
env.add_template(file_name.to_str().unwrap_or_default(), &file)
|
||||
.context(format!(
|
||||
"failed to load template at: {}",
|
||||
template_file.display()
|
||||
))?;
|
||||
env.add_template(file_name, &file_content)
|
||||
.context(format!("failed to load template at: {}", file_name))?;
|
||||
env.add_global("environment", environment);
|
||||
|
||||
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! {
|
||||
vars => variables
|
||||
})?;
|
||||
|
@ -4,7 +4,7 @@ mod can_run_for_env;
|
||||
mod cuddle_vars;
|
||||
|
||||
use cuddle_clusters::{
|
||||
catalog::{cluster_vars::ClusterVars, cuddle_vars::CuddleVars},
|
||||
catalog::{cluster_vars::ClusterVars, cuddle_vars::CuddleVars, vault_secret::VaultSecret},
|
||||
IntoComponent,
|
||||
};
|
||||
|
||||
@ -79,3 +79,20 @@ async fn with_actual_deployment() -> anyhow::Result<()> {
|
||||
|
||||
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(¤t_dir).await?.into_component(),
|
||||
ClusterVars::default().into_component(),
|
||||
VaultSecret::default().into_component(),
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -0,0 +1,9 @@
|
||||
vars:
|
||||
service: service
|
||||
|
||||
cuddle/clusters:
|
||||
dev:
|
||||
env:
|
||||
some.key:
|
||||
vault: true
|
||||
some.other.key: some_value
|
@ -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
|
@ -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
|
@ -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
|
Loading…
Reference in New Issue
Block a user