Skip to content

Commit

Permalink
Serialize API errors as JSON
Browse files Browse the repository at this point in the history
  • Loading branch information
kklingenberg committed Dec 6, 2023
1 parent ca69585 commit df1f72e
Show file tree
Hide file tree
Showing 7 changed files with 67 additions and 12 deletions.
2 changes: 1 addition & 1 deletion Cargo.lock

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

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "k8s-job-dispatcher"
version = "0.4.0"
version = "0.4.1"
edition = "2021"

[dependencies]
Expand Down
2 changes: 1 addition & 1 deletion chart/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ name: job-dispatcher
description: An HTTP API for creating jobs based on a predefined template.
type: application
version: 0.1.0
appVersion: "v0.4.0"
appVersion: "v0.4.1"
47 changes: 47 additions & 0 deletions src/api_error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
//! Provides an error type for API responses.
use actix_web::{http::StatusCode, HttpResponse, ResponseError};
use serde::Serialize;
use serde_json::{json, to_string_pretty};
use std::fmt::{Display, Formatter, Result};

/// An error serialized as JSON and sent as a response.
#[derive(Debug, Serialize)]
pub struct APIError {
status: u16,
msg: String,
}

impl APIError {
fn new<S: ToString>(status: u16, msg: S) -> Self {
Self {
status,
msg: msg.to_string(),
}
}

pub fn bad_request<S: ToString>(msg: S) -> Self {
Self::new(400, msg)
}

pub fn bad_gateway<S: ToString>(msg: S) -> Self {
Self::new(502, msg)
}

pub fn not_found<S: ToString>(msg: S) -> Self {
Self::new(404, msg)
}
}

impl Display for APIError {
fn fmt(&self, f: &mut Formatter) -> Result {
write!(f, "{}", to_string_pretty(self).unwrap())
}
}

impl ResponseError for APIError {
fn error_response(&self) -> HttpResponse {
let err_json = json!({ "error": { "code": self.status, "message": self.msg }});
HttpResponse::build(StatusCode::from_u16(self.status).unwrap()).json(err_json)
}
}
2 changes: 1 addition & 1 deletion src/default_filter.jq
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
imagePullSecrets: [$ENV.JOB_IMAGE_PULL_SECRET | strings | {name: .}],
containers: [{
name: ($ENV.JOB_NAME_PREFIX // "demo-job-") | rtrimstr("-"),
image: $ENV.JOB_IMAGE // "busybox:latest",
image: $ENV.JOB_IMAGE // "debian:stable-slim",
command: [$ENV.JOB_COMMAND // "echo"],
args: .args,
env: [
Expand Down
15 changes: 8 additions & 7 deletions src/k8s_service.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
//! Implements the creation and retrieval of K8s jobs.
use crate::api_error::APIError;
use crate::jq;
use crate::state;

use actix_web::{error, get, routes, web, HttpResponse, Responder, Result};
use actix_web::{get, routes, web, HttpResponse, Responder, Result};
use k8s_openapi::api::batch::v1::{Job, JobStatus};
use kube::{core::params::PostParams, Error};
use serde::{Deserialize, Serialize};
Expand Down Expand Up @@ -37,11 +38,11 @@ async fn create_job(
let path = path.strip_suffix('/').map(String::from).unwrap_or(path);
debug!("Job creation request at {:?}: {:?}", path, body);
let raw_manifest = jq::first_result(&state.filter, body.into_inner(), &path)
.ok_or_else(|| error::ErrorBadRequest("Filter didn't produce results"))?
.map_err(|e| error::ErrorBadRequest(format!("Filter failed: {:?}", e)))?;
.ok_or_else(|| APIError::bad_request("Filter didn't produce results"))?
.map_err(|e| APIError::bad_request(format!("Filter failed: {:?}", e)))?;
debug!("Job raw manifest: {:?}", raw_manifest);
let manifest: Job = serde_json::from_value(raw_manifest)
.map_err(|e| error::ErrorBadRequest(format!("Generated manifest is invalid: {:?}", e)))?;
.map_err(|e| APIError::bad_request(format!("Generated manifest is invalid: {:?}", e)))?;
debug!("Job manifest: {:?}", manifest);
let job_opt = state
.k8s_jobs
Expand All @@ -50,7 +51,7 @@ async fn create_job(
.map_or_else(
|e| match e {
Error::Api(response) if response.code == 409 => Ok(None),
_ => Err(error::ErrorBadRequest(format!(
_ => Err(APIError::bad_request(format!(
"K8s server rejected job manifest: {:?}",
e
))),
Expand Down Expand Up @@ -95,8 +96,8 @@ async fn get_job(
.k8s_jobs
.get_opt(&id)
.await
.map_err(error::ErrorBadGateway)?
.ok_or_else(|| error::ErrorNotFound("The specified job doesn't exist"))?;
.map_err(APIError::bad_gateway)?
.ok_or_else(|| APIError::not_found("The specified job doesn't exist"))?;
info!(
"Fetched job with ID {:?}",
job.metadata
Expand Down
9 changes: 8 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
mod api_error;
mod health_service;
mod jq;
mod k8s_service;
mod state;

use actix_web::{middleware, web, App, HttpServer};
use actix_web::{middleware, web, App, Error, HttpResponse, HttpServer, Result as RouteResult};
use clap::Parser;
use k8s_openapi::api::batch::v1::Job;
use kube::{api::Api, Client, Config};
Expand Down Expand Up @@ -33,6 +34,11 @@ struct Cli {
log_level: tracing::Level,
}

/// Default 404 response
async fn no_route() -> RouteResult<HttpResponse> {
Err::<_, Error>(api_error::APIError::not_found("Route not found").into())
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
let cli = Cli::parse();
Expand Down Expand Up @@ -74,6 +80,7 @@ async fn main() -> std::io::Result<()> {
.service(health_service::readiness_check)
.service(k8s_service::create_job)
.service(k8s_service::get_job)
.default_service(web::route().to(no_route))
})
.bind(("0.0.0.0", cli.port))?
.run()
Expand Down

0 comments on commit df1f72e

Please sign in to comment.