gateway_api/
lib.rs

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