diff --git a/src/admission_response.rs b/src/admission_response.rs index b54a48a6..407e75de 100644 --- a/src/admission_response.rs +++ b/src/admission_response.rs @@ -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 { @@ -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, + pub patch_type: Option, /// The patch body. Currently we only support "JSONPatch" which implements RFC 6902. #[serde(skip_serializing_if = "Option::is_none")] @@ -44,11 +43,48 @@ pub struct AdmissionResponse { pub warnings: Option>, } +/// 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, + + /// A human-readable description of the status of this operation. #[serde(skip_serializing_if = "Option::is_none")] pub message: Option, + /// 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, + + /// 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, + + /// Suggested HTTP return code for this status #[serde(skip_serializing_if = "Option::is_none")] pub code: Option, } @@ -61,6 +97,7 @@ impl AdmissionResponse { status: Some(AdmissionResponseStatus { message: Some(message), code: Some(code), + ..Default::default() }), ..Default::default() } @@ -88,6 +125,7 @@ impl AdmissionResponse { status: Some(AdmissionResponseStatus { message: Some(message.to_string()), code: None, + ..Default::default() }), }); } @@ -108,8 +146,8 @@ impl AdmissionResponse { None => None, }; - let patch_type: Option = if patch.is_some() { - Some(String::from("JSONPatch")) + let patch_type: Option = if patch.is_some() { + Some(PatchType::JSONPatch) } else { None }; @@ -118,6 +156,7 @@ impl AdmissionResponse { Some(AdmissionResponseStatus { message: pol_val_resp.message.clone(), code: pol_val_resp.code, + ..Default::default() }) } else { None @@ -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, + + /// The group attribute of the resource associated with the status StatusReason. + #[serde(skip_serializing_if = "Option::is_none")] + pub group: Option, + + /// 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, + + /// 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, + + /// 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, + + /// 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, +} + +///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, + + // A human-readable description of the cause of the error. This field may be + // presented as-is to a reader. + pub message: Option, + + // 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, +} + +/// 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; @@ -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()) diff --git a/tests/integration_test.rs b/tests/integration_test.rs index 697a1fda..9a74769f 100644 --- a/tests/integration_test.rs +++ b/tests/integration_test.rs @@ -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; @@ -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());