gateway_api/
lib.rs

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