Skip to content

Commit

Permalink
Generate the examples from OAS
Browse files Browse the repository at this point in the history
  • Loading branch information
edenreich committed Mar 19, 2024
1 parent 8213267 commit c40a793
Show file tree
Hide file tree
Showing 6 changed files with 114 additions and 50 deletions.
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ env_logger = "0.11.3"
reqwest = "0.11.26"

[build-dependencies]
serde = { version = "1.0.160", features = ["derive"] }
serde_json = "1.0.96"
serde_yaml = "0.9.21"
codegen = "0.2.0"
openapiv3 = "2.0.0"
Expand Down
94 changes: 70 additions & 24 deletions build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ use syn::{parse_quote, ItemFn};

const CONTROLLERS_DIR: &str = "src/controllers";
const TYPES_DIR: &str = "src/types";
const API_GROUP: &str = "example.com";

fn main() {
let input = "openapi.yaml";
let lib_file_path: String = "src/lib.rs".to_string();
let api_group = "example.com";

// Read the OpenAPI specification from the YAML file
let mut file = File::open(input).expect("Unable to open file");
Expand All @@ -28,7 +28,7 @@ fn main() {
// Generate lib.rs
let mut scope = Scope::new();
generate_lib_imports(&mut scope);
generate_event_capturing_function(&mut scope, api_group);
generate_event_capturing_function(&mut scope);
write_to_file(lib_file_path.clone(), scope.to_string());
format_file(lib_file_path.clone());

Expand All @@ -46,7 +46,7 @@ fn main() {
// Handle references here if needed
}
openapiv3::ReferenceOr::Item(item) => {
let rust_code = generate_rust_code(&name, api_group, "v1", &item);
let rust_code = generate_rust_code(&name, "v1", &item);
let mut file =
File::create(format!("{}/{}.rs", TYPES_DIR, name.to_lowercase())).unwrap();
write!(file, "{}", rust_code).unwrap();
Expand Down Expand Up @@ -93,9 +93,11 @@ fn main() {
}
}

let schema_names = openapi
let components = openapi
.components
.expect("No components in OpenAPI spec")
.clone()
.expect("No components in OpenAPI spec");
let schema_names = components
.schemas
.keys()
.map(|name| name.to_string())
Expand All @@ -113,10 +115,10 @@ fn main() {
}
resources.pop();

let role_file_content = get_role_file_content(api_group, &resources);
let role_file_content = get_role_file_content(&resources);
write_to_file("manifests/rbac/role.yaml".to_string(), role_file_content);

let cluster_role_file_content = get_cluster_role_file_content(api_group, &resources);
let cluster_role_file_content = get_cluster_role_file_content(&resources);
write_to_file(
"manifests/rbac/clusterrole.yaml".to_string(),
cluster_role_file_content,
Expand Down Expand Up @@ -151,12 +153,62 @@ fn main() {
let crdgen_file_content = get_crdgen_file_content(&schema_names);
write_to_file("src/crdgen.rs".to_string(), crdgen_file_content);
format_file("src/crdgen.rs".to_string());

// Generate examples from OAS
for (name, example) in components.examples {
if let openapiv3::ReferenceOr::Item(example) = example {
let manifest = generate_manifest_from_example(&name, &example);

write_to_file(
format!("manifests/examples/{}.yaml", name.to_lowercase()),
manifest,
)
}
}
}

// Define the structure of a Kubernetes manifest
#[derive(serde::Serialize)]
struct K8sManifest {
api_version: String,
kind: String,
metadata: Metadata,
spec: serde_json::Value,
}

// Define the structure of the metadata field
#[derive(serde::Serialize)]
struct Metadata {
name: String,
namespace: String,
}

fn generate_manifest_from_example(name: &str, example: &openapiv3::Example) -> String {
let mut manifest = String::from("---\n");
if let Some(mut value) = example.value.clone() {
if value.is_object() {
let obj = value.as_object_mut().unwrap();
obj.remove("uuid");
}
let k8s_manifest = K8sManifest {
api_version: format!("{}s.{}/v1", name.to_lowercase(), API_GROUP),
kind: name.to_string(),
metadata: Metadata {
name: "example".to_string(),
namespace: "default".to_string(),
},
spec: value,
};
let yaml_str = serde_yaml::to_string(&k8s_manifest).unwrap();
manifest.push_str(&yaml_str);
}
manifest
}

fn generate_rust_code(name: &str, api_group: &str, api_version: &str, schema: &Schema) -> String {
fn generate_rust_code(name: &str, api_version: &str, schema: &Schema) -> String {
let mut scope = Scope::new();
generate_imports(&mut scope);
generate_struct(&mut scope, name, api_group, api_version, schema);
generate_struct(&mut scope, name, api_version, schema);
scope.to_string()
}

Expand All @@ -167,13 +219,7 @@ fn generate_imports(scope: &mut Scope) {
scope.import("schemars", "JsonSchema");
}

fn generate_struct(
scope: &mut Scope,
name: &str,
api_group: &str,
api_version: &str,
schema: &Schema,
) {
fn generate_struct(scope: &mut Scope, name: &str, api_version: &str, schema: &Schema) {
// Create a new struct with the given name
let mut struct_ = codegen::Struct::new(&format!("{}Spec", name));

Expand All @@ -189,7 +235,7 @@ fn generate_struct(

struct_.attr(&format!(
"kube(group = \"{}\", version = \"{}\", kind = \"{}\", plural = \"{}\", status = \"{}\", namespaced)",
api_group,
API_GROUP,
api_version,
name,
name.to_lowercase() + "s",
Expand Down Expand Up @@ -273,7 +319,7 @@ fn generate_lib_imports(scope: &mut Scope) {
scope.raw("pub mod controllers;");
}

fn generate_event_capturing_function(scope: &mut Scope, api_group: &str) {
fn generate_event_capturing_function(scope: &mut Scope) {
let function: ItemFn = parse_quote! {
pub async fn watch_resource<T>(
kubernetes_api: Api<T>,
Expand Down Expand Up @@ -310,7 +356,7 @@ fn generate_event_capturing_function(scope: &mut Scope, api_group: &str) {
+ core::fmt::Debug
+ 'static,
{
let finalizer = String::from(format!("finalizers.{}", #api_group));
let finalizer = String::from(format!("finalizers.{}", #API_GROUP));
let finalizers = resource.meta_mut().finalizers.get_or_insert_with(Vec::new);
if finalizers.contains(&finalizer) {
debug!("Finalizer already exists");
Expand All @@ -336,7 +382,7 @@ fn generate_event_capturing_function(scope: &mut Scope, api_group: &str) {
pub async fn remove_finalizer<T>(resource: &mut T, kubernetes_api: Api<T>)
where
T: Clone + Serialize + DeserializeOwned + Resource + CustomResourceExt + core::fmt::Debug + 'static {
let finalizer = String::from(format!("finalizers.{}", #api_group));
let finalizer = String::from(format!("finalizers.{}", #API_GROUP));
if let Some(finalizers) = &mut resource.meta_mut().finalizers {
if finalizers.contains(&finalizer) {
finalizers.retain(|f| f != &finalizer);
Expand Down Expand Up @@ -818,7 +864,7 @@ fn uppercase_first_letter(name: &str) -> String {
///
/// Later will be moved to a separate template files
///
fn get_role_file_content(api_group: &str, resources: &str) -> String {
fn get_role_file_content(resources: &str) -> String {
format!(
r#"---
apiVersion: rbac.authorization.k8s.io/v1
Expand Down Expand Up @@ -846,11 +892,11 @@ rules:
- create
- patch
"#,
api_group, resources
API_GROUP, resources
)
}

fn get_cluster_role_file_content(api_group: &str, resources: &str) -> String {
fn get_cluster_role_file_content(resources: &str) -> String {
format!(
r#"---
apiVersion: rbac.authorization.k8s.io/v1
Expand All @@ -871,7 +917,7 @@ rules:
- patch
- delete
"#,
api_group, resources
API_GROUP, resources
)
}

Expand Down
11 changes: 6 additions & 5 deletions manifests/examples/cat.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 6 additions & 5 deletions manifests/examples/dog.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 5 additions & 4 deletions manifests/examples/horse.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

37 changes: 25 additions & 12 deletions openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,25 @@ paths:
'204':
description: No content
components:
examples:
Cat:
value:
uuid: '76523f74-484c-4018-b9bf-1c03b134512a'
name: 'Fluffy'
breed: 'Persian'
age: 3
Dog:
value:
uuid: '4e817fcd-6c9f-4a15-9bc2-eddac59a10d1'
name: 'Fido'
breed: 'Bulldog'
age: 3
Horse:
value:
uuid: '4e817fcd-6c9f-4a15-9bc2-eddac59a10d1'
name: 'Star'
breed: 'Arabian'
age: 5
schemas:
Cat:
type: object
Expand All @@ -296,16 +315,14 @@ components:
properties:
uuid:
type: string
example: '76523f74-484c-4018-b9bf-1c03b134512a'
name:
type: string
example: 'Fluffy'
breed:
type: string
example: 'Persian'
age:
type: integer
example: 3
example:
$ref: '#/components/examples/Cat'
Dog:
type: object
required:
Expand All @@ -315,16 +332,14 @@ components:
properties:
uuid:
type: string
example: '4e817fcd-6c9f-4a15-9bc2-eddac59a10d1'
name:
type: string
example: 'Fido'
breed:
type: string
example: 'Bulldog'
age:
type: integer
example: 3
example:
$ref: '#/components/examples/Dog'
Horse:
type: object
required:
Expand All @@ -334,13 +349,11 @@ components:
properties:
uuid:
type: string
example: '4e817fcd-6c9f-4a15-9bc2-eddac59a10d1'
name:
type: string
example: 'Fido'
breed:
type: string
example: 'Bulldog'
age:
type: integer
example: 3
example:
$ref: '#/components/examples/Horse'

0 comments on commit c40a793

Please sign in to comment.