#[cfg(not(feature = "_docsrs"))]
pub mod binding {
#![allow(warnings, errors)]
rust2go::r2g_include_binding!();
}
#[cfg(not(feature = "_docsrs"))]
#[rust2go::r2g]
trait EnvTest {
fn create(req: Environment) -> CreateResponse;
fn exist(kubeconfig: String) -> bool;
fn destroy(kubeconfig: String) -> DestroyResponse;
}
#[derive(Default)]
#[cfg_attr(not(feature = "_docsrs"), derive(rust2go::R2G))]
struct CreateResponse {
err: Option<String>,
error_type: Option<u8>,
server: Server,
}
#[derive(Default)]
#[cfg_attr(not(feature = "_docsrs"), derive(rust2go::R2G))]
struct DestroyResponse {
err: Option<String>,
error_type: Option<u8>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[repr(u8)]
enum CreateErrorType {
SetupBinaryAssetsDirectory = 0,
DecodeCrd = 1,
StartEnvironment = 2,
BuildKubeconfig = 3,
StopEnvironment = 4,
#[default]
Generic = 5,
}
impl From<u8> for CreateErrorType {
fn from(value: u8) -> Self {
match value {
0 => Self::SetupBinaryAssetsDirectory,
1 => Self::DecodeCrd,
2 => Self::StartEnvironment,
3 => Self::BuildKubeconfig,
4 => Self::StopEnvironment,
_ => Self::default(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[repr(u8)]
enum DestroyErrorType {
EnvMissing = 0,
#[default]
StopEnvironment = 1,
}
impl From<u8> for DestroyErrorType {
fn from(value: u8) -> Self {
match value {
0 => Self::EnvMissing,
_ => Self::default(),
}
}
}
#[derive(thiserror::Error, Debug)]
pub enum EnvironmentError {
#[error("Environment create error: {0}")]
Create(String),
#[error("Setup binary assets directory error: {0}")]
SetupBinaryAssetsDirectory(String),
#[error("CRD decode error: {0}")]
DecodeCrd(String),
#[error("Start environment error: {0}")]
StartEnvironment(String),
#[error("Build kubeconfig error: {0}")]
BuildKubeconfig(String),
#[error("Stop environment error: {0}")]
StopEnvironment(String),
#[error("CRD serialization error: {0}")]
CrdSerialize(#[from] serde_json::Error),
#[error("Unsupported CRD type. Expected object, or array of those.")]
UnsupportedCrdType,
}
#[derive(Default, Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(not(feature = "_docsrs"), derive(rust2go::R2G))]
pub struct Environment {
pub crd_install_options: CRDInstallOptions,
pub binary_assets_settings: BinaryAssetsSettings,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(not(feature = "_docsrs"), derive(rust2go::R2G))]
pub struct BinaryAssetsSettings {
pub download_binary_assets: bool,
pub download_binary_assets_version: Option<String>,
pub download_binary_assets_index_url: Option<String>,
pub binary_assets_directory: Option<String>,
}
impl Default for BinaryAssetsSettings {
fn default() -> Self {
Self {
download_binary_assets: true,
download_binary_assets_version: Option::default(),
download_binary_assets_index_url: Option::default(),
binary_assets_directory: Option::default(),
}
}
}
#[derive(Default, Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(not(feature = "_docsrs"), derive(rust2go::R2G))]
pub struct CRDInstallOptions {
pub paths: Vec<String>,
pub crds: Vec<String>,
}
impl Environment {
pub async fn create(&self) -> Result<Server, EnvironmentError> {
#[cfg(feature = "_docsrs")]
let res = CreateResponse::default();
#[cfg(not(feature = "_docsrs"))]
let this = self.clone();
#[cfg(not(feature = "_docsrs"))]
let res = smol::unblock(move || EnvTestImpl::create(this)).await;
if let Some(err) = res.err {
let err = match res.error_type.map(Into::into).unwrap_or_default() {
CreateErrorType::SetupBinaryAssetsDirectory => {
EnvironmentError::SetupBinaryAssetsDirectory(err)
}
CreateErrorType::DecodeCrd => EnvironmentError::DecodeCrd(err),
CreateErrorType::StartEnvironment => EnvironmentError::StartEnvironment(err),
CreateErrorType::BuildKubeconfig => EnvironmentError::BuildKubeconfig(err),
CreateErrorType::StopEnvironment => EnvironmentError::StopEnvironment(err),
CreateErrorType::Generic => EnvironmentError::Create(err),
};
return Err(err);
}
Ok(res.server)
}
pub fn with_crds(mut self, crds: impl serde::Serialize) -> Result<Self, EnvironmentError> {
match serde_json::to_value(crds)? {
serde_json::Value::Array(crds) => {
for crd in crds {
self.crd_install_options
.crds
.push(serde_json::to_string(&crd)?);
}
}
serde_json::Value::Object(crd) => {
self.crd_install_options
.crds
.push(serde_json::to_string(&crd)?);
}
_ => return Err(EnvironmentError::UnsupportedCrdType),
}
Ok(self)
}
}
#[derive(thiserror::Error, Debug)]
pub enum ServerError {
#[error("Environment destroy error: {0}")]
Destroy(String),
#[error("Stop environment error: {0}")]
StopEnvironment(String),
#[error("Stop environment error - missing for the provided kubeconfig: {0}")]
EnvironmentMissing(String),
#[error("Deserialize kubeconfig error: {0}")]
Kubeconfig(#[from] serde_json::Error),
#[cfg(feature = "kube")]
#[error("Opening client error: {0}")]
Client(#[from] kube::Error),
#[cfg(feature = "kube")]
#[error("Attempted to use client after the environment was destroyed")]
EnvironmentDestroyed,
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
#[cfg_attr(not(feature = "_docsrs"), derive(rust2go::R2G))]
pub struct Server {
kubeconfig: String,
}
impl Server {
pub async fn destroy(&self) -> Result<(), ServerError> {
#[cfg(feature = "_docsrs")]
let res = DestroyResponse::default();
#[cfg(not(feature = "_docsrs"))]
let res = EnvTestImpl::destroy(self.kubeconfig.clone());
if let Some(err) = res.err {
let err = match res.error_type.map(Into::into).unwrap_or_default() {
DestroyErrorType::StopEnvironment => ServerError::StopEnvironment(err),
DestroyErrorType::EnvMissing => ServerError::EnvironmentMissing(err),
};
return Err(err);
}
Ok(())
}
pub fn exist(&self) -> bool {
#[cfg(feature = "_docsrs")]
return false;
#[cfg(not(feature = "_docsrs"))]
return EnvTestImpl::exist(self.kubeconfig.clone());
}
#[cfg(feature = "kube")]
#[inline]
pub fn client(&self) -> Result<kube::Client, ServerError> {
if !self.exist() {
return Err(ServerError::EnvironmentDestroyed);
}
Ok(self.kubeconfig()?.try_into()?)
}
#[cfg(feature = "kube")]
#[inline]
pub fn kubeconfig(&self) -> Result<kube::config::Kubeconfig, serde_json::Error> {
serde_json::from_str(self.as_ref())
}
}
impl AsRef<str> for Server {
fn as_ref(&self) -> &str {
&self.kubeconfig
}
}
impl Drop for Server {
fn drop(&mut self) {
let _ = smol::block_on(self.destroy());
}
}
#[cfg(test)]
mod tests {
use super::Environment;
#[cfg(feature = "kube")]
use kube::{CustomResource, CustomResourceExt};
#[cfg(feature = "kube")]
#[derive(
CustomResource, serde::Deserialize, serde::Serialize, Clone, Debug, schemars::JsonSchema,
)]
#[kube(
group = "generated.example.com",
version = "v1",
kind = "GeneratedWidget",
namespaced
)]
struct GeneratedWidgetSpec {
replicas: i32,
}
#[tokio::test]
async fn e2e() {
let env = Environment::default();
let server = env.create().await.unwrap();
server.destroy().await.unwrap();
}
#[tokio::test]
#[cfg(feature = "kube")]
async fn destroyed_environment_is_checked_by_client() {
let env = Environment::default();
let server = env.create().await.unwrap();
server.destroy().await.unwrap();
assert!(matches!(
server.client(),
Err(super::ServerError::EnvironmentDestroyed)
));
}
#[cfg(feature = "kube")]
#[tokio::test]
async fn with_crds_accepts_simple_real_crd() {
let crd = serde_json::json!({
"apiVersion": "apiextensions.k8s.io/v1",
"kind": "CustomResourceDefinition",
"metadata": { "name": "widgets.example.com" },
"spec": {
"group": "example.com",
"scope": "Namespaced",
"names": {
"plural": "widgets",
"singular": "widget",
"kind": "Widget",
"listKind": "WidgetList"
},
"versions": [{
"name": "v1",
"served": true,
"storage": true,
"schema": {
"openAPIV3Schema": {
"type": "object"
}
}
}]
}
});
let env = Environment::default().with_crds(crd.clone()).unwrap();
assert_eq!(env.crd_install_options.crds, vec![crd.to_string()]);
let server = env.create().await.unwrap();
let client = server.client().unwrap();
let groups = client.list_api_groups().await.unwrap();
groups
.groups
.iter()
.find(|g| g.name == "example.com")
.ok_or(())
.unwrap();
}
#[cfg(feature = "kube")]
#[tokio::test]
async fn with_crds_accepts_kube_generated_crd() {
let crd = GeneratedWidget::crd();
let env = Environment::default().with_crds(crd.clone()).unwrap();
assert_eq!(
env.crd_install_options.crds,
vec![serde_json::to_string(&crd).unwrap()]
);
let server = env.create().await.unwrap();
let client = server.client().unwrap();
let groups = client.list_api_groups().await.unwrap();
groups
.groups
.iter()
.find(|g| g.name == "generated.example.com")
.ok_or(())
.unwrap();
}
#[test]
fn with_crds_accepts_option_single_vec_and_slice() {
let crd_a =
serde_json::json!({"kind": "CustomResourceDefinition", "metadata": {"name": "a"}});
let crd_b =
serde_json::json!({"kind": "CustomResourceDefinition", "metadata": {"name": "b"}});
let crd_c =
serde_json::json!({"kind": "CustomResourceDefinition", "metadata": {"name": "c"}});
let env_single = Environment::default().with_crds(crd_a.clone()).unwrap();
assert_eq!(env_single.crd_install_options.crds, vec![crd_a.to_string()]);
let env_option = Environment::default()
.with_crds(Some(crd_a.clone()))
.unwrap();
assert_eq!(env_option.crd_install_options.crds, vec![crd_a.to_string()]);
let env_vec = Environment::default()
.with_crds(vec![crd_a.clone(), crd_b.clone()])
.unwrap();
assert_eq!(
env_vec.crd_install_options.crds,
vec![crd_a.to_string(), crd_b.to_string()]
);
let crds = [crd_a.clone(), crd_b.clone(), crd_c.clone()];
let env_slice = Environment::default().with_crds(&crds).unwrap();
assert_eq!(
env_slice.crd_install_options.crds,
vec![crd_a.to_string(), crd_b.to_string(), crd_c.to_string()]
);
}
#[cfg(feature = "kube")]
seq_macro::seq!(N in 1..=50 {
#[tokio::test]
async fn parallel_test_case_~N() {
let server = Environment::default().create().await.unwrap();
let client = server.client().unwrap();
client.apiserver_version().await.unwrap();
server.destroy().await.unwrap();
}
});
}