Skip to content

Commit 1b2da79

Browse files
committed
feat: Add flux-converter :)
1 parent f029ed6 commit 1b2da79

File tree

14 files changed

+484
-29
lines changed

14 files changed

+484
-29
lines changed

Cargo.lock

Lines changed: 10 additions & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,3 +87,7 @@ rsa.opt-level = 3
8787
[profile.dev.package]
8888
insta.opt-level = 3
8989
similar.opt-level = 3
90+
91+
[patch.crates-io]
92+
# https://github.com/kube-rs/kube/pull/1759 will be in 1.1.0
93+
kube = { git = 'https://github.com/sbernauer/kube.git', branch = "fix/derive-conversion-types-0.99" }

crates/stackable-versioned-macros/Cargo.toml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,9 @@ normal = ["k8s-openapi", "kube"]
2525
proc-macro = true
2626

2727
[features]
28-
full = ["k8s"]
29-
k8s = ["dep:kube", "dep:k8s-openapi"]
28+
full = ["k8s", "flux-converter"]
29+
k8s = ["dep:kube", "dep:k8s-openapi", "dep:snafu"]
30+
flux-converter = ["k8s"]
3031

3132
[dependencies]
3233
k8s-version = { path = "../k8s-version", features = ["darling"] }
@@ -37,6 +38,7 @@ itertools.workspace = true
3738
k8s-openapi = { workspace = true, optional = true }
3839
kube = { workspace = true, optional = true }
3940
proc-macro2.workspace = true
41+
snafu = { workspace = true, optional = true }
4042
syn.workspace = true
4143
quote.workspace = true
4244

crates/stackable-versioned-macros/src/codegen/container/mod.rs

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -84,28 +84,40 @@ impl Container {
8484
}
8585
}
8686

87-
/// Generates Kubernetes specific code to merge two or more CRDs into one.
87+
/// Generates Kubernetes specific code to merge two CRDs or convert between different versions.
8888
///
8989
/// This function only returns `Some` if it is a struct. Enums cannot be used to define
9090
/// Kubernetes custom resources.
91-
pub(crate) fn generate_kubernetes_merge_crds(
91+
pub(crate) fn generate_kubernetes_code(
9292
&self,
9393
enum_variant_idents: &[IdentString],
9494
enum_variant_strings: &[String],
9595
fn_calls: &[TokenStream],
9696
vis: &Visibility,
9797
is_nested: bool,
9898
) -> Option<TokenStream> {
99-
match self {
100-
Container::Struct(s) => s.generate_kubernetes_merge_crds(
101-
enum_variant_idents,
102-
enum_variant_strings,
103-
fn_calls,
104-
vis,
105-
is_nested,
106-
),
107-
Container::Enum(_) => None,
108-
}
99+
let Container::Struct(s) = self else {
100+
return None;
101+
};
102+
103+
let mut tokens = TokenStream::new();
104+
tokens.extend(s.generate_kubernetes_merge_crds(
105+
enum_variant_idents,
106+
enum_variant_strings,
107+
fn_calls,
108+
vis,
109+
is_nested,
110+
));
111+
112+
#[cfg(feature = "flux-converter")]
113+
tokens.extend(super::flux_converter::generate_kubernetes_conversion(
114+
&s.common.idents.kubernetes,
115+
&s.common.idents.original,
116+
enum_variant_idents,
117+
enum_variant_strings,
118+
));
119+
120+
Some(tokens)
109121
}
110122

111123
pub(crate) fn get_original_ident(&self) -> &Ident {
@@ -214,7 +226,7 @@ impl StandaloneContainer {
214226
});
215227
}
216228

