testcontainers_modules/kwok/
mod.rs

1use testcontainers::{
2    core::{ContainerPort, WaitFor},
3    Image,
4};
5
6const NAME: &str = "registry.k8s.io/kwok/cluster";
7const TAG: &str = "v0.5.2-k8s.v1.29.2";
8const DEFAULT_WAIT: u64 = 3000;
9/// Port that the [`Kwok Cluster`] container has internally
10/// Can be rebound externally via [`testcontainers::core::ImageExt::with_mapped_port`]
11///
12/// [`Kwok Cluster`]: https://kwok.sigs.k8s.io/
13pub const KWOK_CLUSTER_PORT: ContainerPort = ContainerPort::Tcp(8080);
14
15/// This module provides [`Kwok Cluster`] (Kubernetes WithOut Kubelet).
16///
17/// Currently pinned to [version `v0.5.2-k8s.v1.29.2`](https://github.com/kubernetes-sigs/kwok/releases/tag/v0.5.2)
18///
19/// # Configuration
20///
21/// For configuration, Kwok Cluster uses environment variables. You can go [here](https://kwok.sigs.k8s.io/docs/user/configuration/#a-note-on-cli-flags-environment-variables-and-configuration-files)
22/// for the full list.
23///
24/// Testcontainers support setting environment variables with the method [`testcontainers::ImageExt::with_env_var`].
25///
26/// ```
27/// use testcontainers_modules::{kwok::KwokCluster, testcontainers::ImageExt};
28///
29/// let container_request = KwokCluster::default().with_env_var("KWOK_PROMETHEUS_PORT", "9090");
30/// ```
31///
32/// No environment variables are required.
33///
34/// [`Kwok Cluster`]: https://kwok.sigs.k8s.io/
35#[derive(Debug, Default, Clone)]
36pub struct KwokCluster {
37    /// (remove if there is another variable)
38    /// Field is included to prevent this struct to be a unit struct.
39    /// This allows extending functionality (and thus further variables) without breaking changes
40    _priv: (),
41}
42
43impl Image for KwokCluster {
44    fn name(&self) -> &str {
45        NAME
46    }
47
48    fn tag(&self) -> &str {
49        TAG
50    }
51
52    fn ready_conditions(&self) -> Vec<WaitFor> {
53        vec![
54            WaitFor::message_on_stdout("Starting to serve on [::]:8080"),
55            WaitFor::millis(DEFAULT_WAIT),
56        ]
57    }
58
59    fn expose_ports(&self) -> &[ContainerPort] {
60        &[KWOK_CLUSTER_PORT]
61    }
62}
63
64#[cfg(test)]
65mod test {
66    use k8s_openapi::api::core::v1::Namespace;
67    use kube::{
68        api::ListParams,
69        client::Client,
70        config::{AuthInfo, Cluster, KubeConfigOptions, Kubeconfig, NamedAuthInfo, NamedCluster},
71        Api, Config,
72    };
73    use rustls::crypto::CryptoProvider;
74    use testcontainers::core::IntoContainerPort;
75
76    use crate::{kwok::KwokCluster, testcontainers::runners::AsyncRunner};
77
78    const CLUSTER_NAME: &str = "kwok-kwok";
79    const CONTEXT_NAME: &str = "kwok-kwok";
80    const CLUSTER_USER: &str = "kwok-kwok";
81
82    #[tokio::test]
83    async fn test_kwok_image() -> Result<(), Box<dyn std::error::Error + 'static>> {
84        if CryptoProvider::get_default().is_none() {
85            rustls::crypto::ring::default_provider()
86                .install_default()
87                .expect("Error initializing rustls provider");
88        }
89
90        let node = KwokCluster::default().start().await?;
91        let host_port = node.get_host_port_ipv4(8080.tcp()).await?;
92
93        // Create a custom Kubeconfig
94        let kubeconfig = Kubeconfig {
95            clusters: vec![NamedCluster {
96                name: String::from(CLUSTER_NAME),
97                cluster: Some(Cluster {
98                    server: Some(format!("http://localhost:{host_port}")), // your custom endpoint
99                    ..Default::default()
100                }),
101            }],
102            contexts: vec![kube::config::NamedContext {
103                name: CONTEXT_NAME.to_string(),
104                context: Option::from(kube::config::Context {
105                    cluster: CLUSTER_NAME.to_string(),
106                    user: Some(String::from(CLUSTER_USER)),
107                    ..Default::default()
108                }),
109            }],
110            auth_infos: vec![NamedAuthInfo {
111                name: String::from(CLUSTER_USER),
112                auth_info: Some(AuthInfo {
113                    token: None,
114                    ..Default::default()
115                }),
116            }],
117            current_context: Some(CONTEXT_NAME.to_string()),
118            ..Default::default()
119        };
120        let kubeconfigoptions = KubeConfigOptions {
121            context: Some(CONTEXT_NAME.to_string()),
122            cluster: Some(CLUSTER_NAME.to_string()),
123            user: None,
124        };
125
126        // Convert the Kubeconfig into a Config
127        let config = Config::from_custom_kubeconfig(kubeconfig, &kubeconfigoptions)
128            .await
129            .unwrap();
130
131        // Create a Client from Config
132        let client = Client::try_from(config).unwrap();
133
134        let api: Api<Namespace> = Api::all(client);
135        let namespaces = api.list(&ListParams::default()).await.unwrap();
136        assert_eq!(namespaces.items.len(), 4);
137        let namespace_names: Vec<&str> = namespaces
138            .items
139            .iter()
140            .map(|namespace| namespace.metadata.name.as_deref().unwrap())
141            .collect();
142        assert_eq!(
143            namespace_names,
144            vec!["default", "kube-node-lease", "kube-public", "kube-system"]
145        );
146
147        Ok(())
148    }
149}