gateway_api/
lib.rs

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    // -------------------------------------------------------------------------
43    // Tests
44    // -------------------------------------------------------------------------
45
46    #[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    // -------------------------------------------------------------------------
137    // Test Utilities
138    // -------------------------------------------------------------------------
139
140    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}