217-
tokens.extend(self.container.generate_kubernetes_merge_crds(
229+
tokens.extend(self.container.generate_kubernetes_code(
218230
&kubernetes_enum_variant_idents,
219231
&kubernetes_enum_variant_strings,
220232
&kubernetes_merge_crds_fn_calls,

crates/stackable-versioned-macros/src/codegen/container/struct.rs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,8 @@ impl Struct {
349349
vis: &Visibility,
350350
is_nested: bool,
351351
) -> Option<TokenStream> {
352+
assert_eq!(enum_variant_idents.len(), enum_variant_strings.len());
353+
352354
match &self.common.options.kubernetes_options {
353355
Some(kubernetes_options) if !kubernetes_options.skip_merged_crd => {
354356
let enum_ident = &self.common.idents.kubernetes;
@@ -377,12 +379,27 @@ impl Struct {
377379
}
378380
}
379381

382+
#automatically_derived
383+
impl ::std::str::FromStr for #enum_ident {
384+
type Err = stackable_versioned::UnknownResourceVersionError;
385+
386+
fn from_str(version: &str) -> Result<Self, Self::Err> {
387+
match version {
388+
#(#enum_variant_strings => Ok(Self::#enum_variant_idents),)*
389+
_ => Err(stackable_versioned::UnknownResourceVersionError{version: version.to_string()}),
390+
}
391+
}
392+
}
393+
380394
#automatically_derived
381395
impl #enum_ident {
382396
/// Generates a merged CRD containing all versions and marking `stored_apiversion` as stored.
383397
pub fn merged_crd(
384398
stored_apiversion: Self
385-
) -> ::std::result::Result<#k8s_openapi_path::apiextensions_apiserver::pkg::apis::apiextensions::v1::CustomResourceDefinition, #kube_core_path::crd::MergeError> {
399+
) -> ::std::result::Result<
400+
#k8s_openapi_path::apiextensions_apiserver::pkg::apis::apiextensions::v1::CustomResourceDefinition,
401+
#kube_core_path::crd::MergeError
402+
> {
386403
#kube_core_path::crd::merge_crds(vec![#(#fn_calls),*], &stored_apiversion.to_string())
387404
}
388405
}
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
use darling::util::IdentString;
2+
use proc_macro2::TokenStream;
3+
use quote::quote;
4+
5+
pub(crate) fn generate_kubernetes_conversion(
6+
enum_ident: &IdentString,
7+
struct_ident: &IdentString,
8+
enum_variant_idents: &[IdentString],
9+
enum_variant_strings: &[String],
10+
) -> Option<TokenStream> {
11+
assert_eq!(enum_variant_idents.len(), enum_variant_strings.len());
12+
13+
let versions = enum_variant_idents
14+
.iter()
15+
.zip(enum_variant_strings)
16+
.collect::<Vec<_>>();
17+
let conversion_chain = generate_conversion_chain(versions);
18+
19+
let matches = conversion_chain.into_iter().map(
20+
|((src, src_lower), (dst, _dst_lower), version_chain)| {
21+
let version_chain_string = version_chain.iter()
22+
.map(|(_,v)| v.parse::<TokenStream>()
23+
.expect("The versions always needs to be a valid TokenStream"));
24+
25+
// TODO: Is there a bit more clever way how we can get this?
26+
let src_lower = src_lower.parse::<TokenStream>().expect("The versions always needs to be a valid TokenStream");
27+
28+
quote! { (Self::#src, Self::#dst) => {
29+
let resource: #src_lower::#struct_ident = serde_json::from_value(object_spec)
30+
.expect(&format!("Failed to deserialize {}", stringify!(#enum_ident)));
31+
32+
#(
33+
let resource: #version_chain_string::#struct_ident = resource.into();
34+
)*
35+
36+
converted.push(
37+
serde_json::to_value(resource).expect(&format!("Failed to serialize {}", stringify!(#enum_ident)))
38+
);
39+
}}
40+
},
41+
);
42+
43+
Some(quote! {
44+
#[automatically_derived]
45+
impl #enum_ident {
46+
pub fn convert(review: kube::core::conversion::ConversionReview) -> kube::core::conversion::ConversionResponse {
47+
let request = kube::core::conversion::ConversionRequest::from_review(review)
48+
.unwrap();
49+
let desired_api_version = <Self as std::str::FromStr>::from_str(&request.desired_api_version)
50+
.expect(&format!("invalid desired version for {} resource", stringify!(#enum_ident)));
51+
52+
let mut converted: Vec<serde_json::Value> = Vec::with_capacity(request.objects.len());
53+
for object in &request.objects {
54+
let object_spec = object
55+
.get("spec")
56+
.expect("The passed object had no spec")
57+
.clone();
58+
let kind = object
59+
.get("kind")
60+
.expect("The objected asked to convert has no kind");
61+
let api_version = object
62+
.get("apiVersion")
63+
.expect("The objected asked to convert has no apiVersion")
64+
.as_str()
65+
.expect("The apiVersion of the objected asked to convert wasn't a String");
66+
67+
assert_eq!(kind, stringify!(#enum_ident));
68+
69+
let current_api_version = <Self as std::str::FromStr>::from_str(api_version)
70+
.expect(&format!("invalid current version for {} resource", stringify!(#enum_ident)));
71+
72+
match (&current_api_version, &desired_api_version) {
73+
#(#matches),*
74+
}
75+
}
76+
77+
let response = kube::core::conversion::ConversionResponse::for_request(request);
78+
response.success(converted)
79+
}
80+
}
81+
})
82+
}
83+
84+
pub fn generate_conversion_chain<Version: Clone>(
85+
versions: Vec<Version>,
86+
) -> Vec<(Version, Version, Vec<Version>)> {
87+
let mut result = Vec::with_capacity(versions.len().pow(2));
88+
let n = versions.len();
89+
90+
for i in 0..n {
91+
for j in 0..n {
92+
let source = versions[i].clone();
93+
let destination = versions[j].clone();
94+
let chain = if i == j {
95+
vec![]
96+
} else if i < j {
97+
versions[i + 1..=j].to_vec()
98+
} else {
99+
versions[j..i].iter().rev().cloned().collect()
100+
};
101+
result.push((source, destination, chain));
102+
}
103+
}
104+
105+
result
106+
}
107+
108+
#[cfg(test)]
109+
mod tests {
110+
use super::generate_conversion_chain;
111+
112+
#[test]
113+
fn test_generate_conversion_chains() {
114+
let versions = vec!["v1alpha1", "v1alpha2", "v1beta1", "v1", "v2"];
115+
let conversion_chain = generate_conversion_chain(versions);
116+
117+
assert_eq!(conversion_chain, vec![
118+
("v1alpha1", "v1alpha1", vec![]),
119+
("v1alpha1", "v1alpha2", vec!["v1alpha2"]),
120+
("v1alpha1", "v1beta1", vec!["v1alpha2", "v1beta1"]),
121+
("v1alpha1", "v1", vec!["v1alpha2", "v1beta1", "v1"]),
122+
("v1alpha1", "v2", vec!["v1alpha2", "v1beta1", "v1", "v2"]),
123+
("v1alpha2", "v1alpha1", vec!["v1alpha1"]),
124+
("v1alpha2", "v1alpha2", vec![]),
125+
("v1alpha2", "v1beta1", vec!["v1beta1"]),
126+
("v1alpha2", "v1", vec!["v1beta1", "v1"]),
127+
("v1alpha2", "v2", vec!["v1beta1", "v1", "v2"]),
128+
("v1beta1", "v1alpha1", vec!["v1alpha2", "v1alpha1"]),
129+
("v1beta1", "v1alpha2", vec!["v1alpha2"]),
130+
("v1beta1", "v1beta1", vec![]),
131+
("v1beta1", "v1", vec!["v1"]),
132+
("v1beta1", "v2", vec!["v1", "v2"]),
133+
("v1", "v1alpha1", vec!["v1beta1", "v1alpha2", "v1alpha1"]),
134+
("v1", "v1alpha2", vec!["v1beta1", "v1alpha2"]),
135+
("v1", "v1beta1", vec!["v1beta1"]),
136+
("v1", "v1", vec![]),
137+
("v1", "v2", vec!["v2"]),
138+
("v2", "v1alpha1", vec![
139+
"v1", "v1beta1", "v1alpha2", "v1alpha1"
140+
]),
141+
("v2", "v1alpha2", vec!["v1", "v1beta1", "v1alpha2"]),
142+
("v2", "v1beta1", vec!["v1", "v1beta1"]),
143+
("v2", "v1", vec!["v1"]),
144+
("v2", "v2", vec![])
145+
]);
146+
}
147+
}

crates/stackable-versioned-macros/src/codegen/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ pub(crate) mod container;
1010
pub(crate) mod item;
1111
pub(crate) mod module;
1212

13+
#[cfg(feature = "flux-converter")]
14+
pub(crate) mod flux_converter;
15+
1316
#[derive(Debug)]
1417
pub(crate) struct VersionDefinition {
1518
/// Indicates that the container version is deprecated.

crates/stackable-versioned-macros/src/codegen/module.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,7 @@ impl Module {
221221
kubernetes_enum_variant_strings,
222222
)) = kubernetes_container_items.get(container.get_original_ident())
223223
{
224-
kubernetes_tokens.extend(container.generate_kubernetes_merge_crds(
224+
kubernetes_tokens.extend(container.generate_kubernetes_code(
225225
kubernetes_enum_variant_idents,
226226
kubernetes_enum_variant_strings,
227227
kubernetes_merge_crds_fn_calls,

crates/stackable-versioned/Cargo.toml

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,25 @@ repository.workspace = true
1111
all-features = true
1212

1313
[features]
14-
full = ["k8s"]
14+
full = ["k8s", "flux-converter"]
1515
k8s = [
1616
"stackable-versioned-macros/k8s", # Forward the k8s feature to the underlying macro crate
1717
]
18+
flux-converter = [
19+
"k8s",
20+
"stackable-versioned-macros/flux-converter",
21+
"dep:kube",
22+
"dep:k8s-openapi",
23+
"dep:serde",
24+
"dep:schemars",
25+
"dep:serde_json",
26+
]
1827

1928
[dependencies]
2029
stackable-versioned-macros = { path = "../stackable-versioned-macros" }
30+
31+
kube = { workspace = true, optional = true }
32+
k8s-openapi = { workspace = true, optional = true }
33+
serde = { workspace = true, optional = true }
34+
schemars = { workspace = true, optional = true }
35+
serde_json = { workspace = true, optional = true }

0 commit comments

Comments
 (0)