use crate::backend::{Backend, Manifest, RenderError};
use serde_json::{json, Value};
use std::fmt::Write;
use tatara_env::compile::{Env, Resource};
#[derive(Debug, Clone)]
pub struct KubernetesYaml {
pub namespace: String,
pub extra_labels: Vec<(String, String)>,
}
impl Default for KubernetesYaml {
fn default() -> Self {
Self {
namespace: "default".into(),
extra_labels: Vec::new(),
}
}
}
impl Backend for KubernetesYaml {
fn render(&self, env: &Env) -> Result<Vec<Manifest>, RenderError> {
let mut out = Vec::new();
for r in &env.resources {
let m = match r.keyword.as_str() {
"defbpf-program" | "defbpf-map" | "defbpf-policy" => {
self.render_bpf_configmap(env, r)?
}
_ => {
if let Some(meta) = tatara_lisp::domain::lookup_render(&r.keyword) {
self.render_via_registry(env, r, &meta)?
} else {
return Err(RenderError::Unsupported(r.keyword.clone()));
}
}
};
out.push(m);
}
Ok(out)
}
}
impl KubernetesYaml {
fn metadata(&self, env: &Env, name: &str) -> Value {
let mut labels = serde_json::Map::new();
labels.insert("pleme.io/env".into(), json!(env.spec.name));
for (k, v) in &env.spec.labels {
labels.insert(format!("pleme.io/{k}"), json!(v));
}
for (k, v) in &self.extra_labels {
labels.insert(k.clone(), json!(v));
}
json!({
"name": name,
"namespace": self.namespace,
"labels": labels,
})
}
fn render_via_registry(
&self,
env: &Env,
r: &Resource,
meta: &tatara_lisp::RenderHandler,
) -> Result<Manifest, RenderError> {
let name = string_field(&r.value, meta.name_field)
.or_else(|| string_field(&r.value, "name"))
.map(str::to_string)
.unwrap_or_else(|| {
format!("{}-{}", env.spec.name, meta.kind.to_lowercase())
});
let manifest = json!({
"apiVersion": meta.api_version,
"kind": meta.kind,
"metadata": self.metadata(env, &name),
"spec": &r.value,
});
let dir = meta.kind.to_lowercase();
let path = format!("{dir}/{name}.yaml");
Ok(Manifest {
kind: "yaml".into(),
path,
content: yaml_string(&manifest)?,
})
}
fn render_bpf_configmap(&self, env: &Env, r: &Resource) -> Result<Manifest, RenderError> {
let name = string_field(&r.value, "name").unwrap_or("bpf");
let cm_name = format!("bpf-{}-{}", r.keyword.trim_start_matches("def"), name);
let payload = serde_json::to_string_pretty(&r.value)
.map_err(|e| RenderError::Yaml(format!("bpf json: {e}")))?;
let manifest = json!({
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": self.metadata(env, &cm_name),
"data": {
"spec.json": payload,
},
});
Ok(Manifest {
kind: "yaml".into(),
path: format!("bpf/{cm_name}.yaml"),
content: yaml_string(&manifest)?,
})
}
}
fn string_field<'a>(v: &'a Value, key: &str) -> Option<&'a str> {
v.as_object()?.get(key)?.as_str()
}
fn yaml_string(v: &Value) -> Result<String, RenderError> {
let yaml = serde_yaml_ng::to_string(v).map_err(|e| RenderError::Yaml(e.to_string()))?;
let mut out = String::new();
let _ = writeln!(out, "---");
out.push_str(&yaml);
Ok(out)
}