Skip to content

Commit

Permalink
impr: Implement errors.rs
Browse files Browse the repository at this point in the history
Signed-off-by: Eden Reich <[email protected]>
  • Loading branch information
edenreich committed Nov 27, 2024
1 parent 46e512f commit 82a727f
Show file tree
Hide file tree
Showing 23 changed files with 293 additions and 333 deletions.
16 changes: 0 additions & 16 deletions cli/src/commands/generate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -327,7 +327,6 @@ fn get_controller_attributes_for_operation(
let attributes = ControllerAttributes {
operation_id: operation_id.to_string().to_snake_case(),
http_method: http_method.to_string(),
action_summary: operation.summary.clone().unwrap_or_default().to_lowercase(),
};

Some((tag.clone(), attributes))
Expand Down Expand Up @@ -415,18 +414,6 @@ fn generate_controller(
return Ok(());
}

let has_create_action = controller_attributes
.iter()
.any(|controller| controller.http_method == "post");

let has_update_action = controller_attributes
.iter()
.any(|controller| controller.http_method == "put");

let has_delete_action = controller_attributes
.iter()
.any(|controller| controller.http_method == "delete");

let type_name = uppercase_first_letter(&tag.to_singular());

let fields = get_fields_for_type(schemas, &type_name, &resource_remote_ref)?;
Expand All @@ -437,9 +424,6 @@ fn generate_controller(
kind_struct: type_name.clone(),
dto_fields: fields,
resource_remote_ref: resource_remote_ref.clone(),
has_create_action,
has_update_action,
has_delete_action,
api_url: "http://localhost:8080".to_string(),
}
.render()?;
Expand Down
7 changes: 6 additions & 1 deletion cli/src/commands/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use crate::templates::{
Taskfile,
},
operator::Main as OperatorMain,
operator::{Cli, Lib},
operator::{Cli, Errors, Lib},
tests::{Main as TestsMain, UtilsClient, UtilsCluster, UtilsOperator},
};
use crate::utils::{
Expand Down Expand Up @@ -134,6 +134,11 @@ pub fn execute(conf: Config, path: &String) -> Result<(), AppError> {
base_path.join(K8S_OPERATOR_DIR).join("src").as_path(),
"cli.rs",
)?;
generate_template_file(
Errors {},
base_path.join(K8S_OPERATOR_DIR).join("src").as_path(),
"errors.rs",
)?;

// Generate root files
generate_template_file(Dockerignore {}, base_path, ".dockerignore")?;
Expand Down
8 changes: 4 additions & 4 deletions cli/src/templates.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ pub struct Field {
pub struct ControllerAttributes {
pub operation_id: String,
pub http_method: String,
pub action_summary: String,
}

// Tests Templates
Expand Down Expand Up @@ -172,6 +171,10 @@ pub mod operator {
pub author: String,
}

#[derive(Template)]
#[template(path = "operator/errors.rs.jinja")]
pub struct Errors {}

#[derive(Template)]
#[template(path = "operator/controller.rs.jinja")]
pub struct Controller {
Expand All @@ -180,9 +183,6 @@ pub mod operator {
pub kind_struct: String,
pub dto_fields: Vec<Field>,
pub resource_remote_ref: String,
pub has_create_action: bool,
pub has_update_action: bool,
pub has_delete_action: bool,
pub api_url: String,
}

Expand Down
2 changes: 1 addition & 1 deletion cli/templates/operator/cli.rs.jinja
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/// This file is generated by kopgen. Do not edit manually. If you need to make adjustments add it to .openapi-generator-ignore file.
// This file is generated by kopgen. Do not edit manually. If you need to make adjustments add it to .openapi-generator-ignore file.
use clap::{Parser, Subcommand};

/// Command-line interface for the operator.
Expand Down
138 changes: 38 additions & 100 deletions cli/templates/operator/controller.rs.jinja
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
/// This file is generated by kopgen. Do not edit manually. If you need to make adjustments add it to .openapi-generator-ignore file.
use anyhow::{Context, Result};
// This file is generated by kopgen. Do not edit manually. If you need to make adjustments add it to .openapi-generator-ignore file.
use std::{sync::Arc, time::Duration};

use futures::StreamExt;
use kube::api::{Api, PostParams, Resource};
use thiserror::Error;

use kube_runtime::{controller::Action, watcher, Controller};
use log::{error, info, warn};
use std::{sync::Arc, time::Duration};

use openapi::{
apis::{
Expand All @@ -21,30 +19,28 @@ use openapi::{
models::{{ kind_struct }} as {{ kind_struct }}Dto,
};

use crate::types::{{ arg_name }}::{
{{ kind_struct }},
{{ kind_struct }}Spec,
{{ kind_struct }}Status,
};
use crate::{
add_finalizer,
create_condition,
remove_finalizer,
update_status,
errors::OperatorError,
types::{{ arg_name }}::{
{{ kind_struct }},
{{ kind_struct }}Spec,
{{ kind_struct }}Status,
},
{add_finalizer, create_condition, remove_finalizer, update_status},
};

const REQUEUE_AFTER_IN_SEC: u64 = 30;
const API_URL: &str = "{{ api_url }}";
const API_USER_AGENT: &str = "k8s-operator";

fn convert_uuid_to_string(uuid: Option<uuid::Uuid>) -> Option<String> {
uuid.map(|uuid| uuid.to_string())
fn convert_{{ resource_remote_ref }}_to_string({{ resource_remote_ref }}: Option<{{ resource_remote_ref }}::Uuid>) -> Option<String> {
{{ resource_remote_ref }}.map(|{{ resource_remote_ref }}| {{ resource_remote_ref }}.to_string())
}

fn convert_string_to_uuid(uuid: Option<String>) -> Option<uuid::Uuid> {
match uuid {
Some(uuid) => match uuid::Uuid::parse_str(&uuid) {
Ok(uuid) => Some(uuid),
fn convert_string_to_{{ resource_remote_ref }}({{ resource_remote_ref }}: Option<String>) -> Option<{{ resource_remote_ref }}::Uuid> {
match {{ resource_remote_ref }} {
Some({{ resource_remote_ref }}) => match {{ resource_remote_ref }}::Uuid::parse_str(&{{ resource_remote_ref }}) {
Ok({{ resource_remote_ref }}) => Some({{ resource_remote_ref }}),
Err(_) => None,
},
None => None,
Expand All @@ -53,7 +49,7 @@ fn convert_string_to_uuid(uuid: Option<String>) -> Option<uuid::Uuid> {

fn convert_kube_type_to_dto({{ arg_name }}: {{ kind_struct }}) -> {{ kind_struct }}Dto {
let {{ resource_remote_ref }} = match {{ arg_name }}.status {
Some(status) => convert_string_to_uuid(status.{{ resource_remote_ref }}),
Some(status) => convert_string_to_{{ resource_remote_ref }}(status.{{ resource_remote_ref }}),
None => None,
};
{{ kind_struct }}Dto {
Expand All @@ -76,31 +72,7 @@ struct ExtraArgs {
kube_client: Api<{{ kind_struct }}>,
}

#[derive(Debug, Error)]
enum OperatorError {
#[error(transparent)]
AnyhowError(#[from] anyhow::Error),
// #[error("Failed to process event: {0}")]
// FailedToProcessEvent(#[source] kube::Error),
#[error("Failed to delete a {{ arg_name }}: {0}")]
FailedToDelete{{ kind_struct }}(#[source] anyhow::Error),
// #[error("Failed to update a {{ arg_name }}: {0}")]
// FailedToUpdate{{ kind_struct }}(#[source] anyhow::Error),
// #[error("Failed to create a {{ arg_name }}: {0}")]
// FailedToCreate{{ kind_struct }}(#[source] anyhow::Error),
// #[error("Failed to get a {{ arg_name }}: {0}")]
// FailedToGet{{ kind_struct }}(#[source] anyhow::Error),
#[error("Failed to update status: {0}")]
FailedToUpdateStatus(#[source] anyhow::Error),
// #[error("Failed to remove finalizer: {0}")]
// FailedToRemoveFinalizer(#[source] anyhow::Error),
// #[error("Failed to add finalizer: {0}")]
// FailedToAddFinalizer(#[source] anyhow::Error),
// #[error("Failed to check for drift: {0}")]
// FailedToCheckForDrift(#[source] anyhow::Error),
}

pub async fn handle(kube_client: Api<{{ kind_struct }}>) -> Result<()> {
pub async fn handle(kube_client: Api<{{ kind_struct }}>) -> Result<(), OperatorError> {
info!("Starting the controller");
let controller = Controller::new(kube_client.clone(), watcher::Config::default());

Expand All @@ -126,61 +98,29 @@ pub async fn handle(kube_client: Api<{{ kind_struct }}>) -> Result<()> {
async fn reconcile({{ arg_name }}: Arc<{{ kind_struct }}>, ctx: Arc<ExtraArgs>) -> Result<Action, OperatorError> {
let kube_client = ctx.kube_client.clone();
let mut {{ arg_name }} = {{ arg_name }}.as_ref().clone();
let {{ resource_remote_ref }} = match {{ arg_name }}.clone().status {
Some(status) => status.{{ resource_remote_ref }}.unwrap_or_default(),
None => "".to_string(),
};
let {{ resource_remote_ref }} = {{ arg_name }}
.status
.as_ref()
.and_then(|status| status.{{ resource_remote_ref }}.clone())
.unwrap_or_default();

// Add default stauts if it's missing
if {{ arg_name }}.status.is_none() {
add_default_status(&kube_client, &mut {{ arg_name }}).await?;
}

{% if has_delete_action %}
// If the resource was marked for deletion, we need to delete it
if {{ arg_name }}.meta().deletion_timestamp.is_some() {
handle_delete(&kube_client, &mut {{ arg_name }}, &{{ resource_remote_ref }}).await?;
}
{% else %}
warn!("OpenAPI Spec doesn't have a delete operation implemented for {{ tag }} tag.");
{% endif %}

// If {{ resource_remote_ref }} is empty, we need to create a new resource
if {{ resource_remote_ref }}.is_empty() {
let condition = create_condition(
"Creating",
"ProgressingCreating",
"Creating the resource",
"Resource is being created",
{{ arg_name }}.meta().generation,
);
if let Some(status) = {{ arg_name }}.clone().status.as_mut() {
status.conditions.push(condition);
status.observed_generation = {{ arg_name }}.meta().generation;
}
update_status(&kube_client, {{ arg_name }}.clone()).await?;
{% if has_create_action %}
handle_create(&kube_client, &mut {{ arg_name }}.clone()).await?;
{% else %}
warn!("OpenAPI Spec doesn't have a create operation implemented for {{ tag }} tag.");
{% endif %}
} else {
{% if has_update_action %}
// If the resource was updated in kubernetes, we need to update the remote resource
if {{ arg_name }}.meta().generation != {{ arg_name }}.status.as_ref().unwrap().observed_generation {
handle_update(&kube_client, &mut {{ arg_name }}, &{{ resource_remote_ref }}).await?;
}
{% else %}
warn!("OpenAPI Spec doesn't have an update operation implemented for {{ tag }} tag.");
{% endif %}
} else if {{ resource_remote_ref }}.is_empty() {
handle_create(&kube_client, &mut {{ arg_name }}).await?;
} else if {{ arg_name }}.meta().generation != {{ arg_name }}.status.as_ref().unwrap().observed_generation {
handle_update(&kube_client, &mut {{ arg_name }}, &{{ resource_remote_ref }}).await?;
}

check_for_drift(&kube_client, &mut {{ arg_name }}).await?;

Ok(Action::requeue(Duration::from_secs(REQUEUE_AFTER_IN_SEC)))
}

async fn get_client_config() -> Result<Configuration> {
async fn get_client_config() -> Result<Configuration, OperatorError> {
let config = Configuration {
base_path: API_URL.to_string(),
client: reqwest::Client::new(),
Expand All @@ -191,25 +131,23 @@ async fn get_client_config() -> Result<Configuration> {
Ok(config)
}

async fn add_default_status(
kube_client: &Api<{{ kind_struct }}>,
{{ arg_name }}: &mut {{ kind_struct }},
) -> Result<(), OperatorError> {
let status = {{ kind_struct }}Status {
async fn add_default_status(kube_client: &Api<{{ kind_struct }}>, {{ arg_name }}: &mut {{ kind_struct }}) -> Result<(), OperatorError> {
{{ arg_name }}.status = Some({{ kind_struct }}Status {
conditions: vec![],
{{ resource_remote_ref }}: None,
observed_generation: Some(0),
};
{{ arg_name }}.status = Some(status);
update_status(kube_client, {{ arg_name }}.clone()).await.map_err(OperatorError::FailedToUpdateStatus)
});
update_status(kube_client, {{ arg_name }}.clone())
.await
.map_err(|e| OperatorError::FailedToUpdateStatus(e.into()))
}

pub async fn check_for_drift(
kube_client: &Api<{{ kind_struct }}>,
{{ arg_name }}: &mut {{ kind_struct }},
) -> Result<()> {
) -> Result<(), OperatorError> {
let dto = convert_kube_type_to_dto({{ arg_name }}.clone());
let {{ resource_remote_ref }} = convert_uuid_to_string(dto.{{ resource_remote_ref }}).unwrap_or_default();
let {{ resource_remote_ref }} = convert_{{ resource_remote_ref }}_to_string(dto.{{ resource_remote_ref }}).unwrap_or_default();
let config = get_client_config().await?;

if {{ resource_remote_ref }}.is_empty() {
Expand Down Expand Up @@ -242,14 +180,14 @@ pub async fn check_for_drift(
}
Err(e) => {
error!("Failed to update {{ kind_struct }}: {:?}", e);
return Err(anyhow::anyhow!("Failed to update {{ arg_name }}: {:?}", e));
return Err(OperatorError::FailedToGetResource(e.into()));
}
}
}
}
Err(e) => {
error!("Failed to get {{ kind_struct }}: {:?}", e);
return Err(anyhow::anyhow!("Failed to get {{ arg_name }}: {:?}", e));
return Err(OperatorError::FailedToGetResource(e.into()));
}
}

Expand Down
42 changes: 23 additions & 19 deletions cli/templates/operator/controller_action_create.jinja
Original file line number Diff line number Diff line change
@@ -1,40 +1,44 @@
{% for controller in controllers %}
{% if controller.http_method == "post" %}
pub async fn handle_create(
kube_client: &Api<{{ kind_struct }}>,
{{ arg_name }}: &mut {{ kind_struct }},
) -> Result<(), anyhow::Error> {
pub async fn handle_create(kube_client: &Api<{{ kind_struct }}>, {{ arg_name }}: &mut {{ kind_struct }}) -> Result<(), OperatorError> {
let dto = convert_kube_type_to_dto({{ arg_name }}.clone());
let config = get_client_config().await?;

match {{ controller.operation_id }}(&config, dto.clone()).await {
Ok(remote_{{ arg_name }}) => match remote_{{ arg_name }}.{{ resource_remote_ref }} {
Some({{ resource_remote_ref }}) => {
let uuid = convert_uuid_to_string(Some({{ resource_remote_ref }})).unwrap();
match create_{{ arg_name }}(&config, dto.clone()).await {
Ok(remote_{{ arg_name }}) => {
if let Some({{ resource_remote_ref }}) = remote_{{ arg_name }}.{{ resource_remote_ref }} {
let {{ resource_remote_ref }} = convert_{{ resource_remote_ref }}_to_string(Some({{ resource_remote_ref }})).unwrap();
add_finalizer({{ arg_name }}, kube_client.clone()).await?;
let generation = {{ arg_name }}.meta().generation;
let condition = create_condition(
"Created",
"AvailableCreated",
"Created the resource",
"Resource has been created",
{{ arg_name }}.meta().generation,
generation,
);
let mut {{ arg_name }}_clone = {{ arg_name }}.clone();
if let Some(status) = {{ arg_name }}_clone.status.as_mut() {
status.conditions.push(condition);
status.uuid = Some(uuid);
status.observed_generation = {{ arg_name }}.meta().generation;
if let Some(status) = {{ arg_name }}.status.as_mut() {
if !status
.conditions
.iter()
.any(|c| c.type_ == "AvailableCreated")
{
status.conditions.push(condition);
}
status.{{ resource_remote_ref }} = Some({{ resource_remote_ref }});
status.observed_generation = generation;
}
update_status(kube_client, {{ arg_name }}_clone).await
}
None => {
update_status(kube_client, {{ arg_name }}.clone())
.await
.map_err(|e| OperatorError::FailedToUpdateStatus(e.into()))
} else {
warn!("Remote {{ arg_name }} has no {{ resource_remote_ref }}, cannot update status");
Ok(())
}
},
}
Err(e) => {
error!("Failed to create a new {{ arg_name }}: {:?}", e);
Err(anyhow::anyhow!("Failed to create a new {{ arg_name }}: {:?}", e))
Err(OperatorError::FailedToCreateResource(e.into()))
}
}
}
Expand Down
6 changes: 3 additions & 3 deletions cli/templates/operator/controller_action_delete.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ async fn handle_delete(
return Ok(());
}

if let Err(e) = {{ controller.operation_id }}(&config, {{ resource_remote_ref }}).await {
{{ controller.operation_id }}(&config, {{ resource_remote_ref }}).await.map_err(|e| {
error!("Failed to delete {{ arg_name }}: {:?}", e);
return Err(OperatorError::FailedToDelete{{ kind_struct }}(e.into()));
}
OperatorError::FailedToDeleteResource(e.into())
})?;

remove_finalizer({{ arg_name }}, kube_client.clone()).await?;
info!("Successfully deleted {{ arg_name }}");
Expand Down
Loading

0 comments on commit 82a727f

Please sign in to comment.