1use std::collections::BTreeMap;
25use std::sync::{Arc, RwLock};
26
27use futures_util::StreamExt;
28use k8s_openapi::api::core::v1::{ConfigMap, Secret};
29use kube::api::Api;
30use kube::runtime::watcher;
31
32use super::{Error, Result};
33
34type Listener = Arc<dyn Fn(&BTreeMap<String, String>) + Send + Sync>;
35
36struct Shared {
37 data: RwLock<BTreeMap<String, String>>,
38 listeners: RwLock<Vec<Listener>>,
39 present: std::sync::atomic::AtomicBool,
40}
41
42#[derive(Clone)]
45pub struct ConfigWatch {
46 shared: Arc<Shared>,
47 _task: Arc<AbortOnDrop>,
48}
49
50struct AbortOnDrop(tokio::task::JoinHandle<()>);
51
52impl Drop for AbortOnDrop {
53 fn drop(&mut self) {
54 self.0.abort();
55 }
56}
57
58pub fn config_map_data(cm: &ConfigMap) -> BTreeMap<String, String> {
60 let mut out = cm.data.clone().unwrap_or_default();
61 for (k, v) in cm.binary_data.clone().unwrap_or_default() {
62 if let Ok(text) = String::from_utf8(v.0) {
63 out.insert(k, text);
64 }
65 }
66 out
67}
68
69pub fn secret_data(secret: &Secret) -> BTreeMap<String, String> {
71 let mut out = BTreeMap::new();
72 for (k, v) in secret.data.clone().unwrap_or_default() {
73 if let Ok(text) = String::from_utf8(v.0) {
74 out.insert(k, text);
75 }
76 }
77 out.extend(secret.string_data.clone().unwrap_or_default());
78 out
79}
80
81impl ConfigWatch {
82 pub async fn config_map(client: kube::Client, namespace: &str, name: &str) -> Result<Self> {
85 Self::start(Api::<ConfigMap>::namespaced(client, namespace), name, config_map_data).await
86 }
87
88 pub async fn secret(client: kube::Client, namespace: &str, name: &str) -> Result<Self> {
90 Self::start(Api::<Secret>::namespaced(client, namespace), name, secret_data).await
91 }
92
93 async fn start<K>(api: Api<K>, name: &str, read: fn(&K) -> BTreeMap<String, String>) -> Result<Self>
94 where
95 K: kube::Resource + Clone + std::fmt::Debug + serde::de::DeserializeOwned + Send + Sync + 'static,
96 {
97 let initial = api.get(name).await?;
98 let shared = Arc::new(Shared {
99 data: RwLock::new(read(&initial)),
100 listeners: RwLock::new(Vec::new()),
101 present: std::sync::atomic::AtomicBool::new(true),
102 });
103 let config = watcher::Config::default().fields(&format!("metadata.name={name}"));
104 let task_shared = Arc::clone(&shared);
105 let task = tokio::spawn(async move {
106 let mut stream = watcher::watcher(api, config).boxed();
107 while let Some(event) = stream.next().await {
108 let next = match event {
109 Ok(watcher::Event::Apply(obj)) | Ok(watcher::Event::InitApply(obj)) => Some(read(&obj)),
110 Ok(watcher::Event::Delete(_)) => {
111 task_shared.present.store(false, std::sync::atomic::Ordering::SeqCst);
112 None
113 }
114 Ok(_) => None,
115 Err(e) => {
116 eprintln!("[k8s] config watch: {e}");
117 tokio::time::sleep(std::time::Duration::from_secs(1)).await;
118 None
119 }
120 };
121 if let Some(data) = next {
122 task_shared.present.store(true, std::sync::atomic::Ordering::SeqCst);
123 let changed = {
124 let mut current = task_shared.data.write().unwrap_or_else(|e| e.into_inner());
125 let changed = *current != data;
126 *current = data.clone();
127 changed
128 };
129 if changed {
130 let listeners = task_shared.listeners.read().unwrap_or_else(|e| e.into_inner()).clone();
131 for listener in listeners {
132 listener(&data);
133 }
134 }
135 }
136 }
137 });
138 Ok(ConfigWatch { shared, _task: Arc::new(AbortOnDrop(task)) })
139 }
140
141 pub fn get(&self, key: &str) -> Option<String> {
143 self.shared.data.read().unwrap_or_else(|e| e.into_inner()).get(key).cloned()
144 }
145
146 pub fn snapshot(&self) -> BTreeMap<String, String> {
148 self.shared.data.read().unwrap_or_else(|e| e.into_inner()).clone()
149 }
150
151 pub fn exists(&self) -> bool {
154 self.shared.present.load(std::sync::atomic::Ordering::SeqCst)
155 }
156
157 pub fn on_change(&self, f: impl Fn(&BTreeMap<String, String>) + Send + Sync + 'static) {
159 self.shared.listeners.write().unwrap_or_else(|e| e.into_inner()).push(Arc::new(f));
160 }
161
162 pub fn apply_to_env(&self, prefix: &str) {
167 apply(&self.snapshot(), prefix);
168 let prefix = prefix.to_string();
169 self.on_change(move |data| apply(data, &prefix));
170 }
171}
172
173fn apply(data: &BTreeMap<String, String>, prefix: &str) {
174 let mut any = false;
175 for (k, v) in data.iter().filter(|(k, _)| k.starts_with(prefix)) {
176 unsafe { std::env::set_var(k, v) };
179 any = true;
180 }
181 if any {
182 rws_server::server_config::invalidate();
183 }
184}
185
186#[derive(Debug, Clone, PartialEq, Eq)]
188pub struct SecretRef {
189 pub namespace: String,
190 pub name: String,
191 pub key: String,
192}
193
194impl SecretRef {
195 pub fn parse(reference: &str) -> Result<SecretRef> {
198 let rest = reference.strip_prefix("k8s://").ok_or_else(|| Error::Other(format!("{reference}: not a k8s:// reference")))?;
199 let (path, key) = rest.split_once('#').ok_or_else(|| Error::Other(format!("{reference}: no #key")))?;
200 let (namespace, name) = match path.split_once('/') {
201 Some((ns, name)) => (ns.to_string(), name.to_string()),
202 None => (super::client::current_namespace(), path.to_string()),
203 };
204 if namespace.is_empty() || name.is_empty() || key.is_empty() || name.contains('/') {
205 return Err(Error::Other(format!("{reference}: expected k8s://namespace/name#key")));
206 }
207 Ok(SecretRef { namespace, name, key: key.to_string() })
208 }
209}
210
211pub async fn resolve(client: &kube::Client, reference: &str) -> Result<String> {
214 let r = SecretRef::parse(reference)?;
215 let secret = Api::<Secret>::namespaced(client.clone(), &r.namespace).get(&r.name).await?;
216 secret_data(&secret)
217 .remove(&r.key)
218 .ok_or_else(|| Error::Other(format!("secret {}/{} has no key {}", r.namespace, r.name, r.key)))
219}
220
221#[cfg(test)]
222mod tests;