Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: full implementation of AdmissionResponse object #559

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading