1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
use testcontainers::{
    core::{ContainerPort, WaitFor},
    Image,
};

const NAME: &str = "registry.k8s.io/kwok/cluster";
const TAG: &str = "v0.5.2-k8s.v1.29.2";
const DEFAULT_WAIT: u64 = 3000;

/// This module provides [Kwok Cluster](https://kwok.sigs.k8s.io/) (Kubernetes WithOut Kubelet).
///
/// Currently pinned to [version `v0.5.2-k8s.v1.29.2`](https://github.com/kubernetes-sigs/kwok/releases/tag/v0.5.2)
///
/// # Configuration
///
/// 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)
/// for the full list.
///
/// Testcontainers support setting environment variables with the method [`testcontainers::ImageExt::with_env_var`].
///
/// ```
/// use testcontainers_modules::{testcontainers::ImageExt, kwok::KwokCluster};
///
/// let container_request = KwokCluster::default().with_env_var("KWOK_PROMETHEUS_PORT", "9090");
/// ```
///
/// No environment variables are required.
#[derive(Debug, Default)]
pub struct KwokCluster;

impl Image for KwokCluster {
    fn name(&self) -> &str {
        NAME
    }

    fn tag(&self) -> &str {
        TAG
    }

    fn ready_conditions(&self) -> Vec<WaitFor> {
        vec![
            WaitFor::message_on_stdout("Starting to serve on [::]:8080"),
            WaitFor::millis(DEFAULT_WAIT),
        ]
    }

    fn expose_ports(&self) -> &[ContainerPort] {
        &[ContainerPort::Tcp(8080)]
    }
}

#[cfg(test)]
mod test {
    use k8s_openapi::api::core::v1::Namespace;
    use kube::{
        api::ListParams,
        client::Client,
        config::{AuthInfo, Cluster, KubeConfigOptions, Kubeconfig, NamedAuthInfo, NamedCluster},
        Api, Config,
    };
    use rustls::crypto::CryptoProvider;
    use testcontainers::core::IntoContainerPort;

    use crate::{kwok::KwokCluster, testcontainers::runners::AsyncRunner};

    const CLUSTER_NAME: &str = "kwok-kwok";
    const CONTEXT_NAME: &str = "kwok-kwok";
    const CLUSTER_USER: &str = "kwok-kwok";

    #[tokio::test]
    async fn test_kwok_image() -> Result<(), Box<dyn std::error::Error + 'static>> {
        if CryptoProvider::get_default().is_none() {
            rustls::crypto::ring::default_provider()
                .install_default()
                .expect("Error initializing rustls provider");
        }

        let node = KwokCluster.start().await?;
        let host_port = node.get_host_port_ipv4(8080.tcp()).await?;

        // Create a custom Kubeconfig
        let kubeconfig = Kubeconfig {
            clusters: vec![NamedCluster {
                name: String::from(CLUSTER_NAME),
                cluster: Some(Cluster {
                    server: Some(format!("http://localhost:{host_port}")), // your custom endpoint
                    ..Default::default()
                }),
            }],
            contexts: vec![kube::config::NamedContext {
                name: CONTEXT_NAME.to_string(),
                context: Option::from(kube::config::Context {
                    cluster: CLUSTER_NAME.to_string(),
                    user: String::from(CLUSTER_USER),
                    ..Default::default()
                }),
            }],
            auth_infos: vec![NamedAuthInfo {
                name: String::from(CLUSTER_USER),
                auth_info: Some(AuthInfo {
                    token: None,
                    ..Default::default()
                }),
            }],
            current_context: Some(CONTEXT_NAME.to_string()),
            ..Default::default()
        };
        let kubeconfigoptions = KubeConfigOptions {
            context: Some(CONTEXT_NAME.to_string()),
            cluster: Some(CLUSTER_NAME.to_string()),
            user: None,
        };

        // Convert the Kubeconfig into a Config
        let config = Config::from_custom_kubeconfig(kubeconfig, &kubeconfigoptions)
            .await
            .unwrap();

        // Create a Client from Config
        let client = Client::try_from(config).unwrap();

        let api: Api<Namespace> = Api::all(client);
        let namespaces = api.list(&ListParams::default()).await.unwrap();
        assert_eq!(namespaces.items.len(), 4);
        let namespace_names: Vec<&str> = namespaces
            .items
            .iter()
            .map(|namespace| namespace.metadata.name.as_deref().unwrap())
            .collect();
        assert_eq!(
            namespace_names,
            vec!["default", "kube-node-lease", "kube-public", "kube-system"]
        );

        Ok(())
    }
}