fakecloud_lambda/runtime/k8s/
mod.rs1pub mod spec;
17
18use std::time::Duration;
19
20use async_trait::async_trait;
21use fakecloud_k8s::{K8sClient, K8sEnv, K8sEnvError};
22
23use super::backend::{BackendHandle, LambdaBackend, RuntimeError, WarmInstance};
24use crate::state::LambdaFunction;
25use spec::{build_pod_spec, pod_name_for, PodSpecContext};
26
27const SERVICE: &str = "lambda";
30
31#[derive(Debug, thiserror::Error)]
34pub enum K8sBackendError {
35 #[error(transparent)]
36 Env(#[from] K8sEnvError),
37 #[error("failed to connect to the Kubernetes cluster: {0}")]
38 Connect(String),
39}
40
41pub struct K8sBackend {
43 client: K8sClient,
44 self_url: String,
48 self_host: String,
51 ecr_host: String,
54 ecr_port: u16,
55 internal_token: String,
58 pull_secret: Option<String>,
61}
62
63impl K8sBackend {
64 pub async fn from_env(
69 default_ecr_port: u16,
70 internal_token: String,
71 ) -> Result<Self, K8sBackendError> {
72 let env = K8sEnv::from_env(default_ecr_port)?;
73 let client = K8sClient::connect(env.namespace.clone())
74 .await
75 .map_err(|e| K8sBackendError::Connect(e.to_string()))?;
76
77 tracing::info!(
78 namespace = %env.namespace,
79 self_url = %env.self_url,
80 ecr = %format!("{}:{}", env.ecr_host, env.ecr_port),
81 "K8s Lambda backend initialized"
82 );
83
84 Ok(Self {
85 client,
86 self_url: env.self_url,
87 self_host: env.self_host,
88 ecr_host: env.ecr_host,
89 ecr_port: env.ecr_port,
90 internal_token,
91 pull_secret: env.pull_secret,
92 })
93 }
94}
95
96fn account_id_from_arn(arn: &str) -> &str {
99 arn.split(':').nth(4).unwrap_or("000000000000")
100}
101
102#[async_trait]
103impl LambdaBackend for K8sBackend {
104 fn name(&self) -> &str {
105 "kubernetes"
106 }
107
108 async fn launch(
109 &self,
110 func: &LambdaFunction,
111 _code_zip: Option<&[u8]>,
112 _layers: &[Vec<u8>],
113 deploy_id: &str,
114 ) -> Result<WarmInstance, RuntimeError> {
115 let account_id = account_id_from_arn(&func.function_arn);
116 let ctx = PodSpecContext {
117 instance_id: self.client.instance_id(),
118 namespace: self.client.namespace(),
119 self_url: &self.self_url,
120 self_host: &self.self_host,
121 ecr_host: &self.ecr_host,
122 ecr_port: self.ecr_port,
123 internal_token: &self.internal_token,
124 account_id,
125 pull_secret: self.pull_secret.as_deref(),
126 };
127 let pod =
128 build_pod_spec(func, deploy_id, &ctx).map_err(RuntimeError::ContainerStartFailed)?;
129 let pod_name = pod
130 .metadata
131 .name
132 .clone()
133 .unwrap_or_else(|| pod_name_for(&func.function_name, deploy_id));
134
135 self.client
136 .create_pod(&pod)
137 .await
138 .map_err(|e| RuntimeError::ContainerStartFailed(format!("k8s create pod: {e}")))?;
139
140 let pod_ip = match self
143 .client
144 .wait_for_pod_ip(&pod_name, Duration::from_secs(60))
145 .await
146 {
147 Ok(ip) => ip,
148 Err(e) => {
149 self.client.delete_pod(&pod_name).await;
150 return Err(RuntimeError::ContainerStartFailed(e.to_string()));
151 }
152 };
153 if let Err(e) = K8sClient::wait_for_tcp(&pod_ip, 8080, Duration::from_secs(10)).await {
156 self.client.delete_pod(&pod_name).await;
157 return Err(RuntimeError::ContainerStartFailed(format!(
158 "RIE on {pod_ip}:8080 not ready: {e}"
159 )));
160 }
161
162 tracing::info!(
163 function = %func.function_name,
164 pod = %pod_name,
165 namespace = %self.client.namespace(),
166 pod_ip = %pod_ip,
167 "Lambda Pod started"
168 );
169
170 Ok(WarmInstance {
171 endpoint: format!("{pod_ip}:8080"),
172 handle: BackendHandle::Pod {
173 namespace: self.client.namespace().to_string(),
174 name: pod_name,
175 },
176 })
177 }
178
179 async fn terminate(&self, handle: &BackendHandle) {
180 match handle {
181 BackendHandle::Pod { name, .. } => self.client.delete_pod(name).await,
182 BackendHandle::Container { .. } => {}
184 }
185 }
186
187 async fn reap_stale(&self) {
192 self.client.reap_stale(SERVICE).await;
193 }
194}
195
196#[cfg(test)]
197mod tests {
198 use super::account_id_from_arn;
199
200 #[test]
201 fn account_id_from_simple_arn() {
202 assert_eq!(
203 account_id_from_arn("arn:aws:lambda:us-east-1:123456789012:function:my-fn"),
204 "123456789012"
205 );
206 }
207
208 #[test]
209 fn account_id_from_qualified_arn() {
210 assert_eq!(
211 account_id_from_arn("arn:aws:lambda:us-east-1:000000000000:function:my-fn:PROD"),
212 "000000000000"
213 );
214 }
215
216 #[test]
217 fn account_id_falls_back_for_malformed_arn() {
218 assert_eq!(account_id_from_arn("not-an-arn"), "000000000000");
219 }
220}