Skip to content

Commit

Permalink
Merge pull request #559 from flavio/implement-all-admission-respose-f…
Browse files Browse the repository at this point in the history
…ields

feat: full implementation of AdmissionResponse object
  • Loading branch information
flavio authored Sep 19, 2024
2 parents 6a18781 + 6c2ffcf commit ca32e7f
Show file tree
Hide file tree
Showing 2 changed files with 276 additions and 10 deletions.
277 changes: 269 additions & 8 deletions src/admission_response.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
use std::collections::HashMap;
use std::result::Result;
use crate::errors::ResponseError;

use base64::{engine::general_purpose, Engine as _};
use kubewarden_policy_sdk::response::ValidationResponse as PolicyValidationResponse;
use serde::{Deserialize, Serialize};

use crate::errors::ResponseError;
use std::{collections::HashMap, result::Result};

/// This models the admission/v1/AdmissionResponse object of Kubernetes
/// See https://pkg.go.dev/k8s.io/kubernetes/pkg/apis/admission#AdmissionResponse
#[derive(Serialize, Deserialize, Debug, Default, PartialEq, Eq, Clone)]
#[serde(rename_all = "camelCase")]
pub struct AdmissionResponse {
Expand All @@ -20,7 +19,7 @@ pub struct AdmissionResponse {

/// The type of Patch. Currently we only allow "JSONPatch".
#[serde(skip_serializing_if = "Option::is_none")]
pub patch_type: Option<String>,
pub patch_type: Option<PatchType>,

/// The patch body. Currently we only support "JSONPatch" which implements RFC 6902.
#[serde(skip_serializing_if = "Option::is_none")]
Expand All @@ -44,11 +43,48 @@ pub struct AdmissionResponse {
pub warnings: Option<Vec<String>>,
}

/// PatchType is the type of patch being used to represent the mutated object
#[derive(Serialize, Deserialize, Debug, Default, PartialEq, Eq, Clone)]
pub enum PatchType {
#[serde(rename = "JSONPatch")]
#[default]
JSONPatch,
}

/// Values that Status.Status of an AdmissionResponse can have
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
pub enum AdmissionResponseStatusValue {
Success,
Failure,
}

#[derive(Serialize, Deserialize, Debug, Default, PartialEq, Eq, Clone)]
pub struct AdmissionResponseStatus {
/// Status of the operation.
/// One of: "Success" or "Failure".
/// More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status
#[serde(skip_serializing_if = "Option::is_none")]
pub status: Option<AdmissionResponseStatusValue>,

/// A human-readable description of the status of this operation.
#[serde(skip_serializing_if = "Option::is_none")]
pub message: Option<String>,

/// A machine-readable description of why this operation is in the
/// "Failure" status. If this value is empty there
/// is no information available. A Reason clarifies an HTTP status
/// code but does not override it.
#[serde(skip_serializing_if = "Option::is_none")]
pub reason: Option<StatusReason>,

/// Extended data associated with the reason. Each reason may define its
/// own extended details. This field is optional and the data returned
/// is not guaranteed to conform to any schema except that defined by
/// the reason type.
#[serde(skip_serializing_if = "Option::is_none")]
pub details: Option<StatusDetails>,

/// Suggested HTTP return code for this status
#[serde(skip_serializing_if = "Option::is_none")]
pub code: Option<u16>,
}
Expand All @@ -61,6 +97,7 @@ impl AdmissionResponse {
status: Some(AdmissionResponseStatus {
message: Some(message),
code: Some(code),
..Default::default()
}),
..Default::default()
}
Expand Down Expand Up @@ -88,6 +125,7 @@ impl AdmissionResponse {
status: Some(AdmissionResponseStatus {
message: Some(message.to_string()),
code: None,
..Default::default()
}),
});
}
Expand All @@ -108,8 +146,8 @@ impl AdmissionResponse {
None => None,
};

