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 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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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
|
||||||
})?;
|
})?;
|
||||||
|
@ -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(¤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