1pub mod duration;
2pub use duration::Duration;
3pub mod apis;
4
5#[cfg(feature = "standard")]
6pub use apis::standard::*;
7
8#[cfg(feature = "experimental")]
9pub use apis::experimental;
10
11#[cfg(test)]
12mod tests {
13 use std::process::Command;
14
15 use anyhow::Error;
16 use hyper_util::client::legacy::Client as HTTPClient;
17 use hyper_util::rt::TokioExecutor;
18 use k8s_openapi::apimachinery::pkg::apis::meta::v1::{Condition, Time};
19 use k8s_openapi::chrono::Utc;
20 use kube::Client as KubeClient;
21 use kube::api::{Patch, PatchParams, PostParams};
22 use kube::config::{KubeConfigOptions, Kubeconfig};
23 use kube::core::ObjectMeta;
24 use kube::{Api, Config, CustomResourceExt, client::ConfigExt};
25 use serde_json::json;
26 use tower::BoxError;
27 use tower::ServiceBuilder;
28 use uuid::Uuid;
29
30 use crate::{
31 apis::standard::common::GatewayAddress,
32 apis::standard::constants::{
33 GatewayConditionReason, GatewayConditionType, ListenerConditionReason,
34 ListenerConditionType,
35 },
36 apis::standard::gatewayclasses::{GatewayClass, GatewayClassSpec},
37 apis::standard::gateways::{
38 Gateway, GatewaySpec, GatewayStatus, GatewayStatusListeners,
39 },
40 };
41
42 #[ignore]
47 #[tokio::test]
48 async fn deploy_gateway() -> Result<(), Error> {
49 let (client, cluster) = get_client().await?;
50 let info = client.apiserver_version().await?;
51
52 println!(
53 "kind cluster {} is running, server version: {}",
54 cluster.name, info.git_version
55 );
56
57 let mut gwc = GatewayClass {
58 metadata: ObjectMeta::default(),
59 spec: GatewayClassSpec {
60 controller_name: "test-controller".to_string(),
61 description: None,
62 parameters_ref: None,
63 },
64 status: None,
65 };
66 gwc.metadata.name = Some("test-gateway-class".to_string());
67 gwc = Api::all(client.clone())
68 .create(&PostParams::default(), &gwc)
69 .await?;
70
71 assert!(gwc.metadata.name.is_some());
72 assert!(gwc.metadata.uid.is_some());
73
74 let mut gw = Gateway {
75 metadata: ObjectMeta::default(),
76 spec: GatewaySpec {
77 gateway_class_name: gwc
78 .metadata
79 .name
80 .ok_or(Error::msg("could not find GatewayClass name"))?,
81 ..Default::default()
82 },
83 status: None,
84 };
85 gw.metadata.name = Some("test-gateway".to_string());
86 gw = Api::default_namespaced(client.clone())
87 .create(&PostParams::default(), &gw)
88 .await?;
89
90 assert!(gw.metadata.name.is_some());
91 assert!(gw.metadata.uid.is_some());
92
93 let gw_status = GatewayStatus {
94 addresses: Some(vec![GatewayAddress::default()]),
95 listeners: Some(vec![GatewayStatusListeners {
96 name: "tcp".into(),
97 attached_routes: 0,
98 supported_kinds: vec![],
99 conditions: vec![Condition {
100 last_transition_time: Time(Utc::now()),
101 message: "testing gateway".to_string(),
102 observed_generation: Some(1),
103 reason: ListenerConditionReason::Programmed.to_string(),
104 status: "True".to_string(),
105 type_: ListenerConditionType::Programmed.to_string(),
106 }],
107 }]),
108 conditions: Some(vec![Condition {
109 last_transition_time: Time(Utc::now()),
110 message: "testing gateway".to_string(),
111 observed_generation: Some(1),
112 reason: GatewayConditionReason::Programmed.to_string(),
113 status: "True".to_string(),
114 type_: GatewayConditionType::Programmed.to_string(),
115 }]),
116 };
117
118 gw = Api::default_namespaced(client)
119 .patch_status(
120 gw.metadata.name.clone().unwrap().as_str(),
121 &PatchParams::default(),
122 &Patch::Merge(json!({
123 "status": Some(gw_status)
124 })),
125 )
126 .await?;
127
128 assert!(gw.status.is_some());
129 assert!(gw.status.clone().unwrap().addresses.is_some());
130 assert!(gw.status.clone().unwrap().listeners.is_some());
131 assert!(gw.status.clone().unwrap().conditions.is_some());
132
133 Ok(())
134 }
135
136 struct Cluster {
141 name: String,
142 }
143
144 impl Drop for Cluster {
145 fn drop(&mut self) {
146 if let Err(err) = delete_kind_cluster(&self.name) {
147 panic!("failed to cleanup kind cluster {}: {}", self.name, err)
148 }
149 }
150 }
151
152 async fn get_client() -> Result<(kube::Client, Cluster), Error> {
153 let cluster = create_kind_cluster()?;
154 let kubeconfig_yaml = get_kind_kubeconfig(&cluster.name)?;
155 let kubeconfig = Kubeconfig::from_yaml(&kubeconfig_yaml)?;
156 let config =
157 Config::from_custom_kubeconfig(kubeconfig, &KubeConfigOptions::default()).await?;
158
159 let https = config.rustls_https_connector()?;
160 let http_client = HTTPClient::builder(TokioExecutor::new()).build(https);
161 let service = ServiceBuilder::new()
162 .layer(config.base_uri_layer())
163 .option_layer(config.auth_layer()?)
164 .map_err(BoxError::from)
165 .service(http_client);
166
167 let client = KubeClient::new(service, config.default_namespace);
168
169 deploy_crds(client.clone()).await?;
170
171 Ok((client, cluster))
172 }
173
174 async fn deploy_crds(client: kube::Client) -> Result<(), Error> {
175 let mut gwc_crd = GatewayClass::crd();
176 gwc_crd.metadata.annotations = Some(std::collections::BTreeMap::from([(
177 "api-approved.kubernetes.io".to_string(),
178 "https://github.com/kubernetes/enhancements/pull/1111".to_string(),
179 )]));
180
181 Api::all(client.clone())
182 .create(&PostParams::default(), &gwc_crd)
183 .await?;
184
185 let mut gw_crd = Gateway::crd();
186 gw_crd.metadata.annotations = Some(std::collections::BTreeMap::from([(
187 "api-approved.kubernetes.io".to_string(),
188 "https://github.com/kubernetes/enhancements/pull/1111".to_string(),
189 )]));
190
191 Api::all(client.clone())
192 .create(&PostParams::default(), &gw_crd)
193 .await?;
194
195 Ok(())
196 }
197
198 fn create_kind_cluster() -> Result<Cluster, Error> {
199 let cluster_name = Uuid::new_v4().to_string();
200
201 let output = Command::new("kind")
202 .arg("create")
203 .arg("cluster")
204 .arg("--name")
205 .arg(&cluster_name)
206 .output()?;
207
208 if !output.status.success() {
209 return Err(Error::msg(String::from_utf8(output.stderr)?));
210 }
211
212 Ok(Cluster { name: cluster_name })
213 }
214
215 fn delete_kind_cluster(cluster_name: &str) -> Result<(), Error> {
216 let output = Command::new("kind")
217 .arg("delete")
218 .arg("cluster")
219 .arg("--name")
220 .arg(cluster_name)
221 .output()?;
222
223 if !output.status.success() {
224 return Err(Error::msg(String::from_utf8(output.stderr)?));
225 }
226
227 Ok(())
228 }
229
230 fn get_kind_kubeconfig(cluster_name: &str) -> Result<String, Error> {
231 let output = Command::new("kind")
232 .arg("get")
233 .arg("kubeconfig")
234 .arg("--name")
235 .arg(cluster_name)
236 .output()?;
237
238 if !output.status.success() {
239 return Err(Error::msg(String::from_utf8(output.stderr)?));
240 }
241
242 Ok(String::from_utf8(output.stdout)?)
243 }
244}