use serde::Deserialize;
use serde_yaml::{self, Value, Mapping};
use std::path::PathBuf;
use std::collections::HashMap;
use std::hash::{Hash, Hasher};
#[derive(Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
#[serde(deny_unknown_fields)]
pub struct Karbonfile {
pub api_version: String,
pub kind: String,
#[serde(default)]
pub resources: HashMap<Resource, ()>,
#[serde(default)]
pub generators: Vec<Generator>,
#[serde(default)]
pub transformations: Vec<Transformation>,
}
#[derive(Deserialize, Clone, Debug)]
#[serde(rename_all = "lowercase")]
#[serde(deny_unknown_fields)]
pub enum Resource {
Files(PathBuf),
Karbonfile(PathBuf),
//Git { path: PathBuf },
}
impl Resource {
pub fn path(&self) -> PathBuf {
use Resource::*;
match self {
Files(p) => { p.clone() }
Karbonfile(p) => { p.clone() }
}
}
}
impl PartialEq for Resource {
fn eq(&self, other: &Self) -> bool {
self.path() == other.path()
}
}
impl Eq for Resource {}
impl Hash for Resource {
fn hash<H: Hasher>(&self, state: &mut H) {
use Resource::*;
match self {
Files(p) => p.hash(state),
Karbonfile(p) => p.hash(state),
}
}
}
#[derive(Deserialize, Clone, Debug)]
#[serde(deny_unknown_fields)]
#[serde(rename_all = "camelCase")]
pub enum Generator {
ConfigMap,
}
#[derive(Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct ResourceType {
pub api_version: String,
pub kind: String,
}
#[derive(Clone, Debug)]
pub struct TransformationSet {
pub path: Option<PathBuf>,
pub original: Option<Value>,
pub transformed: Option<Value>,
pub transformations: OrderedMap,
}
impl TransformationSet {
pub fn new() -> TransformationSet {
TransformationSet {
path: None,
original: None,
transformed: None,
transformations: OrderedMap::new(),
}
}
pub fn set_path(&mut self, p: PathBuf) {
self.path = Some(p);
}
pub fn set_original(&mut self, r: Value) {
self.original = Some(r);
}
fn set_transformed(&mut self, r: Value) {
self.transformed = Some(r);
}
pub fn add_transformation(&mut self, p: PathBuf, t: Vec<Transformation>) {
self.transformations.push(p, t);
}
pub fn apply_transformations(&mut self) {
if let Some(mut result) = self.original.to_owned() {
for k in self.transformations.keys() {
if let Some(ts) = self.transformations.get(&k) {
for t in ts {
if let Some(f) = t.filter {
if !is_filter_match(&result, &f) {
// if the filter is provided but doesn't match
// stop processing this transformation and
// move to the next one.
break;
}
}
// This will run unless the filter is provided and doesn't match
// labels: Option<HashMap<String, String>>,
// add: Option<serde_yaml::Mapping>,
// replace: Option<serde_yaml::Mapping>,
// remove: Option<serde_yaml::Mapping>,
// name_prefix: Option<String>,
// name_suffix: Option<String>,
if let Some(labels) = t.labels {
//for path in label_fields {
// for (k, v) in labels.iter() {
// *pointer_mut(&mut result, &format!("{path}/{k}")).unwrap() = Value::String(v.into());
// }
//}
}
if let Some(add) = t.add {
for (k, v) in add.iter() {
let updated = pointer_mut(&mut result, k.as_str().unwrap()).unwrap();
*updated = v.to_owned();
}
}
if let Some(replace) = t.replace {
}
if let Some(remove) = t.remove {
}
if let Some(prefix) = t.name_prefix {
if let Value::String(current) = &result["metadata"]["name"] {
let new = format!("{prefix}{current}");
result["metadata"]["name"] = Value::String(new.into());
}
}
if let Some(suffix) = t.name_suffix {
if let Value::String(current) = &result["metadata"]["name"] {
let new = format!("{current}{suffix}");
result["metadata"]["name"] = Value::String(new.into());
}
}
}
}
}
self.transformed = Some(result);
}
}
}
fn is_filter_match(doc: &Value, filter: &Mapping) -> bool {
for (field, value) in filter.iter() {
if let Value::String(f) = field {
if pointer(doc, f).unwrap() != value {
return false;
}
}
}
true
}
fn pointer<'a>(doc: &'a Value, pointer: &'a str) -> Option<&'a Value> {
// Taken from serde JSON
if pointer.is_empty() {
return Some(doc);
}
if !pointer.starts_with('/') {
return None;
}
pointer
.split('/')
.skip(1)
.map(|x| x.replace("~1", "/").replace("~0", "~"))
.try_fold(doc, |target, token| {
match target {
Value::Mapping(map) => {
let t: Value = serde_yaml::from_str(&token).unwrap();
map.get(&t)
}
Value::Sequence(seq) => {
let t: usize = token.parse().unwrap();
seq.get(t)
}
_ => None,
}
})
}
fn pointer_mut<'a>(doc: &'a mut Value, pointer: &'a str) -> Option<&'a mut Value> {
// Taken from serde JSON
if pointer.is_empty() {
return Some(doc);
}
if !pointer.starts_with('/') {
return None;
}
pointer
.split('/')
.skip(1)
.map(|x| x.replace("~1", "/").replace("~0", "~"))
.try_fold(doc, |target, token| {
match target {
Value::Mapping(map) => {
let t: Value = serde_yaml::from_str(&token).unwrap();
map.get_mut(&t)
}
Value::Sequence(seq) => {
let t: usize = token.parse().unwrap();
seq.get_mut(t)
}
_ => None,
}
})
}
#[derive(Clone, Debug)]
pub struct OrderedMap {
index: Vec<PathBuf>,
map: HashMap<PathBuf, Vec<Transformation>>,
}
impl OrderedMap {
pub fn new() -> OrderedMap {
OrderedMap {
index: Vec::new(),
map: HashMap::new(),
}
}
pub fn push(&mut self, k: PathBuf, v: Vec<Transformation>) {
self.map.entry(k.clone()).or_insert(v);
self.index.push(k)
}
fn to_vec(&self) -> Vec<(PathBuf, Vec<Transformation>)> {
self.index.iter()
.map( |k| self.map.get_key_value(k).unwrap() )
.map( |(k, v)| (k.to_owned(), v.to_owned()) )
.collect()
}
fn keys(&self) -> Vec<PathBuf> {
self.index.clone()
}
fn get(&self, k: &PathBuf) -> Option<Vec<Transformation>> {
self.map.get(k).map(|v| v.to_owned())
}
}
#[derive(Deserialize, Clone, Debug)]
#[serde(deny_unknown_fields)]
#[serde(rename_all = "camelCase")]
pub struct Transformation {
#[serde(default)]
filter: Option<Mapping>,
#[serde(default)]
labels: Option<HashMap<String, String>>,
#[serde(default)]
add: Option<Mapping>,
#[serde(default)]
replace: Option<Mapping>,
#[serde(default)]
remove: Option<Mapping>,
#[serde(default)]
name_prefix: Option<String>,
#[serde(default)]
name_suffix: Option<String>,
}
const label_fields: [&'static str; 30] = [
"/metadata/labels",
"/spec/selector",
"/spec/template/metadata/labels",
"/spec/selector/matchLabels",
"/spec/template/metadata/labels",
"/spec/template/spec/affinity/podAffinity/preferredDuringSchedulingIgnoredDuringExecution/podAffinityTerm/labelSelector/matchLabels",
"/spec/template/spec/affinity/podAffinity/requiredDuringSchedulingIgnoredDuringExecution/labelSelector/matchLabels",
"/spec/template/spec/affinity/podAntiAffinity/preferredDuringSchedulingIgnoredDuringExecution/podAffinityTerm/labelSelector/matchLabels",
"/spec/template/spec/affinity/podAntiAffinity/requiredDuringSchedulingIgnoredDuringExecution/labelSelector/matchLabels",
"/spec/template/spec/topologySpreadConstraints/labelSelector/matchLabels",
"/spec/selector/matchLabels",
"/spec/template/metadata/labels",
"/spec/selector/matchLabels",
"/spec/template/metadata/labels",
"/spec/selector/matchLabels",
"/spec/template/metadata/labels",
"/spec/template/spec/affinity/podAffinity/preferredDuringSchedulingIgnoredDuringExecution/podAffinityTerm/labelSelector/matchLabels",
"/spec/template/spec/affinity/podAffinity/requiredDuringSchedulingIgnoredDuringExecution/labelSelector/matchLabels",
"/spec/template/spec/affinity/podAntiAffinity/preferredDuringSchedulingIgnoredDuringExecution/podAffinityTerm/labelSelector/matchLabels",
"/spec/template/spec/affinity/podAntiAffinity/requiredDuringSchedulingIgnoredDuringExecution/labelSelector/matchLabels",
"/spec/template/spec/topologySpreadConstraints/labelSelector/matchLabels",
//"/spec/volumeClaimTemplates[]/metadata/labels",
"/spec/selector/matchLabels",
"/spec/template/metadata/labels",
"/spec/jobTemplate/spec/selector/matchLabels",
"/spec/jobTemplate/metadata/labels",
"/spec/jobTemplate/spec/template/metadata/labels",
"/spec/selector/matchLabels",
"/spec/podSelector/matchLabels",
"/spec/ingress/from/podSelector/matchLabels",
"/spec/egress/to/podSelector/matchLabels",
];