let patch_type: Option<String> = if patch.is_some() {
Some(String::from("JSONPatch"))
let patch_type: Option<PatchType> = if patch.is_some() {
Some(PatchType::JSONPatch)
} else {
None
};
Expand All @@ -118,6 +156,7 @@ impl AdmissionResponse {
Some(AdmissionResponseStatus {
message: pol_val_resp.message.clone(),
code: pol_val_resp.code,
..Default::default()
})
} else {
None
Expand All @@ -135,6 +174,228 @@ impl AdmissionResponse {
}
}

/// StatusReason is an enumeration of possible failure causes.
/// Each StatusReason must map to a single HTTP status code, but multiple reasons may map to the same
/// HTTP status code.
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
pub enum StatusReason {
/// StatusReasonUnknown means the server has declined to indicate a specific reason.
/// The details field may contain other information about this error.
/// Status code 500.
#[serde(rename = "")]
Unknown,

/// StatusReasonUnauthorized means the server can be reached and understood the request,
/// but requires the user to present appropriate authorization credentials (identified by
/// the WWW-Authenticate header) in order for the action to be completed.
/// Status code 401.
Unauthorized,

/// StatusReasonForbidden means the server can be reached and understood the request, but
/// refuses to take any further action. This is the result of the server being configured to
/// deny access for some reason to the requested resource by the client.
/// Status code 403.
Forbidden,

/// StatusReasonNotFound means one or more resources required for this operation could not
/// be found.
/// Status code 404.
NotFound,

/// StatusReasonAlreadyExists means the resource you are creating already exists.
/// Status code 409.
AlreadyExists,

/// StatusReasonConflict means the requested operation cannot be completed due to a conflict
/// in the operation. The client may need to alter the request.
/// Status code 409.
Conflict,

/// StatusReasonGone means the item is no longer available at the server and no forwarding
/// address is known.
/// Status code 410.
Gone,

/// StatusReasonInvalid means the requested create or update operation cannot be completed
/// due to invalid data provided as part of the request. The client may need to alter the request.
/// Status code 422.
Invalid,

/// StatusReasonServerTimeout means the server can be reached and understood the request,
/// but cannot complete the action in a reasonable time. The client should retry the request.
/// Status code 500.
ServerTimeout,

/// StatusReasonTimeout means that the request could not be completed within the given time.
/// Clients can get this response only when they specified a timeout param in the request.
/// Status code 504.
Timeout,

/// StatusReasonTooManyRequests means the server experienced too many requests within a
/// given window and that the client must wait to perform the action again.
/// Status code 429.
TooManyRequests,

/// StatusReasonBadRequest means that the request itself was invalid.
/// Status code 400.
BadRequest,

/// StatusReasonMethodNotAllowed means that the action the client attempted to perform on
/// the resource was not supported by the code.
/// Status code 405.
MethodNotAllowed,

/// StatusReasonNotAcceptable means that the accept types indicated by the client were not
/// acceptable to the server.
/// Status code 406.
NotAcceptable,

/// StatusReasonRequestEntityTooLarge means that the request entity is too large.
/// Status code 413.
RequestEntityTooLarge,

/// StatusReasonUnsupportedMediaType means that the content type sent by the client is not
/// acceptable to the server.
/// Status code 415.
UnsupportedMediaType,

/// StatusReasonInternalError indicates that an internal error occurred.
/// Status code 500.
InternalError,

/// StatusReasonExpired indicates that the request is invalid because the content has expired
/// and is no longer available.
/// Status code 410.
Expired,

/// StatusReasonServiceUnavailable means that the requested service is unavailable at this time.
/// Retrying the request after some time might succeed.
/// Status code 503.
ServiceUnavailable,
}

/// StatusDetails is a set of additional properties that MAY be set by the server to provide
/// additional information about a response.
/// The Reason field of a Status object defines what attributes will be set.
/// Clients must ignore fields that do not match the defined type of each attribute,
/// and should assume that any attribute may be empty, invalid, or under defined.
#[derive(Serialize, Deserialize, Debug, Default, PartialEq, Eq, Clone)]
pub struct StatusDetails {
/// The name attribute of the resource associated with the status StatusReason
/// (when there is a single name which can be described).
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,

/// The group attribute of the resource associated with the status StatusReason.
#[serde(skip_serializing_if = "Option::is_none")]
pub group: Option<String>,

/// The kind attribute of the resource associated with the status StatusReason.
/// On some operations may differ from the requested resource Kind.
/// More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
#[serde(skip_serializing_if = "Option::is_none")]
pub kind: Option<String>,

/// UID of the resource.
/// (when there is a single resource which can be described).
/// More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#uids
#[serde(skip_serializing_if = "Option::is_none")]
pub uid: Option<String>,

/// The Causes array includes more details associated with the StatusReason
/// failure. Not all StatusReasons may provide detailed causes.
#[serde(skip_serializing_if = "Vec::is_empty")]
pub causes: Vec<StatusCause>,

/// If specified, the time in seconds before the operation should be retried. Some errors may indicate
/// the client must take an alternate action - for those errors this field may indicate how long to wait
/// before taking the alternate action.
#[serde(skip_serializing_if = "Option::is_none")]
pub retry_after_seconds: Option<i32>,
}

///StatusCause provides more information about an api.Status failure, including cases when multiple errors are encountered.
#[derive(Serialize, Deserialize, Debug, Default, PartialEq, Eq, Clone)]
pub struct StatusCause {
// A machine-readable description of the cause of the error. If this value is
// empty there is no information available.
pub reason: Option<CauseType>,

// A human-readable description of the cause of the error. This field may be
// presented as-is to a reader.
pub message: Option<String>,

// The field of the resource that has caused this error, as named by its JSON
// serialization. May include dot and postfix notation for nested attributes.
// Arrays are zero-indexed. Fields may appear more than once in an array of
// causes due to fields having multiple errors.
//
// Examples:
// "name" - the field "name" on the current resource
// "items[0].name" - the field "name" on the first array entry in "items"
pub field: Option<String>,
}

/// CauseType is a machine readable value providing more detail about what occurred in a
/// status response.
/// An operation may have multiple causes for a status (whether Failure or Success).
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
pub enum CauseType {
/// CauseTypeFieldValueNotFound is used to report failure to find a requested value
/// (e.g., looking up an ID).
FieldValueNotFound,

/// CauseTypeFieldValueRequired is used to report required values that are not
/// provided (e.g., empty strings, null values, or empty arrays).
FieldValueRequired,

/// CauseTypeFieldValueDuplicate is used to report collisions of values that must be
/// unique (e.g., unique IDs).
FieldValueDuplicate,

/// CauseTypeFieldValueInvalid is used to report malformed values (e.g., failed regex
/// match).
FieldValueInvalid,

/// CauseTypeFieldValueNotSupported is used to report valid (as per formatting rules)
/// values that cannot be handled (e.g., an enumerated string).
FieldValueNotSupported,

/// CauseTypeForbidden is used to report valid (as per formatting rules)
/// values which would be accepted under some conditions, but which are not
/// permitted by the current conditions (such as security policy).
FieldValueForbidden,

/// CauseTypeTooLong is used to report that the given value is too long.
/// This is similar to ErrorTypeInvalid, but the error will not include the
/// too-long value.
FieldValueTooLong,

/// CauseTypeTooMany is used to report that a given list has too many items.
/// This is similar to FieldValueTooLong, but the error indicates quantity instead of length.
FieldValueTooMany,

/// CauseTypeInternal is used to report other errors that are not related
/// to user input.
InternalError,

/// CauseTypeTypeInvalid is for when the value did not match the schema type for that field.
FieldValueTypeInvalid,

/// CauseTypeUnexpectedServerResponse is used to report when the server responded to the client
/// without the expected return type. The presence of this cause indicates the error may be
/// due to an intervening proxy or the server software malfunctioning.
UnexpectedServerResponse,

/// CauseTypeFieldManagerConflict is used to report when another client claims to manage this field.
/// It should only be returned for a request using server-side apply.
FieldManagerConflict,

/// CauseTypeResourceVersionTooLarge is used to report that the requested resource version
/// is newer than the data observed by the API server, so the request cannot be served.
ResourceVersionTooLarge,
}

#[cfg(test)]
mod tests {
use std::collections::HashMap;
Expand Down Expand Up @@ -293,7 +554,7 @@ mod tests {
assert_eq!(response.uid, uid);
assert!(response.allowed);
assert!(response.status.is_none());
assert_eq!(response.patch_type, Some(String::from("JSONPatch")));
assert_eq!(response.patch_type, Some(PatchType::JSONPatch));

let patch_decoded_str = general_purpose::STANDARD
.decode(response.patch.unwrap())
Expand Down
9 changes: 7 additions & 2 deletions tests/integration_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use core::panic;
use hyper::{Request, Response};
use kube::client::Body;
use kube::Client;
use policy_evaluator::admission_response::PatchType;
use policy_fetcher::oci_client::manifest::OciImageManifest;
use rstest::*;
use serde_json::json;
Expand Down Expand Up @@ -225,13 +226,17 @@ async fn test_policy_evaluator(
assert!(admission_response.status.is_none());
} else {
assert_eq!(
Some(AdmissionResponseStatus { message, code }),
Some(AdmissionResponseStatus {
message,
code,
..Default::default()
}),
admission_response.status
);
}

if mutating {
assert_eq!(Some("JSONPatch".to_owned()), admission_response.patch_type);
assert_eq!(Some(PatchType::JSONPatch), admission_response.patch_type);
assert!(admission_response.patch.is_some());
} else {
assert!(admission_response.patch.is_none());
Expand Down

0 comments on commit ca32e7f

Please sign in to comment.